FFmpeg连载5-音视频编码

简介: ffmpeg连载系列

导读

在前面的我们使用FFmpeg进行相关实践,对音视视频进行了解封装、解码等相关操作,今天我们继续使用FFmpeg进行实践,使用FFmpeg进行音视频编码。

任务一:
在前面《FFmpeg连载4-音频解码》我们将音频解码成PCM并输出到本地文件,今天我们就把这个输出到本地的PCM文件进行读取重新编码成AAC音频文件并输出到本地。

任务二:
在《FFmpeg连载3-视频解码》一节中我们将视频解码成YUV并且输出到本地文件,今天我们读取这个输出的YUV本地文件进行重新编码成H264视频文件并输出到本地。

H264编码规格简介

因为在设置编码器参数时需要用到profile,所以在这里简单介绍下H264的几种profile规格。

1、Baseline Profile

支持I/P帧,只支持无交错(Progressive)和CAVLC
一般用于低阶或需要额外容错的应用,比如视频通话、手机视频等即时通信领域

2、Extended Profile

在Baseline的基础上增加了额外的功能,支持流之间的切换,改进误码性能
支持I/P/B/SP/SI帧,只支持无交错(Progressive)和CAVLC
适合于视频流在网络上的传输场合,比如视频点播

3、Main Profile

提供I/P/B帧,支持无交错(Progressive)和交错(Interlaced),支持CAVLC和CABAC
用于主流消费类电子产品规格如低解码(相对而言)的MP4、便携的视频播放器、PSP和iPod等。

4、High Profile

最常用的规格
在Main的基础上增加了8x8内部预测、自定义量化、无损视频编码和更多的YUV格式(如4:4:4)
High 4:2:2 Profile(Hi422P)
High 4:4:4 Predictive Profile(Hi444PP)
High 4:2:2 Intra Profile
High 4:4:4 Intra Profile
用于广播及视频碟片存储(蓝光影片),高清电视的应用

YUV视频编码

在前面解码的文章中我们介绍了一组解码的函数avcodec_send_packetavcodec_receive_frame,同样对于编码也有对应的一组函数,它们是avcodec_send_frameavcodec_receive_packet,
同样一个的调用avcodec_send_frame需要对应多个avcodec_receive_packet的接收。

相关代码及注释如下:

VideoEncoder.h
class VideoEncoder {
public:
    void encode_yuv_to_h264(const char *yuv_path,const char *h264_path);
};

C++实现文件:

#include "VideoEncoder.h"
#include <iostream>
extern "C"{
#include "libavcodec/avcodec.h"
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
}

static FILE *h264_out = nullptr;

void encode_video(AVCodecContext* avCodecContext,AVFrame* avFrame,AVPacket* avPacket){
    int ret = avcodec_send_frame(avCodecContext,avFrame);
    if(ret < 0){
        std::cout << "yuv发送编码失败" << std::endl;
    }
    while (true){
        ret = avcodec_receive_packet(avCodecContext,avPacket);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
            std::cout << "需要输送更多yuv数据" << std::endl;
            break;
        }

        std::cout << "写入文件h264" << std::endl;
        fwrite(avPacket->data,1,avPacket->size,h264_out);
        av_packet_unref(avPacket);
    }
}

