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