帶問題重讀ijkPlayer

問題

  1. 主流程上的區(qū)別
  2. 緩沖區(qū)的設(shè)計
  3. 內(nèi)存管理的邏輯
  4. 音視頻播放方式
  5. 音視頻同步
  6. seek的問題:緩沖區(qū)flush橡卤、播放時間顯示扮念、k幀間距大時定位不準(zhǔn)問題...
  7. stop時怎么釋放資源,是否切換到副線程碧库?
  8. 網(wǎng)絡(luò)不好時的處理柜与,如獲取frame速度慢于消耗速度時,如果不暫停谈为,會一致卡頓旅挤,是否會主動暫停踢关?
  9. VTB的解碼和ffmpeg的解碼怎么統(tǒng)一的伞鲫?架構(gòu)上怎么設(shè)計的?

數(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里。這個函數(shù)是上層的音頻播放庫的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裝入到一個有序的隊列里。
  • GetVTBPicture從有序隊列里把frame的封裝拿出來傍药,也就是這個有序隊列只是一個臨時的用來排序的工具罷了,這個思想是可以吸收的魂仍;queue_picture里拐辽,把解碼的frame放入frame緩沖區(qū)
  • 顯示video_refresh+video_image_display2+[IJKSDLGLView display:]
  • 最后的紋理生成放在了render里,對vtb的pixelBuffer,在yuv420sp_vtb_uploadTexture擦酌。使用render這個角色俱诸,渲染的部分都抽象出來了。shader在IJK_GLES2_getFragmentShader_yuv420sp

結(jié)論:主流程上沒有大的差別赊舶。

緩沖區(qū)的設(shè)計

packetQueue:

  1. 數(shù)據(jù)機構(gòu)設(shè)計

packetQueue采用兩條鏈表睁搭,一個是保存數(shù)據(jù)的鏈表,一個是復(fù)用節(jié)點鏈表笼平,保存沒有數(shù)據(jù)的那些節(jié)點园骆。數(shù)據(jù)鏈表從first_pktlast_pkt,插入數(shù)據(jù)接到last_pkt的后面,取數(shù)據(jù)從first_pkt拿寓调。復(fù)用鏈表的開頭是recycle_pkt锌唾,取完數(shù)據(jù)后的空節(jié)點,放到空鏈表recycle_pkt的頭部捶牢,然后這個空節(jié)點成為新的recycle_pkt鸠珠。存數(shù)據(jù)時巍耗,也從recycle_pkt復(fù)用一個節(jié)點。

鏈表的節(jié)點像是包裝盒渐排,裝載數(shù)據(jù)的時候放到數(shù)據(jù)鏈表炬太,數(shù)據(jù)取出后回歸到復(fù)用鏈表。

  1. 進(jìn)出的阻塞控制

取數(shù)據(jù)的時候可能沒有驯耻,那么就有幾種處理:直接返回亲族、阻塞等待。它這里的處理是阻塞等待可缚,并且會把視頻播放暫停霎迫。所以這個回答了問題8,外面看到的效果就是:網(wǎng)絡(luò)卡的時候帘靡,會停止播放然后流暢的播放一會知给,然后又繼續(xù)卡頓,播放和卡頓是清晰分隔的描姚。

進(jìn)數(shù)據(jù)的時候并沒有做阻塞控制涩赢,為什么數(shù)據(jù)不會無限擴大?

是有阻塞的,但阻塞不是在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;
       }

簡化來看就是:

  • infinite_buffer不是無限的緩沖
  • is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size,使用數(shù)據(jù)大小做限制
  • stream_has_enough_packets使用數(shù)據(jù)的個數(shù)做限制

因為個數(shù)設(shè)置到了50000筒扒,一般達(dá)不到,而是數(shù)據(jù)大小做了限制绊寻,在15M左右花墩。

這里精髓的地方有兩點:

  • 采用了數(shù)據(jù)大小做限制,因為對于不同的視頻澄步,分辨率的問題會導(dǎo)致同一個packet差距巨大冰蘑,而我們實際關(guān)心的其實就是內(nèi)存問題。
  • 暫停10ms驮俗,而不是無限暫停等待條件鎖的signal懂缕。從設(shè)計上說會更簡單,而且可以避免頻繁的wait+signal王凑。這個問題還需仔細(xì)思考搪柑,但直覺上覺得這樣的操作非常好。

frameQueue:

數(shù)據(jù)使用一個簡單的數(shù)組保存索烹,可以把這個數(shù)據(jù)看成是環(huán)形的工碾,然后也是其中一段有數(shù)據(jù),另一段沒有數(shù)據(jù)百姓。rindex表示數(shù)據(jù)開頭的index,也是讀取數(shù)據(jù)的index,即read index,windex表示空數(shù)據(jù)開頭的index,是寫入數(shù)據(jù)的index,即write index渊额。

也是不斷循環(huán)重用,然后size表示當(dāng)前數(shù)據(jù)大小,max_size表示最大的槽位數(shù)旬迹,寫入的時候如果size滿了火惊,就會阻塞等待;讀取的時候size為空奔垦,也會阻塞等待屹耐。

有個奇怪的東西是rindex_shown,讀取的時候不是讀的rindex位置的數(shù)據(jù),而是rindex+rindex_shown,需要結(jié)合后面的使用情況再看這個的作用椿猎。后面再看惶岭。

還有serial沒有明白什么意思

結(jié)論:緩沖區(qū)的設(shè)計和我的完全不同,但都使用重用的概念犯眠,而且節(jié)點都是包裝盒按灶,數(shù)據(jù)包裝在節(jié)點里面。性能上不好比較筐咧,但我的設(shè)計更完善鸯旁,frame和packet使用統(tǒng)一設(shè)計,還包含了排序功能嗜浮。

內(nèi)存管理
  1. packet的管理
  • av_read_frame得到初始值羡亩,這個時候引用數(shù)為1,packet是使用一個臨時變量去接的危融,也就是棧內(nèi)存。
  • 然后加入隊列時雷袋,pkt1->pkt = *pkt;使用值拷貝的方式把packet存入吉殃,這樣緩沖區(qū)的數(shù)據(jù)和外面的臨時變量就分離了。
  • packet_queue_get_or_buffering把packet取出來楷怒,同樣使用值復(fù)制的方式蛋勺。
  • 最后使用av_packet_unref把packet關(guān)聯(lián)的buf釋放掉,而臨時變量的packet可以繼續(xù)使用鸠删。

需要注意的一點是:avcodec_send_packet返回EAGAIN表示當(dāng)前還無法接受新的packet抱完,還有frame沒有取出來,所以有了:

d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);

把這個packet存到d->pkt刃泡,在下一個循環(huán)里巧娱,先取frame,再把packet接回來烘贴,接著上面的操作:

if (d->packet_pending) {
    av_packet_move_ref(&pkt, &d->pkt);
    d->packet_pending = 0;
}

可能是存在B幀的時候會這樣禁添,因為B幀需要依賴后面的幀,所以不會解碼出來桨踪,等到后面的幀傳入后老翘,就會有多個幀需要讀取。這時解碼器應(yīng)該就不接受新的packet。但ijkplayer這里的代碼似乎不會出現(xiàn)這樣的情況铺峭,因為讀取frame不是一次一個墓怀,而是一次性讀到報EAGAIN錯誤未知。待考察卫键。

另捺疼,av_packet_move_ref這個函數(shù)就是完全的只復(fù)制,source的值完全的搬到destination,并且把source重置掉永罚。其實就是搬了個位置啤呼,buf的引用數(shù)不改變。

  1. 視頻frame的內(nèi)存管理
  • ffplay_video_thread里呢袱,frame是一個對內(nèi)存官扣,使用get_video_frame從解碼器讀取到frame。這時frame的引用為1
  • 過程中出錯羞福,使用av_frame_unref釋放frame的buf的內(nèi)存,但frame本身還可以繼續(xù)使用惕蹄。不出錯,也會調(diào)用av_frame_unref治专,這樣保證每個讀取的frame都會unref,這個unref跟初始化是對應(yīng)的卖陵。使用引用指數(shù)來管理內(nèi)存,重要的原則就是一一對應(yīng)张峰。

因為這里只是拿到frame泪蔫,然后存入緩沖區(qū),還沒有到使用的時候喘批,如果buf被釋放了撩荣,那么到播放的時候,數(shù)據(jù)就丟失了,所以是怎么處理的呢饶深?

存入緩沖區(qū)在queue_picture里餐曹,再到SDL_VoutFillFrameYUVOverlay,這個函數(shù)會到上層,根據(jù)解碼器不同做不同處理敌厘,以ijksdl_vout_overlay_ffmpeg.cfunc_fill_frame為例台猴。