void VideoEncoder::encode_yuv_to_h264(const char *yuv_path, const char *h264_path) {
    const AVCodec *avCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
    avCodecContext->time_base = {1,25};
    // 配置编码器参数
    avCodecContext->width = 720;
    avCodecContext->height = 1280;
    avCodecContext->bit_rate = 2000000;
    avCodecContext->profile = FF_PROFILE_H264_MAIN;
    avCodecContext->gop_size = 10;
    avCodecContext->time_base = {1,25};
    avCodecContext->framerate = {25,1};
    // b帧的数量
    avCodecContext->max_b_frames = 1;
    avCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
    // 设置H264的编码器参数为延迟模式,提高编码质量,但是会造成编码速度下降
    av_opt_set(avCodecContext->priv_data,"preset","slow",0);
    // 打开编码器
    int ret = avcodec_open2(avCodecContext,avCodec, nullptr);
    if(ret < 0){
        std::cout << "编码器打开失败:" << strerror(ret) << std::endl;
        // todo 在析构函数中释放资源
        return;
    }

    AVPacket *avPacket = av_packet_alloc();
    AVFrame *avFrame = av_frame_alloc();
    avFrame->width = avCodecContext->width;
    avFrame->height = avCodecContext->height;
    avFrame->format = avCodecContext->pix_fmt;
    // 为frame分配buffer
    av_frame_get_buffer(avFrame,0);
    av_frame_make_writable(avFrame);

    h264_out = fopen(h264_path,"wb");
    // 读取yuv数据送入编码器
    FILE *input_media = fopen(yuv_path,"r");
    if(nullptr == input_media){
        std::cout << "输入文件打开失败" << std::endl;
        return;
    }

    int pts = 0;
    while (!feof(input_media)){
        int64_t frame_size = avFrame->width * avFrame->height * 3 / 2;
        int64_t read_size = 0;
        // 这里可以自行了解下ffmpeg字节对齐的问题
        if(avFrame->width == avFrame->linesize[0]){
            std::cout << "不存在padding字节" << std::endl;
            // 读取y
            read_size += fread(avFrame->data[0],1,avFrame->width * avFrame->height,input_media);
            // 读取u
            read_size += fread(avFrame->data[1],1,avFrame->width * avFrame->height / 4,input_media);
            // 读取v
            read_size += fread(avFrame->data[2],1,avFrame->width * avFrame->height / 4,input_media);
        } else{
            std::cout << "存在padding字节" << std::endl;
            // 需要对YUV分量进行逐行读取
            for (int i = 0; i < avFrame->height;i++) {
                // 读取y
                read_size += fread(avFrame->data[0] + i * avFrame->linesize[0],1,avFrame->width,input_media);
            }
            // 读取u和v
            for (int i = 0; i < avFrame->height / 2; i++) {
                read_size += fread(avFrame->data[1] + i * avFrame->linesize[1],1,avFrame->width / 2,input_media);
                read_size += fread(avFrame->data[2] + i * avFrame->linesize[2],1,avFrame->width / 2,input_media);
            }
        }
        pts += (1000000 / 25);
        avFrame->pts = pts;
        if(read_size != frame_size){
            std::cout << "读取数据有误:" << std::endl;
        }
        encode_video(avCodecContext,avFrame,avPacket);
    }

    // 冲刷编码器
    encode_video(avCodecContext, nullptr,avPacket);
    fflush(h264_out);
}

需要注意的是在读取YUV数据填充AVFrame时需要区分释放存在字节对齐的问题。

AAC简单介绍

AAC(Advanced Audio Coding,译为:高级音频编码),是由Fraunhofer IIS、杜比实验室、AT&T、Sony、Nokia等公司共同开发的有损音频编码和文件格式。

AAC相较于MP3的有更多的改进:
1、更多的采样率选择:8kHz ~ 96kHz,MP3为16kHz ~ 48kHz
2、更高的声道数上限:48个,MP3在MPEG-1模式下为最多双声道,MPEG-2模式下5.1声道
3、改进的压缩功能:以较小的文件大小提供更高的质量
4、......等等等

AAC是一个庞大家族,为了适应不同场合的需要,它有很多种规格可供选择。下面简单介绍几种常见的规格:
1、AAC Main:主规格
2、AAC LC:低复杂度规格(Low Complexity),适合中等比特率,比如96kbps ~ 192kbps之间。现在的手机比较常见的MP4文件中的音频部分使用了该规格
3、AAC HE:高效率规格(High Efficiency),适合低比特率,HE有v1和v2两个版本,其中v1适合48kbps ~ 64kbps;v2适合低于32kbps,可在低至32kbps的比特率下提供接近CD品质的声音。
貌似FFmpeg自带的AAC编码器不支持这个???

PCM音频编码

直接上代码吧...

/**
 * 将PCM编码成AAC
 */

extern "C"{
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavutil/samplefmt.h>
#include <libavutil/common.h>
#include <libavutil/channel_layout.h>
}
class AudioEncoder{
public:

    /**
     * 坚持编码器释放支持该采样格式
     * @param codec
     * @param sample_fmt
     * @return
     */
    bool check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt)
    {
        const enum AVSampleFormat *p = codec->sample_fmts;
        while (*p != AV_SAMPLE_FMT_NONE) { // 通过AV_SAMPLE_FMT_NONE作为结束符
            if (*p == sample_fmt)
                return true;
            p++;
        }
        return false;
    }

    /**
     * 检查编码器释放支持该采样率
     * @param codec
     * @param sample_rate
     * @return
     */
    bool check_sample_rate(const AVCodec *codec, const int sample_rate)
    {
        const int *p = codec->supported_samplerates;
        while (*p != 0)  {// 0作为退出条件,比如libfdk-aacenc.c的aac_sample_rates
            printf("%s support %dhz\n", codec->name, *p);
            if (*p == sample_rate)
                return true;
            p++;
        }
        return false;
    }

    void encode_pcm_to_aac(const char *pcm_path,const char *aac_path){
        av_log_set_level(AV_LOG_DEBUG);
        const AVCodec *avCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
        AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
        avCodecContext->sample_rate = 44100;
        // 默认的aac编码器输入的PCM格式为:AV_SAMPLE_FMT_FLTP
        avCodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
        avCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
        avCodecContext->bit_rate = 128 * 1024;
        avCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
        avCodecContext->channels   = av_get_channel_layout_nb_channels(avCodecContext->channel_layout);
        avCodecContext->profile = FF_PROFILE_MPEG2_AAC_HE;
        //ffmpeg默认的aac是不带adts,而fdk_aac默认带adts,这里我们强制不带
        avCodecContext->flags = AV_CODEC_FLAG_GLOBAL_HEADER;

        /* 检测支持采样格式支持情况 */
        if (!check_sample_fmt(avCodec, avCodecContext->sample_fmt)) {
            av_log(nullptr, AV_LOG_DEBUG,"Encoder does not support sample format %s",
                    av_get_sample_fmt_name(avCodecContext->sample_fmt));
            return;
        }
        if (!check_sample_rate(avCodec, avCodecContext->sample_rate)) {
            av_log(nullptr, AV_LOG_DEBUG,"Encoder does not support sample rate %d", avCodecContext->sample_rate);
            return;
        }
        AVFormatContext *avFormatContext = avformat_alloc_context();
        const AVOutputFormat *avOutputFormat = av_guess_format(nullptr,aac_path, nullptr);
        avFormatContext->oformat = avOutputFormat;
        AVStream *aac_stream = avformat_new_stream(avFormatContext,avCodec);
        // 打开编码器
        int ret = avcodec_open2(avCodecContext,avCodec, nullptr);
        if(ret < 0){
            char error[1024];
            av_log(nullptr, AV_LOG_DEBUG,"编码器打开失败: %s",
                   av_strerror(ret,error,1024));
        }
        // 编码信息拷贝,放在打开编码器之后
        ret = avcodec_parameters_from_context(aac_stream->codecpar,avCodecContext);

        // 打开输出流
        avio_open(&avFormatContext->pb,aac_path,AVIO_FLAG_WRITE);
        ret = avformat_write_header(avFormatContext, nullptr);
        if(ret < 0){
            char error[1024];
            av_log(nullptr, AV_LOG_DEBUG,"avformat_write_header fail: %s",
                   av_strerror(ret,error,1024));
            return;
        }
        AVPacket *avPacket = av_packet_alloc();
        AVFrame *avFrame = av_frame_alloc();
        avFrame->channel_layout = avCodecContext->channel_layout;
        avFrame->format = avCodecContext->sample_fmt;
        avFrame->channels = avCodecContext->channels;
        // 每次送多少数据给编码器 aac是1024个采样点
        avFrame->nb_samples = avCodecContext->frame_size;
        // 分配buffer
        av_frame_get_buffer(avFrame,0);
        // 每帧数据大小
        int per_sample = av_get_bytes_per_sample(static_cast<AVSampleFormat>(avFrame->format));

        FILE *pcm_file = fopen(pcm_path,"rb");
        int64_t pts = 0;
        while (!feof(pcm_file)){
            // 设置可写
            ret = av_frame_make_writable(avFrame);

            // 从输入文件中交替读取各个声道的数据
            for (int i = 0; i < avFrame->nb_samples; ++i) {
                for (int ch = 0; ch < avCodecContext->channels; ++ch) {
                    fread(avFrame->data[ch] + per_sample * i,1,per_sample,pcm_file);
                }
            }

            // 设置pts 使用采样率作为pts的单位,具体换算成秒 pts*1/采样率
            pts += avFrame->nb_samples;
            avFrame->pts = pts;

            if(ret < 0){
                char error[1024];
                av_strerror(ret,error,1024);
                av_log(nullptr, AV_LOG_DEBUG,"av_samples_fill_arrays fail: %s",error);
                return;
            }

            // 送去编码
            ret = avcodec_send_frame(avCodecContext,avFrame);
            if(ret < 0){
                char error[1024];
                av_strerror(ret,error,1024);
                av_log(nullptr, AV_LOG_DEBUG,"avcodec_send_frame fail: %s",
                       error);
                return;
            }

            while (true){
                ret = avcodec_receive_packet(avCodecContext,avPacket);
                if(ret == AVERROR_EOF || ret == AVERROR(EAGAIN)){
                    // 需要更多数据
                    av_log(nullptr, AV_LOG_DEBUG,"avcodec_receive_packet need more data");
                    break;
                } else if(ret < 0){
                    char error[1024];
                    av_log(nullptr, AV_LOG_DEBUG,"avcodec_receive_packet fail: %s",
                           av_strerror(ret,error,1024));
                    break;
                } else{
                    avPacket->stream_index = aac_stream->index;
                    av_interleaved_write_frame(avFormatContext,avPacket);
                    av_packet_unref(avPacket);
                }
            }
        }
        av_write_trailer(avFormatContext);
        // 关闭
        avio_close(avFormatContext->pb);
        av_packet_free(&avPacket);
        av_frame_free(&avFrame);
    }
};

