轉(zhuǎn)型到ExoPlayer,實(shí)現(xiàn)更多的自定義功能

原演講地址

Switching to ExoPlayer: Better Video on Android

360|AnDev 的演講中慈缔,Effie Barak展示了Udemy從MediaPlayer轉(zhuǎn)型到Exoplayer的過程减途,包括了基本的實(shí)現(xiàn),除此之外舒萎,還有ExoPlayer的一些高級(jí)視頻功能程储,比如后臺(tái)播放,可變播放速度臂寝,字幕章鲤,以及不同分辨率播放等功能。

開場(chǎng)介紹

Udemy 最開始的播放器使用的是 MediaPlayer 咆贬。在六個(gè)月前败徊,我們決定轉(zhuǎn)型到 ExoPlayer 。ExoPlayer是Android上一個(gè)應(yīng)用級(jí)別的媒體播放器掏缎,它提供了可選的方案來播放媒體文件而不是僅僅使用 MediaPlayer皱蹦。這是一個(gè)Google出品的第三方的播放器的library,完全使用java編寫眷蜈,并且只依賴低級(jí)的媒體編碼API沪哺。
Udemy APP 主要是用來觀看教學(xué)課程以及視頻講座,所以媒體部分是整個(gè)應(yīng)用最核心的功能端蛆。一個(gè)穩(wěn)定可靠的播放器是十分重要的凤粗。同時(shí),我們也需要一些可自定義的部分今豆,除了基本的播放嫌拣、暫停、下一個(gè)之外的一些更酷炫的功能呆躲。

MediaPlayer對(duì)比ExoPlayer

mediaPlayer.setDataSource(url);
mediaPlayer.prepare();
mediaPlayer.start();

MediaPlayer 有一些優(yōu)點(diǎn)异逐,最主要的就是開始使用很簡(jiǎn)單,你只需要上面這三行代碼就能播放大部分的文件插掂。
缺點(diǎn)就是可定制性不高灰瞻,不易擴(kuò)展。隨著功能迭代辅甥,我們需要app添加一些更多的功能酝润,比如支持HLS流,播放速率可變璃弄,這些都是 MediaPlayer 所不能立即完成的要销。
除此之外 MediaPlayer 也有一些其他的缺點(diǎn):這是一個(gè)黑盒,并且內(nèi)部都是native的方法夏块,很難去debug和弄清楚到底異常是怎么出現(xiàn)的疏咐。而且 MediaPlayer 作為framework級(jí)別的解決方案纤掸,這樣在不同的版本,不同的ROM上表現(xiàn)會(huì)有差異浑塞,我們不能控制和擔(dān)保到底使用的是什么樣的版本借跪。而且 MediaPlayer 會(huì)有一些各種奇怪的異常code,很難確信這些crash是怎么產(chǎn)生的酌壕。
ExoPlayer 解決了上述這些提到的問題掏愁,它具有強(qiáng)大的可擴(kuò)展性,但是一開始的學(xué)習(xí)曲線比較陡峭卵牍。好在這是開源的托猩,源碼易于閱讀,并且容易debug辽慕。實(shí)現(xiàn)上基于 MediaCodec京腥,能夠處理HLS流,同時(shí)也支持后臺(tái)播放溅蛉,播放速率公浪,分辨率可配置。

ExoPlayer基礎(chǔ)

不同于 MediaPlayer船侧,使用 ExoPlayer 需要更多的一些代碼來實(shí)現(xiàn)播放視頻欠气,主要分為兩部分:一個(gè)是UI部分來控制播放器的行為(播放,暫停镜撩,下一個(gè))预柒,第二個(gè)核心部分就是獲取數(shù)據(jù)流,解碼袁梗,處理流宜鸯。

播放器
player = ExoPlayer.Factory.newInstance(
  PlayerConstants.RENDERER_COUNT,
  MIN_BUFFER_MS,
  MIN_REBUFFER_MS);

playerControl = new PlayerControl(player);

上面是一個(gè)播放器被初始化的例子,下面的 playerControl 是一個(gè)默認(rèn)組件用來和播放器一起工作遮怜。為了從播放組件得到各種返回的信息來做更多的行為淋袖,比如失敗后的重試,我們可以通過引擎組件的一系列監(jiān)聽達(dá)成這樣的效果

public abstract class UdemyBaseExoplayer
  implements ExoPlayer.Listener,
    ChunkSampleSource.EventListener,
    HlsSampleSource.EventListener,
    DefaultBandwidthMeter.EventListener,
    MediaCodecVideoTrackRenderer.EventListener,
    MediaCodecAudioTrackRenderer.EventListener
