WebRTC 源碼分析——Android 視頻硬件編碼

作者:DevYK

1. 簡(jiǎn)介

本文將重點(diǎn)介紹在 Android 平臺(tái)上,WebRTC 是如何使用 MediaCodec 對(duì)視頻數(shù)據(jù)進(jìn)行編碼,以及在整個(gè)編碼過程中 webrtc native 與 java 的流程交互读跷。

本篇開始會(huì)先回顧一下 Andorid MediaCodec 的概念和基礎(chǔ)使用萍恕,然后再跟著問題去源碼中分析唤崭。

2. MediaCodec 基礎(chǔ)知識(shí)

MediaCodec 是 Android 提供的一個(gè)用于處理音頻和視頻數(shù)據(jù)的底層 API。它支持編碼(將原始數(shù)據(jù)轉(zhuǎn)換為壓縮格式)和解碼(將壓縮數(shù)據(jù)轉(zhuǎn)換回原始格式)的過程。MediaCodec 是自 Android 4.1(API 16)起引入的朗和,(通常與MediaExtractor馁痴、MediaSync谊娇、MediaMuxerMediaCrypto罗晕、 MediaDrm济欢、ImageSurface一起使用)小渊。

以下是 MediaCodec 的一些關(guān)鍵概念和用法:

  1. 創(chuàng)建和配置 MediaCodec:首先法褥,需要根據(jù)所需的編解碼器類型(例如 H.264、VP8酬屉、Opus 等)創(chuàng)建一個(gè) MediaCodec 實(shí)例挖胃。接下來,通過 MediaFormat 對(duì)象指定編解碼器的一些參數(shù)梆惯,如分辨率酱鸭、幀率、碼率等垛吗。然后凹髓,使用 configure() 方法配置 MediaCodec。
            try {
                // 1\. 創(chuàng)建和配置 MediaCodec
                MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
                if (codecInfo == null) {
                    throw new RuntimeException("No codec found for " + MIME_TYPE);
                }
                MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT);
                format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
                format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
                format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
                format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
                encoder = MediaCodec.createByCodecName(codecInfo.getName());
                encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
                encoder.start();
            } catch (IOException e) {
                throw new RuntimeException("Failed to initialize encoder", e);
            }
  1. 輸入和輸出緩沖區(qū):MediaCodec 有兩個(gè)緩沖區(qū)隊(duì)列怯屉,一個(gè)用于輸入蔚舀,另一個(gè)用于輸出。輸入緩沖區(qū)用于接收原始數(shù)據(jù)(例如從攝像頭捕獲的視頻幀)锨络,輸出緩沖區(qū)用于存儲(chǔ)編碼后的數(shù)據(jù)赌躺。在編解碼過程中,需要將這些緩沖區(qū)填充或消費(fèi)羡儿。
  1. 編碼器工作模式:MediaCodec 支持兩種工作模式礼患,分別是同步和異步。在同步模式下,需要手動(dòng)管理輸入和輸出緩沖區(qū)缅叠。在異步模式下悄泥,通過設(shè)置回調(diào)函數(shù)(MediaCodec.Callback),可以在編解碼事件發(fā)生時(shí)自動(dòng)通知應(yīng)用程序肤粱。

    同步:

     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 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(…) {
        …
      }
      @Override
      void onCryptoError(…) {
        …
      }
     });
     codec.configure(format, …);
     mOutputFormat = codec.getOutputFormat(); // option B
     codec.start();
     // wait for processing to complete
     codec.stop();
     codec.release();
  1. MediaCodec 與 Surface:對(duì)于視頻編解碼弹囚,MediaCodec 可以與 Surface 對(duì)象一起使用,以便使用 GPU 進(jìn)行高效處理领曼。通過將編解碼器與 Surface 關(guān)聯(lián)鸥鹉,可以將圖像數(shù)據(jù)直接從 Surface 傳輸?shù)骄幗獯a器,而無需在 CPU 和 GPU 之間復(fù)制數(shù)據(jù)庶骄。這可以提高性能并降低功耗宋舷。

    可使用如下 api 進(jìn)行創(chuàng)建一個(gè)輸入 surface

    public Surface createInputSurface ();
