手记

跟我一步一步实现 Flutter 视频播放插件 (一)

当团队准备着手做 APP 时,我们把目标对准了 Flutter,尤其近期 Flutter 的使用热度一直不断攀升。由于第一次使用 Flutter,就想通过自己的实践去提升自己的能力。

在做 APP 时,我们用到了视频播放器,当前使用官方提供的插件「video_player」https://github.com/flutter/plugins/tree/master/packages/video_player,可能该插件在国外没什么问题,但国内很多视频播放器做的很精良,自定义功能很齐全。

举一个例子:国内的 APP 全屏播放视频时,几乎都是横向全屏的,但官方提供的插件在 iOS 端是竖向直播的,效果很不好。

因此萌生了自己想做一个视频播放插件:

要求

  1. Android 和 iOS 端都是使用原生开发,体验效果好;

  2. 尽可能使用 GitHub Star 靠前的第三方开源插件,减轻自己的开发工作量;

根据以上的「2」要求,我主要找到了 lipangit/JiaoZiVideoPlayernewyjp/JPVideoPlayer

好了,所有铺垫都做好了,我们开始一步步实现插件开发吧~

1. 创建插件


flutter create --org com.***.test --template=plugin bms_video_player

2. 创建关联类

lib/bms_video_player.dart 文件中创建 BmsVideoPlayerController 类,用于和原生代码关联:


class BmsVideoPlayerController {

  MethodChannel _channel;

  BmsVideoPlayerController.init(int id) {

    _channel =  new MethodChannel('bms_video_player_$id');

  }

  Future<void> loadUrl(String url) async {

    assert(url != null);

    return _channel.invokeMethod('loadUrl', url);

  }

}

这里存在的 MethodChannel 有待于下一次好好研究研究。

3. 创建 Callback


typedef void BmsVideoPlayerCreatedCallback(BmsVideoPlayerController controller);

4. 创建 Widget 布局

创建 Widget,用于添加原生布局:


class BmsVideoPlayer extends StatefulWidget {

  final BmsVideoPlayerCreatedCallback onCreated;

  final x;

  final y;

  final width;

  final height;

  BmsVideoPlayer({

    Key key,

    @required this.onCreated,

    @required this.x,

    @required this.y,

    @required this.width,

    @required this.height,

  });

  @override

  State<StatefulWidget> createState() => _VideoPlayerState();

}

class _VideoPlayerState extends State<BmsVideoPlayer> {

  @override

  void initState() {

    super.initState();

  }

  @override

  Widget build(BuildContext context) {

    return GestureDetector(

      behavior: HitTestBehavior.opaque,

      child: nativeView(),

      onHorizontalDragStart: (DragStartDetails details) {

        print("onHorizontalDragStart: ${details.globalPosition}");

        // if (!controller.value.initialized) {

        //   return;

        // }

        // _controllerWasPlaying = controller.value.isPlaying;

        // if (_controllerWasPlaying) {

        //   controller.pause();

        // }

      },

      onHorizontalDragUpdate: (DragUpdateDetails details) {

        print("onHorizontalDragUpdate: ${details.globalPosition}");

        print(details.globalPosition);

        // if (!controller.value.initialized) {

        //   return;

        // }

        // seekToRelativePosition(details.globalPosition);

      },

      onHorizontalDragEnd: (DragEndDetails details) {

        print("onHorizontalDragEnd");

        // if (_controllerWasPlaying) {

        //   controller.play();

        // }

      },

      onTapDown: (TapDownDetails details) {

        print("onTapDown: ${details.globalPosition}");

      },

    );

  }

  nativeView() {

    if (defaultTargetPlatform == TargetPlatform.android) {

      return AndroidView(

        viewType: 'plugins.bms_video_player/view',

        onPlatformViewCreated: onPlatformViewCreated,

        creationParams: <String,dynamic>{

          "x": widget.x,

          "y": widget.y,

          "width": widget.width,

          "height": widget.height,

        },

        creationParamsCodec: const StandardMessageCodec(),

      );

    } else {

      return UiKitView(

        viewType: 'plugins.bms_video_player/view',

        onPlatformViewCreated: onPlatformViewCreated,

        creationParams: <String,dynamic>{

          "x": widget.x,

          "y": widget.y,

          "width": widget.width,

          "height": widget.height,

        },

        creationParamsCodec: const StandardMessageCodec(),

      );

    }

  }

  Future<void> onPlatformViewCreated(id) async {

    if (widget.onCreated == null) {

      return;

      }

    widget.onCreated(new BmsVideoPlayerController.init(id));

  }

}

这里的 AndroidViewUiKitView 字如其意,不同的系统使用不同的 widget。

其中,AndroidViewUiKitView 都自带几个参数,如:

  1. viewType:用于区分不同的插件名称和来源;

  2. onPlatformViewCreated:用于在 widget 创建后,调用其函数 (onPlatformViewCreated);

  3. creationParams:用于将参数传递给原生控件。

下面开始,根据 iOS 和 Android 分别注册插件和实现功能,首先是 Android。

5.1 注册 ViewFactory