有兩種處理:

  • 一種是overlay和frame共享內(nèi)存,就顯示的直接使用frame的內(nèi)存俱两,格式是YUV420p的就是這樣饱狂,因為OpenGL可以直接顯示這種顏色空間的圖像。這種就只需要對frame加一個引用锋华,保證不會被釋放掉就好了嗡官。關(guān)鍵就是這句:av_frame_ref(opaque->linked_frame, frame);
  • 另一種是不共享,因為要轉(zhuǎn)格式毯焕,另建一個frame衍腥,即這里的opaque->managed_frame,然后轉(zhuǎn)格式磺樱。數(shù)據(jù)到了新地方,原frame也就沒用了婆咸。不做ref操作竹捉,它自然的就會釋放了。
  1. 音頻frame的處理

audio_thread里尚骄,不斷通過decoder_decode_frame獲取到新的frame块差。和視頻一樣,這里的frame也是對內(nèi)存倔丈,讀到解碼后的frame后憨闰,引用為1。音頻的格式轉(zhuǎn)換放在了播放階段需五,所以這里只是單純的把frame存入:av_frame_move_ref(af->frame, frame);鹉动。做了一個復(fù)制,把讀取的frame搬運到緩沖區(qū)里了宏邮。

在frame的緩沖區(qū)取數(shù)據(jù)的時候泽示,frame_queue_next里包含了av_frame_unref把frame釋放。這個視頻也是一樣蜜氨。

有一個問題是械筛,上層播放器的讀取音頻數(shù)據(jù)的時候,frame必須是活的飒炎,因為如果音頻不轉(zhuǎn)換格式埋哟,是直接讀取了frame里的數(shù)據(jù)。所以也就是需要在填充播放器數(shù)據(jù)結(jié)束后厌丑,才可以釋放frame定欧。

unref是在frame_queue_next,而這個函數(shù)是在下一次讀取frame的時候才發(fā)生怒竿,下一次讀取frame又是在當(dāng)前的數(shù)據(jù)讀完后,所以讀完了數(shù)據(jù)后扩氢,才會釋放frame,這樣就沒錯了耕驰。

//數(shù)據(jù)讀完才會去拉取下一個frame
if (is->audio_buf_index >= is->audio_buf_size) {
    audio_size = audio_decode_frame(ffp);
音視頻的播放方式

音頻播放使用AudioQueue:

  • 構(gòu)建AudioQueue:AudioQueueNewOutput
  • 開始AudioQueueStart,暫停AudioQueuePause,結(jié)束AudioQueueStop
  • 在回調(diào)函數(shù)IJKSDLAudioQueueOuptutCallback里,調(diào)用下層的填充函數(shù)來填充AudioQueue的buffer录豺。
  • 使用AudioQueueEnqueueBuffer把裝配完的AudioQueue Buffer入隊朦肘,進(jìn)入播放。

上面這些都是AudioQueue的標(biāo)準(zhǔn)操作双饥,特別的是構(gòu)建AudioStreamBasicDescription的時候媒抠,也就是指定音頻播放的格式。格式是由音頻源的格式?jīng)Q定的咏花,在IJKSDLGetAudioStreamBasicDescriptionFromSpec里看趴生,除了格式固定為pcm之外阀趴,其他的都是從底層給的格式復(fù)制過來。這樣就有了很大的自由苍匆,音頻源只需要解碼成pcm就可以了刘急。

而底層的格式是在audio_open里決定的,邏輯是:

  • 根據(jù)源文件浸踩,構(gòu)建一個期望的格式wanted_spec,然后把這個期望的格式提供給上層叔汁,最后把上層的實際格式拿到作為結(jié)果返回。一個類似溝通的操作检碗,這種思維很值得借鑒
  • 如果上傳不接受這種格式据块,返回錯誤,底層修改channel數(shù)折剃、采樣率然后再繼續(xù)溝通另假。
  • 但是樣本格式是固定為s16,即signed integer 16,有符號的int類型,位深為16比特的格式微驶。位深指每個樣本存儲的內(nèi)存大小浪谴,16個比特,加上有符號因苹,所以范圍是[-2^15, 215-1],215為32768苟耻,變化性足夠了。

因為都是pcm,是不壓縮的音頻扶檐,所以決定性的因素就只有:采樣率凶杖、通道數(shù)和樣本格式。樣本格式固定s16款筑,和上層溝通就是決定采樣率和通道數(shù)智蝠。

這里是一個很好的分層架構(gòu)的例子,底層通用奈梳,上層根據(jù)平臺各有不同杈湾。

視頻的播放:

播放都是使用OpenGL ES,使用IJKSDLGLView,重寫了layerClass,把layer類型修改為CAEAGLLayer可以顯示OpenGL ES的渲染內(nèi)容攘须。所有類型的畫面都使用這個顯示漆撞,有區(qū)別的地方都抽象到Render這個角色里了,相關(guān)的方法有:

  • setupRenderer 構(gòu)建一個render
  • IJK_GLES2_Renderer_renderOverlay 繪制overlay于宙。

render的構(gòu)建包括:

  • 使用不同的fragmnt shader和共通的vertex shader構(gòu)建program
  • 提供mvp矩陣
  • 設(shè)置頂點和紋理坐標(biāo)數(shù)據(jù)

render的繪制包括:

  • func_uploadTexture定位到不同的render,執(zhí)行不同的紋理上傳操作
  • 繪制圖形使用glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);,使用了圖元GL_TRIANGLE_STRIP而不是GL_TRIANGLE浮驳,可以節(jié)省頂點。

提供紋理的方法也是重點捞魁,區(qū)別在于顏色空間以及元素的排列方式:

  • rgb類型的提供了3種:565至会、888和8888。rgb類型的元素都是混合在一起的谱俭,也就是只有一個層(plane)奉件,565指是rgb3個元素分別占用的比特位數(shù)宵蛀,同理888,8888是另外包含了alpha元素瓶蚂。所以每個像素565占2個字節(jié)糖埋,888占3個字節(jié),8888占4個字節(jié)窃这。
glTexImage2D(GL_TEXTURE_2D,
                    0,
                    GL_RGBA,
                    widths[plane],
                    heights[plane],
                    0,
                    GL_RGBA,
                    GL_UNSIGNED_BYTE,
                    pixels[plane]);