返回的 inputSurface 可與 EGL 進(jìn)行綁定,與 OpenGL ES 再進(jìn)行關(guān)聯(lián)瓢姻。 sample 可以參考這個(gè)開源庫 grafika
  1. 開始和停止編解碼:配置完 MediaCodec 后祝蝠,調(diào)用 start() 方法開始編解碼過程。在完成編解碼任務(wù)后幻碱,需要調(diào)用 stop() 方法停止編解碼器绎狭,并使用 release() 方法釋放資源。

  2. 錯(cuò)誤處理:在使用 MediaCodec 時(shí)褥傍,可能會(huì)遇到各種類型的錯(cuò)誤儡嘶,如不支持的編解碼格式、資源不足等恍风。為了確保應(yīng)用程序的穩(wěn)定性蹦狂,需要妥善處理這些錯(cuò)誤情況。

總之朋贬,MediaCodec 是 Android 中處理音視頻編解碼的關(guān)鍵組件凯楔。了解其基本概念和用法有助于構(gòu)建高效、穩(wěn)定的媒體應(yīng)用程序锦募。

3. webrtc 中如何使用硬件編碼器摆屯?

由于在 WebRTC 中優(yōu)先使用的是 VP8 編碼器,所以我們想要分析 Android 上硬件編碼的流程糠亩,需要先支持 h264 的硬件編碼

  1. 創(chuàng)建 PeerConnectionFactory 時(shí)設(shè)置視頻編碼器
        private PeerConnectionFactory createPeerConnectionFactory() {
            PeerConnectionFactory.initialize(
                    PeerConnectionFactory.InitializationOptions.builder(applicationContext)
                            .setEnableInternalTracer(true)
                            .createInitializationOptions());

            PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
            DefaultVideoEncoderFactory defaultVideoEncoderFactory =
                    new DefaultVideoEncoderFactory(
                            rootEglBase.getEglBaseContext(), true /* enableIntelVp8Encoder */, true);
            DefaultVideoDecoderFactory defaultVideoDecoderFactory =
                    new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext());

            return PeerConnectionFactory.builder()
                    .setOptions(options)
                    .setVideoEncoderFactory(defaultVideoEncoderFactory)
                    .setVideoDecoderFactory(defaultVideoDecoderFactory)
                    .createPeerConnectionFactory();
        }
  1. 在 createOffer / createAnswer 將 SDP 中 m=video 的 h264 playload 編號(hào)放在第一位

這部分代碼可以參考 preferCodec

4. webrtc 中編碼器是如何初始化的虐骑?

通過上一個(gè)問題得知,我們使用的是 DefaultVideoEncoderFactory 默認(rèn)編碼器赎线,內(nèi)部實(shí)現(xiàn)就是使用的硬件能力

內(nèi)部實(shí)例化了一個(gè) HardwareVideoEncoderFactory 廷没,我們?cè)?DefaultVideoEncoderFactory 中看到了 createEncoder 函數(shù),這里的內(nèi)部就是實(shí)例化 HardwareVideoEncoder 的地方垂寥,我們先 debug 看下是哪里調(diào)用的颠黎,如下圖所示另锋,

下圖的第一點(diǎn)可以發(fā)現(xiàn)底層傳遞過來的已經(jīng)是 h264 編碼器的信息了。

