Android音視頻之MediaCodec

簡介

從 API 16開始巫延,Android提供了MediaCodec類以便開發(fā)者更加靈活的處理音視頻的編解碼,較MeidaPlay提供了更加豐富黎茎、完善的操作接口囊颅。具體詳見:這里

正文

MediaCodec類可用于訪問Android底層的媒體編解碼器。例如:編碼/解碼組件。它是Android為多媒體支持提供的底層接口的一部分,通常(MediaExtractor踢代、MediaSync先鱼、MediaMuxer、MediaCrypto奸鬓、MediaDrm、Image掸读、Surface 和AudioTrack)一起使用串远。

編解碼流程

一個編解碼器可以處理輸入的數(shù)據(jù)來產(chǎn)生輸出的數(shù)據(jù),編解碼器使用一組輸入和輸出緩沖器來異步處理數(shù)據(jù)儿惫。你可以創(chuàng)建一個空的輸入緩沖區(qū)澡罚,填充數(shù)據(jù)后發(fā)送到編解碼器進行處理。編解碼器使用輸入的數(shù)據(jù)進行轉(zhuǎn)換肾请,然后輸出到一個空的輸出緩沖區(qū)留搔。最后你獲取到輸出緩沖區(qū)的數(shù)據(jù),消耗掉里面的數(shù)據(jù)铛铁,釋放回編解碼器隔显。如果后續(xù)還有數(shù)據(jù)需要繼續(xù)處理,編解碼器就會重復(fù)這些操作饵逐。輸出流程如下:

[圖片上傳失敗...(image-ad446d-1553137245958)]

Data Types

編解碼器處理三種類型的數(shù)據(jù):壓縮數(shù)據(jù)括眠、原始音頻數(shù)據(jù)、原始視頻數(shù)據(jù)倍权。上述三種數(shù)據(jù)類型都可以通過ByteBuffers進行處理掷豺。但是需要提供一個Surface來為提高編解碼體驗(顯示視頻圖像)。Surface直接使用本地視頻數(shù)據(jù)buffers薄声,而不是通過映射或復(fù)制的方式当船;因此,這樣做的顯得更加高效默辨。通常在使用Surface的時候你不能夠直接訪問原始視頻數(shù)據(jù)德频,{但是你可以使用ImageReader類來訪問不可靠的解碼后(或原始)的視頻幀}。這可能仍然比使用ByteBuffers更加有效率廓奕,一些原始的buffers可能已經(jīng)映射到了 direct ByteBuffers抱婉。當使用ByteBuffer模式,你可以通過使用Image類和getInput/OutputImage(int)來訪問到原始視頻數(shù)據(jù)幀桌粉。

Compressed Buffers

輸入的buffers(用于解碼)和輸出的buffers(用于編碼)包含的壓縮數(shù)據(jù)是由媒體格式?jīng)Q定的蒸绩。針對視頻類型是一個壓縮的單幀。針對音頻數(shù)據(jù)通常是一個單個可訪問單元(一個編碼后的音頻區(qū)段通常包含由特定格式類型決定的幾毫秒音頻數(shù)據(jù))铃肯,但這種通常也不是十分嚴格患亿,一個buffer可能包含多個可訪問的音頻單元。在這兩種情況下,buffers通常不開始或結(jié)束于任意的字節(jié)邊界步藕,而是結(jié)束于幀/可訪問單元的邊界惦界。

Raw Audio Buffers

原始的音頻數(shù)據(jù)buffers包含整個PCM音頻幀數(shù)據(jù),這是在通道順序下每一個通道的樣本咙冗。每一個樣本就是一個 16-bit signed integer in native byte order沾歪。

Raw Video Buffers

ByteBuffer模式下視頻buffers的的展現(xiàn)是由他們的 color format確定的。你可以通過調(diào)用 getCodecInfo().getCapabilitiesForType(…).colorFormats方法獲得其支持的顏色格式數(shù)組雾消。視頻編解碼器可能支持三種類型的顏色格式:

  • native raw video format: 被COLOR_FormatSurface標記灾搏,其可與輸入或輸出Surface一起使用。
  • flexible YUV buffers: 這些與輸入/輸出Surface一起使用,以及在ByteBuffer模式中立润,通過調(diào)用getInput/OutputImage(int)方法
  • other, specific formats: 通常只在ByteBuffer模式下被支持狂窑。有些顏色格式是特定供應(yīng)商指定的。其他的一些被定義在 MediaCodecInfo.CodecCapabilities中桑腮。顏色格式是一個很靈活的格式泉哈,你仍然可以使用 getInput/OutputImage(int)方法。

