Я пытаюсь создать фрагментированный 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;