文章部分内容参考https://www.matroska.org/technical/specs/index.html

1.mkv封装格式简介


  • Matroska 开源多媒体容器标准。MKV属于其中的一部分。Matroska常见的有.MKV视频格式、MKA音频格式、.MKS字幕格式、.MK3D files (stereoscopic/3D video)。MKV这种多媒体封装是建立在EBML语言的基础上,EBML是可扩展的二进制语言,优势在于其可变长度的整数存储,节省空间。

2.EBML语言

  • EBML中级别
    EBML中的形式表达是树形结构,对应元素出现的树节点深度定义为级别,最高级别为0,类似于树的根节点,特殊级别表达:“+” 表示该元素在该层级可递归存在;“g” 表示为存在于任意级别。
    mkv文件的组织可以看作有两部分组成:
    1.首先是EBML Header 2.segment ,mkv/wbem文件中只包含一个segment,包含其它TOP-LEVEL级别节点。
  • EBML中的语义(EbmlSemantic)
    EbmlSemantic:语义规范是预定义好的。元素基本的语义规则可总结为两类
    value和bool语义: 其中有关bool的描述Mandatory(必须包含)和Unique(是否包含一个);有关value的描述有EbmlUInteger、EbmlDate、EbmlFloat、EbmlSInteger、EbmlUnicodeString。而标识元素最为显著的属性为EbmlId。
const EbmlSemantic EbmlHead_ContextList[] =
{
    EbmlSemantic(true, true, EVersion::ClassInfos),        ///< EBMLVersion
    EbmlSemantic(true, true, EReadVersion::ClassInfos),    ///< EBMLReadVersion
    EbmlSemantic(true, true, EMaxIdLength::ClassInfos),    ///< EBMLMaxIdLength
    EbmlSemantic(true, true, EMaxSizeLength::ClassInfos),  ///< EBMLMaxSizeLength
    EbmlSemantic(true, true, EDocType::ClassInfos),        ///< DocType
    EbmlSemantic(true, true, EDocTypeVersion::ClassInfos), ///< DocTypeVersion
    EbmlSemantic(true, true, EDocTypeReadVersion::ClassInfos), ///< DocTypeReadVersion
};

//EbmlSemanticContext上下文,需要
const EbmlSemanticContext EbmlHead_Context = EbmlSemanticContext(countof(EbmlHead_ContextList), EbmlHead_ContextList, NULL, *GetEbmlGlobal_Context, &EbmlHead::ClassInfos);

EbmlId EbmlHead_TheId(0x1A45DFA3, 4);  //容器格式的可辨识行信息;
const EbmlCallbacks EbmlHead::ClassInfos(EbmlHead::Create, EbmlHead_TheId, "EBMLHead", EbmlHead_Context);
EbmlSemantic KaxSegment_ContextList[8] =
{
    EbmlSemantic(false, false, KaxCluster::ClassInfos),    //
    EbmlSemantic(false, false, KaxSeekHead::ClassInfos),  //SeekHead;
    EbmlSemantic(false, true,  KaxCues::ClassInfos),      //speed seeking;
    EbmlSemantic(false, false, KaxTracks::ClassInfos),    //包含有Tracks;
    EbmlSemantic(true,  true,  KaxInfo::ClassInfos),     //segment infomation;
    EbmlSemantic(false, true,  KaxChapters::ClassInfos),
    EbmlSemantic(false, true,  KaxAttachements::ClassInfos),
    EbmlSemantic(false, true,  KaxTags::ClassInfos),
};

const EbmlSemanticContext KaxMatroska_Context = EbmlSemanticContext(countof(KaxMatroska_ContextList), KaxMatroska_ContextList, NULL, *GetKaxGlobal_Context, NULL);
const EbmlSemanticContext KaxSegment_Context = EbmlSemanticContext(countof(KaxSegment_ContextList), KaxSegment_ContextList, NULL, *GetKaxGlobal_Context, &KaxSegment::ClassInfos);

EbmlId KaxSegment_TheId(0x18538067, 4);
const EbmlCallbacks KaxSegment::ClassInfos(KaxSegment::Create, KaxSegment_TheId, "Segment", KaxSegment_Context);

2.EbmlMaster
派生于EbmlMaster的为Top-Level级节点,缺少value语义,主要用来控制下层级别节点,这里以KaxCluster为例进行说明:

class KaxCluster : public EbmlMaster {
    public:
        KaxCluster();
        static EbmlElement & Create() {return *(new KaxCluster);}
        const EbmlCallbacks & Generic() const {return ClassInfos;}
        static const EbmlCallbacks ClassInfos;
        operator const EbmlId &() const {return ClassInfos.GlobalId;}
        /*I帧*/
        bool AddFrame(const KaxTrackEntry & track, uint64 timecode, DataBuffer & buffer, KaxBlockGroup * & MyNewBlock);
        /*P帧*/
        bool AddFrame(const KaxTrackEntry & track, uint64 timecode, DataBuffer & buffer, KaxBlockGroup * & MyNewBlock, const KaxBlockGroup & PastBlock);

        /*B帧*/
        bool AddFrame(const KaxTrackEntry & track, uint64 timecode, DataBuffer & buffer, KaxBlockGroup * & MyNewBlock, const KaxBlockGroup & PastBlock, const KaxBlockGroup & ForwBlock);

