librtmp读包阻塞问题修复
近期項目中有遇到播放器在使用librtmp播放rtmp碼流時,在弱網環境中,會出現讀包block很久的情況,甚至斷網會直接阻塞讀包線程,導致播放器無法退出造成ANR。librtmp目前社區已無人維護,所以無法通過升級第三方庫來試圖解決此問題,只能自己啃源碼了。
附上rtmp鏈接
https://github.com/ossrs/librtmp
閱讀librtmp源碼得知,讀包有2個地方會造成死鎖問題,這兩個API分別是:
int RTMP_Read(RTMP *r, char *buf, int size) int RTMP_ReadPacket(RTMP *r, RTMPPacket *packet)首先看第一個:
RTMP_Read
????????Read_1_Packet
????????????????RTMP_GetNextMediaPacket
int RTMP_GetNextMediaPacket(RTMP *r, RTMPPacket *packet) {int bHasMediaPacket = 0;// 這里會死循環去讀包,并且讀不到數據也出不了循環while (!bHasMediaPacket && RTMP_IsConnected(r)&& RTMP_ReadPacket(r, packet)){if (!RTMPPacket_IsReady(packet) || !packet->m_nBodySize){continue;}bHasMediaPacket = RTMP_ClientPacket(r, packet);if (!bHasMediaPacket){RTMPPacket_Free(packet);}else if (r->m_pausing == 3){if (packet->m_nTimeStamp <= r->m_mediaStamp){bHasMediaPacket = 0; #ifdef _DEBUGRTMP_Log(RTMP_LOGDEBUG,"Skipped type: %02X, size: %d, TS: %d ms, abs TS: %d, pause: %d ms",packet->m_packetType, packet->m_nBodySize,packet->m_nTimeStamp, packet->m_hasAbsTimestamp,r->m_mediaStamp); #endifRTMPPacket_Free(packet);continue;}r->m_pausing = 0;}}if (bHasMediaPacket)r->m_bPlaying = TRUE;else if (r->m_sb.sb_timedout && !r->m_pausing)r->m_pauseStamp = r->m_mediaChannel < r->m_channelsAllocatedIn ?r->m_channelTimestamp[r->m_mediaChannel] : 0;return bHasMediaPacket; }第二個則是RTMP_ReadPacket:
RTMP_ReadPacket
????????ReadN
????????????????RTMPSockBuf_Fill
int RTMPSockBuf_Fill(RTMPSockBuf *sb) {int nBytes;if (!sb->sb_size)sb->sb_start = sb->sb_buf;while (1){nBytes = sizeof(sb->sb_buf) - 1 - sb->sb_size - (sb->sb_start - sb->sb_buf); #if defined(CRYPTO) && !defined(NO_SSL)if (sb->sb_ssl){nBytes = TLS_read(sb->sb_ssl, sb->sb_start + sb->sb_size, nBytes);}else #endif{// 直接調用系統的recv方法風險很大nBytes = recv(sb->sb_socket, sb->sb_start + sb->sb_size, nBytes, 0);}if (nBytes != -1){sb->sb_size += nBytes;}else{int sockerr = GetSockError();RTMP_Log(RTMP_LOGDEBUG, "%s, recv returned %d. GetSockError(): %d (%s)",__FUNCTION__, nBytes, sockerr, strerror(sockerr));if (sockerr == EINTR && !RTMP_ctrlC)continue;if (sockerr == EWOULDBLOCK || sockerr == EAGAIN){sb->sb_timedout = TRUE;nBytes = 0;}}break;}return nBytes; }在fill socket buffer的方法中會直接調用recv方法下載數據,這是風險極高的操作,弱網環境中會導致recv返回很慢,斷網狀態更是直接會卡在recv里無法返回。
解法方法其實不難,分兩步去做:
1、recv之前我們使用select或者poll去輪訓已經有數據的socket,若返回成功則在去recv即可。建議使用poll來做,因為select有fd的限制,若傳入select方法的socket fd大于當前系統的FD_SETSIZE則會直接導致crash,poll方法則沒有這個限制,或者使用epoll來做效率更高(如果涉及跨IOS開發則不可使用epoll,IOS系統暫時還不支持epoll方法)。
2、因為poll機制如果socket沒有數據則會返回0,通常外層會有一個while循環去一直poll,知道poll到數據返回,那如果弱網或者斷網一直poll不到數據還不是一樣會死循環?這個問題也好解決,我們看一下ffmpeg的做法就能得到答案:
在libavformat/network.c中已有答案:
// 可以看到這里只有3種情況 // 1.poll < 0出錯了,直接返回 // 2.ff_neterrno() : p.revents & (ev | POLLERR | POLLHUP)滿足,說明有數據可讀/寫,返回0,注意,這個方法進行了封裝,poll系統方法返回0是超時,這里返回0才是正常 // 3.其他情況都返回try again int ff_network_wait_fd(int fd, int write) {int ev = write ? POLLOUT : POLLIN;struct pollfd p = { .fd = fd, .events = ev, .revents = 0 };int ret;ret = poll(&p, 1, POLLING_TIME);return ret < 0 ? ff_neterrno() : p.revents & (ev | POLLERR | POLLHUP) ? 0 : AVERROR(EAGAIN); }int ff_network_wait_fd_timeout(int fd, int write, int64_t timeout, AVIOInterruptCB *int_cb) {int ret;int64_t wait_start = 0;// 循環去poll等待數據可讀之后退出whilewhile (1) {// 中斷機制,上層若有退出操作可以通過此中斷異步退出循環if (ff_check_interrupt(int_cb))return AVERROR_EXIT;// poll輪訓一直去等待socket有數據可讀ret = ff_network_wait_fd(fd, write);// 異常情況直接返回錯誤if (ret != AVERROR(EAGAIN))return ret;// 有個超時等待機制,如果太長時間都poll不到數據也要退出if (timeout > 0) {if (!wait_start)wait_start = av_gettime_relative();else if (av_gettime_relative() - wait_start > timeout)return AVERROR(ETIMEDOUT);}} }所以,我們可以通過異步中斷函數來退出循環,附上我的解法:
rtmp.c int RTMPSockBuf_Fill(RTMPSockBuf *sb, RTMPIOInterruptCB *cb) {int nBytes;if (!sb->sb_size)sb->sb_start = sb->sb_buf;while (1){if (RTMP_Read_Check_Interrupt(cb)){RTMP_Log(RTMP_LOGINFO, "%s, poll data, user interrupt!", __FUNCTION__);break;}nBytes = sizeof(sb->sb_buf) - 1 - sb->sb_size - (sb->sb_start - sb->sb_buf); #if defined(CRYPTO) && !defined(NO_SSL)if (sb->sb_ssl){nBytes = TLS_read(sb->sb_ssl, sb->sb_start + sb->sb_size, nBytes);}else #endif{int ret = RTMP_Poll_Fd(sb->sb_socket);if (ret < 0)return ret;else if (ret == EAGAIN)continue;elsenBytes = recv(sb->sb_socket, sb->sb_start + sb->sb_size, nBytes, 0);}if (nBytes != -1){sb->sb_size += nBytes;}else{int sockerr = GetSockError();RTMP_Log(RTMP_LOGDEBUG, "%s, recv returned %d. GetSockError(): %d (%s)",__FUNCTION__, nBytes, sockerr, strerror(sockerr));if (sockerr == EINTR && !RTMP_ctrlC)continue;if (sockerr == EWOULDBLOCK || sockerr == EAGAIN){sb->sb_timedout = TRUE;nBytes = 0;}}break;}return nBytes; }int RTMP_Read_Check_Interrupt(RTMPIOInterruptCB *cb) {if (cb && cb->callback && cb->callback(cb->opaque))return 1;elsereturn 0; }int RTMP_Poll_Fd(int fd) {short ev = POLLIN;struct pollfd p = { .fd = fd, .events = ev, .revents = 0 };int ret;ret = poll(&p, 1, kPollingTimeMs);return ret < 0 ? ret : p.revents & (ev | POLLERR | POLLHUP) ? 0 : EAGAIN; }頭文件:
typedef struct RTMP {RTMPIOInterruptCB m_interruptCallback; /* 讀包中斷函數,從調用librtmp的地方傳入 */ }/*** @brief 中斷結構** @param callback: callback方法* @param opaque: 上層傳入實例 */ typedef struct RTMPIOInterruptCB {int (*callback)(void*);void *opaque; } RTMPIOInterruptCB;/* 確認在讀包過程中,上層有沒有退出播放的操作 */ int RTMP_Read_Check_Interrupt(RTMPIOInterruptCB *cb); int RTMP_Poll_Fd(int fd);注意:
RTMPIOInterruptCB中的callback需要從其他的線程通知才可以,通常的播放器架構,會有一個主線程去處理各種回調的信息還有播放器的各類事件(如start、stop等),可在把callback接到該主線程,如果該主線程接到stop后讓該callback返回true即可正常退出。
總結
以上是生活随笔為你收集整理的librtmp读包阻塞问题修复的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 点云配准1:配准基础及icp算法
- 下一篇: 范围管理论文