ffmpeg對mp3媒體數據的demux和部分decode流程 【ffmpeg-3.3.7】

背景

 在ffplay::read_thread執行的線程中,首先會通過avformat_open_input完成對媒體資源的數據讀取、格式探查、demuxer匹配等行爲:

  1. 針對媒體資源文件初始化對應的URLProtocol,比如ff_http_protocol,之後還會再生成一個相應的lower protocol,對應http的媒體資源就是ff_tcp_protocol。隨後,由URLProtocol完成與服務讀寫數據的行爲。
  2. 隨後,進行探查行爲。根據資源後綴名匹配對應的demuxer。例如,mp3資源對應ff_mp3_demuxer解封裝器,而它屬於AVInputFormat類型的實例。所以,在avformat_open_input的過程中,很重要的一步是生成AVInputFormat。
  3. 讀取id3v2信息。該信息只存在於mp3媒體資源中,用於封裝例如專輯album之類的信息
  4. 讀取頭部信息

  本文主要以mp3媒體資源爲例,探究ffmpeg是如何對mp3進行解封裝的。

mp3 : 一種音頻文件格式,由id3v2+數據部分+id3v1構成,其中數據採用mpeg協議進行壓縮
demux : 解封裝. 以ffmpeg的視角來看,就是從媒體文件中抽取出AVPacket的過程
mpeg協議 : 在解碼之前,mp3的數據部分採用mpeg協議進行壓縮,經過ffmpeg解碼纔會還原爲pcm原始音頻數據

mp3媒體資源的組成結構

在這裏插入圖片描述
普遍支持的格式是id3v2.3,id3v2.3一般由1個標籤頭+N*標籤幀構成。

函數調用流程圖

在這裏插入圖片描述

ff_id3v2_read_dict

avformat_open_input的調用流程中,自匹配完demuxer之後如果媒體資源對應的是mp3音頻則通過id3v2_read_internal開始讀取id3v2信息,否則在後續的read_header中讀取頭部信息。id3v2_read_internal函數如下所示:

// ID3v2_DEFAULT_MAGIC-> "ID3"
// max_search_size == 0
static void id3v2_read_internal(AVIOContext *pb, AVDictionary **metadata,
                                AVFormatContext *s, const char *magic,
                                ID3v2ExtraMeta **extra_meta, int64_t max_search_size)
{
    int len, ret;
    //ID3v2_HEADER_SIZE -> 10,標籤頭的大小
    uint8_t buf[ID3v2_HEADER_SIZE];
    int found_header;
    int64_t start, off;

    if (max_search_size && max_search_size < ID3v2_HEADER_SIZE)
        return;

    start = avio_tell(pb);
    do {
        /* save the current offset in case there's nothing to read/skip */
        off = avio_tell(pb)
        
        //讀取mp3文件的標籤頭, ID3v2_HEADER_SIZE -> 10
        ret = avio_read(pb, buf, ID3v2_HEADER_SIZE);
        //magic -> "ID3",mp3的ID3V2標籤頭要求必須是"ID3"開頭
        found_header = ff_id3v2_match(buf, magic);
        //magic 匹配
        if (found_header) {
            //標籤大小
          
            /* parse ID3v2 header */
            len = ((buf[6] & 0x7f) << 21) |
                  ((buf[7] & 0x7f) << 14) |
                  ((buf[8] & 0x7f) << 7) |
                   (buf[9] & 0x7f);
            //解析id3v2的標籤頭+標籤幀
            id3v2_parse(pb, metadata, s, len, buf[3], buf[5], extra_meta);
        } else {
            //如果讀取到的是數據部分,將指針移動到上一次幀結束的對方
            avio_seek(pb, off, SEEK_SET);
        }
    } while (found_header);//如果一直找到id3v2的header
    
    //設置鍵值對,把ff_id3v2_34_metadata_conv的kv賦值大奧metadata
    ff_metadata_conv(metadata, NULL, ff_id3v2_34_metadata_conv);
    ff_metadata_conv(metadata, NULL, id3v2_2_metadata_conv);
    ff_metadata_conv(metadata, NULL, ff_id3v2_4_metadata_conv);
    merge_date(metadata);
}
  1. 首先,讀取id3v2的標籤頭,標籤頭的大小爲10字節.起始必須爲"ID3".
  2. 隨後,獲取標籤頭的size信息,這個保存在標籤頭的高四字節中

