移動直播的延時控制--基于ijkplayer的實踐

移動直播的興起使得在移動端觀看直播的需求日漸增多贸人,相交于點播而言间景,直播提出了一個新的要求——實時性,也即要求主播端至觀眾端的總延時不能過高艺智。而已有的移動端視頻播放器如: 系統(tǒng)播放器倘要、VLC和ijkplayer等開源播放器均是為了點播視頻播放而設計,雖能播放直播視頻十拣,但是不能降低直播端至終端的延時封拧。
針對以上問題,有必要以播放器為核心實現降低直播延時的功能夭问。

一. 什么是直播延遲

移動直播的基本架構圖如下所示:

圖1. 直播基本架構

移動直播整體架構大致可分為五個部分:

  1. 主播端泽西。主要負責音視頻數據的采集、預覽缰趋、處理(美聲捧杉、美顏、濾鏡等)秘血、編碼及將編碼后的數據推送至源站(可能經過上行加速節(jié)點)味抖。
  2. 源站。該部分屬于云服務的一項功能灰粮,接收來自主播端的音視頻數據仔涩,當來自CDN網絡(下行節(jié)點)的數據拉取請求時,按照對應的格式返回給CDN谋竖。同時也擔負將直播音視頻數據落盤红柱,生成點播回看視頻。
  3. 轉碼蓖乘。可以從源站拉取一路流韧骗,轉碼成多種分辨率嘉抒、碼率,再回推給源站袍暴。這樣實現了一路主播視頻流的推送些侍,制造出碼率不同的多路流。拉取器是轉碼的一種變種政模,它從其他源處拉取數據流(使用某種約定好的拉流協(xié)議)岗宣,并remux成rtmp推送給源站。
  4. CDN淋样。上下行節(jié)點都歸為數據分發(fā)網絡耗式。該部分屬于云服務的一項功能,多層下行節(jié)點從源站獲取直播音視頻數據,然后將數據分發(fā)給各地觀眾刊咳。
  5. 觀眾端彪见。從下行節(jié)點獲取直播數據,解析娱挨、解碼并渲染音視頻余指,以供觀眾觀看。

如果一幀畫面在主播側被采集時刻為t0跷坝,某觀眾屏幕上展示出這幀畫面的時候為t1酵镜,那么該觀眾能感知的延時為t1-t0。這個延時柴钻,我們叫直播延時淮韭。
主播端從攝像頭、麥克風采集音視頻數據顿颅,在移動端處理編碼后缸濒,經由源站、CDN直至觀眾端解碼并渲染播放整個鏈路引入的延時粱腻,直播延時涵蓋了整個鏈路的完整延時庇配。

二. 直播鏈路各模塊對延時的"貢獻"

直播延時大致可分為兩個部分:

  1. 音視頻數據在直播網絡鏈路傳輸所引入的延時,此部分無法避免绍些;
  2. 直播鏈路各模塊對音視頻數據的cache捞慌、process操作引入的延時,則可以采用一定方法降低甚至消除柬批;

下面將分析各模塊對于直播延時的"貢獻"

2.1 主播端

主播端在采集音視頻數據后基本流程如下所示:

圖2. 主播側基本流程
  1. 采集啸澡。首先使用麥克風采集音頻,使用攝像頭采集畫面氮帐。在此時嗅虏,打上對應的時間戳t0
  2. 處理上沐。音頻可以加上混響皮服。畫面可以做各種濾鏡處理。
  3. 混合参咙。將背景音樂龄广、麥克風聲音混合。將攝像頭畫面與背景圖蕴侧、連麥畫面等做圖層疊加择同。
  4. 預覽。預覽包括兩部分:
    • 耳返净宵。音頻處理后的數據敲才,即可送入耳返通道裹纳,此時主播能聽到t0時刻的聲音。主播耳朵提到對應聲音時刻為t1归斤。t1-t0表征了主播唱出一個詞痊夭,到耳朵里面聽到這個詞的耗時。主播對耳返延時的要求比較高脏里,該部分延時較小她我,大約40ms80ms
    • 畫面預覽迫横。 圖像混合后的數據番舆,接入主播屏幕畫面預覽,此時主播屏幕上的畫面渲染時刻為t1矾踱。一般來說t1t0延時極其小恨狈。如果t1-t0大于40ms,人眼即能有所感知delay呛讲。

