一、依赖
因为fijkplayer自带的skin并不好看,没有手势拖动快进、快退,所以需要自己定义一套皮肤给 fijkplayer。同时,fijkplayer_skin只是一套皮肤,并不是播放器,所以 fjikplayer 存在的问题,这里 fijkplayer_skin 一样存在。
dependencies:
flutter:
sdk: flutter
# fijkplayer_skin
fijkplayer_skin:
path: ./component/fijkplayer_skin
fijkplayer: ^0.10.0
wakelock: ^0.5.2
上面是将fijkplayer_skin作为一个 package来引入,其实也可以直接依赖pub上的插件,如下所示:
dependencies:
flutter:
sdk: flutter
# fijkplayer_skin
#fijkplayer_skin:
# path: ./component/fijkplayer_skin
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
fijkplayer_skin: ^2.2.8
fijkplayer: ^0.10.0
wakelock: ^0.5.2
二、简单页面
import 'package:fijkplayer/fijkplayer.dart';
import 'package:fijkplayer_skin/fijkplayer_skin.dart';
import 'package:fijkplayer_skin/schema.dart' show VideoSourceFormat;
import 'package:flutter/material.dart';
// show 表示只导出 VideoSourceFormat 类
class SimpleVideoPage extends StatefulWidget {
const SimpleVideoPage({Key? key}) : super(key: key);
@override
_SimpleVideoPageState createState() => _SimpleVideoPageState();
}
class _SimpleVideoPageState extends State<SimpleVideoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: VideoDetailPage(),
),
);
}
}
//定制UI配置项
class PlayerShowConfig implements ShowConfigAbs {
//自动播放下一个视频
@override
bool autoNext = false;
//底部进度条
@override
bool bottomPro = true;
//剧集显示
@override
bool drawerBtn = true;
//进入页面自动播放
@override
bool isAutoPlay = false;
//是否有锁
@override
bool lockBtn = true;
//下一个按钮
@override
bool nextBtn = false;
//显示封面 但是貌似没有效果
@override
bool showCover = false;
//播放速度 1.0倍 2.0倍
@override
bool speedBtn = true;
@override
bool stateAuto = true;
//顶部标题栏
@override
bool topBar = true;
}
class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key);
@override
_VideoDetailPageState createState() => _VideoDetailPageState();
}
class _VideoDetailPageState extends State<VideoDetailPage>
with SingleTickerProviderStateMixin {
final FijkPlayer player = FijkPlayer();
Map<String, List<Map<String, dynamic>>> videoList = {
"video":[
{
"name":"线路资源一",
"list":[
{
"url": "https://media.w3.org/2010/05/sintel/trailer.mp4",
"name": "视频名称"
},
{
"url": "https://v10.dious.cc/20211009/nONG14sk/index.m3u8",
"name": "视频名称"
},
]
}
]
};
VideoSourceFormat? _videoSourceTabs;
int _currTabIndex = 0;
int _currActiveIdx = 0;
ShowConfigAbs vConfig = PlayerShowConfig();
@override
void dispose() {
player.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
//格式化json转对象
_videoSourceTabs = VideoSourceFormat.fromJson(videoList);
//这句不能省,必须有
speed = 1.0;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
FijkView(
height: 260,
color: Colors.black,
fit: FijkFit.cover,
player: player,
panelBuilder: (
FijkPlayer player,
FijkData data,
BuildContext context,
Size viewSize,
Rect texturePos){
return CustomFijkPanel(
player: player,
viewSize: viewSize,
texturePos: texturePos,
pageContent: context,
//标题 当前页面顶部的标题部分
playerTitle: '顶部视频标题',
//视频显示的配置
showConfig: vConfig,
//json格式化后的视频数据
videoFormat: _videoSourceTabs,
//当前视频源 资源一 资源二等
curTabIdx: _currTabIndex,
//当前视频 高清 标清 流畅等
curActiveIdx: _currActiveIdx
);
},
)
],
);
}
}
效果如下:
如上可以看到封装的效果还是可以的,但是就是剧集这块不能自定义,如果需要自定义,那么就需要下载源码,做成组件 package,然后修改剧集部分代码。下面看下更为复杂写法。
三、复杂页面
代码的代码是仿照常见的长视频的UI来实现的,可以自己选择播放的路线和集数。
import 'package:fijkplayer/fijkplayer.dart';
import 'package:fijkplayer_skin/fijkplayer_skin.dart';
import 'package:fijkplayer_skin/schema.dart';
import 'package:flutter/material.dart';
//相对比较复杂的视频页面
class ComplexVideoPage extends StatefulWidget {
const ComplexVideoPage({Key? key}) : super(key: key);
@override
_ComplexVideoPageState createState() => _ComplexVideoPageState();
}
class _ComplexVideoPageState extends State<ComplexVideoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: VideoDetailPage(),
),
);
}
}
class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key);
@override
_VideoDetailPageState createState() => _VideoDetailPageState();
}
class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin {
final FijkPlayer player = FijkPlayer();
Map<String, List<Map<String, dynamic>>> videoList = {
"video":[
{
"name": "天空资源",
"list": [
{
"url": "https://static.smartisanos.cn/common/video/t1-ui.mp4",
"name": "锤子UI-1",
},
{
"url": "https://static.smartisanos.cn/common/video/video-jgpro.mp4",
"name": "锤子UI-2",
},
{
"url": "https://v-cdn.zjol.com.cn/280443.mp4",
"name": "其他",
},
]
},
{
"name": "天空资源",
"list": [
{
"url": "https://n1.szjal.cn/20210428/lsNZ6QAL/index.m3u8",
"name": "综艺",
},
{
"url": "https://static.smartisanos.cn/common/video/t1-ui.mp4",
"name": "锤子1",
},
{
"url": "https://static.smartisanos.cn/common/video/video-jgpro.mp4",
"name": "锤子2",
}
]
},
]
};
VideoSourceFormat? _videoSourceTabs;
late TabController _controller;
int _currTabIndex = 0;
int _currActiveIdx = 0;
ShowConfigAbs vConfig = PlayerShowConfig();
@override
void dispose() {
player.dispose();
_controller.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
//格式化json对象
_videoSourceTabs = VideoSourceFormat.fromJson(videoList);
//初始化TabController
_controller = TabController(
length: _videoSourceTabs!.video!.length, //length 表示几个视频来源 比如 百度云 迅雷加速
vsync: this); //每个资源包含了具体的剧集内容 name和url
//这句不能省,必须有
speed = 1.0;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
FijkView(
height: 260,
color: Colors.black,
fit: FijkFit.cover,
player: player,
panelBuilder: (FijkPlayer player,
FijkData data, BuildContext context, Size viewSize, Rect texturePos){
return CustomFijkPanel(
player: player,
viewSize: viewSize,
texturePos: texturePos,
playerTitle: '标题',
showConfig: vConfig,
videoFormat: _videoSourceTabs,
curTabIdx: _currTabIndex,
curActiveIdx: _currActiveIdx
);
},
),
Container(
height: 300,
child: buildPlayDrawer(),
),
Container(
child: Text("当前 ${_currTabIndex.toString()} 当前 activeIndex ${_currActiveIdx.toString()}"),
),
],
);
}
//build 剧集
buildPlayDrawer() {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(24),
child: AppBar(
backgroundColor: Colors.black,
automaticallyImplyLeading: false,
primary: false,
elevation: 0,
title: TabBar(
tabs: _videoSourceTabs!.video!
.map((e) => Tab(text: e!.name!,))
.toList(),
isScrollable: true,
controller: _controller,
),
),
),
body: Container(
color: Colors.black87,
child: TabBarView(
controller: _controller,
children: createTabContentList(),
),
),
);
}
createTabContentList() {
List<Widget> list = []; //Page
_videoSourceTabs!.video!.asMap().keys.forEach((int tabIndex) { //资源
List<Widget> playListButtons = _videoSourceTabs!.video![tabIndex]!.list!.asMap().keys.map((int activeIndex){
//剧集
return Padding(
padding: EdgeInsets.only(left: 5, right: 5),
child: ElevatedButton(
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)
)
),
elevation: MaterialStateProperty.all(0),
backgroundColor: MaterialStateProperty.all(
tabIndex == _currTabIndex && activeIndex == _currActiveIdx
? Colors.red //选中是红色的
: Colors.blue
)
),
onPressed: () async {
setState(() {
_currTabIndex = tabIndex;
_currActiveIdx = activeIndex;
});
String nextVideoUrl =
_videoSourceTabs!.video![tabIndex]!.list![activeIndex]!.url!;
//切换资源
//如果不是自动开始播放,那么先执行 stop
if (player.value.state == FijkState.completed){
await player.stop();
}
await player.reset().then((_) async{
player.setDataSource(nextVideoUrl, autoPlay: true);
});
},
child: Text(
_videoSourceTabs!.video![tabIndex]!.list![activeIndex]!.name!,
style: TextStyle(color: Colors.white),
),
),
);
}).toList();
list.add(
SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(left: 5, right: 5),
child: Wrap(
direction: Axis.horizontal,
children: playListButtons,
),
),
)
);
});
return list;
}
}
//定制UI配置项
class PlayerShowConfig implements ShowConfigAbs {
//自动播放下一个视频
@override
bool autoNext = false;
//底部进度条
@override
bool bottomPro = true;
//剧集显示
@override
bool drawerBtn = true;
//进入页面自动播放
@override
bool isAutoPlay = false;
//是否有锁
@override
bool lockBtn = true;
//下一个按钮
@override
bool nextBtn = false;
//显示封面 但是貌似没有效果
@override
bool showCover = false;
//播放速度 1.0倍 2.0倍
@override
bool speedBtn = true;
@override
bool stateAuto = true;
//顶部标题栏
@override
bool topBar = true;
}
效果:
四、总结
利用第三方插件可以很轻松的实现比较完美的视频播放功能,但是经过实践发现 fijkplayer 在 iOS上播放有问题,只能播放声音,但是不能显示视频画面内容,暂时不清楚具体的原因是什么,后续如果有时间会继续跟踪这个问题的修复情况。
参考: