media_proxy 0.0.2 copy "media_proxy: ^0.0.2" to clipboard
media_proxy: ^0.0.2 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';

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),
          ),
        ],
      ),
    );
  }
}
0
likes
0
points
15
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package that provides a local HTTP proxy for caching and prefetching media files to improve playback performance.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, path, path_provider

More

Packages that depend on media_proxy