直播中的首屏加載優化

直播中的首屏加載時間指的是進入直播間時從播放器加載到第一幀畫面渲染出來的時間,這個時間是直播體驗中的一項重要的指標。這篇文章就簡要介紹一下優化直播首屏加載時間的一些經驗。

客戶端業務側優化

說到優化,首先要看客戶端上進入直播間的業務場景是什么樣的?一般而言,都是從一個直播列表頁面,點擊某一個直播卡片(Cell)即進入直播間。這個過程中,數據流是怎么走的呢?最簡單的做法是,從直播列表頁點擊某個直播卡片到直播間后,從服務器請求直播流地址以及各種直播間信息(主播信息、聊天信息、點贊信息、禮物信息等等),拿到直播流地址后,交給播放器播放。

在這個過程中,我們可以看到播放器必須等到進入直播間請求到直播流地址后才能開始播放,這個時間點其實是可以提前的:我們可以在直播列表頁就拿到每個直播間對應的直播流地址,在進入直播間時直接傳過去,這樣一進入直播間播放器就可以拿著直播流地址開始播放了,省去了從服務器請求直播流地址的時間(雖然這個時間可能沒多少)。

甚至,我們可以在直播列表頁當滑到一個卡片就讓播放器拿著直播流地址預加載,進入直播間時則直接展示畫面。

另外,客戶端業務側還可以在進入直播間之前通過 HTTPDNS 來選擇網絡情況最好的 CDN 節點,在進入直播間時從最好的節點拉取直播流播放從而優化網絡加載的時間,加快首屏渲染。

流媒體服務器側優化

協議的選擇

當前流行的直播拉流協議主要有 RTMP 和 HTTP-FLV。經過大量的測試發現,移動端拉流時在相同的 CDN 策略以及播放器控制策略的條件下,HTTP-FLV 協議相比 RTMP 協議,首屏時間要減少 300~400ms 左右。主要是在 RTMP 協議建連過程中,與服務端的交互耗時會更久。所以我們應該優先使用 HTTP-FLV 協議。

服務端 GOP 緩存

除了客戶端業務側的優化外,我們還可以從流媒體服務器側進行優化。我們都知道直播流中的圖像幀分為: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 幀。

通常我們可以在 CDN 的邊緣節點做 GOP 緩存。

服務端快速下發策略

快速啟動優化則是會在 GOP 緩存基本上根據播放器緩沖區大小設定一定的 GOP 數量用于填充播放器緩沖區。

這個優化項并不是客戶端播放器來控制的,而是在 CDN 服務端來控制下發視頻數據的帶寬和速度。因為緩沖區耗時不僅跟緩沖需要的幀數有關,還跟下載數據的速度優化,以網宿 CDN 為例,他們可以配置快速啟動后,在拉取直播流時,服務端將以 5 倍于平時帶寬的速度下發前面緩存的 1s 的數據,這樣的效果除了首屏速度更快以外,首屏秒開也會更穩定,因為有固定 1s 的緩存快速下發。這個優化的效果可以使首屏秒開速度提升 100ms 左右。

直播播放器側優化

耗時分析

HTTP-FLV 協議就是專門拉去 FLV 文件流的 HTTP 協議,所以它的請求流程就是一個 HTTP 的下載流程,如下圖:




從上圖中可以看出,首屏耗時的組成主要以下基本組成:

DNS 解析耗時

TCP 建連耗時

HTTP 響應耗時

音視頻流探測耗時

Buffer 填充耗時

下面我們來分別從這幾個方面討論如何優化。

優化 DNS 解析耗時

DNS 解析是網絡請求的第一步,在我們用基于 FFmpeg 實現的播放器 ffplay 中,所有的 DNS 解析請求都是 FFmpeg 調用?getaddrinfo?方法來獲取的。

我們如何在 FFmpeg 中統計 DNS 耗時呢?

可以在?libavformat/tcp.c?文件中的?tcp_open?方法中,按以下方法統計:

int64_t start = av_gettime();

if (!hostname[0])

? ? ret = getaddrinfo(NULL, portstr, &hints, &ai);

else

? ? ret = getaddrinfo(hostname, portstr, &hints, &ai);

int64_t end = av_gettime();

如果在沒有緩存的情況下,實測發現一次域名的解析會花費至少 300ms 左右的時間,有時候更長,如果本地緩存命中,耗時很短,幾個 ms 左右,可以忽略不計。緩存的有效時間是在DNS 請求包的時候,每個域名會配置對應的緩存 TTL 時間,這個時間不確定,根據各域名的配置,有些長有些短,不確定性比較大。

為什么 DNS 的請求這么久呢?一般理解,DNS 包的請求,會先到附近的運營商的 DNS 服務器上查找,如果沒有,會遞歸到根域名服務器,這個耗時就很久。一般如果請求過一次,這些服務器都會有緩存,而且其他人也在不停的請求,會持續更新,下次再請求的時候就會比較快。

在測試 DNS 請求的過程中,有時候通過抓包發現每次請求都會去請求?A?和?AAAA?查詢,這是去請求 IPv6 的地址,但由于我們的域名沒有 IPv6 的地址,所以每次都要回根域名服務器去查詢。為什么會請求 IPV6 的地址呢,因為 FFmpeg 在配置 DNS 請求的時候是按如下配置的:

hints.ai_family = AF_UNSPEC;

它是一個兼容 IPv4 和 IPv6 的配置,如果修改成?AF_INET,那么就不會有?AAAA?的查詢包了。通過實測發現,如果只有 IPv4 的請求,即使是第一次,也會在 100ms 內完成,后面會更短。這里是一個優化點,但是要考慮將來兼容 IPv6 的問題。

DNS 的解析一直以來都是網絡優化的首要問題,不僅僅有時間解析過長的問題,還有小運營商 DNS 劫持的問題。采用 HTTPDNS 是優化 DNS 解析的常用方案,不過 HTTPDNS 在部分地區也可能存在準確性問題,綜合各方面可以采用 HTTPDNS 和 LocalDNS 結合的方案,來提升解析的速度和準確率。大概思路是,App 啟動的時候就預先解析我們指定的域名,因為拉流域名是固定的幾個,所以完全可以先緩存在 App 本地。然后會根據各個域名解析的時候返回的有效時間,過期后再去解析更新緩存。至于 DNS 劫持的問題,如果 LocalDNS 解析出來的 IP 無法正常使用,或者延時太高,就切換到 HTTPDNS 重新解析。這樣就保證了每次真正去拉流的時候,DNS 解析的耗時幾乎為 0,因為可以定時更新緩存池,使每次獲得的 DNS 都是來自緩存池。

那么怎么去實現 HTTPDNS 呢?

方案一:IP 直連。

假設原直播流的 URL 是:http://www.example.com/abc.flv。假設從 HTTPDNS 服務獲取的 www.example.com 這個 Host 對應的 IP 是:192.168.1.1。那么處理后的 URL 是:http://192.168.1.1/abc.mp4。如果直接用這個 URL 去發起 HTTP 請求,有些情況可以成功,但很多情況是不行的。如果這個 IP 的機器只部署了 www.example.com 對應的服務,就能解析出來,如果有多個域名的服務,CDN 節點就無法正確的解析。這個時候一般需要設置 HTTP 請求的 header 里面的 Host 字段。

AVDictionary **dict = ffplayer_get_opt_dict(ffplayer, opt_category);

av_dict_set(dict, "headers", "Host: www.example.com", 0);

但是這個方案有兩個問題:

1)服務端采用 302/307 跳轉的方式調度資源,則 IP 直連會有問題。

如果在客戶端發出請求(如:http://www.example.com/abc.flv)的時候,服務端是通過 302/307 調度方式返回直播資源的真實地址(如:http://www.realservice.com/abc.flv),這時 IP 直連會有問題。因為客戶端并不知道跳轉邏輯,而客戶端做了 IP 直連,用的是 www.example.com 獲取到的直連 IP 并替換成了 http://192.168.1.1/abc.mp4,這個請求到達服務器,服務器又沒有對應的資源,則會導致錯誤。這種情況可以讓服務端采用不下發 302 跳轉的方式,但這樣就不通用了,會給將來留下隱患。所以常見的做法是做一層播控服務,客戶端請求播控服務獲取到實際的播放地址以及各種其他的信息,然后再走 IP 直連就沒問題。

還可以參考:iOS 302 等重定向業務場景IP直連方案說明

2)使用 HTTPS 時,IP 直連會有問題。

這種方案在使用 HTTPS 時,是會失敗的。因為 HTTPS 在證書驗證的過程,會出現 domain 不匹配導致 SSL/TLS 握手不成功。這時候的方案參考?HTTPS(含SNI)業務場景“IP直連”方案說明?和?iOS HTTPS SNI 業務場景“IP直連”方案說明

