問(wèn)題
- 主流程上的區(qū)別
- 緩沖區(qū)的設(shè)計(jì)
- 內(nèi)存管理的邏輯
- 音視頻播放方式
- 音視頻同步
- seek的問(wèn)題:緩沖區(qū)flush、播放時(shí)間顯示、k幀間距大時(shí)定位不準(zhǔn)問(wèn)題...
- stop時(shí)怎么釋放資源,是否切換到副線(xiàn)程?
- 網(wǎng)絡(luò)不好時(shí)的處理,如獲取frame速度慢于消耗速度時(shí),如果不暫停,會(huì)一致卡頓,是否會(huì)主動(dòng)暫停?
- VTB的解碼和ffmpeg的解碼怎么統(tǒng)一的?架構(gòu)上怎么設(shè)計(jì)的?
數(shù)據(jù)流向
主流程更詳細(xì)看ijkPlayer主流程分析
音頻
av_read_frame
packet_queue_put
-
audio_thread
+decoder_decode_frame
+packet_queue_get_or_buffering
-
frame_queue_peek_writable
+frame_queue_push
-
audio_decode_frame
+frame_queue_peek_readable
,數(shù)據(jù)到is->audio_buf
-
sdl_audio_callback
,數(shù)據(jù)導(dǎo)入到參數(shù)stream里。這個(gè)函數(shù)是上層的音頻播放庫(kù)的buffer填充函數(shù),如iOS里使用audioQueue,回調(diào)函數(shù)IJKSDLAudioQueueOuptutCallback
調(diào)用到這里,然后把數(shù)據(jù)傳入到audioQueue.
視頻
讀取packet部分一樣
-
video_thread
,然后ffpipenode_run_sync
里硬解碼定位到videotoolbox_video_thread
,然后ffp_packet_queue_get_or_buffering
讀取。 -
VTDecoderCallback
解碼完成回調(diào)里,SortQueuePush(ctx, newFrame);
把解碼后的pixelBuffer裝入到一個(gè)有序的隊(duì)列里。 -
GetVTBPicture
從有序隊(duì)列里把frame的封裝拿出來(lái),也就是這個(gè)有序隊(duì)列只是一個(gè)臨時(shí)的用來(lái)排序的工具罷了,這個(gè)思想是可以吸收的;queue_picture
里,把解碼的frame放入frame緩沖區(qū) - 顯示
video_refresh
+video_image_display2
+[IJKSDLGLView display:]
- 最后的紋理生成放在了render里,對(duì)vtb的pixelBuffer,在
yuv420sp_vtb_uploadTexture
。使用render這個(gè)角色,渲染的部分都抽象出來(lái)了。shader在IJK_GLES2_getFragmentShader_yuv420sp
結(jié)論:主流程上沒(méi)有大的差別。
緩沖區(qū)的設(shè)計(jì)
packetQueue:
- 數(shù)據(jù)機(jī)構(gòu)設(shè)計(jì)
packetQueue采用兩條鏈表,一個(gè)是保存數(shù)據(jù)的鏈表,一個(gè)是復(fù)用節(jié)點(diǎn)鏈表,保存沒(méi)有數(shù)據(jù)的那些節(jié)點(diǎn)。數(shù)據(jù)鏈表從first_pkt
到last_pkt
,插入數(shù)據(jù)接到last_pkt
的后面,取數(shù)據(jù)從first_pkt
拿。復(fù)用鏈表的開(kāi)頭是recycle_pkt,取完數(shù)據(jù)后的空節(jié)點(diǎn),放到空鏈表recycle_pkt的頭部,然后這個(gè)空節(jié)點(diǎn)成為新的recycle_pkt。存數(shù)據(jù)時(shí),也從recycle_pkt復(fù)用一個(gè)節(jié)點(diǎn)。
鏈表的節(jié)點(diǎn)像是包裝盒,裝載數(shù)據(jù)的時(shí)候放到數(shù)據(jù)鏈表,數(shù)據(jù)取出后回歸到復(fù)用鏈表。
- 進(jìn)出的阻塞控制
取數(shù)據(jù)的時(shí)候可能沒(méi)有,那么就有幾種處理:直接返回、阻塞等待。它這里的處理是阻塞等待,并且會(huì)把視頻播放暫停。所以這個(gè)回答了問(wèn)題8,外面看到的效果就是:網(wǎng)絡(luò)卡的時(shí)候,會(huì)停止播放然后流暢的播放一會(huì),然后又繼續(xù)卡頓,播放和卡頓是清晰分隔的。
進(jìn)數(shù)據(jù)的時(shí)候并沒(méi)有做阻塞控制,為什么數(shù)據(jù)不會(huì)無(wú)限擴(kuò)大?
是有阻塞的,但阻塞不是在packetQueue里面,而是在readFrame函數(shù)里:
if (ffp->infinite_buffer<1 && !is->seek_req &&
(is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size
|| ( stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq, MIN_FRAMES)
&& stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq, MIN_FRAMES)
&& stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq, MIN_FRAMES)))) {
if (!is->eof) {
ffp_toggle_buffering(ffp, 0);
}
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
簡(jiǎn)化來(lái)看就是:
-
infinite_buffer
不是無(wú)限的緩沖 -
is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size
,使用數(shù)據(jù)大小做限制 -
stream_has_enough_packets
使用數(shù)據(jù)的個(gè)數(shù)做限制
因?yàn)閭€(gè)數(shù)設(shè)置到了50000,一般達(dá)不到,而是數(shù)據(jù)大小做了限制,在15M左右。
這里精髓的地方有兩點(diǎn):
- 采用了數(shù)據(jù)大小做限制,因?yàn)閷?duì)于不同的視頻,分辨率的問(wèn)題會(huì)導(dǎo)致同一個(gè)packet差距巨大,而我們實(shí)際關(guān)心的其實(shí)就是內(nèi)存問(wèn)題。
- 暫停10ms,而不是無(wú)限暫停等待條件鎖的signal。從設(shè)計(jì)上說(shuō)會(huì)更簡(jiǎn)單,而且可以避免頻繁的wait+signal。這個(gè)問(wèn)題還需仔細(xì)思考,但直覺(jué)上覺(jué)得這樣的操作非常好。
frameQueue:
數(shù)據(jù)使用一個(gè)簡(jiǎn)單的數(shù)組保存,可以把這個(gè)數(shù)據(jù)看成是環(huán)形的,然后也是其中一段有數(shù)據(jù),另一段沒(méi)有數(shù)據(jù)。rindex
表示數(shù)據(jù)開(kāi)頭的index,也是讀取數(shù)據(jù)的index,即read index,windex
表示空數(shù)據(jù)開(kāi)頭的index,是寫(xiě)入數(shù)據(jù)的index,即write index。
也是不斷循環(huán)重用,然后size表示當(dāng)前數(shù)據(jù)大小,max_size表示最大的槽位數(shù),寫(xiě)入的時(shí)候如果size滿(mǎn)了,就會(huì)阻塞等待;讀取的時(shí)候size為空,也會(huì)阻塞等待。
有個(gè)奇怪的東西是rindex_shown
,讀取的時(shí)候不是讀的rindex
位置的數(shù)據(jù),而是rindex+rindex_shown
,需要結(jié)合后面的使用情況再看這個(gè)的作用。后面再看。
還有serial沒(méi)有明白什么意思
結(jié)論:緩沖區(qū)的設(shè)計(jì)和我的完全不同,但都使用重用的概念,而且節(jié)點(diǎn)都是包裝盒,數(shù)據(jù)包裝在節(jié)點(diǎn)里面。性能上不好比較,但我的設(shè)計(jì)更完善,frame和packet使用統(tǒng)一設(shè)計(jì),還包含了排序功能。
內(nèi)存管理
- packet的管理
- 從
av_read_frame
得到初始值,這個(gè)時(shí)候引用數(shù)為1,packet是使用一個(gè)臨時(shí)變量去接的,也就是棧內(nèi)存。 - 然后加入隊(duì)列時(shí),
pkt1->pkt = *pkt;
使用值拷貝的方式把packet存入,這樣緩沖區(qū)的數(shù)據(jù)和外面的臨時(shí)變量就分離了。 -
packet_queue_get_or_buffering
把packet取出來(lái),同樣使用值復(fù)制的方式。 - 最后使用
av_packet_unref
把packet關(guān)聯(lián)的buf釋放掉,而臨時(shí)變量的packet可以繼續(xù)使用。
需要注意的一點(diǎn)是:avcodec_send_packet
返回EAGAIN
表示當(dāng)前還無(wú)法接受新的packet,還有frame沒(méi)有取出來(lái),所以有了:
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
把這個(gè)packet存到d->pkt,在下一個(gè)循環(huán)里,先取frame,再把packet接回來(lái),接著上面的操作:
if (d->packet_pending) {
av_packet_move_ref(&pkt, &d->pkt);
d->packet_pending = 0;
}
可能是存在B幀的時(shí)候會(huì)這樣,因?yàn)锽幀需要依賴(lài)后面的幀,所以不會(huì)解碼出來(lái),等到后面的幀傳入后,就會(huì)有多個(gè)幀需要讀取。這時(shí)解碼器應(yīng)該就不接受新的packet。但ijkplayer這里的代碼似乎不會(huì)出現(xiàn)這樣的情況,因?yàn)樽x取frame不是一次一個(gè),而是一次性讀到報(bào)EAGAIN
錯(cuò)誤未知。待考察。
另,av_packet_move_ref
這個(gè)函數(shù)就是完全的只復(fù)制,source的值完全的搬到destination,并且把source重置掉。其實(shí)就是搬了個(gè)位置,buf的引用數(shù)不改變。
- 視頻frame的內(nèi)存管理
- 在
ffplay_video_thread
里,frame是一個(gè)對(duì)內(nèi)存,使用get_video_frame
從解碼器讀取到frame。這時(shí)frame的引用為1 - 過(guò)程中出錯(cuò),使用
av_frame_unref
釋放frame的buf的內(nèi)存,但frame本身還可以繼續(xù)使用。不出錯(cuò),也會(huì)調(diào)用av_frame_unref
,這樣保證每個(gè)讀取的frame都會(huì)unref,這個(gè)unref跟初始化是對(duì)應(yīng)的。使用引用指數(shù)來(lái)管理內(nèi)存,重要的原則就是一一對(duì)應(yīng)。
因?yàn)檫@里只是拿到frame,然后存入緩沖區(qū),還沒(méi)有到使用的時(shí)候,如果buf被釋放了,那么到播放的時(shí)候,數(shù)據(jù)就丟失了,所以是怎么處理的呢?
存入緩沖區(qū)在queue_picture
里,再到SDL_VoutFillFrameYUVOverlay
,這個(gè)函數(shù)會(huì)到上層,根據(jù)解碼器不同做不同處理,以ijksdl_vout_overlay_ffmpeg.c
的func_fill_frame
為例。
有兩種處理:
- 一種是overlay和frame共享內(nèi)存,就顯示的直接使用frame的內(nèi)存,格式是YUV420p的就是這樣,因?yàn)镺penGL可以直接顯示這種顏色空間的圖像。這種就只需要對(duì)frame加一個(gè)引用,保證不會(huì)被釋放掉就好了。關(guān)鍵就是這句:
av_frame_ref(opaque->linked_frame, frame);
- 另一種是不共享,因?yàn)橐D(zhuǎn)格式,另建一個(gè)frame,即這里的
opaque->managed_frame
,然后轉(zhuǎn)格式。數(shù)據(jù)到了新地方,原frame也就沒(méi)用了。不做ref操作,它自然的就會(huì)釋放了。
- 音頻frame的處理
在audio_thread
里,不斷通過(guò)decoder_decode_frame
獲取到新的frame。和視頻一樣,這里的frame也是對(duì)內(nèi)存,讀到解碼后的frame后,引用為1。音頻的格式轉(zhuǎn)換放在了播放階段,所以這里只是單純的把frame存入:av_frame_move_ref(af->frame, frame);
。做了一個(gè)復(fù)制,把讀取的frame搬運(yùn)到緩沖區(qū)里了。
在frame的緩沖區(qū)取數(shù)據(jù)的時(shí)候,frame_queue_next
里包含了av_frame_unref
把frame釋放。這個(gè)視頻也是一樣。
有一個(gè)問(wèn)題是,上層播放器的讀取音頻數(shù)據(jù)的時(shí)候,frame必須是活的,因?yàn)槿绻纛l不轉(zhuǎn)換格式,是直接讀取了frame里的數(shù)據(jù)。所以也就是需要在填充播放器數(shù)據(jù)結(jié)束后,才可以釋放frame。
unref是在frame_queue_next
,而這個(gè)函數(shù)是在下一次讀取frame的時(shí)候才發(fā)生,下一次讀取frame又是在當(dāng)前的數(shù)據(jù)讀完后,所以讀完了數(shù)據(jù)后,才會(huì)釋放frame,這樣就沒(méi)錯(cuò)了。
//數(shù)據(jù)讀完才會(huì)去拉取下一個(gè)frame
if (is->audio_buf_index >= is->audio_buf_size) {
audio_size = audio_decode_frame(ffp);
音視頻的播放方式
音頻播放使用AudioQueue:
- 構(gòu)建AudioQueue:
AudioQueueNewOutput
- 開(kāi)始
AudioQueueStart
,暫停AudioQueuePause
,結(jié)束AudioQueueStop
- 在回調(diào)函數(shù)
IJKSDLAudioQueueOuptutCallback
里,調(diào)用下層的填充函數(shù)來(lái)填充AudioQueue的buffer。 - 使用
AudioQueueEnqueueBuffer
把裝配完的AudioQueue Buffer入隊(duì),進(jìn)入播放。
上面這些都是AudioQueue的標(biāo)準(zhǔn)操作,特別的是構(gòu)建AudioStreamBasicDescription
的時(shí)候,也就是指定音頻播放的格式。格式是由音頻源的格式?jīng)Q定的,在IJKSDLGetAudioStreamBasicDescriptionFromSpec
里看,除了格式固定為pcm之外,其他的都是從底層給的格式復(fù)制過(guò)來(lái)。這樣就有了很大的自由,音頻源只需要解碼成pcm就可以了。
而底層的格式是在audio_open
里決定的,邏輯是:
- 根據(jù)源文件,構(gòu)建一個(gè)期望的格式
wanted_spec
,然后把這個(gè)期望的格式提供給上層,最后把上層的實(shí)際格式拿到作為結(jié)果返回。一個(gè)類(lèi)似溝通的操作,這種思維很值得借鑒 - 如果上傳不接受這種格式,返回錯(cuò)誤,底層修改channel數(shù)、采樣率然后再繼續(xù)溝通。
- 但是樣本格式是固定為s16,即signed integer 16,有符號(hào)的int類(lèi)型,位深為16比特的格式。位深指每個(gè)樣本存儲(chǔ)的內(nèi)存大小,16個(gè)比特,加上有符號(hào),所以范圍是[-2^15, 215-1],215為32768,變化性足夠了。
因?yàn)槎际莗cm,是不壓縮的音頻,所以決定性的因素就只有:采樣率、通道數(shù)和樣本格式。樣本格式固定s16,和上層溝通就是決定采樣率和通道數(shù)。
這里是一個(gè)很好的分層架構(gòu)的例子,底層通用,上層根據(jù)平臺(tái)各有不同。
視頻的播放:
播放都是使用OpenGL ES,使用IJKSDLGLView
,重寫(xiě)了layerClass
,把layer類(lèi)型修改為CAEAGLLayer
可以顯示OpenGL ES的渲染內(nèi)容。所有類(lèi)型的畫(huà)面都使用這個(gè)顯示,有區(qū)別的地方都抽象到Render
這個(gè)角色里了,相關(guān)的方法有:
-
setupRenderer
構(gòu)建一個(gè)render -
IJK_GLES2_Renderer_renderOverlay
繪制overlay。
render的構(gòu)建包括:
- 使用不同的fragmnt shader和共通的vertex shader構(gòu)建program
- 提供mvp矩陣
- 設(shè)置頂點(diǎn)和紋理坐標(biāo)數(shù)據(jù)
render的繪制包括:
-
func_uploadTexture
定位到不同的render,執(zhí)行不同的紋理上傳操作 - 繪制圖形使用
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
,使用了圖元GL_TRIANGLE_STRIP而不是GL_TRIANGLE,可以節(jié)省頂點(diǎn)。
提供紋理的方法也是重點(diǎn),區(qū)別在于顏色空間以及元素的排列方式:
- rgb類(lèi)型的提供了3種:565、888和8888。rgb類(lèi)型的元素都是混合在一起的,也就是只有一個(gè)層(plane),565指是rgb3個(gè)元素分別占用的比特位數(shù),同理888,8888是另外包含了alpha元素。所以每個(gè)像素565占2個(gè)字節(jié),888占3個(gè)字節(jié),8888占4個(gè)字節(jié)。
glTexImage2D(GL_TEXTURE_2D,
0,
GL_RGBA,
widths[plane],
heights[plane],
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
pixels[plane]);
構(gòu)建紋理的時(shí)候區(qū)別就在format跟type參數(shù)上。
- yuv420p的,這種指的是最常用的y、u、v3個(gè)元素全部開(kāi),分3層,然后數(shù)量比是4:1:1,所以u(píng) v的紋理大小高和寬都是y紋理的一半。然后因?yàn)槊總€(gè)分量各自一個(gè)紋理,所以每個(gè)紋理都是單通道的,使用的format為
GL_LUMINANCE
- yuv420sp的,這種yuv的比例也是4:1:1,區(qū)別在于u v不是分開(kāi)兩層,而是混合在同一層里,分層是uuuuvvvv,混合是uvuvuvuv。所以構(gòu)建兩個(gè)紋理,y的紋理不變,uv的紋理使用雙通道的格式
GL_RG_EXT
,大小也是y的1/4(高寬都為1/2)。這種在fragment shader里取值的時(shí)候會(huì)有區(qū)別:
//3層的
yuv.y = (texture2D(us2_SamplerY, vv2_Texcoord).r - 0.5);
yuv.z = (texture2D(us2_SamplerZ, vv2_Texcoord).r - 0.5);
//雙層的
yuv.yz = (texture2D(us2_SamplerY, vv2_Texcoord).rg - vec2(0.5, 0.5));
uv在同一個(gè)紋理里,texture2D直接取了rg兩個(gè)分量。
- yuv444p的不是很懂,看fragment shader貌似每個(gè)像素有兩個(gè)版本的yuv,然后做了一個(gè)插值。
- 最后是yuv420p_vtb,這個(gè)是VideoToolBox硬解出來(lái)的數(shù)據(jù)的顯示,因?yàn)閿?shù)據(jù)存儲(chǔ)在CVPixelBuffer里,所以直接使用了iOS系統(tǒng)的紋理構(gòu)建方法。
ijkplayer里的的OpenGL ES是2.0版本,如果使用3.0版本,雙通道可以使用GL_LUMINANCE_ALPHA
。
音視頻同步
首先看音頻,音頻并沒(méi)有做阻塞控制,上層的的播放器要需要數(shù)據(jù)都會(huì)填充,沒(méi)有看到時(shí)間不到不做填充的操作。所以應(yīng)該是默認(rèn)了音頻鐘做主控制,所以音頻沒(méi)做處理。
1. 視頻顯示時(shí)的時(shí)間控制
視頻的控制在video_refresh
里,播放函數(shù)是video_display2
,進(jìn)入這里代表時(shí)間到了、該播了,這是一個(gè)檢測(cè)點(diǎn)。
有幾個(gè)參數(shù)需要了解:
-
is->frame_timer
,這個(gè)時(shí)間代表上一幀播放的時(shí)間 -
delay
表示這一幀到下一幀的時(shí)間差
if (isnan(is->frame_timer) || time < is->frame_timer){
is->frame_timer = time;
}
上一幀的播放時(shí)間在當(dāng)前時(shí)間后面,說(shuō)明數(shù)據(jù)錯(cuò)誤,調(diào)整到當(dāng)期時(shí)間
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
is->frame_timer + delay
就表示當(dāng)前幀播放的時(shí)間,這個(gè)時(shí)間晚于當(dāng)前時(shí)間,就表示還沒(méi)到播放的時(shí)候。
這個(gè)有個(gè)坑:goto display
并不是去播放了,因?yàn)閐isplay代碼塊里還有一個(gè)判斷,判斷里有個(gè)is->force_refresh
。這個(gè)值默認(rèn)是false,所以直接跳去display,實(shí)際的意義是啥也不干,結(jié)束這次判斷。
反之,如果播放時(shí)間早于當(dāng)前時(shí)間,那就要馬上播放了。所以更新上一幀的播放時(shí)間:is->frame_timer += delay;
。
然后一直到后面,有個(gè)is->force_refresh = 1;
,這時(shí)才是真的播放。
從上面兩段就可以看出基本的流程了:
一開(kāi)始當(dāng)前幀播放時(shí)間沒(méi)到,goto display等待下次循環(huán),循環(huán)多次,時(shí)間不段后移,終于播放時(shí)間到了,播放當(dāng)前幀,frame_timer更新為當(dāng)前幀的時(shí)間。然后又重復(fù)上面的過(guò)程,去播放下一幀。
然后有個(gè)問(wèn)題是:為什么frame_timer的更新是加上delay,而不是直接等于當(dāng)前時(shí)間?
如果直接等于當(dāng)前時(shí)間,因?yàn)?code>time>= frame_timer+delay,那么frame_timer是相對(duì)更大了一些,那么在計(jì)算下一幀時(shí)間,也就是frame_timer+delay的時(shí)候,也就會(huì)大一點(diǎn)。而且每一幀都會(huì)是這個(gè)情況,最后每一幀都會(huì)大那么一點(diǎn),整體而言可能會(huì)有有比較大的差別。
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX){
is->frame_timer = time;
}
在frame_timer比較落后的時(shí)候,直接提到當(dāng)前time上,就可以直接把狀態(tài)修正,之后的播放又會(huì)走上正軌。
2. 同步鐘以及鐘時(shí)間的修正
同步鐘的概念: 音頻或者視頻,如果把內(nèi)容正確的完整的播放,某個(gè)內(nèi)容和一個(gè)時(shí)間是一一對(duì)應(yīng)的,當(dāng)前的音頻或者視頻播放到哪個(gè)位置,它就有一個(gè)時(shí)間來(lái)表示,這個(gè)時(shí)間就是同步鐘的時(shí)間。所以音頻鐘的時(shí)間表示音頻播放到哪個(gè)位置,視頻鐘表示播放到哪個(gè)位置。
因?yàn)橐纛l和視頻是分開(kāi)表現(xiàn)的,就可能會(huì)出現(xiàn)音頻和視頻的進(jìn)度不一致,在同步鐘上就表現(xiàn)為兩個(gè)同步鐘的值不同,如果讓兩者統(tǒng)一,就是音視頻同步的問(wèn)題。
因?yàn)橛辛送界姷母拍睿粢曨l內(nèi)容上的同步就可以簡(jiǎn)化為更準(zhǔn)確的:音頻鐘和視頻鐘時(shí)間相同。
這時(shí)會(huì)有一個(gè)同步鐘作為主鐘,也就是其他的同步鐘根據(jù)這個(gè)主鐘來(lái)調(diào)整自己的時(shí)間。滿(mǎn)了就調(diào)快、快了就調(diào)慢。
compute_target_delay
里的邏輯就是這樣,diff = get_clock(&is->vidclk) - get_master_clock(is);
這個(gè)是視頻鐘和主鐘的差距:
//視頻落后超過(guò)臨界值,縮短下一幀時(shí)間
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
//視頻超前,且超過(guò)臨界值,延長(zhǎng)下一幀時(shí)間
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
else if (diff >= sync_threshold)
delay = 2 * delay;
至于為什么不都是delay + diff
,即為什么還有第3種1情況,我的猜測(cè)是:
延時(shí)直接加上diff,那么下一幀就直接修正了視頻種和主鐘的差異,但有可能這個(gè)差異已經(jīng)比較大了,直接一步到位的修正導(dǎo)致的效果就是:畫(huà)面有明顯的停頓,然后聲音繼續(xù)播,等到同步了視頻再恢復(fù)正常。
而如果采用2*delay
的方式,是每一次修正delay
,多次逐步修正差異,可能變化上會(huì)更平滑一些。效果上就是畫(huà)面和聲音都是正常的,然后聲音逐漸的追上聲音,最后同步。
至于為什么第2種情況選擇一步到位的修正,第3種情況選擇逐步修正,這個(gè)很難說(shuō)。因?yàn)锳V_SYNC_FRAMEDUP_THRESHOLD值為0.15,對(duì)應(yīng)的幀率是7左右,到這個(gè)程度,視頻基本都是幻燈片了,我猜想這時(shí)逐步修正也沒(méi)意義了。
3. 同步鐘時(shí)間獲取的實(shí)現(xiàn)
再看同步鐘時(shí)間的實(shí)現(xiàn):get_clock
獲取時(shí)間, set_clock_at
更新時(shí)間。
解析一下:return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
,為啥這么寫(xiě)?
上一次顯示的時(shí)候,更新了同步鐘,調(diào)用set_clock_at
,上次的時(shí)間為c->last_updated,則:
c->pts_drift + time = (c->pts - c->last_updated)+time;
假設(shè)距離上次的時(shí)間差time_diff = time - c->last_updated
,則表達(dá)式整體可以變?yōu)椋?/p>
c->pts+time_diff+(c->speed - 1)*time_diff
,合并后兩項(xiàng)變?yōu)?
c->pts+c->speed*time_diff
.
我們要求得就是當(dāng)前時(shí)間時(shí)的媒體內(nèi)容位置,上次的位置是c->pts
,而中間過(guò)去了time_diff
這么多時(shí)間,媒體內(nèi)容過(guò)去的時(shí)間就是:播放速度x現(xiàn)實(shí)時(shí)間,也就是c->speed*time_diff。舉例:現(xiàn)實(shí)里過(guò)去10s,如果你2倍速的播放,那視頻就過(guò)去了20s。所以這個(gè)表達(dá)式就很清晰了。
在set_clock_speed
里同時(shí)調(diào)用了set_clock
,這是為了保證從上次更新時(shí)間以來(lái),速度是沒(méi)變的,否則計(jì)算就沒(méi)有意義了。
到這差不多了,還有一點(diǎn)是在seek時(shí)候同步鐘的處理,到seek問(wèn)題的時(shí)候再看。
seek的處理
seek就是調(diào)整進(jìn)度條到新的地方開(kāi)始播,這個(gè)操作會(huì)打亂原本的數(shù)據(jù)流,一些播放秩序要重新建立。需要處理的問(wèn)題包括:
- 緩沖區(qū)數(shù)據(jù)的釋放,而且要重頭到位全部釋放干凈
- 播放時(shí)間顯示
- “加載中”的狀態(tài)的維護(hù),這個(gè)影響著用戶(hù)界面的顯示問(wèn)題
- 剔除錯(cuò)誤幀的問(wèn)題
流程
- 外界seek調(diào)用到
ijkmp_seek_to_l
,然后發(fā)送消息ffp_notify_msg2(mp->ffplayer, FFP_REQ_SEEK, (int)msec);
,消息捕獲到后調(diào)用到stream_seek
,然后設(shè)置seek_req
為1,記錄seek目標(biāo)到seek_pos
。 - 在讀取函數(shù)
read_thread
里,在is->seek_req
為true時(shí),進(jìn)入seek處理,幾個(gè)核心處理:
-
ffp_toggle_buffering
關(guān)閉解碼,packet緩沖區(qū)靜止 - 調(diào)用
avformat_seek_file
進(jìn)行seek - 成功之后用
packet_queue_flush
清空緩沖區(qū),并且把flush_pkt
插入進(jìn)去,這時(shí)一個(gè)標(biāo)記數(shù)據(jù) - 把當(dāng)前的serial記錄下來(lái)
到這里值得學(xué)習(xí)的點(diǎn)是:
- 我在處理seek的時(shí)候,是另開(kāi)一個(gè)線(xiàn)程調(diào)用了ffmpeg的seek方法,而這里是直接在讀取線(xiàn)程里,這樣就不用等待讀取流程的結(jié)束了
- seek成功之后再flush緩沖區(qū)
因?yàn)?/p>
if (pkt == &flush_pkt)
q->serial++;
所以serial的意義就體現(xiàn)出來(lái)了,每次seek,serial+1,也就是serial作為一個(gè)標(biāo)記,相同代表是同一次seek里的。
- 到
decoder_decode_frame
里:
- 因?yàn)閟eek的修改是在讀取線(xiàn)程里,和這里的解碼線(xiàn)程不是一個(gè),所以seek的修改可以在這里代碼的任何位置出現(xiàn)。
-
if (d->queue->serial == d->pkt_serial)
這個(gè)判斷里面為代碼塊1,while (d->queue->serial != d->pkt_serial)
這個(gè)循環(huán)為代碼塊2,if (pkt.data == flush_pkt.data)
這個(gè)判斷為true為代碼塊3,false為代碼塊4. - 如果seek修改出現(xiàn)在代碼塊2之前,那么就一定會(huì)進(jìn)代碼塊2,因?yàn)?code>packet_queue_get_or_buffering會(huì)一直讀取到flush_pkt,所以也就會(huì)一定進(jìn)代碼塊3,會(huì)執(zhí)行
avcodec_flush_buffers
清空解碼器的緩存。 - 如果seek在代碼塊2之后,那么就只會(huì)進(jìn)代碼塊4,但是再循環(huán)回去時(shí),會(huì)進(jìn)代碼塊2、代碼塊3,然后
avcodec_flush_buffers
把這個(gè)就得packet清掉了。 - 綜合上面兩種情況,只有seek之后的packet才會(huì)得到解碼,牛逼!
這一段厲害在:
- seek的修改在任何時(shí)候,它都不會(huì)出錯(cuò)
- seek的處理是在解碼線(xiàn)程里做的,省去了條件鎖等線(xiàn)程間通信的處理,更簡(jiǎn)單穩(wěn)定。如果整個(gè)數(shù)據(jù)流是一條河流,那flush_pkt就像一個(gè)這個(gè)河流的一個(gè)浮標(biāo),遇到這個(gè)浮標(biāo),后面水流的顏色都變了。有一種自己升級(jí)自己的這種意思,而不是由一個(gè)第三方來(lái)做輔助的升級(jí)。對(duì)于流水線(xiàn)式的程序邏輯,這樣做更好。
- 播放處
視頻video_refresh
里:
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
音頻audio_decode_frame
里:
do {
if (!(af = frame_queue_peek_readable(&is->sampq)))
return -1;
frame_queue_next(&is->sampq);
} while (af->serial != is->audioq.serial);
都根據(jù)serial把舊數(shù)據(jù)略過(guò)了。
所以整體看下來(lái),seek體系里最厲害的東西的東西就是使用了serial來(lái)標(biāo)記數(shù)據(jù),從而可以很明確的知道哪些是就數(shù)據(jù),哪些是新數(shù)據(jù)。然后處理都是在原線(xiàn)程里做的處理,而不是在另外的線(xiàn)程里來(lái)修改相關(guān)的數(shù)據(jù),省去了線(xiàn)程控制、線(xiàn)程通訊的麻煩的操作,穩(wěn)定性也提高了。
播放時(shí)間獲取
看ijkmp_get_current_position
,seek時(shí),返回seek的時(shí)間,播放時(shí)看ffp_get_current_position_l
,核心就是內(nèi)容時(shí)間get_master_clock
減去開(kāi)始時(shí)間is->ic->start_time
。
seek的時(shí)候,內(nèi)容位置發(fā)生了一個(gè)巨大的跳躍,所以要怎么維持同步鐘的正確?
- 音頻和視頻數(shù)據(jù)里的pts都是
frame->pts * av_q2d(tb)
,也就是內(nèi)容時(shí)間,但是轉(zhuǎn)成了現(xiàn)實(shí)時(shí)間單位。 - 然后
is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
,所以is->audio_clock
是最新的一幀音頻的數(shù)據(jù)播完時(shí)內(nèi)容時(shí)間 - 在音頻的填充方法里,設(shè)置音頻鐘的代碼是:
set_clock_at(&is->audclk,
is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout),
is->audio_clock_serial,
ffp->audio_callback_time / 1000000.0);
因?yàn)?code>is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;,所以audio_write_buf_size
就是當(dāng)前幀還沒(méi)讀完剩余的大小,所以(double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec
就標(biāo)識(shí)剩余的數(shù)據(jù)播放完的時(shí)間。
SDL_AoutGetLatencySeconds(ffp->aout)
是上層的緩沖區(qū)的數(shù)據(jù)的時(shí)間,對(duì)iOS的AudioQueue而言,有多個(gè)AudioBuffer等待播放,這個(gè)時(shí)間就是它們播放完要花的時(shí)間。
時(shí)間軸上是這樣的:
[幀結(jié)束點(diǎn)][剩余buf時(shí)間][上層的buf時(shí)間][剛結(jié)束播放的點(diǎn)]
所以第二個(gè)參數(shù)的時(shí)間是:當(dāng)前幀結(jié)束時(shí)的內(nèi)容時(shí)間-剩余buf的時(shí)間-上層播放器buf的時(shí)間,也就是剛結(jié)束播放的內(nèi)容時(shí)間。
ffp->audio_callback_time
是填充方法調(diào)用時(shí)的時(shí)間,這里存在一個(gè)假設(shè),就是上層播放器播完了一個(gè)buffer,立馬調(diào)用了填充函數(shù),所以ffp->audio_callback_time
就是剛結(jié)束播放的現(xiàn)實(shí)時(shí)間。
這樣第2個(gè)參數(shù)和第4個(gè)參數(shù)的意義就匹配上了。
回到seek,在seek完成后,會(huì)有第一個(gè)新的frame進(jìn)入播放,它會(huì)把同步鐘的pts,也就是媒體的內(nèi)容時(shí)間調(diào)整到seek后的位置,那么還有一個(gè)問(wèn)題:mp->seek_req
這個(gè)標(biāo)識(shí)重置回0的時(shí)間點(diǎn)必須比第一個(gè)新frame的set_clock_at
要晚,否則同步鐘的時(shí)間還沒(méi)調(diào)到新的,seek的標(biāo)識(shí)就結(jié)束了,然后根據(jù)同步鐘去計(jì)算當(dāng)前的播放時(shí)間,就出錯(cuò)了(界面上應(yīng)該是進(jìn)度條閃回seek之前)。
而事實(shí)上并沒(méi)有這樣,因?yàn)樵谕界姷?code>get_clock,還有一個(gè)
if (*c->queue_serial != c->serial)
return NAN;
這個(gè)serial真是神操作,太好用了!
音頻鐘和視頻鐘的serial都是在播放時(shí)更新的,也就是第一幀新數(shù)據(jù)播放時(shí)更新到seek以后的serial,而c->queue_serial
是一個(gè)指針:init_clock(&is->vidclk, &is->videoq.serial);
,和packetQueue的serial共享內(nèi)存的。
所以也就是到第一幀新數(shù)據(jù)播放后,c->queue_serial != c->serial
這個(gè)才不成立。也就是即使mp->seek_req
重置回0,取得值還是seek的目標(biāo)值,還不是根據(jù)pts計(jì)算的,所以也不會(huì)閃回了。
關(guān)于seek的東西太復(fù)雜了。
stop時(shí)的資源釋放
從方法shutdown
到核心釋放方法stream_close
。操作的流程如下:
- 停掉讀取線(xiàn)程:
-
packet_queue_abort
把音視頻的packetQueue停止讀取 -
abort_request
標(biāo)識(shí)為1,然后SDL_WaitThread
等待線(xiàn)程結(jié)束
- 停掉解碼器部分
stream_component_close
:
-
decoder_abort
停掉packetQueue,放開(kāi)framequeue的阻塞,等待解碼線(xiàn)程結(jié)束,然后清空packetQueue。 -
decoder_destroy
銷(xiāo)毀解碼器 - 重置流數(shù)據(jù)為空
- 停掉顯示線(xiàn)程:在顯示線(xiàn)程里有判斷數(shù)據(jù)流,視頻
is->video_st
,音頻is->audio_st
,在上一步里把流重置為空,顯示線(xiàn)程會(huì)結(jié)束。這里同樣使用SDL_WaitThread
等待線(xiàn)程結(jié)束。 - 清空緩沖區(qū)數(shù)據(jù):
packet_queue_destroy
銷(xiāo)毀packetQueue,frame_queue_destory
銷(xiāo)毀frameQueue。
對(duì)比我寫(xiě)的,需要修改的地方:
- 結(jié)束線(xiàn)程使用
pthread_join
的方式,而不是用鎖 - 解碼器、緩沖區(qū)等全部摧毀,下次播放再重建,不要重用
- 音頻的停止通過(guò)停掉上層播放器,底層是被動(dòng)的,而且沒(méi)有循環(huán)線(xiàn)程;視頻的停止也只需要等待線(xiàn)程結(jié)束。
核心就是第一點(diǎn),使用pthread_join
等待線(xiàn)程結(jié)束。
網(wǎng)絡(luò)不好處理
會(huì)自動(dòng)暫停,等待。內(nèi)部可以控制播放或暫停。
使用VTB時(shí)架構(gòu)的統(tǒng)一
- frame緩沖區(qū)使用自定義的數(shù)據(jù)結(jié)構(gòu)Frame,通過(guò)他可以把各種樣式進(jìn)行統(tǒng)一。
- 下層擁有了Frame數(shù)據(jù),上層的對(duì)接對(duì)象時(shí)Vout,邊界就在這里。然后上層要的是overlay,所以問(wèn)題就是怎么由frame轉(zhuǎn)化成overlay,以及如何顯示overlay。這兩個(gè)操作由Vout提供的
create_overlay
和display_overlay
來(lái)完成。 - 使用VTB之后,數(shù)據(jù)存在解碼后獲得的pixelBuffer里,而ffmpeg解碼后的數(shù)據(jù)在AVFrame里,這個(gè)轉(zhuǎn)化的區(qū)別就在不同的overlay創(chuàng)建函數(shù)里。
總結(jié):
- 對(duì)于兩個(gè)模塊的連接處,為了統(tǒng)一,兩邊都需要封裝統(tǒng)一的模型;
- 在統(tǒng)一的模型內(nèi),又具有不同的操作細(xì)分;
- 輸入數(shù)據(jù)從A到B,那么細(xì)分操作由B來(lái)提供,應(yīng)為B是接受者,它知道需要一個(gè)什么樣的結(jié)果。
- 這樣在執(zhí)行流程上一樣的,能保持流程的穩(wěn)定性;而實(shí)際執(zhí)行時(shí),在某些地方又有不同,從而又可以適應(yīng)各種獨(dú)特的需求。