FFmpeg介绍

一、ffmpeg简述

FFmpeg是一套可以用来记录转换数字音频视频,并能将其转化为的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
框图如图所示:

二、编解码基础知识

(1)封装格式

所谓封装格式是指音视频的组合格式,例如最常见的封装格式有mp4mp3flv等。简单来说,我们平时接触到的带有后缀的音视频文件都是一种封装格式。不同的封装格式遵循不同的协议标准。有兴趣的可以自行扩展。

(2)编码格式

mp4为例,通常应该包含有视频和音频。视频的编码格式为YUV420P,音频的编码格式为PCM。再以YUV420编码格式为例。我们知道通常图像的显示为RGB(红绿蓝三原色),在视频压缩的时候会首先将代表每一帧画面的RGB压缩为YUV,再按照关键帧(I帧),过渡帧(P帧或B帧)进行运算和编码。解码的过程正好相反,解码器会读到I帧,并根据I帧运算和解码P帧以及B帧。并最终根据视频文件预设的FPS还原每一帧画面的RGB数据。最后推送给显卡。所以通常我们说的编码过程就包括:画面采集转码编码封装

(3)视频解码和音频解码有什么区别

FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS太低画面会感觉闪烁不够连贯,FPS越高需要显卡性能越好。一些高速摄像机的采集速度能够达到11000帧/秒,那么在播放这类影片的时候我们是否也需要以11000帧/秒播放呢?当然不是,通常我们会按照25帧/秒或者60帧/秒设定图像的FPS值。但是由于视频存在关键帧过渡帧的区别,关键帧保存了完整的画面过渡帧只是保存了与前一帧画面的变化部分,需要通过关键帧计算获得。因此我们需要对每一帧都进行解码,即获取画面的YUV数据。同时只对我们真正需要显示的画面进行转码,即将YUV数据转换成RGB数据,包括计算画面的宽高等。

但是音频则不然,音频的播放必须和采集保持同步。提高或降低音频的播放速度都会让音质发生变化,这也是变声器的原理。因此在实际开发中为了保证播放的音视频同步,我们往往会按照音频的播放速度来控制视频的解码转码速度。

三、代码实现

(1)注册FFmpeg组件

//注册和初始化FFmpeg封装器和网络设备

av_register_all();
avformat_network_init();
avdevice_register_all();

(2)打开文件和创建输入设备

/*
AVFormatContext 表示一个封装器,
在读取多媒体文件的时候,它负责保存与封装和编解码有关的上下文信息。
avformat_open_input函数可以根据文件后缀名来创建封装器。
*/

AVFormatContext *pFormatCtx = NULL;
int errnum = avformat_open_input(&pFormatCtx, filename, NULL, NULL);
if (errnum < 0) {
	av_strerror(errnum, errbuf, sizeof(errbuf));
	cout << errbuf << endl;
}

(3)遍历流并初始化解码器

/*
封装器中保存了各种流媒体的通道,通常视频通道为0,音频通道为1。
除此以外可能还包含字幕流通道等。
第2步和第3步基本就是打开多媒体文件的主要步骤,
解码和转码的所有参数都可以在这里获取。
接下来我们就需要循环进行读取、解码、转码直到播放完成。
*/

for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
	AVCodecContext *pCodecCtx = pFormatCtx->streams[i]->codec; // 解码器上下文
	if (pCodecCtx->codec_type == AVMEDIA_TYPE_VIDEO) {
		// 视频通道
		int videoIndex = i;
		// 视频的宽,高
		int srcWidth = pCodecCtx->width;
		int srcHeight = pCodecCtx->height;
		// 创建视频解码器,打开解码器
		AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
		if (!codec) {
			// 无法创建对应的解码器
			errnum = avcodec_open2(pCodecCtx, codec, NULL);
			if (errnum < 0) {
				av_strerror(errnum, errbuf, sizeof(errbuf));
				cout << errbuf << endl;
			}
			cout << "video decoder open success!" << endl;
		}
	}
	if (pCodecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
		// 音频通道
		int audioIndex = i;        // 创建音频解码器,打开解码器
		AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
		if (!codec) {
			// 无法创建对应的解码器
			errnum = avcodec_open2(pCodecCtx, codec, NULL);
			if (errnum < 0) {
				av_strerror(errnum, errbuf, sizeof(errbuf));
				cout << errbuf << endl;
			}
			int sampleRate = pCodecCtx->sample_rate; // 音频采样率
			int channels = pCodecCtx->channels; // 声道数
			AVSampleFormat fmt = pCodecCtx->sample_fmt; // 样本格式
			cout << "audio decoder open success!" << endl;
		}
	}
}

(4)读取压缩数据

