Android音視頻【三】硬解碼播放H264

人間觀察
窮人家的孩子真的是在社會上瞎混

遙遠的2020年馬上就過去了,天吶!!!

前兩篇介紹了下H264的知識和碼流結構,本篇就拿上篇從抖音/快手抽離的h264文件實現在Android中進行解碼播放&以及介紹所涉及的知識。

本文代碼用kotlin來寫,最近在學習ing,加油吧,打工人,你要悄悄打工。

視頻效果

文章搞不了視頻,貼個圖吧。


H264DecoderDemo.png

軟硬編解碼

在介紹前我們需要知道什么是軟硬編解碼?

1.軟編解碼:是利用軟件本身或者說是使用CPU對原視頻進行編解碼的方式。

優點:兼容性好。

缺點:CPU占用率高,app內存占用率變高,可能會因CPU發熱而降頻、卡頓,無法流暢錄制、播放視頻等問題。

2.硬編解碼:使用非CPU進行編碼,如顯卡GPU、專用的DSP芯片、廠商芯片等。一般編解碼算法固定所以采用芯片處理。

優點:編碼速度非常快且效率極高,CPU的占用率低,就算長時間高清錄制視頻手機也不會發燙。

缺點:但是兼容性不好,往往畫面不夠精細也很難解決(但是還可以沒到不能看的程度)。

MediaCodec硬編解碼

一般Android中直播采集端/短視頻的編輯軟件都是默認采用硬編解碼,如果手機不支持再采用軟編解碼。硬編解碼是王道。

在Android中是使用MediaCodec類進行編解碼。MediaCodec是什么呢? MediaCodec是Android提供的用于對音視頻進行編解碼的類,它通過訪問底層的codec來實現編解碼的功能,比如你要把攝像頭的視頻yuv數據編碼為h264/h265pcm編碼為aach264/h265解碼為yuvaac解碼為pcm等等。MediaCodec是Android 4.1 API16引入的,在Android 5.0 API21加入了異步模式。

MediaCodec調用的是系統注冊過的編解碼器,硬件廠商把自己的硬編解碼器注冊到系統中就是硬編解碼,如果硬件廠商注冊的是軟編解碼就是軟解碼。往往不同的硬件廠商是不一樣的。然后MediaCodec負責調用。

獲取手機所支持的編解碼器

不同的手機不一樣所支持的編解碼器不同,如何獲取手機支持哪些編解碼器呢?如下:

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    private fun getSupportCodec() {
        val list = MediaCodecList(MediaCodecList.REGULAR_CODECS)
        val codecs = list.codecInfos
        Log.d(TAG, "Decoders:")
        for (codec in codecs) {
            if (!codec.isEncoder) Log.d(TAG, codec.name)
        }
        Log.d(TAG, "Encoders:")
        for (codec in codecs) {
            if (codec.isEncoder) Log.d(TAG, codec.name)
        }
    }

輸出

2020-12-25 19:16:00.914 13115-13115/com.bj.gxz.h264decoderdemo D/H264: Decoders:
2020-12-25 19:16:00.914 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.aac.decoder
2020-12-25 19:16:00.914 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrnb.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrwb.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.flac.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.g711.alaw.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.g711.mlaw.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.gsm.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.mp3.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.opus.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.raw.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vorbis.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.avc
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h264.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h263.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.hevc
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.hevc.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.mpeg2
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.mpeg4
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.mpeg4.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.decoder.vp8
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp8.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp9.decoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: Encoders:
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.aac.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrnb.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.amrwb.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.flac.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.encoder.avc
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h264.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.h263.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.hisi.video.encoder.hevc
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.mpeg4.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp8.encoder
2020-12-25 19:16:00.915 13115-13115/com.bj.gxz.h264decoderdemo D/H264: OMX.google.vp9.encoder

看一下命名方式,軟解碼器通常是OMX.google開頭的,比如上面的OMX.google.h264.decoder。硬解碼器是以OMX.[hardware_vendor]開頭的,比如上面的OMX.hisi.video.decoder.avc 其中hisi應該是海思芯片。當然也有一些不按照這個規則來的,系統會認為他是軟解碼器。 編碼器的命名也是一樣的。

