最近我一直在搞乱 FFMPEG 并通过 Nodejs 流式传输。我的最终目标是通过 HTTP 提供转码的视频流 - 来自任何输入文件类型 - 根据分段的需要实时生成。
我目前正在尝试使用 HLS 来处理这个问题。我使用输入视频的已知持续时间预先生成了一个虚拟 m3u8 清单。它包含一堆指向单个恒定持续时间段的 URL。然后,一旦客户端播放器开始请求各个 URL,我使用请求的路径来确定客户端需要哪个时间范围的视频。然后我对视频进行转码并将该片段流回给他们。
现在解决问题:这种方法大多有效,但有一个小的音频错误。目前,对于大多数测试输入文件,我的代码生成的视频 - 虽然可播放 - 在每个片段的开头似乎有一个非常小的(< .25 秒)音频跳过。
我认为这可能是在 ffmpeg 中使用时间分割的问题,其中音频流可能无法在视频的确切帧处准确切片。到目前为止,我一直无法找到解决此问题的方法。
如果有人有任何方向,他们可以指导我 - 甚至是解决这个用例的现有库/服务器 - 我很感激指导。我对视频编码的了解相当有限。
我将在下面包含我相关当前代码的示例,以便其他人可以看到我卡在哪里。您应该能够将其作为 Nodejs Express 服务器运行,然后将任何 HLS 播放器指向 localhost:8080/master 以加载清单并开始播放。有关相关的转码位,请参见transcode.get('/segment/:seg.ts'
末尾的行。
'use strict';
const express = require('express');
const ffmpeg = require('fluent-ffmpeg');
let PORT = 8080;
let HOST = 'localhost';
const transcode = express();
/*
* This file demonstrates an Express-based server, which transcodes & streams a video file.
* All transcoding is handled in memory, in chunks, as needed by the player.
*
* It works by generating a fake manifest file for an HLS stream, at the endpoint "/m3u8".
* This manifest contains links to each "segment" video clip, which browser-side HLS players will load as-needed.
*
* The "/segment/:seg.ts" endpoint is the request destination for each clip,
* and uses FFMpeg to generate each segment on-the-fly, based off which segment is requested.
*/
const pathToMovie = 'C:\\input-file.mp4'; // The input file to stream as HLS.
const segmentDur = 5; // Controls the duration (in seconds) that the file will be chopped into.
const getMetadata = async(file) => {
return new Promise( resolve => {
ffmpeg.ffprobe(file, function(err, metadata) {
console.log(metadata);
resolve(metadata);
});
});
};
// Generate a "master" m3u8 file, which the player should point to:
transcode.get('/master', async(req, res) => {
res.set({"Content-Disposition":"attachment; filename=\"m3u8.m3u8\""});
res.send(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000
/m3u8?num=1
#EXT-X-STREAM-INF:BANDWIDTH=240000
/m3u8?num=2`)
});
// Generate an m3u8 file to emulate a premade video manifest. Guesses segments based off duration.
transcode.get('/m3u8', async(req, res) => {
let met = await getMetadata(pathToMovie);
let duration = met.format.duration;
let out = '#EXTM3U\n' +
'#EXT-X-VERSION:3\n' +
`#EXT-X-TARGETDURATION:${segmentDur}\n` +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n';
let splits = Math.max(duration / segmentDur);
for(let i=0; i< splits; i++){
out += `#EXTINF:${segmentDur},\n/segment/${i}.ts\n`;
}
out+='#EXT-X-ENDLIST\n';
res.set({"Content-Disposition":"attachment; filename=\"m3u8.m3u8\""});
res.send(out);
});
// Transcode the input video file into segments, using the given segment number as time offset:
transcode.get('/segment/:seg.ts', async(req, res) => {
const segment = req.params.seg;
const time = segment * segmentDur;
let proc = new ffmpeg({source: pathToMovie})
.seekInput(time)
.duration(segmentDur)
.outputOptions('-preset faster')
.outputOptions('-g 50')
.outputOptions('-profile:v main')
.withAudioCodec('aac')
.outputOptions('-ar 48000')
.withAudioBitrate('155k')
.withVideoBitrate('1000k')
.outputOptions('-c:v h264')
.outputOptions(`-output_ts_offset ${time}`)
.format('mpegts')
.on('error', function(err, st, ste) {
console.log('an error happened:', err, st, ste);
}).on('progress', function(progress) {
console.log(progress);
})
.pipe(res, {end: true});
});
transcode.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);