從LOLLIPOP_MR1 API起所有視頻編解碼器支持靈活的YUV 4:2:0 buffers.

States

從概念上講在整個生命周期中編解碼器對象存在于三種狀態(tài)之一:Stopped, Executing 或 Released破讨。整體的Stoped狀態(tài)實際是由三種狀態(tài)的集成:Uninitialized, Configured以及 Error丛晦,而從概念上將Executing狀態(tài)的執(zhí)行時通過三個子狀態(tài):Flushed, Running 以及 End-of-Stream。

[圖片上傳失敗...(image-5c907-1553137245958)]

當你使用工廠方法之一創(chuàng)建一個編解碼器的時候提陶,它的狀態(tài)是處于Uninitialized狀態(tài)采呐。首先,你需要通過configure(…)方法配置它搁骑,以此進入Configured 狀態(tài)斧吐。然后,通過調(diào)用start()方法轉(zhuǎn)入Executing 狀態(tài)仲器。在這個狀態(tài)下你可以通過上述buffer隊列操作過程數(shù)據(jù)煤率。

? Executing狀態(tài)包含三個子狀態(tài): Flushed, Running 以及 End-of-Stream。在調(diào)用start()方法后編解碼器立即進入Flushed 的子狀態(tài)乏冀,其同時包含所有的buffers蝶糯。當?shù)谝粋€輸入buffer一旦出隊列,編解碼器就轉(zhuǎn)入Running 的子狀態(tài)辆沦,這個狀態(tài)占了編解碼器的大部分生命周期時間昼捍。當你以end-of-stream marker標記一個入隊列的輸入buffer,則編解碼器就轉(zhuǎn)入End-of-Stream 子狀態(tài)肢扯。在這個狀態(tài)妒茬,編解碼器不在接收以后傳入的輸入buffers,但它仍然產(chǎn)生輸出buffers直到輸出buffer到達end-of-stream狀態(tài)。你可以在Executing狀態(tài)的任何時候通過調(diào)用flush()狀態(tài)返回Flushed 的子狀態(tài)蔚晨≌ё辏或者通過調(diào)用stop()方法返回編解碼器的Uninitialized 狀態(tài),因此這個編解碼器需要再次configured 。當你使用完編解碼器后银择,你必須調(diào)用release()方法釋放其資源多糠。

? 在極少情況下編解碼器可能會遇到錯誤并進入Error 狀態(tài)。這個錯誤可能是在隊列操作時返回一個錯誤的值或者有時候產(chǎn)生了一個異常導(dǎo)致的浩考。通過調(diào)用reset()方法使編解碼器再次可用夹孔。你可以在任何狀態(tài)調(diào)用reset()方法使編解碼器返回Uninitialized 狀態(tài)。否則析孽,調(diào)用release()方法進入最終的Released 狀態(tài)析蝴。

Creation

通過MediaCodecList創(chuàng)建一個指定MediaFormat的MediaCodec對象。在解碼文件或流時绿淋,你可以通過調(diào)用MediaExtractor.getTrackFormat方法獲得所需的格式。通過調(diào)用MediaFormat.setFeatureEnabled方法你可以注入任意想要添加的特定特性尝盼,然后調(diào)用MediaCodecList.findDecoderForFormat方法獲得可以處理這種特定媒體格式的編解碼器的名字吞滞。最后,通過調(diào)用createByCodecName(String)方法創(chuàng)建一個編解碼器盾沫。

注意裁赠,在API LOLLIPOP上,傳遞給MediaCodecList.findDecoder/EncoderForFormat的格式必須不能包含幀率赴精。通過調(diào)用format.setString(MediaFormat.KEY_FRAME_RATE, null)方法清除任何存在于當前格式中的幀率佩捞。

你也可以通過調(diào)用createDecoder/EncoderByType(String)方法創(chuàng)建一個首選的MIME類型的編解碼器。然而蕾哟,不能夠用于注入特性一忱,以及創(chuàng)建了一個不能處理期望的特定媒體格式的編解碼器。

Creating secure decoders

在版本API KITKAT_WATCH 及以前谭确,secure 編解碼器在MediaCodecList中沒有列出來帘营,但是仍然可以在這個系統(tǒng)中使用。secure 編解碼器的存在只能夠通過名字實例化逐哈,通過在通常的編解碼器添加".secure"(所有的secure 解碼器名稱必須以".secure"結(jié)尾)芬迄,如果系統(tǒng)上不存在指定的編解碼器則createByCodecName(String)方法將拋出一個IOException 異常。

