FFmpeg создает неверный вывод при использовании экзотических разрешений - PullRequest
0 голосов
/ 09 февраля 2019

Я использую FFmpeg для кодирования и объединения сырых кадров YUV в файл .mp4.Это прекрасно работало, пока я не попытался использовать более экзотическое разрешение, например 1440x1080: image

After checking my code and updating FFmpeg to the newest nightly build I, i created this mcve:

#include 

extern "C" {
#include 
#include 
#include 
#include 
#include 
#include 
#include 
}

#define STREAM_PIX_FMT AV_PIX_FMT_YUV420P

using namespace std;

struct FFmpegEncoder {
    AVStream *avStream;
    AVFormatContext *avFormatContext;
    AVOutputFormat *avOutputFormat;
    AVCodecContext *avCodecContext;
    AVCodec *avCodec;

    int64_t nextFrameIndex = 0;
    AVFrame *frame;

    void open(int width, int height, int fps, const char* path);
    virtual void encode(AVFrame* frame, int frameLength);
    virtual void close();

    FFmpegEncoder();
};

void fill_yuv_image3(unsigned char **pict, int frame_index, int width, int height) {
    int x, y, i;
    i = frame_index;

    for (y = 0; y < height; y++)
        for (x = 0; x < width; x++)
            pict[0][y * width + x] = x + y + i * 3;

    for (y = 0; y < height / 2; y++) {
        for (x = 0; x < width / 2; x++) {
            pict[1][y * (width / 2) + x] = 128 + y + i * 2;
            pict[2][y * (width / 2) + x] = 64 + x + i * 5;
        }
    }
}