發(fā)現(xiàn)調(diào)用棧并沒有在 java 端盏缤,那肯定在 native 端了,我們可以通過 createPeerConnectionFactory 查看下調(diào)用

  1. 將 videoEnvoderFactory 引用傳遞到 native
  1. Native 入口在 PeerConnectionFactory_jni.h
  1. 根據(jù)調(diào)用棧蓖扑,發(fā)現(xiàn)將 jencoder_factory 包裝到了 CreateVideoEncoderFactory
    ScopedJavaLocalRef<jobject> CreatePeerConnectionFactoryForJava(
        JNIEnv* jni,
        const JavaParamRef<jobject>& jcontext,
        const JavaParamRef<jobject>& joptions,
        rtc::scoped_refptr<AudioDeviceModule> audio_device_module,
        rtc::scoped_refptr<AudioEncoderFactory> audio_encoder_factory,
        rtc::scoped_refptr<AudioDecoderFactory> audio_decoder_factory,
        const JavaParamRef<jobject>& jencoder_factory,
        const JavaParamRef<jobject>& jdecoder_factory,
        rtc::scoped_refptr<AudioProcessing> audio_processor,
        std::unique_ptr<FecControllerFactoryInterface> fec_controller_factory,
        std::unique_ptr<NetworkControllerFactoryInterface>
            network_controller_factory,
        std::unique_ptr<NetworkStatePredictorFactoryInterface>
            network_state_predictor_factory,
        std::unique_ptr<NetEqFactory> neteq_factory) {

    ...

      media_dependencies.video_encoder_factory =
          absl::WrapUnique(CreateVideoEncoderFactory(jni, jencoder_factory));

    ...
    }

    VideoEncoderFactory* CreateVideoEncoderFactory(
        JNIEnv* jni,
        const JavaRef<jobject>& j_encoder_factory) {
      return IsNull(jni, j_encoder_factory)
                 ? nullptr
                 : new VideoEncoderFactoryWrapper(jni, j_encoder_factory);
    }
  1. 通過一系列的調(diào)用唉铜,我們發(fā)現(xiàn)java 端的引用,被封裝成了 c++ 端的 VideoEncoderFactoryWrapper ,我們看一下它的構(gòu)造函數(shù)

主要就是通過 jni 調(diào)用 java 端的代碼律杠,用以獲取當(dāng)前設(shè)備所支持的編碼器和編碼器的信息

  1. 猜測(cè)既然在 Native 中包裝了 java 端 VideoEncoder.java 的引用潭流,那么肯定也有對(duì)應(yīng)的 CreateEncoder 函數(shù)

我們?cè)?video_encoder_factory_wrapper.h 中看到了我們想要的函數(shù),我們看下它的實(shí)現(xiàn)

這不就是我們找到了 createEncoder jni 調(diào)用的入口嗎柜去?那么是什么時(shí)候調(diào)用的呢灰嫉?我們進(jìn)行 debug 一下

它的調(diào)用棧是媒體協(xié)商成功后,根據(jù)發(fā)起方的編碼器來匹配嗓奢,目前匹配到了最優(yōu)的是 H264 編碼讼撒,然后進(jìn)行創(chuàng)建 H264 編碼器

此時(shí),我們已經(jīng)又回到了 java 端的 createEncoder 代碼股耽,我們來看下是怎么對(duì) MediaCodec 初始化的
  1. MediaCodec 核心初始化代碼

    在 HardwareVideoEncoderFactory 中的 createEncoder 中

上面的邏輯是判斷 MediaCodec 是否只是 baseline 和 high ,如果都不支持返回空根盒,反之返回 HardwareVideoEncoder 實(shí)例,該實(shí)例又返回給了 native 物蝙,然后轉(zhuǎn)為了 native 的智能指針 std::unique_ptr 的實(shí)體 VideoEncoderWrapper
通過 debug 炎滞,我們找到了在 native jni 執(zhí)行 initEncode 的入口函數(shù)
通過媒體協(xié)商后,我們得到了編碼器配置的一些參數(shù)
內(nèi)部執(zhí)行了 **initEncodeInternal** ,我們看下具體實(shí)現(xiàn)
這里就是我們所熟悉的 MediaCodec 編碼配置了诬乞,根據(jù)上面的序號(hào)我們知道册赛,先根據(jù)媒體協(xié)商后的編碼器名稱來創(chuàng)建一個(gè) MediaCodec 對(duì)象,然后配置一些必要的參數(shù)震嫉,最后啟動(dòng)編碼器.

