1、前言
作为一名前端开发者,在项目中经常会遇到需要在网页上播放视频流的情况,比如RTMP或RTSP等格式。不幸的是,这些视频流通常不能直接被现代浏览器支持播放,除非安装了特定的插件。相对而言,HLS(HTTP Live Streaming)和FLV(Flash Video)这样的格式则更为友好,可以直接通过HTML5 <video>
标签来播放。
自今年五月份开始接触视频流处理以来,我断断续续地查阅了不少相关资料。了解到一种常见的解决方案是使用Nginx结合其扩展模块,如nginx-rtmp-module
,可以构建一个简易的流媒体服务器,实现从RTMP到HLS格式的转换,从而使得视频流能够在浏览器端无插件播放。
然而,我想分享的是一种不同的方法------利用FFmpeg进行视频流的转换,并且通过Node.js框架NestJS编写一个简单的服务来控制FFmpeg的工作流程。这种方法允许我们:
- 从源拉取原始视频流。
- 将其转码为HLS格式。
- 提供给客户端播放。
- 在不再需要时中断拉流并释放资源。
采用这种方案的好处在于,它提供了一个更加灵活可控的服务端解决方案,不仅能够满足基本的格式转换需求,还便于后续根据业务需求进行功能拓展。此外,由于整个过程是在后端完成的,因此不会增加前端代码的复杂度,同时也保证了更好的用户体验,因为最终用户只需要面对标准的HLS流即可。
通过这种方式,即使面对复杂的流媒体处理场景,前端开发者也能够以更简洁的方式集成视频播放功能,而无需深入研究底层协议或担忧兼容性问题。
2、配置环境
- 操作系统 :我们将使用Linux Ubuntu 24.04 LTS作为开发和运行的操作系统。
- 视频处理软件 :FFmpeg将被用作我们的视频流处理工具。它是一款非常强大的开源多媒体框架,能够解码、编码、转码、复用、解复用、流传输以及过滤各种多媒体文件格式。
- 开发语言与框架 :选用NestJS来构建服务端应用。NestJS是一个用于构建高效、可扩展的Node.js服务器端应用程序的框架。
- 运行时环境:Node.js版本18.20.4将是执行我们服务的基础。
3、配置流程
1、在Linux上安装FFmpeg
- 更新软件包列表:打开终端并执行以下命令来更新系统的软件包列表,以获取最新的软件信息。
sql
sudo apt update
-
安装FFmpeg :接下来使用
apt
安装FFmpeg。sudo apt install ffmpeg
-
验证安装:安装完成后,可以通过运行下面的命令来检查FFmpeg是否正确安装以及查看其版本信息。
ffmpeg -version
执行 ffmpeg -version
命令后,如果FFmpeg成功安装,终端将显示类似以下的内容,确认安装版本及构建信息
为了测试FFmpeg的可用性,并使用它来转换视频流,您可以使用以下命令。请确保替换 ${inputUrl}
为您的输入视频流地址,以及 ${outputDir}
为输出HLS文件的目标目录。该命令将调整视频分辨率至640x360,并设置一系列编码参数以优化输出质量与性能。
bash
ffmpeg -i ${inputUrl} \
-vf "scale=640:360" \
-c:v libx264 -preset veryfast -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 \
-c:a aac -b:a 160k -ac 2 -ar 44100 \
-f hls -hls_time 10 -hls_list_size 6 -hls_flags delete_segments -hls_segment_filename "${outputDir}/%05d.ts" \
${outputDir}/playlist.m3u8
命令解释:
-i ${inputUrl}
:指定输入视频流的URL。-vf "scale=640:360"
:使用视频滤镜调整视频尺寸到640x360像素。-c:v libx264
:设置视频编码器为libx264。-preset veryfast
:选择一个快速的编码预设,牺牲一些压缩效率以加快编码速度。-maxrate 3000k
和-bufsize 6000k
:设置最大比特率和缓冲区大小,有助于控制输出的质量和流畅度。-pix_fmt yuv420p
:设置像素格式,通常用于更好的兼容性。-g 50
:设定GOP(Group of Pictures)大小,影响关键帧间隔。-c:a aac
:设置音频编码器为AAC。-b:a 160k
:设置音频比特率为160kbps。-ac 2
:设置音频通道数为立体声。-ar 44100
:设置音频采样率为44.1kHz。-f hls
:指定输出格式为HLS。-hls_time 10
:每个分段的时长为10秒。-hls_list_size 6
:播放列表中保留最近的6个分段。-hls_flags delete_segments
:删除不再需要的旧分段。-hls_segment_filename "${outputDir}/%05d.ts"
:定义分段文件名模板。${outputDir}/playlist.m3u8
:指定输出播放列表文件的位置。
在执行此命令之前,请确保您有权限写入到${outputDir}
所指向的目录。如果一切配置正确且FFmpeg安装无误,上述命令将会开始处理视频流并生成HLS格式的内容。
4、使用NestJS调用FFmpeg对视频流进行转码
实现思路
我们将利用Node.js的child_process
模块中的exec
方法来在Node.js程序中执行外部命令。这样可以方便地调用FFmpeg并处理视频流。为了更好地管理和控制每个FFmpeg进程,我们将使用一个Map
对象来存储每个转码任务的信息。
接口定义
首先,定义一个接口来描述存储在Map
中的数据结构:
csharp
interface ProcessData {
inputUrl: string; // 输入视频流地址
outputDir: string; // 输出HLS文件目录
process: ChildProcess; // FFmpeg进程
}
存储映射
然后,在服务类中创建一个Map
实例来保存每个转码任务的信息:
typescript
import { Injectable } from '@nestjs/common';
import { exec } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class VideoService {
private ffmpegProcesses: Map<string, ProcessData> = new Map();
// 其他方法...
}
执行转码
接下来,编写一个方法来启动FFmpeg转码,并将相关信息存储到Map
中:
bash
const command = `sudo ffmpeg -i ${inputUrl} -vf "scale=640:360" -c:v libx264 -preset veryfast -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 -c:a aac -b:a 160k -ac 2 -ar 44100 -f hls -hls_time 10 -hls_list_size 6 -hls_flags delete_segments -hls_segment_filename "${outputDir}/%05d.ts" ${outputDir}/playlist.m3u8`;
const process = exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
}
});
stream.service.ts的全部代码
接下来,编写一个方法来启动FFmpeg转码,并将相关信息存储到Map
中:
typescript
import { Injectable } from '@nestjs/common';
import { exec, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';
// 定义接口
interface ProcessData {
inputUrl: string;
outputDir: string;
process: ChildProcess;
}
@Injectable()
export class StreamService {
private ffmpegProcesses: Map<string, ProcessData> = new Map();
async startStreaming(
inputUrl: string,
): Promise<object> {
for (const [key, value] of this.ffmpegProcesses) {
if(inputUrl === value.inputUrl){
return {
url:`${value.outputDir}`,
message:'已存在转换链接'
}
}
}
const streamId = uuidv4();
const outputDir = path.join('/outHls', streamId);
// 创建输出目录并设置权限
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
fs.chmodSync(outputDir, 0o777); // 设置权限为 777
}
if (this.ffmpegProcesses.has(streamId)) {
throw new Error('Stream already exists');
}
// 设置较低的分辨率,例如 640x360
const command = `sudo ffmpeg -i ${inputUrl} -vf "scale=640:360" -c:v libx264 -preset veryfast -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 -c:a aac -b:a 160k -ac 2 -ar 44100 -f hls -hls_time 10 -hls_list_size 6 -hls_flags delete_segments -hls_segment_filename "${outputDir}/%05d.ts" ${outputDir}/playlist.m3u8`;
const process = exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
this.ffmpegProcesses.delete(streamId);
this.cleanupOutputDirectory(outputDir);
}
});
this.ffmpegProcesses.set(streamId, {
inputUrl:inputUrl,
outputDir:`/outHls/${streamId}/playlist.m3u8`,
process:process
});
process.on('exit', () => {
this.ffmpegProcesses.delete(streamId);
this.cleanupOutputDirectory(outputDir);
});
return {
streamId,
url:`/outHls/${streamId}/playlist.m3u8`,
message:'转换成功'
};
}
stopStreaming(streamId: string): void {
const {process,inputUrl,outputDir} = this.ffmpegProcesses.get(streamId);
if (process) {
process.kill();
this.ffmpegProcesses.delete(streamId);
const outputDir = path.join('/outHls', streamId);
this.cleanupOutputDirectory(outputDir);
}
}
findAll() {
let res = [];
for (let [key, value] of this.ffmpegProcesses.entries()) {
console.log(key + ' = ' + value);
res.push({
id:key,
url:value.outputDir
});
}
return res;
}
private cleanupOutputDirectory(directory: string) {
if (fs.existsSync(directory)) {
fs.rmdirSync(directory, { recursive: true });
}
}
}
5、在线测试
接口说明
- 启动转码
-
- URL :
http://nest.mx002.cn/stream/start?url=rtmp://8.134.173.101:1935/live/stream
- 方法: GET
- 参数:
- URL :
-
-
url
(必填): 要拉取并转码的视频流地址。
-
-
- 返回 : JSON对象,包含生成的唯一
streamId
,用于后续操作。
- 返回 : JSON对象,包含生成的唯一
- 取消转码
-
- URL :
http://nest.mx002.cn/stream/stop?streamId=88e6d7af-ffce-40a2-8326-230c7c70a91a
- 方法: GET
- 参数:
- URL :
-
-
streamId
(必填): 之前启动转码时返回的唯一标识符。
-
-
- 返回: 状态信息,指示操作是否成功。
- 获取所有转码任务
-
- URL :
http://nest.mx002.cn/stream/all
- 方法: GET
- 返回: 当前所有正在进行的转码任务列表。
- URL :
项目地址:
配置Nginx进行推流和拉流:
可以通过Nginx并使用nginx-rtmp-module
模块配置简单的流媒体服务器实现对rtmp推流和拉流
推流和拉流工具
推流软件: 使用OBS (Open Broadcaster Software) 进行视频流推送。
拉流软件: 使用VLC Media Player进行视频流的接收和播放。
6、最后
希望当前内容对大家有用!