flutter开发实战-ijkplayer视频播放器功能
使用better_player播放器进行播放视频时候,在Android上会出现解码失败的问题,better_player使用的是video_player,video_player很多视频无法解码。最终采用ijkplayer播放器插件,在flutter上使用fijkplayer插件。
在使用fijkplayer前可以先看下https://fijkplayer.befovy.com/docs/zh/fijkplayer-api.html
在工程的pubspec.yaml中引入插件
fijkplayer: ^0.11.0
fijkPlayer 就是对 native C 层 ijkplayer 的一个 dart 包装,接口都保持一致。 FijkPlayer 处理所有播放相关的工作,实际工作都是由 native C 层 ijkplayer 完成,包含检查 dataSource 中的媒体信息,打开解码器和解码线程、打开音频输出设备、将解码后数据输出给音频设备或显示设备。
使用fijkplayer,这里创建了IJKVideoPlayer来嵌套一下FijkView,使用IJKVideoPlayerController来控制常用功能操作
IJKVideoPlayerController如下
import 'dart:async';
class IJKVideoPlayerController {
FutureOr Function()? stop;
FutureOr Function()? pause;
FutureOr Function()? play;
FutureOr Function(int msec)? seekTo;
FutureOr Function(double volume)? setVolume;
FutureOr Function(double speed)? setSpeed;
FutureOr Function(int loopCount)? setLoop;
FutureOr Function()? isPlaying;
}
IJKVideoPlayerController来控制停止、暂停、播放、seek、设置音量、设置播放速率、设置循环次数、获取是否在播放中等。
void play() {
if (videoPlayerController.play != null) {
videoPlayerController.play!.call();
}
}
void pause() {
if (videoPlayerController.pause != null) {
videoPlayerController.pause!.call();
}
}
void stop() {
if (videoPlayerController.stop != null) {
videoPlayerController.stop!.call();
}
}
void seekTo(int msec) {
if (videoPlayerController.seekTo != null) {
videoPlayerController.seekTo!.call(msec);
}
}
void setVolume(double volume) {
if (videoPlayerController.setVolume != null) {
videoPlayerController.setVolume!.call(volume);
}
}
void setSpeed(double speed) {
if (videoPlayerController.setSpeed != null) {
videoPlayerController.setSpeed!.call(speed);
}
}
void setLoop(int loopCount) {
if (videoPlayerController.setLoop != null) {
videoPlayerController.setLoop!.call(loopCount);
}
}
Future isPlaying() async {
if (videoPlayerController.isPlaying != null) {
bool videoIsPlaying = await videoPlayerController.isPlaying!.call();
return videoIsPlaying;
}
return Future.value(null);
}
在设置播放器的时候,需要设置source类型。fijkplayer提供了两种方式,一种是本地工程文件、一种是网络视频地址。
/// usage
/// autoPlay 为 true 时等同于连续调用 setDataSource、prepareAsync、start
fplayer.setDataSource("http://samplevideo.com/sample.flv", autoPlay: true);
/// pubspec.yml 中需要指定assets 内容
/// assets:
/// - assets/butterfly.mp4
///
/// scheme 是 `asset`, `://` 是 scheme 分隔符, `/` 是路径起始符号
fplayer.setDataSource("asset:///assets/butterfly.mp4", autoPlay: true);
在setDataSource还有autoPlay(自动播放),showCover(是否显示视频封面,视频默认获取第一帧作为视频封面)
在fijkplayer中,使用FijkView来显示视频。
FijkView({
required this.player,
this.width,
this.height,
this.fit = FijkFit.contain,
this.fsFit = FijkFit.contain,
this.panelBuilder = defaultFijkPanelBuilder,
this.color = const Color(0xFF607D8B),
this.cover,
this.fs = true,
this.onDispose,
});
可以设置显示fit、全屏的fit、背景颜色color、封面图(设置之后会显示在视频播放的上面)、是否全屏等。
在这里我们如果需要自定义样式,可以替换掉panelBuilder。
在这里我们如果需要自定义样式,可以替换掉panelBuilder。我们自定义一个IJKVideoPanel,这个大部分代码来源default,这里调整了部分样式。
IJKVideoPanel完整代码如下
import 'dart:async';
import 'dart:math';
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';
class IJKVideoPanel extends StatefulWidget {
const IJKVideoPanel({
super.key,
required this.player,
required this.buildContext,
required this.viewSize,
required this.texturePos,
});
final FijkPlayer player;
final BuildContext buildContext;
final Size viewSize;
final Rect texturePos;
@override
State createState() => _IJKVideoPanelState();
}
class _IJKVideoPanelState extends State {
FijkPlayer get player => widget.player;
Duration _duration = Duration();
Duration _currentPos = Duration();
Duration _bufferPos = Duration();
bool _playing = false;
bool _prepared = false;
String? _exception;
// bool _buffering = false;
double _seekPos = -1.0;
StreamSubscription? _currentPosSubs;
StreamSubscription? _bufferPosSubs;
//StreamSubscription _bufferingSubs;
Timer? _hideTimer;
bool _hideStuff = true;
double _volume = 1.0;
final barHeight = 40.0;
@override
void initState() {
super.initState();
_duration = player.value.duration;
_currentPos = player.currentPos;
_bufferPos = player.bufferPos;
_prepared = player.state.index >= FijkState.prepared.index;
_playing = player.state == FijkState.started;
_exception = player.value.exception.message;
// _buffering = player.isBuffering;
player.addListener(_playerValueChanged);
_currentPosSubs = player.onCurrentPosUpdate.listen((v) {
setState(() {
_currentPos = v;
});
});
_bufferPosSubs = player.onBufferPosUpdate.listen((v) {
setState(() {
_bufferPos = v;
});
});
}
void _playerValueChanged() {
FijkValue value = player.value;
if (value.duration != _duration) {
setState(() {
_duration = value.duration;
});
}
bool playing = (value.state == FijkState.started);
bool prepared = value.prepared;
String? exception = value.exception.message;
if (playing != _playing ||
prepared != _prepared ||
exception != _exception) {
setState(() {
_playing = playing;
_prepared = prepared;
_exception = exception;
});
}
}
void _playOrPause() {
if (_playing == true) {
player.pause();
} else {
player.start();
}
}
@override
void dispose() {
super.dispose();
_hideTimer?.cancel();
player.removeListener(_playerValueChanged);
_currentPosSubs?.cancel();
_bufferPosSubs?.cancel();
}
void _startHideTimer() {
_hideTimer?.cancel();
_hideTimer = Timer(const Duration(seconds: 3), () {
setState(() {
_hideStuff = true;
});
});
}
void _cancelAndRestartTimer() {
if (_hideStuff == true) {
_startHideTimer();
}
setState(() {
_hideStuff = !_hideStuff;
});
}
Widget _buildVolumeButton() {
IconData iconData;
if (_volume <= 0) {
iconData = Icons.volume_off;
} else {
iconData = Icons.volume_up;
}
return IconButton(
icon: Icon(iconData, color: Colors.white),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
onPressed: () {
setState(() {
_volume = _volume > 0 ? 0.0 : 1.0;
player.setVolume(_volume);
});
},
);
}
AnimatedOpacity _buildBottomBar(BuildContext context) {
double duration = _duration.inMilliseconds.toDouble();
double currentValue =
_seekPos > 0 ? _seekPos : _currentPos.inMilliseconds.toDouble();
currentValue = min(currentValue, duration);
currentValue = max(currentValue, 0);
return AnimatedOpacity(
opacity: _hideStuff ? 0.0 : 0.8,
duration: Duration(milliseconds: 400),
child: Container(
height: barHeight,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.transparent, Colors.black45],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Row(
children: [
_buildVolumeButton(),
Padding(
padding: EdgeInsets.only(right: 5.0, left: 5),
child: Text(
'${_duration2String(_currentPos)}',
style: TextStyle(fontSize: 14.0, color: Colors.white),
),
),
_duration.inMilliseconds == 0
? Expanded(child: Center())
: Expanded(
child: Padding(
padding: EdgeInsets.only(right: 0, left: 0),
child: FijkSlider(
value: currentValue,
cacheValue: _bufferPos.inMilliseconds.toDouble(),
min: 0.0,
max: duration,
onChanged: (v) {
_startHideTimer();
setState(() {
_seekPos = v;
});
},
onChangeEnd: (v) {
setState(() {
player.seekTo(v.toInt());
print("seek to $v");
_currentPos =
Duration(milliseconds: _seekPos.toInt());
_seekPos = -1;
});
},
),
),
),
// duration / position
_duration.inMilliseconds == 0
? Container(child: const Text("LIVE"))
: Padding(
padding: EdgeInsets.only(right: 5.0, left: 5),
child: Text(
'${_duration2String(_duration)}',
style: TextStyle(fontSize: 14.0, color: Colors.white),
),
),
// IconButton(
// icon: Icon(widget.player.value.fullScreen
// ? Icons.fullscreen_exit
// : Icons.fullscreen),
// padding: EdgeInsets.only(left: 10.0, right: 10.0),
// // color: Colors.transparent,
// onPressed: () {
// widget.player.value.fullScreen
// ? player.exitFullScreen()
// : player.enterFullScreen();
// },
// )
//
],
),
),
);
}
@override
Widget build(BuildContext context) {
// Rect rect = player.value.fullScreen
// ? Rect.fromLTWH(0, 0, widget.viewSize.width, widget.viewSize.height)
// : Rect.fromLTRB(
// max(0.0, widget.texturePos.left),
// max(0.0, widget.texturePos.top),
// min(widget.viewSize.width, widget.texturePos.right),
// min(widget.viewSize.height, widget.texturePos.bottom));
Rect rect =
Rect.fromLTWH(0, 0, widget.viewSize.width, widget.viewSize.height);
return Positioned.fromRect(
rect: rect,
child: GestureDetector(
onTap: _cancelAndRestartTimer,
child: AbsorbPointer(
absorbing: _hideStuff,
child: Column(
children: [
Container(height: barHeight),
Expanded(
child: GestureDetector(
onTap: () {
_cancelAndRestartTimer();
},
child: Container(
color: Colors.transparent,
height: double.infinity,
width: double.infinity,
child: Center(
child: _exception != null
? Text(
_exception!,
style: TextStyle(
color: Colors.white,
fontSize: 25,
),
)
: (_prepared ||
player.state == FijkState.initialized)
? AnimatedOpacity(
opacity: _hideStuff ? 0.0 : 0.85,
duration: Duration(milliseconds: 400),
child: IconButton(
iconSize: barHeight * 2,
icon: Icon(
_playing
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
size: 44,
),
padding: EdgeInsets.only(
left: 10.0, right: 10.0),
onPressed: _playOrPause))
: SizedBox(
width: barHeight * 1.5,
height: barHeight * 1.5,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(
Colors.white)),
)),
),
),
),
_buildBottomBar(context),
],
),
),
),
);
}
}
String _duration2String(Duration duration) {
if (duration.inMilliseconds < 0) return "-: negtive";
String twoDigits(int n) {
if (n >= 10) return "$n";
return "0$n";
}
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
int inHours = duration.inHours;
return inHours > 0
? "$inHours:$twoDigitMinutes:$twoDigitSeconds"
: "$twoDigitMinutes:$twoDigitSeconds";
}
在使用时候,使用了IJKVideoPlayer来封装了一层FijkView。
在IJKVideoPlayer中设置了videoPlayerController控制播放的操作 如停止、暂停、播放、seek、设置音量、设置播放速率、设置循环次数、获取是否在播放中。
IJKVideoPlayer完整代码如下
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_demolab/ijk_player/ijk_video_panel.dart';
import 'ijk_video_player_controller.dart';
/// usage
/// autoPlay 为 true 时等同于连续调用 setDataSource、prepareAsync、start
/// fplayer.setDataSource("http://samplevideo.com/sample.flv", autoPlay: true);
///
/// 设置本地资源作为播放源,
/// pubspec.yml 中需要指定assets 内容
/// assets:
/// - assets/butterfly.mp4
///
/// scheme 是 `asset`, `://` 是 scheme 分隔符, `/` 是路径起始符号
/// fplayer.setDataSource("asset:///assets/butterfly.mp4", autoPlay: true);
class IJKVideoPlayer extends StatefulWidget {
const IJKVideoPlayer({
super.key,
required this.path,
this.autoPlay = false,
this.showCover = true,
this.fit = FijkFit.contain,
this.cover,
this.color = Colors.black,
this.width,
this.height,
this.videoPlayerController,
});
final double? width;
final double? height;
final String path;
final bool autoPlay;
final bool showCover;
final FijkFit fit;
final Widget? cover;
final Color color;
final IJKVideoPlayerController? videoPlayerController;
@override
State createState() => _IJKVideoPlayerState();
}
class _IJKVideoPlayerState extends State {
final FijkPlayer player = FijkPlayer();
@override
void initState() {
super.initState();
player.setDataSource(
widget.path,
autoPlay: widget.autoPlay,
showCover: widget.showCover,
);
addVideoPlayerFun();
}
void addVideoPlayerFun() {
if (widget.videoPlayerController != null) {
widget.videoPlayerController!.play = () {
// 触发播放
player.start();
};
widget.videoPlayerController!.stop = () {
// 触发停止
player.stop();
};
widget.videoPlayerController!.pause = () {
// 触发暂停
player.pause();
};
widget.videoPlayerController!.setLoop = (int loopCount) {
// 触发setLoop
if (loopCount < 0) {
loopCount = 1;
}
player.setLoop(loopCount);
};
widget.videoPlayerController!.seekTo = (int msec) {
// 触发seek
if (msec < 0) {
msec = 0;
}
player.seekTo(msec);
};
widget.videoPlayerController!.setVolume = (double volume) {
// 触发setVolume
if (volume < 0.0) {
volume = 0.0;
}
player.setVolume(volume);
};
widget.videoPlayerController!.setSpeed = (double speed) {
// 触发setSpeed
if (speed < 0.0) {
speed = 1.0;
}
player.setSpeed(speed);
};
widget.videoPlayerController!.isPlaying = () {
// 触发setVolume
if (FijkState.started == player.state) {
return true;
} else {
return false;
}
};
}
}
@override
void dispose() {
super.dispose();
player.release();
}
void onIJKDispose(FijkData fijkData) {}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Stack(
alignment: Alignment.center,
children: [
widget.cover != null ? widget.cover! : Container(),
FijkView(
width: widget.width,
height: widget.height,
player: player,
fit: widget.fit,
fsFit: widget.fit,
color: widget.color,
onDispose: onIJKDispose,
panelBuilder: (FijkPlayer player, FijkData data,
BuildContext context, Size viewSize, Rect texturePos) {
return IJKVideoPanel(
player: player,
buildContext: context,
viewSize: viewSize,
texturePos: texturePos,
);
},
),
],
),
);
}
}
这里我创建了一个IJKVideoPage来使用IJKVideoPlayer视频播放,IJKVideoPlayer中需要path与videoPlayerController
IJKVideoPage完整代码如下
import 'dart:async';
import 'package:flutter/material.dart';
import 'ijk_player/ijk_video_player.dart';
import 'ijk_player/ijk_video_player_controller.dart';
class IJKVideoPage extends StatefulWidget {
const IJKVideoPage({
super.key,
required this.url,
});
final String url;
@override
State createState() => _IJKVideoPageState();
}
class _IJKVideoPageState extends State {
final IJKVideoPlayerController videoPlayerController =
IJKVideoPlayerController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(title: Text("Fijkplayer Example")),
body: Center(
child: Container(
width: size.width,
height: size.width * 9.0 / 16.0,
alignment: Alignment.center,
child: IJKVideoPlayer(
path: widget.url,
videoPlayerController: videoPlayerController,
color: Colors.black,
),
),
),
);
}
@override
void dispose() {
super.dispose();
}
}
如果外面的页面跳转到播放页面,需要设置url
void testIJKVideoPage(BuildContext context) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return IJKVideoPage(
url: "https://vd2.bdstatic.com/mda-maif0tt1rirqp27q/540p/h264_cae/1611052585/mda-maif0tt1rirqp27q.mp4");
}));
}
https://brucegwo.blog.csdn.net/article/details/136024588
flutter开发实战-ijkplayer视频播放器功能
学习记录,每天不停进步。