5. webrtc 中是如何將數(shù)據(jù)送入編碼器的森瘪?

WebRTC 使用 VideoEncoder 接口來進(jìn)行視頻編碼,該接口定義了一個(gè)用于編碼視頻幀的方法:encode(VideoFrame frame, EncodeInfo info)票堵。WebRTC 提供了一個(gè)名為 HardwareVideoEncoder 的類柜砾,該類實(shí)現(xiàn)了 VideoEncoder 接口,并使用 MediaCodec 對(duì)視頻幀進(jìn)行編碼换衬。

HardwareVideoEncoder 類中痰驱,WebRTC 將 VideoFrame 對(duì)象轉(zhuǎn)換為與 MediaCodec 關(guān)聯(lián)的 Surface 的紋理。這是通過使用 EglBase 類創(chuàng)建一個(gè) EGL 環(huán)境瞳浦,并使用該環(huán)境將 VideoFrame 的紋理繪制到 Surface 上來實(shí)現(xiàn)的担映。

為了更好的理解 MediaCodec createInputSurface 和 OpenGL ES 、EGL 的關(guān)系叫潦,我簡(jiǎn)單畫了一個(gè)架構(gòu)圖蝇完。如下所示:

EGL、OpenGL ES、 InputSurface 關(guān)系流程:

  1. 使用 OpenGL ES 繪制圖像短蜕。
  2. EGL 管理和連接 OpenGL ES 渲染的表面氢架。
  3. 通過 Input Surface,將 OpenGL ES 繪制的圖像傳遞給 MediaCodec朋魔。
  4. MediaCodec 對(duì)接收到的圖像數(shù)據(jù)進(jìn)行編碼岖研。

根據(jù)上面流程得知,采集到的 VideoFrame 會(huì)提交給 VideoStreamEncoder::OnFrame 然后經(jīng)過調(diào)用 EncodeVideoFrame 會(huì)執(zhí)行到 VideoEncoder.java 的包裝類,webrtc::jni::VideoEnacoderWrapper::Encode 函數(shù)警检,最后通過 jni 將(videoFrame,encodeInfo) 回調(diào)給了 java 端孙援。

接下來我們看 java 端如何處理的 VideoFrame

該函數(shù)的核心是判斷是否使用 surface 模式進(jìn)行編碼,如果條件成立調(diào)用 encodeTextureBuffer 進(jìn)行紋理編碼扇雕,

我們先看上圖的第一步,

第一步的 1-3 小點(diǎn)主要是通過 OpenGL ES 將 OES 紋理數(shù)據(jù)繪制出來拓售,然后第二大步的 textureEglBase.swapBuffers(...) 主要是將 OpenGL ES 處理后的圖像數(shù)據(jù)提交給 EGLSurface 。經(jīng)過這些操作后紋理數(shù)據(jù)就提交給 MediaCodec 的 inputsurface 了镶奉。

6. webrtc 是如何獲取編碼后的數(shù)據(jù)础淤?

HardwareVideoEncoder 類中,使用 MediaCodec 同步模式進(jìn)行獲取編碼后的數(shù)據(jù)哨苛。當(dāng)數(shù)據(jù)可用時(shí)值骇,會(huì)調(diào)用 callback.onEncodedFrame(encodedImage, new CodecSpecificInfo()); 方法,然后將編碼后的幀傳遞給 WebRTC 引擎。WebRTC 引擎會(huì)對(duì)編碼后的幀進(jìn)行進(jìn)一步處理移国,如封裝 RTP 包吱瘩、發(fā)送到對(duì)端等。

主要流程如下:

第一步有點(diǎn)印象吧迹缀?對(duì)使碾,就是在編碼器初始化的時(shí)候會(huì)開啟一個(gè)循環(huán)獲取解碼數(shù)據(jù)的線程,我們分析下 deliverEncodedImage 函數(shù)的實(shí)現(xiàn)邏輯

