ffmpeg对视频流转换为hls

1、前言

作为一名前端开发者,在项目中经常会遇到需要在网页上播放视频流的情况,比如RTMP或RTSP等格式。不幸的是,这些视频流通常不能直接被现代浏览器支持播放,除非安装了特定的插件。相对而言,HLS(HTTP Live Streaming)和FLV(Flash Video)这样的格式则更为友好,可以直接通过HTML5 <video> 标签来播放。

自今年五月份开始接触视频流处理以来,我断断续续地查阅了不少相关资料。了解到一种常见的解决方案是使用Nginx结合其扩展模块,如nginx-rtmp-module,可以构建一个简易的流媒体服务器,实现从RTMP到HLS格式的转换,从而使得视频流能够在浏览器端无插件播放。

然而,我想分享的是一种不同的方法------利用FFmpeg进行视频流的转换,并且通过Node.js框架NestJS编写一个简单的服务来控制FFmpeg的工作流程。这种方法允许我们:

  1. 从源拉取原始视频流。
  2. 将其转码为HLS格式。
  3. 提供给客户端播放。
  4. 在不再需要时中断拉流并释放资源。

采用这种方案的好处在于,它提供了一个更加灵活可控的服务端解决方案,不仅能够满足基本的格式转换需求,还便于后续根据业务需求进行功能拓展。此外,由于整个过程是在后端完成的,因此不会增加前端代码的复杂度,同时也保证了更好的用户体验,因为最终用户只需要面对标准的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 (必填): 要拉取并转码的视频流地址。
    • 返回 : JSON对象,包含生成的唯一streamId,用于后续操作。
  • 取消转码
    • URL : http://nest.mx002.cn/stream/stop?streamId=88e6d7af-ffce-40a2-8326-230c7c70a91a
    • 方法: GET
    • 参数:
      • streamId (必填): 之前启动转码时返回的唯一标识符。
    • 返回: 状态信息,指示操作是否成功。
  • 获取所有转码任务
    • URL : http://nest.mx002.cn/stream/all
    • 方法: GET
    • 返回: 当前所有正在进行的转码任务列表。
项目地址:

gitee.com/mx0002/Vide...

配置Nginx进行推流和拉流:

可以通过Nginx并使用nginx-rtmp-module模块配置简单的流媒体服务器实现对rtmp推流和拉流

推流和拉流工具

推流软件: 使用OBS (Open Broadcaster Software) 进行视频流推送。

拉流软件: 使用VLC Media Player进行视频流的接收和播放。

6、最后

希望当前内容对大家有用!

相关推荐
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun1 小时前
vue VueResource & axios
前端·javascript·vue.js
J总裁的小芒果1 小时前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
浮游本尊1 小时前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
新中地GIS开发老师1 小时前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
Cachel wood2 小时前
Django REST framework (DRF)中的api_view和APIView权限控制
javascript·vue.js·后端·python·ui·django·前端框架
放逐者-保持本心,方可放逐3 小时前
SSE 流式场景应用 及 方案总结
javascript·axios·fetch·eventsource
白云~️3 小时前
uniappX 移动端单行/多行文字隐藏显示省略号
开发语言·前端·javascript
小华同学ai3 小时前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office