Flutter视频播放器,简洁!

·  阅读 15695

效果图

注:亮度调节和音量调节gif无法体现,功能是ok的,其次默认Icon锁的close和open实在难以分辨。

竖屏:

DA145B0D8BB80D04EE7CACAF4717B5C4.jpg

横屏:

26826EB66BCE4B2A2D4A451028259FFB.jpg

gif详情:

2022-01-20 15_26_30.gif


a3b7a9dc81fb12a473bd5710fa41dfca.gif

关于Flutter视频的播放,大多数小伙伴们会有很多方案,Pub上也有很多优秀的插件。但是完全适合自己的项目总是很难,多数还是需要自己去封装。笔者也是的,拖沓了很久,还是决定自己封装一个适合自己项目的Flutter视频播放器,毫无疑问选择了官方的播放器插件video_player进行封装自定义。过程并不复杂,只要认真看完,相信每一位开发者都能定制属于自己的视频播放器!

环境:Flutter 2.8.1 channel stable ;Dart 2.15.1

需要音频播放器的看这里:Flutter音乐播放器

第三方插件

# 播放器
video_player: ^2.2.11
# 屏幕旋转
auto_orientation: ^2.2.1
# 亮度和音量调节
brightness_volume: ^1.0.3

项目结构

3476AB4E-7342-4B7F-9C93-236FA9692ECD.png

video_player_utils

重点说下这个工具类,因为视频播放,涉及到状态改变有很多,笔者刚开始选择使用 InheritedWidget来在众多的widget之间共享数据。但是总感觉这样有点繁琐,且不很优雅!

这里非广告,如果是使用GetX就很简单了,笔者也使用了GetX进行封装了,一泻千里的赶脚!,但是笔者还是那句话:刚开始接触Flutter的开发者不是很建议使用GetX,可以先熟悉下Flutter状态管理的基础原理再行使用。而且为了尽量简洁,还是不引入其他的第三方了。

我们选择对第三方插件进行封装的目的不外乎这几个:

  • 方便调用
  • 适配业务需要
  • 高内聚低耦合
  • 后期迭代维护

于是笔者就写了一个工具类VideoPlayerUtils,专门且只用来处理播放器的所有业务。包括暂停、播放、跳转、调节音量、调节亮度、切换视频等操作。在所有的widget中不会引用关于video_player或其他第三方插件的任何信息,VideoPlayerUtils负责widget与播放器之间的所有操作交互。后续优化迭代或更换播放器插件时,只需针对这个工具类进行修改,对所有widget不会有任何的影响,大大的解耦合了。

public 属性

static String get url => _instance._url; // 当前播放的url

static VideoPlayerState get state => _instance._state; // 当前播放状态

static bool get isInitialized => _instance._isInitialized; // 视频是否已经完成初始化

static Duration get duration => _instance._duration; // 视频总时长

static Duration get position => _instance._position; // 当前视频播放进度

static double get aspectRatio => _instance._aspectRatio; // 视频播放比例

其中VideoPlayerState

/// 播放状态
enum VideoPlayerState{
  stopped, // 初始状态,已停止或发生错误
  playing, // 正在播放
  paused,  // 暂停
  completed // 播放结束
}

提供以上的公共属性,可以通过VideoPlayerUtils来获取对应的值,使用get只读,使外界不会误修改这些属性,以保证数值的安全性。开发者可根据自身需要自行添加属性。

public 方法

// 播放、暂停、切换视频等操作,内部自行判断是播放还是暂停,开发者不用关心
static void playerHandle(String url,{bool autoPlay = true,bool looping = false}) async{}

// 跳转播放
static void seekTo({required Duration position}) async{}

// 初始化结果监听,回调2个参数:1、初始化是否成功,2、播放的widget,方便setState()
static void initializedListener({required dynamic key,required Function(bool,Widget) listener}){}

// 移除初始化结果监听
static void removeInitializedListener(dynamic key){}

// 播放状态监听,stopped、playing、paused、completed等
static void statusListener({required dynamic key,required Function(VideoPlayerState) listener}){}

// 移除播放状态监听
static void removeStatusListener(dynamic key){}

// 播放进度监听
static void positionListener({required dynamic key,required Function(int) listener}){}

// 移除播放进度监听
static void removePositionListener(dynamic key){}

// 获取音量
static Future<double> getVolume() async{}

// 设置音量
static Future<void> setVolume(double volume) async{}

// 获取亮度
static Future<double> getBrightness() async{}

// 设置亮度
static Future<void> setBrightness(double brightness) async{}

// 设置播放速度
static Future<void> setSpeed(double speed) async{}

// 设置是否循环播放
static Future<void> setLooping(bool looping) async{}

// 设置横屏
static setLandscape(){}

// 设置竖屏
static setPortrait(){}

// 简单处理下时间格式化mm:ss (超过1小时可自行处理hh:mm:ss,严格来说不属于播放业务)
static String formatDuration(int second){}

// 释放资源
static dispose(){}

// 开发者可行添加比如:亮度、音量改变监听回调等。

提供以上方法来处理播放器的所有业务。同样的开发者可根据自身需要自行添加或修改。

playerHandle
static void playerHandle(String url,{bool autoPlay = true,bool looping = false}) async{}

重点说下这个方法,是整个业务的核心方法,控制视频的播放或暂停。开发者只要遇到播放或暂停是均可调用此方法,具体是播放或暂停,内部根据传入的url自行判断,开发者不需要关心。

切换新视频也是使用此方法,传入的url与上次不一致,自动切换新视频。笔者可根据statusListener来监听播放状态的改变,以此处理自身逻辑。

