如何在 React 应用中实现 HLS 视频流
TL;DR: 本教程旨在构建一个具有 HLS 视频流功能的 ReactJS 应用。开发了一个 Node.js 和 Express 后端,使用 FFmpeg 将视频转换为 HLS 格式,并根据用户请求提供转换后的视频 URL。利用 Video.js 实现了一个视频播放器,以便在前端无缝播放 HLS 视频。
嘿!你打开这篇博客,可能是因为客户要求你“根据网速添加自适应视频质量”,又或许是因为你的视频播放器加载时会加载完整视频,导致网速较慢的用户长时间等待。或许你正在尝试构建下一个提供最佳流媒体体验的大型视频平台。🍿
我写这篇教程的原因和客户类似,客户要求我添加自适应比特率,我四处寻找资源,却找不到完整的解决方案,最终感到沮丧。我会尽力为你提供一个简单的 HLS 实现,在后端使用 FFmpeg 将视频转换为多种画质,然后在前端使用 Video.js 流式传输 HLS 视频。
请关注我的社交媒体以获取最新的技术资讯:
💼 LinkedIn:@IndranilChutia
🐙 Github:@IndranilChutia
什么是 HLS(HTTP 实时流)?
HTTP 实时流 (HLS) 是由 Apple 开发的一种自适应比特率流媒体协议,用于通过互联网传输媒体内容。它将视频分解成小块,并通过标准 HTTP 协议进行传输。HLS 会根据用户的可用带宽和设备性能动态调整视频流的质量,确保流畅播放,避免缓冲。
可是等等!到底发生了什么?🤯
当我们将 .vmware 或 .vmware(或任何其他视频格式)转换.mp4
为.mov
HLS 时,它会将视频文件拆分成更小的片段,并创建一个带有.m3u8
扩展名的播放列表文件。然后,服务器将这些.m3u8
文件提供给视频播放器,播放器会请求这些.ts
片段并自动调整视频比特率。
先决条件
在开始之前,请确保您已安装以下内容:
- Node.js 和 npm(Node 包管理器)
- React.js
- Express.js
- 邮差
将视频转换为HLS格式
首先,我们需要在电脑上安装FFmpegffmpeg
,用于转换视频。您可以点击此处了解更多关于 FFmpeg 的信息。
- 🖥️ Mac
brew install ffmpeg
-
🪟 Windows:此处提供分步指南
-
🐧 Linux:分步指南在这里
后端
1 - 初始化一个新的 NodeJs 应用程序并安装express
、、&cors
multer
uuid
npm i express cors multer uuid
2 - 创建index.js
文件:
const express = require('express')
const cors = require('cors')
const PORT = 3000;
const app = express();
const corsOptions = {
origin: "*",
};
app.use(cors(corsOptions));
app.use(express.json());
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
3 – 创建一个middlewares
文件夹并添加一个multer.js
文件并导出upload
中间件。
确保
uploads
在根目录中创建一个文件夹const multer = require('multer');
// 为上传的文件设置存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + file.originalname);
}
});
// 创建 multer 实例
const upload = multer({ storage: storage });
模块.导出=上传;
4 - In the `index.js` file create a `/upload` `POST` route and add the following code:
```js
const upload = require('./middlewares/multer')
const { exec } = require('child_process');
const fs = require('fs');
const uuid = require('uuid');
const path = require('path')
const chapter = {} // We will create an in-memory DB for now
app.use("/public", express.static(path.join(__dirname, "public")));
app.post('/upload', upload.single('video'), (req, res) => {
const chapterId = uuid.v4(); // Generate a unique chapter ID
const videoPath = req.file.path;
const outputDir = `public/videos/${chapterId}`;
const outputFileName = 'output.m3u8';
const outputPath = path.join(outputDir, outputFileName);
// Check if output directory exists, create if not
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Command to convert video to HLS format using ffmpeg
const command = `ffmpeg -i ${videoPath} \
-map 0:v -c:v libx264 -crf 23 -preset medium -g 48 \
-map 0:v -c:v libx264 -crf 28 -preset fast -g 48 \
-map 0:v -c:v libx264 -crf 32 -preset fast -g 48 \
-map 0:a -c:a aac -b:a 128k \
-hls_time 10 -hls_playlist_type vod -hls_flags independent_segments -report \
-f hls ${outputPath}`;
// Execute ffmpeg command
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`ffmpeg exec error: ${error}`);
return res.status(500).json({ error: 'Failed to convert video to HLS format' });
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
const videoUrl = `public/videos/${chapterId}/${outputFileName}`;
chapters[chapterId] = { videoUrl, title: req.body.title, description: req.body.description }; // Store chapter information
res.json({ success: true, message: 'Video uploaded and converted to HLS.', chapterId });
});
});
在上面的代码中,我们基本上将其video
作为输入文件和视频标题,然后存储在我们的上传文件夹中。
然后我们使用将文件转换为 HLS 格式ffmpeg command
。
- 让我花一点时间来向您解释该命令:
js const command = `ffmpeg -i ${videoPath} \ -map 0:v -c:v libx264 -crf 23 -preset medium -g 48 \ -map 0:v -c:v libx264 -crf 28 -preset fast -g 48 \ -map 0:v -c:v libx264 -crf 32 -preset fast -g 48 \ -map 0:a -c:a aac -b:a 128k \ -hls_time 10 -hls_playlist_type vod -hls_flags independent_segments -report \ -f hls ${outputPath}`;
-
ffmpeg
:这是处理多媒体文件的命令行工具。 -
-i ${videoPath}
:此选项指定输入文件。${videoPath}
是输入视频文件路径的占位符。 -
-map 0:v
:此选项从输入文件中选择第一个流以包含在输出中,然后对其进行编码。这里有三个
map 0:v
,这表明我们正在创建一个具有多个视频流的输出,每个视频流都使用不同的质量设置进行编码,但它们都将成为同一个 HLS 输出的一部分。 -
-crf 23
、-crf 28
、-crf 32
:这些选项指定视频编码的恒定速率因子 (CRF)。CRF 是一种基于质量的编码方法,它会设定一个目标质量级别,然后编码器会调整比特率以达到该质量级别。较低的 CRF 值可实现更高的质量,但文件大小也会更大。 -
-preset medium
:此选项指定编码预设,用于权衡编码速度和压缩效率。中等预设在速度和压缩率之间取得平衡。其他预设包括veryslow
、slow
、fast
、veryfast
。 -
-map 0:a? -c:a aac -b:a 128k
?
:此选项从输入文件中选择第一个音频流(如果有)并指定用于编码的音频编解码器(aac
)和比特率(128k
)。 -
-hls_time 10
:此选项设置 HLS 播放列表中每个片段的时长。在本例中,每个片段的时长为 10 秒。
例如:一段 60 秒的视频有 10 个片段,播放列表文件将加载前 6 个片段,当加载第 7 个片段时,第 1 个片段将从文件中删除,如果再次访问第 1 个片段,则需要从服务器再次加载。
-
-f hls
:此选项指定输出文件的格式,在本例中为 HLS(HTTP 实时流)。 -
${outputPath}
:这是保存 HLS 文件的输出目录的路径。
点击此处了解有关 FFmpeg 选项的更多信息
- 以下代码用于在
public
路由中静态提供 HLS 文件js app.use("/public", express.static(path.join(__dirname, "public")));
5 - 现在,我们将创建一个/getVideo
路由,用户将在其中提供chapterId
查询,并将视频网址和标题发回
app.get('/getVideo', (req, res) => {
const { chapterId } = req.query;
if (!chapterId || !chapters[chapterId]) {
return res.status(404).json({ error: 'Chapter not found' });
}
const { title, videoUrl } = chapters[chapterId];
console.log(title, " ", videoUrl)
res.json({ title: title, url: videoUrl });
});
- 最后,后端文件结构应该看起来像这样
- 最终
index.js
文件: ```js const express = require('express') const upload = require('./middlewares/multer') const { exec } = require('child_process'); const fs = require('fs'); const uuid = require('uuid'); const path = require('path') const cors = require('cors')
常量端口=3000;
const app = express();
const corsOptions = {
origin: "*",
};
app.use(cors(corsOptions));
app.use(express.json());
app.use("/public", express.static(path.join(__dirname, "public")));
const chapters = {}
app.post('/upload', upload.single('video'), (req, res) => {
const chapterId = uuid.v4(); // 生成唯一的章节 ID
const videoPath = req.file.path;
const outputDir = public/videos/${chapterId}
;
const outputFileName = 'output.m3u8';
const outputPath = path.join(outputDir, outputFileName);
// Check if output directory exists, create if not
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Command to convert video to HLS format using ffmpeg
const 命令 = ;ffmpeg -i ${videoPath} \
-map 0:v -c:v libx264 -crf 23 -preset medium -g 48 \
-map 0:v -c:v libx264 -crf 28 -preset fast -g 48 \
-map 0:v -c:v libx264 -crf 32 -preset fast -g 48 \
-map 0:a -c:a aac -b:a 128k \
-hls_time 10 -hls_playlist_type vod -hls_flags independent_segments -report \
-f hls ${outputPath}
// Execute ffmpeg command
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`ffmpeg exec error: ${error}`);
return res.status(500).json({ error: 'Failed to convert video to HLS format' });
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
const videoUrl = `public/videos/${chapterId}/${outputFileName}`;
chapters[chapterId] = { videoUrl, title: req.body.title }; // Store chapter information
res.json({ success: true, message: 'Video uploaded and converted to HLS.', chapterId });
});
});
app.get('/getVideo', (req, res) => {
const { chapterId } = req.query;
if (!chapterId || !chapters[chapterId]) {
return res.status(404).json({ error: '未找到章节' });
}
const { title, videoUrl } = chapters[chapterId];
console.log(title, " ", videoUrl)
res.json({ title: title, url: videoUrl });
});
app.listen(PORT, () => {
console.log( Server is running on port ${PORT}
)
})
## Uploading videos using Postman
<hr/>
- Start the backend server:
节点索引.js

- Open Postman (Download it [here](https://www.postman.com/))
- Make a `POST` request to `localhost:3000/upload` route with the following `form data` fields:
```ts
video: videoPath: file
title: videoTitle: string
如果视频有效并且转换成功,则响应如下:
-
chapterId
从回复中复制。 -
现在,以作为参数
GET
向路由发出新请求。localhost:3000/getVideo
chapterId
- 如果
chapterId
有效,您将收到视频标题和网址作为回应。
您的视频已成功上传并转换。
检查您的后端
public
和uploads
文件夹。将生成一个或多个文件,并显示output.ts
一个文件,即播放列表文件。output.m3u8
现在,我们需要创建前端。
前端
- 使用 Vite 初始化一个新的 React 应用程序:
npm create vite@latest hls-frontend
- 安装
video.js
npm i video.js
VideoPlayer.jsx
使用以下代码创建一个组件:
import React, { useRef, useEffect } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
export const VideoPlayer = (props) => {
const videoRef = useRef(null);
const playerRef = useRef(null);
const { options, onReady } = props;
useEffect(() => {
// Make sure Video.js player is only initialized once
if (!playerRef.current) {
// The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
const videoElement = document.createElement("video-js");
videoElement.classList.add('vjs-big-play-centered');
videoRef.current.appendChild(videoElement);
const player = playerRef.current = videojs(videoElement, options, () => {
videojs.log('player is ready');
onReady && onReady(player);
});
// You could update an existing player in the `else` block here
// on prop change, for example:
} else {
const player = playerRef.current;
player.autoplay(options.autoplay);
player.src(options.sources);
}
}, [options, videoRef]);
// Dispose the Video.js player when the functional component unmounts
useEffect(() => {
const player = playerRef.current;
return () => {
if (player && !player.isDisposed()) {
player.dispose();
playerRef.current = null;
}
};
}, [playerRef]);
return (
<div data-vjs-player style={{ width: "600px" }}>
<div ref={videoRef} />
</div>
);
}
export default VideoPlayer;
- 在 中写入以下代码
App.jsx
:
import { useRef } from 'react'
import './App.css'
import VideoPlayer from './VideoPlayer';
function App() {
var videoSrc = 'http://localhost:3000/public/videos/2d7b06b6-4913-4f75-907f-9c8c738a3395/output.m3u8';
const playerRef = useRef(null);
const videoJsOptions = {
autoplay: true,
controls: true,
responsive: true,
fluid: true,
sources: [{
src: videoSrc,
type: 'application/x-mpegURL'
}],
};
const handlePlayerReady = (player) => {
playerRef.current = player;
// You can handle player events here, for example:
player.on('waiting', () => {
videojs.log('player is waiting');
});
player.on('dispose', () => {
videojs.log('player will dispose');
});
};
return (
<>
<div>
<VideoPlayer options={videoJsOptions} onReady={handlePlayerReady} />
</div>
</>
)
}
export default App
url
目前我们使用的是通过 Postman 请求获取的视频getVideo
。将变量中的视频 URL 替换成videoSrc
你的URLhostname:port/url
。之后你可以axios
向后端发出请求,然后动态获取 URL,就是这么简单。
- 启动 vite 应用
npm run dev
恭喜🎉 您的 HLS 应用已准备就绪!您可以在此基础上进行其他改进,使其成为更好的应用。我们的目标是为您提供一个基本框架,以便您构建下一个大型应用!
感谢阅读本教程。这是我的第一篇博客文章,我计划以后也写一些类似的、有趣的文章!
💜 如果您发现此博客有用,请点赞
📢 如果您认为有人需要它,请分享
💬 发表评论并为我分享您的见解和技巧
🧑🧑🧒 如果您想阅读更多博客,请关注我!
LinkedIn:@IndranilChutia
Github:@IndranilChutia