方案二:替換 FFmpeg 的 DNS 實現。

另一種方案是替換原來的 DNS 解析的實現。在 FFmpeg 中即替換掉?tcp.c?中?getaddreinfo?方法,這個方法就是實際解析 DNS 的方法,比如下面代碼:

if (my_getaddreinfo) {

? ? ret = my_getaddreinfo(hostname, portstr, &hints, &ai);

} else {

? ? ret = getaddrinfo(hostname, portstr, &hints, &ai);

}

在?my_getaddreinfo?中可以自己實現 HTTPDNS 的解析邏輯從而優化原來的 DNS 解析速度。

總體來說,DNS 優化后,直播首屏時間能減少 100ms~300ms 左右,特別是針對很多首次打開,或者 DNS 本地緩存過期的情況下,能有很好的優化效果。

優化 TCP 建連耗時

TCP 建連耗時在這里即調用 Socket 的 connect 方法建立連接的耗時,它是一個阻塞方法,它會一直等待 TCP 的三次握手完成。它直接反應了客戶端到 CDN 服務器節點的點對點延時情況,實測在一般的 Wifi 網絡環境下耗時在 50ms 以內,基本是沒有太大的優化空間,不過它的時間反應了客戶端的網絡情況或者客戶端到節點的網絡情況。

要統計這段耗時,可以在 libavformat/tcp.c 文件中的?tcp_open?方法中,按以下方法統計:

int64_t start = av_gettime();

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 (ret == AVERROR_EXIT)

? ? ? ? goto fail1;

? ? else

? ? ? ? goto fail;

}

int64_t end = av_gettime();

TCP 連接耗時可優化的空間主要是針對建連節點鏈路的優化,主要受限于三個因素影響:用戶自身網絡條件、用戶到 CDN 邊緣節點中間鏈路的影響、CDN 邊緣節點的穩定性。因為用戶網絡條件有比較大的不可控性,所以優化主要會在后面兩個點。可以結合著用戶所對應的城市、運營商的情況,同時結合優化服務端的 CDN 調度體系,給用戶下發更合適的 CDN 服務域名,然后通過 HTTPDNS SDK 來優化 DNS 解析的結果。

優化 HTTP 響應耗時

HTTP 響應耗時是指客戶端發起一個 HTTP Request 請求,然后等待 HTTP 響應的 Header 返回這部分耗時。直播拉流 HTTP-FLV 協議也是一個 HTTP 請求,客服端發起請求后,服務端會先將 HTTP 的響應頭部返回,不帶音視頻流的數據,響應碼如果是 200,表明視頻流存在,緊接著就開始下發音視頻數據。HTTP 響應耗時非常重要,它直接反應了 CDN 服務節點處理請求的能力。它與 CDN 節點是否有緩存這條流有關,如果在請求之前有緩存這條流,節點就會直接響應客戶端,這個時間一般也在 50ms 左右,最多不會超過 200ms,如果沒有緩存,節點則會回直播源站拉取直播流,耗時就會很久,至少都在 200ms 以上,大部分時間都會更長,所以它反應了這條直播流是是冷流還是熱流,以及 CDN 節點的緩存命中情況。

如果需要統計它的話,可以在 libavformat/http.c 文件中的?http_open?方法:

int64_t start = av_gettime();

ret = http_open_cnx(h, options);

int64_t end = av_gettime();

通常 CDN 的緩存命中策略是與訪問資源的 URL 有關。如果命中策略是 URL 全匹配,那么就要盡量保證 URL 的變化性較低。比如:盡量不要在 URL 的參數中帶上隨機性的值,這樣會造成 CDN 緩存命中下降,從而導致不斷回源,這樣訪問資源耗時也就增加了。當然這樣就失去了一些靈活性。

CDN 方面其實可以提供一些配置策略,比如:根據域名可配置對其緩存命中策略忽略掉某些參數。這樣就能保證一定的靈活性了。

優化音視頻流探測耗時

當我們做直播業務時,播放端需要一個播放器來播放視頻流,當一個播放器支持的視頻格式有很多種時,問題就來了。一個視頻流來了,播放器是不清楚這個視頻流是什么格式的,所以它需要去探測到一定量的視頻流信息,去檢測它的格式并決定如何去處理它。這就意味著在播放視頻前有一個數據預讀過程和一個分析過程。但是對于我們的直播業務來說,我們的提供的直播方案通常是固定的,這就意味著視頻流的格式通常是固定的,所以一些數據預讀和分析過程是不必要的。在直播流協議格式固定的情況下,只需要讀取固定的信息即可開始播放。這樣就縮短了數據預讀和分析的時間,使得播放器能夠更快地渲染出首屏畫面。

