MP4/MP3解封装ffmpeg(十三)

前言

解封装包括很多层步骤,包括协议的解析,封装格式的解析。ffmpeg中,本地文件当做file://协议来解析,远程文件采用的传输协议有http(s),rtsp等等。封装格式比如MP4,MOV,TS,MPEG等等。对于ffmpeg来说,只需要调用一个借口函数即可完成解封装的所有步骤,非常简单

解封装相关流程

image.png

解封装相关函数介绍

  • 1、AVFormatContext重要字段介绍(针对解封装后的)

nb_streams:包含的流的个数
streams:每个流对象,流对象中包括音视频编码参数信息;具体存储在AVStream中AVCodecParameters对象里面
metadata:解封装对应格式的标签信息

AVCodecParameters
codec_type:表示数据类型,音频数据或者视频数据
codec_id:音频或者视频采用的编码器
format:音频,采样格式;视频,像素格式
width/height:视频宽高
color_range:视频颜色范围;AVCOL_RANGE_MPEG:代表以BT601,BT709,BT2020等以广播电视系统类的颜色取值范围类型;AVCOL_RANGE_JPEG:代表以电脑等显示器类的颜色取值范围
color_space:颜色空间;比如RGB,YUV,CMYK等等
channel_layout:音频的声道类型
channels:音频的声道数
sample_rate:音频的采样率
frame_size:音频编码器设置的frame size大小

  • 2、int avformat_open_input(AVFormatContext **ps,const char *url,ff_const59 AVInputFormat *fmt,AVDictionary **options)

根据给定的url和fmt,options参数进行执行解封装,最终解封装的相关参数填入到ps中去,成功则返回0,失败则返回负数;

  • 3、int avformat_find_stream_info(AVFormatContext *ps,AVDictionary **options)

查找流信息,并尝试读取一个数据包,将编解码参数赋值给AVFormatContext;
成功返回0,失败返回负数

  • 4、int av_dump_info(AVFormatContext *ps,int index,const char *url,int is_output)

打印解封装后的参数信息

  • 5、AVFormatContext *avformat_alloc_context(void)

创建一个AVFormatContext对象,并赋值为初始值,不包含编码相关参数

  • 6、AVIOContext *avio_alloc_context(
    unsigned char *buff,
    int buffer_size,
    int write_flag,
    void opaque,
    int (
    read_packet)(void *opaque,uint8_t buffer,int buf_size),
    int (
    write_packet)(void *opaque,uint8_t buffer,int buf_size),
    int64_t (
    seek)(void *opaque,int64_t offset,int whence)
    )

创建一个AVIOContext对象,该对象创建完毕后赋值给AVFormatContext的pb对象。该赋值必须再avformat_open_input()函数调用之前完成;默认情况下,avformat_open_input()函数内部会自己创建pb和AVFormatContext对象,也可以用步骤5和6提前自定义

  • 7、avformat_close_input(AVFormatContext **ps)

关闭AVFormatContext上下文

  • 8、avio_context_free()

关闭AVIOContext上下文

实现代码

头文件

//
//  demuxer.hpp
//  video_encode_decode
//
//  Created by apple on 2020/3/24.
//  Copyright © 2020 apple. All rights reserved.
//

#ifndef demuxer_hpp
#define demuxer_hpp

#include <stdio.h>
#include <string>
#include "CLog.h"
extern "C"{
#include <libavutil/imgutils.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
#include <libavutil/file.h>
#include <libavformat/avio.h>
}
using namespace std;

/** ffmpeg编译完成后支持的解封装器位于libavformat目录下的demuxer_list.c文件中,具体的配置再.configure文件中,如下:
 *  print_enabled_components libavformat/demuxer_list.c AVInputFormat demuxer_list $DEMUXER_LIST
 *  xxxx.mp4对应的解封装器为ff_mov_demuxer
 */
class Demuxer
{
public:
    Demuxer();
    ~Demuxer();
    
    void doDemuxer();
};
#endif /* demuxer_hpp */

实现文件

//
//  demuxer.cpp
//  video_encode_decode
//
//  Created by apple on 2020/3/24.
//  Copyright © 2020 apple. All rights reserved.
//

#include "demuxer.hpp"

struct buffer_data {
    uint8_t *ptr;
    uint8_t *ptr_start;
    size_t size; ///< buffer size
};

Demuxer::Demuxer()
{
    
}

Demuxer::~Demuxer()
{
    
    
}

/** 参考ffmpeg源码file.c的file_seek方法。
 *  1、该函数的作用有两个,第一个返回外部缓冲区的大小,类似于lstat()函数;第二个设置外部缓冲读取指针的位置(读取指针的位置
 *  相对于缓冲区首地址来说的),类似于lseek()函数
 *  2、AVSEEK_SIZE 代表返回外部缓冲区大小
 *  3、SEEK_CUR 代表将外部缓冲区读取指针从目前位置偏移offset
 *  4、SEEK_SET 代表将外部缓冲区读取指针设置到offset指定的偏移
 *  5、SEEK_END 代表将外部缓冲区读取指针设置到相对于尾部地址的偏移
 *  6、3/4/5情况时返回当前指针位置相对于缓冲区首地址的偏移。offset 的值可以为负数和0。
 */
