初学者可能不理解为什么打印日志,还要设置这么多级别,但如果参与过实际项目,可能就会发现,如果不控制日志级别,全部打印出来,茫茫多的日志,很难管理。 例子:
```c include <libavutil/log.h> //包含库 av_log_set_level(AV_LOG_DEBUG) //日志级别,debug级是最低级别的日志了。 av_log(NULL, AV_LOG_INFO, "...%s\n", op) //使用INFO级别,打印一条日志。1:文件的基本操作 ffmpeg对操作系统的文件接口进行了封装,操作更加简便。
#include <iostream> #include "string.h" extern "C"{ #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> #include <libavutil/log.h> } 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; }总的来说就是: 流 包 帧
多媒体文件本质是一个容器。在容器里有多个流(stream/track)。每种流是由不同的编码器进行编码的,比如音频用mp3,视频用h264等。从流中读取的数据,我们称之为 包。包解码后,就变成了一个或一组数据帧,视频帧就是一幅幅图,音频帧就是离散的采样点了。这些概念所对应的结构体如下:
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 <Foundation/Foundation.h> #include <iostream> #include <stdio.h> #include "string.h" extern "C"{ #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> #include <libavutil/log.h> } using namespace std; #include <stdio.h> #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()