基于 FFmpeg 實現的播放器,在播放視頻時都會調用到一個?avformat_find_stream_info?(libavformat/utils.c) 函數,該函數的作用是讀取一定長度的碼流數據,來分析碼流的基本信息,為視頻中各個媒體流的 AVStream 結構體填充好相應的數據。這個函數中做了查找合適的解碼器、打開解碼器、讀取一定的音視頻幀數據、嘗試解碼音視頻幀等工作,基本上完成了解碼的整個流程。這時一個同步調用,在不清楚視頻數據的格式又要做到較好的兼容性時,這個過程是比較耗時的,從而會影響到播放器首屏秒開。

可以在 ijkplayer 的工程中 ff_ffplay.c 文件中的?read_thread?方法統計其耗時:

int64_t start = av_gettime();

avformat_find_stream_info(ic, opts);

int64_t end = av_gettime();

在外部可以通過設置?probesize?和?analyzeduration?兩個參數來控制該函數讀取的數據量大小和分析時長為比較小的值來降低?avformat_find_stream_info?的耗時,從而優化播放器首屏秒開。但是,需要注意的是這兩個參數設置過小時,可能會造成預讀數據不足,無法解析出碼流信息,從而導致播放失敗、無音頻或無視頻的情況。所以,在服務端對視頻格式進行標準化轉碼,從而確定視頻格式,進而再去推算?avformat_find_stream_info?分析碼流信息所兼容的最小的?probesize?和?analyzeduration,就能在保證播放成功率的情況下最大限度地區優化首屏秒開。

在我們能控制視頻格式達到標準化后,我們可以直接修改?avformat_find_stream_info?的實現邏輯,針對該視頻格式做優化,進而優化首屏秒開。

在 FFmpeg 中的?utils.c?文件中的函數實現中有一行代碼是?int fps_analyze_framecount = 20;,這行代碼的大概用處是,如果外部沒有額外設置這個值,那么?avformat_find_stream_info?需要獲取至少 20 幀視頻數據,這對于首屏來說耗時就比較長了,一般都要 1s 左右。而且直播還有實時性的需求,所以沒必要至少取 20 幀。你可以試試將這個值初始化為 0 看看效果。在開發中,我們可以去掉這個條件來實現優化:

av_dict_set_int(&ffp->format_opts, "fpsprobesize", 0, 0);

這樣,avformat_find_stream_info?的耗時就可以縮減到 100ms 以內。

甚至,我們可以進一步直接去掉?avformat_find_stream_info?這個過程,自定義完成解碼環境初始化。參見:VLC優化(1) avformat_find_stream_info 接口延遲降低?和?FFMPEG avformat_find_stream_info 替換

對?avformat_find_stream_info?代碼的分析,還可以看看這里:FFmpeg源代碼簡單分析:avformat_find_stream_info()

優化 Buffer 填充耗時

緩沖耗時是指播放器的緩沖的數據達到了預先設定的閾值,可以開始播放視頻了。這個值是可以動態設置的,所以不同的設置給首屏帶來的影響是不一樣的。

緩沖耗時的統計方法,不像前面幾個那么簡單,因為它涉及到的代碼有多處,所以需要在多個地方計時。開始計時可以直接從?avformat_find_stream_info?后面開始,結束計時可以在第一幀視頻渲染出來的時候結束。

avformat_find_stream_info(ic, opts);

start = av_gettime();

...

if (!ffp->first_video_frame_rendered) {

? ? ffp->first_video_frame_rendered = 1;

? ? ffp_notify_msg1(ffp, FFP_MSG_VIDEO_RENDERING_START);

? ? end = av_gettime();

}

優化一:調整?BUFFERING_CHECK_PER_MILLISECONDS?設置。

緩沖區填充耗時跟播放器里面的一個設置?BUFFERING_CHECK_PER_MILLISECONDS?值有關,因為播放器 check 緩沖區的數據是否達到目標值不是隨意檢測的,因為 check 本身會有一定的浮點數運算,所以 ijkplayer 最初給他設置了 500ms 時間間隔去定時檢查,這個時間明顯比較大,所以會對緩沖耗時有比較大的影響。可以把這個值改小一些。

