简体   繁体   English

ffmpeg:无法将 HLS 流保存到 MKV

[英]ffmpeg: cannot save HLS stream to MKV

I am trying to achieve something straightforward: writing the code that captures a video stream and saves it into an *.mkv file "as-is" (yeah, no demuxing or reencoding or whatever).我试图实现一些简单的事情:编写捕获视频流并将其“按原样”保存到 *.mkv 文件的代码(是的,没有解复用或重新编码或其他)。 Just want to store those AVPacket -s and the MKV container looks ready for that.只想存储那些AVPacket -s 并且 MKV 容器看起来已经准备好了。

Note that the question is about ffmpeg library usage, the ffmpeg binary works fine and can be used to save the HLS steam data via the following:请注意,问题是关于 ffmpeg库的使用,ffmpeg二进制文件工作正常,可用于通过以下方式保存 HLS 流数据:
ffmpeg -i https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8 -c:v copy out.ts
I know that but the goal is to save any (or almost any) stream, thus the MKV.我知道,但目标是保存任何(或几乎任何)流,因此 MKV。 Actually, there is some code that already can save the streams' data, it fails specifically when trying it with HLS.实际上,有一些代码已经可以保存流的数据,但在使用 HLS 尝试时特别失败。

After some efforts to provide a short but readable MCVE, here's a sample code that reproduces the problem.经过一些努力以提供简短但可读的 MCVE,这里是重现问题的示例代码。 The focus is on making the output codec work with HLS streams, thus it may lack a lot of things and details, like extra error checks, corner-cases, optimizations, proper timestamp handling, etc.重点是使输出编解码器与 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;
}

What happens here?这里会发生什么? Simply put:简单的说:

  1. Two threads are spawned, one reads packets from source and stores them in a buffer产生了两个线程,一个从源读取数据包并将它们存储在缓冲区中
  2. The writer waits for the reader to get the AVCodecParameters , so that you can see they are the same being used, almost no manual param setting here AVCodecParameters器等待读者获取AVCodecParameters ,以便您可以看到它们是相同的,这里几乎没有手动参数设置
  3. The reader is supposed to read N packets and finish, then the writer follows him.读者应该阅读N个数据包并完成,然后作者跟随他。 That's how it works with RTSP, RTMP, MJPEG, etc.这就是它与 RTSP、RTMP、MJPEG 等一起工作的方式。

What's the problem?有什么问题? Once an HLS stream is tried, there goes the following error:一旦尝试了 HLS 流,就会出现以下错误:

Tag [27][0][0][0] incompatible with output codec id '27' (H264)标签 [27][0][0][0] 与输出编解码器 ID '27' (H264) 不兼容

After that the writer segfaults on any write attempt via it's context (that is avformat_write_header here) avformat_write_header fails with an error (see UPD2 below) and thus no successfull write operation is possible.之后,在通过它的上下文任何写入尝试作家段错误(即avformat_write_header这里) avformat_write_header因错误而失败(见下文UPD2),因此没有全成写操作是可能的。

What's been tried:尝试了什么:

  1. Forcing arbitrary codecs (ex.: AV_CODEC_ID_H264 ).强制使用任意编解码器(例如: AV_CODEC_ID_H264 )。 No luck there.没有运气。
  2. Trying the AV_CODEC_ID_MPEGTS .尝试AV_CODEC_ID_MPEGTS No way, it's documented as a "fake" codec for internal needs.没办法,它被记录为内部需要的“假”编解码器。
  3. Switching some of the multiple options for input or output contexts, no luck there切换输入或输出上下文的多个选项中的一些,没有运气

I'm currenly confused a lot 'coz the error sounds like "Tag H264 is not compatible with codec H264".我目前很困惑,因为错误听起来像“标签 H264 与编解码器 H264 不兼容”。 The ffmpeg logs look like the library managed to comprehend it's dealing with MPEG-TS being sent via HLS, reading is fine but writing into the chosen media container fails: 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)

