音视频技术学习笔记

104 阅读5分钟

平台工具: windows系统、 QTCreator、仿linux环境的msys软件(结合使用MinGW对FFmpeg进行编译,生成自主编译的动态库例如H264库fdk-264)。

技术实现选项: FFmpeg、SDL

摘要:本文着重在于回顾学习过程中,落实到代码处上的关键点。

音频学习要点简述

1,录制: 使用平台的多媒体系统库dshow操作多媒体输入设备


//格式上下文 必须初始化  可以用来操作设备 例如看一些参数
AVFormatContext *ctx
const AVInputFormat *fmt = av_find_input_format("dshow");
avformat_open_input(&ctx, "audio=麦克风 (Realtek(R) Audio)",fmt,nullptr)
//数据包
AVPacket pkt;
av_read_frame(ctx, pkt)
//必须要加,释放pkt内部的资源
av_packet_unref(pkt);
//释放资源
av_packet_free(&pkt)
//关闭设备
avformat_close_input(&ctx);

2,播放:(对于plannar格式的音频,会在音视频同步一文中表现)

 //开始播放 (0 是取消暂停)
   SDL_PauseAudio(0);
   SDL_AudioSpec

if (buffer.len <= 0) {
    //freq:采样率(每秒采样的次数 代表样本数) 
    //size:每个样本的大小 
    // 字节率 (每秒采样的字节数) = freq * size
    //buffer.pullLength:最后一次测算的样本数量
    //时间 如果知道剩下的字节数,除以字节率即是剩余字节要传送的时间
    //时间 = 推算有多少样本 / 采样率
    int samples = buffer.pullLength / BYTES_PER_SAMPLE ;
    int ms = samples * 1000 / SAMPLE_RATE;
    //此次延迟,是在已经读取到音频结尾处,但是最后一段已经读取了的音频还在传递中的情景,为避免过早SDL_Quit()
    SDL_Delay(ms);
    break;
}

/*
*填充数据,从audioBuffer->data地址的位置开始填充pullLength长度,
*并且以SDL_MIX_MAXVOLUME音量播放当前stream流。
*调节音量、静音操作就是在此函数中实现。
*/
SDL_MixAudio(stream,
            (Uint8 *)audioBuffer->data, 
            audioBuffer->pullLength,
           SDL_MIX_MAXVOLUME);

3,重采样

//通过输入音频的样本数和采样率以及输出参数的采样率推算出目标格式音频的样本数
//转换前后的时间一定不变

//inSamples / inSampleRate = outSamples / outSampleRate
outSamples = av_rescale_rnd(inSamples, outSampleRate, inSampleRate,AV_ROUND_UP);

//创建重采样上下文
//  声道        采样格式       采样率
SwrContext *ctx = swr_alloc_set_opts(nullptr,
                                    outChLayout,outSampleFmt,outSampleRate,
                                    inChLayout,inSampleFmt,inSampleRate, 0, nullptr);

创建缓冲区:
av_samples_alloc_array_and_samples(&outData,&outlineSize,
                                    outChannels,outSamples,outSampleFmt,
                                    1)

//重采样(返回值转换后的样本数量)
swr_convert(ctx,outData,outSamples,
                (const uint8_t **)inData,inSamples);


ps:需要检查是否还有残留的样本,可以对输出的文件(自己代码输出的和ffmpeg输出的)可以进行字节数的比对。

4,aac编码解码:

//寻找指定的编码器
const AVCodec *codec =avcodec_find_encoder_by_name("libfdk_aac")

//根据编码器创建编码上下文
AVCodecContext *ctx = avcodec_alloc_context3(codec)

//存放编码前的数据(pcm)
AVFrame *frame = nullptr;
av_frame_get_buffer(frame, 0)

//存放编码后的数据
AVPacket *pkt = av_packet_alloc()

//发送frame ->到 编码器 转到-> pkt
avcodec_send_frame(ctx,frame);
avcodec_receive_packet(ctx,pkt);
//解码器
const AVCodec *codec = avcodec_find_decoder_by_name("libfdk_aac")
//解析器上下文
AVCodecParserContext *parserCtx = av_parser_init(codec->id);

//创建上下文AVCodecContext
//AVPacket存放解码前的数据(aac)
//AVFrame存放解码后的数据(pcm)
//打开解码器
avcodec_open2(ctx,codec,nullptr)