這段代碼的主要功能是從編解碼器 (MediaCodec) 中獲取編碼后的視頻幀祝懂,并對(duì)關(guān)鍵幀進(jìn)行處理票摇。以下是代碼的逐步分析:

  1. 定義一個(gè) MediaCodec.BufferInfo 對(duì)象,用于存儲(chǔ)輸出緩沖區(qū)的元信息砚蓬。

  2. 調(diào)用 codec.dequeueOutputBuffer() 方法來獲取編碼后的輸出緩沖區(qū)索引矢门。如果索引小于 0,則有特殊含義灰蛙。比如 MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED 表示輸出緩沖區(qū)已更改祟剔,此時(shí)需要重新獲取輸出緩沖區(qū)。

  3. 使用索引獲取編碼后的輸出緩沖區(qū) (ByteBuffer)摩梧。

  4. 設(shè)置緩沖區(qū)的位置 (position) 和限制 (limit)物延,以便讀取數(shù)據(jù)。

  5. 檢查 info.flags 中的 MediaCodec.BUFFER_FLAG_CODEC_CONFIG 標(biāo)志仅父。如果存在叛薯,表示當(dāng)前幀為編解碼器配置幀浑吟。這種情況下,將配置幀數(shù)據(jù)存儲(chǔ)在 configBuffer 中耗溜。

  6. 如果當(dāng)前幀不是配置幀组力,則執(zhí)行以下操作:

    6.1 查看當(dāng)前是否重新配置編碼碼率,如果是就更新比特率抖拴。

    6.2 檢查當(dāng)前幀是否為關(guān)鍵幀燎字。如果 info.flags 中的 MediaCodec.BUFFER_FLAG_SYNC_FRAME 標(biāo)志存在,則表示當(dāng)前幀為關(guān)鍵幀城舞。 6.3 對(duì)于 H.264 編碼的關(guān)鍵幀轩触,將 SPS 和 PPS NALs 數(shù)據(jù)附加到幀的開頭寞酿。創(chuàng)建一個(gè)新的緩沖區(qū)家夺,將 configBuffer 和編碼后的輸出緩沖區(qū)的內(nèi)容復(fù)制到新緩沖區(qū)中。

    6.4 根據(jù)幀類型 (關(guān)鍵幀或非關(guān)鍵幀)伐弹,創(chuàng)建一個(gè) EncodedImage 對(duì)象拉馋。在釋放輸出緩沖區(qū)時(shí),確保不拋出任何異常惨好。

    6.5 調(diào)用 callback.onEncodedFrame() 方法傳遞編碼后的圖像和編解碼器特定信息煌茴。

    6.6 釋放 EncodedImage 對(duì)象。

當(dāng)遇到異常 (例如 IllegalStateException) 時(shí)日川,代碼將記錄錯(cuò)誤信息蔓腐。

總之,這段代碼的目標(biāo)是從 MediaCodec 中獲取編碼后的視頻幀龄句,對(duì)關(guān)鍵幀進(jìn)行處理回论,并將結(jié)果傳遞給回調(diào)函數(shù)。

對(duì)分歇,該疑問的答案就是 6.5 它將編碼后的數(shù)據(jù)通過 onEncodedFrame 告知了 webrtc 引擎傀蓉。由于后面的處理不是本章的重點(diǎn),所以不再分析职抡。

7. webrtc 是如何做碼流控制的葬燎?