todo与思考

1、在上面的例子中我们编码出来的视频文件,如果通过ffprobe命令来看出相关媒体信息的话,是没有时长的,也就是说我们虽然编码成功了,但是视频的时间戳丢失了,如果想要编码出正确的时间那该如何处理呢?

编码出来的音频的时长是对的,可以参考下音频是怎么计算的。

2、无论是编码器还是解码器,在没有更多数据输送时都应该发送空的数据包进行数据冲刷,以达到将解码器或编码器内部所有数据获取完的目的...

推荐阅读

FFmpeg连载1-开发环境搭建
FFmpeg连载2-分离视频和音频
FFmpeg连载3-视频解码
FFmpeg连载4-音频解码

关注我,一起进步,人生不止coding!!!

目录
相关文章
|
7月前
|
编解码
项目实战——Qt实现FFmpeg音视频转码器(二)
项目实战——Qt实现FFmpeg音视频转码器(二)
142 0
|
7月前
|
编解码 编译器
项目实战——Qt实现FFmpeg音视频转码器(一)
项目实战——Qt实现FFmpeg音视频转码器(一)
219 0
|
7月前
|
存储 编解码 数据处理
【FFmpeg 视频基本格式】深入理解FFmpeg:从YUV到PCM,解码到编码(三)
【FFmpeg 视频基本格式】深入理解FFmpeg:从YUV到PCM,解码到编码
227 0
|
7月前
|
存储 编解码 数据处理
【FFmpeg 视频基本格式】深入理解FFmpeg:从YUV到PCM,解码到编码(二)
【FFmpeg 视频基本格式】深入理解FFmpeg:从YUV到PCM,解码到编码
254 0
|
1月前
|
编解码 监控 网络协议
如何使用FFmpeg实现RTSP推送H.264和H.265(HEVC)编码视频
本文详细介绍了如何使用FFmpeg实现RTSP推送H.264和H.265(HEVC)编码视频。内容涵盖环境搭建、编码配置、服务器端与客户端实现等方面,适合视频监控系统和直播平台等应用场景。通过具体命令和示例代码,帮助读者快速上手并实现目标。
277 6
|
4月前
|
Web App开发 5G Linux
FFmpeg开发笔记(四十四)毕业设计可做的几个拉满颜值的音视频APP
一年一度的毕业季来临,计算机专业的毕业设计尤为重要,不仅关乎学业评价还积累实战经验。选择紧跟5G技术趋势的音视频APP作为课题极具吸引力。这里推荐三类应用:一是融合WebRTC技术实现视频通话的即时通信APP;二是具备在线直播功能的短视频分享平台,涉及RTMP/SRT等直播技术;三是具有自定义动画特效及卡拉OK歌词字幕功能的视频剪辑工具。这些项目不仅技术含量高,也符合市场需求,是毕业设计的理想选择。
99 6
FFmpeg开发笔记(四十四)毕业设计可做的几个拉满颜值的音视频APP
|
3月前
|
Android开发 计算机视觉 C++
FFmpeg开发笔记(五十一)适合学习研究的几个音视频开源框架
音视频编程对许多程序员来说是一片充满挑战的领域,但借助如OpenCV、LearnOpenGL、FFmpeg、OBS Studio及VLC media player等强大的开源工具,可以降低入门门槛。这些框架不仅覆盖了计算机视觉、图形渲染,还包括多媒体处理与直播技术,通过多种编程语言如Python、C++的应用,使得音视频开发更为便捷。例如,OpenCV支持跨平台的视觉应用开发,FFmpeg则擅长多媒体文件的处理与转换,而VLC media player则是验证音视频文件质量的有效工具。
107 0
FFmpeg开发笔记(五十一)适合学习研究的几个音视频开源框架
|
3月前
用ffmpeg提取合并音视频
用ffmpeg提取合并音视频
|
5月前
|
达摩院 语音技术 异构计算
语音识别-免费开源的语音转文本软件Whisper的本地搭建详细教程,python版本是3.805,ffmpeg是专门处理音视频的,ffmpeg的下载链接,现在要求安装python和ffmpeg
语音识别-免费开源的语音转文本软件Whisper的本地搭建详细教程,python版本是3.805,ffmpeg是专门处理音视频的,ffmpeg的下载链接,现在要求安装python和ffmpeg
|
6月前
|
存储 编解码 Linux
rodert教你学FFmpeg实战这一篇就够了 - 音视频处理入门篇
rodert教你学FFmpeg实战这一篇就够了 - 音视频处理入门篇
75 1