//经过解析器解析
av_parser_parse2(parserCtx,ctx,
                &packet->data, &packet->size, 
                (const uint8_t *)inData,inLen, 
                AV_NOPTS_VALUE,AV_NOPTS_VALUE,0)

//发送压缩数据到解码器pkt->ctx->frame
avcodec_send_packet(ctx,pkt);
avcodec_receive_frame(ctx,frame);
/*设置真正有效的样本帧数量,防止编码器编码了冗余的数据,
*例如缓冲区长度4096个字节,最后一次的真正内容只有2000字节时,
*则当前缓冲区后半部分2096个字节的数据就是冗余的,还是上一次读取的数据,未被覆盖,
*要设置真正有效的样本帧数量
*/
int bytes = av_get_bytes_per_sample((AVSampleFormat)frame->format);
    int ch = av_get_channel_layout_nb_channels(frame->channel_layout);
    // 设置真正有效的样本帧数量 (bytes * ch: 一个样本帧大小)
    frame->nb_samples = ret / (bytes * ch);
}

视频学习要点简述

1,视频录制:

const AVInputFormat *fmt = av_find_input_format("dshow")
//格式上下文 必须初始化  可以用来操作设备 例如看一些参数
AVFormatContext *ctx = nullptr;
//数据包
AVPacket *pkt = av_packet_alloc();

//打开设备
AVDictionary *options = nullptr;
avformat_open_input(&ctx, "video=HD camera ",fmt, &options)

//采集数据
av_read_frame(ctx,pkt);

/*
* 在mac上采集的pkt->data的数据比一帧图片要大一点的,可以理解为有些数据是不用写到yuv里面的

* 一帧的大小:640x480 x 2 (2 是因为yuyv422 是16位 2个字节)
* 所以mac里面,不应该使用pkt->size,可以使用sizePerImage
* //一帧的大小
*   int pixSize = av_get_bits_per_pixel(av_pix_fmt_desc_get(pixFormat)) >> 3;
*   int sizePerImage = para->width * para->height * pixSize;
*/
file.write((const char *)pkt->data,pkt->size);

2,SDL播放YUV视频渲染到窗口

_window = SDL_CreateWindowFrom((const void *)winId());

//渲染上下文   SDL_RENDERER_ACCELERATED 硬件加速
_render = SDL_CreateRenderer(_window, -1, 
                        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)


//因涉及到FFMPEG和SDL中对同一格式的命名不同,所以需要程序员写一个map映射
static const std::map<AVPixelFormat, SDL_PixelFormatEnum>
PIXEL_FORMAT_MAP = {
    {AV_PIX_FMT_YUV420P, SDL_PIXELFORMAT_IYUV},
    {AV_PIX_FMT_YUV422P, SDL_PIXELFORMAT_YUY2},
    {AV_PIX_FMT_NONE, SDL_PIXELFORMAT_UNKNOWN}
}
//如果PIXEL_FORMAT_MAP不是const 可直接PIXEL_FORMAT_MAP[yuv.pixelFmt]
//创建纹理    
_texture = SDL_CreateTexture(_render, 
                            PIXEL_FORMAT_MAP.find(yuv.pixelFmt)->second ,
                            SDL_TEXTUREACCESS_STREAMING, //< Changes frequently, lockable 
                            _yuv.width, _yuv.height);
                            
//imageSize一帧图片的大小
char data[imageSize];
//将YUV的像素数据填充到texture
SDL_UpdateTexture(_texture,nullptr, (const char *)data, _yuv.width);
//设置绘制的画笔颜色
SDL_SetRenderDrawColor(_render, 0,0,0, SDL_ALPHA_OPAQUE);
//拷贝纹理数据到渲染目标(默认是_window)
SDL_RenderCopy(_render, _texture, nullptr, nullptr);
//更新所有的渲染操作到屏幕上
SDL_RenderPresent(_render);

3,格式转码

//libfdk_aac对输入数据的要求:采样格式必须是16位整数

//YUV格式的一帧的宽高必须是16的倍数
//上下文
SwsContext *ctx = sws_getContext(in.width,in.height, in.format,
                     out.width, out.height, out.format,
                     SWS_BILINEAR,
                     nullptr,nullptr,nullptr);