/*之所以称为压缩数据主要是为了区分AVPacket和AVFrame两个结构体。
AVPacket表示一幅经过了关键帧或过渡帧编码后的画面,
AVFrame表示一个AVPacket经过解码后的完整YUV画面*/


AVPacket *pkt = NULL;
pkt = av_packet_alloc(); // 初始化AVPacket// 读取一帧数据
errnum = av_read_frame(pFormatCtx, pkt);
if (errnum == AVERROR_EOF) {    // 已经读取到文件尾
	av_strerror(errnum, errbuf, sizeof(errbuf));
	cout << errbuf << endl;
}
if (errnum < 0) {
	av_strerror(errnum, errbuf, sizeof(errbuf));
	cout << errbuf << endl;
}

(5)解码

errnum = avcodec_send_packet(pCodecCtx, pkt);
if (errnum < 0) {
	av_strerror(errnum, errbuf, sizeof(errbuf));
	cout << errbuf << endl;
}

AVFrame *yuv = av_frame_alloc();
AVFrame *pcm = av_frame_alloc();
if (pkt->stream_index == videoIndex) { // 判断当前解码帧为视频帧
	errnum = avcodec_receive_frame(pCodecCtx, yuv); // 解码视频
	if (errnum < 0) {
		av_strerror(errnum, errbuf, sizeof(errbuf));
		cout << errbuf << endl;
	}
}
if (pkt->stream_index == audioIndex) { // 判断当前解码帧为音频帧
	errnum = avcodec_receive_frame(pCodecCtx, pcm); // 解码音频
	if (errnum < 0) {
		av_strerror(errnum, errbuf, sizeof(errbuf));
		cout << errbuf << endl;
	}
}

(6)视频转码

// 720p输出标准
/*
这里需要解释一下outWidth * outHeight * 4计算理由:
720p标准的视频画面包含720 * 480个像素点,
每一个像素点包含了RGBA4类数据,每一类数据分别由1个byte即8个bit表示。
因此一幅完整画面所占的大小为outWidth * outHeight * 4。
*/
int outWidth = 720;
int outHeight = 480;
char *outData = new char[outWidth * outHeight * 4]

SwsContext *videoSwsCtx = NULL;
videoSwsCtx = sws_getCachedContext(videoSwsCtx, srcWidth, srcHeight, (AVPixelFormat)pixFmt, // 输入
                                   outWidth, outHeight, AV_PIX_FMT_BGRA, // 输出
                                   SWS_BICUBIC, // 算法
                                   0, 0, 0);// 分配数据空间
uint8_t *dstData[AV_NUM_DATA_POINTERS] = { 0 };
dstData[0] = (uint8_t *)outData;
int dstStride[AV_NUM_DATA_POINTERS] = { 0 };
dstStride[0] = outWidth * 4;
int h = sws_scale(videoSwsCtx, yuv->data, yuv->linesize, 0, srcHeight, dstData, dstStride);
if (h != outHeight) {   } // 转码失败

(7)音频转码

char *outData = new char[10000];//输出指针
AVCodecContext *pCodecCtx = pFormatCtx->streams[audioIndex]->codec; // 获取音频解码器上下文
SwrContext *audioSwrCtx = NULL;
audioSwrCtx = swr_alloc();
audioSwrCtx = swr_alloc_set_opts(audioSwrCtx,
                                 AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100, // 输出参数:双通道立体声 CD音质
                                 pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_rate, // 输入参数
                                 0, 0);
swr_init(audioSwrCtx);

uint8_t *out[AV_NUM_DATA_POINTERS] = { 0 };
out[0] = (uint8_t *)outData;// 计算输出空间
int dst_nb_samples = av_rescale_rnd(pcm->nb_samples, pCodecCtx->sample_rate, pCodecCtx->sample_rate, AV_ROUND_UP);
int len = swr_convert(audioSwrCtx,    out, dst_nb_samples,
                      (const uint8_t **)pcm->data, pcm->nb_samples);
int channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO); // AV_CH_LAYOUT_STEREO -> 2 根据声道类型得到声道数// 实际音频数据长度
int dst_bufsize = av_samples_get_buffer_size(NULL,
                  channels, // 通道数
                  pcm->nb_samples,// 1024
                  AV_SAMPLE_FMT_S16,    0);
if (dst_bufsize < 0) {    }// 音频转码错误

四、代码地址

基于qt的FFmpeg客户端(Linux版本):
https://github.com/wengmq/ffmpeg_demo

服务端可采用LIVE555服务器 ,参考博文:
https://blog.csdn.net/qq_37266079/article/details/90524579


参考来源:
https://blog.csdn.net/muyuyuzhong/article/details/79462310
https://www.imooc.com/article/details/id/28012

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