Initialization

在創(chuàng)建了編解碼器后昂秃,如果你想異步地處理數(shù)據(jù)那么可以通過setCallback方法設(shè)置一個回調(diào)方法禀梳。然后,通過指定的媒體格式configure 這個編解碼器肠骆。這段時間你可以為視頻原始數(shù)據(jù)產(chǎn)生者(例如視頻解碼器)指定輸出Surface算途。此時你也可以為secure 編解碼器設(shè)置解碼參數(shù)(詳見MediaCrypto) 。最后蚀腿,因為有些編解碼器可以操作于多種模式郊艘,你必須指定是想讓他作為一個解碼器或編碼器運行。

從API LOLLIPOP起,你可以在Configured 狀態(tài)查詢輸入和輸出格式的結(jié)果纱注。在開始編解碼前你可以通過這個結(jié)果來驗證配置的結(jié)果畏浆,例如,顏色格式狞贱。

如果你想通過視頻處理者處理原始輸入視頻buffers刻获,一個處理原始視頻輸入的編解碼器,例如視頻編碼器瞎嬉,在配置完成后通過調(diào)用createInputSurface()方法為你的輸入數(shù)據(jù)創(chuàng)建一個目標Surface蝎毡。通過先前創(chuàng)建的persistent input surface調(diào)用setInputSurface(Surface)配置這個編解碼器。

Codec-specific Data

? 有些格式氧枣,特別是ACC音頻和MPEG4沐兵,H.264和H.265視頻格式要求以包含特定數(shù)量的構(gòu)建數(shù)據(jù)buffers或者codec-specific數(shù)據(jù)為前綴的實際數(shù)據(jù)。當處理這樣的壓縮格式時便监,這些數(shù)據(jù)必須在start()方法后和任何幀數(shù)據(jù)之前提交給編解碼器扎谎。這些數(shù)據(jù)必須在調(diào)用queueInputBuffer方法時用BUFFER_FLAG_CODEC_CONFIG標記。

? Codec-specific數(shù)據(jù)也可以被包含在傳遞給configure的ByteBuffer的格式里面烧董,包含的keys是 "csd-0", "csd-1"等毁靶。這些keys通常包含在通過MediaExtractor獲得的軌道MediaFormat中。這個格式中的Codec-specific數(shù)據(jù)將在接近start()方法時自動提交給編解碼器逊移;你不能顯示的提交這些數(shù)據(jù)预吆。如果這個格式不包含編解碼器指定的數(shù)據(jù),你也可以選擇在這個編解碼器中以這個格式所要求的并以正確的順序傳遞特定數(shù)量的buffers來提交這些數(shù)據(jù)胳泉。還有拐叉,你也可以連接所有的codec-specific數(shù)據(jù)并作為一個單獨的codec-config buffer提交。

? Android 使用以下codec-specific數(shù)據(jù)buffers扇商。{這些也被要求在軌道配置的格式軌道屬性MediaMuxer中進行配置}巷嚣。所有設(shè)置的參數(shù)以及被標記為(*)的codec-specific-data必須以 "\x00\x00\x00\x01"字符開頭。

[圖片上傳失敗...(image-e8373c-1553137245958)]

注意:當編解碼器被立即flushed 或start之后不久钳吟,并且在任何輸出buffer或輸出格式變化被返回前需要特別地小心廷粒,編解碼器的code specific 數(shù)據(jù)可能會在flush過程中丟失。為保證編解碼器的正常運行红且,你必須在刷新后通過buffers再次提交被標記為BUFFER_FLAG_CODEC_CONFIGbuffers的這些數(shù)據(jù)坝茎。

? 編碼器(或者產(chǎn)生壓縮數(shù)據(jù)的編解碼器)將在任何合法的輸出buffer前創(chuàng)建并返回被標記為 codec-config flag的codec specific data 。包含codec-specific-data 的Buffers含有沒有意義的時間戳暇番。

Data Processing

API中每一個編解碼器維護一組被一個buffer-ID引用的輸入和輸出buffers嗤放。當成功調(diào)用start()方法后客戶端將“擁有”輸入和輸出buffers。在同步模式下壁酬,通過調(diào)用dequeueInput/OutputBuffer(…) 從編解碼器獲得(具有所有權(quán)的)一個輸入或輸出buffer次酌。在異步模式下恨课,你可以通過MediaCodec.Callback.onInput/OutputBufferAvailable(…)的回調(diào)方法自動地獲得可用的buffers.

