Я использую OpenH264 в качестве кодировщика, и я хочу преобразовать его вывод в воспроизводимый mp4, используя libmp4v2
Результирующий .mp4
работать только частично. Это играбельно в VLC а также MPC-HC но не в проигрывателе Windows Media и не в приложении Windows 10 «Кино и ТВ».
Моя цель — чтобы файл работал со всеми этими проигрывателями.
Оба игрока Windows говорят, что не знают кодек, поэтому не могут его воспроизвести:
Это не совсем так, так как я могу воспроизвести вручную мультиплексированный файл, используя тот же битовый поток h264, используя FFmpeg из кли:
ffmpeg -i "testenc.h264" -c:v copy -f mp4 "output.mp4"
В соответствии с этим знанием я думаю, что мой процесс кодирования работает нормально, и проблема заключается в процедуре мультиплексирования.
Редактировать:
Благодаря ответу Рудольфа Бундулиса, который указал, что данные SPS / PPS отсутствуют, я смог реструктурировать свой код. Теперь он пытается включить отсутствующие данные, анализируя поток битов кодировщиков и вызывая MP4AddH264SequenceParameterSet
или же MP4AddH264PictureParameterSet
когда необходимо. Но все же безуспешно.
Мой полный код:
#include "stdafx.h"#include <iostream>
#include <stdio.h>
#include <chrono>
#include "mp4v2/mp4v2.h"#include "codec_api.h"
#define WIDTH 1280
#define HEIGHT 960
#define DURATION MP4_INVALID_DURATION
#define NAL_SPS 1
#define NAL_PPS 2
#define NAL_I 3
#define NAL_P 4
using namespace std;
using namespace chrono;
/* Just some dummy data to see artifacts ect */
void prepareFrame(int i, SSourcePicture* pic) {
for (int y = 0; y<HEIGHT; y++) {
for (int x = 0; x<WIDTH; x++) {
pic->pData[0][y * WIDTH + x] = x + y + i * 3;
}
}
for (int y = 0; y<HEIGHT / 2; y++) {
for (int x = 0; x<WIDTH / 2; x++) {
pic->pData[1][y * (WIDTH / 2) + x] = 128 + y + i * 2;
pic->pData[2][y * (WIDTH / 2) + x] = 64 + x + i * 5;
}
}
pic->uiTimeStamp = (i + 1) * 1000 / 75;
}
void printHex(const unsigned char* arr, int len) {
for (int i = 0; i < len; i++) {
if (arr[i] < 16) {
cout << "0";
}
cout << hex << (int)arr[i] << " ";
}
cout << endl;
}
void mp4Encode(MP4FileHandle mp4Handle, MP4TrackId track, uint8_t * bitstream, int length) {
int index = -1;
if (bitstream[0] == 0 && bitstream[1] == 0 && bitstream[2] == 0 && bitstream[3] == 1 && bitstream[4] == 0x67) {
index = NAL_SPS;
}
if (bitstream[0] == 0 && bitstream[1] == 0 && bitstream[2] == 0 && bitstream[3] == 1 && bitstream[4] == 0x68) {
index = NAL_PPS;
}
if (bitstream[0] == 0 && bitstream[1] == 0 && bitstream[2] == 0 && bitstream[3] == 1 && bitstream[4] == 0x65) {
index = NAL_I;
}
if (bitstream[0] == 0 && bitstream[1] == 0 && bitstream[2] == 0 && bitstream[3] == 1 && bitstream[4] == 0x61) {
index = NAL_P;
}
switch (index) {
case NAL_SPS:
cout << "Detected SPS" << endl;
MP4AddH264SequenceParameterSet(mp4Handle, track, bitstream + 4, length - 4);
break;
case NAL_PPS:
cout << "Detected PPS" << endl;
MP4AddH264PictureParameterSet(mp4Handle, track, bitstream + 4, length - 4);
break;
case NAL_I:
{
cout << "Detected I" << endl;
uint8_t * IFrameData = (uint8_t *) malloc(length + 1);
IFrameData[0] = (length - 3) >> 24;
IFrameData[1] = (length - 3) >> 16;
IFrameData[2] = (length - 3) >> 8;
IFrameData[3] = (length - 3) & 0xff;
memcpy(IFrameData + 4, bitstream + 3, length - 3);
if (!MP4WriteSample(mp4Handle, track, IFrameData, length + 1, DURATION, 0, 1)) {
cout << "Error when writing sample" << endl;
system("pause");
exit(1);
}
free(IFrameData);
break;
}
case NAL_P:
{
cout << "Detected P" << endl;
bitstream[0] = (length - 4) >> 24;
bitstream[1] = (length - 4) >> 16;
bitstream[2] = (length - 4) >> 8;
bitstream[3] = (length - 4) & 0xff;
if (!MP4WriteSample(mp4Handle, track, bitstream, length, DURATION, 0, 1)) {
cout << "Error when writing sample" << endl;
system("pause");
exit(1);
}
break;
}
}
if (index == -1) {
cout << "Could not detect nal type" << endl;
system("pause");
exit(1);
}
}
int main()
{
//just to measure performance
high_resolution_clock::time_point time = high_resolution_clock::now();
//Create MP4
MP4FileHandle mp4Handle = MP4Create("test.mp4", 0);
MP4SetTimeScale(mp4Handle, 90000);
//Create filestream for binary h264 output for testing
FILE* targetFile;
targetFile = fopen("testenc.h264", "wb");
if (!targetFile) {
cout << "failed to create file" << endl;
system("pause");
return 1;
}
ISVCEncoder *encoder;
int rv = WelsCreateSVCEncoder(&encoder);
//Encoder params
SEncParamExt param;
encoder->GetDefaultParams(¶m);
param.iUsageType = CAMERA_VIDEO_REAL_TIME;
param.fMaxFrameRate = 75.f;
param.iLtrMarkPeriod = 75;
param.iPicWidth = WIDTH;
param.iPicHeight = HEIGHT;
param.iTargetBitrate = 40000000;
param.bEnableDenoise = false;
param.iSpatialLayerNum = 1;
param.bUseLoadBalancing = false;
param.bEnableSceneChangeDetect = false;
param.bEnableBackgroundDetection = false;
param.bEnableAdaptiveQuant = false;
param.bEnableFrameSkip = false;
param.iMultipleThreadIdc = 16;
//param.uiIntraPeriod = 10;
for (int i = 0; i < param.iSpatialLayerNum; i++) {
param.sSpatialLayers[i].iVideoWidth = WIDTH >> (param.iSpatialLayerNum - 1 - i);
param.sSpatialLayers[i].iVideoHeight = HEIGHT >> (param.iSpatialLayerNum - 1 - i);
param.sSpatialLayers[i].fFrameRate = 75.f;
param.sSpatialLayers[i].iSpatialBitrate = param.iTargetBitrate;
param.sSpatialLayers[i].uiProfileIdc = PRO_BASELINE;
param.sSpatialLayers[i].uiLevelIdc = LEVEL_4_2;
param.sSpatialLayers[i].iDLayerQp = 42;
SSliceArgument sliceArg;
sliceArg.uiSliceMode = SM_FIXEDSLCNUM_SLICE;
sliceArg.uiSliceNum = 16;
param.sSpatialLayers[i].sSliceArgument = sliceArg;
}
param.uiMaxNalSize = 1500;
param.iTargetBitrate *= param.iSpatialLayerNum;
encoder->InitializeExt(¶m);
int videoFormat = videoFormatI420;
encoder->SetOption(ENCODER_OPTION_DATAFORMAT, &videoFormat);
MP4TrackId track = MP4AddH264VideoTrack(mp4Handle, 90000, 90000/25, WIDTH, HEIGHT, 66, 192, 42, 3);
MP4SetVideoProfileLevel(mp4Handle, 0x7f);
SFrameBSInfo info;
memset(&info, 0, sizeof(SFrameBSInfo));
SSourcePicture pic;
memset(&pic, 0, sizeof(SSourcePicture));
pic.iPicWidth = WIDTH;
pic.iPicHeight = HEIGHT;
pic.iColorFormat = videoFormatI420;
pic.iStride[0] = pic.iPicWidth;
pic.iStride[1] = pic.iStride[2] = pic.iPicWidth >> 1;
int frameSize = WIDTH * HEIGHT * 3 / 2;
pic.pData[0] = new unsigned char[frameSize];
pic.pData[1] = pic.pData[0] + WIDTH * HEIGHT;
pic.pData[2] = pic.pData[1] + (WIDTH * HEIGHT >> 2);
for (int num = 0; num<75; num++) {
cout << "-------FRAME " << dec << num << "-------" << endl;
prepareFrame(num, &pic);
rv = encoder->EncodeFrame(&pic, &info);
if (!rv == cmResultSuccess) {
cout << "encode failed" << endl;
continue;
}
if (info.eFrameType != videoFrameTypeSkip) {
for (int i = 0; i < info.iLayerNum; ++i) {
int len = 0;
const SLayerBSInfo& layerInfo = info.sLayerInfo[i];
for (int j = 0; j < layerInfo.iNalCount; ++j) {
cout << "Layer: " << dec << i << "| Nal: " << j << endl << "Hex: ";
printHex(info.sLayerInfo[i].pBsBuf + len, 20);
mp4Encode(mp4Handle, track, info.sLayerInfo[i].pBsBuf + len, layerInfo.pNalLengthInByte[j]);
len += layerInfo.pNalLengthInByte[j];
}
//mp4Encode(mp4Handle, track, info.sLayerInfo[i].pBsBuf, len);
}
//fwrite(info.sLayerInfo[0].pBsBuf, 1, len, targetFile);
}
}
int res = 0;
encoder->GetOption(ENCODER_OPTION_PROFILE, &res);
cout << res << endl;
fflush(targetFile);
fclose(targetFile);
encoder->Uninitialize();
WelsDestroySVCEncoder(encoder);
//Close MP4
MP4Close(mp4Handle);
cout << "done in: ";
cout << duration_cast<milliseconds>(high_resolution_clock::now() - time).count() << endl;
system("pause");
return 0;
}
Вы можете использовать MP4Box из GPAC проанализировать макеты блоков MP4 обоих файлов.
Как видно здесь, в плохом файле отсутствуют данные SPS / PPS в поле avcC. Те же самые блоки NAL, скорее всего, также хранятся в единицах NAL, но спецификация требует, чтобы они также присутствовали в поле avcC (некоторые проигрыватели обрабатывают SPS / PPS, встроенные в поток, но это плохая практика, поскольку это прерывает поиск и что нет, так как вы не знаете, какие группы образцов ссылаются на какой параметр устанавливает заранее).
Быстрый поиск в google для libmp4v2 дал мне это пример который показывает, как на самом деле позвонить MP4AddH264SequenceParameterSet
/MP4AddH264PictureParameterSet
предоставить SPS / PPS, пока вы звоните только MP4WriteSample
что может быть проблемой.
Мое субъективное мнение — я никогда не использовал libmp4v2, но если вы не знаете, как его использовать, просто используйте вместо него ffmpeg — больше примеров и сообщество будет больше. Смешать H.264 в mp4 довольно просто, опять же много примеров в интернете.
РЕЗЮМЕ
avcC
коробка — некоторые проигрыватели могут быть в состоянии декодировать поток, если эти единицы помещаются в линию с образцами, но для соответствия спецификации всегда нужно иметь avcC
коробка присутствует, в противном случае игрок может потерпеть неудачу в воспроизведении потока.P4AddH264SequenceParameterSet/MP4AddH264PictureParameterSet
, Для получения данных SPS / PPS необходимо проанализировать поток битов. Это варьируется в зависимости от формата битового потока (если используется формат приложения b с начальными кодами или формат avcc с перемеженными длинами — см. этот для получения дополнительной информации). Когда информация SPS / PPS извлечена, она должна быть передана в библиотеку мультиплексирования.stsd
окна описания потока и затем ссылаются на них, но, насколько я помню, Windows Media Player плохо справляется с этим, поэтому, если возможно, придерживайтесь одного набора SPS / PPS. Нужно иметь возможность настроить кодировщик так, чтобы он не генерировал дублированные записи SPS / PPS на каждом ключевом кадре.Других решений пока нет …