BmsVideoPlayerPlugin 类中注册 ViewFactorynew VideoViewFactory(registrar),并命名为 「plugins.bms_video_player/view」:


public static void registerWith(Registrar registrar) {

    registrar.platformViewRegistry()

             .registerViewFactory("plugins.bms_video_player/view", new VideoViewFactory(registrar));

  }

5.2 创建 VideoViewFactory

VideoViewFactory 类需要集成类 PlatformViewFactory,实现函数:create(Context context, int viewId, Object args)


public class VideoViewFactory extends PlatformViewFactory {

    private final Registrar registrar;

    public VideoViewFactory(Registrar registrar) {

        super(StandardMessageCodec.INSTANCE);

        this.registrar = registrar;

    }

    @Override

    public PlatformView create(Context context, int viewId, Object args) {

        return new VideoView(context, viewId, args, this.registrar);

    }

}

开始我们的正餐了,创建实现类 VideoView

5.3 VideoView


public class VideoView implements PlatformView, MethodCallHandler  {

    private final JzvdStd jzvdStd;

    private final MethodChannel methodChannel;

    private final Registrar registrar;

    VideoView(Context context, int viewId, Object args, Registrar registrar) {

        this.registrar = registrar;

        this.jzvdStd = getJzvStd(registrar, args);

        this.methodChannel = new MethodChannel(registrar.messenger(), "bms_video_player_" + viewId);

        this.methodChannel.setMethodCallHandler(this);

    }

    @Override

    public View getView() {

        return jzvdStd;

    }

    @Override

    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {

        switch (methodCall.method) {

            case "loadUrl":

                String url = methodCall.arguments.toString();

                jzvdStd.setUp(url, "", Jzvd.SCREEN_NORMAL);

                break;

            default:

                result.notImplemented();

        }

    }

    @Override

    public void dispose() {}

    private JzvdStd getJzvStd(Registrar registrar, Object args) {

        JzvdStd view = (JzvdStd) LayoutInflater.from(registrar.activity()).inflate(R.layout.jz_video, null);

        return view;

    }

}

直接分析代码:

  1. 实现接口:PlatformView 和 MethodCallHandler,第一个接口「PlatformView」,用于 return 原生 View,也就是我们使用的第三方插件:JzvdStd。第二个接口「MethodCallHandler」,用于处理从 Dart 发过来的请求函数,如本文创建的函数:loadUrl

  2. 这里 returnJzvdStd,使用 xml:

<?xml version="1.0" encoding="utf-8"?>

<cn.jzvd.JzvdStd

    xmlns:android="http://schemas.android.com/apk/res/android"

    android:id="@+id/jz_video"

    android:layout_width="match_parent"

    android:layout_height="match_parent" />

5.4 引入第三方插件

当然,我们需要在 build.gradle 最后加入插件:


dependencies {

    implementation 'cn.jzvd:jiaozivideoplayer:7.0_preview'

}

至此,我们的 Android 端就算完成了,接下来看看 iOS 端。

6.1 注册 ViewFactory

同样的,在类 BmsVideoPlayerPlugin 中注册 VideoViewFactory


+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {

  VideoViewFactory* factory =

      [[VideoViewFactory alloc] initWithMessenger:registrar.messenger];

  [registrar registerViewFactory:factory withId:@"plugins.bms_video_player/view"];

}

6.2 创建 VideoViewFactory


#import "VideoViewFactory.h"

#import "BMSVideoPlayerViewController.h"

@implementation VideoViewFactory {

  NSObject<FlutterBinaryMessenger>* _messenger;

}

- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {

  self = [super init];

  if (self) {

    _messenger = messenger;

  }

  return self;

}

- (NSObject<FlutterMessageCodec>*)createArgsCodec {

  return [FlutterStandardMessageCodec sharedInstance];

}

- (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame

                                            viewIdentifier:(int64_t)viewId

                                                 arguments:(id _Nullable)args {

    BMSVideoPlayerViewController* viewController =

      [[BMSVideoPlayerViewController alloc] initWithWithFrame:frame

                                       viewIdentifier:viewId

                                            arguments:args

                                      binaryMessenger:_messenger];

    return viewController;

}

@end

代码还是很简单,重点往下看 BMSVideoPlayerViewController

6.3 BMSVideoPlayerViewController


#import "BMSVideoPlayerViewController.h"

#import <JPVideoPlayer/JPVideoPlayerKit.h>

@interface BMSVideoPlayerViewController ()<JPVideoPlayerDelegate>

@end

@implementation BMSVideoPlayerViewController {

    UIView * _videoView;

    int64_t _viewId;

    FlutterMethodChannel* _channel;

}

#pragma mark - life cycle

- (instancetype)initWithWithFrame:(CGRect)frame

                   viewIdentifier:(int64_t)viewId

                        arguments:(id _Nullable)args

                  binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {

  if ([super init]) {

    _viewId = viewId;

    _videoView = [UIView new];

    _videoView.backgroundColor = [UIColor greenColor];

    NSDictionary *dic = args;

    CGFloat x = [dic[@"x"] floatValue];

    CGFloat y = [dic[@"y"] floatValue];

    CGFloat width = [dic[@"width"] floatValue];

    CGFloat height = [dic[@"height"] floatValue];

    _videoView.frame = CGRectMake(x, y, width, height);

    _videoView.jp_videoPlayerDelegate = self;

    NSString* channelName = [NSString stringWithFormat:@"bms_video_player_%lld", viewId];

    _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];

    __weak __typeof__(self) weakSelf = self;

    [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {

      [weakSelf onMethodCall:call result:result];

    }];

  }

  return self;

}

