如何在 React 应用中实现 HLS 视频流

2025-06-07

如何在 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.movHLS 时,它会将视频文件拆分成更小的片段,并创建一个带有.m3u8扩展名的播放列表文件。然后,服务器将这些.m3u8文件提供给视频播放器,播放器会请求这些.ts片段并自动调整视频比特率。

HLS 图表

先决条件

在开始之前,请确保您已安装以下内容:

  • Node.js 和 npm(Node 包管理器)
  • React.js
  • Express.js
  • 邮差

将视频转换为HLS格式

首先,我们需要在电脑上安装FFmpegffmpeg ,用于转换视频。您可以点击此处了解更多关于 FFmpeg 的信息。

后端


1 - 初始化一个新的 NodeJs 应用程序并安装express、、&corsmulteruuid

npm i express cors multer uuid
Enter fullscreen mode Exit fullscreen mode

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}`)
})

Enter fullscreen mode Exit fullscreen mode

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 });
    });
});

Enter fullscreen mode Exit fullscreen mode

在上面的代码中,我们基本上将其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}`;
  1. ffmpeg:这是处理多媒体文件的命令行工具。

  2. -i ${videoPath}:此选项指定输入文件。${videoPath}是输入视频文件路径的占位符。

  3. -map 0:v:此选项从输入文件中选择第一个流以包含在输出中,然后对其进行编码。

    这里有三个map 0:v,这表明我们正在创建一个具有多个视频流的输出,每个视频流都使用不同的质量设置进行编码,但它们都将成为同一个 HLS 输出的一部分。

  4. -crf 23-crf 28-crf 32:这些选项指定视频编码的恒定速率因子 (CRF)。CRF 是一种基于质量的编码方法,它会设定一个目标质量级别,然后编码器会调整比特率以达到该质量级别。较低的 CRF 值可实现更高的质量,但文件大小也会更大。

  5. -preset medium:此选项指定编码预设,用于权衡编码速度和压缩效率。中等预设在速度和压缩率之间取得平衡。其他预设包括veryslowslowfastveryfast

  6. -map 0:a? -c:a aac -b:a 128k?:此选项从输入文件中选择第一个音频流(如果有)并指定用于编码的音频编解码器( aac)和比特率(128k)。

  7. -hls_time 10:此选项设置 HLS 播放列表中每个片段的时长。在本例中,每个片段的时长为 10 秒。

例如:一段 60 秒的视频有 10 个片段,播放列表文件将加载前 6 个片段,当加载第 7 个片段时,第 1 个片段将从文件中删除,如果再次访问第 1 个片段,则需要从服务器再次加载。

  1. -f hls:此选项指定输出文件的格式,在本例中为 HLS(HTTP 实时流)。

  2. ${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 });
});
Enter fullscreen mode Exit fullscreen mode
  • 最后,后端文件结构应该看起来像这样

后端文件结构

  • 最终 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
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

});

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:
Enter fullscreen mode Exit fullscreen mode

节点索引.js

![Start Server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f1i5afizp626xsgbjpox.png)

- 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
Enter fullscreen mode Exit fullscreen mode

图像 POST 请求

如果视频有效并且转换成功,则响应如下:

POST 响应

  • chapterId从回复中复制。

  • 现在,以作为参数GET向路由发出新请求。localhost:3000/getVideochapterId

GET 请求

  • 如果chapterId有效,您将收到视频标题和网址作为回应。

GET 响应

您的视频已成功上传并转换。

检查您的后端publicuploads文件夹。将生成一个或多个文件,并显示output.ts一个文件,即播放列表文件。output.m3u8

图像结构

现在,我们需要创建前端。

前端


  • 使用 Vite 初始化一个新的 React 应用程序:
npm create vite@latest hls-frontend
Enter fullscreen mode Exit fullscreen mode
  • 安装video.js
npm i video.js
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 在 中写入以下代码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
Enter fullscreen mode Exit fullscreen mode

url目前我们使用的是通过 Postman 请求获取的视频getVideo。将变量中的视频 URL 替换成videoSrc你的URL hostname:port/url。之后你可以axios向后端发出请求,然后动态获取 URL,就是这么简单。

  • 启动 vite 应用 npm run dev

前端

恭喜🎉 您的 HLS 应用已准备就绪!您可以在此基础上进行其他改进,使其成为更好的应用。我们的目标是为您提供一个基本框架,以便您构建下一个大型应用!


感谢阅读本教程。这是我的第一篇博客文章,我计划以后也写一些类似的、有趣的文章!

💜 如果您发现此博客有用,请点赞
📢 如果您认为有人需要它,请分享
💬 发表评论并为我分享您的见解和技巧
🧑‍🧑‍🧒 如果您想阅读更多博客,请关注我!

LinkedIn:@IndranilChutia
Github:@IndranilChutia

文章来源:https://dev.to/indranilchutia/how-to-implement-hls-video-streaming-in-a-react-app-2cki
PREV
状态管理之战:Redux 与 Zusand
NEXT
在安卓设备上安装 Linux?安卓系统上的 Linux?但为什么?如何开始?如何扩展我的权限?Linux 不错,但还有什么?社区?捐赠