從Android系統的源碼可以判斷出規則,
源碼地址:http://androidos.net.cn/android/6.0.1_r16/xref/frameworks/av/media/libstagefright/OMXCodec.cpp

static bool IsSoftwareCodec(const char *componentName) {
    if (!strncmp("OMX.google.", componentName, 11)) {
        return true;
    }

    if (!strncmp("OMX.", componentName, 4)) {
        return false;
    }

    return true;
}

MediaCodec處理數據的類型

MediaCodec非常強大,支持的編解碼數據類型有: 壓縮的音頻數據、壓縮的視頻數據、原始音頻數據和原始視頻數據,以及支持不同的封裝格式的編解碼,如前文所訴如果是硬解碼當然也是需要手機廠商支持的。可以設置Surface來獲取/呈現原始的視頻數據。MediaCodec的有關API的方法和每個方法的參數都有它的含義。可以在使用的時候慢慢深究。

MediaCodec的編解碼流程

下圖是Android官方文檔提供的,官方文檔很詳細了。
https://developer.android.google.cn/reference/android/media/MediaCodec?hl=en

mediacodec的工作方式.png

MediaCodec處理輸入數據產生輸出數據,當異步處理數據時,使用一組輸入輸出ByteBuffer.流程通常是

  1. 將數據填入到預先設定的輸入緩沖區(ByteBuffer),
  2. 輸入緩沖區填滿數據后將其傳給MediaCodec進行編解碼處理。編解碼處理完后它又填充到一個輸出ByteBuffer中。
  3. 然后使用方就可以獲取編解碼后的數據,再把ByteBuffer釋放回MediaCodec,往復循環。

需要注意的是Bufffer隊列不是我們自己new對象后塞給MediaCodec,而是MediaCodec為了更好的控制Bufffer的處理,我們需要使用MediaCodec提供的方法獲取然后塞給它數據并取出數據。

MediaCodec API

  • MediaCodec的創建
    • createDecoderByType/createEncoderByType:根據特定MIME類型(比如"video/avc")創建codec。Decoder就是解碼器,Encoder就是編碼器。
    • createByCodecName:知道組件的確切名稱(如OMX.google.h264.decoder)的時候,根據組件名創建codec。使用MediaCodecList可以獲取組件的名稱,如上文所介紹。
  • configure:配置解碼器或者編碼器。比如你可以配置把解碼的數據通過surface進行展示,本文的后續就是解碼h264的demo就是配置surface來把yuv數據渲染到此surface上。
  • start:開始編解碼,處于等待數據的到來。
  • 數據的處理,開始編解碼
    • dequeueInputBuffer:返回有效的輸入buffer的索引
    • queueInputBuffer:輸入流入隊列。一般是把數據塞給它
    • dequeueOutputBuffer:從輸出隊列中取出編/解碼后的數據,如果輸入的數據多,你可能要循環讀取,一般在寫代碼的時候是需要循環調用的
    • releaseOutputBuffer:釋放ByteBuffer數據返回給MediaCodec
    • getInputBuffers:獲取需要編解碼數據的輸入流隊列,返回的是一個ByteBuffer數組
    • getOutputBuffers:獲取編解碼之后的數據輸出流隊列,返回的是一個ByteBuffer數組
  • flush:清空的輸入和輸出隊列buffer
  • stop: 停止編解碼器進行編解碼
  • release:釋放編解碼器

從上面的api中也大概看到了MediaCodec編解碼器API的生命周期,具體的可以再看下官網。

MediaCodec的同步異步編解碼

同步方式

官方示例

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
  int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  if (inputBufferId >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(…);
    // fill inputBuffer with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }
  int outputBufferId = codec.dequeueOutputBuffer(…);
  if (outputBufferId >= 0) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
    // bufferFormat is identical to outputFormat
    // outputBuffer is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    // Subsequent data will conform to new format.
    // Can ignore if using getOutputFormat(outputBufferId)
    outputFormat = codec.getOutputFormat(); // option B
  }
 }
 codec.stop();
 codec.release();

