Flutter组件封装:视频播放组件全局封装
一、需求来源
最近要开发一个在线视频播放的功能,每次实时获取播放,有些卡顿和初始化慢的问题,就随手优化一下。
二、使用示例
1、数据源
class VideoDetailsProvider extends ChangeNotifier {
/// 当前播放中的
final _videoModelController = StreamController<VideoDetailModel?>.broadcast();
/// 当前播放中 Stream<VideoDetailModel>
Stream<VideoDetailModel?> get videoModelStream => _videoModelController.stream;
/// 点击(model不为空时播放,为空时关闭)
sinkModelForVideoPlayer({required VideoDetailModel? model}) {
_videoModelController.add(model);
}
}
2、显示播放器组件
StreamBuilder<VideoDetailModel?>(
stream: provider.videoModelStream,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const SizedBox();
}
final model = snapshot.data;
if (model?.isVideo != true) {
return const SizedBox();
}
return SafeArea(
top: true,
bottom: false,
left: false,
right: false,
child: Container(
decoration: BoxDecoration(
color: Colors.black,
),
child: AppVideoPlayer(
key: Key(model?.url ?? ""),
url: model?.url ?? "",
onFullScreen: (value) async {
if (!value) {
await Future.delayed(const Duration(milliseconds: 300));//等待旋转完成
SystemChromeExt.changeDeviceOrientation(isPortrait: true);
}
},
onClose: () {
DLog.d("close");
provider.sinkModelForVideoPlayer(model: null);
},
),
),
);
},
)
三、源码
1、AppVideoPlayer.dart
//
// AppVideoPlayer.dart
// flutter_templet_project
//
// Created by shang on 2025/12/12 18:11.
// Copyright © 2025/12/12 shang. All rights reserved.
//
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/AppVideoPlayer/AppVideoPlayerService.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';
import 'package:video_player/video_player.dart';
/// 播放器
class AppVideoPlayer extends StatefulWidget {
const AppVideoPlayer({
super.key,
this.controller,
required this.url,
this.autoPlay = true,
this.looping = false,
this.aspectRatio = 16 / 9,
this.isPortrait = true,
this.fullScreenVN,
this.onFullScreen,
this.onClose,
});
final AppVideoPlayerController? controller;
final String url;
final bool autoPlay;
final bool looping;
final double aspectRatio;
/// 设备方向
final bool isPortrait;
final ValueNotifier<bool>? fullScreenVN;
final void Function(bool isFullScreen)? onFullScreen;
final VoidCallback? onClose;
@override
State<AppVideoPlayer> createState() => _AppVideoPlayerState();
}
class _AppVideoPlayerState extends State<AppVideoPlayer> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
VideoPlayerController? _videoController;
ChewieController? _chewieController;
Duration position = Duration.zero;
@override
void dispose() {
widget.controller?._detach(this);
WidgetsBinding.instance.removeObserver(this);
_onClose();
// 不销毁 VideoPlayerController,让全局复用
// _chewieController?.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
widget.controller?._attach(this);
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
initPlayer();
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
{
_chewieController?.pause();
}
break;
case AppLifecycleState.detached:
break;
case AppLifecycleState.resumed:
{
_chewieController?.play();
}
break;
}
}
Future<void> initPlayer() async {
// DLog.d(widget.url.split("/").last);
assert(widget.url.startsWith("http"), "url 错误");
_videoController = await AppVideoPlayerService.instance.getController(widget.url);
_chewieController?.dispose();
_chewieController = ChewieController(
videoPlayerController: _videoController!,
autoPlay: widget.autoPlay,
looping: widget.looping,
aspectRatio: widget.aspectRatio,
autoInitialize: true,
allowFullScreen: true,
allowMuting: false,
showControlsOnInitialize: false,
// customControls: const AppVideoControls(),
);
_chewieController!.addListener(() {
final isFullScreen = _chewieController!.isFullScreen;
widget.fullScreenVN?.value = isFullScreen;
widget.onFullScreen?.call(isFullScreen);
if (isFullScreen) {
DLog.d("进入全屏");
} else {
DLog.d("退出全屏");
}
});
if (mounted) {
setState(() {});
}
}
Future<void> _onClose() async {
if (_videoController?.value.isPlaying == true) {
_videoController?.pause();
}
}
@override
void didUpdateWidget(covariant AppVideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.url != widget.url) {
initPlayer();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
/// 🔥屏幕横竖切换时 rebuild Chewie,但不 rebuild video
DLog.d([MediaQuery.of(context).orientation]);
// setState(() {});
}
@override
Widget build(BuildContext context) {
super.build(context);
if (widget.url.startsWith("http") != true) {
return const SizedBox();
}
if (_chewieController == null) {
return const Center(child: CircularProgressIndicator());
}
// return Chewie(controller: _chewieController!);
return Stack(
children: [
Positioned.fill(
child: Chewie(
controller: _chewieController!,
),
),
Positioned(
right: 20,
top: 10,
child: Container(
// decoration: BoxDecoration(
// color: Colors.red,
// border: Border.all(color: Colors.blue),
// ),
child: buildCloseBtn(onTap: widget.onClose),
),
),
],
);
}
Widget buildCloseBtn({VoidCallback? onTap}) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
DLog.d("buildCloseBtn");
_onClose();
if (onTap != null) {
onTap();
return;
}
Navigator.pop(context);
},
child: Container(
// padding: EdgeInsets.all(6),
// decoration: BoxDecoration(
// color: Colors.black54,
// shape: BoxShape.circle,
// border: Border.all(color: Colors.blue),
// ),
child: const Icon(Icons.close, color: Colors.white, size: 24),
),
);
}
@override
bool get wantKeepAlive => true;
}
class AppVideoPlayerController {
_AppVideoPlayerState? _anchor;
void _attach(_AppVideoPlayerState anchor) {
_anchor = anchor;
}
void _detach(_AppVideoPlayerState anchor) {
if (_anchor == anchor) {
_anchor = null;
}
}
VideoPlayerController? get videoController {
assert(_anchor != null);
return _anchor!._videoController;
}
ChewieController? get chewieController {
assert(_anchor != null);
return _anchor!._chewieController;
}
}
2、AppVideoPlayerService.dart
//
// AppVideoPlayerService.dart
// flutter_templet_project
//
// Created by shang on 2025/12/12 18:10.
// Copyright © 2025/12/12 shang. All rights reserved.
//
import 'package:flutter_templet_project/extension/extension_local.dart';
import 'package:quiver/collection.dart';
import 'package:video_player/video_player.dart';
/// 视频播放控制器全局管理
class AppVideoPlayerService {
AppVideoPlayerService._();
static final AppVideoPlayerService _instance = AppVideoPlayerService._();
factory AppVideoPlayerService() => _instance;
static AppVideoPlayerService get instance => _instance;
/// 播放器字典
LruMap<String, VideoPlayerController> get controllerMap => _controllerMap;
// final _controllerMap = <String, VideoPlayerController>{};
final _controllerMap = LruMap<String, VideoPlayerController>(maximumSize: 10);
/// 最新使用播放器
VideoPlayerController? current;
/// 有缓存控制器
bool hasCtrl({required String url}) => _controllerMap[url] != null;
/// 是网络视频
static bool isVideo(String? url) {
if (url?.isNotEmpty != true) {
return false;
}
final videoUri = Uri.tryParse(url!);
if (videoUri == null) {
return false;
}
final videoExt = ['.mp4', '.mov', '.avi', '.wmv', '.flv', '.mkv', '.webm'];
final ext = url.toLowerCase();
final result = videoExt.any((e) => ext.endsWith(e));
return result;
}
/// 获取 VideoPlayerController
Future<VideoPlayerController?> getController(String url, {bool isLog = false}) async {
assert(url.startsWith("http"), "必须是视频链接,请检查链接是否合法");
final vc = _controllerMap[url];
if (vc != null) {
current = vc;
if (isLog) {
DLog.d(["缓存: ${vc.hashCode}"]);
}
return vc;
}
final videoUri = Uri.tryParse(url);
if (videoUri == null) {
return null;
}
final ctrl = VideoPlayerController.networkUrl(videoUri);
await ctrl.initialize();
_controllerMap[url] = ctrl;
current = ctrl;
if (isLog) {
DLog.d(["新建: ${_controllerMap[url].hashCode}"]);
}
return ctrl;
}
/// 播放
Future<void> play({required String url, bool onlyOne = true}) async {
await getController(url);
for (final e in _controllerMap.entries) {
if (e.key == url) {
if (!e.value.value.isPlaying) {
e.value.play();
} else {
e.value.pause();
}
} else {
if (onlyOne) {
e.value.pause();
}
}
}
}
/// 暂停所有视频
void pauseAll() {
for (final e in _controllerMap.entries) {
e.value.pause();
}
}
void dispose(String url) {
_controllerMap[url]?.dispose();
_controllerMap.remove(url);
}
void disposeAll() {
for (final c in _controllerMap.values) {
c.dispose();
}
_controllerMap.clear();
}
}
最后、总结
1、播放视频组件和视频列表是分开的,通过监听 VideoPlayerController 保持状态(播放,暂停)一致性,类似网络视频小窗口播放。
2、通过 AppVideoPlayerService 实现全局 VideoPlayerController 缓存,提高初始化加载速度。
3、通过 quiver 的 LruMap 实现最大缓存数控制,防止内存爆炸。