本文由「逆流的魚yuiop」原創分享於「何俊林」公衆號,感謝做者的無私分享。php
直播行業的競爭愈來愈激烈,進過2018年這波洗牌後,已經度過了蠻荒暴力期,剩下的都是在不斷追求體驗。最近正好在作直播首開優化工做,實踐中經過多種方案並行,已經能把首開降到500ms如下,藉此機會分享出來,但願能對你們有所啓發。html
本文內容的技術前提:git
1)基於FFmpeg的ijkplayer,最新版本0.88版本;github
2)拉流協議基於http-flv。web
http-flv更穩定些,國內大部分直播公司基本都是使用http-flv了,從咱們實際數據來看,http-flv確實稍微更快些。可是考慮到會有rtmp源,這塊也加了些優化。小程序
(本文同步發佈於:http://www.52im.net/thread-2087-1-1.html)微信小程序
《實現延遲低於500毫秒的1080P實時音視頻直播的實踐分享》服務器
《移動端實時視頻直播技術實踐:如何作到實時秒開、流暢不卡》微信
《技術揭祕:支持百萬級粉絲互動的Facebook實時視頻直播》
《理論聯繫實際:實現一個簡單地基於HTML5的實時視頻直播》
《首次披露:快手是如何作到百萬觀衆同場看直播仍能秒開且不卡頓的?》
《七牛雲技術分享:使用QUIC協議實現實時視頻直播0卡頓!》
簡單理解就是,把域名替換成IP。好比https://www.baidu.com/,你能夠直接換成14.215.177.39,這樣作的目的是,省去了DNS解析的耗時,尤爲在網絡很差時,訪問域名,域名要去解析,再給你返回。不只僅有時間解析過長的問題,還有小運營商DNS劫持的問題。通常就是在啓動應用時,就開始對拉流的域名進行預解析好,存到本地,而後在真正拉流時,直接用就行。典型的案列,就是不少人使用HTTPDNS,這個github上也有開源,能夠自行去研究下。
須要注意的是:這種方案在使用 HTTPS 時,是會失敗的。由於 HTTPS 在證書驗證的過程,會出現 domain 不匹配致使 SSL/TLS 握手不成功。
除了客戶端業務側的優化外,咱們還能夠從流媒體服務器側進行優化。
咱們都知道直播流中的圖像幀分爲:I 幀、P 幀、B 幀,其中只有 I 幀是能不依賴其餘幀獨立完成解碼的,這就意味着當播放器接收到 I 幀它能立刻渲染出來,而接收到 P 幀、B 幀則須要等待依賴的幀而不能當即完成解碼和渲染,這個期間就是「黑屏」了。
因此,在服務器端能夠經過緩存 GOP(在 H.264 中,GOP 是封閉的,是以 I 幀開頭的一組圖像幀序列),保證播放端在接入直播時能先獲取到 I 幀立刻渲染出畫面來,從而優化首屏加載的體驗。
這裏有一個 IDR 幀的概念須要講一下,全部的 IDR 幀都是 I 幀,可是並非全部 I 幀都是 IDR 幀,IDR 幀是 I 幀的子集。
I 幀嚴格定義是幀內編碼幀,因爲是一個全幀壓縮編碼幀,一般用 I 幀表示「關鍵幀」。IDR 是基於 I 幀的一個擴展,帶了控制邏輯,IDR 圖像都是 I 幀圖像,當解碼器解碼到 IDR 圖像時,會當即將參考幀隊列清空,將已解碼的數據所有輸出或拋棄。從新查找參數集,開始一個新的序列。這樣若是前一個序列出現重大錯誤,在這裏能夠得到從新同步的機會。IDR 圖像以後的圖像永遠不會使用 IDR 以前的圖像的數據來解碼。
在 H.264 編碼中,GOP 是封閉式的,一個 GOP 的第一幀都是 IDR 幀。
通常播放器須要拿到一個完整的GOP,才能記性播放。GOP是在推流端能夠設置,好比下面這個圖,是我dump一個流,看到的GOP狀況。GOP大小是50,推流過來的fps設置是25,也就是1s內會顯示25個Frame,50個Frame,恰好直播設置GOP 2S,可是直播通常fps不用設置這麼高,能夠隨便dump任何一家直播公司的推流,設置fps在15-18之間就夠了。
當set一個源給播放器後,播放器須要open這個流,而後和服務端創建長鏈接,而後demux,codec,最後渲染。
咱們能夠按照播放器的四大塊,依次優化:
1)數據請求耗時;
2)解複用耗時;
3)解碼耗時;
4)渲染出圖耗時
這裏就是網絡和協議相關。不管是http-flv,仍是rtmp,都主要是基於tcp的,因此必定會有tcp三次握手,同時打開tcp.c分析。須要加日誌在一些方法中,以下tcp_open方法。
下面是已經改動過的代碼:
/* return non zero if error */
staticinttcp_open(URLContext *h, constchar*uri, intflags)
{
av_log(NULL, AV_LOG_INFO, "tcp_open begin");
...省略部分代碼
if(!dns_entry) {
#ifdef HAVE_PTHREADS
av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock begin.\n");
ret = ijk_tcp_getaddrinfo_nonblock(hostname, portstr, &hints, &ai, s->addrinfo_timeout, &h->interrupt_callback, s->addrinfo_one_by_one);
av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock end.\n");
#else
if(s->addrinfo_timeout > 0)
av_log(h, AV_LOG_WARNING, "Ignore addrinfo_timeout without pthreads support.\n");
av_log(h, AV_LOG_INFO, "getaddrinfo begin.\n");
if(!hostname[0])
ret = getaddrinfo(NULL, portstr, &hints, &ai);
else
ret = getaddrinfo(hostname, portstr, &hints, &ai);
av_log(h, AV_LOG_INFO, "getaddrinfo end.\n");
#endif
if(ret) {
av_log(h, AV_LOG_ERROR,
"Failed to resolve hostname %s: %s\n",
hostname, gai_strerror(ret));
returnAVERROR(EIO);
}
cur_ai = ai;
} else{
av_log(NULL, AV_LOG_INFO, "Hit DNS cache hostname = %s\n", hostname);
cur_ai = dns_entry->res;
}
restart:
#if HAVE_STRUCT_SOCKADDR_IN6
// workaround for IOS9 getaddrinfo in IPv6 only network use hardcode IPv4 address can not resolve port number.
if(cur_ai->ai_family == AF_INET6){
structsockaddr_in6 * sockaddr_v6 = (structsockaddr_in6 *)cur_ai->ai_addr;
if(!sockaddr_v6->sin6_port){
sockaddr_v6->sin6_port = htons(port);
}
}
#endif
fd = ff_socket(cur_ai->ai_family,
cur_ai->ai_socktype,
cur_ai->ai_protocol);
if(fd < 0) {
ret = ff_neterrno();
gotofail;
}
/* Set the socket's send or receive buffer sizes, if specified.
If unspecified or setting fails, system default is used. */
if(s->recv_buffer_size > 0) {
setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &s->recv_buffer_size, sizeof(s->recv_buffer_size));
}
if(s->send_buffer_size > 0) {
setsockopt (fd, SOL_SOCKET, SO_SNDBUF, &s->send_buffer_size, sizeof(s->send_buffer_size));
}
if(s->listen == 2) {
// multi-client
if((ret = ff_listen(fd, cur_ai->ai_addr, cur_ai->ai_addrlen)) < 0)
gotofail1;
} elseif(s->listen == 1) {
// single client
if((ret = ff_listen_bind(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,
s->listen_timeout, h)) < 0)
gotofail1;
// Socket descriptor already closed here. Safe to overwrite to client one.
fd = ret;
} else{
ret = av_application_on_tcp_will_open(s->app_ctx);
if(ret) {
av_log(NULL, AV_LOG_WARNING, "terminated by application in AVAPP_CTRL_WILL_TCP_OPEN");
gotofail1;
}
if((ret = ff_listen_connect(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,
s->open_timeout / 1000, h, !!cur_ai->ai_next)) < 0) {
if(av_application_on_tcp_did_open(s->app_ctx, ret, fd, &control))
gotofail1;
if(ret == AVERROR_EXIT)
gotofail1;
else
gotofail;
} else{
ret = av_application_on_tcp_did_open(s->app_ctx, 0, fd, &control);
if(ret) {
av_log(NULL, AV_LOG_WARNING, "terminated by application in AVAPP_CTRL_DID_TCP_OPEN");
gotofail1;
} elseif(!dns_entry && strcmp(control.ip, hostname_bak)) {
add_dns_cache_entry(hostname_bak, cur_ai, s->dns_cache_timeout);
av_log(NULL, AV_LOG_INFO, "Add dns cache hostname = %s, ip = %s\n", hostname_bak , control.ip);
}
}
}
h->is_streamed = 1;
s->fd = fd;
if(dns_entry) {
release_dns_cache_reference(hostname_bak, &dns_entry);
} else{
freeaddrinfo(ai);
}
av_log(NULL, AV_LOG_INFO, "tcp_open end");
return0;
// 省略部分代碼
}
改動地方主要是hints.ai_family = AF_INET;,原來是 hints.ai_family = AF_UNSPEC;,原來設計是一個兼容IPv4和IPv6的配置,若是修改爲AF_INET,那麼就不會有AAAA的查詢包了。若是隻有IPv4的請求,就能夠改爲AF_INET。固然有IPv6,這裏就不要動了。這麼看是否有,能夠經過抓包工具看。
接着分析,咱們發現tcp_read函數是個阻塞式的,會很是耗時,咱們又不能設置短一點中斷時間,由於短了的話,形成讀取不到數據,就中斷,後續播放就直接失敗了,這裏只能讓它等。
不過仍是優化的點是下面部分:
static int tcp_read(URLContext *h, uint8_t *buf, intsize)
{
av_log(NULL, AV_LOG_INFO, "tcp_read begin %d\n", size);
TCPContext *s = h->priv_data;
intret;
if(!(h->flags & AVIO_FLAG_NONBLOCK)) {
ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
if(ret)
return ret;
}
ret = recv(s->fd, buf, size, 0);
if(ret == 0)
returnAVERROR_EOF;
//if (ret > 0)
// av_application_did_io_tcp_read(s->app_ctx, (void*)h, ret);
av_log(NULL, AV_LOG_INFO, "tcp_read end %d\n", ret);
returnret < 0 ? ff_neterrno() : ret;
}
咱們能夠把上面兩行註釋掉,由於在ff_network_wait_fd_timeout等回來後,數據能夠放到buf中,下面av_application_did_io_tcp_read就不必去執行了。原來每次ret>0,都會執行av_application_did_io_tcp_read這個函數。
在日誌中發現,數據請求到後,進行音視頻分離時,首先須要匹配對應demuxer,其中ffmpeg的av_find_input_format和avformat_find_stream_info很是耗時,前者簡單理解就是打開某中請求到數據,後者就是探測流的一些信息,作一些樣本檢測,讀取必定長度的碼流數據,來分析碼流的基本信息,爲視頻中各個媒體流的 AVStream 結構體填充好相應的數據。這個函數中作了查找合適的解碼器、打開解碼器、讀取必定的音視頻幀數據、嘗試解碼音視頻幀等工做,基本上完成了解碼的整個流程。這時一個同步調用,在不清楚視頻數據的格式又要作到較好的兼容性時,這個過程是比較耗時的,從而會影響到播放器首屏秒開。
這兩個函數調用都在ff_ffplay.c的read_thread函數中:
if(ffp->iformat_name) {
av_log(ffp, AV_LOG_INFO, "av_find_input_format noraml begin");
is->iformat = av_find_input_format(ffp->iformat_name);
av_log(ffp, AV_LOG_INFO, "av_find_input_format normal end");
}
elseif(av_stristart(is->filename, "rtmp", NULL)) {
av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp begin");
is->iformat = av_find_input_format("flv");
av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp end");
ic->probesize = 4096;
ic->max_analyze_duration = 2000000;
ic->flags |= AVFMT_FLAG_NOBUFFER;
}
av_log(ffp, AV_LOG_INFO, "avformat_open_input begin");
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
av_log(ffp, AV_LOG_INFO, "avformat_open_input end");
if(err < 0) {
print_error(is->filename, err);
ret = -1;
gotofail;
}
ffp_notify_msg1(ffp, FFP_MSG_OPEN_INPUT);
if(scan_all_pmts_set)
av_dict_set(&ffp->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);
if((t = av_dict_get(ffp->format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
#ifdef FFP_MERGE
ret = AVERROR_OPTION_NOT_FOUND;
gotofail;
#endif
}
is->ic = ic;
if(ffp->genpts)
ic->flags |= AVFMT_FLAG_GENPTS;
av_format_inject_global_side_data(ic);
if(ffp->find_stream_info) {
AVDictionary **opts = setup_find_stream_info_opts(ic, ffp->codec_opts);
intorig_nb_streams = ic->nb_streams;
do{
if(av_stristart(is->filename, "data:", NULL) && orig_nb_streams > 0) {
for(i = 0; i < orig_nb_streams; i++) {
if(!ic->streams[i] || !ic->streams[i]->codecpar || ic->streams[i]->codecpar->profile == FF_PROFILE_UNKNOWN) {
break;
}
}
if(i == orig_nb_streams) {
break;
}
}
ic->probesize=100*1024;
ic->max_analyze_duration=5*AV_TIME_BASE;
ic->fps_probe_size=0;
av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info begin");
err = avformat_find_stream_info(ic, opts);
av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info end");
} while(0);
ffp_notify_msg1(ffp, FFP_MSG_FIND_STREAM_INFO);
最終改的如上,主要是對rtmp增長了,指定format爲‘flv’,以及樣本大小。
同時在外部能夠經過設置 probesize 和 analyzeduration 兩個參數來控制該函數讀取的數據量大小和分析時長爲比較小的值來下降 avformat_find_stream_info的耗時,從而優化播放器首屏秒開。可是,須要注意的是這兩個參數設置太小時,可能會形成預讀數據不足,沒法解析出碼流信息,從而致使播放失敗、無音頻或無視頻的狀況。因此,在服務端對視頻格式進行標準化轉碼,從而肯定視頻格式,進而再去推算 avformat_find_stream_info分析碼流信息所兼容的最小的probesize和 analyzeduration,就能在保證播放成功率的狀況下最大限度地區優化首屏秒開。
在 FFmpeg 中的 utils.c 文件中的函數實現中有一行代碼是 int fps_analyze_framecount = 20;,這行代碼的大概用處是,若是外部沒有額外設置這個值,那麼 avformat_find_stream_info 須要獲取至少 20 幀視頻數據,這對於首屏來講耗時就比較長了,通常都要 1s 左右。並且直播還有實時性的需求,因此不必至少取 20 幀。
將這個值初始化爲2,看看效果:
/* check if one codec still needs to be handled */
for(i = 0; i < ic->nb_streams; i++) {
intfps_analyze_framecount = 2;
st = ic->streams[i];
if(!has_codec_parameters(st, NULL))
break;
if(ic->metadata) {
AVDictionaryEntry *t = av_dict_get(ic->metadata, "skip-calc-frame-rate", NULL, AV_DICT_MATCH_CASE);
if(t) {
intfps_flag = (int) strtol(t->value, NULL, 10);
if(!st->r_frame_rate.num && st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0 && fps_flag > 0) {
intavg_fps = st->avg_frame_rate.num / st->avg_frame_rate.den;
if(avg_fps > 0 && avg_fps <= 120) {
st->r_frame_rate.num = st->avg_frame_rate.num;
st->r_frame_rate.den = st->avg_frame_rate.den;
}
}
}
}
這樣,avformat_find_stream_info 的耗時就能夠縮減到 100ms 之內。
最後就是解碼耗時和渲染出圖耗時,這塊優化空間不多,大頭都在前面。
有人開始拋出問題了,你這個起播快是快,可是後面網絡很差,卡頓怎麼辦?直播中會引發卡頓,主要是網絡有抖動的時候,沒有足夠的數據來播放,ijkplayer會激發其緩衝機制。
主要是有幾個宏控制:
DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS:網絡差時首次去喚醒read_thread函數去讀取數據。
DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS:第二次去喚醒read_thread函數去讀取數據。
DEFAULT_LAST_HIGH_WATER_MARK_IN_MS這個宏的意思是最後的機會去喚醒read_thread函數去讀取數據。
能夠設置DEFAULT_LAST_HIGH_WATER_MARK_IN_MS爲1 * 1000,也即緩衝1秒後開始通知緩衝完成去讀取數據,默認是5秒,若是過大,會讓用戶等過久,那麼每次讀取的bytes也能夠少些。能夠設置DEFAULT_HIGH_WATER_MARK_IN_BYTES小一些,設置爲30 * 1024,默認是256 * 1024。
把BUFFERING_CHECK_PER_MILLISECONDS設置爲50,默認是500:
#define DEFAULT_HIGH_WATER_MARK_IN_BYTES (30 * 1024)
#define DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS (100)
#define DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS (1 * 1000)
#define DEFAULT_LAST_HIGH_WATER_MARK_IN_MS (1 * 1000)
#define BUFFERING_CHECK_PER_BYTES (512)
#define BUFFERING_CHECK_PER_MILLISECONDS (50)
能夠看下這些宏使用的地方:
inline static void ffp_reset_demux_cache_control(FFDemuxCacheControl *dcc)
{
dcc->min_frames = DEFAULT_MIN_FRAMES;
dcc->max_buffer_size = MAX_QUEUE_SIZE;
dcc->high_water_mark_in_bytes = DEFAULT_HIGH_WATER_MARK_IN_BYTES;
dcc->first_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS;
dcc->next_high_water_mark_in_ms = DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS;
dcc->last_high_water_mark_in_ms = DEFAULT_LAST_HIGH_WATER_MARK_IN_MS;
dcc->current_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS;
}
最後優化的點,是設置一些參數值,也能優化一部分,實際上不少直播用軟件用低分辨率240,甚至360,來達到秒開,能夠能夠做爲一個減小耗時點來展開的,由於分辨率越低,數據量越少,首開越快。
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max_delay", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max-buffer-size", 4* 1024);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 50);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probsize", "1024");
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "100");
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);
//靜音
//mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "an", 1);
//重連模式,若是中途服務器斷開了鏈接,讓它從新鏈接
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
以上完了後,就能夠看下測試數據,分辨率在540p如下基本秒開,在4G網絡下測試:
1)河北衛視直播源,測試10組,平均下來300ms。一組數據386ms,以下:
11-17 14:17:46.659 9896 10147 D IJKMEDIA: IjkMediaPlayer_native_setup
11-17 14:17:46.663 9896 10147 V IJKMEDIA: setDataSource: path [url=http://weblive.hebtv.com/live/hbws_bq/index.m3u8]http://weblive.hebtv.com/live/hbws_bq/index.m3u8[/url]
11-17 14:17:46.666 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_open_input begin
11-17 14:17:46.841 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_open_input end
11-17 14:17:46.841 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_find_stream_info begin
11-17 14:17:46.894 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_find_stream_info end
11-17 14:17:47.045 9896 10191 D IJKMEDIA: Video: first frame decoded
11-17 14:17:47.046 9896 10175 D IJKMEDIA: FFP_MSG_VIDEO_DECODED_START:
2)映客直播秀場源,測試10組,平均下來400ms。一組數據418ms,以下:
11-17 14:21:32.908 11464 11788 D IJKMEDIA: IjkMediaPlayer_native_setup
11-17 14:21:32.952 11464 11788 V IJKMEDIA: setDataSource: path [flash]http://14.215.100.45/hw.pull.inke.cn/live/1542433669916866_0_ud.flv[/flash]?ikDnsOp=1001&ikHost=hw&ikOp=0&codecInfo=8192&ikLog=1&ikSyncBeta=1&dpSrc=6&push_host=trans.push.cls.inke.cn&ikMinBuf=2900&ikMaxBuf=3600&ikSlowRate=0.9&ikFastRate=1.1
11-17 14:21:32.996 11464 11818 I FFMPEG : [FFPlayer @ 0xc2575c00] avformat_open_input begin
11-17 14:21:33.161 11464 11818 I FFMPEG : [FFPlayer @ 0xc2575c00] avformat_open_input end
11-17 14:21:33.326 11464 11829 D IJKMEDIA: Video: first frame decoded
3)熊貓直播遊戲直播源,測試10組,平均下來350ms。一組數據373ms,以下:
11-17 14:29:17.615 15801 16053 D IJKMEDIA: IjkMediaPlayer_native_setup
11-17 14:29:17.645 15801 16053 V IJKMEDIA: setDataSource: path [flash]http://flv-live-qn.xingxiu.panda.tv/panda-xingxiu/dc7eb0c2e78c96646591aae3a20b0686.flv[/flash]
11-17 14:29:17.649 15801 16079 I FFMPEG : [FFPlayer @ 0xeb5ef000] avformat_open_input begin
11-17 14:29:17.731 15801 16079 I FFMPEG : [FFPlayer @ 0xeb5ef000] avformat_open_input end
11-17 14:29:17.988 15801 16090 D IJKMEDIA: Video: first frame decoded
[1] 實時音視頻開發的其它精華資料:
《即時通信音視頻開發(五):認識主流視頻編碼技術H.264》
《即時通信音視頻開發(九):實時語音通信的迴音及迴音消除概述》
《即時通信音視頻開發(十):實時語音通信的迴音消除技術詳解》
《即時通信音視頻開發(十一):實時語音通信丟包補償技術詳解》
《即時通信音視頻開發(十三):實時視頻編碼H.264的特色與優點》
《即時通信音視頻開發(十五):聊聊P2P與實時音視頻的應用狀況》
《即時通信音視頻開發(十六):移動端實時音視頻開發的幾個建議》
《即時通信音視頻開發(十七):視頻編碼H.26四、VP8的前世此生》
《學習RFC3550:RTP/RTCP實時傳輸協議基礎知識》
《基於RTMP數據傳輸協議的實時流媒體技術研究(論文全文)》
《還在靠「喂喂喂」測試實時語音通話質量?本文教你科學的評測方法!》
《實現延遲低於500毫秒的1080P實時音視頻直播的實踐分享》
《技術揭祕:支持百萬級粉絲互動的Facebook實時視頻直播》
《理論聯繫實際:實現一個簡單地基於HTML5的實時視頻直播》
《首次披露:快手是如何作到百萬觀衆同場看直播仍能秒開且不卡頓的?》
《騰訊音視頻實驗室:使用AI黑科技實現超低碼率的高清實時視頻聊天》
《七牛雲技術分享:使用QUIC協議實現實時視頻直播0卡頓!》
《實時視頻直播客戶端技術盤點:Native、HTML五、WebRTC、微信小程序》
《微信多媒體團隊訪談:音視頻開發的學習、微信的音視頻技術和挑戰等》
《以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
>> 更多同類文章 ……
[2] 開源實時音視頻技術WebRTC的文章:
《訪談WebRTC標準之父:WebRTC的過去、如今和將來》
《良心分享:WebRTC 零基礎開發者教程(中文)[附件下載]》
《新手入門:到底什麼是WebRTC服務器,以及它是如何聯接通話的?》
《[觀點] WebRTC應該選擇H.264視頻編碼的四大理由》
《基於開源WebRTC開發實時音視頻靠譜嗎?第3方SDK有哪些?》
《開源實時音視頻技術WebRTC中RTP/RTCP數據傳輸協議的應用》
《開源實時音視頻技術WebRTC在Windows下的簡明編譯教程》
《網頁端實時音視頻技術WebRTC:看起來很美,但離生產應用還有多少坑要填?》
《了不得的WebRTC:生態日趨完善,或將實時音視頻技術白菜化》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
>> 更多同類文章 ……
(本文同步發佈於:http://www.52im.net/thread-2087-1-1.html)