微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

ffmpeg:无法将 HLS 流保存到 MKV 更新UPD2

如何解决ffmpeg:无法将 HLS 流保存到 MKV 更新UPD2

我试图实现一些简单的事情:编写捕获视频流并将其“按原样”保存到 *.mkv 文件代码(是的,没有解复用或重新编码或其他)。只想存储那些 AVPacket-s 并且 MKV 容器看起来已经准备好了。

请注意,问题是关于 ffmpeg library 的使用,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,"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. 作者等待读者获取AVCodecParameters,以便您可以看到它们是相同的,这里几乎没有手动设置参数
  3. 读者应该阅读N个数据包并完成,然后作者跟随他。这就是它与 RTSP、RTMP、MJPEG 等一起工作的方式。

有什么问题吗?一旦尝试了 HLS 流,就会出现以下错误

标签 [27][0][0][0] 与输出编解码器 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)

谷歌搜索没有帮助,我有点绝望。
分享您的想法,不胜感激。

更新

  • 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 失败并返回错误

处理输入时发现无效数据

这仍然让我没有任何线索,问题出在哪里=((

解决方法

好吧……经过大量努力调试和寻找答案后,看起来一个秘诀,而且并不复杂。
我把它留在这里,这样如果其他人偶然发现了同样的魔法,他就不会偏离方向。

首先,this question 已经包含了一个关键细节,当尝试重新混入 MKV 时应该知道该细节。 FFMPEG's maintainer 的回答非常准确。

但是……

  1. AVCodecContext 在某种程度上强制性。也许这对每个人都很明显,但对我来说却不是。将输入流的 codecpar 直接复制到输出流的 codecpar 看起来很自然。好吧,可能不是一些盲目复制,ffmpeg 文档对此提出警告,但这些仍然是 AVCodecParameters,为什么不呢?唉,如果不打开编解码器上下文,代码就无法正常工作。
  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
    // ....
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。