id3v2的標籤頭結構
  char Header[3]; //必須爲“ID3”否則認爲標籤不存在
  char Ver; //版本號ID3V2.3 就記錄3
  char Revision; //副版本號此版本記錄爲0
  char Flag; //標誌字節,只使用高三位,其它位爲0
  char Size[4]; //標籤大小
};

id3v2_parse

id3v2_parse函數主要用於解析id3v2中的標籤頭和標籤幀。在前面的id3v2_read_internal函數調用已經得知了標籤頭+標籤幀的總大小。

static void id3v2_parse(AVIOContext *pb, AVDictionary **metadata,
                        AVFormatContext *s, int len, uint8_t version,
                        uint8_t flags, ID3v2ExtraMeta **extra_meta)
{
    int isv34, unsync;
    unsigned tlen;
    char tag[5];
    int64_t next, end = avio_tell(pb) + len;
    int taghdrlen;
    const char *reason = NULL;
    AVIOContext pb_local;
    AVIOContext *pbx;
    unsigned char *buffer = NULL;
    int buffer_size       = 0;
    const ID3v2EMFunc *extra_func = NULL;
    unsigned char *uncompressed_buffer = NULL;
    av_unused int uncompressed_buffer_size = 0;
    const char *comm_frame;

    av_log(s, AV_LOG_DEBUG, "id3v2 ver:%d flags:%02X len:%d\n", version, flags, len);

    switch (version) {
    case 2:
        if (flags & 0x40) {
            reason = "compression";
            goto error;
        }
        isv34     = 0;
        taghdrlen = 6;
        comm_frame = "COM";
        break;

    case 3:
    case 4:
        isv34     = 1;
        taghdrlen = 10;
        comm_frame = "COMM";
        break;

    default:
        reason = "version";
        goto error;
    }

    unsync = flags & 0x80;

    if (isv34 && flags & 0x40) { /* Extended header present, just skip over it */
        int extlen = get_size(pb, 4);
        if (version == 4)
            /* In v2.4 the length includes the length field we just read. */
            extlen -= 4;

        if (extlen < 0) {
            reason = "invalid extended header length";
            goto error;
        }
        avio_skip(pb, extlen);
        len -= extlen + 4;
        if (len < 0) {
            reason = "extended header too long.";
            goto error;
        }
    }

    while (len >= taghdrlen) {
        unsigned int tflags = 0;
        int tunsync         = 0;
        int tcomp           = 0;
        int tencr           = 0;
        unsigned long av_unused dlen;

        if (isv34) {
            if (avio_read(pb, tag, 4) < 4)
                break;
            tag[4] = 0;
            if (version == 3) {
                tlen = avio_rb32(pb);
            } else {
                /* some encoders incorrectly uses v3 sizes instead of syncsafe ones
                 * so check the next tag to see which one to use */
                tlen = avio_rb32(pb);
                if (tlen > 0x7f) {
                    if (tlen < len) {
                        int64_t cur = avio_tell(pb);

                        if (ffio_ensure_seekback(pb, 2 /* tflags */ + tlen + 4 /* next tag */))
                            break;

                        if (check_tag(pb, cur + 2 + size_to_syncsafe(tlen), 4) == 1)
                            tlen = size_to_syncsafe(tlen);
                        else if (check_tag(pb, cur + 2 + tlen, 4) != 1)
                            break;
                        avio_seek(pb, cur, SEEK_SET);
                    } else
                        tlen = size_to_syncsafe(tlen);
                }
            }
            tflags  = avio_rb16(pb);
            tunsync = tflags & ID3v2_FLAG_UNSYNCH;
        } else {
            if (avio_read(pb, tag, 3) < 3)
                break;
            tag[3] = 0;
            tlen   = avio_rb24(pb);
        }
        if (tlen > (1<<28))
            break;
        len -= taghdrlen + tlen;

        if (len < 0)
            break;

        next = avio_tell(pb) + tlen;

        if (!tlen) {
            if (tag[0])
                av_log(s, AV_LOG_DEBUG, "Invalid empty frame %s, skipping.\n",
                       tag);
            continue;
        }

        if (tflags & ID3v2_FLAG_DATALEN) {
            if (tlen < 4)
                break;
            dlen = avio_rb32(pb);
            tlen -= 4;
        } else
            dlen = tlen;

        tcomp = tflags & ID3v2_FLAG_COMPRESSION;
        tencr = tflags & ID3v2_FLAG_ENCRYPTION;

        /* skip encrypted tags and, if no zlib, compressed tags */
        if (tencr || (!CONFIG_ZLIB && tcomp)) {
            const char *type;
            if (!tcomp)
                type = "encrypted";
            else if (!tencr)
                type = "compressed";
            else
                type = "encrypted and compressed";

            av_log(s, AV_LOG_WARNING, "Skipping %s ID3v2 frame %s.\n", type, tag);
            avio_skip(pb, tlen);
        /* check for text tag or supported special meta tag */
        } else if (tag[0] == 'T' ||
                   !memcmp(tag, "USLT", 4) ||
                   !strcmp(tag, comm_frame) ||
                   (extra_meta &&
                    (extra_func = get_extra_meta_func(tag, isv34)))) {
            pbx = pb;

            if (unsync || tunsync || tcomp) {
                av_fast_malloc(&buffer, &buffer_size, tlen);
                if (!buffer) {
                    av_log(s, AV_LOG_ERROR, "Failed to alloc %d bytes\n", tlen);
                    goto seek;
                }
            }
            if (unsync || tunsync) {
                int64_t end = avio_tell(pb) + tlen;
                uint8_t *b;

                b = buffer;
                while (avio_tell(pb) < end && b - buffer < tlen && !pb->eof_reached) {
                    *b++ = avio_r8(pb);
                    if (*(b - 1) == 0xff && avio_tell(pb) < end - 1 &&
                        b - buffer < tlen &&
                        !pb->eof_reached ) {
                        uint8_t val = avio_r8(pb);
                        *b++ = val ? val : avio_r8(pb);
                    }
                }
                ffio_init_context(&pb_local, buffer, b - buffer, 0, NULL, NULL, NULL,
                                  NULL);
                tlen = b - buffer;
                pbx  = &pb_local; // read from sync buffer
            }

            if (tag[0] == 'T')
                /* parse text tag */
                read_ttag(s, pbx, tlen, metadata, tag);
            else if (!memcmp(tag, "USLT", 4))
                read_uslt(s, pbx, tlen, metadata);
            else if (!strcmp(tag, comm_frame))
                read_comment(s, pbx, tlen, metadata);
            else
                /* parse special meta tag */
                extra_func->read(s, pbx, tlen, tag, extra_meta, isv34);
        } else if (!tag[0]) {
            if (tag[1])
                av_log(s, AV_LOG_WARNING, "invalid frame id, assuming padding\n");
            avio_skip(pb, tlen);
            break;
        }
        /* Skip to end of tag */
seek:
        avio_seek(pb, next, SEEK_SET);
    }

    /* Footer preset, always 10 bytes, skip over it */
    if (version == 4 && flags & 0x10)
        end += 10;

error:
    if (reason)
        av_log(s, AV_LOG_INFO, "ID3v2.%d tag skipped, cannot handle %s\n",
               version, reason);
    avio_seek(pb, end, SEEK_SET);
    av_free(buffer);
    av_free(uncompressed_buffer);
    return;
}
  1. 首先,就id3v2的version字段進行判斷。這樣做的目的是區別是否有帶擴展頭,當version爲3或者4並且flags & 0x40 爲真時,帶有擴展頭。ffmpeg的做法是跳過擴展頭。
  2. 隨後,循環讀取標籤幀,循環結束的條件是while (len >= taghdrlen).每一次讀取都會使len減少當前所遍歷到的標籤幀大小。
  3. 標籤幀由10字節的枕頭和至少一字節的內容構成。ffmpeg讀取四字節的標識時,存放在了tag變量。如果tag的第一個字節是【T】,則代表tag是文本類型,隨後調用read_ttag進行解析。

