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

背景:客户要求 “根据网速添加自适应视频质量”,或者视频播放器可能在加载时加载了完整视频,这让网速较慢的用户等待了很长时间。也许您正在试图打造下一个具有最佳流媒体体验的大型视频平台。

我写这篇教程也是出于类似的原因,客户要求我添加自适应比特率,然后我就出去寻找资源,但却无法获得完整的解决方案,因此感到很沮丧。我将尽力为您提供一个简单的 HLS 实现,在后端使用 FFmpeg 将视频转换为多种质量,然后在前端使用 Video.js 流式传输 HLS 视频。

什么是 HLS(HTTP Live Streaming)?

HLS 是苹果公司开发的一种自适应比特率流协议,用于在互联网上传输媒体内容。它将视频分割成小块,并通过标准 HTTP 协议提供。HLS 可根据用户的可用带宽和设备能力动态调整视频流的质量,确保流畅播放而不会出现缓冲。

但是,等等!实际会发生什么?

当我们将 .mp4 或 .mov(或任何其他视频格式)转换为 HLS 时,它会将视频文件分割成更小的片段,并创建一个扩展名为 .m3u8 的播放列表文件。然后服务器将 .m3u8 文件提供给视频播放器,播放器请求 .ts 片段并自动调整视频比特率。

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

前提条件

在我们开始之前,请确保您已安装:

  • Node.js 和 npm
  • React.js
  • Express.js
  • Postman

将视频转换为 HLS 格式

首先必须先安装 FFmpeg,然后使用 ffmpeg 转换视频。

后台

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 文件并导出上传中间件。

确保在根目录中创建一个“uploads”文件夹

const multer = require('multer');

// Set up storage for uploaded files
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/');
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + '-' + file.originalname);
    }
});

// Create the multer instance
const upload = multer({ storage: storage });

module.exports = upload;

4、在 index.js 文件中创建一个 /upload POST 路由,并添加以下代码:

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 -crf 23 -preset medium \
    -map 0:v -crf 28 -preset medium \
    -map 0:v -crf 32 -preset medium \
    -map 0:a? -c:a aac -b:a 128k \
    -hls_time 10 -hls_list_size 6 -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 作为输入文件,并将视频标题存储在上传文件夹中。

然后使用 ffmpeg 命令将文件转换为 HLS 格式。

让我用一分钟时间向你解释一下该命令:

const command = `ffmpeg -i ${videoPath} \
    -map 0:v -crf 23 -preset medium \
    -map 0:v -crf 28 -preset medium \
    -map 0:v -crf 32 -preset medium \
    -map 0:a? -c:a aac -b:a 128k \
    -hls_time 10 -hls_list_size 6 -f hls ${outputPath}`;
  • ffmpeg: 这是处理多媒体文件的命令行工具。
  • -i ${videoPath}: 此选项指定输入文件。${videoPath} 是输入视频文件路径的占位符。
  • -map 0:v:该选项从输入文件中选择第一条数据流纳入输出,然后对其进行编码。

这里有三个映射 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 秒。
  • -hls_list_size 6:该选项设置 HLS 播放列表中条目数量的上限。达到此上限后,播放列表中的旧片段将被删除。
  • -f hls: 此选项指定输出文件的格式,本例中为 HLS(HTTP 实时流)。
  • ${outputPath}: 这是保存 HLS 文件的输出目录路径。

以下代码用于在 public 路由中静态提供 HLS 文件:

app.use("/public", express.static(path.join(__dirname, "public")));

5、现在,我们将创建一个 /getVideo 路由,在该路由中,用户将提供 chapterId 作为查询,视频 URL 和标题将被发送回来

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

最后,后台文件结构如下所示

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

最终 index.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')

const PORT = 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(); // 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 -crf 23 -preset medium \
    -map 0:v -crf 28 -preset medium \
    -map 0:v -crf 32 -preset medium \
    -map 0:a? -c:a aac -b:a 128k \
    -hls_time 10 -hls_list_size 6 -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: 'Chapter not found' });
    }
    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}`)
})

使用 Postman 上传视频

  • 启动后端服务器
node index.js
  • 打开 Postman
  • 向 localhost:3000/upload 路由发出 POST 请求,并输入以下表单数据字段:
video: videoPath: file
title: videoTitle: string
如何在 React 应用程序中实现 HLS 视频流

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

如何在 React 应用程序中实现 HLS 视频流
  • 从响应中复制 chapterId。
  • 现在,以 chapterId 为参数,向 localhost:3000/getVideo 路由发出新的 GET 请求。
如何在 React 应用程序中实现 HLS 视频流

如果 chapterId 有效,您将收到视频标题和 URL 的回复。

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

视频已成功上传并转换。

检查后台 publicuploads 文件夹。将生成一个或多个 output.ts 文件,并出现一个 output.m3u8 文件,即播放列表文件。

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

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

前端

  • 使用 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

目前,我们使用的是在 Postman 上发出 getVideo 请求后获得的视频网址。将 videoSrc 变量中的视频网址替换为hostname:port/url。稍后,您可以向后台发出 axios 请求,然后动态获取 url。

  • 启动vite应用
npm run dev
如何在 React 应用程序中实现 HLS 视频流

现在 HLS 应用程序已准备就绪!您可以在此基础上进行其他改进,使其成为更好的应用程序。这里的目标是提供一个基本骨架,您可以在此基础上构建下一个大型应用程序!

作者:Indranil Chutia
译自:https://dev.to/indranilchutia/how-to-implement-hls-video-streaming-in-a-react-app-2cki

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/47198.html

(1)

相关推荐

发表回复

登录后才能评论