嵌入式音視頻開發(二)ffmpeg音視頻同步

系列文章目錄

嵌入式音視頻開發(零)移植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_baseAVStream流的時間基用于將 PTS 和 DTS 轉換為實際時間
time_baseAVCodecContext編碼器或解碼器的時間基用于內部處理和同步
video_codec_timebase audio_codec_timebaseAVFormatContext格式上下文的時間基用于整體管理和同步

??值得注意的是,雖然 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 實現思路

??以音頻為基準進行同步的基本思路是:

  1. 選擇音頻流作為同步基準
  2. 解碼音頻數據并更新當前音頻時間戳
  3. 解碼視頻數據并根據音頻時間戳調整視頻幀的顯示時間,確保音視頻同步
  4. 通過適當的緩沖控制,確保播放的流暢性和穩定性

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 實現思路

??以外部時鐘為基準進行同步的基本思路是:

  1. 使用外部時鐘(如系統時鐘)作為基準
  2. 解碼音頻數據包,根據外部時鐘調整音頻播放時間
  3. 解碼視頻數據包,根據外部時鐘調整視頻幀的顯示時間
  4. 通過適當的緩沖控制,確保播放的流暢性和穩定性

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();}// 其他操作
}

免責聲明:本文參考了網上公開的部分資料,僅供學習參考使用,若有侵權或勘誤請聯系筆者

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/70161.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/70161.shtml
英文地址,請注明出處:http://en.pswp.cn/web/70161.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

iOS App的啟動與優化

App的啟動流程 App啟動分為冷啟動和熱啟動 冷啟動&#xff1a;從0開始啟動App熱啟動&#xff1a;App已經在內存中&#xff0c;但是后臺還掛著&#xff0c;再次點擊圖標啟動App。 一般對App啟動的優化都是針對冷啟動。 App冷啟動可分為三個階段&#xff1a; dyld&#xff1a…

oppo,湯臣倍健,康冠科技25屆春招內推

oppo&#xff0c;湯臣倍健&#xff0c;康冠科技25屆春招內推 ①康冠科技25屆春招 【職位】算法、軟件、硬件、技術&#xff0c;結構設計&#xff0c;供應鏈&#xff0c;產品&#xff0c;職能&#xff0c;商務 【一鍵內推】https://sourl.cn/2Mm9Lk 【內推碼】EVBM88 ②湯臣倍健…

centos 9 時間同步服務

在 CentOS 9 中&#xff0c;默認的時間同步服務是 chrony&#xff0c;而不是傳統的 ntpd。 因此&#xff0c;建議使用 chrony 來配置和管理時間同步。 以下是使用 chrony 配置 NTP 服務的步驟&#xff1a; 1. 安裝 chrony 首先&#xff0c;確保系統已安裝 chrony。 在 CentOS…

解鎖 Python 導入系統:從基礎到進階的深度指南

本文全面解讀 Python 導入系統&#xff0c;從導入機制的基礎概念&#xff0c;如模塊、包的導入方式&#xff0c;到查找、加載模塊的詳細過程&#xff0c;再到導入系統的高級特性和應用場景&#xff0c;通過豐富示例、直觀圖表和對比分析&#xff0c;助你深入理解并熟練運用導入…

DDoS技術解析

這里是Themberfue 今天我們不聊別的&#xff0c;我們聊聊著名的網絡攻擊手段之一的 DDoS&#xff0c;看看其背后的技術細節。 DoS 了解 DDoS 前&#xff0c;先來講講 DoS 是什么&#xff0c;此 DoS 而不是 DOS 操作系統啊。1996年9月6日&#xff0c;世界第三古老的網絡服務提供…

docker安裝kafka,并通過springboot快速集成kafka

目錄 一、docker安裝和配置Kafka 1.拉取 Zookeeper 的 Docker 鏡像 2.運行 Zookeeper 容器 3.拉取 Kafka 的 Docker 鏡像 4.運行 Kafka 容器 5.下載 Kafdrop 6.運行 Kafdrop 7.如果docker pull wurstmeister/zookeeper或docker pull wurstmeister/kafka下載很慢&#x…

C++ 與 Java 的對比分析:除法運算中的錯誤處理

博客主頁&#xff1a; [小????????] 本文專欄: Java 文章目錄 &#x1f4af;前言&#x1f4af;C中的除法錯誤處理&#x1f4af;Java中的除法錯誤處理&#x1f4af;C與Java錯誤處理的對比&#x1f4af;錯誤處理的優化和實踐&#x1f4af;小結 &#x1f4af;前言 在…

LLM之循環神經網絡(RNN)

在人工智能的領域中&#xff0c;神經網絡是推動技術發展的核心力量。今天&#xff0c;讓我們深入探討循環神經網絡&#xff08;RNN&#xff09; 一、神經網絡基礎 &#xff08;1&#xff09;什么是神經網絡 神經網絡&#xff0c;又稱人工神經網絡&#xff0c;其設計靈感源于人…

SQL sever數據導入導出實驗