//uint8_t: 8位 一个字节 第4个字节为了兼容可能存在的透明度
//输入输出缓冲区(指向每一个平面的数据)
uint8_t *inData[4], *outData[4];
//每一个平面的大小
//Y平面:一个Y分量占用一个字节, YUV 每一个量都是8位,有多少个像素就代表Y平面有多少个字节;
int inStrides[4], outStrides[4];
//每一帧图片的大小
int inFrameSize, outFrameSize;

//输入缓冲区
ret = av_image_alloc(inData,inStrides, in.width, in.height, in.format, 1);
//输出缓冲区
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
//计算每一帧图片的大小
inFrameSize = av_image_get_buffer_size(in.format,in.width, in.height,1);
outFrameSize = av_image_get_buffer_size(out.format,out.width, out.height, 1);
//拷贝输入数据
memcpy(inData[0], in.pixels,inFrameSize);
//转换
sws_scale(ctx,
          inData,inStrides, 0, in.height,
          outData, outStrides);

out.pixels = (char *)malloc(outFrameSize);
//
memcpy(out.pixels, outData[0], outFrameSize);

4,h264的编码解码实现,在代码逻辑上和音频大致一样,但会有配置参数或细节逻辑的不同,例如音频参数含采样率、音频格式、声道(布局);视频需要设置像素类型,一帧图片size,帧率,pts。

(1) 编码:

//存放编码前的数据(YUV)
AVFrame *frame = nullptr;
//存放编码后的数据(H264)
AVPacket *pkt = nullptr;

如果按照ffmpeg给出的demo演示,使用以下代码,会导致编码后的文件数据偏大。

//官方demo演示
av_frame_get_buffer(frame, 0);

//优化方式一:利用width ,height,  format,创建缓冲区
av_image_alloc(frame->data, frame->linesize, in.width, in.height,in.pixelFmt, 1);
//优化方式二:
uint8_t *= (uint8_t *)av_malloc(imageSize);
av_image_fill_arrays(frame->data,frame->linesize, buf, in.pixelFmt, in.width, in.height, 1);

以及在从文件读取数据时,要使用一帧图片的实际大小(代码如下):

int imageSize = av_image_get_buffer_size(in.pixelFmt, in.width, in.height, 1);
inFile.read((char *)frame->data[0], imageSize)

释放资源前刷新缓冲区。刷新的方式,关键代码

avcodec_send_frame(ctx,nullptr)
avcodec_receive_packet(ctx,pkt)

在释放资源时,需要对frame.data[0]进行释放。

//只有data[0]才指向那块缓冲区的首部
av_freep(&frame->data[0]); 

(2)解码过程只做关键代码展示,在这个过程中会有一个严重的问题就是存在掉尾帧(最后一帧)的问题,网上已经有解决方式,大致就是在读取到文件末尾的时候,仍然进行解析器解析和上下文的解码操作缓冲区,而不仅仅是以是否读取到的文件的有效长度为依据。

//存放解码前的数据(h264)
AVPacket *packet = nullptr;
//存放解码后的数据(yuv数据)
AVFrame *frame = nullptr;
//解码器
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
//codec->id就是AV_CODEC_ID_H264
AVCodecParserContext *parserCtx = av_parser_init(codec->id)
//经过解析器解析
av_parser_parse2(parserCtx,ctx, 
                &packet->data,  &packet->size, 
                (const uint8_t *)inData,inLen, 
                AV_NOPTS_VALUE,AV_NOPTS_VALUE,0)
                
 //发送压缩数据到解码器     
avcodec_send_packet(ctx,pkt);
 //获取解码后的数据         
 avcodec_receive_frame(ctx,frame)
 
 //将解码后的数据写入文件H264(YUV420)
//U和V分量的行数是Y的一半,这里用frame->linesize[0] * ctx->height >> 2 也一样(原理推算)
outFile.write((const char *)frame->data[0], frame->linesize[0] * ctx->height);
outFile.write((const char *)frame->data[0], frame->linesize[1] * ctx->height >> 1);
outFile.write((const char *)frame->data[0], frame->linesize[2] * ctx->height >> 1);

三:音视频同步关键点细节过多,属于综合部分,回头会和rtmp推拉流开一篇。