Dart HttpServer在iOS WebView及浏览器中无法正确响应MP4文件
解决Flutter WebView加载本地SPA中MP4视频的问题
核心问题原因
WebView播放MP4依赖HTTP字节范围请求(Range Requests),直接通过file://协议加载本地文件时,多数WebView内核不支持分段请求,必须通过本地HTTP服务器代理,并正确处理Range请求头。
正确实现本地HttpServer(支持Range请求)
以下是修复后的服务器代码,重点修正了MP4文件的Range请求处理逻辑:
import 'dart:io'; import 'dart:math'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; Future<HttpServer> startLocalServer() async { final docsDir = await getApplicationDocumentsDirectory(); final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080); await for (var request in server) { // 拼接本地文件路径,去掉URL开头的斜杠 var filePath = p.join(docsDir.path, request.uri.path.substring(1)); var file = File(filePath); if (!await file.exists()) { request.response.statusCode = HttpStatus.notFound; await request.response.close(); continue; } var fileLength = await file.length(); var contentType = lookupMimeType(filePath) ?? 'application/octet-stream'; // 手动确保MP4的Content-Type正确 if (filePath.endsWith('.mp4')) contentType = 'video/mp4'; // 处理Range请求 if (request.headers.containsKey('range')) { var rangeHeader = request.headers['range']!.first; var matches = RegExp(r'bytes=(\d+)-(\d+)?').firstMatch(rangeHeader); if (matches != null) { var start = int.parse(matches.group(1)!); var end = matches.group(2) != null ? int.parse(matches.group(2)!) : fileLength - 1; end = min(end, fileLength - 1); // 处理无效的范围请求 if (start > end || start >= fileLength) { request.response.statusCode = HttpStatus.requestedRangeNotSatisfiable; request.response.headers.add('Content-Range', 'bytes */$fileLength'); await request.response.close(); continue; } var contentLength = end - start + 1; request.response ..statusCode = HttpStatus.partialContent ..headers.add('Content-Type', contentType) ..headers.add('Content-Length', contentLength.toString()) ..headers.add('Content-Range', 'bytes $start-$end/$fileLength') ..headers.add('Accept-Ranges', 'bytes'); await file.openRead(start, end + 1).pipe(request.response); continue; } } // 处理普通完整请求 request.response ..statusCode = HttpStatus.ok ..headers.add('Content-Type', contentType) ..headers.add('Content-Length', fileLength.toString()) ..headers.add('Accept-Ranges', 'bytes'); await file.openRead().pipe(request.response); } return server; }
关键逻辑修正点
- Range头解析:用正则覆盖
bytes=0-、bytes=100-200等常见格式,避免分支遗漏 - 边界校验:新增对
start >= fileLength的判断,返回标准416状态码及响应头 - MP4类型强制设置:防止
lookupMimeType识别失败导致的Content-Type错误 - 分段流传输:用
openRead(start, end+1)精准截取文件片段,确保字节范围正确
启动服务器并加载WebView
HttpServer? _localServer; @override void initState() { super.initState(); _initLocalServer(); } Future<void> _initLocalServer() async { _localServer = await startLocalServer(); setState(() {}); } @override void dispose() { _localServer?.close(); super.dispose(); } // WebView组件配置 WebView( initialUrl: _localServer != null ? 'http://localhost:8080/index.html' : '', javascriptMode: JavascriptMode.unrestricted, allowsInlineMediaPlayback: true, // 允许内联播放视频 );
常见错误排查
- 文件路径验证:打印
filePath确认是否指向DocumentsDirectory中的目标文件 - 端口冲突:若8080端口被占用,更换为其他未使用端口(如8081)
- WebView配置:确保开启
allowsInlineMediaPlayback,部分设备需此配置支持内联播放 - Range请求测试:用curl命令模拟请求,验证服务器返回的
Content-Range是否正确
内容的提问来源于stack exchange,提问作者Vinod Sutar
相关产品推荐
相关产品推荐

