使用 Node JS、FFMPEG 和 Bento4 进行流式传输的视频编码器

本文我们将使用 Bento4 和 ffmpeg 制作一个强大的视频编码解决方案。相比《使用 Node JS 和 FFMPEG 编写视频编码器》,唯一的变化是,上次我们使用 ffmpeg 完成了包括视频分割在内的所有编码操作,而这次将使用 Bento4 完成这部分操作。因此,将视频转换为不同码率的输出将由 ffmpeg 完成,而将编码视频集分割并转换为 dash 格式将由 bento4 完成。

使用 Node JS、FFMPEG 和 Bento4 进行流式传输的视频编码器

现在您可能想知道什么是转换为片段视频。普通的 mp4 文件和片段式 mp4 文件并不相似。片段式 MP4 包含一系列片段,如果您的服务器支持字节范围请求,可以单独请求这些片段。MP4 文件是由许多称为原子(或 “盒子”)的离散单元组成的。因此,普通 mp4 和片段式 mp4 中的原子排列是不同的。

第一步,安装 Bento4。我们将在本地环境中安装 Bento4,对于 Mac 用户,只需执行以下命令:

brew install bento4

对于其他操作系统,请访问 https://www.bento4.com/downloads/ 下载压缩文件,并在路径中添加 bento4 bin 文件夹的路径。

现在,正如我之前提到的有 3 件事要做:

  • 将视频编码为不同的分辨率和比特率
  • 将编码后的视频进行分段
  • 快速转换这些分段文件

让我们先解决第一个问题。创建一个名为 encoder.js 的文件,并添加以下几行。

const ffmpegStatic = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
const fs = require("fs");

ffmpeg.setFfmpegPath(ffmpegStatic);

const bitrates = [
  {
    resolution: "1280x720",
    videoBitrate: "1500k",
    audioBitrate: "128k",
    outputName: "output_720p.mp4",
  },
  {
    resolution: "854x480",
    videoBitrate: "500k",
    audioBitrate: "96k",
    outputName: "output_480p.mp4",
  },
  {
    resolution: "640x360",
    videoBitrate: "250k",
    audioBitrate: "64k",
    outputName: "output_360p.mp4",
  },
];

const encodeVideo = (inputVideo, outputFolder, config) => {
  return new Promise((resolve, reject) => {
    ffmpeg(inputVideo)
      .videoCodec("libx264")
      .audioCodec("aac")
      .videoBitrate(config.videoBitrate)
      .audioBitrate(config.audioBitrate)
      .size(config.resolution)
      .output(`${outputFolder}/${config.outputName}`)
      .on("end", () => {
        console.log(`Finished encoding ${config.outputName}`);
        resolve(`${outputFolder}/${config.outputName}`);
      })
      .on("error", (err) => {
        console.error(`Error encoding ${config.outputName}: ${err}`);
        reject(err);
      })
      .run();
  });
};

const encoder = async () => {
  const inputVideo = "input.mp4";
  const outputDirectory = "output_folder";

  const permissions = 0o777;

  try {
    if (fs.existsSync(outputDirectory)) {
      fs.rmSync(outputDirectory, { recursive: true });
    }

    fs.mkdirSync(outputDirectory);
    fs.chmodSync(outputDirectory, permissions);

    const encodingQueue = [];

    for (const config of bitrates) {
      encodingQueue.push(encodeVideo(inputVideo, outputDirectory, config));
    }

    const outPutVideoFiles = await Promise.all(encodingQueue);
    console.log("All encoding tasks completed.");
  } catch (err) {
    console.error("Error during encoding:", err);
  }
};

encoder();

现在,我们有了一个名为 encoder 的函数,它正在运行。它的基本功能是创建一个名为 output_folder 的文件夹来存储编码文件,然后调用不同比特率的编码函数。因此,ffmpeg 运行不会立即给出结果,它有回调方法,我们必须等待它运行。因此,为了确认所有媒体都已编码,我们使用了 Javascript Promises。因此,一个编码任务将返回一个承诺,只有当所有承诺都完成时,我们才会打印 “所有编码任务已完成”。

现在,下一步就是分割这些已编码的视频。为此,我们将在代码中添加新内容。

const ffmpegStatic = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
const fs = require("fs");
const { exec } = require("child_process");

ffmpeg.setFfmpegPath(ffmpegStatic);

