前言
由于Flutter项目中需要使用到播放器功能,因此对flutter中各种播放器解决方案进行了一番研究和比对,最后决定还是自己通过Plugin的方法去引用原生播放器符合自己的需求,本篇文章会对各种解决方案做一个简单的比较,以及讲解一下发Flutter3.0中ios引用原生view的步骤和逻辑,方便大家遇到相同问题时,可以进行一个参考。
video_player
video_player:flutter官方出品。
优点:集成速度快,易上手,使用简单,同时支持Android,ios,web;
缺点:ui丑,功能简单,可定制化差,不支持rtmp等协议直播;
适用:对播放器要求不高,不需要直播,只要视频能播放出来就可以的用户;
better_player
better_player:国外大神在video_player的基础之上二次开发而来,对video_player进行了各种优化,并添加了非常多的实用功能。
优点:同video_player,且比video_player更强大一点;
缺点:依然是可定制化差,不支持rtmp等直播格式;
适用:对视频播放器稍微有要求比如视频格式,播放速度,缓存等功能,又不想自己动手去写原生插件,对ui定制化要求也不高的用户;
fijkplayer
fijkplayer:基于ijkplayer,是对 ijkplayer 的 Flutter 封装,支持安卓和ios;
优缺点:做过安卓和ios原生的,对ijk应该都非常熟悉了,这里就不做过多说明了;
fijkplayer支持各种视频格式包括rtmp等协议直播,支持各种常用功能,支持定制化UI,具体可以去它的官网查看文档说明;
适用:对播放器要求稍高,需要简单定制化播放器ui,需要支持直播的用户;
这里说一些个人的使用体验,因为我的项目需要支持rtmp直播,并且对ui定制化要求较高,所以放弃了官方的video_player,优先选择了fijkplayer,但实际体验上不太尽如人意。虽然支持ui定制,但想要达到自己产品的ui设计,还需要费上很大一番功夫。并且该项目作者有一年多未更新维护了,后续是否会继续更新维护解决bug不得而知。另外,我在播放rtmp时播放失败,不知何故,不过我并没深究原因,因为此时,我已经动了自己动手引用双端原生播放器的念头了!
IOS制作并引用原生View步骤
1)使用Xcode运行项目:
双击flutter项目/ios目录下的Runner.xcworkspace,Xcode会自动打开项目;
2)检验项目:
直接运行,查看是否有报错信息;
如果你已经在pubspec.yaml中使用了大量的第三方插件,此时运行可能会报错:xxx Module not Found!那么你需要在打开终端并cd到ios目录,执行 pod install
:
一般情况下都可以解决问题!
3)创建VideoViewPlugin实现FlutterPlugin协议:
import Flutter
import UIKit
class VideoViewPlugin: NSObject, FlutterPlugin {
@objc static func register(with registrar: FlutterPluginRegistrar) {
registrar.register(VideoViewFactory(registrar: registrar), withId: "plugins.my_video_player/view")
}
}
由于FlutterPlugin是OC写的,所以在Swift中实现OC协议,前面需要加上NSObject。通过FlutterPluginRegistrar的registrar注册PlatformViewFactory。
plugins.my_video_player/view即为该插件的id,在flutter中引用原生view时需要写入并且安卓,ios和flutter三方都要保持一致!
4)创建VideoViewFactory实现FlutterPlatformViewFactory协议:
import UIKit
import Flutter
class VideoViewFactory:NSObject, FlutterPlatformViewFactory {
private var registrar:FlutterPluginRegistrar
init(registrar: FlutterPluginRegistrar) {
self.registrar=registrar
super.init()
}
//create方法是在flutter中该widget加载显示出来时才执行
//所以自定flutter中使用原生View时传递的参数在create执行后,且获取到参数后,再创建channel
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
let id=args as! Int
return VideoViewPlayer(id: id, registrar: registrar)
}
//这个方法一定要写,否则接受不到flutter的传参
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
注意,createArgsCodec()一定要重写,否则无法接收到flutter在使用view时传过来的参数;create方法中的arguments即为传递的参数,create方法返回PlatformView(UIView);
5)创建VideoViewPlayer实现FlutterPlatformView协议:
import UIKit
import Flutter
class VideoViewPlayer: NSObject,FlutterPlatformView {
//懒加载
private var videoView:UIView={
let videoView=UIView()
return videoView
}()
//主要就是在这里,返回原生view
func view() -> UIView {
return videoView
}
}
6)在AppDelegate.swift中注册插件:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
//插件名自定义
if let register=self.registrar(forPlugin: "VideoViewPlugin"){
VideoViewPlugin.register(with: register)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
这里其实需要两步:
let register=self.registrar(forPlugin: "VideoViewPlugin")
VideoViewPlugin.register(with: register)
注意registrar和register是不一样的!
7)在flutter中引用VideoView:
- 自定义Widget,MyVideoPlayer:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MyVideoPlayer extends StatefulWidget {
final int id;
const MyVideoPlayer({
super.key,
required this.id,
});
State<StatefulWidget> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<MyVideoPlayer> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.my_video_player/view',
creationParams: widget.id, //传递到原生插件的参数(任意类型)
creationParamsCodec: const StandardMessageCodec(),
);
} else {
return UiKitView(
viewType: 'plugins.my_video_player/view',
creationParams: widget.id, //传递到原生插件的参数(任意类型)
creationParamsCodec: const StandardMessageCodec(),
);
}
}
}
viewType即上面VideoViewPlugin类中注册的插件id:plugins.my_video_player/view,安卓,ios和flutter保持一致!
- 引用MyVideoPlayer:
SizedBox(
height: 200,
child: MyVideoPlayer(id: 0)
)
这里可以看到,我给原生View传了一个id(需要唯一,我用的是时间戳)的参数,这个是为了后面在创建MethodChannel时加以区分,在同时使用了多个MyVideoPlayer时,不会相互影响!
此时我们就可以来测试一下,引用是否成功了,为了效果明显,我们可以在VideoViewPlayer.swift中为UIView添加一个背景色:
private var videoView:UIView={
let videoView=UIView()
videoView.backgroundColor=UIColor.red
return videoView
}()
运行效果:
如上图,说明原生View已经引用成功了!不过,本篇文章的目的,不只是讲解如何引用原生View,我们的目标是,如何引用原生播放器!
如果你本身是ios开发者,或者开发过ios项目,并且在所开发的项目中使用过第三方的播放器,那么你其实可以直接将你所使用的播放器库,引入到本项目中直接使用,无非就是在VideoViewPlayer.swift中初始化并配置一些参数,最后封装进返回的videoView中!
如果你未开发过ios项目,或者没使用过第三方的播放器,那么你就可以在github上自行搜索star数量高的ios开源播放器项目了。由于我使用的是Swift语言,所以我优先查找了用Swift写的播放器,但查找后发现star量都太低了,而且还必须要支持rtmp播放,最后还是选择了OC写的star量最高的ZFPlayer!
引入开源播放器:ZFPlayer
1)安装:Podfile中添加:
pod 'ZFPlayer', '~> 4.0'
pod 'ZFPlayer/ControlView', '~> 4.0'
pod 'ZFPlayer/AVPlayer', '~> 4.0'
pod 'ZFPlayer/ijkplayer', '~> 4.0'
由于AVPlayer本身不支持直播,所以还需要引入ZFPlayer/ijkplayer,来支持直播功能!
执行pod intall!由于github总是间歇性无法访问,如果提示SSL超时等问题,可以多试几遍!
2)参考ZFPlayer文档,将ZFPlayer封装进UIView(viewPlayer)返回给Flutter:
VideoViewPlayer中:
//zfplayer控制器
private var player:ZFPlayerController = ZFPlayerController()
//播放器进度控制条UIView
private var playerControlView=ZFPlayerControlView()
//AVPlayer管理器
private var avPlayerManager:ZFAVPlayerManager?
//IjkPlayer管理器
private var ijkPlayerManager:ZFIJKPlayerManager?
init(id:Int,registrar:FlutterPluginRegistrar) {
super.init()
//为player设置进度控制条
player.controlView=playerControlView
//为player设置containerView
player.containerView=videoView
}
此时其实就可以播放视频了,传入视频Url:
//可以根据视频类型,选择使用AVPlayer还是IjkPlayer播放:
//我这里点播使用AVPlayer,直播使用IjkPlayer,自己可以根据自身项目情况选择
if url.starts(with: "http"){
avPlayerManager=ZFAVPlayerManager()
avPlayerManager!.assetURL=URL(string: url)
player.currentPlayerManager=avPlayerManager!
playerControlView.portraitControlView.slider.isHidden=false
playerControlView.portraitControlView.currentTimeLabel.isHidden=false
playerControlView.portraitControlView.totalTimeLabel.isHidden=false
}
if url.starts(with: "rtmp"){
ijkPlayerManager=ZFIJKPlayerManager()
ijkPlayerManager!.assetURL=URL(string: url)
player.currentPlayerManager=ijkPlayerManager!
playerControlView.portraitControlView.slider.isHidden=true
playerControlView.portraitControlView.currentTimeLabel.isHidden=true
playerControlView.portraitControlView.totalTimeLabel.isHidden=true
}
if avPlayerManager != nil {
avPlayerManager!.play()
}
if ijkPlayerManager != nil {
ijkPlayerManager!.play()
}
但这样,无法动态从flutter传入url啊?所以,我们还需要flutter和ios通信,即使用MethodChannel!
3)IOS端创建FlutterMethodChannel:
//这里就用到了从flutter传入的id
func initMethodChannel(id:Int,registrar:FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "my_video_player_\(id)", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler(handleMethod)
}
//接收flutter发来的消息
func handleMethod(call: FlutterMethodCall, result: FlutterResult){
switch call.method {
case "setUrl":
let url: String=call.arguments as! String
initPlayer(url: url)
break
case "start":
play()
break
case "pause":
pause()
break
case "release":
stop()
break
default:
result(FlutterMethodNotImplemented)
break
}
}
VideoViewPlayer完整代码如下:
import UIKit
import Flutter
import ZFPlayer
class VideoViewPlayer: NSObject,FlutterPlatformView {
private var videoView:UIView={
let videoView=UIView()
return videoView
}()
private var player:ZFPlayerController = ZFPlayerController()
private var playerControlView=ZFPlayerControlView()
private var avPlayerManager:ZFAVPlayerManager?
private var ijkPlayerManager:ZFIJKPlayerManager?
init(id:Int,registrar:FlutterPluginRegistrar) {
super.init()
player.controlView=playerControlView
initMethodChannel(id: id, registrar: registrar)
}
func view() -> UIView {
return videoView
}
}
// MARK:- 播放器初始化及控制
extension VideoViewPlayer{
//初始化播放器
func initPlayer(url:String){
player.containerView=videoView
if avPlayerManager != nil {
avPlayerManager!.pause()
avPlayerManager=nil
}
if ijkPlayerManager != nil {
ijkPlayerManager!.pause()
ijkPlayerManager=nil
}
if url.starts(with: "http"){
avPlayerManager=ZFAVPlayerManager()
avPlayerManager!.assetURL=URL(string: url)
player.currentPlayerManager=avPlayerManager!
playerControlView.portraitControlView.slider.isHidden=false
playerControlView.portraitControlView.currentTimeLabel.isHidden=false
playerControlView.portraitControlView.totalTimeLabel.isHidden=false
}
if url.starts(with: "rtmp"){
ijkPlayerManager=ZFIJKPlayerManager()
ijkPlayerManager!.assetURL=URL(string: url)
player.currentPlayerManager=ijkPlayerManager!
playerControlView.portraitControlView.slider.isHidden=true
playerControlView.portraitControlView.currentTimeLabel.isHidden=true
playerControlView.portraitControlView.totalTimeLabel.isHidden=true
}
}
func play(){
if avPlayerManager != nil {
avPlayerManager!.play()
}
if ijkPlayerManager != nil {
ijkPlayerManager!.play()
}
}
func pause(){
if avPlayerManager != nil {
avPlayerManager!.pause()
}
if ijkPlayerManager != nil {
ijkPlayerManager!.pause()
}
}
func stop(){
pause()
player.stop()
}
}
// MARK:- flutter消息通道处理
extension VideoViewPlayer{
func initMethodChannel(id:Int,registrar:FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "my_video_player_\(id)", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler(handleMethod)
}
//接收flutter发来的消息
func handleMethod(call: FlutterMethodCall, result: FlutterResult){
switch call.method {
case "setUrl":
let url: String=call.arguments as! String
initPlayer(url: url)
break
case "start":
play()
break
case "pause":
pause()
break
case "release":
stop()
break
default:
result(FlutterMethodNotImplemented)
break
}
}
}
4)flutter端创建MethodChannel:
init() {
methodChannel = MethodChannel('my_video_player_$id');
methodChannel.setMethodCallHandler((call) => flutterMethod(call));
}
Future<void> setUrl(String url) async {
return methodChannel.invokeMethod('setUrl', url);
}
Future<void> start() async {
return methodChannel.invokeMethod('start');
}
...
flutter中调用setUrl后,再调用start方法即可播放了!
我这里给flutter端的MethodChanel封装为了一个Controller:
import 'package:flutter/services.dart';
import 'package:kjjl_flutter/components/loading.dart';
import 'package:kjjl_flutter/utils/log_util.dart';
class VideoViewController {
late MethodChannel methodChannel;
int id;
VideoViewController({required this.id});
init() {
methodChannel = MethodChannel('my_video_player_$id');
methodChannel.setMethodCallHandler((call) => flutterMethod(call));
}
Future<void> setUrl(String url) async {
return methodChannel.invokeMethod('setUrl', url);
}
Future<void> start() async {
return methodChannel.invokeMethod('start');
}
Future<void> pause() async {
return methodChannel.invokeMethod('pause');
}
Future<void> release() async {
return methodChannel.invokeMethod('release');
}
Future<void> stopFullScreen() async {
return methodChannel.invokeMethod('stopFullScreen');
}
Future<dynamic> flutterMethod(MethodCall methodCall) async {
switch (methodCall.method) {
}
}
}
在State中使用时的部分代码:
SizedBox(
height: videoHeight,
child: currentVideo != null && videoViewController != null
? MyVideoPlayer(id: videoViewController!.id)
: SizedBox(),
)
...
VideoModel? currentVideo;
VideoViewController? videoViewController;
//页面销毁时释放播放器
void dispose() {
if (videoViewController != null) videoViewController!.release();
super.dispose();
}
//点击播放时执行
play(VideoModel video) {
if (videoViewController != null) {
if (currentVideo != null && currentVideo!.id == video.id) return;
setState(() {
currentVideo = video;
videoViewController!.release();
startPlay(video);
});
} else {
setState(() {
currentVideo = video;
videoViewController = VideoViewController(id: DateTime.now().millisecondsSinceEpoch);
videoViewController!.init();
//延时500ms执行,是为了防止播放器还未初始化完成,就调用了播放,导致首次播放失败;
Future.delayed(const Duration(milliseconds: 500), () {
startPlay(video);
});
});
}
}
startPlay(VideoModel video){
videoViewController!.setUrl(video.mobileUrl);
videoViewController!.start();
}
效果图:
直播:
录播:
下一篇:Flutter3引用原生播放器-Android篇