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

React(TypeScript)客户端如何将m3u8 HLS流下载保存为MP4

在React(TypeScript)中实现m3u8 HLS流转存为MP4视频

你的m3u8包含AES-128加密的TS片段,这是之前用@ffmpeg/ffmpeg失败的核心原因——加密密钥获取、跨域限制或FFmpeg配置不到位。下面是完整的解决方案,涵盖前端处理逻辑和关键细节:

核心问题排查

你之前的失败大概率源于以下几点:

  • 加密密钥(EXT-X-KEY中的URI)存在跨域限制,前端无法直接获取
  • FFmpeg WASM未正确加载,或命令参数配置错误
  • TS片段的相对路径未拼接成完整可访问的URL

解决方案步骤

1. 安装依赖

确保安装最新版FFmpeg前端库:

npm install @ffmpeg/ffmpeg@latest @ffmpeg/core@latest

2. 完整转码逻辑实现

以下是React+TypeScript的组件代码,包含m3u8解析、密钥处理、FFmpeg转码全流程:

import React, { useState, useRef } from 'react';
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';

const HlsToMp4Converter = () => {
  const [isProcessing, setIsProcessing] = useState(false);
  const ffmpegRef = useRef(createFFmpeg({ log: true }));

  // 解析m3u8内容,提取加密密钥和TS片段列表
  const parseM3u8Content = (content: string) => {
    const lines = content.split('\n');
    let keyUrl = '';
    const tsSegments: string[] = [];
    const baseTsUrl = 'https://example.com/hls/'; // 替换为你的TS片段基础URL

    lines.forEach(line => {
      if (line.startsWith('#EXT-X-KEY:')) {
        const uriMatch = line.match(/URI="([^"]+)"/);
        if (uriMatch) keyUrl = uriMatch[1];
      } else if (!line.startsWith('#') && line.trim()) {
        tsSegments.push(`${baseTsUrl}${line.trim()}`);
      }
    });

    return { keyUrl, tsSegments };
  };

  const startConversion = async () => {
    setIsProcessing(true);
    const ffmpeg = ffmpegRef.current;

    // 加载FFmpeg WASM
    if (!ffmpeg.isLoaded()) await ffmpeg.load();

    // 1. 获取m3u8文本内容
    const m3u8Res = await fetch('https://example.com/your-stream.m3u8');
    const m3u8Content = await m3u8Res.text();
    const { keyUrl, tsSegments } = parseM3u8Content(m3u8Content);

    // 2. 处理加密密钥:跨域则需后端代理
    let keyBuffer: Uint8Array | undefined;
    if (keyUrl) {
      const keyRes = await fetch(keyUrl);
      const keyArrayBuffer = await keyRes.arrayBuffer();
      keyBuffer = new Uint8Array(keyArrayBuffer);
      // 将密钥写入FFmpeg虚拟文件系统
      ffmpeg.FS('writeFile', 'enc.key', keyBuffer);
    }

    // 3. 下载所有TS片段到虚拟文件系统
    for (let i = 0; i < tsSegments.length; i++) {
      const segmentName = `segment-${i}.ts`;
      const segmentData = await fetchFile(tsSegments[i]);
      ffmpeg.FS('writeFile', segmentName, segmentData);
    }

    // 4. 构造FFmpeg命令:处理加密+合并转码
    const inputArgs = tsSegments.map((_, i) => `-i`).concat(tsSegments.map((_, i) => `segment-${i}.ts`));
    const filterArgs = ['-filter_complex', `concat=n=${tsSegments.length}:v=1:a=1[outv][outa]`];
    const mapArgs = ['-map', '[outv]', '-map', '[outa]'];
    const outputArgs = ['-c:v', 'libx264', '-c:a', 'aac', 'output.mp4'];
    
    // 添加解密参数(如果有密钥)
    const decryptArgs = keyBuffer ? ['-decryption_key', Buffer.from(keyBuffer).toString('hex')] : [];

    // 执行转码
    await ffmpeg.run(...inputArgs, ...filterArgs, ...mapArgs, ...decryptArgs, ...outputArgs);

    // 5. 导出MP4文件
    const mp4Data = ffmpeg.FS('readFile', 'output.mp4');
    const blobUrl = URL.createObjectURL(new Blob([mp4Data.buffer], { type: 'video/mp4' }));
    const downloadLink = document.createElement('a');
    downloadLink.href = blobUrl;
    downloadLink.download = 'stream-converted.mp4';
    downloadLink.click();
    URL.revokeObjectURL(blobUrl);

    // 清理虚拟文件系统
    ffmpeg.FS('unlink', 'output.mp4');
    tsSegments.forEach((_, i) => ffmpeg.FS('unlink', `segment-${i}.ts`));
    if (keyUrl) ffmpeg.FS('unlink', 'enc.key');

    setIsProcessing(false);
  };

  return (
    <div>
      <button onClick={startConversion} disabled={isProcessing}>
        {isProcessing ? '转码中...' : '下载并转码为MP4'}
      </button>
    </div>
  );
};

export default HlsToMp4Converter;

3. 关键注意事项

  • 跨域处理:若直接请求密钥或TS片段触发CORS错误,必须通过后端代理这些请求。比如在Node.js后端写转发接口,前端请求后端接口获取资源。
  • 内存限制:前端FFmpeg处理大视频易出现内存不足,建议限制视频时长,或优先采用后端转码方案。
  • 命令优化:如果是已完成的VOD流(你的示例是VOD),可简化FFmpeg命令为ffmpeg -i "m3u8-url" -c copy output.mp4,但仍需处理密钥和跨域问题。

备选方案:后端辅助转码

若前端方案受限于内存或跨域,可采用Node.js后端转码:

  1. 前端将m3u8 URL发送至后端
  2. 后端用fluent-ffmpeg执行转码:
const ffmpeg = require('fluent-ffmpeg');

ffmpeg('https://example.com/your-stream.m3u8')
  .outputOptions('-c copy')
  .save('output.mp4')
  .on('end', () => { /* 返回前端下载链接 */ });

内容的提问来源于stack exchange,提问作者Swapnil Prakash

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 02:45:02