        /*!
            \brief Render the data to the stream and retrieve the position of BlockGroups for later cue entries
        */
        uint32 Render(IOCallback & output, KaxCues & CueToUpdate, bool bSaveDefault = false);

        /*!
            \return the global timecode of this Cluster
        */
        uint64 GlobalTimecode() const;

        KaxBlockGroup & GetNewBlock();

        /*!
            \brief release all the frames of all Blocks
            \note this is a convenience to be able to keep Clusters+Blocks in memory (for future reference) withouht being a memory hog
        */
        void ReleaseFrames();

        /*返回offset偏移量*/
        uint64 GetPosition() const;
        void SetParent(const KaxSegment & aParentSegment) {ParentSegment = &aParentSegment;}
        void SetPreviousTimecode(uint64 aPreviousTimecode, int64 aTimecodeScale) {
            bPreviousTimecodeIsSet = true; 
            PreviousTimecode = aPreviousTimecode;
            SetGlobalTimecodeScale(aTimecodeScale);
        }

        /*!
            \note dirty hack to get the mandatory data back after reading
            \todo there should be a better way to get mandatory data
        */
        void InitTimecode(uint64 aTimecode, int64 aTimecodeScale) {
            SetGlobalTimecodeScale(aTimecodeScale);
            MinTimecode = MaxTimecode = PreviousTimecode = aTimecode * TimecodeScale;
            bFirstFrameInside = bPreviousTimecodeIsSet = true;
        }

        int16 GetBlockLocalTimecode(uint64 GlobalTimecode) const;

        uint64 GetBlockGlobalTimecode(int16 LocalTimecode);

        void SetGlobalTimecodeScale(uint64 aGlobalTimecodeScale) {
            TimecodeScale = aGlobalTimecodeScale;
            bTimecodeScaleIsSet = true;
        }
        uint64 GlobalTimecodeScale() const {
            assert(bTimecodeScaleIsSet); 
            return TimecodeScale;
        }

    protected:
        KaxBlockGroup    * currentNewBlock;
        const KaxSegment * ParentSegment;

        uint64 MinTimecode, MaxTimecode, PreviousTimecode;
        int64  TimecodeScale;

        bool bFirstFrameInside; // used to speed research
        bool bPreviousTimecodeIsSet;
        bool bTimecodeScaleIsSet;

        /*!
            \note method used internally
        */
        bool AddFrameInternal(const KaxTrackEntry & track, uint64 timecode, DataBuffer & buffer, KaxBlockGroup * & MyNewBlock, const KaxBlockGroup * PastBlock, const KaxBlockGroup * ForwBlock);

};

KaxCluster记录了一个最小时间戳和最大时间戳参数值,AddFrameInternal函数接口实现添加帧并更新时间戳,参数MyNewBlock为动态创建的GroupBlock对应引用。KaxCluster记录一个当前最新BlockGroup节点currentNewBlock
,通过currentNewBlock来新增Block类型节点,并返回一个指针引用给MyNewBlock。
那么接下来分析一下EbmlMaster中的UpdateSize函数

uint64 EbmlMaster::UpdateSize(bool bSaveDefault){
Size = 0;
    assert(CheckMandatory());
    size_t Index;
    for (Index = 0; Index < ElementList.size(); Index++) {
        if (!bSaveDefault && (ElementList[Index])->IsDefaultValue())
            continue;
        (ElementList[Index])->UpdateSize(bSaveDefault);

        uint64 SizeToAdd = (ElementList[Index])->ElementSize(bSaveDefault);
        Size += SizeToAdd;
    }
    return Size;
}

返回值Size的大小由所有子节点大小的总和,获取大小之前需要将元素大小进行更新调用接口UpdateSize(对比元素节点默认值)。而ElementSize大小的计算通过Size + EbmlId(*this).Length + CodedSizeLength()得到。元素的大小=value对应的长度Size +id对应的长度Length +表示Size用的字节数。

EbmlElement元素保存

uint32 EbmlElement::MakeRenderHead(IOCallback & output)
{
    binary FinalHead[4+8]; // Class D + 64 bits coded size
    unsigned int FinalHeadSize;
    FinalHeadSize = EbmlId(*this).Length;
    EbmlId(*this).Fill(FinalHead);
    int CodedSize = CodedSizeLength();
    // Set the EBML bits
    FinalHead[FinalHeadSize] = 1 << (8 - CodedSize);
    //后面位表达codesize的个数;
    uint64 TempSize = GetSize();
    int SizeMask = 0xFF;
    for (int i=1; i<CodedSize; i++) {
        FinalHead[FinalHeadSize + CodedSize - i] = TempSize & 0xFF; //256取余数;
        TempSize >>= 8;  //256取得数;
        SizeMask >>= 1; 
    }
//  SizeMask <<= 1;
    // first one use a OR with the "EBML size head"
    FinalHead[FinalHeadSize] |= TempSize & 0xFF & SizeMask;
    FinalHeadSize += CodedSize; 
    output.writeFully(FinalHead, FinalHeadSize);
    ElementPosition = output.getFilePointer() - FinalHeadSize;
    SizePosition = ElementPosition + EbmlId(*this).Length;
    return FinalHeadSize;
}

继续对block的分析,
Size = 1 + (1-8) + 4 + (4 + (4)) octets. So from 6 to 21 octets.
1字节可得到track对应的num;
2-3可得到block对应的时间戳;
4字节可分为高低8位进一步获取关键帧、lace、是否可见、可丢弃等参数

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