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