No hard googling helped, I'm a bit desperate.谷歌搜索没有帮助,我有点绝望。
Plz, share your ideas, would be grateful for any. Plz,分享您的想法,将不胜感激。

UPD UPD

  • ffmpeg -i https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8 out.mkv works fine 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 also works fine ffmpeg -i http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8 -c:v copy out.mkv也可以正常工作

... which means ffmpeg can do the trick and the desired result can be achieved ...这意味着 ffmpeg可以做到这一点,并且可以达到预期的结果

UPD2 UPD2

It occured that the tag error can be suppressed via发生标签错误可以通过以下方式抑制
out_ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
I assume it's smth about spelling the "h264" properly in a string tag, doesn't look serious.我认为在字符串标签中正确拼写“h264”是有问题的,看起来并不严重。

Also, after a closer look it occured that it's av_write_frame that actually segfaults.此外,仔细观察后发现实际上是av_write_frame了段错误。 No wonder -- with HLS streams avformat_write_header fails and returns error:难怪 - 使用 HLS 流avformat_write_header失败并返回错误:

Invalid data found when processing input处理输入时发现无效数据

That still leaves me with no clues, where's the problem here =((这仍然让我没有任何线索,这里的问题在哪里=((

Okaaay... after significant efforts in debugging and searching for the answer, it looks like there is a recipe and it's not that complex. Okaaay ...后的调试和搜索的答案显著的努力,它看起来像一个配方,它不是那么复杂。
I'll leave it here, so that if anyone else stumbles upon the same magic, he would not get sidetracked.我把它留在这里,这样如果其他人偶然发现了同样的魔法,他就不会偏离方向。

First of all this question already contains a critical detail that one should know when trying remuxing into MKV.首先, 这个问题已经包含一个关键细节,当尝试重新混合到 MKV 时应该知道。 The answer from FFMPEG's maintainer is quite accurate there. FFMPEG 维护者的回答在那里非常准确。

But...但...

  1. AVCodecContext is somehow mandatory . AVCodecContext在某种程度上是强制性的 Maybe that's obvious to everyone, but it was not for me.也许这对每个人都很明显,但对我来说却不是。 Looked quite natural to copy the input stream's codecpar directly into the output stream's codecpar .看着很自然的输入流的复制codecpar直接到输出流的codecpar Okay, probably not some blind copying, the ffmpeg docs warn against that, but still these are AVCodecParameters , why not?好吧,可能不是一些盲目复制,ffmpeg 文档对此提出警告,但这些仍然是AVCodecParameters ,为什么不呢? Alas, the code never worked properly without opening a codec context.唉,如果不打开编解码器上下文,代码就无法正常工作。
  2. AV_CODEC_FLAG_GLOBAL_HEADER is the key to the solution for sure . AV_CODEC_FLAG_GLOBAL_HEADER是解决方案的关键 There's a mention in AVOutputFormat::flags about AVFMT_GLOBALHEADER , but the exact way to use it (can be found in ffmpeg sources and examples) is as shown in the code snippet belowAVOutputFormat::flags提到了AVFMT_GLOBALHEADER ,但使用它的确切方法(可以在 ffmpeg 源和示例中找到)如下面的代码片段所示
  3. FF_COMPLIANCE_UNOFFICIAL appeared to be mandatory as well for a fair amount of hls streams (at least those being at hand), otherwise ffmpeg thinks that the code tries to remux packets between different codecs (yeah, because of codec name spelling), which is a slightly different story. FF_COMPLIANCE_UNOFFICIAL对于相当数量的 hls 流(至少是手头的那些)似乎也是强制性的,否则 ffmpeg 认为代码试图在不同的编解码器之间重新混合数据包(是的,因为编解码器名称拼写),这是一个稍微不同的故事。 Assume it the difference between using the ffmpeg tool with -c:v copy specified and without it.假设使用 ffmpeg 工具与-c:v copy指定和不使用它之间的区别。

Here's the necessary update to my code that makes everything working as expected:这是我的代码的必要更新,使一切按预期工作:

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
    // ....
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM