AVPlayer视频播放之 - AVPlayer

AVPlayer是驱动播放用例的中心阶层,是用于管理媒体资产的回放和定时的控制器对象。它提供了控制播放器传输行为的界面,例如播放,暂停,改变播放速度的能力,以及在媒体时间线内寻找各个时间点的能力,主要使用一个AVPlayer播放本地和远程基于文件的媒体。AVPlayer一次只能播放一个媒体资源。播放器可以使用其- (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item方法被重新使用来播放额外的媒体资产,但是它一次只管理单个媒体资产的播放。该框架还提供了一个 AVQueuePlayer 子类,该类可以使用来创建和管理媒体资产的队列,以便按顺序播放。

  • + (instancetype)playerWithURL:(NSURL *)URL / - (instancetype)initWithURL:(NSURL *)URL 根据给定URL生成单个的视听资源,会隐式创建一个AVPlayerItem;

  • + (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item / - (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item根据Item生成视听资源 ;

  • @property (nonatomic, readonly) AVPlayerStatus status指示播放器是否可用于播放,KVO重点观测属性;

typedef NS_ENUM(NSInteger, AVPlayerStatus) {
    AVPlayerStatusUnknown, //表示播放器的状态尚未知道,因为它尚未尝试加载新的媒体资源
    AVPlayerStatusReadyToPlay, // 表示播放器已准备好播放AVPlayerItem实例
    AVPlayerStatusFailed // 表示播放器由于错误而不能再播放AVPlayerItem实例
};
  • @property (nonatomic, readonly, nullable) NSError *error当状态是AVPlayerStatusFailed时描述导致失败的错误,其他情况为nil;

-@property (nonatomic, readonly) BOOL outputObscuredDueToInsufficientExternalProtection由于外部保护不足,解码输出是否被遮蔽,指因为当前设备配置不满足外部保护机制的要求,播放器是否有意模糊当前项目的视觉输出。可观察属性;

播放控制相关⏬

  • @property (nonatomic) float rate;目前的播放速度,0.0值暂停视频,导致timeControlStatus的值更改为AVPlayerTimeControlStatusPaused,而1.0值以自然速率播放当前项目。如果关联的播放器项为AVPlayerItem属性canPlaySlowForwardcanPlayFastForward返回YES,则可以使用0.0和1.0之外的其他比率。如果AVPlayerItem的canPlayReversecanPlaySlowReversecanPlayFastReverse属性返回YES,则支持负值范围;

  • - (void)play开始播放,与将rate值直接设置为1.0是等效的;

  • - (void)pause暂停,与将rate值直接设置为0.0是等效的;

 下面一些属性在iOS 10之后才可以使用:
  • @property (nonatomic, readonly) AVPlayerTimeControlStatus timeControlStatus NS_AVAILABLE(10_12, 10_0)指示当前是否正在播放,无限期暂停播放,或在等待适当的网络条件时暂停播放,这一状态只有在automaticallyWaitsToMinimizeStalling属性为YES的情况下才有意义;
typedef NS_ENUM(NSInteger, AVPlayerTimeControlStatus) {
    /**
    当播放`rate`变为0.0时,进入此状态.此更改可能是调用`pause`方法或
    更改播放的`rate`属性为0.0的结果,但也可能由于外部事件(如iOS中断)
    而发生。在这种状态下,播放会无限期地暂停,直到播放速率变为大于0.0的
    值: 接收到具有非零值的-setRate:或-playImmediatelyAtRate:,
    并且有足够的媒体数据已被缓冲以进行播放为止。
    */
    AVPlayerTimeControlStatusPaused, 

   /**
    当播放器在AVPlayerTimeControlStatusPlaying状态下因为播放缓冲器
    变空而播放停止时或者在AVPlayerTimeControlStatusPaused状态下,播
    放速度从0.0被设置为其他值但是并没有足够的媒体数据已经被缓冲可以用来进
    行播放时或者播放器没有Item去进行播放时,即player的currentItem为nil
    时会进入此状态。在等待缓冲时,可以尝试通过 -playImmediatelyAtRate: 
    方法开始播放任何其他可用的媒体数据。
    */
    AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate,

   /**
    在这种状态下,播放正在进行,速率(rate)的更改将立即生效。 如果因为媒体
    数据不足而播放失败,则timeControlStatus的属性值将更改为
    AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate
    */
    AVPlayerTimeControlStatusPlaying

} NS_ENUM_AVAILABLE(10_12, 10_0);
  • @property (nonatomic, readonly, nullable) AVPlayerWaitingReason reasonForWaitingToPlay NS_AVAILABLE(10_12, 10_0);timeControlStatus的值为AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate时,表示等待的原因,其他情况下该属性值为nil,可KVO来有条件地指示播放器在不同状态下的UI;

    • typedef NSString * AVPlayerWaitingReason NS_STRING_ENUM;定义的播放器等待播放的原因的枚举类型;

    • AVF_EXPORT AVPlayerWaitingReason const AVPlayerWaitingToMinimizeStallsReason表示播放器在开始播放之前正在等待合适的缓冲区情况。这一原因意味着在automaticallyWaitToMinimizeStalling属性为YES时,以指定的速率播放可能会导致播放缓冲区在播放完成之前变为空。当automaticallyWaitToMinimizeStalling属性为NO时,timeControlStatus不会成为AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate;

    • AVF_EXPORT AVPlayerWaitingReason const AVPlayerWaitingWhileEvaluatingBufferingRateReason 表示播放器正在监视播放缓冲区填充率,以确定播放是否可能不中断地完成。这一原因意味着automaticallyWaitToMinimizeStalling属性为YES时,尚未确定以指定速率开始播放是否可能导致缓冲区变空。 当短暂的初始监视期结束后将开始播放或者将原因的值切换到AVPlayerWaitingToMinimizeStallsReason,不建议在此状态下显示指示等待状态的UI;

    • AVF_EXPORT AVPlayerWaitingReason const AVPlayerWaitingWithNoItemToPlayReason指示播放器因为automaticallyWaitToMinimizeStalling是YES,且currentItem为nil导致播放等待;

  • - (void)playImmediatelyAtRate:(float)rate立即以指定的速率播放可用媒体数据。此方法以指定的速率播放可用媒体数据,无论是否有足够的媒体缓冲以确保流畅播放。如果播放缓冲区中存在媒体数据,则调用此方法会将播放器的播放速率更改为指定速率,并将其timeControlStatus更改为AVPlayerTimeControlStatusPlaying。如果播放器缓冲的媒体数据不足以开始播放,如果不会发布AVPlayerItemPlaybackStalledNotification通知,播放器将在播放期间暂停;

  • @property (nonatomic) BOOL automaticallyWaitsToMinimizeStalling NS_AVAILABLE(10_12, 10_0)指示播放器是否应自动延迟播放以尽量减少停顿。该属性主要是影响timeControlStatus属性的AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate值得出现时机;

    • 如果该属性为YES,且播放器从暂停状态变为开始播放状态,则该播放器将尝试当前的Item是否可以在当前指定的rate下播放到最后。如果确定可能遇到延迟,那么播放器的timeControlStatus属性值将变为AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate,直到延迟的可能性已经变成最小时,播放将自动开始。当播放器在播放期间出现播放的缓冲区耗尽且播放停止类似情况时,当延迟的可能性已经变成最小时,播放将重新开始 ;

    • 当你需要精确控制播放开始时间或者使用AVAssetResourceLoader委托来加载媒体数据时,需要将此属性设置为NO。如果此属性的值为NO,则reasonForWaitingToPlay不能采用AVPlayerWaitingToMinimizeStallsReason的值,且只要播放缓冲区不为空,不管会不会播放到最后,播放都会将立即开始播放。 如果播放缓冲区变空并且播放停止,则播放器的timeControlStatus属性值将变为AVPlayerTimeControlStatusPaused

    • 此属性默认为YES,对于播放在线视频流时其行为与该属性设置为YES相同,对于播放本地文件,其行为与该属性设置为NO相同。


  • - (CMTime)currentTime返回Item的当前时间;

  • - (void)seekToDate:(NSDate *)date; / - (void)seekToTime:(CMTime)time拖动滑块播放跳跃播放,跳到指定的播放时间;

  • - (void)seekToDate:(NSDate *)date completionHandler:(void (^)(BOOL finished))completionHandler / - (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler移动播放光标跳跃播放,并在搜索操作已完成或被中断时调用指定的块 ;

  • - (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter查找的时间将在[time -toleranceBefore,time+ toleranceAfter],将kCMTimeZero传递给toleranceBefore和toleranceAfter以请求采样精确查找,这可能会导致额外的解码延迟。全部传递kCMTimePositiveInfinity,则与seekToTime:方法相同。一般采用此方法实现精准跳跃播放;

  • - (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter completionHandler:(void (^)(BOOL finished))completionHandler在上面的方法上添加了一个被打断或者完成后的block;

  • - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;在播放期间请求定期调用给定的块以报告更改时间。interval:间隔 --- 根据播放器当前的时间进度,在正常播放期间周期性调用block;queue:执行队列,该队列必须为串行队列,传入NULL将使用主队列,传入并发队列将导致异常;返回值是一个符合NSObject协议的对象。只要希望播放器调用时间观察器,就必须保留这个返回的值;

    • 当时间跳转和播放开始或停止时,block块也被调用;

    • -addPeriodicTimeObserverForInterval:queue:usingBlock:的每次调用都应与对应的-removeTimeObserver:调用配对。

    • 不调用-removeTimeObserver:释放观察者对象将导致异常。

// 官方示例:
- (void)addPeriodicTimeObserver {
    // Invoke callback every half second
    CMTime interval = CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC);
    // Queue on which to invoke the callback
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    // Add time observer
    self.timeObserverToken =
        [self.player addPeriodicTimeObserverForInterval:interval
                                                  queue:mainQueue
                                             usingBlock:^(CMTime time) {
                                                 // Use weak reference to self
                                                 // Update player transport UI
                                             }];
}
  • - (id)addBoundaryTimeObserverForTimes:(NSArray<NSValue *> *)times queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(void))block该方法为边界检测方法 -- 当在正常播放期间特定的时间经过,该Block就会调用;
// 官方示例:
- (void)addBoundaryTimeObserver {
    NSMutableArray *times = [NSMutableArray array];

    // Set initial time to zero
    CMTime currentTime = kCMTimeZero;
    // Get asset duration
    CMTime assetDuration = self.asset.duration;
    // Divide the asset duration into quarters
    CMTime interval = CMTimeMultiplyByFloat64(assetDuration, 0.25);
 
    // Build boundary times at 25%, 50%, 75%, 100%
    while (CMTIME_COMPARE_INLINE(currentTime, <, assetDuration)) {
        currentTime = CMTimeAdd(currentTime, interval);
        [times addObject:[NSValue valueWithCMTime:currentTime]];
    }
    // Add time observer
    self.timeObserverToken =
        [self.player addBoundaryTimeObserverForTimes:times
                                               queue:dispatch_get_main_queue()
                                          usingBlock:^{
                                              // Use weak reference to self
                                              // Update user interface state
        }];
}
  • - (void)removeTimeObserver:(id)observer;取消时间观察者,与上面两个方法对应配对使用。此方法执行后正在执行的Block依然可能继续执行,可以使用将该方法添加至配对方法执行的队列进行调用或者使用dispatch_sync()同步调用等待任何正在运行的block完成执行后调用该方法;
// 官方示例
- (void)removeBoundaryTimeObserver {
    if (self.timeObserverToken) {
        [self.player removeTimeObserver:self.timeObserverToken];
        self.timeObserverToken = nil;
    }
}
  • @property (nonatomic) float volume指示播放器的当前音频音量; 0.0表示静音所有音频,1.0表示以当前Item的全部音量播放。注意:此属性用于控制相对于系统音量的播放器音量,不要用此属性来实现媒体播放的音量滑块。 为此,请使用MPVolumeView,该外观可自定义,并提供用户期望的标准媒体播放行为。可以使用MediaPlayer框架中MPVolumeView类来呈现用于控制系统音量的标准用户界面。

  • @property (nonatomic, getter=isMuted) BOOL muted指示播放器的音频输出是否静音。 只影响播放器实例的音频静音,而不影响设备是否静音;

配置媒体选择标准设置⏬

  • @property (nonatomic) BOOL appliesMediaSelectionCriteriaAutomatically表示是否应将当前选择条件自动应用于AVPlayerItems,iOS 7以上默认是YES;

  • - (void)setMediaSelectionCriteria:(nullable AVPlayerMediaSelectionCriteria *)criteria forMediaCharacteristic:(AVMediaCharacteristic)mediaCharacteristic为媒体应用具有指定特征的自动选择条件。参数criteria为AVPlayerMediaSelectionCriteria对象, mediaCharacteristic为AVMediaCharacteristic类型参数,但在这里只支持AVMediaCharacteristicLegible字幕资源,AVMediaCharacteristicAudible 音轨资源,AVMediaCharacteristicVisual 视频资源三种特征。具体请参考AVMediaFormat.h文件;

  • - (nullable AVPlayerMediaSelectionCriteria *)mediaSelectionCriteriaForMediaCharacteristic:(AVMediaCharacteristic)mediaCharacteristic返回具有指定媒体特征的媒体的自动选择条件;

外部播放支持相关⏬

  • @property (nonatomic) BOOL allowsExternalPlayback指示播放器是否允许切换到外部播放模式,默认YES;

  • @property (nonatomic, readonly, getter=isExternalPlaybackActive) BOOL externalPlaybackActive指示播放器当前是否正在以外部播放模式播放视频;

  • @property (nonatomic) BOOL usesExternalPlaybackWhileExternalScreenIsActive表示播放器在“外部屏幕”模式处于活动状态时是否应自动切换到“外部播放”模式,默认是NO,如果allowExternalPlayback为NO,则不起作用;

  • @property (nonatomic, copy) AVLayerVideoGravity externalPlaybackVideoGravity设置用于外部播放的视频重力选项;

将播放同步到外部源⏬

 这类方法仅适用于基于文件的播放,暂时不支持HLS(HTTP Live Streaming)的设置。

  • - (void)setRate:(float)rate time:(CMTime)itemTime atHostTime:(CMTime)hostClockTime NS_AVAILABLE(10_8, 6_0)将当前项目的播放速率和时间与外部源同步。itemTime为与播放item相匹配的精准的时间,如果要使用Item的currentTime则需要指定为kCMTimeInvalid。hostClockTime为同步播放的主机时间,如果指定为kCMTimeInvalid,则rate和itemTime的设置均不需要任何的外部同步。在iOS 10之后使用该方法时一定要先将automaticallyWaitsToMinimizeStalling设置为NO,否者会引起异常。

  • - (void)prerollAtRate:(float)rate completionHandler:(nullable void (^)(BOOL finished))completionHandler以给定的rate在当前时间开始加载媒体数据,以为播放准备媒体管道;

  • - (void)cancelPendingPrerolls取消任何挂起的预卷请求,并调用相应的完成处理操作。上面一个方法的取消操作;

  • @property (nonatomic, retain, nullable) __attribute__((NSObject)) CMClockRef masterClock这个属性的默认值是NULL,这意味着主时钟是自动选择的。非NULL属性时,此属性将覆盖item的时间基础的主时钟自动选择。这在纯视频电影与其他音源播放的音频同步时,是非常有用的;

与AVPlayerItem相关⏬

  • @property (nonatomic, readonly, nullable) AVPlayerItem *currentItem获取与当前播放器关联的Item;

  • - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item替换当前的Item。若参数Item与播放器当前Item是同一个则无影响,需要注意的是该方法对于AVQueuePlayer类是不适用的,若多个Item情况下调用该方法会爆异常;

  • @property (nonatomic) AVPlayerActionAtItemEnd actionAtItemEnd指示当AVPlayerItem到达结束时间时播放器应执行的动作;

typedef NS_ENUM(NSInteger, AVPlayerActionAtItemEnd)
{
    /**
    指示当AVPlayerItem达到其结束时间时,播放器将自动前进到其
    队列中的下一个项目。此值仅支持AVQueuePlayer类的播放器。
    如果对非AVQueuePlayer类设置此值则会发生异常。
    */
    AVPlayerActionAtItemEndAdvance  = 0,

    /**
     播放完成后自动将rate设置为0.0使视频暂停。
    */
    AVPlayerActionAtItemEndPause    = 1,

    /**
    表示当AVPlayerItem达到其结束时间时,播放器将不采取任何行动。
    播放器的播放速度不会改变,其currentItem不会改变,其currentTime
    将会随着时间的推移而不断地增加或减少。
    */
    AVPlayerActionAtItemEndNone     = 2,
};

AVQueuePlayer子类

  • + (instancetype)queuePlayerWithItems:(NSArray<AVPlayerItem *> *)items; / - (AVQueuePlayer *)initWithItems:(NSArray<AVPlayerItem *> *)items 初始化;

  • - (NSArray<AVPlayerItem *> *)items;设置或获取相关联的一组AVPlayerItem ;

  • - (void)advanceToNextItem;从播放队列中删除当前Item,结束当前Item的播放,并启动播放器队列中下一个Item的播放;

  • - (BOOL)canInsertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;该值表示给定的AVPlayerItem是否可以插入到队列中指定Item的后面;

  • - (void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;插入操作;

  • - (void)removeItem:(AVPlayerItem *)item;移除指定Item,如果要删除的item正在播放,则与-advanceToNextItem具有相同的效果。;

  • - (void)removeAllItems;移除所有Item;

AVPlayerLayer相关

  • + (AVPlayerLayer *)playerLayerWithPlayer:(nullable AVPlayer *)player通过AVPlayer,直接指定AVPlayer的视觉输出;

  • @property (nonatomic, retain, nullable) AVPlayer *player播放器层显示视觉输出的播放器;

  • @property(copy) AVLayerVideoGravity videoGravity;定义视频在AVPlayerLayer中的显示方式,默认方式AVLayerVideoGravityResizeAspect;

typedef NSString * AVLayerVideoGravity NS_STRING_ENUM;
/**
保留宽高比的情况下,尽量适合layer的bounds。
*/
AVF_EXPORT AVLayerVideoGravity const AVLayerVideoGravityResizeAspect
/**
保留宽高比的情况下,完全填充layer的bounds。
*/
AVF_EXPORT AVLayerVideoGravity const AVLayerVideoGravityResizeAspectFill
/**
拉伸填充满layer的bounds。
*/
AVF_EXPORT AVLayerVideoGravity const AVLayerVideoGravityResize
  • @property(nonatomic, readonly, getter=isReadyForDisplay) BOOL readyForDisplay指示第一个视频帧已准备好显示相关AVPlayer,
    将此属性作为标识当视图中的AVPlayerLayer可以最佳显示的时候。当此属性为NO时,一个AVPlayerLayer可以被显示,或者是可见的,但是该层将不具有任何用户可见的内容,直到值变为YES。

  • @property (nonatomic, readonly) CGRect videoRect视频显示框在接受者bounds内的当前尺寸和位置。可观察属性;

  • @property (nonatomic, copy, nullable) NSDictionary<NSString *, id> *pixelBufferAttributes NS_AVAILABLE(10_11, 9_0); 这个属性可以用来定制播放到播放器层的像素缓冲区的格式。(暂时不知道用处~);

// 官方示例
// PlayerView.h
@interface PlayerView : UIView
@property AVPlayer *player;
@property (readonly) AVPlayerLayer *playerLayer;
@end
 
// PlayerView.m
@implementation PlayerView
- (AVPlayer *)player {
    return self.playerLayer.player;
}
 
- (void)setPlayer:(AVPlayer *)player {
    self.playerLayer.player = player;
}
 
// Override UIView method
+ (Class)layerClass {
    return [AVPlayerLayer class];
}
 
- (AVPlayerLayer *)playerLayer {
    return (AVPlayerLayer *)self.layer;
}
@end
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容