MP4碎片-在浏览器中播放问题

PookyFan

我尝试从原始H264视频数据创建碎片化的MP4,以便可以在Internet浏览器的播放器中播放它。我的目标是创建实时流式传输系统,在该系统中,媒体服务器会将碎片化的MP4片段发送到浏览器。服务器将缓冲来自RaspberryPi摄像机的输入数据,该摄像机将视频作为H264帧发送。然后,它将多路复用该视频数据并将其提供给客户端。浏览器将使用Media Source Extensions播放媒体数据(由服务器混合并通过websocket发送)。

为了测试目的,我编写了以下代码(使用在intenet中找到的许多示例):

使用avcodec的C ++应用程序,它将原始H264视频混合到片段MP4并将其保存到文件中:

#define READBUFSIZE 4096
#define IOBUFSIZE 4096
#define ERRMSGSIZE 128

#include <cstdint>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>

extern "C"
{
    #include <libavformat/avformat.h>
    #include <libavutil/error.h>
    #include <libavutil/opt.h>
}

enum NalType : uint8_t
{
    //NALs containing stream metadata
    SEQ_PARAM_SET = 0x7,
    PIC_PARAM_SET = 0x8
};

std::vector<uint8_t> outputData;

int mediaMuxCallback(void *opaque, uint8_t *buf, int bufSize)
{
    outputData.insert(outputData.end(), buf, buf + bufSize);
    return bufSize;
}

std::string getAvErrorString(int errNr)
{
    char errMsg[ERRMSGSIZE];
    av_strerror(errNr, errMsg, ERRMSGSIZE);
    return std::string(errMsg);
}

