flutter開(kāi)發(fā)實(shí)戰(zhàn)-video_player插件播放抖音直播實(shí)現(xiàn)(僅限Android端)
在之前的開(kāi)發(fā)過(guò)程中,遇到video_player播放視頻,通過(guò)查看video_player插件描述,可以看到video_player在Android端使用exoplayer,在iOS端使用的是AVPlayer。由于iOS的AVPlayer不支持flv、m3u8格式的直播,這里video_player播放抖音直播僅僅在Android有效,在iOS端,如果需要播放抖音直播,可以使用fijkplayer插件進(jìn)行播放,由于fijkplayer使用的是ijkplayer,可以播放flv、m3u8格式的直播。
一、引入
在pubspec.yaml中引入video_player
# 播放器
video_player: ^2.7.0
# fijkplayer: ^0.11.0
二、實(shí)現(xiàn)VideoPlayer的Widget
2.1 在iOS中的設(shè)置
在iOS工程中info.plist添加一下設(shè)置,以便支持Https,HTTP的視頻地址
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
2.2 在Android中的設(shè)置
需要在/android/app/src/main/AndroidManifest.xml文件中添加網(wǎng)絡(luò)權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>
2.3 播放的VideoPlayer
使用video_player插件,需要使用VideoPlayerController來(lái)控制播放、暫停、添加監(jiān)聽(tīng)
初始化后添加監(jiān)聽(tīng),來(lái)獲取VideoPlayerController中的Value值,可以看到一些狀態(tài)。例如
VideoPlayerValue(duration: 0:00:00.001000, size: Size(1280.0, 720.0), position: 0:32:14.877000, caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: ), captionOffset: 0:00:00.000000, buffered: [DurationRange(start: 0:00:00.000000, end: 0:32:17.868000)], isInitialized: true, isPlaying: true, isLooping: false, isBuffering: false, volume: 1.0, playbackSpeed: 1.0, errorDescription: null, isCompleted: false)
添加監(jiān)聽(tīng)
// 添加監(jiān)聽(tīng)
void addListener() {
if (_controller != null) {
_controller!.addListener(videoListenerCallback);
}
}
移除監(jiān)聽(tīng)
// 移除監(jiān)聽(tīng)
void removeListener() {
if (_controller != null) {
_controller!.removeListener(videoListenerCallback);
}
}
監(jiān)聽(tīng)的callback回調(diào)
void videoListenerCallback() {
// 監(jiān)聽(tīng)結(jié)果
if (_controller != null) {
if (_controller!.value.hasError) {
// 出現(xiàn)錯(cuò)誤
setState(() {});
}
if (_controller!.value.isCompleted) {
// 直播完成
setState(() {});
}
if (_controller!.value.isBuffering) {
// 正在buffer
}
if (_controller!.value.hasError || _controller!.value.isCompleted) {
// 是否處于錯(cuò)誤狀態(tài) 或者 播放完成
if (widget.liveController.onOutLinkPlayerCompleted != null) {
widget.liveController.onOutLinkPlayerCompleted!();
}
}
if (_controller!.value.hasError == false) {
// 可播放,隱藏封面
if (widget.liveController.onOutLinkPlayerCanPlay != null) {
widget.liveController.onOutLinkPlayerCanPlay!();
}
}
}
}
播放
Future<void> play() async {
if (_controller != null) {
await _controller?.play();
}
}
暫停
Future<void> play() async {
if (_controller != null) {
await _controller?.pause();
}
}
完整代碼如下
// 視頻播放測(cè)試
class VideoPlayerSkeleton extends StatefulWidget {
const VideoPlayerSkeleton({
Key? key,
required this.videoUrl,
required this.isLooping,
this.autoPlay = true,
required this.width,
required this.height,
}) : super(key: key);
final String videoUrl;
final bool isLooping;
final bool autoPlay;
final double width;
final double height;
State<VideoPlayerSkeleton> createState() => _VideoPlayerSkeletonState();
}
class _VideoPlayerSkeletonState extends State<VideoPlayerSkeleton> {
VideoPlayerController? _controller;
void initState() {
super.initState();
videoPlay();
print("_VideoPlayerSkeletonState videoUrl:${widget.videoUrl}");
}
// 添加監(jiān)聽(tīng)
void addListener() {
if (_controller != null) {
_controller!.addListener(videoListenerCallback);
}
}
void videoListenerCallback() {
// 監(jiān)聽(tīng)結(jié)果
if (_controller != null) {
if (_controller!.value.hasError) {
// 出現(xiàn)錯(cuò)誤
setState(() {});
}
if (_controller!.value.isCompleted) {
// 直播完成
setState(() {});
}
if (_controller!.value.isBuffering) {
// 正在buffer
}
}
}
// 移除監(jiān)聽(tīng)
void removeListener() {
if (_controller != null) {
_controller!.removeListener(videoListenerCallback);
}
}
// 播放視頻
Future<void> videoPlay() async {
_controller?.dispose();
_controller = VideoPlayerController.networkUrl(
Uri.parse(widget.videoUrl),
videoPlayerOptions: VideoPlayerOptions(
mixWithOthers: true,
allowBackgroundPlayback: false,
),
);
addListener();
await _controller?.initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
}).catchError((error) {
// 是否處于錯(cuò)誤狀態(tài) 或者 播放完成
if (widget.liveController.onOutLinkPlayerCompleted != null) {
widget.liveController.onOutLinkPlayerCompleted!();
}
}).whenComplete(() {
// print('checkAnimationTimeout whenComplete');
});
await _controller!.setLooping(widget.isLooping);
if (widget.autoPlay) {
await _controller?.play();
} else {
await _controller?.pause();
}
}
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
color: Colors.black87,
child: Stack(
alignment: Alignment.center,
children: [
buildVideoPlayer(context),
buildStateIntro(context),
],
),
);
}
// 播放視頻
Widget buildVideoPlayer(BuildContext context) {
if (_controller != null && _controller!.value.isInitialized) {
return AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
);
}
return Container();
}
// 播放過(guò)程中出現(xiàn)error
Widget buildStateIntro(BuildContext context) {
if (_controller != null) {
String title = "";
String message = "";
bool showIntro = false;
if (_controller!.value.hasError) {
showIntro = true;
title = "播放出現(xiàn)錯(cuò)誤";
message = _controller!.value.errorDescription ?? "";
} else {
if (_controller!.value.isCompleted) {
showIntro = true;
title = "播放結(jié)束";
}
}
if (showIntro) {
return Container(
padding: EdgeInsets.symmetric(vertical: 50.r, horizontal: 50.r),
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: Container()),
Text(
title,
textAlign: TextAlign.center,
softWrap: true,
style: TextStyle(
fontSize: 28.r,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: Colors.white,
decoration: TextDecoration.none,
),
),
SizedBox(
height: 25.r,
),
Text(
message,
textAlign: TextAlign.center,
softWrap: true,
style: TextStyle(
fontSize: 22.r,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: Colors.white,
decoration: TextDecoration.none,
),
),
Expanded(child: Container()),
],
),
);
}
}
return Container();
}
void dispose() {
// TODO: implement dispose
removeListener();
_controller?.dispose();
super.dispose();
}
}
三、從抖音網(wǎng)站上找到直播地址
由于使用抖音播放地址,這里簡(jiǎn)單描述一下從抖音網(wǎng)站上找到直播的flv地址。
進(jìn)入抖音直播間,在網(wǎng)頁(yè)點(diǎn)擊鼠標(biāo)右鍵,看到檢查。
https://live.douyin.com/567752440034
找到網(wǎng)絡(luò),刷新頁(yè)面,可以看到stream的一條,
復(fù)制地址即可,使用該地址播放直播
https://pull-hs-spe-f5.douyincdn.com/fantasy/stream-728687306789918920718_sd.flv?_neptune_token=MIGlBAxGexWdmRAYAAGs67QEgYIZi9nqbdY3bbfeK9dCVFBnlFTJNF1WNGRZ3AVrQ1ixrE_54JzkGsfuBjGER_2RhP5Qy_GzELSQuct4bK5aktJ2P2xnNznJG87KKhybkeCuefBAkOCI9Tx8eA1mz2GcmfcfqFNeR8DFPDcbzFp_sKyyJRnytmILegqrqjcjxgW04GYwBBDMFIKjhmF1jpi96O53wH7v&expire=1696731973&sign=38f51d46dcd5828fdbc212372bbb3522&volcSecret=38f51d46dcd5828fdbc212372bbb3522&volcTime=1696731973
四、查看直播結(jié)果
之后,我們將地址復(fù)制到VideoPlayerSkeleton中,運(yùn)行后,可以看到播放的效果
注意
:直接在Container上設(shè)置大小后,child是AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
);
會(huì)出現(xiàn)畫(huà)面變形,可以使用Stack嵌套一下。
五、小結(jié)
flutter開(kāi)發(fā)實(shí)戰(zhàn)-video_player插件播放抖音直播實(shí)現(xiàn)(僅限Android端)。描述可能不是特別準(zhǔn)確,請(qǐng)見(jiàn)諒。
https://blog.csdn.net/gloryFlow/article/details/133634186文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-743226.html
學(xué)習(xí)記錄,每天不停進(jìn)步。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-743226.html
到了這里,關(guān)于flutter開(kāi)發(fā)實(shí)戰(zhàn)-video_player插件播放抖音直播實(shí)現(xiàn)(僅限Android端)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!