initializedListener
// 初始化结果监听
static void initializedListener({required dynamic key,required Function(bool,Widget) listener}){}

这个也需要提下,视频播放器在播放新视频时会异步初始化,一般我们的操作是在initState()初始化,成功后再setState()。这里笔者遇到一个让人蛋疼的问题:

我们看video_player的使用:

AspectRatio(
  aspectRatio: controller.aspectRatio,
  child: VideoPlayer(controller),
);

VideoPlayer(controller):widget中已经持有了controller。本来笔者封装的目的就是为了让widget与controller的之间解耦合。但此时的笔者。。。。

pb.png

放弃不是不可能放弃的,这辈子都不会放弃的!

于是笔者取了巧,写了一个初始化监听器initializedListener,包换2个参数:bool,Widget,初始化是否成功;其中widget为初始化成功返回需要展示的播放器UI,失败默认返回const SizedBox()

使用

到这里就可以简单使用了:

class _VideoPlayerUIState extends State<VideoPlayerUI> {
  Widget? _playerUI;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 播放视频
    VideoPlayerUtils.playerHandle("http://flv3.bn.netease.com/tvmrepo/2018/6/9/R/EDJTRAD9R/SD/EDJTRAD9R-mobile.mp4");
    // 播放新视频,初始化监听
    VideoPlayerUtils.initializedListener(key: this, listener: (initialize,widget){
      if(initialize){ // 初始化成功后,更新UI
        _playerUI = widget;
        if(!mounted) return;
        setState(() {});
      }
    });
  }
  @override
  void dispose() {
    // TODO: implement dispose
    // 移除监听
    VideoPlayerUtils.removeInitializedListener(this);
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      width: 414,
      height: 414*9/16,
      color: Colors.black26,
      child: _playerUI ?? const CircularProgressIndicator(
        strokeWidth: 3,
      )
    );
  }
}

没看错,视频播放就是这么简单。

Widget

如果有更多的业务功能,笔者也按照自己的需求写了一套,同样的开发者可根据自身需要自行添加或修改。

video_player_gestures

VideoPlayerGestures主要是处理手势的,比如快进、快退等跳转播放;左侧上下滑动调节亮度;右侧上下滑动调节音量;单击是否开启沉浸式播放,所有widget的隐藏与显示;双击播放、暂停等。

GestureDetector(
  onTap: _onTap, // 单击上下widget隐藏与显示
  onDoubleTap: _onDoubleTap, // 双击暂停、播放
  onVerticalDragStart:_onVerticalDragStart, // 根据起始位置。确定是调整亮度还是调整声音
  onVerticalDragUpdate: _onVerticalDragUpdate,// 一般在更新的时候,同步调整亮度或声音
  onVerticalDragEnd: _onVerticalDragEnd, // 结束后,隐藏百分比提示信息widget
  onHorizontalDragStart: _onHorizontalDragStart,  // 手势跳转播放起始位置
  onHorizontalDragUpdate: _onHorizontalDragUpdate, // 根据手势更新快进或快退
  onHorizontalDragEnd: _onHorizontalDragEnd,  // 手势结束seekTo
  child: Stack(
    children: _children,
  ),
);

哦,还有PercentageWidget也放到这个文件下了,就是这玩意:

0D865B79-BE40-4D7E-A758-939FAA4D5EF2.png

因为显示的百分比与手势相关,随着手势移动而更新。开发者可自行处理。

video_player_top

笔者处出于简单考虑,就按照整个UI的位置命名了。瞅一眼就知道是啥玩意。

6C829789-F4F2-464E-8FFC-9960F90129D0.png

同样的开发者可根据自身需要自行添加或修改。

video_player_center

就是这玩意:

340DD278-9485-4963-9121-BD4CA4E1AE8F.png

同样的开发者可根据自身需要自行添加或修改。话说这个锁的Icon的open和close是真的难分辨!

video_player_bottom

就是这玩意:

D9F74B37-B501-436D-8C9A-6309B794C778.png

同样的开发者可根据自身需要自行添加或修改。

video_player_slider

这玩意是自定义的,别问,问就是跟产品干一架落了下风

QQ20220121-0.jpg

主要就是自定义这玩意:

SliderThemeData(
  trackHeight: 8,
  inactiveTrackColor: Colors.grey,
  activeTrackColor: Colors.greenAccent,
  thumbShape: SliderThumbImage(image: _customImage),// 自定义
  trackShape: const CustomTrackShape(), // 自定义
),

同样的开发者可根据自身需要自定义。

注:这里没有添加缓冲的进度,开发可查看video_player中的源码VideoProgressIndicator,按业务自行定义。

video_player_page

这玩意就是整合以上的widget,再考虑下全屏的安全区域,没啥东西。开发者可自行处理!

SafeArea( 
  top: !_isFullScreen,
  bottom: !_isFullScreen,
  left: !_isFullScreen,
  right: !_isFullScreen,
  child: SizedBox(
    height: _height,
    width: _width,
    child: _playerUI != null ? VideoPlayerGestures(
      appearCallback: (appear){
        _top!.opacityCallback(appear);
        _lockIcon!.opacityCallback(appear);
        _bottom!.opacityCallback(appear);
        },
      children: [
        Center(
          child: _playerUI,
        ),
        _top!,
        _lockIcon!,
        _bottom!
      ],
    ) : Container(
      alignment: Alignment.center,
      color: Colors.black26,
      child: const CircularProgressIndicator(
        strokeWidth: 3,
      ),
    )
  ),
);

RCFlutterVideoPlayer

具体的实现监听器的思路,看这里

自此一个漂亮的Flutter视频播放器就已经结束了。如果您觉得对您有些许帮助的话,欢迎 Star

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改