如果Android設備不支持Low-Latency時禾怠,耳返功能本身耗時較大,大約300ms以上贝搁,但是并不影響直播整體延時吗氏。
關于Android耳返測試效果,請見鏈接雷逆。

  1. 編碼弦讽。如果直播準備采用30fps推流,那么視頻編碼需要達到至少30fps的性能膀哲。每幀編碼耗時需要控制在33ms以下往产。整個編碼的耗時除了單幀耗時,還有B幀參數數量某宪。編碼器配置和編碼性能會引入耗時仿村。
  2. 網絡自適應內部有個發(fā)送buffer,用于監(jiān)控網絡發(fā)送情況兴喂,并在網絡惡劣情況下丟掉待發(fā)送的碼流數據奠宜。基本邏輯如下(可以看到瞻想,只在網絡從良好到惡劣的轉變過程中,臨時引入延時):
    • 網絡良好時娩嚼,發(fā)送buffer內為空蘑险。該環(huán)節(jié)不引入延時。
    • 網絡惡劣時岳悟,發(fā)送buffer堆積佃迄,超過閾值觸發(fā)丟幀泼差。該環(huán)節(jié)引入固定延時。
    • 網絡惡劣時呵俏,監(jiān)控buffer堆積堆缘,反饋編碼器降低輸出碼率,buffer堆積情況轉好普碎,直至清空buffer吼肥。清空后延時歸零。
  3. 封包麻车。flv封包過程簡單缀皱,不引入延時。
  4. 發(fā)送动猬。對于不同的協(xié)議:
    • RTMP推流層不引入延時啤斗,客戶端tcp協(xié)議棧buffer延時很小,可以忽略赁咙。TCP引入的延時主要在高丟包钮莲、高重傳率網絡下,鏈路引入的延時彼水。
    • 基于UDP的私有推流協(xié)議崔拥,協(xié)議層可能引入buffer,依照實際情況而定猿涨。高丟包或者高重傳率的網絡情況下握童,鏈路延時UDP優(yōu)于TCP。

總結就是叛赚,推流端經過不懈努力澡绩,除了突變的網絡情況臨時引入的buffer延時,推流SDK的延時主要是濾鏡處理(gpu性能相關)俺附、編碼性能引入的延時(cpu性能相關)肥卡。該延時一般在100ms左右。

2.2 上行節(jié)點

上行節(jié)點會透明轉發(fā)數據事镣,合理的上行加速步鉴,會降低主播直連源站的鏈路延時。

同時上行節(jié)點也支持就近分發(fā)璃哟,也能降低鏈路延時氛琢。

2.3 源站

源站在接收直播數據時會緩存該路直播的最新音視頻數據,一般為若干個GOP随闪,某CDN節(jié)點初次向源站請求某直播流數據時阳似,源站會將緩存的數據全部傳給該CDN節(jié)點。
在CDN已與源站建立鏈接并拉取該路直播的數據時铐伴,源站會將最新的數據轉發(fā)給CDN撮奏。

2.4 轉碼

轉碼服務從源站拉取直播流俏讹,并轉碼轉推回源站。此時會引入轉碼延時畜吊。實時轉碼延時一般會引入100ms-200ms延時泽疆。

2.5 拉取器

從其他數據源拉取直播流后,轉推到源站玲献。
如果拉取器與數據源帶寬滿足實時傳輸的前提下殉疼,延時主要依賴數據源的延時。

