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

Flutter离线模式下HLS(.m3u8)无法播放及本地下载播放方案

解决Flutter中HLS视频离线播放的问题

首先得搞清楚为什么你放在Assets里的.m3u8文件无法播放:HLS不是单一文件,它是一个索引文件,指向多个视频分片(.ts文件)和音频索引文件,你只把主m3u8放进Assets,缺少它依赖的子m3u8和所有分片文件,自然播放不了。而MP4是单个完整文件,所以Assets里的MP4能正常播放。

要实现HLS离线播放,你需要把整个HLS资源包(主m3u8、子m3u8、所有.ts分片、音频分片)下载到本地文件系统,然后修改索引文件中的路径指向本地文件,最后用本地文件路径加载播放。下面是具体的实现步骤和代码示例:

步骤1:准备依赖包

你需要用到几个工具包来简化开发:

  • dio:用于网络下载文件
  • path_provider:获取本地存储目录
  • chewie 和 video_player:播放视频(你已经在使用了)
  • hls_parser(可选):帮助解析m3u8文件结构,避免手动解析的麻烦

在pubspec.yaml中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.3.0
  path_provider: ^2.1.1
  chewie: ^1.7.0
  video_player: ^2.8.0
  hls_parser: ^0.1.0 # 可选,用于解析m3u8

步骤2:解析HLS资源并下载所有文件

首先要解析主m3u8文件,提取所有需要下载的子文件链接,然后批量下载到本地目录。这里以你的示例m3u8为例,假设你有一个远程的主m3u8链接(或者先从Assets读取主m3u8内容,再解析依赖文件)。

示例代码:解析并下载HLS资源

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:hls_parser/hls_parser.dart';

class HlsDownloader {
  final Dio _dio = Dio();

  // 下载HLS资源到本地
  Future<String> downloadHls(String masterHlsUrl, String saveDirName) async {
    // 获取本地存储目录
    final Directory appDocDir = await getApplicationDocumentsDirectory();
    final String savePath = '${appDocDir.path}/$saveDirName';
    await Directory(savePath).create(recursive: true);

    // 1. 下载主m3u8文件
    final String masterM3u8Path = '$savePath/master.m3u8';
    await _dio.download(masterHlsUrl, masterM3u8Path);

    // 2. 解析主m3u8,获取所有需要下载的子文件链接
    final String masterM3u8Content = await File(masterM3u8Path).readAsString();
    final HlsPlaylist playlist = HlsPlaylistParser.parseString(masterM3u8Content);

    // 提取所有子m3u8和音频m3u8的链接
    final List<String> subUrls = [];
    if (playlist is HlsMasterPlaylist) {
      // 添加视频子流的m3u8链接
      for (var variant in playlist.variants) {
        subUrls.add(variant.url);
      }
      // 添加音频流的m3u8链接
      for (var audio in playlist.audioGroups.values.expand((e) => e)) {
        subUrls.add(audio.url);
      }
    }

    // 3. 下载所有子m3u8文件,以及它们依赖的.ts分片
    for (String subUrl in subUrls) {
      // 子m3u8的本地保存路径
      final String subFileName = subUrl.split('/').last;
      final String subM3u8Path = '$savePath/$subFileName';
      await _dio.download('${masterHlsUrl.substring(0, masterHlsUrl.lastIndexOf('/'))}/$subUrl', subM3u8Path);

      // 解析子m3u8,下载所有.ts分片
      final String subM3u8Content = await File(subM3u8Path).readAsString();
      final HlsPlaylist subPlaylist = HlsPlaylistParser.parseString(subM3u8Content);
      if (subPlaylist is HlsMediaPlaylist) {
        for (var segment in subPlaylist.segments) {
          final String tsFileName = segment.url.split('/').last;
          final String tsPath = '$savePath/$tsFileName';
          await _dio.download('${masterHlsUrl.substring(0, masterHlsUrl.lastIndexOf('/'))}/${segment.url}', tsPath);
        }
      }
    }

    // 4. 修改主m3u8中的路径为本地相对路径(因为所有文件都在同一个目录)
    String updatedMasterContent = masterM3u8Content;
    // 替换所有远程链接为本地文件名
    for (String subUrl in subUrls) {
      final String fileName = subUrl.split('/').last;
      updatedMasterContent = updatedMasterContent.replaceAll(subUrl, fileName);
    }
    await File(masterM3u8Path).writeAsString(updatedMasterContent);

    // 返回本地主m3u8的路径
    return masterM3u8Path;
  }
}

步骤3:播放本地HLS文件

下载完成后,使用VideoPlayerController.file加载本地的m3u8文件,然后用Chewie播放:

修改你的MyHomePage代码

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String? localHlsPath;
  bool isDownloading = false;

  final HlsDownloader _downloader = HlsDownloader();

  // 下载HLS资源
  Future<void> _downloadHls() async {
    setState(() {
      isDownloading = true;
    });
    try {
      // 替换成你的远程HLS主链接
      final String hlsUrl = 'https://your-server-url/videos/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8';
      localHlsPath = await _downloader.downloadHls(hlsUrl, 'my_hls_video');
    } catch (e) {
      print('下载失败: $e');
    } finally {
      setState(() {
        isDownloading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('HLS 离线播放'),
      ),
      body: ListView(
        children: [
          // 下载按钮
          ElevatedButton(
            onPressed: isDownloading ? null : _downloadHls,
            child: isDownloading ? CircularProgressIndicator(color: Colors.white) : Text('下载HLS视频'),
          ),
          // 本地HLS播放
          if (localHlsPath != null)
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16),
              child: ChewieDemo(
                looping: true,
                videoPlayerController: VideoPlayerController.file(
                  File(localHlsPath!),
                ),
              ),
            ),
          // 原MP4播放(保留)
          ChewieDemo(
            looping: true,
            videoPlayerController: VideoPlayerController.network(
              'https://assets.mixkit.co/videos/preview/mixkit-forest-stream-in-the-sunlight-529-large.mp4',
            ),
          )
        ],
      ),
    );
  }
}

// 你的ChewieDemo组件(假设已经实现)
class ChewieDemo extends StatefulWidget {
  final bool looping;
  final VideoPlayerController videoPlayerController;

  const ChewieDemo({
    required this.looping,
    required this.videoPlayerController,
    Key? key,
  }) : super(key: key);

  @override
  _ChewieDemoState createState() => _ChewieDemoState();
}

class _ChewieDemoState extends State<ChewieDemo> {
  late ChewieController _chewieController;

  @override
  void initState() {
    super.initState();
    _chewieController = ChewieController(
      videoPlayerController: widget.videoPlayerController,
      autoPlay: false,
      looping: widget.looping,
      // 可添加更多配置,比如播放控件样式、静音等
    );
  }

  @override
  void dispose() {
    widget.videoPlayerController.dispose();
    _chewieController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Chewie(controller: _chewieController);
  }
}

关键注意事项

  • 权限配置:
    • Android:在AndroidManifest.xml中添加存储权限(Android 13+需要READ_MEDIA_VIDEO,旧版本需要WRITE_EXTERNAL_STORAGE)
    • iOS:在Info.plist中添加NSDocumentDirectoryUsageDescription,说明使用存储的原因
  • 路径处理:确保所有下载的文件都在同一个目录,并且m3u8中的路径修改为相对路径或者绝对路径,避免播放时找不到文件
  • 错误处理:实际开发中要添加更多错误捕获,比如网络失败、文件写入失败等情况
  • 分片数量:如果HLS包含大量分片,考虑添加下载进度显示,提升用户体验

内容的提问来源于stack exchange,提问作者Pravin Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 07:28:14