vlc也是可以当一个流媒体服务器进行串流,是OK的,可以参看这地址。此外vlc还可以串流http、udp等当服务器
在流输出时,新目标选择RTSP,然后点击右侧的添加,就让设置端口(这里默认的是8554)、路径,
然后配置文件就选择(TS)的
mediainfo:github去看看,是用来看各种音视频格式的。
FlvAnalyser:github,
Elecard Stream Analyzer 码流分析工具
音视频开发职责可能这些:
#pragma comment(lib, "math.lib")
是windows特有的指令,可看这里介绍。
YUV:是一种亮度与色度分离的色彩格式。
早期的电视都是黑白的,即只有亮度,即Y,有了彩色电视后,加入了UV两种色度,形成现在的YUV,。
YCbCr也称之为YCbCr,是YUV的压缩版本,不同之处在于YCbCr是用于数字图像领域,YUV用于模拟信号领域,MPEG、DVD、摄像机中常说的YUV其实就是YCbCr。
优势:人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼无法感知出来,这样可以通过压缩UV的分辨率。在不影响观感的前提下,减小视频的体积。
RGB与YUV的换算:
Y = 0.299R + 0.587G + 0.114B
U = -0.147R - 0.289G + 0.436B
V = 0.615R - 0.515G - 0.100B
R = Y + 1.14V
G = Y - 0.39U - 0.58V
B = Y + 2.03U
YUV主流的采样方式有三种:
YUV4:4:4 看图,前面四个格子,大家都有;
YUV4:2:2 前面四个格子,Y都有,是4个;U、V各两个
YUV4:2:0 起那么四个格子,Y有4个,U有两个,V是0个;但注意第二行U、V又反过来了 (通常采用这,能获得1/2的压缩率)
就先看第一排,四个四个一组的看, YUV411基本不用
视频帧又分为:I帧、B帧、P帧:
码率/比特率:单位时间播放连续的媒体如压缩后的音频或视频的比特数量。
常用单位“比特每秒”,缩写是“bit/s”。比特率越高,带宽消耗得越多。
比特率即码率,在不同领域有不同的含义,在多媒体领域,==指单位时间播放音频或视频的比特数==,可以理解成吞吐量或带宽。
在一个视频中,不同时段画面的复杂程度是不同的,比如高速变化的场景和几乎静止的场景,所需的数据量也是不同的,若都使用同一种比特率是不太合理的,所以引入了==动态比特率==:
一分钟视频的数据量:(RGB,每色有8bit,这种方式表达出来的颜色,也被称为24位色(占用24bit),也是像素的位深)
视频格式,就不细写了,看视频吧:
PTS 和 DTS:
注:在没有B帧的情况下,DTS和PTS的输出顺序是一样的,一旦存在B帧,PTS和DTS则会不同。
GOP: 即Group of picture(图像组),指==两个I帧之间的距离==,Reference(参考周期)指两个Р帧之间的距离。
另外:
非常好说明的图:
音频相关的东西:
音频数据的承载方式最常用的是==脉冲编码调制==,即PCM。
根据==奈奎斯特采样定理==:为了不失真的恢复模拟信号,采样频率应该不小于模拟信号频谱中最高频率的2倍。
常见的音频编码方式有:PCM 和 ADPCM ,这些数据代表着无损的原始数字音频信号,添加了一些头信息,就可以存储为WAV文件了,它是一种由微软和IBM联合开发。
FFmpeg,在QT中的配置,教程。
ffmpeg中文文档参考地址。windows,用mingw来编译ffmpeg,放这里吧,暂时不折腾了。
ffmpeg解码流程:(新版可能有变化,这不一定对,后期要来校验)
ffmpeg的常规处理流程:
FFmpeg:是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它包括了领先的音/视频编码库libavcodec等。
命令行的三个文件:
八大库:
==FFmpeg的封装转换部分==:,主要是通过libavformat,这部分来完成的功能,通过 libavformat库进行mux和demux操作,多媒体文件的格式有很多种,但是还是有好多demux与 mux的操作参数是公用的,下面来详细介绍一下这些公用的参数:
通过查看ffmpeg的 help full参数,找到AVFormatContext参数部分,在这个参数下面的所有的参数均为封装转换可使用的参数:
avioflags: format的缓冲设置,默认为0,就是有缓冲; direct是无缓冲状态;
probesize:获得文件内容的大小;
formatprobesiz:设置一个获得文件内容的大小来解析文件的 format;
fflags: 设置读取或者写出的格式的形式的标签,分为多种方式: flush_packets, ignidx, genpts,nofillin,noparse,igndts,discardcorrupt,sortdts,keepside,fastseek,latm,nobuffer,bitexact,
==FFmpeg的编解码部分==:
一般的处理流程:
FFmpeg十大数据结构:FFmpeg中结构体很多,
最关键的结构体可以分成以下几类:
a)解协议(http,rtsp,rtmp,mms,hls,file,tcp,udp,…)
AVIOContext, URLContext, URLProtocol:主要存储音视频使用的协议的类型以及状态。URLProtocol存储输入音视频使用的封装格式,每种协议都对应一个URLProtocol结构
b)解封装(flv,avi,rmvb,mp4)
AVFormatContext主要存储音视频封装格式中包含的信息;AVInputFormat存储输入音视频使用的封装格式,每种音视频封装格式都对应一个AVInputFormat结构
c)解码(h264,mpeg2,aac,mp3)
每个AVStream存储一个视频/音频流的相关数据;
每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;
每个AVCodecContext 中对应一个AVCodec,包含该视频/音频对应的解码器。
每种解码器都对应一个AVCodec结构。
d)存数据
视频的话,每个结构一般是存一帧,音频可能有好几帧。
解码前数据:AVPacket 一个个数据包
解码后数据:AVFrame 视频就是一帧图片
这是以前版本的流程主要用到的API:
FFmepg4.x解码流程图解,主要用到的API:
FFmpeg中主要数据结构存在包含关系,如下标题显示的就是包含层级的关系。
AVFormatContext->AVStream->AVCodecContext->AVCodec, 其中后者是前者的数据成员(可能新版的有些出入了,AVCodecContext->AVCodec这俩好像不太对,用的也不是很多)
AVFormatContext是一个贯穿始终的数据结构,很多函数都用到它作为参数,是输入输出相关信息的一个容器。 主要成员如下:
AVCodecContext保存AVCodec指针和与codec相关的数据。在AVStream初始化后AVCodecContext的初始化时Codec使用中最重要的一环。AVCodecContext中的codec_type,codec_id二个变量对于encoder/decoder的最为重要。 AvCodecContext中有两个成员:AvCodec,AVFrame
下面是一些基础的函数,用来了解ffmpeg,主要有:
#include <iostream>
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib")
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavutil/opt.h>
#include <libavutil/parseutils.h>
#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib")
#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib")
}
void test_log() {
AVFormatContext *pAVFmtCtx = nullptr;
pAVFmtCtx = avformat_alloc_context(); // 注意释放,成对写
av_log(pAVFmtCtx, AV_LOG_PANIC, "Panic: \n");
av_log(pAVFmtCtx, AV_LOG_FATAL, "fatal: \n");
av_log(pAVFmtCtx, AV_LOG_ERROR, "ERROE: \n");
av_log(pAVFmtCtx, AV_LOG_WARNING, "warinning: \n");
// info、verbose、debug
avformat_free_context(pAVFmtCtx);
}
void test_avdictionary() {
AVDictionary *d = nullptr;
AVDictionaryEntry *t = nullptr;
// 0 是默认前面插入
av_dict_set(&d, "name", "zhansan", 0);
av_dict_set(&d, "age", "22", 0);
// 另一种方式插入
char *k = av_strdup("location");
char *v = av_strdup("Beijing-China");
av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
// 获取字典长度
int dict_cnt = av_dict_count(d);
std::cout << "字典个数: " << dict_cnt << std::endl; // 3
// 循环获取
while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
std::cout << "kye: " << t->key << "; value: " << t->value << std::endl;
}
// 指定名字获取
t = av_dict_get(d, "age", t, AV_DICT_IGNORE_SUFFIX);
std::cout << "age: " << t->value << std::endl;
av_dict_free(&d); // 注意释放
}
void test_parseutil() {
const char* input_str = "1920x1080"; // 这里是用小写字母x,替代的*
int output_w = 0;
int output_h = 0;
/*
- vga 640x480
- hd1080 1920x1080
- ntsc 720x480
*/
// 获取给定格式的画面尺寸
av_parse_video_size(&output_w, &output_h, input_str);
std::cout << "w: " << output_w << "; h: " << output_h << std::endl; // 1920x1080
av_parse_video_size(&output_w, &output_h, "ntsc"); // ntsc(N制)、pal(啪制)
std::cout << "w: " << output_w << "; h: " << output_h << std::endl; // 640x480
// 获取帧率
AVRational output_rational = { 0, 0 };
av_parse_video_rate(&output_rational, "15/1");
std::cout << "framerate: " << output_rational.num / output_rational.den << std::endl; // 15
// ntsc、 pal都有对应宽高以及帧率
av_parse_video_rate(&output_rational, "pal");
std::cout << "framerate: " << output_rational.num / output_rational.den << std::endl; // 25
// 获取持续时间
int64_t output_timeval;
// 最后一个参数,跟教程出入(可能版本问题),看代码参数解释说,如果不是0,则表示这是持续时间,否则被解释为日期
av_parse_time(&output_timeval, "00:01:01", 1);
std::cout << "microseconds: " << output_timeval << std::endl; // 61000000
std::cout << "microseconds: " << output_timeval / AV_TIME_BASE << std::endl; // 61
}
int main() {
int loglevel = av_log_get_level();
av_log_set_level(AV_LOG_DEBUG);
//av_log_set_level(AV_LOG_PRINT_LEVEL)
test_log();
test_avdictionary();
test_parseutil();
return 0;
}
读取本地、视频流的简单流程:(头文件导入,去看2.3,一模一样,这里不再写)
extern "C" {
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib") // av_log_set_level 需要
}
int main(int argc, char* argv[]) {
av_log_set_level(AV_LOG_FATAL); // 不加这个,读流时ffmpeg自己会有一些报错,加了这,那些报错就没了
avformat_network_init(); // 网络文件才需要,本地文件起始可以不要,老版本的av_register_all已经被丢弃
//const char *input = "rtsp://admin:@192.168.108.135";
const char *input = "C:\\Users\\Administrator\\Videos\\ts.ts"; // 这是自己rtsp存的,
AVFormatContext *pFormatCtx = nullptr;
AVInputFormat *pInFmt = nullptr;
pFormatCtx = avformat_alloc_context();
do {
// avformat_open_input分析流,把pFormatCtx、pInFmt这些给填满
// 后面的 AVDictionary 参数可以传一个空指针;pInFmt这个参数也可以是空指针
if (avformat_open_input(&pFormatCtx, input, pInFmt, nullptr) < 0) {
std::cerr << "打开失败..." << std::endl;
break;
}
if (avformat_find_stream_info(pFormatCtx, nullptr) < 0) {
std::cerr << "avformat_find_stream_info error..." << std::endl;
break;
}
std::cout << "流中的数据流的路数:" << pFormatCtx->nb_streams << std::endl;
} while (0);
// 要对应去释放
avformat_close_input(&pFormatCtx);
avformat_free_context(pFormatCtx);
avformat_network_deinit();
return 0;
}
这一般不涉及到编解码,只是拿下,给出新的封装格式而已,但不是所有的格式都能互转,跟输入视频文件关系很大。
基本上自己的使用就按照这个代码来拿视频帧吧。
#include <iostream>
/*
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib")
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavutil/opt.h>
#include <libavutil/parseutils.h>
#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib")
#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib")
}
*/
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib") // av_packet_alloc 这些函数需要
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib")
}
static double r2d(AVRational r) {
return r.den == 0 ? 0 : (double)r.num / (double)r.den;
}
int main(int argc, char* argv[]) {
avformat_network_init(); // 网络文件才需要,本地文件起始可以不要,老版本的av_register_all已经被丢弃
//const char *input = "rtsp://admin:@192.168.108.135";
const char *input = "C:\\Users\\Administrator\\Videos\\audio.mp4"; // 这是自己rtsp存的,
AVFormatContext *pFormatCtx = nullptr;
AVDictionary *opts = nullptr;
// 1.分配解复用器上下文
pFormatCtx = avformat_alloc_context();
do {
// 2.打开文件
int ret = avformat_open_input(&pFormatCtx, input, nullptr, &opts);
if (ret != 0) {
// 用ffmpeg自带的把错误转换打印出来
char buf[1024] = { 0 };
av_strerror(ret, buf, sizeof(buf) - 1); // 需要avutil.lib库
std::cout << "open failed: " << buf;
break;
}
// 3.读取码流信息
if (avformat_find_stream_info(pFormatCtx, nullptr) != 0) break;
/* // 3.打印总体信息
pFormatCtx->url # media名字
pFormatCtx->bit_rate # 媒体的平均码率,单位为bps b代表比特
这个值 / 1024 就得到了 kbps
pFormatCtx->nb_streams # 流的个数
pFormatCtx->duration # 时长(微妙)(可以像下面具体流转成时分秒)
pFormatCtx->duration / AV_TIME_BASE # 这就转成了秒
*/
av_dump_format(pFormatCtx, 0, input, 0); // 这个API就是想ffmpeg那样打印输出
// 4.读取码流信息(这是旧版本,新版本也是兼容的)
for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
AVStream *as = pFormatCtx->streams[i];
// 视频
if (AVMEDIA_TYPE_VIDEO == as->codecpar->codec_type) {
std::cout << "视频信息: " << std::endl;
std::cout << "index: " << as->index << std::endl;
std::cout << "视频帧率: " << r2d(as->avg_frame_rate) << "fps.\n";
// 视频压缩编码格式
if (AV_CODEC_ID_MPEG4 == as->codecpar->codec_id) {
std::cout << "视频压缩编码格式: MPEG4" << std::endl;
}
else if (AV_CODEC_ID_HEVC == as->codecpar->codec_id) {
std::cout << "视频压缩编码格式: h265" << std::endl;
}
std::cout << "(w, h): " << as->codecpar->width << ", " << as->codecpar->height;
// 视频总时长,转化成了秒
int durationAudio = (as->duration) * r2d(as->time_base);
std::cout << "视频总时长:" << durationAudio / 3600 << "时" << (durationAudio % 3600) / 60 << "分" << (durationAudio % 60) << "秒\n" << std::endl;;
}
// 音频
else if (AVMEDIA_TYPE_AUDIO == as->codecpar->codec_type) {
std::cout << "音频信息: " << std::endl;
std::cout << "index: " << as->index << std::endl;
std::cout << "音频采样率: " << as->codecpar->sample_rate << "Hz.\n";
// 下面这已经被5.1丢弃了,编译会报错
//std::cout << "音频通道数: " << as->codecpar->channels << std::endl;
// 音频采样格式
if (AV_SAMPLE_FMT_FLTP == as->codecpar->format) {
std::cout << "音频采样格式:AV_SAMPLE_FMT_FLTP\n";
}
else if (AV_SAMPLE_FMT_S16P == as->codecpar->format) {
std::cout << "音频采样格式:AV_SAMPLE_FMT_S16P\n";
}
// 音频压缩编码格式
if (AV_CODEC_ID_AAC == as->codecpar->codec_id) {
std::cout << "音频压缩编码格式:AV_CODEC_ID_AAC\n";
}
else if (AV_CODEC_ID_MP3 == as->codecpar->codec_id) {
std::cout << "音频压缩编码格式:AV_CODEC_ID_MP3\n";
}
// 音频总时长,转化成了秒
int durationAudio = (as->duration) * r2d(as->time_base);
std::cout << "音频总时长:" << durationAudio / 3600 << "时" << (durationAudio % 3600) / 60 << "分" << (durationAudio % 60) << "秒\n";
}
}
// 4.读取码流信息(新版)av_find_best_stream
// 后面四个参数就这样默认吧
int audioIdx = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (audioIdx < 0) {
std::cout << "av_find_best_stream error: " << av_get_media_type_string(AVMEDIA_TYPE_AUDIO);
break;
}
AVStream *audio_stream = pFormatCtx->streams[audioIdx];
// 字幕流就是 AVMEDIA_TYPE_SUBTITLE 如果是这种方式,不是用循环,多路流时要注意哦
int videoIdx = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (videoIdx < 0) {
std::cout << "av_find_best_stream error: " << av_get_media_type_string(AVMEDIA_TYPE_VIDEO);
break;
}
AVStream *video_stream = pFormatCtx->streams[videoIdx];
std::cout << "index: " << video_stream->index << std::endl;
std::cout << "fps: " << av_q2d(video_stream->avg_frame_rate) << std::endl;
std::cout << "w, h" << video_stream->codecpar->width << ", " << video_stream->codecpar->height << std::endl;
std::cout << "video codec: " << video_stream->codecpar->codec_id << std::endl;
if (video_stream->duration != AV_NOPTS_VALUE) {
int video_duration = video_stream->duration * av_q2d(video_stream->time_base);
std::cout << "video duration: " << video_duration / 3600 << "时" << (video_duration % 3600) / 60 << "分" << (video_duration % 60) << "秒\n";
}
// 5.提取码流数据 (一个包一个包的读)
// 打印结果可以看到音视频是交织输出的,,可能一开始只有视频视频,也可能一开始只有音频数据
int pkt_count = 0;
int print_max_count = 100;
std::cout << "\n---------av_read_frames start\n";
AVPacket *pkt = av_packet_alloc();
while (1) {
ret = av_read_frame(pFormatCtx, pkt);
if (ret < 0) {
std::cout << "av_read_frame end\n";
break;
}
if (pkt_count++ > print_max_count) break;
if (pkt->stream_index == audioIdx) {
std::cout << "audio pts: " << pkt->pts << std::endl;
std::cout << "audio dts: " << pkt->dts << std::endl;
std::cout << "audio size: " << pkt->size << std::endl;
std::cout << "audio pos: " << pkt->pos << std::endl;
std::cout << "audio duration: " << pkt->duration * av_q2d(pFormatCtx->streams[audioIdx]->time_base) << std::endl;
}
else if (pkt->stream_index == videoIdx) {
std::cout << "video pts: " << pkt->pts << std::endl;
std::cout << "video dts: " << pkt->dts << std::endl;
std::cout << "video size: " << pkt->size << std::endl;
std::cout << "video pos: " << pkt->pos << std::endl;
// 30fps的视频,这里的结果结果就是0.0333秒
std::cout << "one frame duration: " << pkt->duration * av_q2d(video_stream->time_base) << std::endl;
}
else {
std::cout << "UNknown stream_idx: " << pkt->stream_index << std::endl;
}
std::cout << "\n";
// 记得是在这里接触ref,代表这个包不用了,好重复利用pkt这个空间(不要修改这个位置到循环外面,会造成内存泄漏)
av_packet_unref(pkt);
}
// 6.结束
if (pkt) {
av_packet_free(&pkt);
}
} while (0);
// 要对应去释放
if (pFormatCtx) {
avformat_close_input(&pFormatCtx);
avformat_free_context(pFormatCtx);
}
avformat_network_deinit();
system("pause");
return 0;
}
转封装:从一种视频容器转成另一种视频容器。
所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(对应.avi,.flv,.mkv,.mp4文件)。需要注意的是,本程序并不进行视音频的编码和解码工作。而是直接将视音频压缩码流从一种封装格式文件中获取出来然后打包成另外一种封装格式的文件。说是可以不涉及到编解码,就是说数据还是用原来的数据,只是重新加头部信息封装成别的格式的文件,速度特别快,几乎是无损的。
比如:
下面是代码的实现:(注意,没办法像命令行那样随意转换,且有的格式命令行都是做不了的,跟输入视频格式有很大关系,如“sintel_trailer-480p.webm”作为输入,命令行都是不行的)
#include <iostream>
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib") // av_packet_alloc 这些函数需要
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavutil/avutil.h>
#include <libavutil/timestamp.h>
#pragma comment(lib, "avutil.lib")
}
static void log_packet(const AVFormatContext *fmt_ctx, const AVPacket *pkt, const char* tag) {
AVRational *time_base = &(fmt_ctx->streams[pkt->stream_index]->time_base);
char a[32] = { 0 };
std::cout << tag << ":pts:" << av_ts_make_string(a, pkt->pts) << " pts_time:" << av_ts_make_time_string(a, pkt->pts, time_base)
<< " dts:" << av_ts_make_string(a, pkt->dts) << " dts_time:" << av_ts_make_time_string(a, pkt->dts, time_base)
<< " duration:" << av_ts_make_string(a, pkt->duration) << " duration_time:" << av_ts_make_time_string(a, pkt->duration, time_base)
<< " index:" << pkt->stream_index << std::endl;
/*
av_ts2str、av_ts2timestr会报错,应该是源码中这些写法现在在c++里,有点问题,我就自己把它扩展写出来了
std::cout << tag << ":\npts:" << av_ts2str(pkt->pts) << " pts_time:" << av_ts2timestr(pkt->pts, time_base)
<< " dts:" << av_ts2str(pkt->dts) << " dts_time:" << av_ts2timestr(pkt->dts, time_base)
<< " duration:" << av_ts2str(pkt->duration) << " duration_time:" << av_ts2timestr(pkt->duration, time_base)
<< " index:" << pkt->stream_index;
*/
}
int main(int argc, char* argv[]) {
AVFormatContext *ifmt_ctx = nullptr;
AVFormatContext *ofmt_ctx = nullptr;
const AVOutputFormat *ofmt = nullptr;
AVPacket pkt;
const char* in_filename;
const char* out_filename;
int ret = 0;
int streadm_index = 0;
int *stream_mapping = nullptr;
int stream_mapping_size = 0;
if (argc < 3) {
std::cout << "usage: " << argv[0] << "input output\n"
"API example program to remux a media file with "
"The output format is guessed according to the file\n";
return 1;
}
in_filename = argv[1];
out_filename = argv[2];
do {
// 很多用0替代nullptr、NULL
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, nullptr, nullptr)) < 0) {
std::cerr << "Could not open input file: " << in_filename << std::endl;
break;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
std::cerr << "Failed to retrieve input stream inform" << std::endl;
break;
}
std::cout << "\n\n----------------av_dump_format: ifmt_ctx--------\n";
av_dump_format(ifmt_ctx, 0, in_filename, 0);
// 第三个参数可指定后缀名(可以类似给 avi mp4 这种)
ret = avformat_alloc_output_context2(&ofmt_ctx, nullptr, nullptr, out_filename);
if (ret < 0 || !ofmt_ctx) {
std::cerr << "Could not create output context\n";
ret = AVERROR_UNKNOWN;
break;
}
stream_mapping_size = ifmt_ctx->nb_streams;
// av_mallocz_array 是旧的版本了,用 av_calloc
// 用了指针转换,不知道行不行
stream_mapping = reinterpret_cast<int*>(av_calloc(stream_mapping_size, sizeof(*stream_mapping)));
if (!stream_mapping) {
ret = AVERROR(ENOMEM);
break;
}
ofmt = ofmt_ctx->oformat;
bool flag = false;
// 根据输入流创建输出流 (这就把所有codec参数都存进了 ofmt_ctx )
for (size_t i = 0; i < ifmt_ctx->nb_streams; ++i) {
AVStream *in_stream = ifmt_ctx->streams[i];
AVCodecParameters *in_codecpar = in_stream->codecpar;
// 只处理音频流、视频流、字幕流
if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
stream_mapping[i] = -1;
continue;
}
stream_mapping[i] = streadm_index++;
// 根据输出的上下文信息,创建一个新的流
// out_stream是一个临时变量,后续对它的改变的值都存到了ofmt_ctx
// 每次循环都会重新创建,(第二个参数,新版都是给空指针,它里面也有说明,讲这参数什么也不错,老版可能需要传递)
AVStream *out_stream = avformat_new_stream(ofmt_ctx, nullptr);
if (!out_stream) {
std::cerr << "Failed allocting output stream\n";
ret = AVERROR_UNKNOWN;
flag = true; // 这错了,要直接跳出do while 循环
break;
}
// 复制编解码器的参数
ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
if (ret < 0) {
std::cout << "Failed to copy codec parameters\n";
break;
}
// 以后凡是只看到 ->codec-> 就代表是老版本,新版已经丢弃,是用的 ->codecpar->
out_stream->codecpar->codec_tag = 0; // 标记不需要重新编解码
}
if (flag) break;
std::cout << "\n\n----------------av_dump_format: ofmt_ctx--------\n";
// (读里面参数就会懂,最后一个参数:是输出流就给1,不是就给0)
av_dump_format(ofmt_ctx, 0, out_filename, 1);
if ((ofmt->flags & AVFMT_NOFILE)) break;
// 写入文件
ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
std::cerr << "Could not opne output file: " << out_filename << std::endl;
break;
}
// 头部信息
ret = avformat_write_header(ofmt_ctx, nullptr);
if (ret < 0) {
std::cerr << "Error occurred when opening output file: " << out_filename << std::endl;
break;
}
// 开始每个包
while (1) {
AVStream *in_stream = nullptr;
AVStream *out_stream = nullptr;
// 去读音视频压缩包
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
in_stream = ifmt_ctx->streams[pkt.stream_index];
if (pkt.stream_index >= stream_mapping_size || stream_mapping[pkt.stream_index] < 0) {
av_packet_unref(&pkt);
continue;
}
pkt.stream_index = stream_mapping[pkt.stream_index];
// log_packet(ifmt_ctx, &pkt, "in");
out_stream = ofmt_ctx->streams[pkt.stream_index];
// copy packet(只定义了一个pkt,输入用的这个,改了内在的变量后赋值给它,然后输出也是用的这个pkt)
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
// log_packet(ofmt_ctx, &pkt, "out");
// 交织写音视频包
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0) {
std::cerr << "Error muxing packet\n";
break;
}
av_packet_unref(&pkt); // 单个包用完一次就一定要解引用一次,不然可能内存泄露
}
// 写入尾部信息
av_write_trailer(ofmt_ctx);
} while (0);
// 释放资源
if (ifmt_ctx) {
avformat_close_input(&ifmt_ctx);
// 如果没有用 avformat_alloc_context 去创建ifmt_ctx,好像不用下面,(加了问题应该也不大)
avformat_free_context(ifmt_ctx);
}
// 关闭输出流
if (ofmt_ctx && ofmt && !(ofmt->flags & AVFMT_NOFILE)) {
avio_closep(&(ofmt_ctx->pb));
}
avformat_free_context(ofmt_ctx);
system("pause");
return 0;
}
注:
流程图:
转码流程:(信息解码后,两条线,一条是给到硬件播放,一条是重新编码)
下面的这些示例,各有各的一些特定,如转格式啊,像素格式啊,写入头部信息啊,不尽相同,想做一个功能时,最好都去参考看看。
参考的是官方demo:encode_video.c。里面是用C的写二进制的方式,我改成C++的了.
编译出来后,使用: main.exe # 会得到时长1秒的视频
这就是把原始数据编码成帧数据,再成视频。
无论是编码器还是解码器,都是用的这个上下文:avcodec_alloc_context3
无论是这里的av_err2str,还是上面的av_ts2str,直接用会报错,因为一些C的初始化语法在C++中不行,那就要F12进去改它的源码。
/* // 原来是:
#define av_err2str(errnum) \
av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, AV_ERROR_MAX_STRING_SIZE, errnum)
*/
// 现在改成先把这字符串定义并初始化出来
char my_error[AV_ERROR_MAX_STRING_SIZE] = { 0 };
#define av_err2str(errnum) \
av_make_error_string(my_error, AV_ERROR_MAX_STRING_SIZE, errnum)
==编码==API使用详解:核心是==avcodec_send_frame==、==avcodec_receive_packet==这俩函数,他们都是成对出现使用,跟编码刚好相反。
#include <iostream>
#include <fstream>
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib") // av_packet_alloc 这些函数需要
#include <libavutil/opt.h> // av_opt_set 需要
#pragma comment(lib, "avutil.lib")
}
// 编码核心是这个函数,给它一个数据帧,和提前仅分配好空间的packet,
// 编解码器上下文通过avcodec_send_frame从frame数据帧读取数据,再通过avcodec_receive_packet把数据给到packet包
static void encode(AVCodecContext * enc_ctx, AVFrame *frame, AVPacket *pkt, std::ofstream &ofs) {
int ret = 0;
/* send the frame to the encoder */
if (frame)
std::cout << "Send frame: " << frame->pts << std::endl;
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0) {
std::cerr << "Error sending a frame for encoding\n";
exit(1);
}
// 一个原始帧frame可能几个packet包,所以要用循环
while (ret >= 0) {
// 下面这个api往packet里填充数据
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
std::cerr << "Error during encoding\n";
exit(1);
}
std::cout << "Write packet, pts: " << pkt->pts << ", size:" << pkt->size << std::endl;
//ofs.write((const char *)pkt->data, pkt->size);
ofs.write(reinterpret_cast<const char*>(pkt->data), pkt->size);
av_packet_unref(pkt);
}
}
int main(int argc, char* argv[]) {
const char *out_filename = "01.mp4";
const char *codec_name = "mpeg1video";
const AVCodec *codec = nullptr;
AVCodecContext *c_ctx = nullptr;
int ret = 0;
int x = 0, y = 0;
AVFrame *frame = nullptr;
AVPacket *pkt = nullptr;
uint8_t encodec[] = {0, 0, 1, 0xb7};
/* 发现 mpeg1video 编码器 */
codec = avcodec_find_encoder_by_name(codec_name);
if (!codec) {
std::cerr << "Could not file th codec: " << codec_name << std::endl;
exit(1);
}
// 别与 avformat那个context搞混了
c_ctx = avcodec_alloc_context3(codec); // 出现了alloc的都记得去释放
if (!c_ctx) {
std::cerr << "Could not allocate video codec context\n";
return EXIT_FAILURE;
}
pkt = av_packet_alloc();
if (!pkt) exit(1);
/* 自己随便指定一些参数 */
c_ctx->bit_rate = 400000;
/*resolution must be a multiple of two 分辨率必须是2的倍数*/
c_ctx->width = 352;
c_ctx->height = 288;
/*
- 时间基刚好与fps互为倒数
- 这种结构体没有构造函数,只能这样用{}初始化,不能用()
- c中的写法是:c_ctx->time_base = (AVRational){ 1, 25 }; 这在C++中直接报错的
*/
c_ctx->framerate = AVRational{ 25, 1 }; // 这是匿名对象
c_ctx->time_base = AVRational{ 1, 25 }; // C中的写法是用的类型转换
/*
- 每十帧发出一帧内帧,在传递帧之前检查帧的pict_type到编码器,
- 如果frame->pict_type是AV_PICTURE_TYPE_I,则忽略gop_size,
- 并且encoder的输出将永远是I帧,而不管gop_size
*/
c_ctx->gop_size = 10;
c_ctx->max_b_frames = 1; // 说是B帧就1个,就是IBPPIBPP
c_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // 指定像素格式,yuv420p
if (codec->id == AVCodecID::AV_CODEC_ID_H264)
av_opt_set(c_ctx->priv_data, "preset", "slow", 0);
// 打开
ret = avcodec_open2(c_ctx, codec, nullptr);
if (ret < 0) {
std::cerr << "Could not open codec: " << av_err2str(ret) << std::endl;;
exit(1);
}
// 注意要以二进制的方式去写
std::ofstream ofs(out_filename, std::ios::out | std::ios::binary);
if (!ofs) {
std::cerr << "Could not open " << out_filename << std::endl;
exit(1);
}
frame = av_frame_alloc();
if (!frame) {
std::cerr << "Could not allocate video frame\n";
exit(1);
}
frame->format = c_ctx->pix_fmt;
frame->width = c_ctx->width;
frame->height = c_ctx->height;
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
std::cerr << "Could not allocate the video frame data\n" << std::endl;
exit(1);
}
/* 编码一秒的视频,使用25帧 */
for (int i = 0; i < 25; ++i) {
// C的用法,清空输出缓冲区,并把缓冲区内容输出,参看
// https://blog.csdn.net/qq_45761493/article/details/118893882
//fflush(stdout);
/*
确保帧数据是可写的。
在第一轮中,帧是从av_frame_get_buffer()中获取的,因此我们知道它是可写的。
但是在下一轮中,encode()将调用Avcodec_send_frame(),
编解码器可能在其内部结构中保留了对帧的引用,这使得帧不可写.
Av_frame_make_writable()检查并仅在必要时为帧分配一个新的缓冲区。
*/
ret = av_frame_make_writable(frame);
if (ret < 0) exit(1);
/* Prepare a dummy image
说是真实的yuv420p,固定就是这么写
*/
// Y 亮度
for (y = 0; y < c_ctx->height; ++y) {
for (x = 0; x < c_ctx->width; ++x) {
frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
}
}
// Cb Cr 至于 / 2,要去看前面yuv420的理论了
for (y = 0; y < c_ctx->height / 2; ++y) {
for (x = 0; x < c_ctx->width / 2; ++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;
/* 这才开始真正的编码 */
encode(c_ctx, frame, pkt, ofs);
}
/* flush the encoder, 通过第二个参数传递空指针,底层就会处理,代表着结束 */
encode(c_ctx, nullptr, pkt, ofs);
/*
添加序列结束代码以有一个真正的MPEG文件。
它之所以有意义,是因为这个小示例直接写入数据包,
This is called "elementary stream",而且值对有些编解码器有效。
要创建一个有效的文件,通常需要将数据包写入适当的文件格式或协议,可以去看muxing.c。
*/
if (codec->id == AV_CODEC_ID_MPEG1VIDEO || codec->id == AV_CODEC_ID_MPEG2VIDEO) {
ofs.write(reinterpret_cast<const char*>(encodec), sizeof(encodec));
}
ofs.close();
avcodec_free_context(&c_ctx);
av_frame_free(&frame);
av_packet_free(&pkt);
return 0;
}
参考的是官方demo:decode_video.c。里面是用C的写二进制的方式,我改成C++的了.
==解码==API使用详解:核心是==avcodec_send_packet==、==avcodec_receive_frame==这俩函数,他们都是成对出现使用,跟解码刚好相反。
#include <iostream>
#include <fstream>
#include <sstream>
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib") // av_packet_alloc 这些函数需要
#include <libavutil/opt.h>
#pragma comment(lib, "avutil.lib") // 不加这个库,av_frame_alloc这些函数都无法链接
}
#define INBUF_SIZE 4096
static void pgm_save(unsigned char *buf, int wrap, int xsize, int ysize, const char *filename) {
/* C的风格
FILE *f;
int i;
f = fopen(filename,"wb");
fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
for (i = 0; i < ysize; i++)
fwrite(buf + i * wrap, 1, xsize, f);
fclose(f);
// fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255); 执行后文件里会有
P5
100 200 xsize ysize
255
"P5"可能是某种图像格式的标识符,
接下来的两行分别表示图像的宽度和高度(在这个例子中是100和200),
最后一行可能表示图像的某种属性(如最大颜色值,这里是255,表示这是一个8位深度的灰度图像)•
*/
// C++这虽然是二进制打开的,依然可直接这样写明文
std::ofstream ofs(filename, std::ios::out | std::ios::binary);
ofs << "P5\n" << xsize << " " << ysize << "\n" << 255 << std::endl;
for (int i = 0; i < ysize; ++i) {
ofs.write(reinterpret_cast<const char*>(buf + i * wrap), xsize);
}
ofs.close();
}
static void decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt, const char* filename) {
int ret;
std::stringstream ss;
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
std::cerr << "Error sending a packet for decoding\n";
exit(1);
}
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
std::cerr << "Error during decoding\n" << std::flush;
exit(1);
}
// fflush(stdout); // 作用是清空标准输出缓冲区stdout,•确保所有待输出的数据都被发送到目的地(•通常是屏幕)•
// std::flush 是一个作用,std::endl也是和这个作用,不过后者会自己多添加一个换行符
/* 图片由解码器分配。不需要释放它 */ // C的实现是
// snprintf(buf, sizeof(buf), "%s-%d", filename, dec_ctx->frame_number);
ss << filename << "-" << dec_ctx->frame_number;
pgm_save(frame->data[0], frame->linesize[0], frame->width, frame->height, ss.str().c_str());
}
}
int main(int argc, char* argv[]) {
const char* input_filename = "01.mp4";
const char* output_filename = "out";
AVCodecContext *c_ctx = nullptr;
const AVCodec *codec = nullptr;
AVCodecParserContext *parser = nullptr;
AVFrame *frame = nullptr;
AVPacket *pkt = nullptr;
uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE]; // 4096 + 64
uint8_t *data;
size_t data_size;
int ret;
int eof;
std::ifstream ifs;
pkt = av_packet_alloc();
if (!pkt) exit(1);
/* 将缓冲区的结束设置为0(这确保损坏的MPEG流不会发生过读) */
memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);
/* find the MPEG-1 video decoder */
codec = avcodec_find_decoder(AV_CODEC_ID_MPEG1VIDEO);
if (!codec) {
std::cerr << "Codec not found";
exit(1);
}
parser = av_parser_init(codec->id);
if (!parser) {
std::cerr << "parser not found\n";
exit(1);
}
c_ctx = avcodec_alloc_context3(codec);
if (!c_ctx) {
std::cerr << "Could not allocate video codec context\n";
exit(1);
}
/* 对于一些编解码器,如msmpeg4和mpeg4,宽度和高度必须在那里初始化,因为这个信息在比特流中不可用。 */
/* open it */
if (avcodec_open2(c_ctx, codec, nullptr)) {
std::cout << "Could not open codec\n";
exit(1);
}
ifs.open(input_filename, std::ios::in | std::ios::binary);
if (!ifs) {
std::cerr << "Could not open: " << input_filename << std::endl;
exit(1);
}
frame = av_frame_alloc();
if (!frame) {
std::cerr << "Could not allocate video frame\n";
exit(1);
}
do {
ifs.read((char *)inbuf, INBUF_SIZE);
data_size = ifs.gcount(); // 返回实际读取的字节数
if (!ifs) break; // 原来用的是C,是这么写的 if (ferror(f))
eof = !data_size; // 不懂这行代码
/* 使用解析器将数据拆分成帧 */
data = inbuf;
while (data_size > 0 || eof) {
ret = av_parser_parse2(parser, c_ctx, &pkt->data, &pkt->size, data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) {
std::cerr << "Error while parsing\n";
exit(1);
}
data += ret;
data_size -= ret;
if (pkt->size) {
decode(c_ctx, frame, pkt, output_filename);
}
else if (eof) { break; }
}
} while (!eof);
/* flush the decoder */
decode(c_ctx, frame, nullptr, output_filename);
ifs.close();
av_parser_close(parser);
avcodec_free_context(&c_ctx);
av_frame_free(&frame);
av_packet_free(&pkt);
return 0;
}
就是将给定的视频抽离出来单独的音频、视频。like this: “maix.exe audio.mp4 out.yuv out.pcm”
自己后面按照类的方法实现:也是官方示例:demuxing_decoding.c。
av_err2str(ret)源码在C++中使用有点问题,我是F12进去改了它的实现的,改成了
char my_error[AV_ERROR_MAX_STRING_SIZE] = { 0 }; // 提前去初始化的
#define av_err2str(errnum) \
av_make_error_string(my_error, AV_ERROR_MAX_STRING_SIZE, errnum)
一定要注意:
内部自己的函数“open_codec_context”的参数,AVCodecContext **dec_ctx
,一定要二级指针这样写,不然视频解码器的上下文一级指针mVideo_dec_ctx传进去,编译都能通过,但是运行完时,mVideo_dec_ctx还是传进去之前的空指针状态
模仿了一下OpenCV的一个方式,能更加清晰的知道哪个传递指针或引用是为了获取值。
#define AV_IN_OUT // AV_是自己起的,后面的 IN_OUT、OUT 是仿照的OpenCV的API
#define AV_OUT
// C++一般用const来标记输入的参数是只读的,即这个值是为了传进去给别人使用;用指针或是引用不加const,表示这参数是用来接收输出的值的。但C的API中一般没有const,自己加了const,在ffmpeg的api中就无法传递,所以自己这么定义了一下,看起来就很直观
int open_codec_context(AV_IN_OUT AVFormatContext *fmt_ctx, AV_IN_OUT enum AVMediaType type, AV_OUT AVCodecContext **dec_ctx, AV_OUT int *stream_idx)
DemuxingDecoding.hpp: # 后面还是分头文件、源文件写吧
#pragma once
#include <iostream>
#include <fstream>
extern "C" {
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib")
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavutil/imgutils.h> //av_image_alloc 需要
#include <libavutil/timestamp.h> // av_ts2timestr 需要
#pragma comment(lib, "avutil.lib")
}
#define AV_IN_OUT
#define AV_OUT
class DemuxingDecoding {
public:
DemuxingDecoding(const char* src_filename, const char* video_dst_filename, const char* audio_dst_filename) : mSrc_filename(src_filename), mVideo_dst_filename(video_dst_filename), mAudio_dst_filename(audio_dst_filename) { }
~DemuxingDecoding() {
this->finalize();
}
public:
int run() {
int ret = 0;
AVStream *video_stream = nullptr;
AVStream *audio_stream = nullptr;
// 初始化一些参数
ret = this->initialize();
if (ret != 0) {
std::cout << "初始化失败..." << std::endl;
return EXIT_FAILURE;
}
/* open input file, and allocate format context */
if (avformat_open_input(&mFmt_ctx, mSrc_filename, nullptr, nullptr) < 0) {
std::cerr << "Could not open source file: " << mSrc_filename << std::endl;
return EXIT_FAILURE;
}
/* retrieve stream information */
if (avformat_find_stream_info(mFmt_ctx, nullptr) < 0) {
std::cerr << "Could not find stream information\n";
return EXIT_FAILURE;
}
// 获取视频编解码器的数据
if (this->open_codec_context(mFmt_ctx, AVMEDIA_TYPE_VIDEO, &mVideo_dec_ctx, &mVideo_stream_idx) >= 0) {
video_stream = mFmt_ctx->streams[mVideo_stream_idx];
mVideo_dst_file.open(mVideo_dst_filename, std::ios::out | std::ios::binary);
if (!mVideo_dst_file.is_open()) {
std::cerr << "Could not open destination file " << mVideo_dst_filename << std::endl;
return 1;
}
/* allocate image where the decoded image will be put */
mWidth = mVideo_dec_ctx->width;
mHeight = mVideo_dec_ctx->height;
mPix_fmt = mVideo_dec_ctx->pix_fmt;
// 这个函数就是返回的buffer size
ret = av_image_alloc(mVideo_dst_data, mVideo_dst_linesize, mWidth, mHeight, mPix_fmt, 1);
if (ret < 0) {
std::cerr << "Could not allocate raw video buffer\n";
return EXIT_FAILURE;
}
mVideo_dst_bufsize = ret;
}
// 获取音频编解码器的数据
if ((this->open_codec_context(mFmt_ctx, AVMEDIA_TYPE_AUDIO, &mAudio_dec_ctx, &mAudio_stream_idx)) >= 0) {
audio_stream = mFmt_ctx->streams[mAudio_stream_idx];
mAudio_dst_file.open(mAudio_dst_filename, std::ios::out | std::ios::binary);
if (!mAudio_dst_file.is_open()) {
std::cerr << "Could not open destination file " << mAudio_dst_filename << std::endl;
return 1;
}
}
/* 打印输入文件的信息 */
av_dump_format(mFmt_ctx, 0, mSrc_filename, 0);
if (!video_stream && !audio_stream) {
std::cerr << "Could not find audio or video stream in the input, aborting\n";
return EXIT_FAILURE;
}
if (video_stream)
std::cout << "Demuxing video from file " << mSrc_filename << " into " << mVideo_dst_filename;
if (audio_stream)
std::cout << "Demuxing audio from file " << mSrc_filename << " into " << mAudio_dst_filename;
/* read frames from the file */
while (av_read_frame(mFmt_ctx, mPkt) >= 0) {
// 仅处理音频、视频流
if (mPkt->stream_index == mVideo_stream_idx) {
ret = this->decode_packet(mVideo_dec_ctx, mPkt);
}
else if (mPkt->stream_index == mAudio_stream_idx) {
ret = this->decode_packet(mAudio_dec_ctx, mPkt);
}
// 跟frame一样,一帧的packet用完后,要及时unref,不要最后再去,不然可能会内存泄露
av_packet_unref(mPkt);
if (ret < 0) break;
}
/* flush the decoders */
if (mVideo_dec_ctx) {
this->decode_packet(mVideo_dec_ctx, nullptr);
}
if (mAudio_dec_ctx) {
this->decode_packet(mAudio_dec_ctx, nullptr);
}
std::cout << "Demuxing successed.\n";
if (video_stream) {
std::cout << "Play the output video file with the command:\n"
<< "ffplay -f rawvideo -pix_fmt " << av_get_pix_fmt_name(mPix_fmt)
<< " -video_size " << mWidth << "x" << mHeight << " " << mVideo_dst_filename << std::endl;
}
if (audio_stream) {
enum AVSampleFormat &sfmt = mAudio_dec_ctx->sample_fmt;
int n_channels = mAudio_dec_ctx->ch_layout.nb_channels;
const char* fmt;
if (av_sample_fmt_is_planar(sfmt)) {
const char* packed = av_get_sample_fmt_name(sfmt);
std::cout << "Warning: the sample format the decoder produced is planar: " << (packed ? packed : "?") << "This example will output the first channel only.\n";
sfmt = av_get_packed_sample_fmt(sfmt);
n_channels = 1;
}
if (this->get_format_from_sample_fmt(&fmt, sfmt) < 0) {
return 1;
}
std::cout << "Play the output audio file with the command:\n"
<< "ffplay -f " << fmt << " -ac " << n_channels << " -ar " << mAudio_dec_ctx->sample_rate << " " << mAudio_dst_filename;
}
}
private:
int initialize() {
mFmt_ctx = avformat_alloc_context();
mVideo_dec_ctx = nullptr;
mAudio_dec_ctx = nullptr;
mVideo_stream_idx = -1;
mAudio_stream_idx = -1;
mFrame = av_frame_alloc();
if (!mFrame) {
std::cerr << "Could not allocate frame\n";
return AVERROR(ENOMEM);
}
mPkt = av_packet_alloc();
if (!mPkt) {
std::cerr << "Could not allocate packet\n";
return AVERROR(ENOMEM);
}
mVideo_frame_count = 0;
mAudio_frame_count = 0;
return 0;
}
void finalize() {
if (mFmt_ctx) {
avformat_close_input(&mFmt_ctx);
avformat_free_context(mFmt_ctx);
}
// 因为自定义函数中用了 avcodec_alloc_context3 ,所以要去释放编解码器的上下文
if (mVideo_dec_ctx)
avcodec_free_context(&mVideo_dec_ctx);
if (mAudio_dec_ctx)
avcodec_free_context(&mAudio_dec_ctx);
if (mVideo_dst_file)
mVideo_dst_file.close();
if (mAudio_dst_file)
mAudio_dst_file.close();
if (mFrame)
av_frame_free(&mFrame);
if (mPkt)
av_packet_free(&mPkt);
av_free(mVideo_dst_data[0]);
}
// 用来获取音频、视频解码器的上下文具体信息
// 用const来标记输入的参数是只读的,用指针或是引用不加const,表示这参数是用来输出的
// 这里一定要用 AVCodecContext **dec_ctx 这样的二级指针,用 AVCodecContext *dec_ctx这的话,明明全部运行正确,但运行后传递进来的mAudio_dec_ctx指针依旧是空指针
int open_codec_context(AV_IN_OUT AVFormatContext *fmt_ctx, AV_IN_OUT enum AVMediaType type, AV_OUT AVCodecContext **dec_ctx, AV_OUT int *stream_idx) {
int ret = 0;
int stream_index = 0;
AVStream *stream = nullptr;
const AVCodec *dec = nullptr;
const char* media_type_str = av_get_media_type_string(type);
ret = av_find_best_stream(fmt_ctx, type, -1, -1, nullptr, 0);
if (ret < 0) {
std::cerr << "Could not find stream of " << media_type_str << std::endl;
return ret;
}
stream_index = ret;
stream = fmt_ctx->streams[stream_index];
/* find decoder for the stream */
// 别忘了编码用到的类似的,因为编码一般用传入的格式指定,avcodec_find_encoder_by_name(const char*name); 这里解码,因为流里已经带了编码格式,这里直接就查找出来,用来解码
dec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!dec) {
std::cerr << "Failed to find codec of " << media_type_str << std::endl;
return AVERROR(EINVAL);
}
/* Allocate a codec context for the decoder */
// TODO: 后面记得要去释放
*dec_ctx = avcodec_alloc_context3(dec);
if (!*dec_ctx) {
std::cerr << "Failed to allocate the codec context of " << media_type_str << std::endl;
return AVERROR(ENOMEM);
}
/* Copy codec parameters from input stream to output codec context */
if ((ret = avcodec_parameters_to_context(*dec_ctx, stream->codecpar)) < 0) {
std::cerr << "Failed to copy " << media_type_str << " codec parameters to decoder context\n";
return ret;
}
/* Init the decoders */
if ((ret = avcodec_open2(*dec_ctx, dec, nullptr)) < 0) {
std::cerr << "Failed to open codec of " << media_type_str << std::endl;
return ret;
}
*stream_idx = stream_index;
return 0;
}
// 用来进行音频、视频的packet解码
int decode_packet(AVCodecContext *dec, const AVPacket *pkt) {
int ret = 0;
ret = avcodec_send_packet(dec, pkt);
if (ret < 0) {
std::cerr << "Error submitting a packet for decoding: " << av_err2str(ret);
return ret;
}
// get all the available frames from the decoder
while (ret >= 0) {
ret = avcodec_receive_frame(dec, mFrame);
if (ret < 0) {
// 这是两个特殊值,意味着没有输出
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
return 0;
std::cerr << "Error during decoding: " << av_err2str(ret);
return ret;
}
// write the frame data to output file
if (dec->codec->type == AVMEDIA_TYPE_VIDEO) {
ret = this->output_video_frame(mFrame);
}
else if (dec->codec->type == AVMEDIA_TYPE_AUDIO) {
ret = this->output_audio_frame(mFrame);
}
// 一帧数据用完后,一定要unref,之前写的内存泄露可能就是因为这
av_frame_unref(mFrame);
if (ret < 0) return ret;
}
return 0;
}
// 内部函数 decode_packet 里要用的,把这部分代码封装了
int output_video_frame(AVFrame *frame) {
if (frame->width != mWidth || frame->height != mHeight || frame->format != mPix_fmt) {
std::cerr << "Error: Width, height and pixel format have to be constant in a rawvideo file, but the width, height or pixel format of the input video changed:\n"
<< "old: width=" << mWidth << ", height=" << mHeight << ", format=" << av_get_pix_fmt_name(mPix_fmt)
<< "new: width=" << frame->width << ", height=" << frame->height << ", format=" << av_get_pix_fmt_name(static_cast<AVPixelFormat>(frame->format));
return -1;
}
std::cout << "video_frame n: " << mVideo_frame_count++ << " coded_n: " << frame->coded_picture_number << std::endl;
/* copy decoded frame to destination buffer:
* this is required since rawvideo expects non aligned data */
// 源代码是用的 (const uint8_t **)(frame->data)
av_image_copy(mVideo_dst_data, mVideo_dst_linesize, const_cast<const uint8_t **>(frame->data), frame->linesize, mPix_fmt, mWidth, mHeight);
/* write to rawvideo file */
mVideo_dst_file.write(reinterpret_cast<const char *>(mVideo_dst_data[0]), mVideo_dst_bufsize);
return 0;
}
//
int output_audio_frame(AVFrame *frame) {
size_t unpadded_linesize = frame->nb_samples *
av_get_bytes_per_sample(static_cast<AVSampleFormat>(frame->format));
/*
av_ts2timestr(frame->pts, &mAudio_dec_ctx->time_base) 要去改源码的字符串初始化方式,
*/
char my_error[AV_TS_MAX_STRING_SIZE] = { 0 };
std::cout << "audio_frame n: " << mAudio_frame_count++ << " nb_samples: " << frame->nb_samples << " pts: " << av_ts_make_time_string(my_error, frame->pts, &mAudio_dec_ctx->time_base) << std::endl;;
/* Write the raw audio data samples of the first plane. This works
* fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
* most audio decoders output planar audio, which uses a separate
* plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
* In other words, this code will write only the first audio channel
* in these cases.
* You should use libswresample or libavfilter to convert the frame
* to packed data. */
mAudio_dst_file.write(reinterpret_cast<const char *>(frame->extended_data[0]), unpadded_linesize);
return 0;
}
// 纯为音频,了解吧
int get_format_from_sample_fmt(const char* *fmt, enum AVSampleFormat sample_fmt) {
struct sample_fmt_entry {
enum AVSampleFormat sample_fmt;
const char* fmt_be;
const char* fmt_le;
};
struct sample_fmt_entry sample_fmt_entries[] = {
{ AV_SAMPLE_FMT_U8, "u8", "u8" },
{ AV_SAMPLE_FMT_S16, "s16be", "s16le" },
{ AV_SAMPLE_FMT_S32, "s32be", "s32le" },
{ AV_SAMPLE_FMT_FLT, "f32be", "f32le" },
{ AV_SAMPLE_FMT_DBL, "f64be", "f64le" },
};
*fmt = nullptr;
for (int i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entries); ++i) {
struct sample_fmt_entry *entry = &sample_fmt_entries[i];
if (sample_fmt == entry->sample_fmt) {
*fmt = AV_NE(entry->fmt_be, entry->fmt_le);
return 0;
}
}
std::cerr << "sample format " << av_get_sample_fmt_name(sample_fmt) << " is not supported as output format\n";
return -1;
}
private:
const char* mSrc_filename; // 输入的带音频的视频文件路径
const char* mVideo_dst_filename;
const char* mAudio_dst_filename;
AVFormatContext *mFmt_ctx;
AVCodecContext *mVideo_dec_ctx;
AVCodecContext *mAudio_dec_ctx;
AVFrame *mFrame;
AVPacket *mPkt;
int mVideo_stream_idx;
int mAudio_stream_idx;
std::ofstream mVideo_dst_file;
std::ofstream mAudio_dst_file;
int mWidth;
int mHeight;
enum AVPixelFormat mPix_fmt;
// 视频里说,这你用[4],是Y、U、V各占一个,还剩下一个是作为扩展
uint8_t* mVideo_dst_data[4] = { nullptr };
int mVideo_dst_linesize[4];
int mVideo_dst_bufsize;
int mVideo_frame_count;
int mAudio_frame_count;
};
main.cpp
#include "DemuxingDecoding.hpp"
int main(int aegc, char* argv[]) {
const char* src_filename = "C:\\Users\\Administrator\\Desktop\\05\\audio.mp4";
const char* video_dst_filename = "C:\\Users\\Administrator\\Desktop\\05\\01.mp4"; // 上面代码一般给01.yuv格式
const char* audio_dst_filename = "C:\\Users\\Administrator\\Desktop\\05\\01.mp3"; // 一般给01.pcm格式
DemuxingDecoding demux(src_filename, video_dst_filename, audio_dst_filename);
demux.run();
return 0;
}
ffplay -f rawvideo -pix_fmt yuv420p -video_size 1920x1080 C:\Users\Administrator\Desktop\05\01.mp4
。ffplay -f f32le -ac 1 -ar 44100 C:\Users\Administrator\Desktop\05\01.mp3
。 # 这些命令是在运行程序后会自动打印出来的。这里是用了 -vf scale=640:480 ,且把图像处理会灰度图了,最终存成了结果视频。这个更多是了解吧。
也是官方示例:filtering_video.c。流式处理,参考教程,不多写了。
FilterVideo.hpp
#pragma once
#include <iostream>
#include <fstream>
#include <sstream>
#include <thread>
extern "C" {
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")
#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib")
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h> // filter需要的头文件
#pragma comment(lib, "avfilter.lib")
#include <libavutil/opt.h> // av_opt_set_int_list 需要
#pragma comment(lib, "avutil.lib")
}
class FilterVideo {
public:
FilterVideo(const char* Srcfilename, const char* Dstfilename) : mSrcfilename(Srcfilename), mDstfilename(Dstfilename) {}
~FilterVideo() {
if (mFmt_ctx) {
avformat_close_input(&mFmt_ctx);
avformat_free_context(mFmt_ctx);
}
if (mDec_ctx)
avcodec_free_context(&mDec_ctx);
if (mFilter_graph)
avfilter_graph_free(&mFilter_graph);
if (mOfs)
mOfs.close();
}
void run(const char* filter_descr) {
this->initialize();
int ret = 0;
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
AVFrame *filter_frame = av_frame_alloc();
if (!pkt || !frame || !filter_frame) {
std::cerr << "Could not allocate frame or packet\n";
exit(1);
}
// 通过自定义的函数进行参数的赋值
if ((ret = this->open_input_file()) < 0) goto end;
if ((ret = this->init_filters(filter_descr)) < 0) goto end;
/* read all packets */
while (1) {
if ((ret = av_read_frame(mFmt_ctx, pkt)) < 0) break;
if (pkt->stream_index == mVideo_stream_idx) {
ret = avcodec_send_packet(mDec_ctx, pkt);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
break;
}
while (ret >= 0) {
ret = avcodec_receive_frame(mDec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
else if (ret < 0) {
av_log(NULL,AV_LOG_ERROR, "Error while receiving a frame from the decoder\n");
goto end;
}
frame->pts = frame->best_effort_timestamp;
/* push the decoded frame into the filtergraph */
if (av_buffersrc_add_frame_flags(mBuffersrc_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0) {
av_log(NULL, AV_LOG_ERROR, "Error while feeding the filtergraph\n");
break;
}
/* pull filtered frames from the filtergraph */
while (1) {
ret = av_buffersink_get_frame(mBuffersink_ctx, filter_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) goto end;
// 自定义的函数来展示
// this->display_frame(filter_frame, mBuffersink_ctx->inputs[0]->time_base);
// 这里原则存下来
this->write_frame(filter_frame);
av_frame_unref(filter_frame);
}
av_frame_unref(frame);
}
}
av_packet_unref(pkt);
}
end:
av_frame_free(&frame);
av_frame_free(&filter_frame);
av_packet_free(&pkt);
if (ret < 0 && ret != AVERROR_EOF) {
std::cerr << "Error occurred: " << av_err2str(ret) << std::endl;
}
}
private:
void initialize() {
mVideo_stream_idx = -1;
// 这个无法直接alloc,都是在avcodec_alloc_context3分配,后面也要释放
mDec_ctx = nullptr;
mFmt_ctx = avformat_alloc_context();
mFilter_graph = avfilter_graph_alloc();
mBuffersrc_ctx = nullptr;
mBuffersink_ctx = nullptr; // 从它的源码来看不初始化也行
mLast_pts = AV_NOPTS_VALUE;
mOfs.open(mDstfilename, std::ios::out | std::ios::binary);
}
int open_input_file() {
int ret = 0;
const AVCodec *dec = nullptr;
if ((ret = avformat_open_input(&mFmt_ctx, mSrcfilename, nullptr, nullptr)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
return ret;
}
if ((ret = avformat_find_stream_info(mFmt_ctx, nullptr)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
return ret;
}
// 拿取视频流
ret = av_find_best_stream(mFmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot find a video stream in the input file\n");
return ret;
}
mVideo_stream_idx = ret;
/* create decoding context */
mDec_ctx = avcodec_alloc_context3(dec);
if (!mDec_ctx)
return AVERROR(ENOMEM);
avcodec_parameters_to_context(mDec_ctx, mFmt_ctx->streams[mVideo_stream_idx]->codecpar);
// 初始化视频解码器
if ((ret = avcodec_open2(mDec_ctx, dec, nullptr)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open video decoder\n");
return ret;
}
return 0;
}
int init_filters(const char* filter_descr) {
int ret = 0;
char args_char[512];
std::ostringstream args; // 变量的声明一定要在goto之前,不然会提示错误
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
AVRational time_base = mFmt_ctx->streams[mVideo_stream_idx]->time_base;
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_GRAY8 , AV_PIX_FMT_NONE };
if (!outputs || !inputs || !mFilter_graph) {
ret = AVERROR(ENOMEM);
goto end; // 同名的end都是OK的,即不同的函数都可以在内部使用同名标志
}
/* buffer video source: the decoded frames from the decoder will be inserted here. */
args << "video_size=" << mDec_ctx->width << "x" << mDec_ctx->height
<< ":pix_fmt=" << mDec_ctx->pix_fmt
<< ":time_base=" << time_base.num << "/" << time_base.den
<< ":pixel_aspect=" << mDec_ctx->sample_aspect_ratio.num << "/" << mDec_ctx->sample_aspect_ratio.den;
/* 它源码中的写法是:
snprintf(args_char, sizeof(args_char),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
mDec_ctx->width, mDec_ctx->height, mDec_ctx->pix_fmt,
time_base.num, time_base.den,
mDec_ctx->sample_aspect_ratio.num, mDec_ctx->sample_aspect_ratio.den);
// 得到的结果类似于是这样的:
video_size=1920x1080:pix_fmt=0:time_base=1/16000:pixel_aspect=0/1
注:pix_fmt=0是 AV_PIX_FMT_YUV420P
pix_fmt=8是 AV_PIX_FMT_GRAY8
*/
ret = avfilter_graph_create_filter(&mBuffersrc_ctx, buffersrc, "in", args.str().c_str(), nullptr, mFilter_graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
goto end;
}
/* buffer video sink: to terminate the filter chain. */
ret = avfilter_graph_create_filter(&mBuffersink_ctx, buffersink, "out", nullptr, nullptr, mFilter_graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
goto end;
}
ret = av_opt_set_int_list(mBuffersink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
goto end;
}
/*
* Set the endpoints for the filter graph. The filter_graph will
* be linked to the graph described by filters_descr.
*/
/*
* The buffer source output must be connected to the input pad of
* the first filter described by filters_descr; since the first
* filter input label is not specified, it is set to "in" by
* default.
*/
outputs->name = av_strdup("in");
outputs->filter_ctx = mBuffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = nullptr;
/*
* The buffer sink input must be connected to the output pad of
* the last filter described by filters_descr; since the last
* filter output label is not specified, it is set to "out" by
* default.
*/
inputs->name = av_strdup("out");
inputs->filter_ctx = mBuffersink_ctx;
inputs->pad_idx = 0;
inputs->next = nullptr;
if ((ret = avfilter_graph_parse_ptr(mFilter_graph, filter_descr, &inputs, &outputs, nullptr)) < 0) goto end;
if ((ret = avfilter_graph_config(mFilter_graph, nullptr)) < 0) goto end;
end:
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return ret;
}
void display_frame(const AVFrame *frame, AVRational time_base) {
int x, y;
uint8_t *p0, *p;
int64_t delay;
if (frame->pts != AV_NOPTS_VALUE) {
if (mLast_pts != AV_NOPTS_VALUE) {
/* sleep roughly the right amount of time;
* usleep is in microseconds, just like AV_TIME_BASE. */
/* #define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
因此在c++中AV_TIME_BASE_Q用不了,不能用这种类型转换,
所以C++中是用一个匿名对象来替代 */
delay = av_rescale_q(frame->pts - mLast_pts, time_base, AVRational{ 1, AV_TIME_BASE });
if (delay > 0 && delay < 1000000) { // 1000000 us 也就是1s
std::this_thread::sleep_for(std::chrono::microseconds(delay));
}
mLast_pts = frame->pts;
}
}
/* Trivial ASCII grayscale display. */
p0 = frame->data[0];
puts("\033c");
for (y = 0; y < frame->height; y++) {
p = p0;
for (x = 0; x < frame->width; x++)
putchar(" .-+#"[*(p++) / 52]);
putchar('\n');
p0 += frame->linesize[0];
}
fflush(stdout);
}
// 后面新增,用于写入文件
void write_frame(const AVFrame *frame) {
static int printf_flag = 0;
if (!printf_flag) {
printf_flag = 1;
std::cout << "frame width=" << frame->width
<< ", frame height=" << frame->height << std::endl;
if (frame->format == AV_PIX_FMT_YUV420P) {
std::cout << "format is yuv420p\n";
}
else {
// 8 ,对应的是 AV_PIX_FMT_GRAY8
std::cout << "format is = " << frame->format << std::endl;
}
}
/* // yuv40p格式这么写
mOfs.write(reinterpret_cast<const char *>(frame->data[0]), frame->width * frame->height);
mOfs.write(reinterpret_cast<const char *>(frame->data[1]), frame->width/2 * frame->height/2);
mOfs.write(reinterpret_cast<const char *>(frame->data[2]), frame->width/2 * frame->height/2);
*/
// 这里写 frame->data[0] 与frame->data 是一个意思。
mOfs.write(reinterpret_cast<const char *>(frame->data[0]), frame->width * frame->height);
}
private:
const char* mSrcfilename;
const char* mDstfilename;
AVFormatContext *mFmt_ctx;
AVCodecContext *mDec_ctx;
int mVideo_stream_idx;
AVFilterGraph *mFilter_graph;
AVFilterContext *mBuffersrc_ctx;
AVFilterContext *mBuffersink_ctx;
int64_t mLast_pts;
std::ofstream mOfs;
};
main.cpp
#include "FilterVideo.hpp"
int main(int aegc, char* argv[]) {
// 视频里说,这里还有其他需求变换,就直接在后面加,如 "scale=640:480,transpose=1" 可以接很多,逗号隔开就是
const char* filter_descr = "scale=640:480,transpose=cclock";
const char* srcfilename = "./sintel_trailer-480p.webm";
const char* dstfilename = "./out.gray8";
FilterVideo fd(srcfilename, dstfilename);
fd.run(filter_descr);
system("pause");
return 0;
}
注意:
ffplay -f rawvideo -pix_fmt gray8 -video_size 480x640 out.gray8
;
一般来说的流程是:录制->编码->网络传输->解码->播放。
具体代码去看“SRS推流.md”中的2.3小节。
这里有两种形式:
虽然示例代码里有些不是按下面说的来,但自己后续如果用到,一定按照这些来,很可能避免一些坑:
AVCodecContext **dec_ctx
这个一定要用二级指针,不然结果始终是空指针,所以看到*dec_ctx = avcodec_alloc_context3(dec);
这写法一定要遵从,不要觉得前面还解引用一次,直接传递AVCodecContext *一级指针,然后就把那解引用去了,一定有问题的,上面2.6.3音视频混合编码写的非常清楚。