Фрагментированный MP4 - проблема воспроизведения в браузере - PullRequest
0 голосов
/ 10 января 2019

Я пытаюсь создать фрагментированный MP4 из необработанных видеоданных H264, чтобы воспроизвести его в плеере интернет-браузера. Моя цель - создать систему потокового вещания, в которой медиасервер будет отправлять фрагментированные фрагменты MP4 в браузер. Сервер будет буферизовать входные данные с камеры RaspberryPi, которая отправляет видео в виде кадров H264. Затем он объединит эти видеоданные и сделает их доступными для клиента. Браузер будет воспроизводить мультимедийные данные (которые были мультиплексированы сервером и отправлены через веб-сокет) с использованием расширений Media Source.

Для целей тестирования я написал следующие фрагменты кода (используя много примеров, которые я нашел в интенете):

Приложение C ++, использующее avcodec, которое смешивает необработанное видео 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;
}

(Я жестко закодировал несколько значений, таких как размеры видео или частота кадров, но, как я уже сказал, это всего лишь тестовый код.)


Простая HTML-страница с использованием MSE для воспроизведения моего фрагментированного MP4

<!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>

Выходной файл MP4, созданный моим приложением C ++, может воспроизводиться, например, в MPC, но он не воспроизводится ни в одном веб-браузере, с которым я его тестировал. Это также не имеет никакой продолжительности (MPC продолжает показывать 00:00).

Для сравнения выходного файла MP4, полученного из моего приложения C ++, описанного выше, я также использовал FFMPEG для создания фрагментированного файла MP4 из того же исходного файла с необработанным потоком H264. Я использовал следующую команду:

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

Этот файл, созданный FFMPEG, корректно воспроизводится всеми веб-браузерами, которые я использовал для тестов. Он также имеет правильную продолжительность (но также имеет конечный атом, который в любом случае не будет присутствовать в моем живом потоке, и, поскольку мне нужен живой поток, он не будет иметь фиксированной продолжительности в первую очередь).

Атомы MP4 для обоих файлов выглядят очень похоже (у них точно такой же раздел avcc). Что интересно (но не уверен, что это имеет какое-либо значение), оба файла имеют формат NAL, отличный от входного файла (камера RPI создает видеопоток в формате Annex-B, в то время как выходные файлы MP4 содержат NAL в формате AVCC ... или, по крайней мере, его похоже, это тот случай, когда я сравниваю атомы mdat с входными данными H264).

Я предполагаю, что есть какое-то поле (или несколько полей), которое мне нужно установить для avcodec, чтобы он создавал видеопоток, который будет правильно декодироваться и воспроизводиться проигрывателями браузеров. Но какие поля мне нужно установить? А может проблема в другом? У меня кончились идеи.


РЕДАКТИРОВАТЬ 1: Как и предполагалось, я исследовал двоичное содержимое обоих файлов MP4 (созданных моим приложением и инструментом FFMPEG) с помощью шестнадцатеричного редактора. Что я могу подтвердить:

  • оба файла имеют идентичный раздел avcc (они идеально совпадают и находятся в формате AVCC, я проанализировал его побайтно за байтом, и в этом нет ошибок)
  • оба файла имеют NAL в формате AVCC (я внимательно изучил атомы mdat, и они не различаются в обоих файлах MP4)

Так что, я полагаю, нет ничего плохого в создании дополнительных данных в моем коде - avcodec позаботится об этом должным образом, даже если я просто использую SPS и PPS NAL. Он конвертирует их сам по себе, поэтому мне не нужно делать это вручную. Тем не менее, моя первоначальная проблема остается.

РЕДАКТИРОВАТЬ 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;

Ответы [ 4 ]

0 голосов
/ 12 июня 2019

Мы можем найти это объяснение в [Chrome Source] (https://chromium.googlesource.com/chromium/src/+/refs/heads/master/media/formats/mp4/mp4_stream_parser.cc#799) «Исходный код Chrome Media»:

// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.


  // Use |analysis.is_keyframe|, if it was actually determined, for logging
  // if the analysis mismatches the container's keyframe metadata for
  // |frame_buf|.
  if (analysis.is_keyframe.has_value() &&
      is_keyframe != analysis.is_keyframe.value()) {
    LIMITED_MEDIA_LOG(DEBUG, media_log_, num_video_keyframe_mismatches_,
                      kMaxVideoKeyframeMismatchLogs)
        << "ISO-BMFF container metadata for video frame indicates that the "
           "frame is "
        << (is_keyframe ? "" : "not ")
        << "a keyframe, but the video frame contents indicate the "
           "opposite.";
    // As of September 2018, it appears that all of Edge, Firefox, Safari
    // work with content that marks non-avc-keyframes as a keyframe in the
    // container. Encoders/muxers/old streams still exist that produce
    // all-keyframe mp4 video tracks, though many of the coded frames are
    // not keyframes (likely workaround due to the impact on low-latency
    // live streams until https://crbug.com/229412 was fixed).  We'll trust
    // the AVC frame's keyframe-ness over the mp4 container's metadata if
    // they mismatch. If other out-of-order codecs in mp4 (e.g. HEVC, DV)
    // implement keyframe analysis in their frame_bitstream_converter, we'll
    // similarly trust that analysis instead of the mp4.
    is_keyframe = analysis.is_keyframe.value();
  }

Как показывают комментарии к коду, chrome доверяет ключевому кадру фрейма AVC над метаданными контейнера mp4. Поэтому тип nalu в H264 / HEVC должен быть важнее, чем описание контейнера mp4. Описание sdtp и trun.

0 голосов
/ 10 января 2019

Вам необходимо заполнить дополнительные данные записью конфигурации декодера AVC, а не только SPS / PPS

Вот как должна выглядеть запись: AVCDCR

0 голосов
/ 14 января 2019

Я наконец нашел решение. Мой 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 слайса).

0 голосов
/ 10 января 2019

атомы MP4 для обоих файлов выглядят очень похоже (они имеют одинаковое значение avcc раздел наверняка)

Дважды проверьте это, предоставленный код подсказывает мне обратное.

Что интересно (но не уверен, что это имеет какое-либо значение), оба файлы имеют другой формат NAL, чем входной файл (камера RPI производит видеопоток в формате Annex-B, а выходные файлы MP4 содержат NAL в Формат AVCC ... или, по крайней мере, похоже, что это тот случай, когда я сравниваю атомы мдата с входными данными H264).

Это очень важно, mp4 не будет работать с приложением b.

...