FFmpeg学习记录(三)—— ffmpeg编解码实战

发布于:2024-05-08 ⋅ 阅读:(27) ⋅ 点赞:(0)

解码步骤

  • 查找解码器 (avcodec_find_decoder)
  • 打开解码器 (avcodec_open2)
  • 解码 (avcodec_decode_video2)

1.视频编码

编码的详细步骤:

  • 1.输入参数
  • 2.查找编码器
  • 3.创建编码器上下文
  • 4.设置编码器参数
  • 5.编码器与编码器上下文绑定到一起
  • 6.创建输出文件
  • 7.创建AVFrame
  • 8.创建AVPacket
  • 9.生成视频内容
  • 10.编码
// 1.输入参数
    if(argc < 3) {
        av_log(NULL, AV_LOG_ERROR, "arguments must be more than 3\n");
        goto _ERROR;
    }

    dst = argv[1];
    codecName = argv[2];

    // 2.查找编码器
    codec = avcodec_find_encoder_by_name(codecName);
    if(!codec) {
        av_log(NULL, AV_LOG_ERROR, "don't find Codec: %s\n", codecName);
        goto _ERROR;
    }
// 3.创建编码器上下文
    ctx = avcodec_alloc_context3(codec);
    if(!ctx) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }

    // 4.设置编码器参数
    ctx->width = 640;
    ctx->height = 480;
    ctx->bit_rate = 500000;

    ctx->time_base = (AVRational){1, 25};
    ctx->framerate = (AVRational){25, 1};

    ctx->gop_size = 10;
    ctx->max_b_frames = 1;
    ctx->pix_fmt = AV_PIX_FMT_YUV420P;

    if(codec->id == AV_CODEC_ID_H264) {
        av_opt_set(ctx->priv_data, "preset", "slow"0);
    }
// 5.编码器与编码器上下文绑定到一起
    ret = avcodec_open2(ctx, codec, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Dont't open codec: %s\n", av_err2str(ret));
        goto _ERROR;
    }

    // 6.创建输出文件
    f = fopen(dst, "wb");
    if(!f) {
        av_log(NULL, AV_LOG_ERROR, "Dont't open file: %s\n", dst);
        goto _ERROR;
    
    }
    // 7.创建AVFrame
    frame = av_frame_alloc();
    if(!frame) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }
	
	frame->width = ctx->width;
    frame->height = ctx->height;
    frame->format = ctx->pix_fmt;

    ret = av_frame_get_buffer(frame, 0);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Dont't get buffer: %s\n", av_err2str(ret));
        goto _ERROR;
    }

    // 8.创建AVPacket
    pkt = av_packet_alloc();
    if(!pkt) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }
    // 9.生成视频内容
    for(int i = 0; i < 25; i++) {
        ret = av_frame_make_writable(frame);
        if(ret < 0) break;

        // Y分量
        for(int y = 0; y < ctx->height; y++) {
            for(int x = 0; x < ctx->width; x++) {
                frame->data[0][y*frame->linesize[0] + x] = x + y + i * 3;
            }
        }

        // UV分量
        for(int y = 0; y < ctx->height; y++) {
            for(int x = 0; x < ctx->width; x++) {
                frame->data[1][y*frame->linesize[1] + x] = 128 + y + i * 2;
                frame->data[2][y*frame->linesize[2] + x] = 64 + x + i * 5;
            }
        }

        frame->pts = i;

        // 10.编码
        ret = encode(ctx, frame, pkt, f);
        if(ret == -1) {
            goto _ERROR;
        }
    }

    // 10.编码
    encode(ctx, NULL, pkt, f);

static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *out) {
    int ret = -1;

    ret = avcodec_send_frame(ctx, frame);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Failed to send\n");
        goto _END;
    }

    while(ret >= 0) {
        ret = avcodec_receive_frame(ctx, pkt);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if(ret < 0) {
            return -1;
        }

        fwrite(pkt->data, 1, pkt->size, out);
        av_packet_unref(pkt);
    }

_END:
    return 0;
}

这里需要注意:

  • 一定要在外面再执行一次 encode 函数,并把第二个参数置为 NULL,因为有可能缓冲区中还有剩余的数据,如果不执行可能会产生花屏等现象。
  • 在使用av_frame_get_buffer函数分配data的空间之前,一定要给frame分配宽、 高、 格式,只有有了这些参数,才能正确的分配空间

