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

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

举一个例子:国内的 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,});@overrideState<StatefulWidget> createState() => _VideoPlayerState();}class _VideoPlayerState extends State<BmsVideoPlayer> {@overridevoid initState() {super.initState();}@overrideWidget 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;}@Overridepublic 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);}@Overridepublic View getView() {return jzvdStd;}@Overridepublic 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();}}@Overridepublic 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.JzvdStdxmlns: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)frameviewIdentifier:(int64_t)viewIdarguments:(id _Nullable)args {BMSVideoPlayerViewController* viewController =[[BMSVideoPlayerViewController alloc] initWithWithFrame:frameviewIdentifier:viewIdarguments:argsbinaryMessenger:_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)frameviewIdentifier:(int64_t)viewIdarguments:(id _Nullable)argsbinaryMessenger:(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:nsUrlbufferingIndicator:nilcontrolView:nilprogressView:nilconfiguration:^(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;@overridevoid initState() {super.initState();}@overrideWidget 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 和原生代码进行通讯的。

未完待续,敬请期待

跟我一步一步实现 Flutter 视频播放插件 (一)相关推荐

  1. alert()的功能_功能强大的Flutter 视频播放插件

    简介 flutter_tencentplayer_plus 是基于腾讯云点播封装的flutter版的播放器插件 提供video_player 相似的api, 是flutter_tencentplaye ...

  2. 调试JDK源码-一步一步看HashMap怎么Hash和扩容

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  3. 一步一步指引你在Windows7上配置编译使用Caffe(https://github.com/fengbingchun/Caffe_Test)

    之前写过几篇关于Caffe源码在Windows764位上配置编译及使用过程,只是没有把整个工程放到网上,最近把整个工程整理清理了下,把它放到了GitHub上.下面对这个工程的使用作几点说明: 1.   ...

  4. 一步一步实现扫雷游戏(C语言实现)(三)

    使用WIN32API连接窗口 此项目相关博文链接 一步一步实现扫雷游戏(C语言实现)(一) 一步一步实现扫雷游戏(C语言实现)(二) 一步一步实现扫雷游戏(C语言实现)(三) 一步一步实现扫雷游戏(C ...

  5. 一步一步学Silverlight 2系列(3):界面布局

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  6. 一步一步写算法(之图结构)

    原文:一步一步写算法(之图结构) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 图是数据结构里面的重要一章.通过图,我们可以判断两个点之间是 ...

  7. 融合应用11.1.8安装,一步一步的引导

    融合应用11.1.8安装,一步一步的引导 融合应用11.1.8 安装并不是简单的与电子商务套件11 i / R12安装. 所以我们需要安装划分为许多步骤. 请注意,11.1.8 11.1.7总统发布供 ...

  8. 一步一步学Silverlight 2系列(10):使用用户控件

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  9. 【深度学习基础】一步一步讲解卷积神经网络

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送 本文转自:一步一步讲解卷积神经网络 卷积神经网络(Convoluti ...

最新文章

  1. 排序---快速排序及其切分函数Partition应用
  2. 第一章:1.1.4信号运算
  3. (视频+图文)机器学习入门系列-第6章 机器学习库Scikit-learn
  4. c语言 字母赋值给变量,C++变量(变量定义和赋值)详解
  5. u8转完看不到菜单_填制凭证界面上的菜单看不见
  6. Spring延迟依赖注入ObjectFactory/ObjectProvider
  7. 导 Kinect2库,opencv库,pcl库
  8. fcpx调整图层_【FCPX萌新系列】新手常遇到的4个基础调色问题
  9. 关于 QueryDSL 配置和使用(详细)
  10. 鸢尾花(Iris)数据集下载保存到Excel文件
  11. ACCESS-入门思维导图
  12. 二叉树非递归遍历方法总结
  13. 《需求工程:软件建模与分析》笔记(一)
  14. Kubernetes30--弹性伸缩总结
  15. 2021-08-07:与数组中元素的最大异或值。给你一个由非负整数组成的数组 nums 。另有一个查询数组 queries ,其中 queries[i] = [xi, mi] 。第 i 个查询的答案是
  16. 计算机内存怎样清理,怎么样清理电脑内存 电脑清理内存方法【图文】
  17. 解决Eclipse中无法直接使用sun.misc.BASE64Encoder及sun.misc.BASE64Decoder的问题---gxl
  18. musiclink-v1.9 一款php源码的音乐外链程序,musiclink-v1.9 一款PHP源码的音乐外链程序 - 下载 - 搜珍网...
  19. 如何优雅的使用LiveData实现一套EventBus(事件总线)
  20. PSO粒子群算法微电网优化调度(微电网孤岛运行优化调度)matlab程序

热门文章

  1. 【Docker系列】docker manifest
  2. 砥砺前行20载,他见证了中国制造信息化的成长
  3. 实时分析数据库 Druid,Mark 一下
  4. VMware启动报错Attempting to start up from:EFI VMware Virtual SCSI Hard Drive(0.0)
  5. 感知器-面向幼儿园的人工智能(2)
  6. 管理书籍推荐:这本书所有管理者必读!
  7. 网站APP数据库里的用户信息被泄露篡改怎么办
  8. 濒临死亡的校园BBS
  9. 如何留住优秀的测试人员
  10. 国内领先的企业级免费开源电商系统 ShopXO 入驻 GitCode