- (nonnull UIView *)view {

    return _videoView;

}

- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {

  if ([[call method] isEqualToString:@"loadUrl"]) {

    [self onLoadUrl:call result:result];

  } else {

    result(FlutterMethodNotImplemented);

  }

}

- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result {

  NSString* url = [call arguments];

  if (![self loadUrl:url]) {

    result([FlutterError errorWithCode:@"loadUrl_failed"

                               message:@"Failed parsing the URL"

                               details:[NSString stringWithFormat:@"URL was: '%@'", url]]);

  } else {

    result(nil);

  }

}

- (bool)loadUrl:(NSString*)url {

  NSURL* nsUrl = [NSURL URLWithString:url];

  if (!nsUrl) {

    return false;

  }

  [_videoView jp_playVideoWithURL:nsUrl

                           bufferingIndicator:nil

                                  controlView:nil

                                 progressView:nil

                                configuration:^(UIView *view, JPVideoPlayerModel *playerModel) {

                                    // self.muteSwitch.on = ![self.videoContainer jp_muted];

                                }];

  return true;

}

#pragma mark - JPVideoPlayerDelegate

- (BOOL)shouldAutoReplayForURL:(nonnull NSURL *)videoURL {

    return true;

}

@end

其实,代码实现都很简单,唯一和 Android 端不一样的就是控件的创建不一样,Android 的我直接用 xml,iOS 的主要是需要定义 Frame 大小,我尝试使用函数传递的 frame 值,貌似不管用。如果有人知道问题所在,欢迎告知我!

最后,和 Android 一样,引入我们使用的第三方插件:

6.4 引入 JPVideoPlayer

在文件 bms_video_player.podspec 引入:


s.dependency 'JPVideoPlayer'

7. 链接调用

看「4」的创建 widget 后的回调函数:


Future<void> onPlatformViewCreated(id) async {

    if (widget.onCreated == null) {

      return;

      }

    widget.onCreated(new BmsVideoPlayerController.init(id));

  }

直接 new BmsVideoPlayerController.init(id),即创建了 channel


MethodChannel _channel;

BmsVideoPlayerController.init(int id) {

_channel =  new MethodChannel('bms_video_player_$id');

}

Future<void> loadUrl(String url) async {

assert(url != null);

return _channel.invokeMethod('loadUrl', url);

}

有了 channel 自然和原生代码串联起来了,同时创建 loadUrl 函数供外界调用。

8. 测试使用

藉此,我们的插件实现了基本功能了,写个 demo,测试下效果:


import 'package:flutter/material.dart';

import 'package:bms_video_player/bms_video_player.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {

  @override

  _MyAppState createState() => _MyAppState();

}

class _MyAppState extends State<MyApp> {

  var viewPlayerController;

  @override

  void initState() {

    super.initState();

  }

  @override

  Widget build(BuildContext context) {

    var x = 0.0;

    var y = 0.0;

    var width = 400.0;

    var height = width * 9.0 / 16.0;

    BmsVideoPlayer videoPlayer = new BmsVideoPlayer(

      onCreated: onViewPlayerCreated,

      x: x,

      y: y,

      width: width,

      height: height

    );

    return MaterialApp(

      home: Scaffold(

        appBar: AppBar(

          title: const Text('Plugin example app'),

        ),

        body: Container(

          child: videoPlayer,

          width: width,

          height: height

        )

      ),

    );

  }

  void onViewPlayerCreated(viewPlayerController) {

    this.viewPlayerController = viewPlayerController;

    this.viewPlayerController.loadUrl("https://www.****.com/****.mp4");

  }

}

相信这代码不用多解释了,引入我们的插件 widget,然后调用 loadUrl 函数,传入我们的视频链接,即可开始播放了。

iOS 效果

Android 效果

总结

第一次使用 Flutter,第一次实现基本的插件功能,写的比较粗糙,但相信基本的写法都在里面了。接下来就是实现播放视频的所有功能,如:暂停/播放,小窗口播放、全屏播放、缓存、静音等。

还有,就是如何实现 Dart 和原生代码进行通讯的。

未完待续,敬请期待

2人推荐
随时随地看视频
慕课网APP

热门评论

这种方式集成的播放器,在横竖屏切换的时候,会有闪烁问题,显得很不自然,不够平滑。请问这种情况有什么好的解决方式吗?

老师,能给份代码吗? 我学习学习

叶老师您好,我这边想在flutter中插入一个iOS原生视图。但是我在 

+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry 方法里面,调用

registry的方法的时候没有registerViewFactory方法,只有registrarForPlugin方法。这里应该怎么处理呢 ?请指教,谢谢。https://img4.mukewang.com/5cb752ae000143bd13800626.jpg


查看全部评论