帶問(wèn)題重讀ijkPlayer

問(wèn)題

  1. 主流程上的區(qū)別
  2. 緩沖區(qū)的設(shè)計(jì)
  3. 內(nèi)存管理的邏輯
  4. 音視頻播放方式
  5. 音視頻同步
  6. seek的問(wèn)題:緩沖區(qū)flush、播放時(shí)間顯示、k幀間距大時(shí)定位不準(zhǔn)問(wèn)題...
  7. stop時(shí)怎么釋放資源,是否切換到副線(xiàn)程?
  8. 網(wǎng)絡(luò)不好時(shí)的處理,如獲取frame速度慢于消耗速度時(shí),如果不暫停,會(huì)一致卡頓,是否會(huì)主動(dòng)暫停?
  9. 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:

  1. 數(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_pktlast_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ù)用鏈表。

  1. 進(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)存管理
  1. 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ù)不改變。

  1. 視頻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.cfunc_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ì)釋放了。
  1. 音頻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)題

流程

  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時(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里的。

  1. 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)式的程序邏輯,這樣做更好。
  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ù)略過(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。操作的流程如下:

  1. 停掉讀取線(xiàn)程:
  • packet_queue_abort把音視頻的packetQueue停止讀取
  • abort_request標(biāo)識(shí)為1,然后SDL_WaitThread等待線(xiàn)程結(jié)束
  1. 停掉解碼器部分stream_component_close
  • decoder_abort停掉packetQueue,放開(kāi)framequeue的阻塞,等待解碼線(xiàn)程結(jié)束,然后清空packetQueue。
  • decoder_destroy 銷(xiāo)毀解碼器
  • 重置流數(shù)據(jù)為空
  1. 停掉顯示線(xiàn)程:在顯示線(xiàn)程里有判斷數(shù)據(jù)流,視頻is->video_st,音頻is->audio_st,在上一步里把流重置為空,顯示線(xiàn)程會(huì)結(jié)束。這里同樣使用SDL_WaitThread等待線(xiàn)程結(jié)束。
  2. 清空緩沖區(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)一
  1. frame緩沖區(qū)使用自定義的數(shù)據(jù)結(jié)構(gòu)Frame,通過(guò)他可以把各種樣式進(jìn)行統(tǒng)一。
  2. 下層擁有了Frame數(shù)據(jù),上層的對(duì)接對(duì)象時(shí)Vout,邊界就在這里。然后上層要的是overlay,所以問(wèn)題就是怎么由frame轉(zhuǎn)化成overlay,以及如何顯示overlay。這兩個(gè)操作由Vout提供的create_overlaydisplay_overlay來(lái)完成。
  3. 使用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ú)特的需求。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評(píng)論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,041評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,475評(píng)論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,253評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,801評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,023評(píng)論 0 285
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,494評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,639評(píng)論 1 366
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,890評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,289評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,552評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,242評(píng)論 3 389
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,626評(píng)論 2 370

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

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