static int64_t io_seek(void* opaque,int64_t offset,int whence)
{
    struct buffer_data *bd = (struct buffer_data*)opaque;
    if (whence == AVSEEK_SIZE) {
        return bd->size;
    }
    
    if (whence == SEEK_CUR) {
        bd->ptr += offset;
    } else if (whence == SEEK_SET) {
        bd->ptr = bd->ptr_start+offset;
    } else if (whence == SEEK_END) {
        bd->ptr = bd->ptr_start + bd->size + offset;
    }
    
    return (int64_t)(bd->ptr - bd->ptr_start);
}

/** 参考ffmpeg file.c的file_read()源码
 *  1、该函数的意思就是需要从外部读取指定大小buf_size的数据到指定的buf中;这里外部是一个内存缓存
 *  2、每次读取完数据后需要将读取指针后移
 *  3、如果外部数据读取完毕,则需要返回AVERROR_EOF错误
 *  4、读取成功,返回实际读取的字节数
 */
static int io_read(void *opaque, uint8_t *buf, int buf_size)
{
    static int total = 0;
    struct buffer_data *bd = (struct buffer_data *)opaque;
    buf_size = FFMIN(buf_size, (int)(bd->ptr_start+bd->size-bd->ptr));
    total += buf_size;
    if (buf_size <= 0)
        return AVERROR_EOF;
//    LOGD("ptr:%p size:%zu buf_size %d total %d\n", bd->ptr, bd->size,buf_size,total);

    /* copy internal buffer data to buf */
    memcpy(buf, bd->ptr, buf_size);
    bd->ptr  += buf_size;

    return buf_size;
}

void Demuxer::doDemuxer()
{
    string curFile(__FILE__);
    unsigned long pos = curFile.find("1-video_encode_decode");
    if (pos == string::npos) {
        LOGD("can not find file");
        return;
    }
    
    string recourDir = curFile.substr(0,pos)+"filesources/";
    // mdata标签在moov之前
    string srcPath = recourDir+"test_1280x720.MP4";
    // mdata标签在moov之后
//    string srcPath = "/Users/apple/Downloads/Screenrecorder-2020-03-31-16-36-12-749\(0\).mp4";
    
    AVFormatContext *inFmtCtx = NULL;
    int ret = 0;
#define Use_Custom_io   0
#if Use_Custom_io
    AVIOContext *ioCtx;
    uint8_t *io_ctx_buffer = NULL,*buffer = NULL;
    size_t io_ctx_buffer_size = 4096,buffer_size;
    buffer_data bd = {0};
    ret = av_file_map(srcPath.c_str(),&buffer,&buffer_size,0,NULL);
    if (ret < 0) {
        LOGD("av_file_map fail");
        return;
    }
    bd.ptr = buffer;
    bd.ptr_start = buffer;
    bd.size = buffer_size;
    
    inFmtCtx = avformat_alloc_context();
    if (inFmtCtx == NULL) {
        LOGD("avformat_alloc_context fail");
        return;
    }
    io_ctx_buffer = (uint8_t*)av_mallocz(io_ctx_buffer_size);
    /** 遇到问题:如果没有指定io_seek函数,对于MP4文件来说,如果mdata在moov标签的后面,采用自定义的AVIOContext时
     *  候avformat_find_stream_info() 返回"Could not findcodec parameters for stream 0 ........:
     *  unspecified pixel formatConsider increasing the value for the 'analyzeduration' and 'probesize' options的错误
     *  av_read_frame()返回Invalid data found when processing input的错误
     *  分析原因:在创建AVIOContext时没有指定seek函数
     *  解决方案:因为创建AVIOContext时没有指定io_seek函数并正确实现io_read()和io_seek()相关逻辑;参考如上io_seek和io_read函数
     */
    ioCtx = avio_alloc_context(io_ctx_buffer,(int)io_ctx_buffer_size,0,&bd,&io_read,NULL,&io_seek);
    if (ioCtx == NULL) {
        LOGD("avio_alloc_context fail");
        return;
    }
    inFmtCtx->pb = ioCtx;
#endif
    /** 遇到问题:avformat_open_input -1094995529
     *  原因分析:这里要打开的文件是MP4文件,而对应的MP4的解封装器没有编译到ffmpeg中,--enable-demuxer=mov打开重新编译即可。
     */
    /** 参数1:AVFormatContext 指针变量,可以用avformat_alloc_context()先初始化或者直接初始化为NULL
     *  参数2:文件名路径
     *  参数3:AVInputFormat,接封装器对象,传NULL,则根据文件名后缀猜测。非NULL,则由这个指定的AVInputFormat进行解封装
     *  参数4:解封装相关参数,传NULL用默认即可
     */
    ret = avformat_open_input(&inFmtCtx,srcPath.c_str(),NULL,NULL);
    if (ret < 0) {
        LOGD("avformat_open_input fail %d error:%s",ret,av_err2str(ret));
        return;
    }
    
    LOGD("ddd probesize %d analyzeduration %d",inFmtCtx->probesize,inFmtCtx->max_analyze_duration);
    ret = avformat_find_stream_info(inFmtCtx,NULL);
    if (ret < 0) {
        LOGD("avformat_find_stream_info fail %d error:%s",ret,av_err2str(ret));
        return;
    }
    LOGD("begin av_dump_format");
    av_dump_format(inFmtCtx,0,NULL,0);
    LOGD("end av_dump_format");
    
    // 解析出封装格式中的标签
    LOGD("begin mediadata \n\n");
    const AVDictionaryEntry *tag = NULL;
    while ((tag = av_dict_get(inFmtCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
        LOGD("tag key:%s value:%s",tag->key,tag->value);
    }
    LOGD("end mediadata \n\n");
    
    // 解析出封装格式中的编码相关参数
    for (int i = 0;i<inFmtCtx->nb_streams;i++) {
        AVStream *stream = inFmtCtx->streams[i];
        enum AVCodecID cId = stream->codecpar->codec_id;
        int format = stream->codecpar->format;
        if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            LOGD("begin video AVStream \n\n");
            LOGD("code_id %s format ",avcodec_get_name(cId));
            
            const AVPixFmtDescriptor *fmtDes = av_pix_fmt_desc_get((enum AVPixelFormat)format);
            LOGD("AVPixFmtDescriptor name %s",fmtDes->name);
            
            LOGD("width %d height %d",stream->codecpar->width,stream->codecpar->height);
            /** 颜色的取值范围
             *  AVCOL_RANGE_MPEG:代表以BT601,BT709,BT2020等以广播电视系统类的颜色取值范围类型
             *  AVCOL_RANGE_JPEG:代表以电脑等显示器类的颜色取值范围
             */
            LOGD("color_range %d",stream->codecpar->color_range);
            /** 所采用的颜色空间,比如RGB,YUV,CMYK等等
             */
            LOGD("color_space %s",av_get_colorspace_name(stream->codecpar->color_space));
            LOGD("video_delay %d\n\n",stream->codecpar->video_delay);
        } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            LOGD("begin audio AVStream \n\n");
            LOGD("code_id %s format ",avcodec_get_name(cId));
            LOGD("samplefmt %s",av_get_sample_fmt_name((enum AVSampleFormat)format));
            LOGD("channel_layout %s channels %d",av_get_channel_name(stream->codecpar->channel_layout),stream->codecpar->channels);
            LOGD("sample_rate %d",stream->codecpar->sample_rate);
            LOGD("frame_size %d",stream->codecpar->frame_size);
            
        } else {
            LOGD("other type");
        }
    }
    LOGD("end AVStream\n\n");
    
    
    AVPacket *packet = av_packet_alloc();
    static int num = 0;
    LOGD("begin av_read_frame");
    while ((ret = av_read_frame(inFmtCtx, packet)) >= 0) {
        num++;
//        LOGD("av_read_frame size %d num %d",packet->size,num);
        
        av_packet_unref(packet);
    }
    LOGD("end av_read_frame ret %s",av_err2str(ret));
    
    
    /** 释放内存
     */
    avformat_close_input(&inFmtCtx);
    // 对于自定义的AVIOContext,先释放里面的buffer,在释放AVIOContext对象
#if Use_Custom_io
    if (ioCtx) {
        av_freep(&ioCtx->buffer);
    }
    avio_context_free(&ioCtx);
    av_file_unmap(buffer, buffer_size);
#endif
}

备注:分别实现了自定义AVIOContext的方式和采用avformat_opent_input()函数默认方式进行解封装,Use_Custom_io为0代表采用默认方式

遇到问题

1、如果没有指定io_seek函数,对于MP4文件来说,如果mdata在moov标签的后面,采用自定义的AVIOContext时候avformat_find_stream_info() 返回"Could not findcodec parameters for stream 0 ........:unspecified pixel formatConsider increasing the value for the 'analyzeduration' and 'probesize' options的错误av_read_frame()返回Invalid data found when processing input的错误
分析原因:在创建AVIOContext时没有指定seek函数
解决方案:因为创建AVIOContext时没有指定io_seek函数并正确实现io_read()和io_see()相关逻辑;参考如上io_seek和io_read函数

项目代码

示例地址

示例代码位于cppsrc目录下文件
demuxer.hpp
demuxer.cpp

项目下示例可运行于iOS/android/mac平台,工程分别位于demo-ios/demo-android/demo-mac三个目录下,可根据需要选择不同平台

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343