流程大概如下:

- 創建并配置MediaCodec對象
- 循環直到完成:
  - 如果輸入buffer準備好了
    - 讀取一段輸入,將其填充到輸入buffer中進行編解碼
  - 如果輸出buffer準備好了:
    - 從輸出buffer中獲取編解碼后數據進行處理。
- 處理完畢后,銷毀 MediaCodec 對象。

異步方式

在Android 5.0, API21,引入了異步模式。官方示例:

MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
  @Override
  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // fill inputBuffer with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }
 
  @Override
  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
    // bufferFormat is equivalent to mOutputFormat
    // outputBuffer is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  }
 
  @Override
  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
    // Subsequent data will conform to new format.
    // Can ignore if using getOutputFormat(outputBufferId)
    mOutputFormat = format; // option B
  }
 
  @Override
  void onError(…) {
    …
  }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();
- 創建并配置MediaCodec對象。
- 給MediaCodec對象設置回調MediaCodec.Callback
- 在onInputBufferAvailable回調中:
    - 讀取一段輸入,將其填充到輸入buffer中進行編解碼
- 在onOutputBufferAvailable回調中:
    - 從輸出buffer中獲取進行編解碼后數據進行處理。
- 處理完畢后,銷毀 MediaCodec 對象。

解碼h264視頻

我們就解碼一個h264的視頻(拿上篇從抖音/快手抽離的h264文件)。h265也一樣,只要你明白了h264。h265的編碼方式和原理和碼流結構,都是小菜一碟。為了更明白h264的碼流數據,我們demo就一次性把文件讀如刀內存的byte數據中。

處理我們分兩種方式,都能正常播放,只是我們更清楚的了解h264碼流數據。

  • 是我們按照h264的碼流結構,每次截取一個NAL單元(NALU)塞給MediaCodec,包含最開始的SPS,PPS。
  • 是我們就固定截取幾k,然后塞給MediaCodec

首先 初始化MediaCodec

    var bytes: ByteArray? = null
    var mediaCodec: MediaCodec
    init {
        // demo測試,為方便一次性讀取到內存
        bytes = FileUtil.getBytes(path)
        // video/avc就是H264,創建解碼器
        mediaCodec = MediaCodec.createDecoderByType("video/avc")
        val mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height)
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15)
        mediaCodec.configure(mediaFormat, surface, null, 0)
    }