player.addListener(this);
核心

如果我們想要處理一些非自適應(yīng)的流類型(比如MP3或者M(jìn)P4)會(huì)有一點(diǎn)不一樣锯梁,如果是HLS和Dash流就更復(fù)雜一些即碗。
流的來源一般是一個(gè)URI,然后通過一個(gè) ExtractorSampleSource 來獲取流陌凳,并且會(huì)根據(jù)編碼類型有一個(gè)對(duì)應(yīng)的實(shí)際流處理(比如是MP4剥懒,就是Mp4Extractor),這些Extractor通過將視頻和音頻文件解碼成原始的信息,通過Render的方式給播放器來進(jìn)行播放合敦。
還有另一種方式初橘,播放器直接向renders要緩沖buffer,這樣它就會(huì)說,“我沒有可以放的東西啦壁却,ExtractorSampleSource,再給我一些數(shù)據(jù)”裸准,然后 ExtractorSampleSource 就會(huì)說展东,“我需要獲取一些數(shù)據(jù),會(huì)通過默認(rèn)的 URI 數(shù)據(jù)源來拿數(shù)據(jù)”炒俱。
第一眼看過去這些操作需要不少代碼盐肃,但我們真的需要寫這么多東西么?

Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
Handler mainHandler = player.getMainHandler();

DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, null);
DataSource dataSource = new DefaultUriDataSource(
  context, bandwidthMeter, Util.getUserAgent(mContext, Constants.UDEMY_NAME));
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
  BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE, mainHandler, player, 0);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
  sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
  mainHandler, player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
  MediaCodecSelector.DEFAULT, null, true, mainHandler, player,
  AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);

TrackRenderer[] renderers = new TrackRenderer[PlayerConstants.RENDERER_COUNT];
renderers[PlayerConstants.TYPE_VIDEO] = videoRenderer;
renderers[PlayerConstants.TYPE_AUDIO] = audioRenderer;
player.onRenderers(renderers, bandwidthMeter);

這些代碼還是需要的权悟,但是寫起來并不困難砸王,我直接從官方的demo app中copy過來,而且能直接使用峦阁,所以并不需要我們?nèi)戇@些代碼谦铃。

// Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
// Handler mainHandler = player.getMainHandler();
// DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, null);
// DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter,
//                         Util.getUserAgent(mContext, Constants.UDEMY_NAME));

ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
  BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE, mainHandler, player, 0);

MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
  sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
  mainHandler, player, 50);

MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
  MediaCodecSelector.DEFAULT, null, true, mainHandler, player,
  AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);

TrackRenderer[] renderers = new TrackRenderer[PlayerConstants.RENDERER_COUNT];
renderers[PlayerConstants.TYPE_VIDEO] = videoRenderer;
renderers[PlayerConstants.TYPE_AUDIO] = audioRenderer;
player.onRenderers(renderers, bandwidthMeter);

有了上面的代碼后,我們還需要一些代碼

Build Extractors
DefaultUriDataSource uriDataSource = new DefaultUriDataSource
    (context, bandwidthMeter, userAgent);
ExtractorSampleSource sampleSource = new ExtractorSampleSource
    (uri, uriDataSource, allocator,
    PlayerConstants.BUFFER_SEGMENT_COUNT * PlayerConstants.BUFFER_SEGMENT_SIZE);
Build Renderers
TrackRenderer[] renderers =
    new TrackRenderer[PlayerConstants.RENDERER_COUNT];
    
MediaCodecAudioTrackRenderer audioRenderer =
  new MediaCodecAudioTrackRenderer(
    sampleSource, MediaCodecSelector.DEFAULT,
      null, true, player.getMainHandler(), player,
    AudioCapabilities.getCapabilities(context),
    AudioManager.STREAM_MUSIC);
    
renderers[PlayerConstants.TYPE_AUDIO] = audioRenderer;
Connect Renderers to the Player
player.prepare(renderers);
Udemy 自定義的一些結(jié)構(gòu)

Udemy 對(duì)于基本結(jié)構(gòu)做了一些優(yōu)化榔昔,比如減少了緩沖區(qū)buffer的大小以及單一片段 segment的大小驹闰,因?yàn)樵谝恍┑投藱C(jī)型上會(huì)出現(xiàn)內(nèi)存不足的異常。
而且在使用上撒会,一般我們已經(jīng)知道了我們要播放的媒體文件類型嘹朗,我們可以只需要相應(yīng)類型的extractor ,讓程序更加的精簡(jiǎn)诵肛,比如播放一個(gè)MP4文件只需要下面這些配置

