因为项目原因接触使用ffmpeg,当时是使用c#调用ffmpeg,通过指令对视频进行转码。指令的方式比较容易上手,但是如果涉及到复杂点的音视频二次开发,如果没有对音视频相关概念有一定的了解的话,感觉很难理解代码的含义和逻辑。由于兴趣最近开始摸索学习ffmpeg API的相关使用。
相关概念理解
1、多媒体文件的基本概念
- 多媒体文件就是一个容器
- 在容器中有很多流(Stream/Track)
- 每种流是由不同的编码器编码的
- 从流中读出的数据称为包
- 在一个包中包含着一个或多个帧
2、音频的量化编码
- 模拟信号到数字信号的转换过程(连续 ->离散、不连续的过程才能被计算机使用
- 模拟信号->采样->量化->编码->数字信号
- 量化的基本概念: 采样大小:一个采样用多少个bit存放,常用的是16bit
- 采样率:也就是采样频率(1秒采样次数),一般采样率有8kHz、16kHz、32kHz、44.1kHz、48kHz等,采样频率越高,声音的还原就越真实越自然,当然数据量就越大
- 声道数:为了播放声音时能够还原真实的声场,在录制声音时在前后左右几个不同的方位同时获取声音,每个方位的声音就是一个声道。声道数是声音录制时的音源数量或回放时相应的扬声器数量,有单声道、双声道、多声道
- 码率:也叫比特率,是指每秒传送的bit数。单位为 bps(Bit Per Second),比特率越高,每秒传送数据就越多,音质就越好。
码率计算公式:
码率 = 采样率 * 采样大小 * 声道数
比如采样率44.1kHz,采样大小为16bit,双声道PCM编码的WAV文件:
码率=44.1hHz*16bit*2=1411.2kbit/s。
录制1分钟的音乐的大小为(1411.2 * 1000 * 60) / 8 / 1024 / 1024 = 10.09M。
3、时间基
- time_base是用来度量时间的,比如time_base = {1,40},它的意思是将1秒分成40段,那么每段就是1/40秒,在FFmpeg中函数av_q2d(time_base)就是用来计算一段的时间的,计算结果就是1/40秒。比如一个视频中某一帧的pts是800,也就是说有800段,那么它表示多少秒呢,pts av_q2d(time_base)=800(1/40)=20s,也就是说要在第20秒的时候播放这一帧 时间基的转换。不同格式时间基的不同。
- PTS是渲染用的时间戳。DTS是解码时间戳。
音频的PTS:以AAC音频为例,一个AAC原始帧包含一段时间内1024个采样及相关数据,也就是说一帧有1024个样本,如果采样率为44.1kHz(1秒采集44100个样本),所以aac音频1秒有44100/1024帧,每一帧的持续时间是1024/44100秒,由此可以计算出每一帧的pts。 - 转换公式
timestamp(秒) = pts * av_q2d(st->time_base)//计算该帧在视频音频中的位置
time(秒) = st->duration * av_q2d(st->time_base)//计算视频音频中的长度
st 为AVStream流指针
时间基转换公式
timestamp(ffmpeg内部时间戳) = AV_TIME_BASE * time(秒)
time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部时间戳)//timestamp就算是PTS/DTS
环境配置
相关下载
进入官网 分别下载Dev和Shared压缩包。下载注意平台的选择对应。
将dev中include、lib文件分别解压到如下目录下。将shared中dll文件拷贝到项目Debug目录下,分则会出现报错。
环境配置
在VS中创建c/c++项目,右键项目属性
在其中添加如下dll文件
avcodec.lib; avformat.lib; avutil.lib; avdevice.lib; avfilter.lib; postproc.lib; swresample.lib; swscale.lib
libavcodec 提供一系列编码器的实现
libavformat 实现在流协议,容器格式及其IO访问
libavutil 包括了hash器、解码器和各种工具函数
libavfilter 提供了各种音视频过滤器
libavdevice 提供了访问捕获设备和回放设备的接口
libswresample 实现了混音和重采样
libswscale 实现了色彩转换和缩放功能
测试
本人是使用VS2017作为编辑器进行开发。
#include<stdio.h>
#include <iostream>
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
}
int main(int argc, char* argv[]) {
printf(avcodec_configuration());
system("pause");
return 0;
}
开发案例
实现对两组视频的视频音频混搭,一个类似小咖秀的功能。
处理逻辑及使用API
- API注册
- 创建输入、输出上下文
- 获取输入音频流、输入视频流
- 创建输出音频流、输出视频流
- 将输入流参数拷贝到输出流参数
- 判断文件大小,确定输出文件长度
- 写入头信息
- 初始化包、分别读取音视频数据并写入文件
相关代码
#include<stdio.h>
#include <iostream>
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavformat/avio.h"
#include <libavutil/log.h>
#include <libavutil/timestamp.h>
}
#define ERROR_STR_SIZE 1024
int main(int argc, char const *argv[])
{
int ret = -1;
int err_code;
char errors[ERROR_STR_SIZE];
AVFormatContext *ifmt_ctx1 = NULL;
AVFormatContext *ifmt_ctx2 = NULL;
AVFormatContext *ofmt_ctx = NULL;
AVOutputFormat *ofmt = NULL;
AVStream *in_stream1 = NULL;
AVStream *in_stream2 = NULL;
AVStream *out_stream1 = NULL;
AVStream *out_stream2 = NULL;
int audio_stream_index = 0;
int vedio_stream_indes = 0;
// 文件最大时长,保证音频和视频数据长度一致
double max_duration = 0;
AVPacket pkt;
int stream1 = 0, stream2 = 0;
av_log_set_level(AV_LOG_DEBUG);
//打开两个输入文件
if ((err_code = avformat_open_input(&ifmt_ctx1, "C:\\Users\\haizhengzheng\\Desktop\\meta.mp4", 0, 0)) < 0) {
av_strerror(err_code, errors, ERROR_STR_SIZE);
av_log(NULL, AV_LOG_ERROR, "Could not open src file, %s, %d(%s)\n",
"C:\\Users\\haizhengzheng\\Desktop\\meta.mp4", err_code, errors);
goto END;
}
if ((err_code = avformat_open_input(&ifmt_ctx2, "C:\\Users\\haizhengzheng\\Desktop\\mercury.mp4", 0, 0)) < 0) {
av_strerror(err_code, errors, ERROR_STR_SIZE);
av_log(NULL, AV_LOG_ERROR,
"Could not open the second src file, %s, %d(%s)\n",
"C:\\Users\\haizhengzheng\\Desktop\\mercury.mp4", err_code, errors);
goto END;
}
//创建输出上下文
if ((err_code = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, "C:\\Users\\haizhengzheng\\Desktop\\amv.mp4")) < 0) {
av_strerror(err_code, errors, ERROR_STR_SIZE);
av_log(NULL, AV_LOG_ERROR, "Failed to create an context of outfile , %d(%s) \n",
err_code, errors);
}
ofmt = ofmt_ctx->oformat;//获得输出文件的格式信息
// 找到第一个参数里最好的音频流和第二个文件中的视频流下标
audio_stream_index = av_find_best_stream(ifmt_ctx1, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);//获取音频流下标
vedio_stream_indes = av_find_best_stream(ifmt_ctx2, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);//获取视频流下标
// 获取第一个文件中的音频流
in_stream1 = ifmt_ctx1->streams[audio_stream_index];
stream1 = 0;
// 创建音频输出流
out_stream1 = avformat_new_stream(ofmt_ctx, NULL);
if (!out_stream1) {
av_log(NULL, AV_LOG_ERROR, "Failed to alloc out stream!\n");
goto END;
}
// 拷贝流参数
if ((err_code = avcodec_parameters_copy(out_stream1->codecpar, in_stream1->codecpar)) < 0) {
av_strerror(err_code, errors, ERROR_STR_SIZE);
av_log(NULL, AV_LOG_ERROR,
"Failed to copy codec parameter, %d(%s)\n",
err_code, errors);
}
out_stream1->codecpar->codec_tag = 0;
// 获取第二个文件中的视频流
in_stream2 = ifmt_ctx2->streams[vedio_stream_indes];
stream2 = 1;
// 创建视频输出流
out_stream2 = avformat_new_stream(ofmt_ctx, NULL);
if (!out_stream2) {
av_log(NULL, AV_LOG_ERROR, "Failed to alloc out stream!\n");
goto END;
}
// 拷贝流参数
if ((err_code = avcodec_parameters_copy(out_stream2->codecpar, in_stream2->codecpar)) < 0) {
av_strerror(err_code, errors, ERROR_STR_SIZE);
av_log(NULL, AV_LOG_ERROR,
"Failed to copy codec parameter, %d(%s)\n",
err_code, errors);
goto END;
}
out_stream2->codecpar->codec_tag = 0;
//输出流信息
av_dump_format(ofmt_ctx, 0, "C:\\Users\\haizhengzheng\\Desktop\\amv.mp4", 1);
// 判断两个流的长度,确定最终文件的长度 time(秒) = st->duration * av_q2d(st->time_base) duration 就是dts\pts av_q2d()就是倒数
if (in_stream1->duration * av_q2d(in_stream1->time_base) > in_stream2->duration * av_q2d(in_stream2->time_base)) {
max_duration = in_stream2->duration * av_q2d(in_stream2->time_base);
}
else {
max_duration = in_stream1->duration * av_q2d(in_stream1->time_base);
}
//打开输出文件
if (!(ofmt->flags & AVFMT_NOFILE)) {
if ((err_code = avio_open(&ofmt_ctx->pb, "C:\\Users\\haizhengzheng\\Desktop\\amv.mp4", AVIO_FLAG_WRITE)) < 0) {
av_strerror(err_code, errors, ERROR_STR_SIZE);
av_log(NULL, AV_LOG_ERROR,
"Could not open output file, %s, %d(%s)\n",
"C:\\Users\\haizhengzheng\\Desktop\\amv.mp4", err_code, errors);
goto END;
}
}
//写头信息
avformat_write_header(ofmt_ctx, NULL);
av_init_packet(&pkt);
// 读取音频数据并写入输出文件中
while (av_read_frame(ifmt_ctx1, &pkt) >= 0) {
// 如果读取的时间超过了最长时间表示不需要该帧,跳过
if (pkt.pts * av_q2d(in_stream1->time_base) > max_duration) {
av_packet_unref(&pkt);
continue;
}
// 如果是我们需要的音频流,转换时间基后写入文件 av_rescale_q_rnd()时间基转换函数
if (pkt.stream_index == audio_stream_index) {
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream1->time_base, out_stream1->time_base,//获取包的PTS\DTS\duration
(AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream1->time_base, out_stream1->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(max_duration, in_stream1->time_base, out_stream1->time_base);
pkt.pos = -1;
pkt.stream_index = stream1;
av_interleaved_write_frame(ofmt_ctx, &pkt);
av_packet_unref(&pkt);
}
}
// 读取视频数据并写入输出文件中
while (av_read_frame(ifmt_ctx2, &pkt) >= 0) {
// 如果读取的时间超过了最长时间表示不需要该帧,跳过
if (pkt.pts * av_q2d(in_stream2->time_base) > max_duration) {
av_packet_unref(&pkt);
continue;
}
// 如果是我们需要的视频流,转换时间基后写入文件
if (pkt.stream_index == vedio_stream_indes) {
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream2->time_base, out_stream2->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream2->time_base, out_stream2->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(max_duration, in_stream2->time_base, out_stream2->time_base);
pkt.pos = -1;
pkt.stream_index = stream2;
av_interleaved_write_frame(ofmt_ctx, &pkt);
av_packet_unref(&pkt);
}
}
//写尾信息
av_write_trailer(ofmt_ctx);
ret = 0;
END:
// 释放内存
if (ifmt_ctx1) {
avformat_close_input(&ifmt_ctx1);
}
if (ifmt_ctx2) {
avformat_close_input(&ifmt_ctx2);
}
if (ofmt_ctx) {
if (!(ofmt->flags & AVFMT_NOFILE)) {
avio_closep(&ofmt_ctx->pb);
}
avformat_free_context(ofmt_ctx);
}
}