構(gòu)建紋理的時候區(qū)別就在format跟type參數(shù)上瞳别。

  • yuv420p的,這種指的是最常用的y杭攻、u祟敛、v3個元素全部開,分3層兆解,然后數(shù)量比是4:1:1馆铁,所以u v的紋理大小高和寬都是y紋理的一半。然后因為每個分量各自一個紋理锅睛,所以每個紋理都是單通道的埠巨,使用的format為GL_LUMINANCE
  • yuv420sp的,這種yuv的比例也是4:1:1现拒,區(qū)別在于u v不是分開兩層辣垒,而是混合在同一層里,分層是uuuuvvvv,混合是uvuvuvuv印蔬。所以構(gòu)建兩個紋理勋桶,y的紋理不變,uv的紋理使用雙通道的格式GL_RG_EXT,大小也是y的1/4(高寬都為1/2)侥猬。這種在fragment shader里取值的時候會有區(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在同一個紋理里例驹,texture2D直接取了rg兩個分量。

  • yuv444p的不是很懂退唠,看fragment shader貌似每個像素有兩個版本的yuv鹃锈,然后做了一個插值。
  • 最后是yuv420p_vtb,這個是VideoToolBox硬解出來的數(shù)據(jù)的顯示瞧预,因為數(shù)據(jù)存儲在CVPixelBuffer里仪召,所以直接使用了iOS系統(tǒng)的紋理構(gòu)建方法。

ijkplayer里的的OpenGL ES是2.0版本松蒜,如果使用3.0版本,雙通道可以使用GL_LUMINANCE_ALPHA已旧。

音視頻同步

首先看音頻秸苗,音頻并沒有做阻塞控制,上層的的播放器要需要數(shù)據(jù)都會填充运褪,沒有看到時間不到不做填充的操作惊楼。所以應(yīng)該是默認(rèn)了音頻鐘做主控制玖瘸,所以音頻沒做處理。

1. 視頻顯示時的時間控制

視頻的控制在video_refresh里檀咙,播放函數(shù)是video_display2,進(jìn)入這里代表時間到了雅倒、該播了,這是一個檢測點弧可。

有幾個參數(shù)需要了解:

  • is->frame_timer,這個時間代表上一幀播放的時間
  • delay表示這一幀到下一幀的時間差
if (isnan(is->frame_timer) || time < is->frame_timer){
    is->frame_timer = time;
}

上一幀的播放時間在當(dāng)前時間后面蔑匣,說明數(shù)據(jù)錯誤,調(diào)整到當(dāng)期時間

if (time < is->frame_timer + delay) {
    *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
    goto display;
}

is->frame_timer + delay就表示當(dāng)前幀播放的時間棕诵,這個時間晚于當(dāng)前時間裁良,就表示還沒到播放的時候。

這個有個坑goto display并不是去播放了校套,因為display代碼塊里還有一個判斷价脾,判斷里有個is->force_refresh。這個值默認(rèn)是false,所以直接跳去display笛匙,實際的意義是啥也不干侨把,結(jié)束這次判斷。

反之妹孙,如果播放時間早于當(dāng)前時間秋柄,那就要馬上播放了。所以更新上一幀的播放時間:is->frame_timer += delay;涕蜂。

然后一直到后面,有個is->force_refresh = 1;,這時才是真的播放华匾。

從上面兩段就可以看出基本的流程了:

一開始當(dāng)前幀播放時間沒到,goto display等待下次循環(huán)机隙,循環(huán)多次蜘拉,時間不段后移,終于播放時間到了,播放當(dāng)前幀有鹿,frame_timer更新為當(dāng)前幀的時間旭旭。然后又重復(fù)上面的過程,去播放下一幀葱跋。

然后有個問題是:為什么frame_timer的更新是加上delay持寄,而不是直接等于當(dāng)前時間?

如果直接等于當(dāng)前時間娱俺,因為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比較落后的時候怜姿,直接提到當(dāng)前time上,就可以直接把狀態(tài)修正疼燥,之后的播放又會走上正軌沧卢。

2. 同步鐘以及鐘時間的修正

同步鐘的概念: 音頻或者視頻,如果把內(nèi)容正確的完整的播放醉者,某個內(nèi)容和一個時間是一一對應(yīng)的但狭,當(dāng)前的音頻或者視頻播放到哪個位置,它就有一個時間來表示湃交,這個時間就是同步鐘的時間熟空。所以音頻鐘的時間表示音頻播放到哪個位置,視頻鐘表示播放到哪個位置搞莺。

因為音頻和視頻是分開表現(xiàn)的息罗,就可能會出現(xiàn)音頻和視頻的進(jìn)度不一致,在同步鐘上就表現(xiàn)為兩個同步鐘的值不同才沧,如果讓兩者統(tǒng)一迈喉,就是音視頻同步的問題。

因為有了同步鐘的概念温圆,音視頻內(nèi)容上的同步就可以簡化為更準(zhǔn)確的:音頻鐘和視頻鐘時間相同挨摸。

這時會有一個同步鐘作為主鐘,也就是其他的同步鐘根據(jù)這個主鐘來調(diào)整自己的時間岁歉。滿了就調(diào)快得运、快了就調(diào)慢。

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,那么下一幀就直接修正了視頻種和主鐘的差異,但有可能這個差異已經(jīng)比較大了备绽,直接一步到位的修正導(dǎo)致的效果就是:畫面有明顯的停頓券坞,然后聲音繼續(xù)播,等到同步了視頻再恢復(fù)正常肺素。

而如果采用2*delay的方式恨锚,是每一次修正delay,多次逐步修正差異,可能變化上會更平滑一些倍靡。效果上就是畫面和聲音都是正常的眠冈,然后聲音逐漸的追上聲音,最后同步。

至于為什么第2種情況選擇一步到位的修正蜗顽,第3種情況選擇逐步修正,這個很難說雨让。因為AV_SYNC_FRAMEDUP_THRESHOLD值為0.15雇盖,對應(yīng)的幀率是7左右,到這個程度栖忠,視頻基本都是幻燈片了崔挖,我猜想這時逐步修正也沒意義了。

3. 同步鐘時間獲取的實現(xiàn)

再看同步鐘時間的實現(xiàn):get_clock獲取時間庵寞, set_clock_at更新時間狸相。

解析一下:return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);,為啥這么寫?

上一次顯示的時候捐川,更新了同步鐘脓鹃,調(diào)用set_clock_at,上次的時間為c->last_updated,則:

c->pts_drift + time = (c->pts - c->last_updated)+time;

假設(shè)距離上次的時間差time_diff = time - c->last_updated,則表達(dá)式整體可以變?yōu)椋?/p>

c->pts+time_diff+(c->speed - 1)*time_diff,合并后兩項變?yōu)?
c->pts+c->speed*time_diff.

我們要求得就是當(dāng)前時間時的媒體內(nèi)容位置古沥,上次的位置是c->pts,而中間過去了time_diff這么多時間瘸右,媒體內(nèi)容過去的時間就是:播放速度x現(xiàn)實時間,也就是c->speed*time_diff岩齿。舉例:現(xiàn)實里過去10s,如果你2倍速的播放太颤,那視頻就過去了20s。所以這個表達(dá)式就很清晰了盹沈。

set_clock_speed里同時調(diào)用了set_clock,這是為了保證從上次更新時間以來龄章,速度是沒變的,否則計算就沒有意義了乞封。