id3v2的標籤幀結構
    char ID[4]; /標識,說明其內容,例如作者/標題等/
    char Size[4]; /幀內容的大小,不包括幀頭,不得小於1/
    char Flags[2]; /標誌幀,只定義了6 位/

read_ttag

parse a text tag.代碼如下:

static void read_ttag(AVFormatContext *s, AVIOContext *pb, int taglen,
                     AVDictionary **metadata, const char *key)
{
   uint8_t *dst;
   int encoding, dict_flags = AV_DICT_DONT_OVERWRITE | AV_DICT_DONT_STRDUP_VAL;
   unsigned genre;

   if (taglen < 1)
       return;

   encoding = avio_r8(pb);
   taglen--; /* account for encoding type byte */

   if (decode_str(s, pb, encoding, &dst, &taglen) < 0) {
       av_log(s, AV_LOG_ERROR, "Error reading frame %s, skipped\n", key);
       return;
   }

   if (!(strcmp(key, "TCON") && strcmp(key, "TCO"))                         &&
       (sscanf(dst, "(%d)", &genre) == 1 || sscanf(dst, "%d", &genre) == 1) &&
       genre <= ID3v1_GENRE_MAX) {
       av_freep(&dst);
       dst = av_strdup(ff_id3v1_genre_str[genre]);
   } else if (!(strcmp(key, "TXXX") && strcmp(key, "TXX"))) {
       /* dst now contains the key, need to get value */
       key = dst;
       if (decode_str(s, pb, encoding, &dst, &taglen) < 0) {
           av_log(s, AV_LOG_ERROR, "Error reading frame %s, skipped\n", key);
           av_freep(&key);
           return;
       }
       dict_flags |= AV_DICT_DONT_STRDUP_KEY;
   } else if (!*dst)
       av_freep(&dst);

   if (dst)
       av_dict_set(metadata, key, dst, dict_flags);
}
  1. 首先會讀取一個字節,如果該字節代表編碼格式,則繼續讀取後續內容直至到達tlen大小
  2. 如果該字節爲【TCON】,則代表類型直接用字符串表示。這時ffmpeg會到類型表中去找到對應的映射,例如Blues、Classic Rock、Country這樣的類型。
  3. 如果該字節對應【TXXX】,則是用戶自定義數據。

