JetPack知識(shí)點(diǎn)實(shí)戰(zhàn)系列十:ExoPlayer進(jìn)行視頻播放的實(shí)現(xiàn)

本節(jié)教程我們將來(lái)介紹下ExoPlayer的視頻播放功能选调。

效果

我們?cè)诒竟?jié)將主要介紹以下知識(shí)點(diǎn):

  1. ExoPlayer高級(jí)自定義的實(shí)現(xiàn)
  2. 視頻的全屏播放和退出全屏播放
  3. ExoPlayer在RecyclerView中的復(fù)用

ExoPlayer介紹

MediaPlayerExoPlayer是Google官方支持的兩種播放器,但是ExoPlayerMediaPlayer多了支持基于 HTTP 的動(dòng)態(tài)自適應(yīng)流 (DASH)、SmoothStreaming 和通用加密等功能吨灭。

并且重要的是它獨(dú)立于Android代碼框架,以一個(gè)開(kāi)源代碼庫(kù)的形式存在,所以在自定義上更有優(yōu)勢(shì)。

ExoPlayer簡(jiǎn)單的使用方法

  • 引入依賴庫(kù)
implementation 'com.google.android.exoplayer:exoplayer:2.12.0'
  • 布局中引入PlayerView

播放視頻我們需要使用PlayerView泣特,我們簡(jiǎn)單來(lái)看下PlayerView的源碼,其繼承于FrameLayout挑随,其中有三個(gè)重要的屬性状您,

public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
    @Nullable private final View surfaceView;
    @Nullable private final PlayerControlView controller;
    private Player player;
}
  1. surfaceView是呈現(xiàn)視頻的View,可以是TextureView兜挨,SurfaceView, 默認(rèn)是SurfaceView膏孟。
  2. controller是播放控制的View,上面提供一些控件可以控制視頻的播放暑劝,暫停骆莹,顯示當(dāng)前進(jìn)度等颗搂。默認(rèn)是PlayerControlView担猛。
  3. player 是視頻的播放器,在構(gòu)造函數(shù)初始化的時(shí)候沒(méi)有賦值丢氢,需要單獨(dú)設(shè)置傅联。

總結(jié):PlayerView通過(guò)player播放視頻顯示在surfaceView上,用戶可以通過(guò)提供的controller進(jìn)行播放的控制疚察。

介紹了基本的知識(shí)點(diǎn)后蒸走,我們?cè)诓季治募幸?strong>PlayerView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".DefaultViewActivity">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/video_player"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:show_buffering="always"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  • 設(shè)置播放器
val player: SimpleExoPlayer = SimpleExoPlayer.Builder(this@MainActivity).build().also { it.playWhenReady = true }
video_player.player = player

我們前面提到PlayerView的兩個(gè)屬性在構(gòu)造函數(shù)調(diào)用時(shí)賦值了,但是player沒(méi)有貌嫡,需要主動(dòng)設(shè)置比驻。這里我們?cè)O(shè)置成SimpleExoPlayer對(duì)象。

SimpleExoPlayer是庫(kù)中提供的播放器岛抄,可以直接使用别惦。

  • 設(shè)置播放源
// play item
val uri = Uri.parse("https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4")
val dataSourceFactory = DefaultHttpDataSourceFactory()
val videoSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
// prepare
player.prepare(videoSource)
  • 監(jiān)聽(tīng)播放器的狀態(tài)

我們可以監(jiān)聽(tīng)播放器的狀態(tài),代碼如下:

player.addListener(object: Player.EventListener {
    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
    Log.d("JJMusic","playWhenReady: $playWhenReady playbackState: $playbackState")
    when (playbackState) {
        Player.STATE_BUFFERING ->
            Log.d("JJMusic","加載中")
        Player.STATE_READY ->
            Log.d("JJMusic","準(zhǔn)備完畢")
        Player.STATE_ENDED ->
            Log.d("JJMusic","播放完成")
        }
    }

    override fun onPlayerError(error: ExoPlaybackException) {
        Log.e("JJMusic","ExoPlaybackException: $error")
    }
})

最后得到的效果如下所示:

默認(rèn)控制器

ExoPlayer簡(jiǎn)單自定義