mp4Extractor = new Mp4Extractor();
mp3Extractor = new Mp3Extractor();
sampleSource = new ExtractorSampleSource(..., mp4Extractor, mp3Extractor);

HLS流

對(duì)于播放HLS還是有一點(diǎn)復(fù)雜的屹培,HLS是一種特殊協(xié)議流,將原始數(shù)據(jù)分割成很多小的序列文件來下載怔檩。每個(gè)下載都是整體流中的一小塊褪秀,在播放時(shí),客戶端可以從不同的數(shù)據(jù)流中選擇可以替換的流薛训,并且允許流基于帶寬等邏輯來進(jìn)行切換溜歪。
在流會(huì)話的開始,會(huì)有一個(gè) m3u8的文件许蓖,這個(gè)文件就是存儲(chǔ)了不同分片ts文件的列表蝴猪,HLS會(huì)根據(jù)帶寬來決定使用哪一個(gè)chunk。然后剩下的就比較相似了膊爪,選擇好了哪個(gè)質(zhì)量的流之后自阱,這些流會(huì)被 HlsChunkSourceHlsSampleSource 處理,一般我們不需要寫額外的邏輯米酬,使用默認(rèn)的就好了沛豌,除非想自定義一些行為。

DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
PtsTimestampAdjusterProvider timestampAdjusterProvider =
  new PtsTimestampAdjusterProvider();
HlsChunkSource chunkSource = new HlsChunkSource(...,
  uriDataSource,
  url,...,
  bandwidthMeter,
  timestampAdjusterProvider,
  HlsChunkSource.ADAPTIVE_MODE_SPLICE);

上面這段初始化了一個(gè)新的 HlsChunkSource ,首先通過一個(gè) bandwidthMeter 來計(jì)算當(dāng)前有效的帶寬加派,同時(shí)還有一個(gè) PtsTimestamp 的對(duì)象叫确,Pts是一個(gè)用來度量解碼后的視頻幀什么時(shí)候播放的參數(shù),這里也用默認(rèn)的就行了芍锦。
最后一個(gè) HlsChunkSource 構(gòu)造函數(shù)的參數(shù)是一個(gè)mode竹勉,總共有三種mode:nonesplice娄琉,abrupt次乓。
none 沒什么特殊的,這樣選擇從一開始到最后都是一樣的
splice 意味著在chunk間切換時(shí)孽水,會(huì)同時(shí)下載老的以及新的票腰,以免出現(xiàn)前一個(gè)還沒結(jié)束后一個(gè)已經(jīng)開始,這樣會(huì)導(dǎo)致切換產(chǎn)生噪點(diǎn)女气。
abrupt 這個(gè)只會(huì)下載新的杏慰,老的不管,如果出現(xiàn)時(shí)間戳不匹配的話炼鞠,就容易在分片切換時(shí)出現(xiàn)噪點(diǎn)
綜合來看使用splice是比較好的選擇逃默,當(dāng)然會(huì)下載兩份流量上有一些偏多。

sampleSource = new HlsSampleSource(chunkSource, ...);

然后需要一個(gè) HlsSampleSource 簇搅,持有一個(gè)上面初始化的 chunkSource 對(duì)象完域,上面的這些是一個(gè)例子展示了一個(gè) HlsChunkSource 怎么拿到 metafile,并組成一個(gè)可供選擇的流列表瘩将,并最終展示出來吟税。
在 Udemy ,我們有時(shí)候需要重寫這個(gè)例子姿现,比如有時(shí)只給用戶一個(gè)選擇肠仪,只有一種質(zhì)量的視頻,不需要?jiǎng)討B(tài)改變視頻質(zhì)量备典,那么我們就能將 mode 轉(zhuǎn)變?yōu)?ADAPTIVE_MODE_NONE

HlsChunkSource chunkSource = new HlsChunkSource(..., HlsChunkSource.ADAPTIVE_MODE_NONE);

而且我們有時(shí)候也需要告訴播放器從哪種質(zhì)量開始异旧,如果沒有設(shè)置,播放器總是選擇一個(gè)默認(rèn)的值提佣,但是手動(dòng)設(shè)置改變它是比較麻煩的吮蛹,因?yàn)檫@個(gè)參數(shù)是一個(gè)私有private變量 private int selectedVariantIndex,這個(gè)變量貫穿了整個(gè)類拌屏,特別是 getChunkOperation 方法潮针,所以我們不得不創(chuàng)建一個(gè)自己的類實(shí)例來添加這個(gè)功能,這也算是設(shè)計(jì)的一個(gè)瑕疵了

