初学者可能不理解为什么打印日志,还要设置这么多级别,但如果参与过实际项目,可能就会发现,如果不控制日志级别,全部打印出来,茫茫多的日志,很难管理。
例子:
```c
include //包含库
av_log_set_level(AV_LOG_DEBUG) //日志级别,debug级是最低级别的日志了。
av_log(NULL, AV_LOG_INFO, "...%s\n", op) //使用INFO级别,打印一条日志。
1:文件的基本操作
ffmpeg对操作系统的文件接口进行了封装,操作更加简便。
#include
#include "string.h"
extern "C"{
#include
#include
#include
#include
#include
}
using namespace std;
int add(int a,int b,int c=0,int d=0){
return 0;
}
int main(int argc, const char * argv[]) {
av_log_set_level(AV_LOG_DEBUG);
av_log(NULL, AV_LOG_INFO, "hello world!");
int ret = 0;
// 文件
avpriv_io_move("/Users/heyutang/Desktop/1.mp4", "/Users/heyutang/Desktop/2.mp4"); //重命名
ret = avpriv_io_delete("/Users/heyutang/Desktop/2.mp4"); //删除
if (ret < 0) {
av_log(NULL, AV_LOG_INFO, "delete fial!");
return -1;
}
//目录
AVIODirContext* ctx = NULL;
ret = avio_open_dir(&ctx, "/Users/heyutang/Desktop", NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "can't open dir");
return -1;
}
AVIODirEntry* entry;
while (1) {
ret = avio_read_dir(ctx, &entry); //读取目录中的文件
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "can't read file");
goto __fail;
}
if (!entry) {
break;
}
av_log(NULL, AV_LOG_INFO, "%s:%lld\n",entry->name,entry->size);
avio_free_directory_entry(&entry);
}
avio_close_dir(&ctx); //关闭目录
return 0;
//使用goto语句对错误进行统一处理。
__fail:
avio_close_dir(&ctx);
return -1;
}
总的来说就是: 流 包 帧
这些概念所对应的结构体如下:
AVFormatContext:格式上下文,即连接多个api的一个桥梁。
AVStream:流
AVPacket:包
举个?:打印音视频信息
几个基本api
av_registr_all() //目前解码器和编码器的初始化都是通过全局变量来完成,这个函数已经不需要了。
avformat_open_input() / avformat_close_input() //打开和关闭输入流
av_dump_format() //打印音视频相关信息
int main(int argc, const char * argv[]) {
av_log_set_level(AV_LOG_INFO);
AVFormatContext* context = NULL;
//这里第三个参数是要打开的文件的格式,这里由于路径中已经指明是 .mp4,第三个参数就不需要再指定了。
int ret = avformat_open_input(&context, "/Users/heyutang/Desktop/test.mp4", NULL, NULL);
if (ret < 0) {
av_log(NULL,AV_LOG_ERROR, "open fail: %s", av_err2str(ret));
goto __final;
}
//四个参数分别是
//1: 上下文
//2: 流编号
//3: 路径
//4: 0-输出流 1-输入流
av_dump_format(context, 0, "/Users/heyutang/Desktop/test.mp4", 0);
avformat_close_input(&context);
return 0;
__final:
return -1;
}
下面是输出结果:
//input的后的0就是我们刚刚指定的流编号
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/heyutang/Desktop/test.mp4':
Metadata:
major_brand : mp42
minor_version : 1
compatible_brands: isommp423gp5
creation_time : 2018-11-02T08:07:34.000000Z
encoder : FormatFactory : www.pcfreetime.com
Duration: 00:06:24.08, bitrate: N/A
//视频流0,码率是228 kb/s,帧率是 14.91 fps
Stream #0:0(und): Video: mpeg4 (mp4v / 0x7634706D), none, 228 kb/s, SAR 1:1 DAR 0:0, 14.91 fps, 14.91 tbr, 14906 tbn (default)
Metadata:
creation_time : 2018-11-02T08:07:34.000000Z
handler_name : video
Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 44100 Hz, 2 channels, 125 kb/s (default)
Metadata:
creation_time : 2018-11-02T08:07:34.000000Z
handler_name : sound
Stream #0:2(und): Data: none (mp4s / 0x7334706D), 0 kb/s (default)
Metadata:
creation_time : 2018-11-02T08:07:34.000000Z
handler_name : GPAC MPEG-4 OD Handler
Stream #0:3(und): Data: none (mp4s / 0x7334706D), 0 kb/s (default)
Metadata:
creation_time : 2018-11-02T08:07:34.000000Z
handler_name : GPAC MPEG-4 Scene Description Handler
Program ended with exit code: 0
分为三步:
1: 获取音频流和视频流的编号。
2: 从上下文中读入frame。
3: 如果是音频frame,写入音频文件中,如果是视频frame,写入视频文件中。
这里需要注意的是
h264有两种结构,一种是mp4的,sps pps信息保存在 fmt_ctx->streams[in->stream_index]->codecpar->extradata 中,一种是将sps pps信息放在关键帧(IDR)前面,因此从mp4中提取的h264如果想直接播放,需要在IDR帧之前添加sps pps信息。
AVPacket并不是标准的nalu,其中的前4个字节表示nalu长度,后面才是nalu数据,而每个nalu数据之前缺少分隔符(00 00 00 01),因此将nalu前4个字节替换成分隔符(00 00 00 01)就可以得到标准的 nalu数据了。
ffmpeg为我们提供了滤镜:h264_mp4toannexb 来做这件事情。
H.264码流有NALU构成,第一个NALU是SPS(序列参数集)、第二个NALU是PPS(图像参数集)、第三个NALU是IDR。其中IDR是一种特殊的I帧,IDR帧会导致DPB(参考帧列表)清空,因此IDR帧之后的所有帧不会参考IDR帧之前的帧,具有随机访问的能力。
下面这种实现方式转自雷神博客:
#import
#include
#include
#include "string.h"
extern "C"{
#include
#include
#include
#include
#include
}
using namespace std;
#include
#define __STDC_CONSTANT_MACROS
#define USE_H264BSF 1
int main(int argc, char* argv[])
{
AVFormatContext *ifmt_ctx = NULL;
AVPacket pkt;
int ret;
int videoindex=-1,audioindex=-1;
const char *in_filename = "/Users/heyutang/Desktop/test.mp4";//Input file URL
const char *out_filename_v = "/Users/heyutang/Desktop/test.h264";//Output file URL
const char *out_filename_a = "/Users/heyutang/Desktop/test.mp3";
//Input
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
printf( "Could not open input file.");
return -1;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
printf( "Failed to retrieve input stream information");
return -1;
}
videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
audioindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
//输出视频的基本信息
av_dump_format(ifmt_ctx, 0, in_filename, 0);
FILE *fp_audio=fopen(out_filename_a,"wb+");
FILE *fp_video=fopen(out_filename_v,"wb+");
//h264有两种封装,一种是annexb模式,有startcode,SPS和PPS在流中,一种是mp4模式,没有startcode,sps_pps被封装在container中,每一个frame之前是这个frame的长度,因此,从mp4文件中读取的h264视频流要转化成 annexb模式才可以播放。
//输入:mp4文件中,h264文件是如下排布的 nalu_size nalu nalu_size nalu...,其中sps_pps数据在extradata中。
//通过如下方式,将mp4文件中的流转化为annexb流。
//如果nalu对应的是关键帧,在处理的时候就需要在前面加上sps_pps数据。
//nalu之间,nalu和sps_pps之间通过起始码进行分隔。
//nalu单元本身 = nalu头部 + 数据,其中头部一个字节,8位,第一位0不管,第二三位为优先级,后面5位代表nalu单元的类型,5为关键帧。
//输出:总体结构为 offset | sps_pps | startcode(00 00 01 or 00 00 00 01) | nalu单元(IDR) | startcode(00 00 01) | nalu单元(普通帧)...
//这部分代码,ffmpeg已经帮我们实现了,调用滤镜 h264_mp4toannexb
#if USE_H264BSF
AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb");
#endif
while(av_read_frame(ifmt_ctx, &pkt)>=0){
//如果当前读取的流的编号是想要提取的编号。
if(pkt.stream_index==videoindex){
#if USE_H264BSF
// 使用滤镜
av_bitstream_filter_filter(h264bsfc, ifmt_ctx->streams[videoindex]->codec, NULL, &pkt.data, &pkt.size, pkt.data, pkt.size, 0);
#endif
//
printf("Write Video Packet. size:%d\tpts:%lld\n",pkt.size,pkt.pts);
fwrite(pkt.data, 1, pkt.size, fp_video);
}else if(pkt.stream_index==audioindex){
/*
AAC in some container format (FLV, MP4, MKV etc.) need to add 7 Bytes
ADTS Header in front of AVPacket data manually.
Other Audio Codec (MP3...) works well.
*/
printf("Write Audio Packet. size:%d\tpts:%lld\n",pkt.size,pkt.pts);
fwrite(pkt.data,1,pkt.size,fp_audio);
}
av_free_packet(&pkt);
}
#if USE_H264BSF
av_bitstream_filter_close(h264bsfc);
#endif
fclose(fp_video);
fclose(fp_audio);
avformat_close_input(&ifmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
printf( "Error occurred.\n");
return -1;
}
return 0;
}
但是这种方式中的有些方法已经废弃了,如 av_bitstream_filter_init,下面是新的版本,但对于某些情况还有问题,后续需要修复。
int main(int argc, char* argv[])
{
AVFormatContext *ifmt_ctx = NULL;
AVPacket pkt;
int ret;
int videoindex=-1,audioindex=-1;
const char *in_filename = "/Users/heyutang/Desktop/test.mp4";//Input file URL
const char *out_filename_v = "/Users/heyutang/Desktop/test.h264";//Output file URL
const char *out_filename_a = "/Users/heyutang/Desktop/test.aac";
//Input
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
printf( "Could not open input file.");
return -1;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
printf( "Failed to retrieve input stream information");
return -1;
}
videoindex=-1;
videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
audioindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
//输出视频的基本信息
av_dump_format(ifmt_ctx, 0, in_filename, 0);
FILE *fp_audio=fopen(out_filename_a,"wb+");
FILE *fp_video=fopen(out_filename_v,"wb+");
const AVBitStreamFilter *bsf = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext* bsf_context;
av_bsf_alloc(bsf, &bsf_context);
while(av_read_frame(ifmt_ctx, &pkt)>=0){
//如果当前读取的流的编号是想要提取的编号。
if(pkt.stream_index==videoindex){
//2: 获取参数,添加到上下文,然后初始化。
AVStream* vedioStream = ifmt_ctx->streams[videoindex];
avcodec_parameters_copy(bsf_context->par_in, vedioStream->codecpar);
av_bsf_init(bsf_context);
//3: 将参数写入packet
av_bsf_send_packet(bsf_context, &pkt);
fwrite(pkt.data, 1, pkt.size, fp_video);
}else if(pkt.stream_index==audioindex){
fwrite(pkt.data,1,pkt.size,fp_audio);
}
av_packet_unref(&pkt);
}
#if USE_H264BSF
// av_bitstream_filter_close(h264bsfc);
av_bsf_free(&bsf_context);
#endif
fclose(fp_video);
fclose(fp_audio);
avformat_close_input(&ifmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
printf( "Error occurred.\n");
return -1;
}
return 0;
}
主要api如下:
//要转文件格式,首先还是要新建和释放上下文
avformat_alloc_output_context2() / avformat_free_context()
//新建流
avformat_new_stream()
//参数拷贝(sps pps信息)
avcodec_parameters_copy()
//多媒体文件头
avformat_write_header()
//写入数据
av_write_frame()/av_interleaved_write_frame()
//写入多媒体文件尾
av_write_trailer()