Fel*_*lix 5 video transcode ffmpeg node.js http-live-streaming
最近我一直在摆弄 FFMPEG 和通过 Nodejs 进行流传输。我的最终目标是通过 HTTP 提供来自任何输入文件类型的转码视频流,根据分段需要实时生成。
我目前正在尝试使用 HLS 来处理这个问题。我使用输入视频的已知持续时间预先生成一个虚拟 m3u8 清单。它包含一堆指向各个恒定持续时间段的 URL。然后,一旦客户端播放器开始请求各个 URL,我就会使用请求的路径来确定客户端需要哪个时间范围的视频。然后我对视频进行转码并将该片段传输回给他们。
现在解决问题:这种方法大部分有效,但有一个小的音频错误。目前,对于大多数测试输入文件,我的代码生成的视频虽然可播放,但在每个片段的开头似乎有一个非常小的(< 0.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}`);
Run Code Online (Sandbox Code Playgroud)
我遇到了和你一样的问题,正如我在评论中提到的那样,我已经成功解决了这个问题,方法是启动完整的 HLS 转码,而不是手动执行客户端请求的段。我将简化我所做的事情,并分享我实现此操作的 github 存储库的链接。我做了与您相同的操作来生成 m3u8 清单:
const segmentDur = 4; // Segment duration in seconds
const splits = Math.max(duration / segmentDur); // duration = duration of the video in seconds
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';
for (let i = 0; i < splits; i++) {
out += `#EXTINF:${segmentDur}, nodesc\n/api/video/${id}/hls/${quality}/segments/${i}.ts?segments=${splits}&group=${group}&audioStream=${audioStream}&type=${type}\n`;
}
out += '#EXT-X-ENDLIST\n';
res.send(out);
resolve();
Run Code Online (Sandbox Code Playgroud)
当您对视频进行转码时(即稍后在 ffmpeg 命令中使用 libx264 作为视频编码器),这可以正常工作。如果您使用视频编解码器复制,则片段将与我的测试中的片段持续时间不匹配。现在您可以选择,要么在请求 m3u8 清单时开始 ffmpeg 转码,要么等到请求第一个片段。我选择了第二个选项,因为我想支持根据请求的段开始转码。
现在是棘手的部分,在我的情况下,当客户端请求片段时,api/video/${id}/hls/<quality>/segments/<segment_number>.ts您必须首先检查是否有任何转码已经处于活动状态。如果转码处于活动状态,您必须检查请求的片段是否已被处理。如果它已被处理,我们可以简单地将请求的段发送回客户端。如果尚未处理(例如由于用户查找操作),我们可以等待它(如果最新处理的段接近请求的段),或者我们可以停止之前的转码并在新请求的段处重新启动。
我会尽量让这个答案尽可能简单,我用来实现 HLS 转码的 ffmpeg 命令如下所示:
this.ffmpegProc = ffmpeg(this.filePath)
.withVideoCodec(this.getVideoCodec())
.withAudioCodec(audioCodec)
.inputOptions(inputOptions)
.outputOptions(outputOptions)
.on('end', () => {
this.finished = true;
})
.on('progress', progress => {
const seconds = this.addSeekTimeToSeconds(this.timestampToSeconds(progress.timemark));
const latestSegment = Math.max(Math.floor(seconds / Transcoding.SEGMENT_DURATION) - 1); // - 1 because the first segment is 0
this.latestSegment = latestSegment;
})
.on('start', (commandLine) => {
logger.DEBUG(`[HLS] Spawned Ffmpeg (startSegment: ${this.startSegment}) with command: ${commandLine}`);
resolve();
})
.on('error', (err, stdout, stderr) => {
if (err.message != 'Output stream closed' && err.message != 'ffmpeg was killed with signal SIGKILL') {
logger.ERROR(`Cannot process video: ${err.message}`);
logger.ERROR(`ffmpeg stderr: ${stderr}`);
}
})
.output(this.output)
this.ffmpegProc.run();
Run Code Online (Sandbox Code Playgroud)
其中输出选项为:
return [
'-copyts', // Fixes timestamp issues (Keep timestamps as original file)
'-pix_fmt yuv420p',
'-map 0',
'-map -v',
'-map 0:V',
'-g 52',
`-crf ${this.CRF_SETTING}`,
'-sn',
'-deadline realtime',
'-preset:v ultrafast',
'-f hls',
`-hls_time ${Transcoding.SEGMENT_DURATION}`,
'-force_key_frames expr:gte(t,n_forced*2)',
'-hls_playlist_type vod',
`-start_number ${this.startSegment}`,
'-strict -2',
'-level 4.1', // Fixes chromecast issues
'-ac 2', // Set two audio channels. Fixes audio issues for chromecast
'-b:v 1024k',
'-b:a 192k',
];
Run Code Online (Sandbox Code Playgroud)
和输入选项:
let inputOptions = [
'-copyts', // Fixes timestamp issues (Keep timestamps as original file)
'-threads 8',
`-ss ${this.startSegment * Transcoding.SEGMENT_DURATION}`
];
Run Code Online (Sandbox Code Playgroud)
值得注意的参数是-start_number输出选项中的 ,这基本上告诉 ffmpeg 第一个分段使用哪个数字,如果客户端请求例如分段 500,我们希望保持简单,如果我们必须重新启动转码,则从 500 开始编号。然后我们有标准的 HLS 设置(hls_time、hls_playlist_type 和 f)。-ss在我用来寻找请求的转码的inputoptions 中,因为我们知道我们在生成的 m3u8 清单中告诉客户端每个段的长度为 4 秒,所以我们可以只寻找 4 * requestSegment。
您可以在 ffmpeg 的“进度”事件中看到,我通过查看时间标记来计算最新处理的片段。通过将时间标记转换为秒,然后添加转码所应用的寻道时间,我们可以通过将秒数除以我设置为 4 的片段持续时间来大致计算出哪个片段刚刚完成。
现在,除了这个之外,还有很多事情需要跟踪,您必须保存已启动的 ffmpeg 进程,以便您可以检查片段是否已完成,以及在请求片段时转码是否处于活动状态。如果用户在很远的将来请求某个片段,您还必须停止已经运行的转码,以便您可以使用正确的查找时间重新启动它。
这种方法的缺点是,在转码运行时,文件实际上正在被转码并保存到您的文件系统中,因此您需要在用户停止请求片段时删除文件。
我已经实现了这个,所以它可以处理我提到的事情(长搜索、不同的分辨率请求、等待段完成等)。如果您想查看它,它位于此处:Github Dose,最有趣的文件是转码类、hlsManger 类和段的端点。我尽力解释这一点,所以我希望您可以将此作为如何前进的某种基础或想法。
| 归档时间: |
|
| 查看次数: |
3338 次 |
| 最近记录: |