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 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
吖秧吖1 小时前
three.js 杂记
开发语言·前端·javascript
前端小超超1 小时前
vue3 ts项目结合vant4 复选框+气泡弹框实现一个类似Select样式的下拉选择功能
前端·javascript·vue.js
大叔是90后大叔1 小时前
vue3中查找字典列表中某个元素的值
前端·javascript·vue.js
IT大玩客1 小时前
JS如何获取MQTT的主题
开发语言·javascript·ecmascript