const bitrates = [
  {
    resolution: "1280x720",
    videoBitrate: "1500k",
    audioBitrate: "128k",
    outputName: "output_720p.mp4",
  },
  {
    resolution: "854x480",
    videoBitrate: "500k",
    audioBitrate: "96k",
    outputName: "output_480p.mp4",
  },
  {
    resolution: "640x360",
    videoBitrate: "250k",
    audioBitrate: "64k",
    outputName: "output_360p.mp4",
  },
];

const encodeVideo = (inputVideo, outputFolder, config) => {
  return new Promise((resolve, reject) => {
    ffmpeg(inputVideo)
      .videoCodec("libx264")
      .audioCodec("aac")
      .videoBitrate(config.videoBitrate)
      .audioBitrate(config.audioBitrate)
      .size(config.resolution)
      .output(`${outputFolder}/${config.outputName}`)
      .on("end", () => {
        console.log(`Finished encoding ${config.outputName}`);
        resolve(`${outputFolder}/${config.outputName}`);
      })
      .on("error", (err) => {
        console.error(`Error encoding ${config.outputName}: ${err}`);
        reject(err);
      })
      .run();
  });
};

const fragmentVideo = (inputVideo, outputFolder) => {
  return new Promise((resolve, reject) => {
    const strArr = inputVideo.split("/");
    const fragmentedVideoFile = `${outputFolder}/fragmented_${strArr[1]}`;
    const mp4fragmentCommand = `mp4fragment ${inputVideo} ${fragmentedVideoFile}`;
    exec(mp4fragmentCommand, (error, stdout, stderr) => {
      if (error) {
        console.error(`Error running mp4fragment: ${error.message}`);
        reject(stderr);
      }
      console.log(stdout);
      resolve(fragmentedVideoFile);
    });
  });
};

const encoder = async () => {
  const inputVideo = "input.mp4";
  const outputDirectory = "output_folder";
  const outputDirectoryFragment = "output_fragment";

  const permissions = 0o777;

  try {
    if (fs.existsSync(outputDirectory)) {
      fs.rmSync(outputDirectory, { recursive: true });
    }

    fs.mkdirSync(outputDirectory);
    fs.chmodSync(outputDirectory, permissions);

    const encodingQueue = [];

    for (const config of bitrates) {
      encodingQueue.push(encodeVideo(inputVideo, outputDirectory, config));
    }

    const outPutVideoFiles = await Promise.all(encodingQueue);
    console.log("All encoding tasks completed.");
    if (fs.existsSync(outputDirectoryFragment)) {
      fs.rmSync(outputDirectoryFragment, { recursive: true });
    }

    fs.mkdirSync(outputDirectoryFragment);
    fs.chmodSync(outputDirectoryFragment, permissions);

    const fragmentingQueue = [];

    for (const bitrateFile of outPutVideoFiles) {
      fragmentingQueue.push(
        fragmentVideo(bitrateFile, outputDirectoryFragment)
      );
    }

    const fragmentedFiles = await Promise.all(fragmentingQueue);
    console.log("All fragmenting tasks completed.");
  } catch (err) {
    console.error("Error during encoding:", err);
  }
};

encoder();

为此,我们直接在程序内部执行命令,并使用 “exec “来执行。所有视频编码完成后,我们会创建一个名为 output_fragment 的文件夹来存储分片文件并赋予权限。接下来,就像使用 Promise 编码一样,我要检查是否完成了所有分片工作。这就是 fragmenting 命令完成的简单任务。

mp4fragment INPUT_VIDEO OUTPUT_VIDEO

下一步是对分段视频进行 Dash 编码。这很简单,我们只需调用以下命令:

mp4dash --output-dir=OUTPUT_FOLDER FRAGMENTED_VIDEO1 FRAGMENTED_VIDEO2 ..

因此,代码将这样更改:

const ffmpegStatic = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
const fs = require("fs");
const { exec } = require("child_process");

ffmpeg.setFfmpegPath(ffmpegStatic);

const bitrates = [
  {
    resolution: "1280x720",
    videoBitrate: "1500k",
    audioBitrate: "128k",
    outputName: "output_720p.mp4",
  },
  {
    resolution: "854x480",
    videoBitrate: "500k",
    audioBitrate: "96k",
    outputName: "output_480p.mp4",
  },
  {
    resolution: "640x360",
    videoBitrate: "250k",
    audioBitrate: "64k",
    outputName: "output_360p.mp4",
  },
];