到這差不多了做裙,還有一點是在seek時候同步鐘的處理,到seek問題的時候再看歌亲。

seek的處理

seek就是調(diào)整進(jìn)度條到新的地方開始播菇用,這個操作會打亂原本的數(shù)據(jù)流,一些播放秩序要重新建立陷揪。需要處理的問題包括:

  • 緩沖區(qū)數(shù)據(jù)的釋放惋鸥,而且要重頭到位全部釋放干凈
  • 播放時間顯示
  • “加載中”的狀態(tài)的維護(hù),這個影響著用戶界面的顯示問題
  • 剔除錯誤幀的問題

流程

  1. 外界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卦绣。
  2. 在讀取函數(shù)read_thread里,在is->seek_req為true時飞蚓,進(jìn)入seek處理,幾個核心處理:
  • ffp_toggle_buffering關(guān)閉解碼滤港,packet緩沖區(qū)靜止
  • 調(diào)用avformat_seek_file進(jìn)行seek
  • 成功之后用packet_queue_flush清空緩沖區(qū),并且把flush_pkt插入進(jìn)去,這時一個標(biāo)記數(shù)據(jù)
  • 把當(dāng)前的serial記錄下來

到這里值得學(xué)習(xí)的點是:

  • 我在處理seek的時候溅漾,是另開一個線程調(diào)用了ffmpeg的seek方法山叮,而這里是直接在讀取線程里,這樣就不用等待讀取流程的結(jié)束了
  • seek成功之后再flush緩沖區(qū)

因為

if (pkt == &flush_pkt)
        q->serial++;

所以serial的意義就體現(xiàn)出來了添履,每次seek,serial+1,也就是serial作為一個標(biāo)記屁倔,相同代表是同一次seek里的。

  1. decoder_decode_frame里:
  • 因為seek的修改是在讀取線程里暮胧,和這里的解碼線程不是一個锐借,所以seek的修改可以在這里代碼的任何位置出現(xiàn)。
  • if (d->queue->serial == d->pkt_serial)這個判斷里面為代碼塊1往衷,while (d->queue->serial != d->pkt_serial)這個循環(huán)為代碼塊2钞翔,if (pkt.data == flush_pkt.data)這個判斷為true為代碼塊3,false為代碼塊4.
  • 如果seek修改出現(xiàn)在代碼塊2之前席舍,那么就一定會進(jìn)代碼塊2布轿,因為packet_queue_get_or_buffering會一直讀取到flush_pkt,所以也就會一定進(jìn)代碼塊3俺亮,會執(zhí)行avcodec_flush_buffers清空解碼器的緩存驮捍。
  • 如果seek在代碼塊2之后,那么就只會進(jìn)代碼塊4脚曾,但是再循環(huán)回去時东且,會進(jìn)代碼塊2、代碼塊3本讥,然后avcodec_flush_buffers把這個就得packet清掉了珊泳。
  • 綜合上面兩種情況,只有seek之后的packet才會得到解碼拷沸,牛逼色查!

這一段厲害在:

  • seek的修改在任何時候,它都不會出錯
  • seek的處理是在解碼線程里做的撞芍,省去了條件鎖等線程間通信的處理秧了,更簡單穩(wěn)定。如果整個數(shù)據(jù)流是一條河流序无,那flush_pkt就像一個這個河流的一個浮標(biāo)验毡,遇到這個浮標(biāo),后面水流的顏色都變了帝嗡。有一種自己升級自己的這種意思晶通,而不是由一個第三方來做輔助的升級。對于流水線式的程序邏輯哟玷,這樣做更好狮辽。
  1. 播放處

視頻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ù)略過了。

所以整體看下來,seek體系里最厲害的東西的東西就是使用了serial來標(biāo)記數(shù)據(jù)喉脖,從而可以很明確的知道哪些是就數(shù)據(jù)椰苟,哪些是新數(shù)據(jù)。然后處理都是在原線程里做的處理动看,而不是在另外的線程里來修改相關(guān)的數(shù)據(jù)尊剔,省去了線程控制、線程通訊的麻煩的操作菱皆,穩(wěn)定性也提高了。

播放時間獲取

ijkmp_get_current_position,seek時挨稿,返回seek的時間仇轻,播放時看ffp_get_current_position_l,核心就是內(nèi)容時間get_master_clock減去開始時間is->ic->start_time

