media_proxy 0.0.4
media_proxy: ^0.0.4 copied to clipboard
A Flutter package that provides a local HTTP proxy for caching and prefetching media files to improve playback performance.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_proxy/media_proxy.dart';
// . 进一步的完善建议
// A. 增强 LRU 的鲁棒性 (Directory Integrity)
// * 观察:目前的 LRU 清理是基于 config.json 的。如果文件系统异常导致 config.json 损坏,该目录可能永远不会被清理。
// * 建议:在 cleanupCacheLRU 中增加一个兜底逻辑:如果文件夹中不存在 config.json 且修改时间超过 48 小时,直接视为僵尸缓存进行物理删除。
void main() {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
runApp(const MyApp());
}
/// 打印日志(仅在开启日志时打印)
void _log(String message) {
if (kEnableLogging) {
if (kDebugMode) {
print('[MediaCacheProxy] ${DateTime.now().toIso8601String()} - $message');
}
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Media Proxy Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const ProxyDemoPage(),
);
}
}
class ProxyDemoPage extends StatefulWidget {
const ProxyDemoPage({super.key});
@override
State<ProxyDemoPage> createState() => _ProxyDemoPageState();
}
class _ProxyDemoPageState extends State<ProxyDemoPage> {
late final Player _player;
late final VideoController _controller;
/// 是否初始化完成
bool _isInitialized = false;
/// 状态描述文字
String _status = '正在准备...';
/// 是否正在退出页面(防止退出时手势事件导致崩溃)
bool _isDisposing = false;
/// 下载进度信息
DownloadProgressInfo? _progressInfo;
/// 进度监听的 StreamSubscription
StreamSubscription<DownloadProgressInfo>? _progressSubscription;
/// 进度监听的 StreamSubscription
StreamSubscription<Duration>? _positionSubscription;
// late final Player _player1;
// late final VideoController _controller1;
final _cdnUrl = 'http://vjs.zencdn.net/v/oceans.mp4';
@override
void initState() {
super.initState();
_player = Player();
_controller = VideoController(_player);
_positionSubscription = _controller.player.stream.position.listen((event) {
if (event.inSeconds < 1) {
_log('player - event: $event');
}
});
_init();
}
Future<void> _init() async {
if (!mounted) return;
setState(() {
_status = '初始化播放器...';
});
// 预加载和播放并行执行(不等待预加载完成)
// 这样可以更快开始播放,但分片下载顺序可能受网络竞争影响
MediaCacheProxy.preload(_cdnUrl, segmentCount: 2);
// await _playVideo();
// 启动下载进度监听
_startProgressListener();
}
Future<void> _playVideo() async {
// 代理内部会默认设置最新的url为当前播放的url
MediaCacheProxy.setCurrentPlaying(_cdnUrl);
final proxyUrl = await MediaCacheProxy.getProxyUrl(_cdnUrl);
if (!mounted) return;
setState(() {
_status = '加载媒体资源...';
});
await _player.open(Media(proxyUrl));
if (mounted) {
setState(() {
_isInitialized = true;
_status = '播放中 (代理模式)';
});
}
}
/// 启动下载进度监听
void _startProgressListener() {
_progressSubscription =
MediaDownloadProgressListener.listen(_cdnUrl, intervalMs: 500).listen(
(info) {
if (!mounted || _isDisposing) return;
// 更新 UI
setState(() {
_progressInfo = info;
});
// 打印进度到控制台
_log(
'📊 下载进度: ${info.progressPercent} '
'(${info.downloadedMB} / ${info.totalMB}) '
'分片: ${info.completedSegments}/${info.totalSegments} '
'速度: ${info.speedFormatted ?? "计算中..."}',
);
// 下载完成
if (info.isCompleted) {
_log('✅ 视频下载完成!');
// _playVideo();
}
},
onError: (error) {
_log('❌ 进度监听错误: $error');
},
onDone: () {
_log('📊 进度监听结束');
},
);
}
@override
void dispose() {
_isDisposing = true;
// 取消进度监听
_progressSubscription?.cancel();
_progressSubscription = null;
// 先停止播放,避免在 dispose 过程中还有回调
_player.stop();
_player.dispose();
// 取消进度监听
_positionSubscription?.cancel();
_positionSubscription = null;
MediaCacheProxy.cancelMediaDownload(_cdnUrl);
super.dispose();
}
Future<void> _playOrPause() async {
if (_isDisposing) return;
final playing = _player.state.playing;
if (playing) {
await _player.pause();
} else {
await _player.play();
}
}
/// 处理返回事件,确保播放器正确停止
Future<bool> _onWillPop() async {
if (_isDisposing) return true;
_isDisposing = true;
// 取消进度监听
_progressSubscription?.cancel();
// 先暂停播放器,给手势事件一点时间完成
await _player.pause();
// 短暂延迟,让手势事件处理完成
await Future.delayed(const Duration(milliseconds: 100));
return true;
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final canPop = await _onWillPop();
if (canPop && mounted) {
if (context.mounted) Navigator.of(context).pop();
}
},
child: Scaffold(
appBar: AppBar(title: const Text('边下边播')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 下载进度指示器
_buildProgressArea(),
const SizedBox(height: 10),
if (!_isDisposing)
Stack(
alignment: Alignment.center,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Colors.black,
child: Video(controller: _controller),
),
),
if (!_player.state.playing && _isInitialized)
IconButton(
icon: const Icon(
Icons.play_circle_fill,
size: 64,
color: Colors.white70,
),
onPressed: _playOrPause,
),
],
),
const SizedBox(height: 20),
Text(
'状态: $_status',
style: const TextStyle(fontSize: 14, color: Colors.blueGrey),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Wrap(
spacing: 10,
runSpacing: 10,
alignment: WrapAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _playOrPause,
icon: Icon(
_player.state.playing ? Icons.pause : Icons.play_arrow,
),
label: Text(_player.state.playing ? '暂停' : '播放'),
),
ElevatedButton.icon(
onPressed: () async {
await MediaCacheProxy.clearCache();
setState(() {
_status = '缓存已清除';
_progressInfo = null;
});
_log('🧹 缓存已手动清除');
},
icon: const Icon(Icons.delete_sweep),
label: const Text('清除缓存'),
),
ElevatedButton.icon(
onPressed: () async {
final stats = await MediaCacheProxy.getCacheStats();
setState(() {
_status = '缓存大小: ${stats['totalSizeMB']} MB';
});
},
icon: const Icon(Icons.info_outline),
label: const Text('查看统计'),
),
],
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
setState(() {
_status = '正在重新加载...';
});
await _player.stop();
await _playVideo();
},
tooltip: '重新加载',
child: const Icon(Icons.refresh),
),
),
);
}
/// 构建进度区域(包含占位符)
Widget _buildProgressArea() {
if (_progressInfo == null) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: const Text(
'等待开始播放以监听进度...',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
);
}
return _buildProgressIndicator();
}
/// 构建下载进度指示器
Widget _buildProgressIndicator() {
final info = _progressInfo!;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
// 进度条
LinearProgressIndicator(
value: info.progress,
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(
info.isCompleted ? Colors.green : Colors.blue,
),
),
const SizedBox(height: 8),
// 进度文本
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${info.progressPercent} (${info.downloadedMB} / ${info.totalMB})',
style: const TextStyle(fontSize: 12),
),
Text(
info.isCompleted
? '✅ 已缓存'
: '⬇️ ${info.speedFormatted ?? "..."}',
style: TextStyle(
fontSize: 12,
color: info.isCompleted ? Colors.green : Colors.blue,
),
),
],
),
// 分片信息
Text(
'分片: ${info.completedSegments}/${info.totalSegments}',
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
],
),
);
}
}