const encodeVideo = (inputVideo, outputFolder, config) => {
  return new Promise((resolve, reject) => {
    ffmpeg(inputVideo)
      .videoCodec("libx264")
      .audioCodec("aac")
      .videoBitrate(config.videoBitrate)
      .audioBitrate(config.audioBitrate)
      .size(config.resolution)
      .output(`${outputFolder}/${config.outputName}`)
      .on("end", () => {
        console.log(`Finished encoding ${config.outputName}`);
        resolve(`${outputFolder}/${config.outputName}`);
      })
      .on("error", (err) => {
        console.error(`Error encoding ${config.outputName}: ${err}`);
        reject(err);
      })
      .run();
  });
};

const fragmentVideo = (inputVideo, outputFolder) => {
  return new Promise((resolve, reject) => {
    const strArr = inputVideo.split("/");
    const fragmentedVideoFile = `${outputFolder}/fragmented_${strArr[1]}`;
    const mp4fragmentCommand = `mp4fragment ${inputVideo} ${fragmentedVideoFile}`;
    exec(mp4fragmentCommand, (error, stdout, stderr) => {
      if (error) {
        console.error(`Error running mp4fragment: ${error.message}`);
        reject(stderr);
      }
      console.log(stdout);
      resolve(fragmentedVideoFile);
    });
  });
};

const dashEncodeVideo = (fragmentedFiles, outputDirectory) => {
  return new Promise((resolve, reject) => {
    const mpdOutputFile = `${outputDirectory}/stream.mpd`;
    let mp4dashCommand = `mp4dash --output-dir=${outputDirectory}`;

    for (const fragmentedFile of fragmentedFiles) {
      mp4dashCommand = mp4dashCommand + ` ${fragmentedFile}`;
    }

    exec(mp4dashCommand, (error, stdout, stderr) => {
      if (error) {
        console.error(`Error running mp4dash: ${error.message}`);
        reject(stderr);
      }

      const mpdContent = fs.readFileSync(mpdOutputFile, "utf8");
      const dashManifest = `<?xml version="1.0" encoding="utf-8"?>
      <MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="dynamic" mediaPresentationDuration="PT0H3M17.13S" maxSegmentDuration="PT0H0M4.800S">
        ${mpdContent}
      </MPD>`;

      const dashManifestFile = `${outputDirectory}/manifest.mpd`;
      fs.writeFileSync(dashManifestFile, dashManifest);
      resolve(dashManifest);
    });
  });
};

const encoder = async () => {
  const inputVideo = "input.mp4";
  const outputDirectory = "output_folder";
  const outputDirectoryDash = "output_dash";
  const outputDirectoryFragment = "output_fragment";

  const permissions = 0o777;

  try {
    if (fs.existsSync(outputDirectory)) {
      fs.rmSync(outputDirectory, { recursive: true });
    }

    fs.mkdirSync(outputDirectory);
    fs.chmodSync(outputDirectory, permissions);

    const encodingQueue = [];

    for (const config of bitrates) {
      encodingQueue.push(encodeVideo(inputVideo, outputDirectory, config));
    }

    const outPutVideoFiles = await Promise.all(encodingQueue);
    console.log("All encoding tasks completed.");
    if (fs.existsSync(outputDirectoryFragment)) {
      fs.rmSync(outputDirectoryFragment, { recursive: true });
    }

    fs.mkdirSync(outputDirectoryFragment);
    fs.chmodSync(outputDirectoryFragment, permissions);

    const fragmentingQueue = [];

    for (const bitrateFile of outPutVideoFiles) {
      fragmentingQueue.push(
        fragmentVideo(bitrateFile, outputDirectoryFragment)
      );
    }

    const fragmentedFiles = await Promise.all(fragmentingQueue);
    console.log("All fragmenting tasks completed.");

    if (fs.existsSync(outputDirectoryDash)) {
      fs.rmSync(outputDirectoryDash, { recursive: true });
    }
    const dashManifest = await dashEncodeVideo(
      fragmentedFiles,
      outputDirectoryDash
    );
    console.log("Encoding successfully completed");
    console.log(dashManifest);
  } catch (err) {
    console.error("Error during encoding:", err);
  }
};

encoder();

现在,如果运行这段代码,就会在控制台日志输出中看到最终的清单 mpd 文件。如果您参考之前的教程,我创建了一个 index.html 文件和一个 server.js 文件来渲染视频。使用该文件,您也可以在这里查看仪表盘格式的视频。

Github 代码:https://github.com/deBilla/bento-encoder

作者:Dimuthu Wickramanayake
源自:https://medium.com/nerd-for-tech/writing-a-video-encoder-using-node-js-ffmpeg-and-bento4-a4a04033dfa1

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

(0)

相关推荐

发表回复

登录后才能评论