Flutter 视频代理完全教程

视频代理

目录

  1. 代理的作用
  2. 核心原理
  3. [MP4 代理(边播边缓存)](#MP4 代理(边播边缓存))
  4. [m3u8 代理](#m3u8 代理)
  5. 播放器集成
  6. 平台配置

代理的作用

在 Flutter 中为 video_player 加入代理,核心作用是拦截、处理和缓存视频数据

作用 说明
边下边播 下载一段、播放一段,无需等待完整下载
二次秒开 缓存到本地,第二次播放直接从缓存读取
节省流量 重复播放同一视频不消耗网络流量
解决鉴权 可自动添加 Token 等请求头

核心原理

播放器 → 本地代理 (127.0.0.1:8888) → 远程视频服务器

缓存到本地文件

三步走:

  1. 在 App 内部启动一个 HTTP 服务器(如 http://127.0.0.1:8888
  2. 播放器请求这个本地地址(而不是直接请求远程视频)
  3. 代理收到请求后,去远程获取数据,同时保存到本地

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 配置

  1. 添加网络权限(AndroidManifest.xml):
xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />
  1. 配置网络安全(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>
  1. 在 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>
相关推荐
nashane1 小时前
HarmonyOS 6学习:麦克风“抢戏”打断音频?AudioSession焦点避坑指南
学习·音视频·harmonyos
恋猫de小郭1 小时前
AI 时代,谷歌都在 Android 官方做了哪些支持?
android·前端·flutter
潜创微科技1 小时前
2026年高清音视频领域分配器方案服务商核心竞争力分析与实力梳理
音视频
三易串口屏2 小时前
实验15 视频控件实验
音视频·串口屏·三易串口屏·uart 通信
sN2vuQ08W2 小时前
uni-app 实现视频聊天、屏幕分享,支持Android、HarmonyOS、iOS
android·uni-app·音视频
Tech-Net2 小时前
YT视频怎么下载?2026最新4K/8K超清YT视频下载与批量解析教程
经验分享·音视频·视频编解码·视频下载·视频下载工具·视频解析·视频下载器
愚者Pro14 小时前
Flutter Widget组件学习(专为 Uniapp 转 Flutter 定制)
vue.js·学习·flutter·uni-app
OpenApi.cc15 小时前
2026年最新openapi:免费图片人脸识别和视频人脸识别工具
音视频
Flynt17 小时前
升级Flutter 3.44,我踩了HCPP和AGP 9的坑
android·flutter·dart