2.6 下行節(jié)點

在第一次接收到播放某直播流的請求后青自,CDN邊緣節(jié)點會通過CDN網絡拉取該直播流的數據并緩存最新若干*gop的數據株依,以便應答后續(xù)可能的播放請求。
當某一個觀眾端發(fā)起播放請求延窜,播放器在與CDN節(jié)點初次建立鏈接后恋腕,播放器會快速從CDN邊緣節(jié)點讀取其緩存數據直至讀取到最新數據。在播放器耗盡對應的gop緩存前逆瑞,下行節(jié)點引入了短暫的延時荠藤。
耗盡gop緩存的場景大致幾種:

  1. 播放端拉流速度足夠快,會很快耗盡該buffer获高;
  2. 播放端拉流速度和直播流碼率相差不大哈肖,該buffer長期位于CDN邊緣節(jié)點,該部分緩存無法清除念秧;
  3. 播放器拉流速度低于直播流碼率淤井,播放端頻繁卡頓,該buffer持續(xù)增長摊趾,觸發(fā)CDN邊緣節(jié)點對buffer的丟幀邏輯币狠。該場景的延時等于CDN邊緣節(jié)點的buffer最大閾值。該情況下砾层,觀眾端觀看體驗很差漩绵,應該通過客戶端監(jiān)控斷開連接并選擇更低碼率的直播流。

一般情況下肛炮,用戶場景主要在1場景下止吐,即觀眾拉流速度最大值大于直播流碼率。下文重點考慮該場景侨糟。

2.7 播放端

觀眾端開始播放某直播流碍扔,大量gop cache數據到了播放器內存,這部分緩存是影響直播延時的關鍵部分秕重。

舉個例子蕴忆,該直播流gop為3秒,CDN邊緣節(jié)點gop配置為6秒悲幅。觀眾端拉流速度足夠快套鹅,開播后,播放器內會出現6至9秒的音視頻數據汰具。

本文的核心考量是如何快速消耗這部分數據卓鹿,以達到降低直播延時的目的。

三. 延時控制思路

3.1 延時的說明

章節(jié)2.6留荔、2.7已經說明了原理吟孙,這里畫個圖說明一下。


圖3 player buffer example

圖中黃色箭頭是時間軸聚蝶,t0時刻首先到來杰妓。
為了方便舉例,先說前提條件:當前直播流是固定關鍵幀間隔碘勉,固定幀率30fps巷挥。在CDN邊緣節(jié)點,t0時刻到了第一個關鍵幀验靡。t1時刻到了第一個gop最后一幀倍宾。t2時刻到了第二個關鍵幀。t2-t0值為3秒胜嗓。t3-t2為1秒高职。t4是第二個gop最后一幀。t5時刻到了第三個關鍵幀辞州。當前CDN邊緣節(jié)點緩存配置為3秒怔锌。那么有如下結論:

  • t2-t0為關鍵幀間隔,值為3秒变过;
  • CDN buffer的最小數據長度為t0至t1埃元,即3秒緩存數據;
  • CDN buffer的最大數據長度為t0至t4牵啦,即6秒緩存數據亚情;
  • t5時刻關鍵幀的到來,會觸發(fā)t0至t1的整個gop從當前buffer中清空哈雏;

如果觀眾在t3時刻發(fā)起播放請求楞件,如果觀眾的拉流速度足夠快,從t0對應的關鍵幀到t3對應的視頻數據裳瘪,會快速轉移到播放器待解碼隊列中土浸。由于t3位于t2后一秒,即此時播放器待解碼隊列中cache了4秒音視頻數據彭羹,觀眾看到的畫面與主播畫面最小延時4秒(忽略了鏈路延時)黄伊。后續(xù)拉流的再次卡頓,會持續(xù)引入更多的延時派殷。

3.2 思路

