ffmpeg初级开发

FFmpeg代码结构:

ffmpeg初级开发_第1张图片

一:日志系统

ffmpeg初级开发_第2张图片
初学者可能不理解为什么打印日志,还要设置这么多级别,但如果参与过实际项目,可能就会发现,如果不控制日志级别,全部打印出来,茫茫多的日志,很难管理。
例子:

```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;
}

2:多媒体文件的基本概念和打印

总的来说就是: 流 包 帧

  • 多媒体文件本质是一个容器。
  • 在容器里有多个流(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,写入视频文件中。

这里需要注意的是

  1. h264有两种结构,一种是mp4的,sps pps信息保存在 fmt_ctx->streams[in->stream_index]->codecpar->extradata 中,一种是将sps pps信息放在关键帧(IDR)前面,因此从mp4中提取的h264如果想直接播放,需要在IDR帧之前添加sps pps信息。

  2. 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;
}

四:将MP4转成FLV格式

主要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()

你可能感兴趣的:(音视频)