// The index in variants of the currently selected variant.
private int selectedVariantIndex;

public void getChunkOperation(...) {
  int nextVariantIndex;
  ...
  if (adaptiveMode == ADAPTIVE_MODE_NONE) {
    nextVariantIndex = selectedVariantIndex;
    switchingVariantSpliced = false;
  } else {
... }
...

后臺(tái)播放

Udemy app支持后臺(tái)服務(wù)播放media倚喂,我們希望在應(yīng)用退到后臺(tái)時(shí)音頻文件依然能播放每篷,并且展示一個(gè)通知欄給用戶操作。在使用 MediaPlayer 的時(shí)候其實(shí)已經(jīng)實(shí)現(xiàn)了這樣的功能,但是很麻煩焦读,因?yàn)樾枰猻ervice和activity之間不斷的通信子库。

player.blockingSendMessage(
  videoRenderer,
  MediaCodecVideoTrackRenderer.MSG_SET_SURFACE,
  null);

而 ExoPlayer 內(nèi)置了后臺(tái)播放音頻的能力,這讓一切變得很簡(jiǎn)單矗晃。當(dāng)切換到后臺(tái)時(shí)第一件事就是清除播放器的surface仑嗅,這樣就能不attach到view上,這樣音頻就能在后臺(tái)保持播放的狀態(tài)喧兄。
上面幾行代碼展示了發(fā)送一個(gè)消息給播放器,設(shè)置surface一個(gè)占位啊楚,如果你直接跑官方的sample app吠冤,會(huì)發(fā)現(xiàn)可能在后臺(tái)播放一段時(shí)間后就掛掉了,這是因?yàn)檎麄€(gè)app的進(jìn)程被系統(tǒng)殺死了恭理,通過一個(gè)service可以避免這種現(xiàn)象拯辙,同樣的在service中,我們照樣可以創(chuàng)建 notification來控制整個(gè)播放器颜价。

<service android:name="com.udemy.android.player.exoplayer.UdemyExoplayerService"
    android:exported="true" android:label="@string/app_name" android:enabled="true">
    <intent-filter>
      <action android:name="ccom.udemy.android.player.exoplayer.UdemyExoplayerService">
      </action>
    </intent-filter>
</service>

這里service和activity共用同一個(gè)player的實(shí)例涯保,所以不需要通信來同步,這里以前的 MediaPlayer 的一個(gè)問題周伦,當(dāng)activity的生命周期變成 resume 狀態(tài)時(shí)夕春,我們可以重新將surface和view結(jié)合在一起,無縫的繼續(xù)播放专挪。

setPlayerSurface(surfaceView.getHolder().getSurface());
public void setSurface(Surface surface) {
  this.surface = surface;
  pushSurface(false);
}

字幕

Udemy app同樣也能支持字幕及志,而且基于此給ExoPlayer 提了一些issue,比如:

  1. 不支持 .srt 文件格式 寨腔,這是一種比較通用的字幕格式速侈,
  2. 如果想要字幕,必須實(shí)例化另一個(gè)render迫卢,這也將導(dǎo)致可能出現(xiàn)的不同步倚搬,比如視頻crash了,而字幕依舊在播放乾蛤,所以需要在視頻出錯(cuò)的情況下不會(huì)播放字幕每界。
  3. 希望能支持更多的格式,不僅是UTF-8家卖,我們有許多不同的語言的字幕盆犁,這個(gè)功能十分的重要,現(xiàn)在的解決方案是手動(dòng)的去填充字幕

對(duì)于視頻的步調(diào)和時(shí)間戳篡九,我們使用了一個(gè) subtitle conversion library.谐岁。

public void displayExoplayerSubtitles(
    File file,
    final  MediaController.MediaPlayerControl playerControl,
    final ViewGroup subtitleLayout,
    final Context context) {
  convertFileCaptionList(file, context);
  runnableCode = new Runnable() {
    @Override
    public void run() {
      displayForPosition(playerControl.getCurrentPosition(), subtitleLayout, context);
      handler.postDelayed(runnableCode, 200);
    }
};
  handler.post(runnableCode);
}

改變回放速率