緩存即延時还最,播放器緩存的數據即引入延時的關鍵點墓阀,將播放器的緩存快速消耗就能降低直播延時。有兩種方案可供選擇拓轻,各有優(yōu)劣:

  1. 倍速播放
    若想快速消耗播放器緩存的數據斯撮,則需要設置較高的播放倍速,可能導致音頻播放時有尖銳的聲音扶叉。
    播放倍速較低時不會有尖銳的聲音勿锅,但是持續(xù)時間較長。
  2. 丟棄數據
    此方案必須考慮音視頻數據各自的特性枣氧,即音頻數據可視情況隨意丟棄溢十,而視頻幀就必須考慮幀與幀之間的參考關系,不能隨意丟棄达吞。與此同時還需考慮音視頻同步的情況张弛,以免造成新的問題。

NetStream bufferTimeMax提供了播放RTMP/HTTP-FLV直播流時flash播放內核控制延時的思路宗挥,金山云多媒體團隊借鑒了該思路乌庶。
flash控制時延的思路是,當大于閾值bufferTimeMax時契耿,NetStream會根據當前延時的具體情況瞒大,audio播放速度提速1.5%6.25%。這個較小的提速搪桂,可以保證音頻下采樣引入的變聲無法察覺透敌。

四. 直播延時控制實踐

金山云多媒體SDK直播實踐中,降低直播延時采用的第二種方案踢械,該方案涉及播放器使用的音視頻同步策略酗电。下面將簡述播放器使用的音視頻同步策略視頻同步至音頻,即

  • 音頻解碼后分次將數據寫入播放音頻的對象内列,根據該音頻幀PTS及已寫入數據量更新音頻時間軸撵术;
  • 視頻解碼后將數據放入隊列,由視頻渲染線程從隊列中取一幀視頻话瞧,根據該視頻幀的PTS及音頻時間軸等信息判斷是否可渲染嫩与。

4.1 降低直播延遲的條件

文件解析后,播放器內部會有待解碼的音頻數據緩存隊列與視頻數據緩存隊列交排,根據現有的音視頻同步策略划滋,音頻時間軸是基準時間軸,音頻緩存隊列的可播放時長反映了播放器的緩存時長埃篓。因此可以

  • 使用音頻緩存隊列的可播放時長是否超過設定閾值做為判斷是否發(fā)起降低直播延遲的動作的條件处坪;
  • 音頻緩存隊列的可播放時長是否低于閾值作為判斷是否停止降低直播延遲的條件;

4.2 丟棄音頻數據

基于現有的音視頻同步策略,當音頻時間軸出現跳躍時視頻幀會使用最新的音頻時間軸做同步同窘,導致視頻快速渲染玄帕,也即多余的緩存被快速消耗。
下圖為降低直播延時時對音視頻數據操作的示意圖塞椎,豎直紅色虛線表示降低直播延時行為的開始與結束桨仿。

圖3. audio drop by live delay control

  • 音頻緩存隊列中首幀與尾幀的PTSaF、aL案狠,視頻緩存隊列中首幀與尾幀的PTSvF、vL钱雷,后續(xù)讀取的視頻幀的PTS為v1骂铁、v2等。正常情況下Video1罩抗、Video2的視頻數據大致分別同步至Audio1拉庵、Audio2的音頻數據。
  • 降低直播延遲期間新讀取到的音頻數據會被丟棄套蒂,也即藍色方塊所代表的音頻數據會被丟棄钞支。讀取到音頻幀Audio4時,音頻緩存隊列可播放時長已低于預設的閾值操刀,降低直播延時的行為結束烁挟,Audio4會被放入音頻緩存隊列
  • 從上圖可以看到,播放過程中音頻時間軸的發(fā)生了一次跳躍骨坑,在音頻幀ALast播放完畢時會繼續(xù)播放音頻幀Audio4撼嗓,音頻時間軸會跳躍至音頻幀Audio4PTS: a4
  • 視頻幀Video2會被解碼并等待渲染時已經開始播放音頻幀Audio4Video2會同步至Audio4欢唾,但此時音頻時間軸已經領先于視頻時間軸(a4 > v2)且警,導致Video2會被立刻渲染,同理于Video3礁遣。此過程持續(xù)至音頻時間軸與視頻時間軸的差值在閾值內