2.音频编码

与视频编码类似,主要修改的地方如下:

  • 输入参数的个数
  • 分配data空间之前,frame参数的设置
  • 生成音频的内容
    // 1.输入参数
    if(argc < 2) {
        av_log(NULL, AV_LOG_ERROR, "arguments must be more than 2\n");
        goto _ERROR;
    }

    dst = argv[1];
    // 7.创建AVFrame
    frame = av_frame_alloc();
    if(!frame) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }

    frame->nb_samples = ctx->frame_size;
    frame->format = ctx->sample_fmt;
    av_channel_layout_copy(&frame->ch_layout, &ctx->ch_layout);

    ret = av_frame_get_buffer(frame, 0);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Dont't get buffer: %s\n", av_err2str(ret));
        goto _ERROR;
    }
    // 9.生成音频内容
    float t = 0;
    float tincr = 2 * M_PI * 440 / ctx->sample_rate;
    for(int i = 0; i < 200; i++) {
        ret = av_frame_make_writable(frame);
        if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Could not allocate space\n");
            goto _ERROR;
        }

        samples = (uint16_t *)frame->data[0];
        for(int j = 0; j < ctx->frame_size; j++) {
            samples[2*j] = (int)(sin(t) * 10000);
            for(int k = 1; k < ctx->ch_layout.nb_channels; k++) {
                samples[2*j+k] = samples[2*j];
            }

            t += tincr;
        }

        encode(ctx, frame, pkt, f);
    }

相关的2个函数:

static int check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmt;

    while(*p != AV_SAMPLE_FMT_NONE) {
        if(*p == sample_fmt) {
            return 1;
        }
        *p++;
    }
    return 0;
}

static int select_sample_rate(const AVCodec *codec)
{
    const int *p;
    int best_samplerate = 0;

    if (!codec->supported_samplerates)
        return 44100;

    p = codec->supported_samplerates;
    while (*p) {
        if (!best_samplerate || abs(44100 - *p) < abs(44100 - best_samplerate))
            best_samplerate = *p;
        p++;
    }
    return best_samplerate;
}

3.生成图片

实际上是把提取视频文件和视频编码进行融合和修改,是视频编码的逆向操作,也就是视频解码。

将视频解码成一帧一帧的图片。

核心代码:

static void savePic(unsigned char *buf, int linesize, int width, int height, char* name) {
    FILE *f;

    f = fopen(name, "wb");
    fprintf(f, "P5\n%d %d\n%d\n", width, height, 255);

    for(int i = 0; i < height; i++) {
        fwrite(buf + i * linesize, 1, width, f);
    }

    fclose(f);
}

static int dencode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, const char *fileName) {
    int ret = -1;
    char buf[1024];

    ret = avcodec_send_packet(ctx, pkt);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Failed to send dencode\n");
        goto _END;
    }

    while(ret >= 0) {
        ret = avcodec_receive_frame(ctx, frame);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if(ret < 0) {
            return -1;
        }

        snprintf(buf, sizeof(buf), "%s-%d", fileName, ctx->frame_number);
        savePic(frame->data[0],
                frame->linesize[0],
                frame->width,
                frame->height,
                buf);

        if(pkt) {
            av_packet_unref(pkt);
        }
    }

_END:
    return 0;
}
 // 2.打开多媒体文件
    ret = avformat_open_input(&pFmtCtx, src, NULL, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "%s\n", av_err2str(ret));
        exit(-1);
    }

    // 3.从多媒体文件中找到视频流
    idx = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if(idx < 0) {
        av_log(pFmtCtx, AV_LOG_ERROR, "Does not include VIDEO\n");
        goto _ERROR;
    }

    inStream = pFmtCtx->streams[idx];

    // 4.查找解码器
    codec = avcodec_find_encoder_by_name(inStream->codecpar->codec->id);
    if(!codec) {
        av_log(NULL, AV_LOG_ERROR, "Could not find Codec\n");
        goto _ERROR;
    }

    // 5.创建解码器上下文
    ctx = avcodec_alloc_context3(codec);
    if(!ctx) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }

    avcodec_parameters_to_context(ctx, inStream->codecpar);

    // 6.解码器与解码器上下文绑定到一起
    ret = avcodec_open2(ctx, codec, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Dont't open codec: %s\n", av_err2str(ret));
        goto _ERROR;
    }

    // 7.创建AVFrame
    frame = av_frame_alloc();
    if(!frame) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }

    // 8.创建AVPacket
    pkt = av_packet_alloc();
    if(!pkt) {
        av_log(NULL, AV_LOG_ERROR, "No Memory\n");
        goto _ERROR;
    }
    

    // 9.从源多媒体文件中读到视频数据到目的文件中
    while(av_read_frame(pFmtCtx, pkt) >= 0) {
        if(pkt->stream_index == idx) {
            decode(ctx, frame, pkt, dst);
        }
    }

    decode(ctx, frame, NULL, dst);