回放的速率可以變化是一個(gè)非常重要的功能,這是 MediaPlayer 所不能做到的。
在 ExoPlayer 中伊佃,視頻一般都會(huì)有音頻一起窜司,在邏輯上我們保持音視頻同步,一般都是視頻跟著音頻走航揉,如果把音頻的速率加快塞祈,這樣視頻就能跟著一起加快。
這里使用的另一個(gè) library 帅涂,Sonic 议薪,它可以拿到一個(gè)已有的音頻的buffer,然后讓其變得更快或是更慢媳友,然后返回一個(gè)新的 buffer斯议。
這里我們需要繼承實(shí)現(xiàn)音頻的渲染器

public class VariableSpeedAudioRenderer  extends MediaCodecAudioTrackRenderer

// Method to override
private byte[] sonicInputBuffer;
private byte[] sonicOutputBuffer;

@Override
protected void onOutputFormatChanged(final MediaFormat format) {

我們需要告訴音頻渲染器 不要使用以前的buffer ,將這個(gè)buffer 給 Sonic 醇锚,然后返回一個(gè)新的 buffer哼御, 直接先繼承 MediaCodecAudioTrackRenderer,并且覆寫兩個(gè)方法焊唬。
第一個(gè)方法就是 onOutputFormatChanged 恋昼,它會(huì)在 track 第一次被實(shí)例化的時(shí)候會(huì)調(diào)用,在這個(gè)方法里我們將需要處理的buffer赶促,以及Sonic本身都放進(jìn)來液肌,然后刷新整個(gè)流,設(shè)置用戶選擇的一個(gè)速度鸥滨。

// Two samples per frame * 2 to support audio speeds down to 0.5
final int bufferSizeBytes = SAMPLES_PER_CODEC_FRAME * 2 * 2 * channelCount;

this.sonicInputBuffer = new byte[bufferSizeBytes];
this.sonicOutputBuffer = new byte[bufferSizeBytes];

this.sonic = new Sonic(
  format.getInteger(MediaFormat.KEY_SAMPLE_RATE),
  format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));

this.lastInternalBuffer = ByteBuffer.wrap(sonicOutputBuffer, 0, 0);

sonic.flushStream();
sonic.setSpeed(audioSpeed);

第二個(gè)覆寫的方法就是 processOutputBuffer矩屁,這里是真正拿到buffer,并且播放的地方爵赵,在這里我們拿到buffer后吝秕,將buffer寫到Sonic中,然后從返回里面讀到新的根據(jù)我們?cè)O(shè)置速率改變后的buffer空幻,然后使用 superclass 烁峭,讓父類實(shí)現(xiàn)去使用它。

@Override
protected boolean processOutputBuffer(..., final ByteBuffer buffer,...)
private ByteBuffer lastInternalBuffer;

buffer.get(sonicInputBuffer, 0, bytesToRead);
sonic.writeBytesToStream(sonicInputBuffer, bytesToRead);
sonic.readBytesFromStream(sonicOutputBuffer, sonicOutputBuffer.length);

return super.processOutputBuffer(..., lastInternalBuffer, ...);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秕铛,一起剝皮案震驚了整個(gè)濱河市约郁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌但两,老刑警劉巖鬓梅,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異谨湘,居然都是意外死亡绽快,警方通過查閱死者的電腦和手機(jī)芥丧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坊罢,“玉大人续担,你說我怎么就攤上這事』詈ⅲ” “怎么了物遇?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)憾儒。 經(jīng)常有香客問我询兴,道長(zhǎng),這世上最難降的妖魔是什么起趾? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任诗舰,我火速辦了婚禮,結(jié)果婚禮上阳掐,老公的妹妹穿的比我還像新娘始衅。我一直安慰自己冷蚂,他們只是感情好缭保,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蝙茶,像睡著了一般艺骂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上隆夯,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天钳恕,我揣著相機(jī)與錄音,去河邊找鬼蹄衷。 笑死忧额,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的愧口。 我是一名探鬼主播睦番,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼耍属!你這毒婦竟也來了托嚣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤厚骗,失蹤者是張志新(化名)和其女友劉穎示启,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體领舰,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夫嗓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年迟螺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啤月。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡煮仇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谎仲,到底是詐尸還是另有隱情浙垫,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布郑诺,位于F島的核電站夹姥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏辙诞。R本人自食惡果不足惜辙售,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望飞涂。 院中可真熱鬧旦部,春花似錦、人聲如沸较店。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽梁呈。三九已至婚度,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間官卡,已是汗流浹背蝗茁。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寻咒,地道東北人哮翘。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像毛秘,于是被迫代替她去往敵國(guó)和親饭寺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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