此方法會導致視頻快速渲染斑芜,出現類似于快進效果以及解碼后丟幀。

4.3 丟棄視頻數據

上一步驟講述了通過丟棄音頻數據快速消耗播放緩存數據以降低直播延時的方法祟霍,該方法會要求視頻解碼器的快速解碼杏头。
在降低直播延時的過程中,滿足一定條件的情況下是可以丟棄視頻數據的浅碾。
下圖為降低直播延時過程中丟棄音頻及視頻數據的示意圖:


圖4. video drop by live delay control
  • 在開始降低直播延時之后讀取的視頻數據均會先放入視頻緩存隊列大州,上圖中Video1Video2均為降低直播延時過程中讀取到的非關鍵視頻幀
    +Video3視頻幀為IDR幀(關鍵幀,此幀之后的視頻幀不能以此幀之前的視頻幀為參考幀)垂谢,此刻可查找視頻隊列厦画,DTS大于aL(音頻緩存隊列尾幀的PTS)的視頻幀可被丟棄,例如Video1Video2。然后將Video3放入視頻緩存隊列中
  • 這樣操作會使視頻內容與時間軸發(fā)生跳躍根暑。

降低直播延時的過程中力试,音頻緩存隊列尾幀ALast之后的音頻幀均會被丟棄,在讀到視頻的IDR幀時排嫌,將視頻緩存隊列中DTS大于音頻幀ALastPTS的視頻幀丟棄畸裳,可視為丟棄與已丟棄音頻對應的視頻幀〈镜兀可避免出現只丟棄音頻幀時視頻畫面快進的效果怖糊。

五. ijkplayer代碼實踐

本節(jié)會基于ijkplayer最新版本k0.8.4,簡要介紹降低直播延時功能的關鍵代碼實現颇象。本節(jié)后續(xù)代碼默認諸位讀者對ijkplayer的基本結構伍伤、核心結構體與關鍵函數有基本的認識,對ijkplayer不熟悉的同學可以參考文章ijkplayer架構深入剖析遣钳。

5.1 基本定義

關于下述結構體的定義于文件 ijkmedia/ijkplayer/ff_ffplay_def.h

struct VideoState {
    int audio_stream;  // 音頻流索引
    PacketQueue audioq; // 音頻緩存隊列
    int video_stream; // 視頻流索引
    PacketQueue videoq; // 視頻緩存隊列
    int realtime; // 標志是否為直播視頻
    int chasing_status; // 標志是否開啟 降低直播延時功能
    int64_t latest_pts_in_audio_queue; // 音頻隊列尾幀的PTS

    int buffer_time_max; // 開始降低直播延時的閾值
};

struct FFPlayer {
    VideoState *is;
    FFStatistic stat;
}

5.2 狀態(tài)管理與丟棄音頻數據

上文提到降低直播延時功能的開啟與關閉是以音頻緩存隊列可播放時長為基準扰魂,因此在播放過程中每次讀取到音頻數據之后需判斷音頻緩存隊列可播放時長,開啟蕴茴、關閉降低直播延時的操作或無操作劝评。
文件ijkmedia/ijkplayer/ff_ffplay.c中函數read_thread

