视频代理
目录
代理的作用
在 Flutter 中为 video_player 加入代理,核心作用是拦截、处理和缓存视频数据:
| 作用 | 说明 |
|---|---|
| 边下边播 | 下载一段、播放一段,无需等待完整下载 |
| 二次秒开 | 缓存到本地,第二次播放直接从缓存读取 |
| 节省流量 | 重复播放同一视频不消耗网络流量 |
| 解决鉴权 | 可自动添加 Token 等请求头 |
核心原理
播放器 → 本地代理 (127.0.0.1:8888) → 远程视频服务器
↓
缓存到本地文件
三步走:
- 在 App 内部启动一个 HTTP 服务器(如
http://127.0.0.1:8888) - 播放器请求这个本地地址(而不是直接请求远程视频)
- 代理收到请求后,去远程获取数据,同时保存到本地
MP4 代理(边播边缓存)
最简版(50行核心代码)
dart
import 'dart:io';
import 'package:http/http.dart' as http;
class MiniProxy {
late HttpServer _server;
Future<void> start(int port, String remoteUrl) async {
_server = await HttpServer.bind('127.0.0.1', port);
print('代理已启动: http://127.0.0.1:$port');
await for (HttpRequest request in _server) {
final response = await http.get(Uri.parse(remoteUrl));
request.response
..statusCode = 200
..headers.contentType = ContentType('video', 'mp4')
..write(response.bodyBytes);
await request.response.close();
}
}
}
边播边缓存版(核心)
dart
class StreamingProxy {
late HttpServer _server;
File? _cacheFile;
Future<void> start(int port, String remoteUrl) async {
_cacheFile = File('${Directory.systemTemp.path}/video_cache.mp4');
_server = await HttpServer.bind('127.0.0.1', port);
await for (HttpRequest request in _server) {
if (await _cacheFile!.exists()) {
// 有缓存:直接返回
final data = await _cacheFile!.readAsBytes();
request.response..add(data);
} else {
// 无缓存:边下载边返回边保存
final client = http.Client();
final streamedRequest = http.Request('GET', Uri.parse(remoteUrl));
final streamedResponse = await client.send(streamedRequest);
final sink = _cacheFile!.openWrite();
await for (final chunk in streamedResponse.stream) {
sink.add(chunk); // 写入缓存
request.response.add(chunk); // 发送给播放器
}
await sink.close();
}
await request.response.close();
}
}
}
支持拖动进度条(Range 请求)
dart
Future<void> _serveFromCache(HttpRequest request) async {
final fileSize = await _cacheFile!.length();
final rangeHeader = request.headers['range'];
if (rangeHeader != null && rangeHeader.startsWith('bytes=')) {
// 解析 Range: bytes=100-200
final parts = rangeHeader.substring(6).split('-');
final start = int.parse(parts[0]);
final end = parts[1].isNotEmpty ? int.parse(parts[1]) : fileSize - 1;
final raf = _cacheFile!.openSync();
raf.setPositionSync(start);
final chunk = raf.readSync(end - start + 1);
raf.closeSync();
request.response
..statusCode = 206 // Partial Content
..headers.add({
'Content-Range': 'bytes $start-$end/$fileSize',
'Content-Length': '${end - start + 1}',
})
..add(chunk);
} else {
// 完整文件
request.response.addStream(_cacheFile!.openRead());
}
await request.response.close();
}
m3u8 代理
m3u8 文件格式示例
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:10,
https://example.com/segment1.ts
#EXTINF:10,
https://example.com/segment2.ts
核心代理代码
dart
class SimpleM3u8Proxy {
late HttpServer _server;
final String _remoteBaseUrl;
final String _cacheDir;
SimpleM3u8Proxy(this._remoteBaseUrl)
: _cacheDir = '${Directory.systemTemp.path}/m3u8_cache' {
Directory(_cacheDir).createSync(recursive: true);
}
Future<void> start(int port) async {
_server = await HttpServer.bind('127.0.0.1', port);
await for (HttpRequest request in _server) {
final path = request.uri.path;
if (path.endsWith('.m3u8')) {
await _handleM3u8(request);
} else if (path.endsWith('.ts')) {
await _handleTs(request, path);
}
}
}
/// 处理 m3u8:下载并替换 ts 地址
Future<void> _handleM3u8(HttpRequest request) async {
final response = await http.get(Uri.parse('$_remoteBaseUrl/index.m3u8'));
String content = response.body;
// 将远程 ts 地址替换为本地代理地址
content = content.replaceAllMapped(
RegExp(r'(https?://[^\s]+\.ts)'),
(match) => 'http://127.0.0.1:${_server.port}/${match[1]!.split('/').last}'
);
request.response
..statusCode = 200
..headers.contentType = ContentType('application', 'vnd.apple.mpegurl')
..write(content);
await request.response.close();
}
/// 处理 ts 分片:缓存
Future<void> _handleTs(HttpRequest request, String path) async {
final tsName = path.split('/').last;
final cacheFile = File('$_cacheDir/$tsName');
if (await cacheFile.exists()) {
// 命中缓存
final data = await cacheFile.readAsBytes();
request.response..add(data);
} else {
// 下载并缓存
final response = await http.get(Uri.parse('$_remoteBaseUrl/$tsName'));
await cacheFile.writeAsBytes(response.bodyBytes);
request.response..add(response.bodyBytes);
}
await request.response.close();
}
}
播放器集成
dart
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerPage extends StatefulWidget {
@override
_VideoPlayerPageState createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage> {
late StreamingProxy _proxy;
late VideoPlayerController _controller;
bool _isReady = false;
final String videoUrl = 'https://example.com/video.mp4';
@override
void initState() {
super.initState();
_setupProxyAndPlay();
}
Future<void> _setupProxyAndPlay() async {
// 1. 启动代理
_proxy = StreamingProxy();
await _proxy.start(8888, videoUrl);
// 2. 播放器请求本地代理地址
_controller = VideoPlayerController.network('http://127.0.0.1:8888');
await _controller.initialize();
setState(() => _isReady = true);
_controller.play();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('视频代理 Demo')),
body: Center(
child: _isReady
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: CircularProgressIndicator(),
),
);
}
@override
void dispose() {
_controller.dispose();
_proxy.stop();
super.dispose();
}
}
平台配置
Android 配置
- 添加网络权限(AndroidManifest.xml):
xml
<uses-permission android:name="android.permission.INTERNET" />
- 配置网络安全(res/xml/network_security_config.xml):
xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>
- 在 AndroidManifest.xml 中引用:
xml
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
iOS 配置
在 ios/Runner/Info.plist 中添加:
xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<false/>
</dict>
</dict>
</dict>