原文地址:http://blog.yundiantech.com/?log=blog&id=12
前面我們已經爲播放器加上了簡單音視頻同步功能。
播放mp4文件的時候似乎沒啥問題,但是當播放rmvb文件的時候,問題就暴露出來了。
以電影天堂下載的電影文件爲例:
下載地址:
CD1
ftp://dygod2:[email protected]:2088/黑客帝國3.[中英雙字.1024高分辨率]/[電影天堂www.dygod.cn]黑客帝國3CD1.rmvb |
CD2
ftp://dygod2:[email protected]:2088/黑客帝國3.[中英雙字.1024高分辨率]/[電影天堂www.dygod.cn]黑客帝國3CD2.rmvb |
CD3
ftp://dygod2:[email protected]:2088/黑客帝國3.[中英雙字.1024高分辨率]/[電影天堂www.dygod.cn]黑客帝國3CD3.rmvb |
播放第一個文件(既cd1)的時候,問題似乎不大。但播放第二個和第三個文件的時候發現音頻和視頻會存在固定的偏差(我測試的時候大概是2S)。
使用普通播放器打開這個文件,也會發現開始播放的前幾秒,只出聲音,卻不出圖像。
於是分析了一下CD2和CD3的文件,發現其視頻pts不是從0開始的,也就是說前面一段數據裏面只有音頻數據,而沒有視頻數據。而我們實現同步的方法是通過讀取的時候判斷pts來決定顯示不顯示,因此碰到這種情況問題就出現了。
前面使用SDL的時候,我們發現,音頻不管讀的多塊,SDL播放的時候都是按照正常的速度放出來的,因此可以在SDL的回調函數中解碼出每一幀音頻數據的時候,記錄下音頻的pts,然後根據此Pts來決定是否顯示視頻。
前面已經說到視頻的pts不是從0開始的,且音頻和視頻的pts似乎不是連續的。
因此我們也要給視頻開一個緩存,先讀取出一定數量的視頻幀,存入隊列。
然後再開一個線程專門用來從視頻隊列中取出視頻並解碼,解碼之後便將得到的pts與音頻線程中記錄的音頻pts做對比,這樣就可以達到同步的目的了。
那麼現在 我們就有了3個線程:
1.讀取視頻的線程
2.解碼視頻的線程
3.解碼播放音頻的線程(這個是由SDL創建的)
1.讀取視頻線程如下:
讀取線程和前面的差別不大,都是打開視頻,然後在while循環裏面讀取視頻,分別放入音視頻的隊列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
while (1) { if (av_read_frame(pFormatCtx, packet) < 0) { break ; //這裏認爲視頻讀取完了 } if (packet->stream_index == videoStream) { packet_queue_put(&is->videoq, packet); //這裏我們將數據存入隊列 因此不調用 av_free_packet 釋放 } else if ( packet->stream_index == audioStream ) { packet_queue_put(&is->audioq, packet); //這裏我們將數據存入隊列 因此不調用 av_free_packet 釋放 } else { // Free the packet that was allocated by av_read_frame av_free_packet(packet); } } |
這裏我們是瞬間就把整個視頻讀入內存了,這樣我們播放小視頻的話 問題不大,但是當播放大視頻的時候,就把內存喫光了,因此必須要管管他。
目前想到比較好的方法就是 讀取的時候限制隊列的長度,當裏面的數據超過某個範圍的時候 就先等等。
於是改成如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
while (1) { //這裏做了個限制 當隊列裏面的數據超過某個大小的時候 就暫停讀取 防止一下子就把視頻讀完了,導致的空間分配不足 /* 這裏audioq.size是指隊列中的所有數據包帶的音頻數據的總量或者視頻數據總量,並不是包的數量 */ //這個值可以稍微寫大一些 if (is->audioq.size > MAX_AUDIO_SIZE || is->videoq.size > MAX_VIDEO_SIZE) { SDL_Delay(10); continue ; } if (av_read_frame(pFormatCtx, packet) < 0) { break ; //這裏認爲視頻讀取完了 } if (packet->stream_index == videoStream) { packet_queue_put(&is->videoq, packet); //這裏我們將數據存入隊列 因此不調用 av_free_packet 釋放 } else if ( packet->stream_index == audioStream ) { packet_queue_put(&is->audioq, packet); //這裏我們將數據存入隊列 因此不調用 av_free_packet 釋放 } else { // Free the packet that was allocated by av_read_frame av_free_packet(packet); } } |
2.視頻線程如下:
首先需要先創建一個線程,這裏我們不再用Qt的線程 而是直接使用SDL的API來創建線程,這麼做當然是爲了以後移植着想。
1
2
|
///創建一個線程專門用來解碼視頻 is->video_tid = SDL_CreateThread(video_thread, "video_thread" , &mVideoState); |
視頻線程操作如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
int video_thread( void *arg){ VideoState *is = (VideoState *) arg; AVPacket pkt1, *packet = &pkt1; int ret, got_picture, numBytes; double video_pts = 0; //當前視頻的pts double audio_pts = 0; //音頻pts ///解碼視頻相關 AVFrame *pFrame, *pFrameRGB; uint8_t *out_buffer_rgb; //解碼後的rgb數據 struct SwsContext *img_convert_ctx; //用於解碼後的視頻格式轉換 AVCodecContext *pCodecCtx = is->video_st->codec; //視頻解碼器 pFrame = av_frame_alloc(); pFrameRGB = av_frame_alloc(); ///這裏我們改成了 將解碼後的YUV數據轉換成RGB32 img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); numBytes = avpicture_get_size(PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height); out_buffer_rgb = (uint8_t *) av_malloc(numBytes * sizeof (uint8_t)); avpicture_fill((AVPicture *) pFrameRGB, out_buffer_rgb, PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height); while (1) { if (packet_queue_get(&is->videoq, packet, 1) <= 0) break ; //隊列裏面沒有數據了 讀取完畢了 ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); // if (ret < 0) { // printf("decode error. "); // return; // } if (packet->dts == AV_NOPTS_VALUE && pFrame->opaque&& *(uint64_t*) pFrame->opaque != AV_NOPTS_VALUE) { video_pts = *(uint64_t *) pFrame->opaque; } else if (packet->dts != AV_NOPTS_VALUE) { video_pts = packet->dts; } else { video_pts = 0; } video_pts *= av_q2d(is->video_st->time_base); video_pts = synchronize_video(is, pFrame, video_pts); while (1) { audio_pts = is->audio_clock; if (video_pts <= audio_pts) break ; int delayTime = (video_pts - audio_pts) * 1000; delayTime = delayTime > 5 ? 5:delayTime; SDL_Delay(delayTime); } if (got_picture) { sws_scale(img_convert_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); //把這個RGB數據 用QImage加載 QImage tmpImg((uchar *)out_buffer_rgb,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32); QImage image = tmpImg.copy(); //把圖像複製一份 傳遞給界面顯示 is->player->disPlayVideo(image); //調用激發信號的函數 } av_free_packet(packet); } av_free(pFrame); av_free(pFrameRGB); av_free(out_buffer_rgb); return 0; } |
3.音頻線程
至於音頻線程就不說了,我也是直接拷貝了被人的代碼。其實主要就是在前面的基礎上加了獲取音頻pts的功能,至於怎麼獲取沒有仔細研究他。
主要參考了這篇博客:http://blog.csdn.net/tanlon_0308/article/details/40428139
到這裏,我們的播放器已經實現了真正意義上的同步,這次是真的可以播放所有的視頻了。
當然啦,這還不能叫播放器,連基本的播放暫停都沒有,後面我們就給他加上播放、暫停、跳轉等常用的功能。
完整工程下載地址:
http://download.csdn.net/detail/qq214517703/9630369
音視頻技術交流討論歡迎加 QQ羣 121376426
原文地址:http://blog.yundiantech.com/?log=blog&id=12