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后端转码:
- 前端将m3u8 URL发送至后端
- 后端用
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
相关产品推荐
相关产品推荐

