ffmpeg:无法将 HLS 流保存到 MKV

ffmpeg: cannot save HLS stream to MKV

我正在尝试实现一些直截了当的事情:编写捕获视频流的代码并将其“按原样”保存到 *.mkv 文件中(是​​的,没有解复用或重新编码或其他)。只想存储那些 AVPacket-s,而 MKV 容器看起来已准备就绪。

注意问题是关于ffmpeg library 用法,ffmpeg binary 工作正常,可以用来保存HLS steam 数据通过以下内容:
ffmpeg -i https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8 -c:v copy out.ts
我知道,但目标是保存 any(或几乎任何)流,因此是 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;
}

这里发生了什么?简单地说:

  1. 产生了两个线程,一个从源读取数据包并将它们存储在缓冲区中
  2. 作者等待reader得到AVCodecParameters,所以你可以看到它们是一样的被使用,这里几乎没有手动设置参数
  3. reader本应读取N个包完成,然后作者跟随他。这就是它与 RTSP、RTMP、MJPEG 等一起工作的方式。

有什么问题吗?尝试 HLS 流后,出现以下错误:

Tag [27][0][0][0] incompatible with output codec id '27' (H264)

在那之后作者通过它的上下文(即这里的avformat_write_header)对任何写入尝试进行段错误 avformat_write_header失败并出现错误(参见下面的UPD2 ) 因此不可能有成功的写入操作。

尝试过的内容:

  1. 强制任意编解码器(例如:AV_CODEC_ID_H264)。运气不好。
  2. 正在尝试 AV_CODEC_ID_MPEGTS。没办法,它被记录为内部需要的“假”编解码器。
  3. 切换一些输入或输出上下文的多个选项,运气不好

我现在很困惑,因为错误听起来像“标签 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 可以 做这个技巧并且可以达到预期的结果

UPD2

发生了可以通过
抑制标签错误的情况 out_ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
我认为在字符串标签中正确拼写“h264”是错误的,看起来并不严重。

此外,仔细观察后发现 av_write_frame 实际上是段错误。难怪——HLS 流 avformat_write_header 失败并且 returns 错误:

Invalid data found when processing input

这仍然让我毫无头绪,问题出在哪里=((

好的...在调试和搜索答案方面付出了巨大努力后,看起来一个食谱,而且并不那么复杂。
我会把它留在这里,这样如果其他人偶然发现了同样的魔法,他就不会走神。

首先 already contains a critical detail that one should know when trying remuxing into MKV. The answer from FFMPEG's maintainer 那里是相当准确的。

但是...

  1. AVCodecContext 在某种程度上是 强制性的 。也许这对每个人来说都是显而易见的,但对我来说却不是。将输入流的 codecpar 直接复制到输出流的 codecpar 看起来很自然。好吧,可能不是一些盲目的复制,ffmpeg 文档警告不要这样做,但这些仍然是 AVCodecParameters,为什么不呢? las,如果不打开编解码器上下文,代码就无法正常工作。
  2. AV_CODEC_FLAG_GLOBAL_HEADER肯定是解决的关键。在 AVOutputFormat::flags 中提到了 AVFMT_GLOBALHEADER,但是使用它的确切方法(可以在 ffmpeg 源和示例中找到)如下面的代码片段所示
  3. 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
    // ....
}