我們目前使用的是默認(rèn)的播放控制布局文件夫椭,我們可以修改播放的布局文件達(dá)到自定義效果掸掸。

  • 自定義播放控制的布局文件

假設(shè)我們把布局文件設(shè)計(jì)如下所示:

<!-- layout_video_simple.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/constraint"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/exo_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@null"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@mipmap/exo_btn_play" />

    <ImageView
        android:id="@+id/exo_pause"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@null"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@mipmap/exo_btn_pause" />

    <TextView
        android:id="@+id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="6dp"
        android:layout_marginBottom="12dp"
        android:contentDescription="@null"
        android:text="1"
        android:textColor="@color/colorPrimary"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/splash_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="1dp"
        android:layout_marginBottom="12dp"
        android:contentDescription="@null"
        android:text="/"
        android:textColor="@color/colorPrimary"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        tools:text="/" />

    <TextView
        android:id="@+id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="1dp"
        android:layout_marginBottom="12dp"
        android:contentDescription="@null"
        android:text="1"
        android:textColor="@color/colorPrimary"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@+id/splash_tv" />

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@+id/exo_progress"
        android:layout_width="0dp"
        android:layout_height="15dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:bar_height="2dp"
        app:unplayed_color="@color/exo_gray_ripple"
        app:played_color="@color/colorAccent"
        app:scrubber_color="@color/colorAccent"
        app:buffered_color="@color/colorPrimary"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. idexo_play的按鈕和idexo_pause的按鈕在屏幕正中間位置
  2. idexo_position的文本和idexo_duration的文本在左下角
  3. idexo_progress的進(jìn)度條在最底部。進(jìn)度條的類是DefaultTimeBar蹭秋,可以設(shè)置一些屬性扰付。譬如上面的bar_height(進(jìn)度條的高度),unplayed_color(未緩沖部分的顏色),played_color(已播放部分的顏色)和buffered_color(已緩沖完部分的顏色)等等。

注意:這些idPlayerControlView源代碼中能找到的id,否則是沒(méi)有效果的仁讨。

  • 修改PlayerView布局文件
<com.google.android.exoplayer2.ui.PlayerView
    ...
    app:controller_layout_id="@layout/layout_video_simple"
    />

其他的和前面的類似羽莺,只是加了個(gè)屬性controller_layout_id,值為我們剛才設(shè)計(jì)的布局文件layout_video_simple洞豁。

簡(jiǎn)單自定義得到的效果如下所示:

簡(jiǎn)單自定義

ExoPlayer高級(jí)自定義

簡(jiǎn)單的自定義我們只是更改了PlayerControlView的布局文件盐固,復(fù)用了其中的id屠橄,能修改的很有限,沒(méi)有涉及到源代碼的修改闰挡。

高級(jí)自定義就需要修改源代碼了锐墙。其實(shí)就是修改PlayerViewPlayerControlView长酗,甚至是TimeBar的源代碼溪北。

接下來(lái)我們就用高級(jí)自定義來(lái)實(shí)現(xiàn)下網(wǎng)易云音樂(lè)的全屏播放功能夺脾,需要的效果如下:

網(wǎng)易云音樂(lè)效果
  • 修改PlayerControlView

新建一個(gè)JJPlayerControlView類之拨,然后將PlayerControlView所有源代碼拷貝在這個(gè)類中。

public class JJPlayerControlView extends FrameLayout {
    // PlayerControlView內(nèi)容
}

接下來(lái)在JJPlayerControlView中加入一個(gè)全屏按鈕屬性咧叭。

public class JJPlayerControlView extends FrameLayout {
    // 全屏按鈕
    private final ImageButton maxButton;
    // PlayerControlView內(nèi)容
    public JJPlayerControlView(
            Context context,
            @Nullable AttributeSet attrs,
            int defStyleAttr,
            @Nullable AttributeSet playbackAttrs) {
        ...
        maxButton = findViewById(R.id.exo_max_btn);
        if (maxButton != null) {
            maxButton.setOnClickListener(componentListener);
        }
        ...
    }
}
  • 修改PlayerView

新建一個(gè)JJPlayerView類蚀乔,然后將PlayerView所有源代碼拷貝在這個(gè)類中。

public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
// PlayerView的內(nèi)容
}

JJPlayerViewcontroller指定為JJPlayerControlView菲茬,即:

public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
    @Nullable private final JJPlayerControlView controller;
    // PlayerView的其他內(nèi)容
}
  • 修改TimeBar

如果需要修改進(jìn)度條吉挣,新建一個(gè)JJTimeBar類,然后將DefaultTimeBar所有源代碼拷貝在這個(gè)類中婉弹。

public class JJTimeBar extends View implements TimeBar {
    ...
}

當(dāng)然修改將JJPlayerControlView中的timeBar改為JJTimeBar類睬魂。

public class JJPlayerControlView extends FrameLayout {
    // 全屏按鈕
    private final ImageButton maxButton;
    // 自定義進(jìn)度條
    @Nullable private JJTimeBar timeBar;
    // PlayerControlView內(nèi)容
    public JJPlayerControlView(
            Context context,
            @Nullable AttributeSet attrs,
            int defStyleAttr,
            @Nullable AttributeSet playbackAttrs) {
        ...
        maxButton = findViewById(R.id.exo_max_btn);
        if (maxButton != null) {
            maxButton.setOnClickListener(componentListener);
        }
        ...
    }
}
  • 修改JJPlayerControlView布局文件
自定義的布局文件

layout_video_recyclerview.xml相對(duì)前面,我們多添加了一個(gè)idexo_max_btn的按鈕镀赌。

為了看的更加明顯氯哮,我把其他的按鈕或者文本的id都改了,不再使用默認(rèn)的id商佛,這時(shí)候?yàn)榱苏业綄?duì)應(yīng)的控件喉钢,就需要修改對(duì)應(yīng)的源代碼了。譬如我把播放按鈕的id改為了exo_play_btn良姆。

public class JJPlayerControlView extends FrameLayout {
    // 代碼修改
    playButton = findViewById(R.id.exo_play_btn);
    if (playButton != null) {
        playButton.setOnClickListener(componentListener);
    }
}
  • JJPlayerView布局文件

JJPlayerView使用JJPlayerControlView自定義的布局文件

<com.johnny.jjmusic.exoplayer.JJPlayerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:show_buffering="always"
    app:controller_layout_id="@layout/layout_video_recyclerview"
    >

</com.johnny.jjmusic.exoplayer.JJPlayerView>
  • 全屏和退出全屏的實(shí)現(xiàn)邏輯

我們先來(lái)看一張圖就能很清晰的了解全屏和退出全屏的邏輯了:

全屏邏輯

全屏的時(shí)候JJPlayerView放在ActivityR.id.content上肠虽,隱藏ActionBar,切換成橫屏顯示歇盼,退出全屏的時(shí)候就重新放在RecyclerViewItemView上舔痕,顯示ActionBar,切換成豎屏顯示豹缀。

所以最后很簡(jiǎn)單伯复,只要處理maxButton點(diǎn)擊事件時(shí)實(shí)現(xiàn)這個(gè)功能就可以了。

進(jìn)入全屏播放

fun enterFullScreen() {
    // 橫豎屏狀態(tài)判斷
    if (viewModel.playMode == VideoPlayMode.MODE_FULL_SCREEN) return
    // 隱藏ActionBar
    playerView.context.hideActionBar()
    // 旋轉(zhuǎn)屏幕
    playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
    // 將JJPlayerView從RecyclerView移除邢笙,加入Activity的R.id.content下
    playerView.context.activity?.let {
        val contentView = it.findViewById<ViewGroup>(android.R.id.content)
        // remove
        removePlayerView()
        viewModel.isVideoViewAdded = true

        // add
        val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        contentView.addView(playerView, params)

        val frameParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        playerView.controller?.timeBarContainer?.addView(timeBar, frameParams)

        viewModel.playMode = VideoPlayMode.MODE_FULL_SCREEN
    }

}

退出全屏播放

/* 退出全屏 */
    fun exitFullScreen() {
    // 橫豎屏狀態(tài)判斷
    if (viewModel.playMode == VideoPlayMode.MODE_NORMAL) return
    // 顯示ActionBar
    playerView.context.showActionBar()
    // 旋轉(zhuǎn)屏幕
    playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
    // 將JJPlayerView從Activity的R.id.content移除啸如,加入RecyclerView的ItemView下
    playerView.context.activity?.let {
        // remove
        val contentView = it.findViewById<ViewGroup>(android.R.id.content)
        contentView.removeView(playerView)
        playerView.controller?.timeBarContainer?.removeView(timeBar)

        // add
        viewModel.viewModelScope.launch {
            delay(100)
            addPlayerView()
        }

        viewModel.playMode = VideoPlayMode.MODE_NORMAL
    }
}

