系列文章目錄
嵌入式音視頻開發(零)移植ffmpeg及推流測試
嵌入式音視頻開發(一)ffmpeg框架及內核解析
嵌入式音視頻開發(二)ffmpeg音視頻同步
嵌入式音視頻開發(三)直播協議及編碼器
文章目錄
- 系列文章目錄
- 前言
- 一、音視頻同步
- 1.1 基礎概念
- 1.2 三種同步方法
- 二、音視頻同步的實現
- 2.1 時間基的轉換問題
- 2.2 音頻為基準
- 2.2.1 實現思路
- 2.2.2 代碼大綱
- 2.3 外部時鐘同步
- 2.3.1 實現思路
- 2.3.2 代碼大綱
前言
??前文中已經講述了音視頻處理的流程,需要我們將音頻數據和視頻數據分開處理,這個時候我們就需要音視頻同步操作。
一、音視頻同步
??我們平常看視頻的時候最煩惱的就是各種音畫不同步,例如音頻是100ms延時而視頻需要150ms延時才能到達,這其中我們就需要進行音視頻同步來解決這個問題。 音視頻同步是多媒體處理中的一個關鍵問題,常用方法包括三種不同的同步策略:以視頻為基準、以音頻為基準和以外部時鐘為基準。
1.1 基礎概念
??在FFmpeg進行音視頻解碼時,PTS (Presentation Time Stamp) 是一個非常重要的概念,它表示每一幀數據(音頻幀或視頻幀)的展示時間,即該幀應該在播放設備上顯示的精確時間。
??時間基(Timebase)是一個分數,表示每秒的時間單位。它用于將 PTS和 DTS(從基于時鐘滴答的計數轉換為實際的時間(秒)。常見的表示形式為 1/fps 或 1/sample_rate例如,假設視頻流的時間基準是1/90000,那么每個時間單位代表1/90000秒。因此,PTS值為90000時,相當于1秒。實際上ffmpeg內部存在多種時間基,在不同的階段(結構體)中,對應的時間基的值都不相同。
表示方法 | 結構體 | 描述 | 作用 |
---|---|---|---|
time_base | AVStream | 流的時間基 | 用于將 PTS 和 DTS 轉換為實際時間 |
time_base | AVCodecContext | 編碼器或解碼器的時間基 | 用于內部處理和同步 |
video_codec_timebase audio_codec_timebase | AVFormatContext | 格式上下文的時間基 | 用于整體管理和同步 |
??值得注意的是,雖然 AVPacket 和 AVFrame 本身沒有直接的時間基字段,但它們的時間戳(PTS 和 DTS)是基于其所屬流的時間基來解釋的。
??時間戳可以簡單理解為計時器,用于記錄或設置對應時間點的操作。在 FFmpeg 中,時間戳用于同步音視頻幀的播放時間。時間戳的計算公式如下:
timestamp(ffmpeg 內部時間戳) = PTS * 時間基
time(秒) = PTS * 時間基
??例如,假設我們有一個視頻流,其時間基為 1/90000,若某幀的 PTS 值為 90000,則該幀的實際展示時間為time(秒) = PTS * 時間基 = 90000 * (1/90000) = 1 秒
1.2 三種同步方法
??這里先簡單舉個例子,例如下圖所示,原本的視頻應在0.080秒有一幀,但是現在出現了掉幀,此時對應音頻就需要加速播放或者也相應丟一幀。簡單來說就是,以誰為基準就由誰來維護時間軸。
??(1)以視頻為基準:視頻被視為主要的同步標準,音頻的播放時間會根據視頻幀的時間戳來進行調整。如果音頻的播放時間比視頻快,系統會延遲音頻的播放,為避免過多積壓可能會丟棄部分音頻幀;如果音頻播放落后于視頻,系統會通過延時音頻的播放來保證同步。
??(2)以音頻為基準:以音頻為基準時,視頻會根據音頻的時間戳進行調整。如果視頻的播放時間比音頻快,系統會延遲視頻的播放,直到音頻達到相應的時間點;而視頻播放落后于音頻,系統會加速視頻的播放,丟掉部分視頻幀,從而保證音視頻同步。
??(3) 以外部時鐘為基準:外部時鐘同步是一種更為綜合的方式,它使用一個外部時鐘(例如系統時鐘或硬件時鐘)來同時控制音頻和視頻的播放。外部時鐘會提供一個精確的時間基準,視頻和音頻都需要根據這個時鐘進行調整。
二、音視頻同步的實現
2.1 時間基的轉換問題
??前面提到了ffmpeg內部存在多種時間基,在不同的階段(結構體)中,對應的時間基的值都不相同,此外視頻流的時間基和音頻流的時間基也不同。通常情況下需要使用av_q2d()函數將AVRational 類型的時間基(Timebase)轉換為雙精度浮點數(double)。AVRational 是一個表示分數的結構體,通常用于表示時間基、幀率等需要精確表示的比率。
typedef struct AVRational {int num; ///< 分子 (numerator)int den; ///< 分母 (denominator)
} AVRational;// 通過 av_q2d 函數將時間基轉換為浮點數后,可以將其乘以 PTS 或 DTS 來得到實際時間
double av_q2d(AVRational q) {return q.num / (double)q.den;
}
2.2 音頻為基準
??音頻為基準和視頻為基準在實現邏輯上差不多,這里以音頻為例。
2.2.1 實現思路
??以音頻為基準進行同步的基本思路是:
- 選擇音頻流作為同步基準
- 解碼音頻數據并更新當前音頻時間戳
- 解碼視頻數據并根據音頻時間戳調整視頻幀的顯示時間,確保音視頻同步
- 通過適當的緩沖控制,確保播放的流暢性和穩定性
2.2.2 代碼大綱
int main{// 初始化 FFmpeg 庫av_register_all();AVFormatContext *fmt_ctx = NULL;// 打開輸入文件并獲取流信息if (open_input_file(&fmt_ctx, "input.mp4") < 0) {return -1;}// 查找音視頻流并初始化解碼器int audio_stream_idx = find_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO);int video_stream_idx = find_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO);AVCodecContext *audio_dec_ctx = init_decoder(fmt_ctx, audio_stream_idx);AVCodecContext *video_dec_ctx = init_decoder(fmt_ctx, video_stream_idx);// 循環讀取數據包并同步播放AVPacket pkt;while (read_packet(fmt_ctx, &pkt) >= 0) {if (pkt.stream_index == audio_stream_idx) {process_audio_packet(&pkt, audio_dec_ctx);} else if (pkt.stream_index == video_stream_idx) {process_video_packet(&pkt, video_dec_ctx, audio_dec_ctx->time_base);}av_packet_unref(&pkt);}// 清理資源cleanup(fmt_ctx, audio_dec_ctx, video_dec_ctx);
}// 解碼音頻數據包并更新當前音頻時間戳
void process_audio_packet(AVPacket *pkt, AVCodecContext *dec_ctx) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 更新當前音頻時間戳update_current_audio_pts(frame->pts, dec_ctx->time_base);}
}void update_current_audio_pts(int64_t pts, AVRational time_base) {double pts_in_seconds = pts * av_q2d(time_base);current_audio_pts = pts_in_seconds;
}void process_video_packet(AVPacket *pkt, AVCodecContext *dec_ctx, AVRational audio_time_base) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 獲取視頻幀的 PTS 并轉換為秒double video_pts_in_seconds = frame->pts * av_q2d(dec_ctx->time_base);// 根據音頻時間戳調整視頻幀的顯示時間sync_video_to_audio(video_pts_in_seconds, audio_time_base);}
}void sync_video_to_audio(double video_pts, AVRational audio_time_base) {while (video_pts > current_audio_pts) {usleep(1000); // 簡單的等待機制// 更新當前音頻時間戳current_audio_pts = get_current_audio_pts(audio_time_base);// 其他操作}
}double get_current_audio_pts(AVRational audio_time_base) {// 這里應該實現一個函數來獲取最新的音頻時間戳// 例如通過解碼更多的音頻幀或使用其他方法return current_audio_pts;
}
2.3 外部時鐘同步
2.3.1 實現思路
??以外部時鐘為基準進行同步的基本思路是:
- 使用外部時鐘(如系統時鐘)作為基準
- 解碼音頻數據包,根據外部時鐘調整音頻播放時間
- 解碼視頻數據包,根據外部時鐘調整視頻幀的顯示時間
- 通過適當的緩沖控制,確保播放的流暢性和穩定性
2.3.2 代碼大綱
??這里的代碼和上文差不多,只有調整部分的邏輯不太一樣:
// 獲取當前外部時鐘時間(秒)
double get_external_clock() {struct timespec now;clock_gettime(CLOCK_MONOTONIC, &now); // 使用單調遞增的時鐘避免系統時間變化的影響double elapsed = (now.tv_sec - start_time.tv_sec) + (now.tv_nsec - start_time.tv_nsec) / 1e9;return elapsed;
}// 解碼音頻數據包并根據外部時鐘調整音頻播放時間
void process_audio_packet(AVPacket *pkt, AVCodecContext *dec_ctx) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 將音頻幀的時間戳轉換為秒double audio_pts_in_seconds = frame->pts * av_q2d(dec_ctx->time_base);// 根據外部時鐘調整音頻幀的播放時間sync_audio_to_external_clock(audio_pts_in_seconds, dec_ctx->time_base);}
}void sync_audio_to_external_clock(double audio_pts, AVRational time_base) {double external_clock_time = get_external_clock(); // 獲取外部時鐘時間(秒)// 等待直到音頻幀應該播放的時間while (audio_pts > external_clock_time) {usleep(1000); // 簡單的等待機制external_clock_time = get_external_clock();}// 其他操作
}void process_video_packet(AVPacket *pkt, AVCodecContext *dec_ctx, AVRational audio_time_base) {int ret = avcodec_send_packet(dec_ctx, pkt);if (ret < 0) {fprintf(stderr, "Error sending a packet for decoding\n");return;}while (ret >= 0) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Error during decoding\n");break;}// 獲取視頻幀的 PTS 并轉換為秒double video_pts_in_seconds = frame->pts * av_q2d(dec_ctx->time_base);// 根據外部時鐘調整視頻幀的顯示時間sync_video_to_external_clock(video_pts_in_seconds, dec_ctx->time_base);}
}void sync_video_to_external_clock(double video_pts, AVRational video_time_base) {double external_clock_time = get_external_clock(); // 獲取外部時鐘時間(秒)// 等待直到視頻幀應該顯示的時間while (video_pts > external_clock_time) {usleep(1000); // 簡單的等待機制external_clock_time = get_external_clock();}// 其他操作
}
免責聲明:本文參考了網上公開的部分資料,僅供學習參考使用,若有侵權或勘誤請聯系筆者