seek的時候奶甘,內(nèi)容位置發(fā)生了一個巨大的跳躍篷店,所以要怎么維持同步鐘的正確?

  • 音頻和視頻數(shù)據(jù)里的pts都是frame->pts * av_q2d(tb),也就是內(nèi)容時間臭家,但是轉(zhuǎn)成了現(xiàn)實時間單位疲陕。
  • 然后is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;,所以is->audio_clock是最新的一幀音頻的數(shù)據(jù)播完時內(nèi)容時間
  • 在音頻的填充方法里,設(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);

因為is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;,所以audio_write_buf_size就是當(dāng)前幀還沒讀完剩余的大小钉赁,所以(double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec就標(biāo)識剩余的數(shù)據(jù)播放完的時間蹄殃。

SDL_AoutGetLatencySeconds(ffp->aout)是上層的緩沖區(qū)的數(shù)據(jù)的時間,對iOS的AudioQueue而言你踩,有多個AudioBuffer等待播放诅岩,這個時間就是它們播放完要花的時間。

時間軸上是這樣的:

[幀結(jié)束點][剩余buf時間][上層的buf時間][剛結(jié)束播放的點]

所以第二個參數(shù)的時間是:當(dāng)前幀結(jié)束時的內(nèi)容時間-剩余buf的時間-上層播放器buf的時間带膜,也就是剛結(jié)束播放的內(nèi)容時間吩谦。

ffp->audio_callback_time是填充方法調(diào)用時的時間,這里存在一個假設(shè)膝藕,就是上層播放器播完了一個buffer式廷,立馬調(diào)用了填充函數(shù),所以ffp->audio_callback_time就是剛結(jié)束播放的現(xiàn)實時間芭挽。

這樣第2個參數(shù)和第4個參數(shù)的意義就匹配上了滑废。

回到seek,在seek完成后,會有第一個新的frame進(jìn)入播放览绿,它會把同步鐘的pts策严,也就是媒體的內(nèi)容時間調(diào)整到seek后的位置,那么還有一個問題:mp->seek_req這個標(biāo)識重置回0的時間點必須比第一個新frame的set_clock_at要晚,否則同步鐘的時間還沒調(diào)到新的忠蝗,seek的標(biāo)識就結(jié)束了山橄,然后根據(jù)同步鐘去計算當(dāng)前的播放時間频伤,就出錯了(界面上應(yīng)該是進(jìn)度條閃回seek之前)倔韭。

而事實上并沒有這樣术浪,因為在同步鐘的get_clock,還有一個

if (*c->queue_serial != c->serial)
        return NAN;

這個serial真是神操作,太好用了寿酌!

音頻鐘和視頻鐘的serial都是在播放時更新的胰苏,也就是第一幀新數(shù)據(jù)播放時更新到seek以后的serial,而c->queue_serial是一個指針:init_clock(&is->vidclk, &is->videoq.serial);,和packetQueue的serial共享內(nèi)存的。

所以也就是到第一幀新數(shù)據(jù)播放后醇疼,c->queue_serial != c->serial這個才不成立硕并。也就是即使mp->seek_req重置回0,取得值還是seek的目標(biāo)值秧荆,還不是根據(jù)pts計算的倔毙,所以也不會閃回了。

關(guān)于seek的東西太復(fù)雜了乙濒。

stop時的資源釋放

從方法shutdown到核心釋放方法stream_close陕赃。操作的流程如下:

  1. 停掉讀取線程:
  • packet_queue_abort把音視頻的packetQueue停止讀取
  • abort_request標(biāo)識為1,然后SDL_WaitThread等待線程結(jié)束
  1. 停掉解碼器部分stream_component_close
  • decoder_abort停掉packetQueue,放開framequeue的阻塞颁股,等待解碼線程結(jié)束么库,然后清空packetQueue。
  • decoder_destroy 銷毀解碼器
  • 重置流數(shù)據(jù)為空
  1. 停掉顯示線程:在顯示線程里有判斷數(shù)據(jù)流甘有,視頻is->video_st,音頻is->audio_st,在上一步里把流重置為空诉儒,顯示線程會結(jié)束。這里同樣使用SDL_WaitThread等待線程結(jié)束梧疲。
  2. 清空緩沖區(qū)數(shù)據(jù):packet_queue_destroy銷毀packetQueue,frame_queue_destory銷毀frameQueue允睹。

對比我寫的,需要修改的地方:

  • 結(jié)束線程使用pthread_join的方式幌氮,而不是用鎖
  • 解碼器缭受、緩沖區(qū)等全部摧毀,下次播放再重建该互,不要重用
  • 音頻的停止通過停掉上層播放器米者,底層是被動的,而且沒有循環(huán)線程宇智;視頻的停止也只需要等待線程結(jié)束蔓搞。

核心就是第一點,使用pthread_join等待線程結(jié)束随橘。

網(wǎng)絡(luò)不好處理

會自動暫停喂分,等待。內(nèi)部可以控制播放或暫停机蔗。

使用VTB時架構(gòu)的統(tǒng)一
  1. frame緩沖區(qū)使用自定義的數(shù)據(jù)結(jié)構(gòu)Frame,通過他可以把各種樣式進(jìn)行統(tǒng)一蒲祈。
  2. 下層擁有了Frame數(shù)據(jù)甘萧,上層的對接對象時Vout,邊界就在這里梆掸。然后上層要的是overlay,所以問題就是怎么由frame轉(zhuǎn)化成overlay,以及如何顯示overlay扬卷。這兩個操作由Vout提供的create_overlaydisplay_overlay來完成。
  3. 使用VTB之后酸钦,數(shù)據(jù)存在解碼后獲得的pixelBuffer里怪得,而ffmpeg解碼后的數(shù)據(jù)在AVFrame里,這個轉(zhuǎn)化的區(qū)別就在不同的overlay創(chuàng)建函數(shù)里卑硫。

總結(jié):

  • 對于兩個模塊的連接處徒恋,為了統(tǒng)一,兩邊都需要封裝統(tǒng)一的模型欢伏;
  • 在統(tǒng)一的模型內(nèi)因谎,又具有不同的操作細(xì)分;
  • 輸入數(shù)據(jù)從A到B颜懊,那么細(xì)分操作由B來提供,應(yīng)為B是接受者风皿,它知道需要一個什么樣的結(jié)果河爹。
  • 這樣在執(zhí)行流程上一樣的,能保持流程的穩(wěn)定性桐款;而實際執(zhí)行時咸这,在某些地方又有不同,從而又可以適應(yīng)各種獨特的需求魔眨。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末媳维,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子遏暴,更是在濱河造成了極大的恐慌侄刽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朋凉,死亡現(xiàn)場離奇詭異州丹,居然都是意外死亡,警方通過查閱死者的電腦和手機杂彭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門墓毒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人亲怠,你說我怎么就攤上這事所计。” “怎么了团秽?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵主胧,是天一觀的道長叭首。 經(jīng)常有香客問我,道長讥裤,這世上最難降的妖魔是什么放棒? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮己英,結(jié)果婚禮上间螟,老公的妹妹穿的比我還像新娘。我一直安慰自己损肛,他們只是感情好厢破,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著治拿,像睡著了一般摩泪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劫谅,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天见坑,我揣著相機與錄音,去河邊找鬼捏检。 笑死荞驴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贯城。 我是一名探鬼主播熊楼,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼能犯!你這毒婦竟也來了鲫骗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤踩晶,失蹤者是張志新(化名)和其女友劉穎执泰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體合瓢,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡坦胶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了晴楔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顿苇。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖税弃,靈堂內(nèi)的尸體忽然破棺而出纪岁,到底是詐尸還是另有隱情,我是刑警寧澤则果,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布幔翰,位于F島的核電站漩氨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏遗增。R本人自食惡果不足惜叫惊,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望做修。 院中可真熱鬧霍狰,春花似錦、人聲如沸饰及。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燎含。三九已至宾濒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屏箍,已是汗流浹背绘梦。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赴魁,地道東北人谚咬。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像尚粘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子敲长,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,654評論 0 3
  • 在上一篇筆記中我們已經(jīng)完成了使用SDL播放聲音和視頻郎嫁,聲音播放沒有什么問題,而視頻播放太快祈噪,很明顯視頻沒有同步泽铛。在...
    762683ff5d3d閱讀 1,301評論 0 1
  • 這是一個跨平臺的播放器ijkplayer,iOS上集成看【如何快速的開發(fā)一個完整的iOS直播app】(原理篇)辑鲤。 ...
    FindCrt閱讀 7,041評論 2 45
  • Linear PCM 在介紹Core Audio之前盔腔,先介紹一下最常用的非壓縮數(shù)字音頻格式Linear PCM(線...
    huangjun0閱讀 4,309評論 0 2
  • 根據(jù)ffmpeg官方網(wǎng)站上的例子程序開始學(xué)習(xí)ffmpeg和SDL編程。 SDL是一個跨平臺的多媒體開發(fā)包月褥。適用于游...
    762683ff5d3d閱讀 1,781評論 0 2