在獲得一個輸入buffer,在使用解密方式下通過queueInputBuffer或queueSecureInputBuffer向編解碼器填充相應(yīng)數(shù)據(jù)。不要提交多個具有相同時間戳的輸入bufers(除非他的codec-specific 數(shù)據(jù)時那樣標記的)岳服。

在異步模式下剂公,編解碼器將通過onOutputBufferAvailable的回調(diào)返回一個只讀的輸出buffer,或者在同步模式下響應(yīng)dequeuOutputBuffer的調(diào)用吊宋。在輸出buffer被處理后纲辽,調(diào)用releaseOutputBuffer方法中其中一個將這個buffer返回給編解碼器。

在異步模式下璃搜,編解碼器將通過onOutputBufferAvailable的回調(diào)返回一個只讀的輸出buffer拖吼,或者在同步模式下響應(yīng)dequeuOutputBuffer的調(diào)用。在輸出buffer被處理后这吻,調(diào)用releaseOutputBuffer方法中其中一個將這個buffer返回給編解碼器吊档。

你不需要立即向編解碼器重新提交或釋放buffers,{獲得的輸入或輸出buffers可能失去編解碼器}唾糯,當然這些行為依賴于設(shè)備情況怠硼。具體地說,編解碼器可能推遲產(chǎn)生輸出buffers直到輸出的buffers被釋放或重新提交趾断。因此,盡可能保存可用的buffers吩愧。

根據(jù)API版本情況芋酌,你有三種方式處理相關(guān)數(shù)據(jù):

[圖片上傳失敗...(image-a3f2ee-1553137245958)]

Asynchronous Processing using Buffers

從LOLLIPOP API版本開始,首選的異步處理數(shù)據(jù)的方法是通過在調(diào)用configure前設(shè)置異步的回調(diào)方法雁佳。異步模式將間接地修改狀態(tài)轉(zhuǎn)換情況脐帝,因為你必須在flush()方法后調(diào)用start()方法將編解碼器的狀態(tài)轉(zhuǎn)換為Running 子狀態(tài)并開始接收輸入buffers。同樣糖权,初始化調(diào)用start方法將編解碼器的狀態(tài)直接變化為Running 子狀態(tài)并通過回調(diào)方法開始傳遞可用的輸入buufers堵腹。

[圖片上傳失敗...(image-5790fe-1553137245958)]

Synchronous Processing using Buffers

從LOLLIPOP API版本開始,在同步模式下使用編解碼器你應(yīng)該通過getInput/OutputBuffer(int) 和/或 getInput/OutputImage(int) 檢索輸入和輸出buffers星澳。這允許通過框架進行某些優(yōu)化疚顷,例如,在處理動態(tài)內(nèi)容過程中禁偎。如果你調(diào)用getInput/OutputBuffers()方法這種優(yōu)化是不可用的腿堤。

注意,不要在同時使用buffers和buffer時產(chǎn)生混淆如暖。特別地笆檀,僅僅在調(diào)用start()方法后或取出一個值為 INFO_OUTPUT_FORMAT_CHANGED的輸出buffer ID后你才可以直接調(diào)用getInput/OutputBuffers方法。

End-of-stream Handling

當?shù)竭_輸入數(shù)據(jù)的結(jié)尾盒至,你必須向這個編解碼器在調(diào)用queueInputBuffer方法中指定BUFFER_FLAG_END_OF_STREAM 來標記輸入數(shù)據(jù)酗洒。你可以在最后一個合法的輸入buffer上做這些操作士修,或者提交一個以 end-of-stream 標記的額外的空的輸入buffer。如果使用一個空的buffer,它的時間戳將被忽略樱衷。

編解碼器將繼續(xù)返回輸出buffers棋嘲,直到在這個設(shè)置在indequeueOutputBuffer 里的 MediaCodec.BufferInfo 中被同樣標記為 end-of-stream 的輸出流結(jié)束的時候或者通過onOutputBufferAvailable返回。這些可以被設(shè)置在最后一個合法的輸出buffer上箫老,或者在最后一個合法的buffer后的一個空buffer封字。那樣的空buffer的時間戳將被忽略。

不要在輸入流被標記為結(jié)束后提交額外的輸入buffers耍鬓,除非這個編解碼器被flushed阔籽,或者stopped 和restarted。

Using an Output Surface

在使用一個輸出Surface時牲蜀,其數(shù)據(jù)處理基本上與處理ByteBuffer模式相同笆制。然而,這個輸出buffers將不可訪問涣达,并且被描述為null值在辆。例如,調(diào)用getOutputBuffer/Image(int)將返回null度苔,以及調(diào)用getOutputBuffers()將返回一個只包含null-s的數(shù)組匆篓。

