播放器相关知识点详解

       最近的项目需要实现视频的播放功能,底层的播放实现需要兼容常规的url视频源播放和基于搜狐SDK的视频源播放(视频id),通过了解视频播放的相关知识点,总结了一下视频相关知识。 


播放器控件相关知识点汇总:

一:播放器相关基础知识点介绍

二:Android中视频播放器的选择

三:实现视频播放的几种方式介绍

四:播放器控件相关开源项目介绍

五:播放器的常见使用场景

六:手撸播放器控件遇到的问题

七:播放器不同模块代码的解耦

八:播放器控件实现思路建议

九:遇到相关疑难杂症

十:扩展阅读


一:播放器相关基础知识点介绍

      视频播放的流程常规的视频播放分为传输,解封装,解码,绘制四个步骤,以下按播放网站上(HTTP)的mp4文件为例,简单介绍一下几个过程:

HTTP传输:

描述:播放器使用HTTP协议把MP4下载下来,这部分需求一般需要边下边播,服务器的HttpServer一定要支持HTTPSeek,因为播放的过程中需要跳转到不同的位置下载内容,比如MP4结构中的moov元数据信息在很多视频文件中都被放置在文件末尾了。


MP4解封包:

描述:这部分我们常见的封包格式就是mp4,视频编码后H264数据被拆分为多个片段封包到mp4中,解封包就是从mp4中解析出H264视频裸码流的过程。


H264解码:

描述:视频解码就是将h264裸码流解析成视频像素数据的过程(一般是yuv,也可以是rgba,类似于将jpg图片解码为bitmap数据。h264格式可以使用Android系统提供MediaCoder硬解码,也可以使用FFMpeg进行软解码。


FFmpeg介绍:

描述:是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。


硬解码和软解码:

一、软解码和硬解码如何区分

软解码:使用CPU进行解码

硬解码:使用非CPU进行解码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等


二、软解码和硬解码比较

软解码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬解码低,低码率下质量通常比硬解码要好一点。

硬解码:性能高,低码率下通常质量低于硬解码器,但部分产品在GPU硬件平台移植了优秀的软解码算法(如X264)的,质量基本等同于软解码。


优缺点对比:

结论:

1、根据项目的需要,现在几乎所有的设备都支持硬解码和软解码,之前更多的人愿意选择软解码,更大的原因是因为硬件解码支持的格式较少,而软解码对于格式是不受限制的。

2、现在随着硬件的不断提高,解码技术的不断成熟和完善,我是更倾向硬解码,但硬件提升的同时,CPU也在不断的优化和提高,现在也不需要像之前那样尽可能节省CPU,现在处于性能过剩的时代,CPU已经很难处于负荷状态,选择软解码或者硬解码都是没有谁对谁错,刚刚图上已经贴出和标记两者的优点,根据项目需要选择。


图像渲染:SurfaceView、TextureView、GLSurfaceView、SurfaceTexture的区别

一、SurfaceView

       是一个可以在子线程中更新UI的View,且不会影响到主线程。它为自己创建了一个窗口(window),就好像在视图层次(View Hierarchy)上穿了个“洞”,让绘图层(Surface)直接显示出来。但是,和常规视图(view)不同,它没有动画或者变形特效,一些View的特性也无法使用。

概括:

1、SurfaceView独立于视图层次(View Hierarchy),拥有自己的绘图层(Surface),但也没有一些常规视图(View)的特性,如动画等。

2、SurfaceView的实现中具有两个绘图层(Surface),即我们所说的双缓冲机制。我们的绘制发生在后台画布上,并通过交换前后台画布来刷新画面,可避免局部刷新带来的闪烁,也提高了渲染效率。

3、SurfaceView中的SurfaceHolder是Surface的持有者和管理控制者。

4、SurfaceHolder.Callback的各个回调发生在主线程。


二、GLSurfaceView

       继承SurfaceView,除了拥有SurfaceView所有特性外,还加入了EGL(EGL是OpenGL ES和原生窗口系统之间的桥梁) 的管理,并自带了一个单独的渲染线程。

概括:

1、继承自SurfaceView,拥有其所有特性。

2、加入了EGL管理,是SurfaceView应用OpenGL ES的典型场景。

3、有单独的渲染线程GLThread。

4、单独出了Renderer接口负责实际渲染,不同的Renderer实现相当于不同的渲染策略,使用方式灵活(策略模式)。


三、SurfaceTexture

       Android3.0(API 11)新加入的一个类,不同于SurfaceView会将图像显示在屏幕上,SurfaceTexture对图像流的处理并不直接显示,而是转为GL外部纹理。

概括:

1、SurfaceTexture可以从图像流(相机、视频)中捕获帧数据用作OpenGL ES外部纹理(GL_TEXTURE_EXTERNAL_OES),实现无缝连接。

2、我们可以很方便的对这个外部纹理进行二次处理(如添加滤镜等)。

3、输出目标是Camera或MediaPlayer时,可以用SurfaceTexture代替SurfaceHolder,这样图像流将把所有帧传给SurfaceTexture而不是显示在设备上。

4、使用updateTexImage()方法更新最新的图像。


四、TextureView

      TextureView是Android4.0(API 14)引入,它必须使用在开启了硬件加速的窗体中。除了拥有SurfaceView的特性外,它还可以进行像常规视图(View)那样进行平移、缩放等动画。

概括:

1、必须开启硬件加速(这个默认就是开启的)。

2、可以像常规视图(View)那样使用它,包括进行平移、缩放等操作。

3、TextureView重载了draw()方法,主要是使用SurfaceTexture中收到的图像数据作为纹理更新到对应的HardwareLayer中。

4、通过SurfaceTextureListener接口让使用者知道SurfaceTexture的各种状态。


SurfaceView对比TextureView:


Open GL,Open GL ES与Android EGL介绍:

介绍:OpenGL ES(OpenGL for Embedded Systems,以下简称OpenGL)

介绍:是OpenGL三维图形API的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。该API由Khronos集团定义推广,Khronos是一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准。


Android EGL:

介绍:EGL是介于诸如OpenGL或OpenVG的Khronos渲染API与底层本地平台窗口系统的接口,是OpenGL ES和本地窗口系统(Native Window System)之间的通信接口,它被用于处理图形管理、表面/缓冲捆绑、渲染同步及支援使用其他KhronosAPI进行的高效、加速、混合模式2D和3D渲染。

EGL的主要功能:

EGL是用来管理绘图表面(Drawing surfaces),并且提供了如下的机制

1、与本地窗口系统进行通信

2、查找绘图表面可用的类型和配置信息

3、创建绘图表面

4、同步OpenGL ES 2.0和其他的渲染API(Open VG、本地窗口系统的绘图命令等)

5、管理渲染资源,比如材质


二:Android中视频播放器的选择

1、MediaPlayer

描述:在Android系统中对于视频播放器有原生的实现MediaPlayer,以及将MediaPlayer和SurfaceView封装在一起的VideoView,两者都只是使用硬解码播放,基本上只支持本地和HTTP协议的视频播放,扩展性都很差,只适合最简单的视频播放需求。


2、ExoPlayer

描述:谷歌后来有开源了一个播放器项目[ExoPlayer])(https://github.com/google/ExoPlayer),提供了更好的扩展性和定制能力,并加入了对DASH和HLS等直播协议的支持,但也只支持硬解码,如果项目中只需要支持对H264格式的视频播放,以及流媒体协议比较常规(比如HTTP,HLS),基于ExoPlayer定制也是不错的选择。 

       与Android内置的MediaPlayer相比,ExoPlayer具有许多优点:

1、支持HTTP上的动态自适应流DASH和SmoothStreaming。

2、支持高级的HLS特点,例如正确的处理#EXT-X-DISCONTINUITY标签。

3、能够无缝的合并,串联,循环播放媒体文件。

4、能够被高度扩展和定制,以适用不同的场景,ExoPlayer专门设计了这一点,大部分组件都可以自己替换。(可以自定义视频缓存,视频进度实时回调)

5、各个组件可以自定义,还可以接入ffmpeg组件,基本能满足99.9%的需求。(具体详情去官网了解)

缺点:

1、在某些设备上播放音频,ExoPlayer可能会比MediaPlayer消耗更多的电量。

2、最低支持版本4.4 

3、实现比较复杂


3、ijkplayer

描述:ijkplayer是Bilibili公司开源的播放器实现,整合了FFMpeg,ExoPlayer,MediaPlayer等多种实现,提供了类似于MediaPlayer的API,可以实现软硬解码自由切换,自定义TextureView实现,同时得益于FFMpeg的能力,也能支持多种流媒体协议(RTSP,RTMP,HLS等),多种视频编码格式(h264, mpeg4,mjpeg),具有很高的灵活性,可以定制实现自己特色的播放器(比如支持视频缩放,视频翻转等)。


三:实现视频播放的几种方式介绍

一:MediaController+VideoView实现方式(灵活度低,SDK已包含)

介绍:这种方式是最简单的实现方式,VideoView继承了SurfaceView同时实现了MediaPlayerControl接口,MediaController则是安卓封装的辅助控制器,带有暂停,播放,停止,进度条等控件。通过VideoView+MediaController可以很轻松的实现视频播放、停止、快进、快退等功能,虽然VideoView的实现方式很简单,但是由于是自带的封装好的类,所以无论是播放器的大小、位置以及控制都不受我们控制。


二:MediaPlayer+SurfaceView+MediaController(灵活度中,SDK已包含)

描述:MediaController是安卓封装的辅助控制器,带有暂停,播放,停止,进度条等控件,播放实现使用SurfaceView作为播放容器实现,播放的控制使用MediaPlayer实现,这种方式由于使用了MediaController,同样无法定制播放器控件,不过使用MediaPlayer实现播放,提供了视频播放时更多的回调函数,可以实现更多元化的需求,例如:回调播放视频的宽高,视频播放开始,停止,完成等回调。


三:MediaPlayer+SurfaceView+自定义控制器(灵活度高,SDK已包含)

描述:使用MediaPlayer实现视频播放,可以提供视频播放时状态的多种回调,然后不使用MediaController,而是使用自定义的控制器,实现更加灵活的界面需求。


四:使用Exo,IJKPlayer,FFMPEG等开源实现(灵活度极高,接入相对复杂,引入外部jar,aar或者Module)

描述:与Android内置的MediaPlayer相比,ExoPlayer具有许多优点:

1、支持HTTP上的动态自适应流DASH和SmoothStreaming。

2、支持高级的HLS特点,例如正确的处理#EXT-X-DISCONTINUITY标签。

3、能够无缝的合并,串联,循环播放媒体文件。

4、能够被高度扩展和定制,以适用不同的场景,ExoPlayer专门设计了这一点,大部分组件都可以自己替换。(例如:视频缓存的定制)

5、各个组件可以自定义,还可以接入ffmpeg组件,基本能满足99.9%的需求。(具体详情去官网了解)


五:使用爱奇艺,腾讯,优酷,搜狐视频SDK(灵活度取决于SDK开放接口)

描述:视频播放的相关数据及状态,不同的SDK会有不同的接口设计,接入一般有官方demo,定制的东西相对较少,接入相对简单,一般使用视频资源id标识资源。


四:播放器控件相关开源项目介绍

一:ijkplayer(26.5k)

项目地址:https://github.com/Bilibili/ijkplayer

介绍:Ijkplayer是Bilibili发布的基于FFplay的轻量级Android/iOS视频播放器。实现了跨平台功能,API易于集成;编译配置可裁剪,方便控制安装包大小;支持硬件加速解码,更加省电;提供Android平台下应用弹幕集成的解决方案。


二:ExoPlayer(15.6k)

项目地址:https://github.com/google/ExoPlayer

介绍:这款由YouTube开发的播放器真的是非常强大。对于自定义播放器非常友好,里面讲很多模块抽象成独立的组件可供使用者自行定制,当然官方也提供了一些默认的实现。

优点:

1、在不同Android版本和不同的手机设备上拥有更统一的行为表现,更少的设备差异带来的问题。

2、作为一个独立的库,可以很轻易的升级。

3、可以根据用户的需求方便的对播放器行为进行定制和扩展,ExoPlayer中的很多组件都支持自定义和扩展。

4、支持播放视频列表,并且可以支持对视频的裁剪、合并,以及循环播放设置。

5、支持更多的视频格式,包括MediaPlayer不支持的DASH、SmoothStreaming。

6、支持Widevine功能,这个功能可以下载和播放经过Google加密的视频文件。

7、能够方便的集成额外的扩展库,比如IMA扩展库。

缺点:

1、相比于Android原生的MediaPlayer,ExoPlayer将显著的消耗更多的电量

2、集成ExoPlayer将对你的APP包体增加几百KB的大小


三:GSYVideoPlayer(14.1k)

项目地址:https://github.com/CarGuo/GSYVideoPlayer

介绍:视频播放器,支持基本的拖动,声音、亮度调节,支持边播边缓存,支持视频本身自带rotation的旋转(90,270之类),重力旋转与手动旋转的同步支持,支持列表播放,直接添加控件为封面,列表全屏动画。

主要特点:

1、支持基本的拖动,声音、亮度调节。

2、支持边播边缓存,使用了AndroidVideoCache。(https://github.com/danikula/AndroidVideoCache

3、支持视频本身自带rotation的旋转。

4、增加了重力旋转与手动旋转的同步支持。

5、支持列表播放。

6、直接添加控件为封面。

7、全屏和播放等的动画效果。

8、列表的全屏效果优化,多种配置模式。

9、列表的小窗口播放,可拖动。

10、网络视频加载速度。

11、锁定/解锁全屏点击功能。

12、支持快播和慢播。

13、调整显示比例:默认、16:9、4:3。

14、调整不同清晰度的支持。

15、支持IJKPlayer和EXOPlayer切换。

16、进度条小窗口预览(测试)。

17、Https支持。

18、连续播放一个列表的视频。

19、支持全屏与非全屏两套布局切换

20、弹幕支持


四:JieCaoVideoPlayer(10.1k)

项目地址:https://github.com/lipangit/JieCaoVideoPlayer(JiaoZiVideoPlayer)

介绍:节操视频播放器是一个让开发者可以三两行代码就能集成到应用中的视频播放框架,并且提供了开放的接口来满足不同开发者的不同需求。

JiaoZiVideo主要特点:

1、可以完全自定义UI和任何功能

2、一行代码切换播放引擎,支持的视频格式和协议取决于播放引擎,android.media.MediaPlayerijkplayer

3、完美检测列表滑动

4、可实现全屏播放,小窗播放

5、能在ListView、ViewPager和ListView、ViewPager和Fragment等多重嵌套模式下全屏工作

6、可以在加载、暂停、播放等各种状态中正常进入全屏和退出全屏

7、多种视频适配屏幕的方式,可铺满全屏,可以全屏剪裁

8、重力感应自动进入全屏

9、全屏后手势修改进度和音量。

10、Home键退出界面暂停播放,返回界面继续播放。


Android主流开源视频播放器对比:(时间节点:2019年6月)

优缺点对比:

市面上热度最高的开源播放器就是ijkplayer和Exoplayer,其他大多数是在它们的基础上进行二次封装。

五:播放器的常见使用场景

场景一:一个Activity就显示一个视频控件

解析:如果需要实现点击全屏显示功能,需要添加辅助类:OrientationUtils,实现该

场景转屏相关逻辑实现。(参考开源项目:GSYVideoPlayer)


场景二:视频控件嵌套在列表的中

解析:当视频控件嵌套在列表里面时的注意点:

1、列表滑动无法看到视频时,视频停止播放。

2、列表控件全屏播放时,需要停止列表中的小视频播放,把全屏视频布局覆盖在界面顶部,并播放全屏视频。

3、需要在列表Adapter中,添加视频相关逻辑,例如全屏按钮点击时,列表刷新等操作需要添加辅助类:GSYVideoHelper和内部类GSYVideoHelper.GSYVideoHelperBuilder实现该场景下的视频播放实现。


场景三:在详情页,视频控件固定在顶部

解析:如果需要实现手动和屏幕旋转时,全屏播放视频,需要添加辅助类OrientationUtils。

实现相关功能如:

1、在进入界面视频未开始播放时,禁止手机的旋转功能。(不然会乱屏)

2、需要在视频开始播放后,开启手机旋转功能。

3、需要OrientationUtils类在onConfigurationChanged函数中,传入播放器对象实现屏幕旋转的监听。


六、手撸播放器控件遇到的问题汇总

5.1、全屏播放的实现(注意状态栏与导航栏的系统版主适配)

5.2、音频焦点抢占问题(注意系统版主适配)

5.3、横竖屏手动和被动切换注意点

5.4、Activity或Fragment在不同状态下的,视频控件如何适配,关闭屏幕,来电,切换到后台,退出等处理。

(与Activity与Fragment生命周期的联动)

5.5、自定义时间进度条的实现(ProgressTimeBar)

5.6、兼容不同底层播放能力(EXO,FFMPEG,SohuSdk等)

5.7、不同状态在是否需要自动息屏

5.8、不同状态的切换(使用Visible与InVisible或使用addView与removeView)

5.9、视频的自动宽高适配


七:播放器不同模块代码的解耦

6.1、底层播放能力的解耦,单独实现,使用策列模式实现(EXO和SohuSdk,后续添加不同的视频源SDK)

6.2、手势操作的解耦(滑动调节,单击,双击的监听与响应事件)

6.3、各种响应对话框单独分离出来

6.4、视频状态的控制相关操作(暂停,继续等操作)

八、播放器控件实现思路建议(个人理解,仅供参考)

1、如果在Android与IOS需要有统一的标准,并需要对特定流媒体协议,特定视频编码有一定的要求,建议优先参考ijkPlayer。

2、如果只需要实现底层播放,且项目视频播放模块需要大量定制功能的话,建议查看ExoPlayer和jiaoziPlayer,大量定制功能需要自己实现。

3、如果需要一个较为完善,已经实现了常见视频播放控件的相关功能(爱奇艺,优酷,腾讯视频的视频控件),并实现了播放器控件的各种场景下的使用demo,GSYPlayer是首选,此外,GSY也有实现了对EXO和IJKPlayer的封装实现分支。


九:遇到相关疑难杂症(持续更新)

问题一:在调节亮度和音量时,弹出的对话框使用getContext()或外部传入的Context,在使用外部传入的Context时,对话框布局底部会被截取一小段。

解决方案:统一使用getContext()获取。


问题二:在Activity中的Ondestroy中执行视频控件的Release()函数,发现如果在release()函数中才放弃音频焦点的话,会使得Activity的OnDestroy会延迟执行,甚至退出Activity,重新启动其他Activity时,其他Activity都初始化了,销毁的Activity的OnDestroy函数还没有执行。

解决方案:在视频控件的OnPause中就放弃音频焦点。


十:扩展阅读

1、https://blog.csdn.net/u010072711/article/details/51517170(Android视频播放器实现小窗口和全屏状态切换)

2、https://blog.csdn.net/qq_25955641/article/details/89790669(Android播放器的三种实现方法)

3、https://blog.csdn.net/liuzhi0724/article/details/81318816?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-1(Android实现视频播放的3种实现方式)

4、https://www.jianshu.com/p/53581512ba3f(github上十二款最著名的Android播放器开源项目)

5、https://blog.csdn.net/qq_34895720/article/details/101511876(Android中视频播放器的选择,MediaPlayer、ExoPlayer、ijkplayer简单对比)

6、http://www.voidcn.com/article/p-ktividrq-tk.html(MP4文件格式解析)

7、https://www.jianshu.com/p/291ff6ddc164(TextureView+SurfaceTexture+OpenGLES来播放视频(三)

8、https://blog.csdn.net/charleslei/article/details/44599041(简单谈谈硬编码和软编码)

9、https://blog.csdn.net/pangpang123654/article/details/78125038?utm_medium=distribute.pc_relevant.

none-task-blog-baidujs-1(硬解码与软解码的选择)

10、https://blog.csdn.net/afei__/article/details/100023701?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-8.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-8.nonecase(浅谈SurfaceView、TextureView、GLSurfaceView、SurfaceTexture)

11、https://blog.csdn.net/m475664483/article/details/52998445(SurfaceTexture,TextureView,SurfaceView和GLSurfaceView的区别)

12、https://blog.csdn.net/aa642531/article/details/93230076(Android主流开源视频播放器对比)

13、https://zhuanlan.zhihu.com/p/115220766(Android OpenGL ES系列连载:(06)EGL)

最後編輯於
©著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama閱讀 158,560評論 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama閱讀 67,104評論 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人閱讀 108,297評論 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人閱讀 43,869評論 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事閱讀 52,275評論 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人閱讀 40,563評論 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama閱讀 31,833評論 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人閱讀 30,543評論 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama閱讀 34,245評論 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事閱讀 30,512評論 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事閱讀 32,011評論 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama閱讀 28,359評論 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事閱讀 33,006評論 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人閱讀 26,062評論 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人閱讀 26,825評論 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama閱讀 35,590評論 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事閱讀 35,501評論 2 268