c++ - ffmpeg:无法将 HLS 流保存到 MKV
问题描述
我正在尝试实现一些简单的事情:编写捕获视频流并将其“按原样”保存到 *.mkv 文件中的代码(是的,没有解复用或重新编码等)。只想存储那些AVPacket
-s 并且 MKV 容器看起来已经准备好了。
请注意,问题是关于 ffmpeg库的使用,ffmpeg二进制文件工作正常,可用于通过以下方式保存 HLS 流数据:
ffmpeg -i https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8 -c:v copy out.ts
我知道,但目标是保存任何(或几乎任何)流,因此是 MKV。实际上,有一些代码已经可以保存流的数据,特别是在使用 HLS 尝试时它会失败。
经过一些努力以提供一个简短但可读的 MCVE,这里有一个重现问题的示例代码。重点是使输出编解码器与 HLS 流一起工作,因此它可能缺少很多东西和细节,例如额外的错误检查、极端情况、优化、正确的时间戳处理等。
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavfilter/buffersink.h"
#include "libavfilter/buffersrc.h"
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
}
// Some public stream. The code works with RTSP, RTMP, MJPEG, etc.
// static const char SOURCE_NAME[] = "http://81.83.10.9:8001/mjpg/video.mjpg"; // works!
// My goal was an actual cam streaming via HLS, but here are some random HLS streams
// that reproduce the problem quite well. Playlists may differ, but the error is exactly the same
static const char SOURCE_NAME[] = "http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8"; // fails!
// static const char SOURCE_NAME[] = "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8"; // fails!
using Pkt = std::unique_ptr<AVPacket, void (*)(AVPacket *)>;
std::deque<Pkt> frame_buffer;
std::mutex frame_mtx;
std::condition_variable frame_cv;
std::atomic_bool keep_running{true};
AVCodecParameters *common_codecpar = nullptr;
std::mutex codecpar_mtx;
std::condition_variable codecpar_cv;
void read_frames_from_source(unsigned N)
{
AVFormatContext *fmt_ctx = avformat_alloc_context();
int err = avformat_open_input(&fmt_ctx, SOURCE_NAME, nullptr, nullptr);
if (err < 0) {
std::cerr << "cannot open input" << std::endl;
avformat_free_context(fmt_ctx);
return;
}
err = avformat_find_stream_info(fmt_ctx, nullptr);
if (err < 0) {
std::cerr << "cannot find stream info" << std::endl;
avformat_free_context(fmt_ctx);
return;
}
// Simply finding the first video stream, preferrably H.264. Others are ignored below
int video_stream_id = -1;
for (unsigned i = 0; i < fmt_ctx->nb_streams; i++) {
auto *c = fmt_ctx->streams[i]->codecpar;
if (c->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_id = i;
if (c->codec_id == AV_CODEC_ID_H264)
break;
}
}
if (video_stream_id < 0) {
std::cerr << "failed to find find video stream" << std::endl;
avformat_free_context(fmt_ctx);
return;
}
{ // Here we have the codec params and can launch the writer
std::lock_guard<std::mutex> locker(codecpar_mtx);
common_codecpar = fmt_ctx->streams[video_stream_id]->codecpar;
}
codecpar_cv.notify_all();
unsigned cnt = 0;
while (++cnt <= N) { // we read some limited number of frames
Pkt pkt{av_packet_alloc(), [](AVPacket *p) { av_packet_free(&p); }};
err = av_read_frame(fmt_ctx, pkt.get());
if (err < 0) {
std::cerr << "read packet error" << std::endl;
continue;
}
// That's why the cycle above, we write only one video stream here
if (pkt->stream_index != video_stream_id)
continue;
{
std::lock_guard<std::mutex> locker(frame_mtx);
frame_buffer.push_back(std::move(pkt));
}
frame_cv.notify_one();
}
keep_running.store(false);
avformat_free_context(fmt_ctx);
}
void write_frames_into_file(std::string filepath)
{
AVFormatContext *out_ctx = nullptr;
int err = avformat_alloc_output_context2(&out_ctx, nullptr, "matroska", filepath.c_str());
if (err < 0) {
std::cerr << "avformat_alloc_output_context2 failed" << std::endl;
return;
}
AVStream *video_stream = avformat_new_stream(out_ctx, avcodec_find_encoder(common_codecpar->codec_id)); // the proper way
// AVStream *video_stream = avformat_new_stream(out_ctx, avcodec_find_encoder(AV_CODEC_ID_H264)); // forcing the H.264
// ------>> HERE IS THE TROUBLE, NO CODEC WORKS WITH HLS <<------
int video_stream_id = video_stream->index;
err = avcodec_parameters_copy(video_stream->codecpar, common_codecpar);
if (err < 0) {
std::cerr << "avcodec_parameters_copy failed" << std::endl;
}
if (!(out_ctx->flags & AVFMT_NOFILE)) {
err = avio_open(&out_ctx->pb, filepath.c_str(), AVIO_FLAG_WRITE);
if (err < 0) {
std::cerr << "avio_open fail" << std::endl;
return;
}
}
err = avformat_write_header(out_ctx, nullptr); // <<--- ERROR WITH HLS HERE
if (err < 0) {
std::cerr << "avformat_write_header failed" << std::endl;
return; // here we go with hls
}
unsigned cnt = 0;
while (true) {
std::unique_lock<std::mutex> locker(frame_mtx);
frame_cv.wait(locker, [&] { return !frame_buffer.empty() || !keep_running; });
if (!keep_running)
break;
Pkt pkt = std::move(frame_buffer.front());
frame_buffer.pop_front();
++cnt;
locker.unlock();
pkt->stream_index = video_stream_id; // mandatory
err = av_write_frame(out_ctx, pkt.get());
if (err < 0) {
std::cerr << "av_write_frame failed " << cnt << std::endl;
} else if (cnt % 25 == 0) {
std::cout << cnt << " OK" << std::endl;
}
}
av_write_trailer(out_ctx);
avformat_free_context(out_ctx);
}
int main()
{
std::thread reader(std::bind(&read_frames_from_source, 1000));
std::thread writer;
// Writer wont start until reader's got AVCodecParameters
// In this example it spares us from setting writer's params properly manually
{ // Waiting for codec params to be set
std::unique_lock<std::mutex> locker(codecpar_mtx);
codecpar_cv.wait(locker, [&] { return common_codecpar != nullptr; });
writer = std::thread(std::bind(&write_frames_into_file, "out.mkv"));
}
reader.join();
keep_running.store(false);
writer.join();
return 0;
}
这里会发生什么?简单的说:
- 产生了两个线程,一个从源读取数据包并将它们存储在缓冲区中
- 作者等待读者获取
AVCodecParameters
,这样你就可以看到它们被使用的是相同的,这里几乎没有手动参数设置 - 读者应该阅读N个数据包并完成,然后作者跟随他。这就是它与 RTSP、RTMP、MJPEG 等一起工作的方式。
有什么问题?尝试 HLS 流后,会出现以下错误:
标签 [27][0][0][0] 与输出编解码器 id '27' (H264) 不兼容
之后,作者通过其上下文(即 avformat_write_header
此处)avformat_write_header
对任何写入尝试进行段错误,并出现错误(请参阅下面的 UPD2),因此不可能进行成功的写入操作。
尝试过的内容:
- 强制任意编解码器(例如:)
AV_CODEC_ID_H264
。那里没有运气。 - 尝试
AV_CODEC_ID_MPEGTS
. 没办法,它被记录为内部需求的“假”编解码器。 - 切换输入或输出上下文的多个选项中的一些,那里没有运气
我现在很困惑,因为错误听起来像“标签 H264 与编解码器 H264 不兼容”。ffmpeg 日志看起来像库设法理解它正在处理通过 HLS 发送的 MPEG-TS,读取很好,但写入所选媒体容器失败:
[hls @ 0x7f94b0000900] Opening 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/540_1200000/hls/segment_0.ts' for reading
[hls @ 0x7f94b0000900] Opening 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/540_1200000/hls/segment_1.ts' for reading
[hls @ 0x7f94b0000900] Opening 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/hls/segment_0.ts' for reading
[hls @ 0x7f94b0000900] Opening 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/hls/segment_1.ts' for reading
[hls @ 0x7f94b0000900] Opening 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/1080_4800000/hls/segment_0.ts' for reading
[hls @ 0x7f94b0000900] Opening 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/1080_4800000/hls/segment_1.ts' for reading
[hls @ 0x7f94b0000900] Could not find codec parameters for stream 0 (Audio: aac ([15][0][0][0] / 0x000F), 0 channels, 112 kb/s): unspecified sample rate
Consider increasing the value for the 'analyzeduration' and 'probesize' options
[matroska @ 0x7f94a8000900] Tag [27][0][0][0] incompatible with output codec id '27' (H264)
avformat_write_header failed
Segmentation fault (core dumped)
谷歌搜索没有帮助,我有点绝望。
请分享您的想法,将不胜感激。
UPD
ffmpeg -i https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8 out.mkv
工作正常ffmpeg -i http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8 -c:v copy out.mkv
也可以正常工作
...这意味着 ffmpeg可以做到这一点并且可以达到预期的结果
UPD2
发生标签错误可以通过
out_ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
我假设在字符串标签中正确拼写“h264”来抑制,看起来并不严重。
此外,仔细观察后发现它av_write_frame
实际上是段错误。难怪 - HLS 流avformat_write_header
失败并返回错误:
处理输入时发现无效数据
这仍然让我没有任何线索,这里的问题在哪里=((
解决方案
Okaaay ...经过大量调试和寻找答案后,看起来有一个秘诀,它并不那么复杂。
我会把它留在这里,这样如果其他人偶然发现了同样的魔法,他就不会走神了。
首先,这个问题已经包含一个关键细节,在尝试重新混合到 MKV 时应该知道这一点。FFMPEG 维护者的回答在那里非常准确。
但...
AVCodecContext
不知何故是强制性的。也许这对每个人来说都是显而易见的,但对我来说不是。codecpar
将输入流直接复制到输出流中看起来很自然codecpar
。好吧,可能不是一些盲目的复制,ffmpeg 文档警告不要这样做,但这些仍然是AVCodecParameters
,为什么不呢?唉,如果不打开编解码器上下文,代码就无法正常工作。AV_CODEC_FLAG_GLOBAL_HEADER
肯定是解决问题的关键。AVOutputFormat::flags
about中有提及AVFMT_GLOBALHEADER
,但是使用它的确切方法(可以在 ffmpeg 源代码和示例中找到)如下面的代码片段所示FF_COMPLIANCE_UNOFFICIAL
对于相当多的 hls 流(至少手头的流)似乎也是强制性的,否则 ffmpeg 认为代码试图在不同的编解码器之间重新混合数据包(是的,因为编解码器名称拼写),这略有不同故事。-c:v copy
假设它在指定和不指定的情况下使用 ffmpeg 工具之间的区别。
这是我的代码的必要更新,使一切按预期工作:
void write_frames_into_file(std::string filepath)
{
AVFormatContext *out_ctx = nullptr;
int err = avformat_alloc_output_context2(&out_ctx, nullptr, "matroska", filepath.c_str());
if (err < 0) {
std::cerr << "avformat_alloc_output_context2 failed" << std::endl;
return;
}
out_ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; // !!! (3)
AVCodec* codec = avcodec_find_encoder(common_codecpar->codec_id);
AVStream *video_stream = avformat_new_stream(out_ctx, codec); // the proper way
int video_stream_id = video_stream->index;
AVCodecContext *encoder = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(encoder, common_codecpar);
encoder->time_base = time_base;
encoder->framerate = frame_rate;
if (out_ctx->oformat->flags & AVFMT_GLOBALHEADER) // !!! (2)
encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
err = avcodec_open2(encoder, codec, nullptr); // !!! (1)
if (err < 0) {
std::cerr << "avcodec_open2 failed" << std::endl;
return;
}
err = avcodec_parameters_from_context(video_stream->codecpar, encoder);
if (err < 0) {
std::cerr << "avcodec_parameters_from_context failed" << std::endl;
return;
}
if (!(out_ctx->flags & AVFMT_NOFILE)) {
err = avio_open(&out_ctx->pb, filepath.c_str(), AVIO_FLAG_WRITE);
if (err < 0) {
std::cerr << "avio_open fail" << std::endl;
return;
}
}
err = avformat_write_header(out_ctx, nullptr);
if (err < 0) {
char ffmpeg_err_buf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(&ffmpeg_err_buf[0], AV_ERROR_MAX_STRING_SIZE, err);
std::cerr << "avformat_write_header failed: " << ffmpeg_err_buf << std::endl;
return;
}
// ....
// Writing AVPackets here, as in the question, or the other way you wanted to do it
// ....
}
推荐阅读
- python - ValueError:找不到与“测试”对应的任何实体
- prometheus - 如何检查我的 prometheus.yml 是否有错误?
- html - backgroundVideo' 已定义但从未使用 no-unused-vars (ReactJS)
- javascript - html css js 按钮打印功能
- flutter - 图像资源服务捕获的异常 - Crashlytics 非致命错误 - http 请求失败
- html - 为什么媒体查询不适用于 0px 和 767px 之间的范围?
- javascript - 在结果VueJS中使搜索的字符/单词变粗
- c - 我们如何在c语言中同时从键盘读取两个字符
- javascript - FabricJS 仅从画布背景中获取图像部分
- docker - 我应该创建一个 docker 容器还是 docker 启动一个停止的容器?