上面代碼中涉及到的幾個(gè)擴(kuò)展方法,也一同貼出來(lái):

//----------Activity----------
val Context.activity: Activity?
    get() {
        return when (this) {
            is Activity -> {
                this
            }
            is ContextWrapper -> {
                this.baseContext.activity
            }
            else -> {
                null
            }
        }
    }

val Context.appCompActivity: AppCompatActivity?
    get() {
        return when (this) {
            is AppCompatActivity -> {
                this
            }
            is ContextThemeWrapper -> {
                this.baseContext.appCompActivity
            }
            else -> {
                null
            }
        }
    }

//---------- ActionBar ----------
@SuppressLint("RestrictedApi")
fun Context.showActionBar() {
    this.appCompActivity?.supportActionBar?.let {
        it.setShowHideAnimationEnabled(false)
        it.show()
    }
    this.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}

@SuppressLint("RestrictedApi")
fun Context.hideActionBar() {
    this.appCompActivity?.supportActionBar?.let {
        it.setShowHideAnimationEnabled(false)
        it.hide()
    }
    this.activity?.window?.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
}

至此ExoPlayer的高級(jí)自定義就到此為止了氮惯。

由于可以修改源碼叮雳,所以進(jìn)行高度自定義就變得可實(shí)現(xiàn)了想暗。當(dāng)然是在熟悉源碼的前提下進(jìn)行修改。

ExoPlayer在RecyclerView中的復(fù)用

復(fù)用

上面的實(shí)現(xiàn)效果中帘不,我們點(diǎn)擊RecyclerView不同的Item说莫,都能播放視頻,如果每個(gè)ItemView都有一個(gè)PlayerView那是非常不合適的寞焙。對(duì)PlayerView是一個(gè)非常合適的解決方案储狭。

其實(shí)這個(gè)解決方案和全屏的方案也非常相似,就是將PlayerView在不同的Item中移除和加入捣郊。然后播放新的視頻辽狈。

其中有一些細(xì)節(jié)需要處理,譬如播放的進(jìn)度需要記錄下來(lái)呛牲,下次再點(diǎn)擊的時(shí)候從上次停止的地方進(jìn)行播放刮萌。還譬如需要監(jiān)聽(tīng)RecyclerView.OnChildAttachStateChangeListener,當(dāng)執(zhí)行onChildViewDetachedFromWindow時(shí)候娘扩,如果在播放需要將播放器停止着茸。等等

有了思路,解決起來(lái)也就很簡(jiǎn)單了畜侦。這里不再貼代碼了元扔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市旋膳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌途事,老刑警劉巖验懊,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異尸变,居然都是意外死亡义图,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)召烂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碱工,“玉大人,你說(shuō)我怎么就攤上這事奏夫∨屡瘢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵酗昼,是天一觀的道長(zhǎng)廊谓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)麻削,這世上最難降的妖魔是什么蒸痹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任春弥,我火速辦了婚禮,結(jié)果婚禮上叠荠,老公的妹妹穿的比我還像新娘匿沛。我一直安慰自己,他們只是感情好榛鼎,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布俺祠。 她就那樣靜靜地躺著,像睡著了一般借帘。 火紅的嫁衣襯著肌膚如雪蜘渣。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天肺然,我揣著相機(jī)與錄音蔫缸,去河邊找鬼。 笑死际起,一個(gè)胖子當(dāng)著我的面吹牛拾碌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播街望,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼校翔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了灾前?” 一聲冷哼從身側(cè)響起防症,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哎甲,沒(méi)想到半個(gè)月后蔫敲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡炭玫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年奈嘿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吞加。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡裙犹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出衔憨,到底是詐尸還是另有隱情叶圃,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布巫财,位于F島的核電站盗似,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏平项。R本人自食惡果不足惜赫舒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一悍及、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧接癌,春花似錦心赶、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至荔燎,卻和暖如春耻姥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背有咨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工琐簇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人座享。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓婉商,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親渣叛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子丈秩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359