void FFmpegEncoder::open(int width, int height, int fps, const char* filename) {
    avformat_alloc_output_context2(&avFormatContext, NULL, NULL, filename);
    avOutputFormat = avFormatContext->oformat;
    avCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    avStream = avformat_new_stream(avFormatContext, NULL);
    avStream->id = avFormatContext->nb_streams - 1;
    avCodecContext = avcodec_alloc_context3(avCodec);

    avCodecContext->codec_id = AV_CODEC_ID_H264;
    avCodecContext->bit_rate = width * height * fps;
    avCodecContext->width = width;
    avCodecContext->height = height;
    avStream->time_base.den = fps;
    avStream->time_base.num = 1;
    avCodecContext->time_base = avStream->time_base;
    avCodecContext->gop_size = 15;
    avCodecContext->pix_fmt = STREAM_PIX_FMT;
    avCodecContext->thread_count = 16;

    if (avFormatContext->oformat->flags & AVFMT_GLOBALHEADER) {
        avCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    avcodec_open2(avCodecContext, avCodec, NULL);

    frame = av_frame_alloc();
    frame->format = avCodecContext->pix_fmt;
    frame->width = avCodecContext->width;
    frame->height = avCodecContext->height;
    av_frame_get_buffer(frame, 0);

    avcodec_parameters_from_context(avStream->codecpar, avCodecContext);
    av_dump_format(avFormatContext, 0, filename, 1);

    avio_open(&avFormatContext->pb, filename, AVIO_FLAG_WRITE);
    avformat_write_header(avFormatContext, NULL);
}

void FFmpegEncoder::encode(AVFrame* frame, int _frameLength) {
    AVPacket* pkt = new AVPacket();

    av_init_packet(pkt);

    frame->pts = nextFrameIndex++;

    avcodec_send_frame(avCodecContext, frame);

    if (avcodec_receive_packet(avCodecContext, pkt) == 0) {
        av_packet_rescale_ts(pkt, avCodecContext->time_base, avStream->time_base);
        pkt->stream_index = avStream->index;
        av_write_frame(avFormatContext, pkt);
    }
    delete pkt;
}

void FFmpegEncoder::close() {
    av_write_trailer(avFormatContext);
    avcodec_free_context(&avCodecContext);
    av_frame_free(&frame);

    if (!(avOutputFormat->flags & AVFMT_NOFILE)) {
        avio_closep(&avFormatContext->pb);
    }
    avformat_free_context(avFormatContext);
}

FFmpegEncoder::FFmpegEncoder() {

}


int main(int argc, char **argv) {
    FFmpegEncoder encoder;
    int width = 1440; //when using 1920 here it works fine
    int height = 1080;

    encoder.open(width, height, 30, "testoutput.mp4");
    int frameCount = 200;

    //Allocate testframes
    unsigned char*** frames = new unsigned char**[frameCount];
    for (int i = 0; i < frameCount; i++) {
        frames[i] = new unsigned char*[3];
        frames[i][0] = new unsigned char[width * height];
        frames[i][1] = new unsigned char[(width / 2) * (height / 2)];
        frames[i][2] = new unsigned char[(width / 2) * (height / 2)];
        fill_yuv_image3(frames[i], i, width, height);
    }

    AVFrame* avFrame = av_frame_alloc();
    avFrame->format = STREAM_PIX_FMT;
    avFrame->width = width;
    avFrame->height = height;
    av_frame_get_buffer(avFrame, 0);

    //start encoding
    for (int i = 0; i < frameCount; i++) {
        memcpy(avFrame->data[0], frames[i][0], width * height);
        memcpy(avFrame->data[1], frames[i][1], (width / 2) * (height / 2));
        memcpy(avFrame->data[2], frames[i][2], (width / 2) * (height / 2));
        encoder.encode(avFrame, 0);
    }
    encoder.close();

    return 0;
}

I know the code is still long but I even removed error handling to reduce its length.

Please Note:

  • The output file is playable in all common players and looks the same
  • When changing the resolution width to a more common one like 1280, 1600, 1920 the output looks perfectly fine
  • I tried the following codecs: MPEG4, x264, openh264

Creating a file with the same dimensions using ffmpeg from the command line:

ffmpeg -i valid1920x1080.mp4 -s 1440x1080 -c:a copy output.mp4

Is creating valids outputs, so it must be possible.

Which setting is wrong? I looked at the outdated muxing и кодирование примеров, но не смог объяснить, что я делаю неправильно.

1 Ответ

0 голосов
/ 09 февраля 2019

Все плоскости кадра, отправленного в кодер, выровнены по размеру линии, кратному 32 (или больше).Для формата YUV420P плоскости цветности имеют ширину, равную половине ширины компонента яркости.Таким образом, для ширины кадра 1440 ширина цветности равна 720, а 720% 32! = 0. Но буферам U и V был присвоен наивный размер ширины x высоты.Таким образом, когда выполняется memcpy, первые (width/2 - (width/2) % 32) элементы следующей строки копируются в заполненные шагами элементы кадра, отправляемого в кодер.Это приведет к визуальному искажению, как видно на изображениях Q.

Исправление состоит в том, чтобы заполнить необработанные буферы плоскости цветности до размеров с выравниванием шага.Модификации ОП, как указано в комментариях:

void fill_yuv_imageY(unsigned char **pict, int frame_index, int width, int height) {
    int x, y, i;
    i = frame_index;

    for (y = 0; y < height; y++)
        for (x = 0; x < width; x++)
            pict[0][y * width + x] = x + y + i * 3;
}

void fill_yuv_imageUV(unsigned char **pict, int frame_index, int halfWidth, int height) {
    int x, y, i;
    for (y = 0; y < height / 2; y++) {
        for (x = 0; x < halfWidth; x++) {
            pict[1][y * halfWidth + x] = 128 + y + i * 2;
            pict[2][y * halfWidth + x] = 64 + x + i * 5;
        }
    }
}

int roundUp(int numToRound, int multiple){
    if (multiple == 0)
        return numToRound;

    int remainder = numToRound % multiple;
    if (remainder == 0)
        return numToRound;

    return numToRound + multiple - remainder;
}

//Allocating test frames
unsigned char*** frames = new unsigned char**[frameCount];
for (int i = 0; i < frameCount; i++) {
    frames[i] = new unsigned char*[3];
    frames[i][0] = new unsigned char[width * height];
    fill_yuv_imageY(frames[i], i, width, height);
    frames[i][1] = new unsigned char[roundUp(width / 2, 32) * (height / 2)];
    frames[i][2] = new unsigned char[roundUp(width / 2, 32) * (height / 2)];
    fill_yuv_imageUV(frames[i], i, roundUp(width / 2, 32), height);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...