1.創建數據庫TCP-H &#xff08;1&#xff09;右鍵“數據庫”&#xff0c;點擊“新建數據庫”即可 &#xff08;2&#xff09;用sql語言創建&#xff0c;此處以創建數據庫DB_test為例&#xff0c;代碼如下&#xff1a; use master;go--檢查在當前服務器系統中的所有數據里面…

讓編程變成一種享受-明基RD320U顯示器

引言 作為一名有著多年JAVA開發經驗的從業者&#xff0c;在工作過程中&#xff0c;顯示器的重要性不言而喻。它不僅是我們與代碼交互的窗口&#xff0c;更是影響工作效率和體驗的關鍵因素。在多年的編程生涯中&#xff0c;我遇到過各種各樣的問題。比如&#xff0c;在進行代碼…

計算機網絡(涵蓋OSI,TCP/IP,交換機,路由器,局域網)

一、網絡通信基礎 &#xff08;一&#xff09;網絡通信的概念 網絡通信是指終端設備之間通過計算機網絡進行的信息傳遞與交流。它類似于現實生活中的物品傳遞過程&#xff1a;數據&#xff08;物品&#xff09;被封裝成報文&#xff08;包裹&#xff09;&#xff0c;通過網絡…

圖像處理篇---基本OpenMV圖像處理

文章目錄 前言1. 灰度化&#xff08;Grayscale&#xff09;2. 二值化&#xff08;Thresholding&#xff09;3. 掩膜&#xff08;Mask&#xff09;4. 腐蝕&#xff08;Erosion&#xff09;5. 膨脹&#xff08;Dilation&#xff09;6. 縮放&#xff08;Scaling&#xff09;7. 旋轉…

SpringMVC重定向接口,參數暴露在url中解決方案!RedirectAttributes

OK&#xff0c;首先描述下業務場景&#xff0c;終端數量限制登錄 1.首先訪問項目login的get接口 2.輸入賬號密碼點擊登錄后&#xff0c;會請求login的POST接口 3.后臺對終端數量邏輯處理不允許登錄跳回到登錄頁面 4.因代碼原因需在后臺進行多次重定向接口&#xff0c;最后跳…

Spring Boot01(注解、)---java八股

Spring Boot中常用注解及其底層實現 1、SpringBootApplication注解&#xff1a; SpringBootApplication注解&#xff1a;這個注解標識了一個SpringBoot工程&#xff0c;它實際上是另外三個注解的組合&#xff0c;這三個注解是&#xff1a; aSpringBootConfiguration&#xff1a…

?2.快速了解HTML5的標簽類型

??HTML5 的標簽類型豐富多樣&#xff0c;每種類型都有其獨特的功能和用途&#xff0c;以下是一些常見的 HTML5 標簽類型介紹&#xff1a; &#x1f98b;結構標簽 &#x1faad;<html>&#xff1a;它是 HTML 文檔的根標簽&#xff0c;所有其他標簽都包含在這個標簽內&am…

eNSP防火墻綜合實驗

一、實驗拓撲 二、ip和安全區域配置 1、防火墻ip和安全區域配置 新建兩個安全區域 ip配置 Client1 Client2 電信DNS 百度web-1 聯通DNS 百度web-2 R2 R1 三、DNS透明代理相關配置 1、導入運營商地址庫 2、新建鏈路接口 3、配置真實DNS服務器 4、創建虛擬DNS服務器 5、配置D…

Linux 配置交換空間(Swap)解決內存不足

&#x1f680; 作者主頁&#xff1a; 有來技術 &#x1f525; 開源項目&#xff1a; youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template &#x1f33a; 倉庫主頁&#xff1a; GitCode︱ Gitee ︱ Github &#x1f496; 歡迎點贊 &#x1f44d; 收藏 ?評論 …

個人shell腳本分享

在周一到周五做增量備份&#xff0c;在周六周日做完全備份 #!/bin/bash定義變量 SRC“/path/to/source” # 源目錄 BKUP“/backup” # 備份主目錄 FUL“KaTeX parse error: Expected EOF, got # at position 22: …ull" #? 完全備份目錄 INC"BKUP/inc” # 增量備份…

Django 5 實用指南(一)安裝與配置

1.1 Django5的背景與發展 Django 自從2005年由Adrian Holovaty和Simon Willison在 Lawrence Journal-World 新聞網站上首次發布以來&#xff0c;Django 一直是 Web 開發領域最受歡迎的框架之一。Django 框架經歷了多個版本的演進&#xff0c;每次版本更新都引入了新功能、改進了…

百度搜索融合 DeepSeek 滿血版,開啟智能搜索新篇

百度搜索融合 DeepSeek 滿血版&#xff0c;開啟智能搜索新篇 &#x1f680; &#x1f539; 一、百度搜索全量接入 DeepSeek &#x1f539; 百度搜索迎來重要升級&#xff0c;DeepSeek 滿血版全面上線&#xff01;&#x1f389; 用戶在百度 APP 搜索后&#xff0c;點擊「AI」即…