[英]Precise method of segmenting & transcoding video+audio (via ffmpeg), into an on-demand HLS stream?
最近我一直在搞乱 FFMPEG 并通过 Nodejs 流式传输。 我的最终目标是通过 HTTP 提供转码的视频 stream - 来自任何输入文件类型 - 根据分段的需要实时生成。
我目前正在尝试使用 HLS 来处理这个问题。 我使用输入视频的已知持续时间预先生成了一个虚拟 m3u8 清单。 它包含一堆指向单个恒定持续时间段的 URL。 然后,一旦客户端播放器开始请求各个 URL,我使用请求的路径来确定客户端需要哪个时间范围的视频。 然后我将视频和 stream 转码回给他们。
现在解决问题:这种方法大多有效,但有一个小的音频错误。 目前,对于大多数测试输入文件,我的代码生成的视频 - 虽然可播放 - 在每个片段的开头似乎有一个非常小的(<.25 秒)音频跳过。
我认为这可能是在 ffmpeg 中使用时间分割的问题,其中音频 stream 可能无法在视频的确切帧处准确切片。 到目前为止,我一直无法找到解决此问题的方法。
如果有人有任何方向,他们可以指导我 - 甚至是解决这个用例的现有库/服务器 - 我很感激指导。 我对视频编码的了解相当有限。
我将在下面包含我相关当前代码的示例,以便其他人可以看到我卡在哪里。 您应该能够将其作为 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}`);
我和你有同样的问题,正如我在评论中提到的那样,我已经设法解决了这个问题,方法是启动完整的 HLS 转码,而不是手动执行客户端请求的段。 我将简化我所做的工作,并分享我的 github repo 的链接,我已经在其中实现了这个。 我和你一样生成 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();
当您对视频进行转码时,这可以正常工作(例如,稍后在 ffmpeg 命令中使用 libx264 作为视频编码器)。 如果您使用视频编解码器副本,则片段将与我的测试中的 segmentDuration 不匹配。 现在您可以在这里选择,或者在请求 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();
其中 output 选项为:
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',
];
和输入选项:
let inputOptions = [
'-copyts', // Fixes timestamp issues (Keep timestamps as original file)
'-threads 8',
`-ss ${this.startSegment * Transcoding.SEGMENT_DURATION}`
];
值得注意的参数是-start_number
选项中的 -start_number ,这基本上告诉 ffmpeg 第一个段使用哪个编号,如果客户端请求例如段 500,我们希望保持简单,如果必须重新启动,则从 500 开始编号转码。 然后我们有标准的 HLS 设置(hls_time、hls_playlist_type 和 f)。 在输入选项中,我使用-ss
来查找请求的转码,因为我们知道我们在生成的 m3u8 清单中告诉客户端每个段长 4 秒,我们可以只查找 4 * requestedSegment。
您可以在 ffmpeg 的“进度”事件中看到我通过查看时间标记来计算最新处理的段。 通过将时间标记转换为秒,然后为转码添加应用的寻道时间,我们可以通过将秒数除以我设置为 4 的片段持续时间来近似计算刚刚完成的片段。
现在,除了这些,还有更多需要跟踪的内容,您必须保存已启动的 ffmpeg 进程,以便检查段是否已完成以及在请求段时转码是否处于活动状态。 如果用户在很远的将来请求段,您还必须停止已经运行的转码,以便您可以使用正确的寻道时间重新启动它。
这种方法的缺点是文件实际上正在被转码并在转码运行时保存到您的文件系统,因此您需要在用户停止请求段时删除文件。
我已经实现了这个,所以它可以处理我提到的事情(长时间搜索,不同的分辨率请求,等到段完成等)。 如果您想查看它,它位于此处: Github Dose ,最有趣的文件是转码 class , hlsManger class和段的端点我试着尽可能地解释这一点,所以我希望你能把它作为某种基础或想法来推动前进。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.