注意:

  • 查找解码器的时候,不要直接写 codec = avcodec_find_encoder_by_name("libx264");,而是应该先获取到输入流,然后根据流查找对应的解码器。
  • 在进行绑定操作之前,也是应该将 inStream中的参数拷贝到 ctx
  • 出现的段错误,是因为再次进行解码的时候,pkt传入的是NULL,此时如果再次释放就会出错,所以应该添加判断

4.生成彩色的BMP图片

上面第3部分生成的图片是黑白的,在这个部分,我们进一步优化,生成彩色的图片。

主要就是在进行解码的时候,传入了一个转换的上下文 swsCtx, 把yuv转换成了 bmp仅此而已,原理很简单。接下来看具体的代码

核心代码:

    // 6.1获得sws上下文
    swsCtx = sws_getCachedContext(NULL,
                                 ctx->width,
                                 ctx->height,
                                 AV_PIX_FMT_YUV420P,
                                //  ctx->width,
                                //  ctx->height,
                                 640, 
                                 360,
                                 AV_PIX_FMT_BGR24,
                                 SWS_BICUBIC, NULL, NULL, NULL);

	... ... 
    // 9.从源多媒体文件中读到视频数据到目的文件中
    while(av_read_frame(pFmtCtx, pkt) >= 0) {
        if(pkt->stream_index == idx) {
            decode(ctx, swsCtx, frame, pkt, dst);
        }
    }

    decode(ctx, swsCtx, frame, NULL, dst);
static void saveBMP(struct SwsContext *swsCtx, AVFrame *frame, int w, int h, char *name) {

    int dataSize = w * h * 3;

    // 1.先进行转换,将YUV frame转换成BGR24 frame
    AVFrame *frameBGR = av_frame_alloc();
    frameBGR->width  = w;
    frameBGR->height = h;
    frameBGR->format = AV_PIX_FMT_BGR24;

    av_frame_get_buffer(frameBGR, 0);
    sws_scale(swsCtx, (const uint8_t * const *)frame->data, frame->linesize, 0, frame->height, frameBGR->data, frameBGR->linesize);

    // 2.构造 BITMAOINFOHEADER
     BITMAPINFOHEADER header;
    header.biSize = sizeof(BITMAPINFOHEADER);
    header.biWidth = w;
    header.biHeight = h*(-1);
    header.biBitCount = 24;
    header.biCompression = 0;
    header.biSizeImage = 0;
    header.biClrImportant = 0;
    header.biClrUsed = 0;
    header.biXPelsPerMeter = 0;
    header.biYPelsPerMeter = 0;
    header.biPlanes = 1;

    // 3.构造 BITMAOFILEHEADER
    BITMAPFILEHEADER bmpFileHeader;

    bmpFileHeader.bfType = 0x4d42; //'BM';
    bmpFileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)+ dataSize;
    bmpFileHeader.bfOffBits=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER);

    // 4.将数据写到文件
    FILE* pf = fopen(filename, "wb");
    fwrite(&bmpFileHeader, sizeof(BITMAPFILEHEADER), 1, pf);
    fwrite(&header, sizeof(BITMAPINFOHEADER), 1, pf);
    fwrite(pFrameRGB->data[0], 1, dataSize, pf);
    fclose(pf);


    // 5.释放资源
    av_freep(&pFrameRGB[0]);
    av_free(pFrameRGB);
    fclose(pf);
}

注意:

  • 在创建swsCtx上下文的时候,一定要明确转换前的格式和转换后的格式,直接使用ctx里面的内容有可能会出现错误,可以直接明确写出来
  • 输出的图片格式一定要是符合标准的4:3 , 16:9等,原始数据有可能会不符合要求,在这里也是明确给出了,640 x 360
  • 需要构造构造 BITMAOINFOHEADERBITMAOFILEHEADER 这两个头并填充