WebRTC 的碼流控制包括擁塞控制和比特率自適應(yīng)兩個(gè)主要方面。這里只簡(jiǎn)單介紹下概念缚甩,及 Android 是如何配合 webrtc 來動(dòng)態(tài)修改碼率的谱净。

  1. 擁塞控制 (Congestion Control): 擁塞控制主要關(guān)注在不引起網(wǎng)絡(luò)擁塞的情況下傳輸盡可能多的數(shù)據(jù)。WebRTC 實(shí)現(xiàn)了基于 Google Congestion Control (GCC) 的擁塞控制算法擅威,它也被稱為 Send Side Bandwidth Estimation(發(fā)送端帶寬估計(jì))岳遥。此算法根據(jù)丟包率、往返時(shí)間 (RTT) 和接收端的 ACK 信息來調(diào)整發(fā)送端的碼率裕寨。擁塞控制算法會(huì)持續(xù)監(jiān)測(cè)網(wǎng)絡(luò)狀況浩蓉,并根據(jù)需要?jiǎng)討B(tài)調(diào)整發(fā)送碼率派继。
  2. 比特率自適應(yīng) (Bitrate Adaptation): 比特率自適應(yīng)關(guān)注如何根據(jù)網(wǎng)絡(luò)條件和設(shè)備性能調(diào)整視頻編碼參數(shù),以實(shí)現(xiàn)最佳的視頻質(zhì)量捻艳。

當(dāng)比特率發(fā)生變化時(shí)驾窟,WebRTC 會(huì)調(diào)用 VideoEncoder.setRateAllocation() 方法來通知更新比特率。

在編碼的時(shí)候认轨,其實(shí)在上一個(gè)疑問中已經(jīng)知道了如何調(diào)節(jié)碼率绅络。判斷條件是當(dāng)當(dāng)前的碼率與需要調(diào)節(jié)的碼率不匹配時(shí),調(diào)用如下代碼進(jìn)行更新:

8. 總結(jié)

本文深入剖析了 WebRTC 在 Android 平臺(tái)上是如何使用 MediaCodec 對(duì)視頻數(shù)據(jù)進(jìn)行編碼的嘁字,以及整個(gè)編碼過程中 webrtc native 與 java 的流程交互恩急。首先回顧了 Android MediaCodec 的概念和基礎(chǔ)使用,包括創(chuàng)建和配置 MediaCodec纪蜒、輸入和輸出緩沖區(qū)衷恭、編碼器工作模式以及 MediaCodec 與 Surface 的關(guān)系。然后纯续,通過具體的代碼示例随珠,詳細(xì)說明了在 WebRTC 中如何實(shí)現(xiàn)視頻數(shù)據(jù)的編解碼。并通過幾個(gè)疑問的方式從源碼的角度了解到了整個(gè)編碼流程猬错。希望通過此文能幫助讀者更好地理解 WebRTC Android 編碼技術(shù)窗看。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市倦炒,隨后出現(xiàn)的幾起案子显沈,更是在濱河造成了極大的恐慌,老刑警劉巖逢唤,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拉讯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡智玻,警方通過查閱死者的電腦和手機(jī)遂唧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吊奢,“玉大人盖彭,你說我怎么就攤上這事∫彻觯” “怎么了召边?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)裹驰。 經(jīng)常有香客問我隧熙,道長(zhǎng),這世上最難降的妖魔是什么幻林? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任贞盯,我火速辦了婚禮音念,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘躏敢。我一直安慰自己闷愤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布件余。 她就那樣靜靜地躺著讥脐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啼器。 梳的紋絲不亂的頭發(fā)上旬渠,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音端壳,去河邊找鬼告丢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛更哄,可吹牛的內(nèi)容都是我干的芋齿。 我是一名探鬼主播腥寇,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼成翩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了赦役?” 一聲冷哼從身側(cè)響起麻敌,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎掂摔,沒想到半個(gè)月后术羔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乙漓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年级历,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叭披。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡寥殖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涩蜘,到底是詐尸還是另有隱情嚼贡,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布同诫,位于F島的核電站粤策,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏误窖。R本人自食惡果不足惜叮盘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一秩贰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柔吼,春花似錦萍膛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蝌戒,卻和暖如春串塑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背北苟。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工桩匪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人友鼻。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓傻昙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親彩扔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子妆档,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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