You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 00:48:26