方式一:分割NAL單元(NALU)方式

    private fun decodeSplitNalu() {
        if (bytes == null) {
            return
        }
        // 數據開始下標
        var startFrameIndex = 0
        val totalSizeIndex = bytes!!.size - 1
        Log.i(TAG, "totalSize=$totalSizeIndex")
        val inputBuffers = mediaCodec.inputBuffers
        val info = MediaCodec.BufferInfo()
        while (true) {
            // 1ms=1000us 微妙
            val inIndex = mediaCodec.dequeueInputBuffer(10_000)
            if (inIndex >= 0) {
                // 分割出一幀數據
                if (totalSizeIndex == 0 || startFrameIndex >= totalSizeIndex) {
                    Log.e(TAG, "startIndex >= totalSize-1 ,break")
                    break
                }
                val nextFrameStartIndex: Int =
                    findNextFrame(bytes!!, startFrameIndex + 1, totalSizeIndex)
                if (nextFrameStartIndex == -1) {
                    Log.e(TAG, "nextFrameStartIndex==-1 break")
                    break
                }
                // 填充數據
                val byteBuffer = inputBuffers[inIndex]
                byteBuffer.clear()
                byteBuffer.put(bytes!!, startFrameIndex, nextFrameStartIndex - startFrameIndex)

                mediaCodec.queueInputBuffer(inIndex, 0, nextFrameStartIndex - startFrameIndex, 0, 0)

                startFrameIndex = nextFrameStartIndex

            }
            var outIndex = mediaCodec.dequeueOutputBuffer(info, 10_000)
            
            while (outIndex >= 0) {
                // 這里用簡單的時間方式保持視頻的fps,不然視頻會播放很快
                // demo 的H264文件是30fps
                try {
                    sleep(33)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
                // 參數2 渲染到surface上,surface就是mediaCodec.configure的參數2
                mediaCodec.releaseOutputBuffer(outIndex, true)
                outIndex = mediaCodec.dequeueOutputBuffer(info, 0)
            }
        }
    }

NALU分割方法

    private fun findNextFrame(bytes: ByteArray, startIndex: Int, totalSizeIndex: Int): Int {
        for (i in startIndex..totalSizeIndex) {
            // 00 00 00 01 H264的啟始碼
            if (bytes[i].toInt() == 0x00 && bytes[i + 1].toInt() == 0x00 && bytes[i + 2].toInt() == 0x00 && bytes[i + 3].toInt() == 0x01) {
//                Log.e(TAG, "bytes[i+4]=0X${Integer.toHexString(bytes[i + 4].toInt())}")
//                Log.e(TAG, "bytes[i+4]=${(bytes[i + 4].toInt().and(0X1F))}")
                return i
                // 00 00 01 H264的啟始碼
            } else if (bytes[i].toInt() == 0x00 && bytes[i + 1].toInt() == 0x00 && bytes[i + 2].toInt() == 0x01) {
//                Log.e(TAG, "bytes[i+3]=0X${Integer.toHexString(bytes[i + 3].toInt())}")
//                Log.e(TAG, "bytes[i+3]=${(bytes[i + 3].toInt().and(0X1F))}")
                return i
            }
        }
        return -1
    }

方式一:固定字節數據塞入

    private fun findNextFrameFix(bytes: ByteArray, startIndex: Int, totalSizeIndex: Int): Int {
        // 每次最好數據里大點,不然就像弱網的情況,數據流慢導致視頻卡
        val len = startIndex + 40000
        return if (len > totalSizeIndex) totalSizeIndex else len
    }

說明:在真實的項目中一般是網絡/數據流的方式塞入,這里只是為了demo演示MediaCodec解碼h264文件進行播放。

保存解碼h264視頻的yuv數據為圖片

我們在哪里進行保存里,就如前問所說,肯定是在h264解碼后進行保存,解碼后的數據為yuv數據。也就是在dequeueOutputBuffer后取出解碼后的數據,然后用YuvImage類的compressToJpeg保存為Jpeg圖片即可。我們3s保存一張吧。
局部代碼:

                // 3s 保存一張圖片
                if (System.currentTimeMillis() - saveImage > 3000) {
                    saveImage = System.currentTimeMillis()

                    val byteBuffer: ByteBuffer = mediaCodec.outputBuffers[outIndex]
                    byteBuffer.position(info.offset)
                    byteBuffer.limit(info.offset + info.size)
                    val ba = ByteArray(byteBuffer.remaining())
                    byteBuffer.get(ba)

                    try {
                        val parent =
                            File(Environment.getExternalStorageDirectory().absolutePath + "/h264pic/")
                        if (!parent.exists()) {
                            parent.mkdirs()
                            Log.d(TAG, "parent=${parent.absolutePath}")
                        }

                        // 將NV21格式圖片,以質量70壓縮成Jpeg
                        val path = "${parent.absolutePath}/${System.currentTimeMillis()}-frame.jpg"
                        Log.e(TAG, "path:$path")
                        val fos = FileOutputStream(File(path))
                        val yuvImage = YuvImage(ba, ImageFormat.NV21, width, height, null)
                        yuvImage.compressToJpeg(
                            Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()),
                            80, fos)
                        fos.flush()
                        fos.close()
                    } catch (e: IOException) {
                        e.printStackTrace()
                    }
                }

最后說明一點就是硬解碼是非常快,很高效率的,播放視頻是需要PTS時間戳處理的。demo的處理方法就是讓它渲染慢一點(demo視頻文件是30fps,也就是1000ms/30=33ms一幀yuv數據),所以在mediaCodec.releaseOutputBuffer(outIndex, true)前在sleep(33ms)來達到正常的播放速度。

文章源代碼

https://github.com/ta893115871/H264DecoderDemo

如果描述不正確的,歡迎指正。

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

推薦閱讀更多精彩內容