static int read_thread(void *arg) {
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    AVFormatContext *ic = NULL;
    AVPacket pkt1, *pkt = &pkt1;
    int ret, pkt_in_play_range = 0;
    // ...

    ret = av_read_frame(ic, pkt);
    // ...
    if (is->realtime && pkt->stream_index == is->audio_stream) {
        // 開啟降低直播延時功能
        if( ffp->stat.audio_cache.duration > ffp->buffer_time_max) {
            is->chasing_status = 1;
            if(is->audioq.last_pkt)
                is->latest_pts_in_audio_queue = is->audioq.last_pkt->pkt.pts;
            else
                is->latest_pts_in_audio_queue = pkt->pts;
        }
        // 關閉降低直播延時的功能
        if (is->chasing_status && ffp->stat.audio_cache.duration < ffp->i_buffer_time_max) {
            is->chasing_status = 0;
            is->latest_pts_in_audio_queue = INT64_MAX;
        }
        // 丟棄音頻數據
        if (is->chasing_status)
            pkt_in_play_range = 0;
    }
}

5.3 丟棄視頻數據

文件ijkmedia/ijkplayer/ff_ffplay.c中函數read_thread

static void packet_queue_flush_by_dts(PacketQueue *q, int64_t dts) {
    // 實現根據輸入dts丟棄PacketQueue里的相應數據
}

static int read_thread(void *arg) {
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    AVFormatContext *ic = NULL;
    AVPacket pkt1, *pkt = &pkt1;
    int ret, pkt_in_play_range = 0倦淀;
    // ...

    ret = av_read_frame(ic, pkt);
    // ...
    // 丟棄視頻數據
    if (is->realtime && pkt->stream_index == is->video_stream) {
        if (pkt->flags & ((pkt->flags & AV_PKT_FLAG_KEY) == AV_PKT_FLAG_KEY)) {
            if (is->chasing_status) 
                packet_queue_flush_by_dts(&is->videoq, is->latest_pts_in_audio_queue);
        }
    }
}

六. 結語

通過以上介紹的方法就實現了降低直播延時的功能蒋畜,在探索實現降低直播延時的過程中遇到不少坑,這樣的實現方案只對有音頻的直播視頻有效晃听,對純視頻的直播沒有效果百侧,后續(xù)會改進此不足之處。

轉載請注明:
作者金山視頻云能扒,首發(fā)簡書 Jianshu.com


也歡迎大家使用我們的直播佣渴、短視頻SDK。金山云SDK倉庫地址:
https://github.com/ksvc

金山云SDK相關的QQ交流群:

  • 視頻云技術交流群:574179720
  • 視頻云Android技術交流:6200036233
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末初斑,一起剝皮案震驚了整個濱河市辛润,隨后出現的幾起案子,更是在濱河造成了極大的恐慌见秤,老刑警劉巖砂竖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異鹃答,居然都是意外死亡乎澄,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門测摔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來置济,“玉大人解恰,你說我怎么就攤上這事≌阌冢” “怎么了护盈?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長羞酗。 經常有香客問我腐宋,道長,這世上最難降的妖魔是什么檀轨? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任胸竞,我火速辦了婚禮,結果婚禮上参萄,老公的妹妹穿的比我還像新娘撤师。我一直安慰自己,他們只是感情好拧揽,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腺占,像睡著了一般淤袜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衰伯,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天铡羡,我揣著相機與錄音,去河邊找鬼意鲸。 笑死烦周,一個胖子當著我的面吹牛,可吹牛的內容都是我干的怎顾。 我是一名探鬼主播读慎,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼槐雾!你這毒婦竟也來了夭委?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤募强,失蹤者是張志新(化名)和其女友劉穎株灸,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體擎值,經...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡慌烧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了鸠儿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屹蚊。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出淑翼,到底是詐尸還是另有隱情腐巢,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布玄括,位于F島的核電站冯丙,受9級特大地震影響,放射性物質發(fā)生泄漏遭京。R本人自食惡果不足惜胃惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哪雕。 院中可真熱鬧船殉,春花似錦、人聲如沸斯嚎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堡僻。三九已至糠惫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钉疫,已是汗流浹背硼讽。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留牲阁,地道東北人固阁。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像城菊,于是被迫代替她去往敵國和親备燃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內容