首页专栏详情
打赏
ffmpeg(九)MP4/MP3解封装
易百纳技术社区 不完整教程 2021-04-27 16:32:49

ffmpeg(九)MP4/MP3解封装

前言

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

解封装相关流程

img点击并拖拽以移动

解封装相关函数介绍

  • 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函数

打赏
共1人已赏
完整的教程https://fizzz.blog.csdn.net。该网站都是残缺
评论
0个
内容存在敏感词
相关专栏
打赏作者
易百纳技术社区
不完整教程
您的支持将鼓励我继续创作!
打赏金额:
¥1 易百纳技术社区
¥5 易百纳技术社区
¥10 易百纳技术社区
¥50 易百纳技术社区
¥100 易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区 微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

审核成功

发布时间设置
发布时间:

审核失败

失败原因
备注
Loading...
易百纳技术社区
确定要删除此文章、专栏、评论吗?
确定
取消
易百纳技术社区
易百纳技术社区
在专栏模块发布专栏,可获得其他E友的打赏
易百纳技术社区
回答悬赏问答,被题主采纳后即可获得悬赏金
易百纳技术社区
在上传资料时,有价值的资料可设置为付费资源
易百纳技术社区
达到一定金额,收益即可提现~
收益也可用来充值ebc,下载资料、兑换礼品更容易
易百纳技术社区
活动规则
  • 1.周任务为周期性任务,每周周一00:00刷新,上周完成的任务不会累计到本周,本周需要从头开始任务,当前任务完成后才可以完成下一个任务
  • 2.发布的专栏与资料需要与平台的板块有相关性,禁止注水,专栏/资料任务以审核通过的篇数为准
  • 3.任务完成后,现金奖励直接打款到微信账户;EBC/收益将自动发放到个人账户,可前往“我的钱包”查看;其他奖励请联系客服兑换
  • 4.每周最后三个任务将会有以下奖品掉落:社区热卖开发板、小米音响、视频年度会员、京东卡、华为手机等等
易百纳技术社区
升级提醒
易百纳技术社区

恭喜您由入门

社区送出礼品一份

请填写您的收件地址,礼品将在3个工作日寄出

易百纳技术社区