1. 終端音頻卡頓的可能情況
分析問題有一個很有用的鏈路分析法,將鏈路切分為多個環節,分析每個環節從而找到問題根源。
解碼框圖
接收碼流數據 -> 解復用 -> 音視頻解碼 -> 音視頻同步 -> 音視頻(顯卡、聲卡)輸出
音頻輸出環節分析:
音頻卡頓是人感官聽到的,也就是聲卡發出的聲音有卡頓;
聲卡卡頓的可能原因:1. 聲卡硬件異常 2.聲卡收到的音頻有問題
- 聲卡硬件異常的問題,只能換硬件。
- 聲卡的上一環節送過來的聲音有卡頓。
音視頻同步環節分析:
- 音頻解碼后的數據出現音頻卡頓
- 解碼后的音頻數據送給sample buffer出現了溢出。
音頻解碼器環節分析:
音頻解碼器工作比較簡單,收到數據就進行解碼,解碼后就將數據發送給下一級。
- 編碼的原始數據就是音頻卡頓。
- 接收解復用的數據時候,發送過來的數據量抖動太大,audio buffer 出現溢出。
- 音頻的PTS小於音頻包到達的時間,大部分解碼器都會直接扔掉。
解復用環節分析:
這一環節一般不會出問題。
分析總結:
分析了整個鏈路的每個環節,音頻卡頓的幾種可能:
- 音頻來得太遲(PTS < 到達時間),音頻包被丟棄了
- 接收到的流碼率抖動太大,解碼端的相關音頻buffer出現溢出
- 原始音頻數據就是卡頓。
- 聲卡硬件問題
情況1和情況2都可以使用相關工具分析判斷;
情況3需要錄流,軟件解碼出音頻PCM,播放PCM可以知道;
情況4用不同的終端對比測試可以判斷。
2. 解決方案
2.1 音頻PTS錯誤問題
max_delay
: 這個參數可以拉開PCR和PTS直接的差距,從而解決音頻包的PTS小於音頻包到達時間;ffmpeg 默認是0.7s, 可以根據需要設置它的值。
設置max_delay
參數的接口:
av_dict_set(&dicCtx, "max_delay", "1000000", 0)
2.2 輸出碼率抖動問題
核心問題就是讓FFMPEG碼流平滑輸出
ffmpeg 命令行輸出平滑碼率:
ffmpeg -re -i input.ts -c copy -muxrate 7000000 -f mpegts "udp://227.40.50.60:1234?pkt_size=1316&fifo_size=27887&bitrate=7000000&burst_bits=1000000"
說明:
input.ts:輸入流
muxrate
:設置復用碼率,ffmpeg通過填充空包達到CBR流,這個值不能低於input.ts流的實際碼率。
fifo_size
:ffmpeg采用av_fifo來緩存一定量數據用於控制平滑輸出,從ffmpeg的源碼中,這里設置27887,實際上ffmpeg內部分配大小為27887*188bytes的,可以
在libavformat/udp.c
中查看。
bitrate
:控制碼率平滑輸出,需要設置和muxrate參數一樣,否則從av_fifo中輸出和輸入對應不上導致av_fifo會出現上下溢情況。
burst_bits
:設置突發碼率的,這里設置為1000000突發碼率,控制平滑輸出時,防止輸入流那邊有突發,需要設置一個突發閥值。
音視頻+表+空包的碼率(muxrate)是恆定的,ffmpeg 通過調整空包碼率來保持復用碼率(CBR碼率)恆定;
但是這個恆定的碼率是秒級別的,直接輸出的話,碼率還是會抖動,bitrate參數是控制平滑輸出的;
若碼率6Mbit/s,一秒內要輸出的數據量就是5Mbit,平滑處理的時,將6Mbit的數據,分成10等分(舉例說明),每隔0.1s就發送500Kbit的數據,達到平滑輸出的目的。
FFMPEG 平滑輸出的核心代碼:
ffmpeg處理UDP平滑輸出代碼再libavformat/udp.c中的circular_buffer_task_tx接口中,如下代碼:
static void *circular_buffer_task_tx( void *_URLContext)
{
URLContext *h = _URLContext;
UDPContext *s = h->priv_data;
int old_cancelstate;
int64_t target_timestamp = av_gettime_relative();
int64_t start_timestamp = av_gettime_relative();
int64_t sent_bits = 0;
//根據上面設置的burst_bits計算出碼率突發值,單位為us
int64_t burst_interval = s->bitrate ? (s->burst_bits * 1000000 / s->bitrate) : 0;
//max_delay根據平滑碼率bitrate值及每一次發送UDP包的大小來設置的,單位為us,一般UDP發送
大小設置 為1316字節,即h->max_packet_size值為1316,確定好max_delay后用於下面控制碼率
平滑輸 出。
int64_t max_delay = s->bitrate ? ((int64_t)h->max_packet_size * 8 * 1000000 / s->bitrate + 1) : 0;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
pthread_mutex_lock(&s->mutex);
if (ff_socket_nonblock(s->udp_fd, 0) < 0) {
av_log(h, AV_LOG_ERROR, "Failed to set blocking mode");
s->circular_buffer_error = AVERROR(EIO);
goto end;
}
for(;;) {
int len;
const uint8_t *p;
uint8_t tmp[4];
int64_t timestamp;
len=av_fifo_size(s->fifo);
while (len<4) {
if (s->close_req)
goto end;
if (pthread_cond_wait(&s->cond, &s->mutex) < 0) {
goto end;
}
len=av_fifo_size(s->fifo);
}
av_fifo_generic_read(s->fifo, tmp, 4, NULL);
len=AV_RL32(tmp);
av_assert0(len >= 0);
av_assert0(len <= sizeof(s->tmp));
av_fifo_generic_read(s->fifo, s->tmp, len, NULL);
pthread_mutex_unlock(&s->mutex);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
//控制碼率平滑輸出
if (s->bitrate) {
timestamp = av_gettime_relative();
if (timestamp < target_timestamp) {
int64_t delay = target_timestamp - timestamp;
if (delay > max_delay) {
delay = max_delay;
start_timestamp = timestamp + delay;
sent_bits = 0;
}
av_usleep(delay);
} else {
if (timestamp - burst_interval > target_timestamp) {
start_timestamp = timestamp - burst_interval;
sent_bits = 0;
}
}
sent_bits += len * 8;
target_timestamp = start_timestamp + sent_bits * 1000000 / s->bitrate;
}
p = s->tmp;
while (len) {
int ret;
av_assert0(len > 0);
if (!s->is_connected) {
ret = sendto (s->udp_fd, p, len, 0,
(struct sockaddr *) &s->dest_addr,
s->dest_addr_len);
} else
ret = send(s->udp_fd, p, len, 0);
if (ret >= 0) {
len -= ret;
p += ret;
} else {
ret = ff_neterrno();
if (ret != AVERROR(EAGAIN) && ret != AVERROR(EINTR)) {
pthread_mutex_lock(&s->mutex);
s->circular_buffer_error = ret;
pthread_mutex_unlock(&s->mutex);
return NULL;
}
}
}
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
pthread_mutex_lock(&s->mutex);
}
end:
pthread_mutex_unlock(&s->mutex);
return NULL;
}