小工具知识网

当前位置: 首页 > 行业知识 > Flutter 第三方 视频播放器(better

行业知识

Flutter 第三方 视频播放器(better

2026-10-20 15:46:26 来源:互联网转载

一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络,希望未来技术之巅上有你们也有我。

前言

该���站记载了很多Better_player资料的属性和SDK的使用值的参考一下,然后我把资料下载下来了.最好可以在这里下载参考.

betterplayer资料.md文件

better_player(中文)该链接是SDK在理解过程中的一些中文注释用到再在这个基础上表明.

如何运行官方的demol

demol地址: better_play官方下载demol地址

运行ios的话需要,打开设置配置证书和profile文件,boundleID

然后你会发现第三方的pod是红色的,

这个时候不用管他,直接用AndroidStudio打开,直接点击运行就会,自动下载.

集成

手动集成

下面的集成方法是安卓的同事在better_player的demol基础上进行修改,然后手动集成的.

1.上去pub.de网站吧better_player的demol下载下来,

2.创建一个文件把demol复制到自己的项目中

3.pubspec.yaml文件引入路径 Pub get一下就可以了

  better_player:    path: package/betterplayer

手动集成SDK的时候可以把example(例子)删除,不用集成进去

自动集成

dependencies:  better_player: ^0.0.83

手动集成SDK+集成自定义界面 = 开始better_player使用

better-player播放器(自定义SDK+自定义界面) 代码下载链接

上面已经写好的手动集成的方法,只是集成的SDK的来之官网的,下面的demol链接是经过修改的better_player SDK

修改内容:我自己增加了seetTo成功的回调

安卓的修改不太清楚,主要是播放过程中的输出问题(当时他说better_player监听进度输出的状态不准,是异步的,公司需要用来计时播放时间进行收费的.),不过修改后不影响使用.

1.手动集成

1.创建一个文件把demol复制到自己的项目中

2.pubspec.yaml文件引入路径 Pub get一下就可以了

  better_player:    path: package/betterplayer

2.把自定义控件拖拽进项目

把下图红色方框的全部内容拖拽进来进行使用

3.使用

下面是整个使用的页面代码

import 'package:flutter/material.dart';import 'package:flutter/services.dart';import 'package:better_player/better_player.dart';import 'package:flutterdemols/video_control/custom_control_widget.dart';import 'package:flutterdemols/video_control/seek_to_progress.dart';class MovieDetailsScreen extends StatefulWidget {  const MovieDetailsScreen({super.key});  @override  State createState() => _MovieDetailsScreenState();}class _MovieDetailsScreenState extends State {  late BetterPlayerController _playerController;  final List _pointList = [0.2, 0.5, 0.7];  SeekToProgress _seekToProgress = SeekToProgress();  final GlobalKey _betterPlayerKey = GlobalKey();  @override  void initState() {    BetterPlayerConfiguration playerConfig = BetterPlayerConfiguration(      aspectRatio: 16 / 9,      fit: BoxFit.contain,      autoPlay: true,      deviceOrientationsAfterFullScreen: [        DeviceOrientation.portraitDown,        DeviceOrientation.portraitUp      ],      subtitlesConfiguration: const BetterPlayerSubtitlesConfiguration(        backgroundColor: Colors.transparent,        fontColor: Colors.white,        outlineColor: Colors.black,        fontSize: 14,      ),      controlsConfiguration: BetterPlayerControlsConfiguration(        customControlsBuilder: (controller, onControlsVisibilityChanged) =>            CustomControlWidget(          controller: controller,          onControlsVisibilityChanged: onControlsVisibilityChanged,          pointList: _pointList,          seekToProgress: _seekToProgress,          betterPlayerKey: _betterPlayerKey,        ),        playerTheme: BetterPlayerTheme.custom,      ),    );    _playerController = BetterPlayerController(playerConfig);    // _playerController.setPartTime(partTimes);    _setupDataSource();    _playerController.setBetterPlayerGlobalKey(_betterPlayerKey);    super.initState();  }  void _setupDataSource() async {    BetterPlayerDataSource dataSource = BetterPlayerDataSource(      BetterPlayerDataSourceType.network,      // 'https://vd3.bdstatic.com/mda-kksevzw0s5ap0x6k/hd/cae_h264_nowatermark/1606445103/mda-kksevzw0s5ap0x6k.mp4',      'http://test-naxosdirect-res.kuke.com/nml/hls-m4s/2mb/2/2.110280-81.2mb.part2/index.m3u8',      subtitles: BetterPlayerSubtitlesSource.single(        type: BetterPlayerSubtitlesSourceType.network,        url:            'https://www.naxosvideolibrary.com/supplemental/OA1334D/deutsch.part1.vtt',        name: "My subtitles",        selectedByDefault: true,      ),    );    _playerController.setupDataSource(dataSource);  }  @override  Widget build(BuildContext context) {    double screenWidth = MediaQuery.of(context).size.width;    return Scaffold(      appBar: AppBar(          // title: Text(name),          ),      body: ListView(        children: [          AspectRatio(            aspectRatio: 16 / 9,            child: BetterPlayer(              controller: _playerController,              key: _betterPlayerKey,            ),          ),          Container(            width: screenWidth,            height: 60,            alignment: Alignment.center,            color: Colors.white,            child: Row(              mainAxisAlignment: MainAxisAlignment.spaceAround,              children: [                TextButton(                  style: TextButton.styleFrom(                    backgroundColor: Colors.blue[300], //背景颜色                  ),                  onPressed: () {                    _seekToProgress.updateProgress(_pointList[0]);// progressChangeNotifier.updateProgressValue(0.2);                  },                  child: const Text(                    '第一章',                    style: TextStyle(                      color: Colors.white,                      fontSize: 14,                    ),                  ),                ),                TextButton(                  style: TextButton.styleFrom(                    backgroundColor: Colors.blue[300], //背景颜色                  ),                  onPressed: () {                    _seekToProgress.updateProgress(_pointList[1]);                  },                  child: const Text(                    '第二章',                    style: TextStyle(                      color: Colors.white,                      fontSize: 14,                    ),                  ),                ),                TextButton(                  style: TextButton.styleFrom(                    backgroundColor: Colors.blue[300], //背景颜色                  ),                  onPressed: () {                    _seekToProgress.updateProgress(_pointList[2]);                  },                  child: const Text(                    '第三章',                    style: TextStyle(                      color: Colors.white,                      fontSize: 14,                    ),                  ),                ),              ],            ),          ),        ],      ),    );  }  //跳转到章节1  void _partOne() {    // _playerController.seekTo(Duration(milliseconds: partTimes[0]));  }  //跳转到章节2  void _partTwo() {    // _playerController.seekTo(Duration(milliseconds: partTimes[1]));  }  //跳转到章节3  void _partThree() {    // _playerController.seekTo(Duration(milliseconds: partTimes[2]));  }  //设置倍速  void _speed(double speed) {    _playerController.setSpeed(speed);  }  //字幕开  void _subTitleOpen() {    _playerController.setupSubtitleSource(        _playerController.betterPlayerSubtitlesSourceList.first);  }  //字幕关  void _subTitleClose() {    _playerController.setupSubtitleSource(BetterPlayerSubtitlesSource(        type: BetterPlayerSubtitlesSourceType.none));  }  @override  void dispose() {    super.dispose();    _playerController.dispose();  }}

使用

1.最简单的播放器

1.flutter-betterplayer最简单

1.配置好参数

2.设置视频源

3.约束好UI

import 'package:better_player/better_player.dart';import 'package:flutter/material.dart';import 'package:flutter/services.dart';class VideoPlayer extends StatefulWidget {  const VideoPlayer({super.key});  @override  State createState() {    return _VideoPlayerPageState();  }}class _VideoPlayerPageState extends State {  late BetterPlayerController _betterPlayerController;  @override  void initState() {    //控制器配置参数    BetterPlayerConfiguration betterPlayerConfiguration = const BetterPlayerConfiguration(      aspectRatio: 16 / 9,//短边全部显示,长边按照16:9      fit: BoxFit.contain,//视频在屏幕的适应方式  contain:尽量放大,然后保持缩放比例(上面已经设置16/9)      autoPlay: true,//视频显示后立即播放      deviceOrientationsAfterFullScreen: [DeviceOrientation.portraitDown, DeviceOrientation.portraitUp],//定义退出全屏后允许的设备方向集    );    _betterPlayerController = BetterPlayerController(betterPlayerConfiguration);    // 配置视频数据源    BetterPlayerDataSource dataSource = BetterPlayerDataSource(      BetterPlayerDataSourceType.network,      "https://vd3.bdstatic.com/mda-kksevzw0s5ap0x6k/hd/cae_h264_nowatermark/1606445103/mda-kksevzw0s5ap0x6k.mp4",    );    _betterPlayerController.setupDataSource(dataSource);    super.initState();  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: const Text('BetterPlayer'),      ),      body: AspectRatio(        aspectRatio: 16 / 9,        child: BetterPlayer(controller: _betterPlayerController),      ),    );  }}

2.切换播放进度,播放速度

  void _partOne() {    _betterPlayerController.seekTo(const Duration(milliseconds: 1*60*1000));  }  void _partTwo() {    _betterPlayerController.seekTo(const Duration(milliseconds: 2*60*1000));  }  void _partThree() {    _betterPlayerController.seekTo(const Duration(milliseconds: 3*60*1000));  }  void _speedOne() {    _betterPlayerController.setSpeed(0.5);  }  void _speedTwo() {    _betterPlayerController.setSpeed(1);  }  void _speedThree() {    _betterPlayerController.setSpeed(2);  }

3.添加字幕功能,控制是否显示

3.flutter-betterplayer添加字幕功能控制是否显示

subtitlesConfiguration: BetterPlayerSubtitlesConfiguration(  backgroundColor: Colors.transparent,//字幕背景颜色  fontColor: Colors.white,//字体颜色  outlineColor: Colors.black,  fontSize: 14,//字体大小),
subtitles: BetterPlayerSubtitlesSource.single(  type: BetterPlayerSubtitlesSourceType.network,  url: "https://www.naxosvideolibrary.com/supplemental/OA1334D/deutsch.part1.vtt",  name: "My subtitles",  selectedByDefault: true,),//配置字幕数据

控制字幕是否显示

显示

_betterPlayerController.setupSubtitleSource(_betterPlayerController.betterPlayerSubtitlesSourceList.first);

隐藏

_betterPlayerController.setupSubtitleSource(BetterPlayerSubtitlesSource(type: BetterPlayerSubtitlesSourceType.none));

4.对Control-UI的属性的使用

4.flutter-betterplayerControl-UI的属性的使用

better_player播放器自带播放的控制UI,better_player提供了很多属性关于它的设置,下面说明一下

controlsConfiguration: BetterPlayerControlsConfiguration(  controlBarColor: Colors.green,//全屏控制栏的背景颜色  textColor: Colors.white,//文字颜色  iconsColor: Colors.white,//图标的颜色  enableFullscreen: true,//是否显示全屏按键(false: 不显示  true: 显示)  enableMute: false,//是否静音(false:不静音 true:静音)(未见过效果)  enableProgressText: false,//启用/禁用进度文本的标志(未见过效果)  enableProgressBar: true,//是否显示进度条(false: 不显示  true: 显示)  enableProgressBarDrag: false,//是否允许拖拽进度条(false: 不允许  true: 允许)  enablePlayPause: false,//是否显示暂停播放按键(false: 不显示  true: 显示)  enableSkips: false,//是否显示前进后退按键(false: 不显示  true: 显示)  showControlsOnInitialize: false,//用于显示 init 上的控件的标志  controlBarHeight: 50,//底部控件栏的高度  enableOverflowMenu: true,//字幕溢出是否显示(false: 不显示  true: 显示)  enablePlaybackSpeed: true,//是否显示播放按键  enableSubtitles: true,//是否显示字幕按键  enableQualities: true,//是否显示画质按键  enablePip: true,//是否显示画中画效果  enableRetry: true,//是否显示重试效果按键(false: 不显示  true: 显示)  enableAudioTracks: true,//是否显示音轨按键(false: 不显示  true: 显示)  showControls: true,//隐藏所有控件(false: 不显示  true: 显示)  customControlsBuilder:      (controller, onControlsVisibilityChanged) =>      CustomControlWidget(        progressChangeNotifier: progressChangeNotifier,        controller: controller,        onControlsVisibilityChanged: onControlsVisibilityChanged,      ),  playerTheme: BetterPlayerTheme.custom,),

controlBarColor: Colors.green, 全屏控制栏的背景颜色

textColor: Colors.red, 文字颜色

iconsColor: Colors.red,图标的颜色

enableFullscreen: false, 是否显示全屏按键(false: 不显示 true: 显示)

enableMute: true,//是否静音(false:不静音 true:静音) true false 都没有效果

enableProgressText: false, 启用/禁用进度文本的标志

enableProgressBar: false, 是否显示进度条(false: 不显示 true: 显示)

enableProgressBarDrag: false, 是否允许拖拽进度条(false: 不允许 true: 允许)

false 使用手势无法拖拽进度条

enablePlayPause: false, 是否显示暂停播放按键(false: 不显示 true: 显示)

enableSkips: false, 是否显示前进后退按键(false: 不显示 true: 显示)

showControlsOnInitialize: true,用于显示 init 上的控件的标志(未见效果)

controlBarHeight: 80, 底部控件栏的高度

enableOverflowMenu: true, 字幕溢出是否显示(false: 不显示 true: 显示)

enablePlaybackSpeed: false, 是否显示播放按键

enableSubtitles: false,是否显示字幕按键

enableQualities: true,是否显示画质按键

enableAudioTracks: true, 是否显示音轨按键(false: 不显示 true: 显示)

enableRetry: false, 是否显示重试效果按键(false: 不显示 true: 显示)(未知效果)

enablePip: true, 是否显示画中画效果(未知效果)

5.控制器属性使用

5.flutter-betterplayer控制器属性使用

looping: true,//是否循环播放(播放完毕之后从头开始播放)showPlaceholderUntilPlay: false,//是否应在按下播放按钮之前显示占位符placeholderOnTop: false,//玩家堆栈的占位符位置。如果为 false,则占位符将为,显示在底部,所以用户需要手动隐藏它。默认为:truefullScreenByDefault: false,//定义按下播放按钮时播放器是否以全屏模式启动allowedScreenSleep: false,//定义玩家是否会在全屏模式下休眠fullScreenAspectRatio: 16 / 9,//(默认跟随:aspectRatio属性)定义全屏时使用的宽高比rotation: 0,//(默认:0)定义视频的旋转度数。角度将仅旋转视频框,控件将位于同一位置。 默认值为 0。可以是 0、90、180、270。(界面显示默认的旋转角度)autoDetectFullscreenDeviceOrientation: false,//根据宽高比是否大于1,自动适应全屏还是非全屏handleLifecycle: true,//(默认:true) 定义启用/禁用生命周期处理的标志(应用程序回到后台停止播放视频,应用程序回到前台回复视频播放)autoDispose: true,//(默认:true) 定义在 BetterPlayer 处理上启用/禁用自动处理的标志。

6.自定义控件

6.flutter-betterplayer自定义控件

一般开发中,better_playrer提供的UI并不能满足要求的,很多时候都是根据公司的需求进行开发.

我在开发控件的时候,录了一个视频,解析一下整个自定义控件的原理.

导入写好的控件封装

import 'package:flutterdemols/CustomControlsWidget/custom_control_widget.dart';

使用

playerTheme: BetterPlayerTheme.custom,//设置自定义UIcustomControlsBuilder: (controller, onControlsVisibilityChanged) => CustomControlWidget(  controller: controller,  onControlsVisibilityChanged: onControlsVisibilityChanged,),//赋值自定义UI

下面说一下整个开发的原理:

核心思想:

1: 他们之间的属性更新状态是通过值传递(顺传),逆传通过block,传递到外面调用setState更新UI.

2: 类与类之间里面会有相同的属性,然后会把自己的属性回调出去,其实基于上面的第一点,他们需要通过自身属性控制状态,然后他自己的状态传递出去进行其他类的状态同步更新.

3: .整个控件主要就是 :

3.1: 属性的状态控制

3.2: 赋值给UI更新

3.3: UI的布局

3.4: 计算:

3.4.1: 进度条的百分比换算

3.4.2: 时间的换算

每个类的说明:

point类

point点这个类主要的工作就是用来去记录分段式的一个模型数据来的,它有三个属性,一个是左边起点的位置。圆的右边起点的位置,还有圆心的位置,到时候我们去点击进度条的时候,我们就需要用来去判断当前你点击的这个点的。是否在圆心的范围,这个圆的范围,如果是的话,我们就直接把手柄移动到对应的位置就好了。

progress_colors类

这个类的作用就是用来管理滑动进度条控件的颜色来的

video_progress_bar_painter类

这个类的作用就是用来去绘制进度条的,它里面主要就是绘制了这个进度条的背景的view。然后呢,有一个预加载进度的view,然后呢,就是正在播放的这个view,然后通过for循环去绘制分段式的三个点,然后。再后就是绘制这个手柄主要就是整个UI的进行的,这个绘制就相当于iOS的draw的方法。

video_progress_bar类

那么我们刚刚已经封装好了整个绘制进度条的UI的这个类的,然后呢,我们在外面也再次封装了一个关于进度条的。类它的目的呢,就是调用我们刚刚绘制进度条UI的,然后呢,我们在这个UI的外面再包裹一个手势的相关操作的。其实这个类的目的就是说主要记载的就是手势点击和滑动的整个UI操作的一些逻辑,然后计算好它,然后再通过回调方法来回调出去的。

touch_widget类

这个类的作用就是视频正在播放过程中,我们可以左滑右滑来去修改整个播放进度的。我们可以在视频的。中央就是任意位置,能够左滑右滑来修改进度的。

custom_control_widget类

最后一个类的,就是对刚刚我们上面所说的所有类的进行一个汇集的,那么这个类呢,主要我们呢,就把它这个控制器的视频播放器的控制器传递进来。传递进来之后呢,就方便我们对这个控制器和UI的一个结合的相关操作,一个就是我们可以拿到这个控制器进行监听,它拿到当前的播放进度。嗯,开始的播放时间,正在播放的时间和结束的播放时间,然后呢,我们也可以拿到这个控制器提供给我们的API,去修改它当前的播放进度等等的一些相关操作,例如是否显示字幕。还有全屏,还有就是切换播放速度的一些操作来的。

这个类呢,在底部呢,有三个方法,一个就是把毫秒级的时间转换成label的显示,然后label的时间又转换成毫秒级的时间。然后就是当我们切换进度之后呢,进行一些简单的计算之后,再去调用控制器提供给我们的切换进度的API。最后就是。上面就有一些,剩下的就是UI的绘制,那么绘制之后嘞,每个点击方法都有相应的逻辑处理在上面,就是一些是有属性来的,就是在这个类里面去用的。然后呢,再上面就是一些构造函数的方法。

7.预加载进度(基于自定义控件)

7.better-player预加载进度

预加载进度当时已经在VideoProgressBarPainter的类已经写好了,只是使用better_player的时候预加载进度的属性不知道在哪里获取信息回来进行赋值.

其实可以在监听方法里面进行获取的:

8.增加播放过程中,没有触摸屏幕,5秒中后隐藏触摸板(基于自定义控件)

8.better-player(5秒后隐藏触摸板)

主要原理就是,开一个倒计时器,如果5秒钟以内,没有触摸过屏幕就显示透明的view,有触摸就重新赋值5秒,烧毁控制器,重新计算倒计时.用该属性 _showControl控制是否显示触摸板.

接下来剩下的逻辑就是在触摸板的每一个按键里面当点击的时候都调用_againCountdown()

9.画中画功能

better_player 画中画功能

画中画better_player的demol里面有事例.我们也可以自己创建一个demol,然后直接把这个控制器拖拽过来看一下就明白了.

然后把控制器拖拽过来,这个一个有效的视频url就可以用了.

下面说一下使用过程中的设置步骤:

//创建一个全局画中画属性final GlobalKey _betterPlayerKey = GlobalKey();//赋值到better_player_betterPlayerController.setBetterPlayerGlobalKey(_betterPlayerKey);//开启画中画_betterPlayerController.enablePictureInPicture(_betterPlayerKey);//隐藏画中画_betterPlayerController.disablePictureInPicture();

把上面的代码复制到下面的位置

运行的时候发现,点击了按钮,画中画还是粘在App里面,好像有效果了,但是没有移出来一样,就是没有开启允许后台播放模式.

10.视频加载影响播放显示菊花转

属性的获取

如何知道当前是否开启画中画

widget.controller!.videoPlayerController!.value.isPipbetterPlayerController.videoPlayerController!.value.isPip

需求

1.如何添加自定义的桥接方法

例如:better_player调用iOS的seetTo方法写的不是很好,(iOS)seetTo之后Flutter方法没有回调,导致拖拽滚动条会出现,回显的问题.所以打算自定义better_player的seetTo带回调的方法桥接到ios那边进行通讯.

2023.12.26iOS经过测试之后发现,seekTo的方法在flutter那边要在它原来的seekTo方法的基础上去改,当时自定义了一个seekTo的方法,发现修改进度条之后再也无法更新进度,监听方法的progress没有收到回调,看下面的图片

因为里面的seekTo方法和监听涉及到其他地方的逻辑,所以最后再原来seekTo的基础上去修改,看下面的图片修改过程就好.

逻辑图:

下面是这个写法:

先写ios这边

填写iOS桥接方法(用于跟flutter那边的调用)

再写flutter那边

video_player_platform_interface

method_channel_video_player

video_player

better_player_controller

自定义触摸板的调用: custom_contaol_widget

调试成功

解决问题

1.开启画中画,app回到后台暂停播放视频的问题?

问题在于,1.使用ios原生开发画中画功能,app回到后台不会出现,视频暂停的情况.2.better_player在安卓上面开启画中画功能,app回到后台不会出现视频暂停的问题

产生这个原因是因为,当app回到后台的时候,better_player的生命周期会进来下面的方法. state会输出pause,

然后暂停了视频的播放

最后的修改方法, 区别一下,如果开启的画中画功能,而且当前是ios平台就略过不暂停视频.

为什么当时开发的人会这么写呢?

播放视频过程中,程序回到后台就会暂停音乐,逻辑上来说是对的,只是,画中画的时候,就不用停止播放视频而已,

但是为什么不会停止画中画,而安卓就会停止呢?

ios 调用[AVPlayer puase] 会停止app控制器中的播放器,连同画中画的播放器,但是安卓只会停止控制器中的播放器,画中画的就不会,可能性在这里

2.播放过程中,拖拽滚动条出现回显的问题?

回显的原因是当修改进度时,progress输出了一个之前的一个久的值.看下面图片久明白了

原生ios使用AVPlayer开发,我之前写过也遇到过这个问题:Swift 技术 AVPlayer视频播放器(带节点)滚动条(源码)

但是两者的解决办法是不一样的.

iOS的解决办法:

因为ios有进度修改成功的回调方法,等回调设置进度成功之后再去更新进度条就可以了.

self.avPlayerView.player.seek(to: seekTime) { [weak self] (completed) inguard let self = self else { return }}

better_player的解决办法:

解决方案1:(废弃)

废弃的原因是:

event.betterPlayerEventType == BetterPlayerEventType.progress

event.betterPlayerEventType == BetterPlayerEventType.bufferingUpdate

这两个方法回调的信息不准确

利用:

widget.controller!.addEventsListener((BetterPlayerEvent event) {

if (event.betterPlayerEventType == BetterPlayerEventType.progress) 监听播放进度的方法,

else if (event.betterPlayerEventType == BetterPlayerEventType.bufferingUpdate) 监听预加载进度方法

去修改这个问题,实际就是利用,正常播放过程中,预加载的值肯定大于播放的值,如果拖拽了进度,播放的值肯定大于预加载的值,所以当播放的值大于预加载的值,证明拖拽了进度条,由于监听回调播放的值,和预加载的值,不是非常准确,所以废弃了

因为better_player没有封装类似seetTo回调的方法其实可以自己封装(已经封装成功了2023.12.16)因为better_player是调用video_player,video_player是调用桥接接口AVplayer的原生,虽然没有封装,其实会写桥接接口,可以自己写.

但是下面使用另外一个办法来解决:利用设置进度成功之后,预加载的监听方法会不停回到预加载进度.所以:

预加载进度的时间>当前播放时间 允许更新进度条,

预加载进度的时间 [weak self] (completed) inguard let self = self else { return }}

上一篇:Win10触屏功能关闭指南,轻松解决操作难题!

下一篇:为什么微信突然发不了语音?