mp3數據部分的格式解析

mp3的數據並不是由裸的pcm流構成,而是採用mpeg協的壓縮數據。數據部分也由多個幀構成,且每個幀都有對應的格式。
在這裏插入圖片描述
avformat_open_input函數的末尾,會調用iformat->read_header函數進行數據幀幀頭的讀取。而對應到mp3媒體資源,則是調用mp3_read_header

mp3_read_header

ffmpeg的角度來說,讀取第一個數據幀幀頭的行爲,在獲得mp3媒體資源總時長得一些信息至關重要,特別是對於CBR(固定位率)格式的壓縮數據。因爲這些數據幀的位率都是一樣的,大小也是一樣的,因此可以通過每個數據幀的大小、位率求出每幀的時長,從而求出mp3媒體資源的總時長等其它信息。所以ffmpeg在完成demuxer匹配之後,就立馬進行了首個數據幀幀頭的解析。

static int mp3_read_header(AVFormatContext *s)
{
    MP3DecContext *mp3 = s->priv_data;
    AVStream *st;
    int64_t off;
    int ret;
    int i;
    
    //事先讀取的id3v2信息
    s->metadata = s->internal->id3v2_meta;
    s->internal->id3v2_meta = NULL;

    //todo: 
    st = avformat_new_stream(s, NULL);
    if (!st)
        return AVERROR(ENOMEM);

    st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
    st->codecpar->codec_id = AV_CODEC_ID_MP3;
    st->need_parsing = AVSTREAM_PARSE_FULL_RAW;
    st->start_time = 0;

    // lcm of all mp3 sample rates
    avpriv_set_pts_info(st, 64, 1, 14112000);
    
    //s->pb: AVIOContext
    s->pb->maxsize = -1;
    off = avio_tell(s->pb);

    if (!av_dict_get(s->metadata, "", NULL, AV_DICT_IGNORE_SUFFIX))
        ff_id3v1_read(s);
    
    //fileszie -> 文件大小,可以從例如content-length中獲得
    if(s->pb->seekable & AVIO_SEEKABLE_NORMAL)
        mp3->filesize = avio_size(s->pb);

    //vbr格式解析
    if (mp3_parse_vbr_tags(s, st, off) < 0)
        avio_seek(s->pb, off, SEEK_SET);

    ret = ff_replaygain_export(st, s->metadata);
    if (ret < 0)
        return ret;

    off = avio_tell(s->pb);
    
    //解析mp3的數據部分
    for (i = 0; i < 64 * 1024; i++) {
        uint32_t header, header2;
        int frame_size;
        if (!(i&1023))
            ffio_ensure_seekback(s->pb, i + 1024 + 4);
        
        //讀取數據幀的枕頭, frame_size -> 幀長度,包含幀頭的四個字節
        frame_size = check(s->pb, off + i, &header);
        
        if (frame_size > 0) {
            //重新seek到未讀取數據幀的位置
            ret = avio_seek(s->pb, off, SEEK_SET);

            ffio_ensure_seekback(s->pb, i + 1024 + frame_size + 4);
            //去讀下一個數據幀的frame sizee
            ret = check(s->pb, off + i + frame_size, &header2);
            if (ret >= 0 &&
                (header & SAME_HEADER_MASK) == (header2 & SAME_HEADER_MASK))  //我也不知道是什麼操作
            {
                av_log(s, i > 0 ? AV_LOG_INFO : AV_LOG_VERBOSE, "Skipping %d bytes of junk at %"PRId64".\n", i, off);
                ret = avio_seek(s->pb, off + i, SEEK_SET);
                if (ret < 0)
                    return ret;
                break;
            } else if (ret == CHECK_SEEK_FAILED) {
                av_log(s, AV_LOG_ERROR, "Invalid frame size (%d): Could not seek to %"PRId64".\n", frame_size, off + i + frame_size);
                return AVERROR(EINVAL);
            }
        } else if (frame_size == CHECK_SEEK_FAILED) {
            av_log(s, AV_LOG_ERROR, "Failed to read frame size: Could not seek to %"PRId64".\n", (int64_t) (i + 1024 + frame_size + 4));
            return AVERROR(EINVAL);
        }
        ret = avio_seek(s->pb, off, SEEK_SET);
        if (ret < 0)
            return ret;
    }

    // the seek index is relative to the end of the xing vbr headers
    for (i = 0; i < st->nb_index_entries; i++)
        st->index_entries[i].pos += avio_tell(s->pb);

    /* the parameters will be extracted from the compressed bitstream */
    return 0;
}
  1. mp3_read_header函數首先調用check進行數據幀幀頭的解析,預讀四個字節,並調用avpriv_mpegaudio_decode_header獲得採樣數、採樣頻率、幀大小等信息。
  2. 由於mp3的壓縮數據可以按照mpeg-1、mpeg-2、mpeg-2.5來壓縮,因此也需要從幀頭中進行判斷,以便後續解碼利用。
  3. 採樣頻率由採用的mpeg協議版本和layer共同決定。
  4. 幀的大小的計算公式:a).layer1 -> ((每幀採樣數/8*比特率)/採樣頻率)+填充*4 b).layer2、3 -> ((每幀採樣數/8*比特率)/採樣頻率)+填充

在這裏插入圖片描述

發佈了5 篇原創文章 · 獲贊 33 · 訪問量 4433
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章