??摘要:在拿到一個新的格式后,FFmpeg總是能夠足夠正確的判斷格式的內容并進行相應的處理。本文在描述FFmpeg如何進行格式檢測來確認正在處理的媒體格式類型,并進行相應的處理。
??關鍵字:FFmpeg,format,probe
??在調用FFmpeg的APIavformat_open_input
之后就能在對應的AVFormatContext
看到具體的媒體信息。為了獲取媒體信息,首先需要打開流文件,然后讀取HEAD檢測媒體格式。
//打開文件
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)return ret;if (s->iformat)return 0;
//探測文件格式
return av_probe_input_buffer2(s->pb, &s->iformat, filename,s, 0, s->format_probesize);
1 打開文件
流協議
??為了打開文件首先需要確認輸入的流協議。由于FFMpeg支持很多種協議,比如文件、RTMP、HTTP等,為了正確的讀取數據首先需要確認輸入的流是哪一種協議。對于輸入文件,FFMpeg中確認具體是那種流協議,需要使用哪種protcol打開文件讀取數據。
??FFmpeg中很多場景對于支持格式的檢測都是對當前支持的格式列表的遍歷來選擇最匹配的那一個作為最終匹配的結果。協議選擇也是一樣。如果外界調用API時指定了打開的協議的類型則不會進行檢測,沒有的花會按照下面的調用鏈進行匹配。
init_input->io_open_default->ffio_open_whitelist->ffurl_open_whitelist->ffurl_alloc->url_find_protocol
??Protcol的核心代碼如下,通過遍歷內部維護的全局靜態url表格匹配每一個url的name,最終選擇能夠匹配到的URLProtocol
作為最終的流協議。一旦確認了流協議后續就是調用對應的open和read函數指針打開和讀文件。
protocols = ffurl_get_protocols(NULL, NULL);
if (!protocols)return NULL;
for (i = 0; protocols[i]; i++) {const URLProtocol *up = protocols[i];if (!strcmp(proto_str, up->name)) {av_freep(&protocols);return up;}if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&!strcmp(proto_nested, up->name)) {av_freep(&protocols);return up;}
}
2 檢測媒體格式
確認iformat
??媒體格式探測其實就是確認使用哪種AVInputFormat
解析(該成員是AVFormatContext->iformat
字段)。如上面的代碼所示這里的流協議檢測是調用av_probe_input_format2
來實現的,而該函數也只是轉調了av_probe_input_format3
而已。
??av_probe_input_format3
檢測文件格式的方式就是遍歷FFmpeg內部的iformat靜態表格,選擇匹配度個最高的那個作為最終的格式。核心代碼如下。
while ((fmt1 = av_demuxer_iterate(&i))) {if (fmt1->flags & AVFMT_EXPERIMENTAL)continue;if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))continue;score = 0;if (ffifmt(fmt1)->read_probe) {score = ffifmt(fmt1)->read_probe(&lpd);if (score)av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {switch (nodat) {case NO_ID3:score = FFMAX(score, 1);break;case ID3_GREATER_PROBE:case ID3_ALMOST_GREATER_PROBE:score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);break;case ID3_GREATER_MAX_PROBE:score = FFMAX(score, AVPROBE_SCORE_EXTENSION);break;}}} else if (fmt1->extensions) {if (av_match_ext(lpd.filename, fmt1->extensions))score = AVPROBE_SCORE_EXTENSION;}if (av_match_name(lpd.mime_type, fmt1->mime_type)) {if (AVPROBE_SCORE_MIME > score) {av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);score = AVPROBE_SCORE_MIME;}}if (score > score_max) {score_max = score;fmt = fmt1;} else if (score == score_max)fmt = NULL;
}
??從上面的流程中可以看到FFmpeg媒體信息探測會給每個格式一個分數,將根據輸入流和對應的格式確認當前的分數,分數越高則匹配度越高,最后選擇分數最高的格式作為選中的格式。而分數計算主要參考read_probe
返回的分數值、后綴名和ID3元數據。
??從代碼中也能夠看到read_probe
給的分數相比比較優先,而輸入路徑的后綴名會作為兜底策略,最低50分。這樣是為什么對于一個不是媒體文件的格式個更改后綴名后卻有可能檢測成功的原因。那為什么需要保留后綴名檢測呢?原因是并不是所有格式都能夠通過文件內容判斷其格式,比如相機的raw格式往往更改后綴名后就無法正常解析,這是因為raw格式只包含數據不包含任何其他多余的信息。
read_probe
??FFmpeg內部read_probe
其實就是就是調用對應AVFormatInput
的read_probe
函數,每個格式都有自己的實現。一般情況下分為兩種,一種是文件開頭有tag直接表明當前文件類型的,比如GIF等;另一種是需要讀取部分文件內容來確認格式信息的,比如mov、mp4格式。
??第一種比較簡單直接讀取文件內容然后匹配tag即可,比如gif的格式檢測:
static int gif_probe(const AVProbeData *p){/* check magick */if (memcmp(p->buf, gif87a_sig, 6) && memcmp(p->buf, gif89a_sig, 6))return 0;/* width or height contains zero? */if (!AV_RL16(&p->buf[6]) || !AV_RL16(&p->buf[8]))return 0;return AVPROBE_SCORE_MAX;
}
??另一種需要度卻文件內容,比如mp4格式檢測需要讀取其中的box來確認部分文件信息。
static int mov_probe(const AVProbeData *p)
{int64_t offset;uint32_t tag;int score = 0;int moov_offset = -1;/* check file header */offset = 0;for (;;) {int64_t size;int minsize = 8;/* ignore invalid offset */if ((offset + 8ULL) > (unsigned int)p->buf_size)break;size = AV_RB32(p->buf + offset);if (size == 1 && offset + 16 <= (unsigned int)p->buf_size) {size = AV_RB64(p->buf+offset + 8);minsize = 16;} else if (size == 0) {size = p->buf_size - offset;}if (size < minsize) {offset += 4;continue;}tag = AV_RL32(p->buf + offset + 4);switch(tag) {/* check for obvious tags */case MKTAG('m','o','o','v'):moov_offset = offset + 4;case MKTAG('m','d','a','t'):case MKTAG('p','n','o','t'): /* detect movs with preview pics like ew.mov and april.mov */case MKTAG('u','d','t','a'): /* Packet Video PVAuthor adds this and a lot of more junk */case MKTAG('f','t','y','p'):if (tag == MKTAG('f','t','y','p') &&( AV_RL32(p->buf + offset + 8) == MKTAG('j','p','2',' ')|| AV_RL32(p->buf + offset + 8) == MKTAG('j','p','x',' ')|| AV_RL32(p->buf + offset + 8) == MKTAG('j','x','l',' '))) {score = FFMAX(score, 5);} else {score = AVPROBE_SCORE_MAX;}break;/* those are more common words, so rate then a bit less */case MKTAG('e','d','i','w'): /* xdcam files have reverted first tags */case MKTAG('w','i','d','e'):case MKTAG('f','r','e','e'):case MKTAG('j','u','n','k'):case MKTAG('p','i','c','t'):score = FFMAX(score, AVPROBE_SCORE_MAX - 5);break;case MKTAG(0x82,0x82,0x7f,0x7d):score = FFMAX(score, AVPROBE_SCORE_EXTENSION - 5);break;case MKTAG('s','k','i','p'):case MKTAG('u','u','i','d'):case MKTAG('p','r','f','l'):/* if we only find those cause probedata is too small at least rate them */score = FFMAX(score, AVPROBE_SCORE_EXTENSION);break;}if (size > INT64_MAX - offset)break;offset += size;}if (score > AVPROBE_SCORE_MAX - 50 && moov_offset != -1) {/* moov atom in the header - we should make sure that this is not a* MOV-packed MPEG-PS */offset = moov_offset;while (offset < (p->buf_size - 16)) { /* Sufficient space *//* We found an actual hdlr atom */if (AV_RL32(p->buf + offset ) == MKTAG('h','d','l','r') &&AV_RL32(p->buf + offset + 8) == MKTAG('m','h','l','r') &&AV_RL32(p->buf + offset + 12) == MKTAG('M','P','E','G')) {av_log(NULL, AV_LOG_WARNING, "Found media data tag MPEG indicating this is a MOV-packed MPEG-PS.\n");/* We found a media handler reference atom describing an* MPEG-PS-in-MOV, return a* low score to force expanding the probe window until* mpegps_probe finds what it needs */return 5;} else {/* Keep looking */offset += 2;}}}return score;
}