• 作者:老汪软件技巧
  • 发表时间:2024-10-01 15:02
  • 浏览量:

1、前言

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

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

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

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

采用这种方案的好处在于,它提供了一个更加灵活可控的服务端解决方案,不仅能够满足基本的格式转换需求,还便于后续根据业务需求进行功能拓展。此外,由于整个过程是在后端完成的,因此不会增加前端代码的复杂度,同时也保证了更好的用户体验,因为最终用户只需要面对标准的HLS流即可。

通过这种方式,即使面对复杂的流媒体处理场景,前端开发者也能够以更简洁的方式集成视频播放功能,而无需深入研究底层协议或担忧兼容性问题。

2、配置环境

1、在Linux上安装FFmpeg

sudo apt update

sudo apt install ffmpeg

ffmpeg -version

执行 ffmpeg -version 命令后,如果FFmpeg成功安装,终端将显示类似以下的内容,确认安装版本及构建信息

为了测试FFmpeg的可用性,并使用它来转换视频流,您可以使用以下命令。请确保替换 ${inputUrl} 为您的输入视频流地址,以及 ${outputDir} 为输出HLS文件的目标目录。该命令将调整视频分辨率至640x360,并设置一系列编码参数以优化输出质量与性能。

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}/d.ts" \
${outputDir}/playlist.m3u8

命令解释:

在执行此命令之前,请确保您有权限写入到${outputDir}所指向的目录。如果一切配置正确且FFmpeg安装无误,上述命令将会开始处理视频流并生成HLS格式的内容。

4、使用NestJS调用FFmpeg对视频流进行转码实现思路

我们将利用Node.js的child_process模块中的exec方法来在Node.js程序中执行外部命令。这样可以方便地调用FFmpeg并处理视频流。为了更好地管理和控制每个FFmpeg进程,我们将使用一个Map对象来存储每个转码任务的信息。

_视频转化flv格式_ffmpeg视频转换

接口定义

首先,定义一个接口来描述存储在Map中的数据结构:

interface ProcessData {
  inputUrl: string; // 输入视频流地址
  outputDir: string; // 输出HLS文件目录
  process: ChildProcess; // FFmpeg进程
}

存储映射

然后,在服务类中创建一个Map实例来保存每个转码任务的信息:

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中:

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}/d.ts" ${outputDir}/playlist.m3u8`;
const process = exec(command, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
  }
});

stream.service.ts的全部代码

接下来,编写一个方法来启动FFmpeg转码,并将相关信息存储到Map中:

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}/d.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、在线测试接口说明项目地址:

/mx0002/Vide…

配置Nginx进行推流和拉流:

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

推流和拉流工具

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

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

6、最后

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