Using an Input Surface

當使用輸入Surface時,將沒有可訪問的輸入buffers,因為這些buffers將會從輸入surface自動地向編解碼器傳輸寇窑。調(diào)用dequeueInputBuffer時將拋出一個IllegalStateException鸦概,調(diào)用getInputBuffers()將要返回一個不能寫入的假的ByteBUffer[]數(shù)組。調(diào)用signalEndOfInputStream() 方法標記end-of-stream甩骏。調(diào)用這個方法后窗市,輸入surface將會立即停止向編解碼器提交數(shù)據(jù)。

Seeking & Adaptive Playback Support

{視頻解碼器(通常是消費壓縮視頻數(shù)據(jù)的編解碼器)關(guān)于seek和格式變化的行為是不同的饮笛,不管他們是否支持以及被配置為adaptive playback}咨察。你可以通過調(diào)用CodecCapabilities.isFeatureSupported(String)方法來檢查解碼器是否支持adaptive playback 。只有在編解碼器被配置在Surface上解碼時支持Adaptive playback播放的解碼器才被激活福青。

Stream Boundary and Key Frames

在調(diào)用start()或flush()方法后輸入數(shù)據(jù)以合適的流邊界開始是非常重要的:其第一幀必須是關(guān)鍵幀摄狱。一個關(guān)鍵幀能夠通過其自身完全解碼(針對大多數(shù)編解碼器它是一個I幀),沒有幀能夠在關(guān)鍵幀之前或之后顯示无午。

下面的表格針對不同的格式總結(jié)了合適的關(guān)鍵幀二蓝。

[圖片上傳失敗...(image-56fcbf-1553137245958)]

MediaCodec API 說明

MediaCodec可以處理具體的視頻流,主要有這幾個方法:

  • configure:配置為編碼器
  • start:成功地配置組件后指厌,調(diào)用start方法刊愚。
  • getInputBuffers:獲取需要編碼數(shù)據(jù)的輸入流隊列,返回的是一個ByteBuffer數(shù)組
  • queueInputBuffer:輸入流入隊列
  • dequeueInputBuffer:從輸入流隊列中取數(shù)據(jù)進行編碼操作
  • getOutputBuffers:獲取編解碼之后的數(shù)據(jù)輸出流隊列踩验,返回的是一個ByteBuffer數(shù)組
  • dequeueOutputBuffer:從輸出隊列中取出編碼操作之后的數(shù)據(jù)
  • releaseOutputBuffer:處理完成鸥诽,釋放ByteBuffer數(shù)據(jù)
  • stop:完成解碼/編碼任務(wù)后商玫,需注意的是codec任然處于活躍狀態(tài)且準備重新start。
  • flush:沖洗組件的輸入和輸出端口
  • release:釋放codec實例使用的資源牡借。
  • reset:使codec返回到初始(未初始化)狀態(tài)拳昌。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市钠龙,隨后出現(xiàn)的幾起案子炬藤,更是在濱河造成了極大的恐慌,老刑警劉巖碴里,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沈矿,死亡現(xiàn)場離奇詭異,居然都是意外死亡咬腋,警方通過查閱死者的電腦和手機羹膳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來根竿,“玉大人陵像,你說我怎么就攤上這事】芸牵” “怎么了醒颖?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長壳炎。 經(jīng)常有香客問我泞歉,道長,這世上最難降的妖魔是什么冕广? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任疏日,我火速辦了婚禮偿洁,結(jié)果婚禮上撒汉,老公的妹妹穿的比我還像新娘。我一直安慰自己涕滋,他們只是感情好睬辐,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宾肺,像睡著了一般溯饵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锨用,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天丰刊,我揣著相機與錄音,去河邊找鬼增拥。 笑死啄巧,一個胖子當著我的面吹牛寻歧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秩仆,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼码泛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了澄耍?” 一聲冷哼從身側(cè)響起噪珊,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎齐莲,沒想到半個月后痢站,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡铅搓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年瑟押,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片星掰。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡多望,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出氢烘,到底是詐尸還是另有隱情怀偷,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布播玖,位于F島的核電站椎工,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜀踏。R本人自食惡果不足惜维蒙,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望果覆。 院中可真熱鬧颅痊,春花似錦、人聲如沸局待。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钳榨。三九已至舰罚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間薛耻,已是汗流浹背营罢。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饼齿,地道東北人饲漾。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓瘟滨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親能颁。 傳聞我的和親對象是個殘疾皇子杂瘸,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容