int main(int argc, char **argv)
{
    if(argc < 2)
    {
        std::cout << "Missing file name" << std::endl;
        return 1;
    }

    std::fstream file(argv[1], std::ios::in | std::ios::binary);
    if(!file.is_open())
    {
        std::cout << "Couldn't open file " << argv[1] << std::endl;
        return 2;
    }

    std::vector<uint8_t> inputMediaData;
    do
    {
        char buf[READBUFSIZE];
        file.read(buf, READBUFSIZE);

        int size = file.gcount();
        if(size > 0)
            inputMediaData.insert(inputMediaData.end(), buf, buf + size);
    } while(!file.eof());
    file.close();

    //Initialize avcodec
    av_register_all();
    uint8_t *ioBuffer;
    AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    AVCodecContext *codecCtxt = avcodec_alloc_context3(codec);
    AVCodecParserContext *parserCtxt = av_parser_init(AV_CODEC_ID_H264);
    AVOutputFormat *outputFormat = av_guess_format("mp4", nullptr, nullptr);
    AVFormatContext *formatCtxt;
    AVIOContext *ioCtxt;
    AVStream *videoStream;

    int res = avformat_alloc_output_context2(&formatCtxt, outputFormat, nullptr, nullptr);
    if(res < 0)
    {
        std::cout << "Couldn't initialize format context; the error was: " << getAvErrorString(res) << std::endl;
        return 3;
    }

    if((videoStream = avformat_new_stream( formatCtxt, avcodec_find_encoder(formatCtxt->oformat->video_codec) )) == nullptr)
    {
        std::cout << "Couldn't initialize video stream" << std::endl;
        return 4;
    }
    else if(!codec)
    {
        std::cout << "Couldn't initialize codec" << std::endl;
        return 5;
    }
    else if(codecCtxt == nullptr)
    {
        std::cout << "Couldn't initialize codec context" << std::endl;
        return 6;
    }
    else if(parserCtxt == nullptr)
    {
        std::cout << "Couldn't initialize parser context" << std::endl;
        return 7;
    }
    else if((ioBuffer = (uint8_t*)av_malloc(IOBUFSIZE)) == nullptr)
    {
        std::cout << "Couldn't allocate I/O buffer" << std::endl;
        return 8;
    }
    else if((ioCtxt = avio_alloc_context(ioBuffer, IOBUFSIZE, 1, nullptr, nullptr, mediaMuxCallback, nullptr)) == nullptr)
    {
        std::cout << "Couldn't initialize I/O context" << std::endl;
        return 9;
    }

    //Set video stream data
    videoStream->id = formatCtxt->nb_streams - 1;
    videoStream->codec->width = 1280;
    videoStream->codec->height = 720;
    videoStream->time_base.den = 60; //FPS
    videoStream->time_base.num = 1;
    videoStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    formatCtxt->pb = ioCtxt;

    //Retrieve SPS and PPS for codec extdata
    const uint32_t synchMarker = 0x01000000;
    unsigned int i = 0;
    int spsStart = -1, ppsStart = -1;
    uint16_t spsSize = 0, ppsSize = 0;
    while(spsSize == 0 || ppsSize == 0)
    {
        uint32_t *curr =  (uint32_t*)(inputMediaData.data() + i);
        if(*curr == synchMarker)
        {
            unsigned int currentNalStart = i;
            i += sizeof(uint32_t);
            uint8_t nalType = inputMediaData.data()[i] & 0x1F;
            if(nalType == SEQ_PARAM_SET)
                spsStart = currentNalStart;
            else if(nalType == PIC_PARAM_SET)
                ppsStart = currentNalStart;

            if(spsStart >= 0 && spsSize == 0 && spsStart != i)
                spsSize = currentNalStart - spsStart;
            else if(ppsStart >= 0 && ppsSize == 0 && ppsStart != i)
                ppsSize = currentNalStart - ppsStart;
        }
        ++i;
    }

    videoStream->codec->extradata = inputMediaData.data() + spsStart;
    videoStream->codec->extradata_size = ppsStart + ppsSize;

    //Write main header
    AVDictionary *options = nullptr;
    av_dict_set(&options, "movflags", "frag_custom+empty_moov", 0);
    res = avformat_write_header(formatCtxt, &options);
    if(res < 0)
    {
        std::cout << "Couldn't write container main header; the error was: " << getAvErrorString(res) << std::endl;
        return 10;
    }

    //Retrieve frames from input video and wrap them in container
    int currentInputIndex = 0;
    int framesInSecond = 0;
    while(currentInputIndex < inputMediaData.size())
    {
        uint8_t *frameBuffer;
        int frameSize;
        res = av_parser_parse2(parserCtxt, codecCtxt, &frameBuffer, &frameSize, inputMediaData.data() + currentInputIndex,
            inputMediaData.size() - currentInputIndex, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if(frameSize == 0) //No more frames while some data still remains (is that even possible?)
        {
            std::cout << "Some data left unparsed: " << std::to_string(inputMediaData.size() - currentInputIndex) << std::endl;
            break;
        }

        //Prepare packet with video frame to be dumped into container
        AVPacket packet;
        av_init_packet(&packet);
        packet.data = frameBuffer;
        packet.size = frameSize;
        packet.stream_index = videoStream->index;
        currentInputIndex += frameSize;

        //Write packet to the video stream
        res = av_write_frame(formatCtxt, &packet);
        if(res < 0)
        {
            std::cout << "Couldn't write packet with video frame; the error was: " << getAvErrorString(res) << std::endl;
            return 11;
        }

        if(++framesInSecond == 60) //We want 1 segment per second
        {
            framesInSecond = 0;
            res = av_write_frame(formatCtxt, nullptr); //Flush segment
        }
    }
    res = av_write_frame(formatCtxt, nullptr); //Flush if something has been left

    //Write media data in container to file
    file.open("my_mp4.mp4", std::ios::out | std::ios::binary);
    if(!file.is_open())
    {
        std::cout << "Couldn't open output file " << std::endl;
        return 12;
    }

    file.write((char*)outputData.data(), outputData.size());
    if(file.fail())
    {
        std::cout << "Couldn't write to file" << std::endl;
        return 13;
    }

    std::cout << "Media file muxed successfully" << std::endl;
    return 0;
}

(我对一些值进行了硬编码,例如视频尺寸或帧速率,但正如我所说的,这只是测试代码。)


使用MSE播放我的MP4片段的简单HTML网页

<!DOCTYPE html>
<html>
<head>
    <title>Test strumienia</title>
</head>
<body>
    <video width="1280" height="720" controls>
    </video>
</body>
<script>
var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log("The Media Source Extensions API is not supported.")
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/mp4; codecs="avc1.640028"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'my_mp4.mp4';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}
</script>
</html>

我的C ++应用程序生成的输出MP4文件可以在MPC中播放,但不能在我测试过的任何Web浏览器中播放。它也没有任何持续时间(MPC持续显示00:00)。

为了比较我从上述C ++应用程序获得的输出MP4文件,我还使用FFMPEG从具有原始H264流的同一源文件创建碎片化的MP4文件。我使用以下命令:

ffmpeg -r 60 -i input.h264 -c:v copy -f mp4 -movflags empty_moov+default_base_moof+frag_keyframe test.mp4

我用于测试的每个Web浏览器均可正确播放FFMPEG生成的此文件。它还具有正确的持续时间(但是它也具有尾原子,无论如何我的实时流中都不会出现尾原子,并且由于我需要实时流,因此它一开始就没有固定的持续时间)。

两个文件的MP4原子看起来都非常相似(可以肯定,它们具有相同的avcc部分)。有趣的是(但不确定是否重要),两个文件的NAL格式都与输入文件不同(RPI摄像机生成附件B格式的视频流,而输出的MP4文件则包含AVCC格式的NAL……至少是这样)当我将mdat原子与输入的H264数据进行比较时,情况就是这样。