#define BUFFERING_CHECK_PER_MILLISECONDS? ? ? ? (500)

這個值會在 ijkplayer 工程中?ff_ffplay.c?文件中的?read_thread?方法中用到:

if (ffp->packet_buffering) {

? ? io_tick_counter = SDL_GetTickHR();

? ? if (abs((int)(io_tick_counter - prev_io_tick_counter)) > BUFFERING_CHECK_PER_MILLISECONDS){

? ? ? ? prev_io_tick_counter = io_tick_counter;

? ? ? ? ffp_check_buffering_l(ffp);

? ? }

}

從這個代碼邏輯中可以看出,每次調用?ffp_check_buffering_l?去檢查 buffer 是否滿足條件的時間間隔是 500ms 左右,如果剛好這次只差一幀數據就滿足條件了,那么還需要再等 500ms 才能再次檢查了。這個時間,對于直播來說太長了。我們當前的做法是降低到 50ms,從實測效果來看平均可以減少 200ms 左右。

優化二:調整?MIN_MIN_FRAMES?設置。

另外一個跟緩沖區相關的設置是?MIN_MIN_FRAMES,其對應的使用邏輯在?ffp_check_buffering_l(ffp)?函數中:

#define MIN_MIN_FRAMES? ? ? 10

if (is->buffer_indicator_queue && is->buffer_indicator_queue->nb_packets > 0) {

? ? if (? (is->audioq.nb_packets > MIN_MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request)

? ? ? ? && (is->videoq.nb_packets > MIN_MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request)) {

? ? ? ? printf("ffp_check_buffering_l buffering end \n");

? ? ? ? ffp_toggle_buffering(ffp, 0);

? ? }

}

這里大概的意思需要緩沖的數據至少要有 11 幀視頻和 11 個音頻數據包,才能離開緩沖區開始播放。音頻數據很容易滿足條件,因為如果采樣率是 44.1k 的音頻,那么 1s 的數據平均有 44 個音頻包,0.25s 的數據就能達到 11 個音頻包。但對于視頻,如果是 24 幀的幀率,至少需要 0.4s 左右的數據才能達到 11 幀。如果視頻采集的編碼幀率較低(美顏、AR 情況下由于處理消耗較大可能采集的幀率較低),只有 10~15,那就需要接近 1s 的數據才能達到 11 幀,緩沖區需要這么多數據才能開始播放,這個時長太大。

緩沖區里達到這么多數據時,實際上播放器已經下載了多少數據呢?我們深入?ff_ffplay.c?源碼可以看到視頻解碼后會放到一個?frame_queue?里面,用于渲染數據。可以看到視頻數據的流程是這樣的:下載緩沖區 -> 解碼 -> 渲染緩沖區 -> 渲染。其中渲染的緩沖區就是?frame_queue。下載的數據會先經過解碼線程將數據輸出到?frame_queue?中,然后等?frame_queue?隊列滿了,才開始渲染。在?ff_ffplay.c?中,可以找到如下代碼:

#define VIDEO_PICTURE_QUEUE_SIZE_MIN? ? ? ? (3)

#define VIDEO_PICTURE_QUEUE_SIZE_MAX? ? ? ? (16)

#define VIDEO_PICTURE_QUEUE_SIZE_DEFAULT? ? (VIDEO_PICTURE_QUEUE_SIZE_MIN)

ffp->pictq_size = VIDEO_PICTURE_QUEUE_SIZE_DEFAULT; // option

/* start video display */

if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)

? ? goto fail;

所以目前來看,如果設置?MIN_MIN_FRAMES?為 10,播放器開始播放時至少有 14 幀視頻。對于低幀率的視頻來說,也相當大了。在實踐中我們把它調整到 5,首屏時間減少了 300ms 左右,并且卡頓率只上升了 2 個百分點左右。

參考

SRS 低延時直播應用

美拍直播優化

FFmpeg之直播 IP 拉流優化

快手直播優化

移動直播技術秒開優化經驗

低延時超清直播傳輸技術

Blog

iOS 開發知識索引

實現 iOS UIView 及其 Subview 透明區域的事件穿透

讀《怪誕行為學》

Andrew Ng 機器學習課程筆記

讀《未來簡史》(4)

Opinion

育兒之道

聊聊抖音

聊聊直播和短視頻

用微信紅包彈一首「兩只老虎」

動態圖帶你體驗微信小程序

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容