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

解码步骤

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

1.视频编码

编码的详细步骤:

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

    dst = argv[1];
c 复制代码
    // 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;
    }
c 复制代码
    // 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个函数:

c 复制代码
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.生成图片

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

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

核心代码:

c 复制代码
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;
}
c 复制代码
 // 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仅此而已,原理很简单。接下来看具体的代码

核心代码:

c 复制代码
    // 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);
c 复制代码
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 这两个头并填充
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛6 天前
计算机系统概论——校验码
学习
babe小鑫6 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms6 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下6 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。6 天前
2026.2.25监控学习
学习
im_AMBER6 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J6 天前
从“Hello World“ 开始 C++
c语言·c++·学习