我假设需要为avcodec设置一些字段(或几个字段),以使其产生可被浏览器播放器正确解码和播放的视频流。但是我需要设置哪些字段?也许问题出在其他地方?我没主意了。


编辑1:按照建议,我使用十六进制编辑器调查了两个MP4文件的二进制内容(由我的应用和FFMPEG工具生成)。我可以确认的是:

  • 两个文件都具有相同的avcc部分(它们完全匹配并且为AVCC格式,我逐字节分析了它,对此没有任何错误)
  • 两个文件都具有AVCC格式的NAL(我仔细查看了mdat原子,两个MP4文件之间没有区别)

因此,我想我的代码中创建额外的数据没有什么问题-即使我仅使用SPS和PPS NAL进行输入,avcodec也会正确处理它。它会自行转换它们,因此不需要我手动进行。尽管如此,我最初的问题仍然存在。

编辑2:我取得了部分成功-我的应用程序生成的MP4现在可以在Firefox中播放。我将此行添加到了代码中(以及其余的流初始化):

videoStream->codec->time_base = videoStream->time_base;

所以现在我的代码这一部分看起来像这样:

//Set video stream data
videoStream->id = formatCtxt->nb_streams - 1;
videoStream->codec->width = 1280;
videoStream->codec->height = 720;
videoStream->time_base.den = 60; //FPS
videoStream->time_base.num = 1;
videoStream->codec->time_base = videoStream->time_base;
videoStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
formatCtxt->pb = ioCtxt;
PookyFan

我终于找到了解决方案。我的MP4现在可以在Chrome中播放(同时仍可以在其他经过测试的浏览器中播放)。

在Chrome chrome:// media-internals /中,显示(某种)MSE日志。当我看着那里时,我发现了一些针对测试玩家的以下警告:

ISO-BMFF container metadata for video frame indicates that the frame is not a keyframe, but the video frame contents indicate the opposite.

这让我开始思考,并鼓励设置AV_PKT_FLAG_KEY带有关键帧的数据包。我在具有填充AVPacket结构的部分中添加了以下代码

    //Check if keyframe field needs to be set
    int allowedNalsCount = 3; //In one packet there would be at most three NALs: SPS, PPS and video frame
    packet.flags = 0;
    for(int i = 0; i < frameSize && allowedNalsCount > 0; ++i)
    {
        uint32_t *curr =  (uint32_t*)(frameBuffer + i);
        if(*curr == synchMarker)
        {
            uint8_t nalType = frameBuffer[i + sizeof(uint32_t)] & 0x1F;
            if(nalType == KEYFRAME)
            {
                std::cout << "Keyframe detected at frame nr " << framesTotal << std::endl;
                packet.flags = AV_PKT_FLAG_KEY;
                break;
            }
            else
                i += sizeof(uint32_t) + 1; //We parsed this already, no point in doing it again

            --allowedNalsCount;
        }
    }

一个KEYFRAME不断被证明是0x5在我的情况(切片IDR)。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

如何启动从网址下载mp4(并防止mp4在浏览器中播放)?

碎片化的 MP4 不在 ffplay/QuickTime/Safari 中播放,而是在 VLC 中播放

ffmpeg / OneDrive-MXF中的MP4无法在浏览器中播放

FFMPEG中碎片化MP4创建的冲洗和延迟问题

有没有办法随机播放碎片化的mp4?

mp4内容不会在提供给Samsung Internet浏览器的网页中播放

如何在所有主要浏览器上播放mp4视频?

在 iOS 上从 mov 转换为 mp4 的视频无法在浏览器等上播放

如何强制浏览器下载巨大的 mp4 文件而不是播放它

在浏览器中播放 MP3

HTML5视频:ffmpeg编码的MP4不能在任何浏览器中播放(尽管在VLC中播放)

.mp4 文件无法在浏览器中播放,是否使用了一些奇怪的编解码器?

播放加密的MP4

从浏览器中的mp4链接获取缩略图

仅在旧浏览器中处理Html5视频+ Mp4源

在Layout android中播放mp4

在http中输出一个.mp4文件,将问题从数据库拖到浏览器

跳过MP3会导致播放器在Webkit浏览器中损坏

Python cv2 .mp4 编解码器无法在浏览器中显示

将非碎片化的 mp4 重新编码为碎片化的 mp4

如何在MAC SAFARI浏览器的html音频播放器中播放m4a音频文件

Firefox无法播放MP4

如何播放360 MP4西元?

Chrome无法播放MP4

Flutter Camera 录制的视频与浏览器不兼容 - 如何转换为 .mp4?

Chrome无法下载,但其他浏览器可以... mp4上的html链接

从MP3创建碎片MP4

Firefox 不会从服务器播放 MP4 文件

如何在Safari / IE浏览器中检测HTML5或Javascript中用于播放mp3而非webm的浏览器类型?