iOS視頻需求詳解

控制assets的播放汰翠,你可以使用AVPlayer對象踪栋。在播放的過程中倦青,你可以使用AVPlayerItem對象來管理asset的呈現(xiàn)祭饭,AVPlayerItemTrack來管理track芜茵。要顯示視頻,需要使用AVPlayerLayer倡蝙。

播放Assets


一個播放器就是控制asset播放的對象九串,比如開始和結(jié)束,seek到指定的時間悠咱≌袅荆可以使用AVPlayer來播放單個asset,用AVQueuePlayer來播放多個連續(xù)的asset析既。

一個player向你提供播放的信息躬贡,如果需要,你通過player的狀態(tài)同步顯示到界面上眼坏。你也可以直接把player的輸出顯示笑傲指定的動畫層(AVPlayerLayer或者AVSynchronizedLayer)拂玻,想知道更多關(guān)于layer的信息酸些,請查看Core Animation Programming Guide

多個layer的情況:你可以創(chuàng)建多個AVPlayerLayer對象,但是只有最近創(chuàng)建的layer才會顯示視頻畫面檐蚜。

雖然是播放asset魄懂,但是不能直接把a(bǔ)sset傳給AVPlayer對象,你應(yīng)該提供AVPlayerItem對象給AVPlayer闯第。一個player item管理著和它相關(guān)的asset市栗。一個player item包括player item tracks-(AVPlayerItemTrack對象,表示asset中的tracks)咳短。他們之間的關(guān)系如下圖:


AVPlayer

這表明你可以同時用不同的player播放同一個asset填帽,如上圖顯示,兩個不同的player播放同一個asset咙好。你可以用一個存在asset直接初始化player篡腌,或者直接用URL初始化。和AVAsset一樣勾效,簡單的初始化一個player并不表示可以馬上進(jìn)行播放嘹悼,你需要觀察它的status(通過kvo)來決定是否可以播放。

處理不同類型的asset


配置asset的方式由需要播放的asset的類型決定的层宫。概括的說杨伙,有兩種方式:基于文件的asset,基于流式的(http live streaming format)

加載基于文件的asset卒密,有如下幾步:

· 使用AVURLAsset創(chuàng)建一個asset缀台。
· 使用創(chuàng)建的asset來創(chuàng)建一個AVPlayerItem對象item
· item和AVPlayer關(guān)聯(lián)
· 等待item的狀態(tài),知道可以播放哮奇。

創(chuàng)建基于HTTP live stream的播放器。

用url初始化一個AVPlayerItem對象睛约。(http live stream的情況下不能直接創(chuàng)建AVAsset對象)

NSURL *url = [NSURL URLWithString:@"<#Live stream URL#>];
// You may find a test stream at <http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8>.
self.playerItem = [AVPlayerItem playerItemWithURL:url];
[playerItem addObserver:self forKeyPath:@"status" options:0 context:&ItemStatusContext];
self.player = [AVPlayer playerWithPlayerItem:playerItem];

當(dāng)你關(guān)聯(lián)一個player item到player的時候鼎俘,這個播放器開始準(zhǔn)備播放。當(dāng)它可以播放的時候辩涝,player item會創(chuàng)建AVAsset和AVAssetTrack對象贸伐,這些對象可以用來檢查live stream的內(nèi)容。
為了獲取stream的時間怔揩,可以通過kvo的方式觀察player item的duration的屬性捉邢。當(dāng)可以播放的時候,這個屬性被設(shè)置為正確的值商膊,這時就可以獲取時間伏伐。

注意:只能在iOS4.3之后使用player item的duration屬性。下面這種獲取duration的方法適用于所有的iOS系統(tǒng)版本:當(dāng)player item的狀態(tài)變?yōu)锳VPlayerItemStatusReadyToPlay時晕拆,duration可以通過下面代碼獲取
[[[[[playerItem tracks] objectAtIndex:0] assetTrack] asset] duration];

如果僅僅是想播放一個live stream藐翎,可以直接用下面的簡短代碼實現(xiàn):

self.player = [AVPlayer playerWithURL:<#Live stream URL#>];
[player addObserver:self forKeyPath:@"status" options:0 context:&PlayerStatusContext];

正如assets和items一樣,初始化一個player之后并不表明可以馬上播放,你需要觀察player的status屬性吝镣,當(dāng)status變?yōu)锳VPlayerStatusReadyToPlay時表示可以播放了堤器,你也需要觀察curretItem屬性來訪問player item。

如果你不能確定你用的url是什么類型末贾,可以用下面的方法檢測:
1闸溃、嘗試用url初始化AVURLAsset,然后load它的tracks key拱撵,如果tracks load 成功圈暗,表明你可以用這個asset創(chuàng)建player item。
2裕膀、如果第一步失敗员串,直接用url創(chuàng)建AVPlayerItem,觀察status屬性昼扛,看是否有可播放的狀態(tài)寸齐。

播放一個item


如果想要播放,你可以想player發(fā)送play消息抄谐,如下代碼:

-(IBAction)play:sender {
[player play];
}

除了播放之外渺鹦,還可以管理player的各種信息,比如rate和播放頭蛹含,你也可以監(jiān)控player的狀態(tài)毅厚,這很有用,比如說你需要根據(jù)播放的狀態(tài)來更新界面浦箱。

改變播放的rate


可以改變播放的rate吸耿,代碼如下:

aPlayer.rate = 0.5;
aPlayer.rate = 2.0;

rate=1.0表示正常的播放。0.0表示暫停酷窥。
player item支持逆向播放咽安,當(dāng)rate設(shè)置為負(fù)數(shù)的時候就是逆向播放.
playeritem的 canPlayReverse 表示rate為-1.0, 
canPlaySlowReverse表示rate的范圍是-0.0到-1.0蓬推,  
canPlayFastReverse表示rate小于-1.0f妆棒。

seeking-重定位播放頭


可以使用seekToTime:重定位播放頭到指定的時間,如下代碼:

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn];

seekTime:不能精確定位沸伏,如果需要精確定位糕珊,可以使用seekToTime:toleranceBefore:toleranceAfter:,代碼如下:

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];

當(dāng)tolerance=0的時候毅糟,framework需要進(jìn)行大量解碼工作红选,比較耗性能,所以留特,只有當(dāng)你必須使用的時候才用這個方法纠脾,比如開發(fā)一個復(fù)雜的多媒體編輯應(yīng)用玛瘸,這需要精確的控制。

播放結(jié)束后苟蹈,播放頭移動到playerItem的末尾糊渊,如果此時調(diào)用play方法是沒有效果的,應(yīng)該先把播放頭移到player item起始位置慧脱。如果需要實現(xiàn)循環(huán)播放的功能渺绒,可以監(jiān)聽通知AVPlayerItemDidPlayToEndTimeNotification,當(dāng)收到這個通知的時候菱鸥,調(diào)用seekToTime:把播放頭移動到起始位置宗兼,代碼如下:

// Register with the notification center after creating the player item.
    [[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(playerItemDidReachEnd:)
        name:AVPlayerItemDidPlayToEndTimeNotification
        object:<#The player item#>];
 
- (void)playerItemDidReachEnd:(NSNotification *)notification {
    [player seekToTime:kCMTimeZero];
}

播放多個items


可以使用AVQueuePlayer播放多個items,AVQueuePlayer是AVPlayer的子類氮采,可以用一個數(shù)組來初始化一個AVQueuePlayer對象殷绍。代碼如下:

NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];

和AVPlayer一樣,直接調(diào)用play方法來播放鹊漠,queue player順序播放隊列中的item主到,如果想要跳過一個item,播放下一個item躯概,可以調(diào)用方法advanceToNextItem登钥。

可以對隊列進(jìn)行插入和刪除操作,調(diào)用方法insertItem:afterItem:, removeItem:, 和 removeAllItems娶靡。正常情況下當(dāng)插入一個item之前牧牢,應(yīng)該檢查是否可以插入,通過使用canInsertItem:afterItem:方法姿锭,第二個參數(shù)傳nil塔鳍,代碼如下:

AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
    [queuePlayer insertItem:anItem afterItem:nil];
}

監(jiān)測播放狀態(tài)


可以監(jiān)測player和player item的狀態(tài),這個非常有用艾凯。比如:
· 如果用戶切換到其他應(yīng)用程序献幔,則需要把player的rate設(shè)為0.0
· 如果播放的是遠(yuǎn)程媒體,當(dāng)收到更多的數(shù)據(jù)的時候趾诗,player的loadedTimeRange和seekableTimeRange屬性將會不斷改變。
· 當(dāng)player播放的是http live stream的時候蹬蚁,player的currentItem會不斷改變恃泪。
· 播放http live stream的時候,player item的tracks屬性也不斷改變犀斋。這會發(fā)生在player改變編碼方式的時候贝乎。
· 當(dāng)播放失敗的時候,player或者player item的status屬性也會改變叽粹。
可以使用kvo來監(jiān)測上述改變览效。

注意:只能在主線程注冊和取消kvo

status改變后的處理方式

當(dāng)player或者player item的狀態(tài)status改變却舀,系統(tǒng)會發(fā)送一個kvo的notification,如果一個對象由于一些原因不能播放锤灿,stauts會變成AVPlayerStatusFailed 或者 AVPlayerItemStatusFailed 挽拔,在這種情況下,這個對象的error屬性會被附上一個error類型的對象但校,這個error對象描述了失敗的原因螃诅。

AV Foundation不會指定這個notification是由哪個線程發(fā)出的,所以状囱,如果你要更新UI术裸,就必須確保更新的代碼在主線程中調(diào)用,下面的代碼表示收到status更新的處理方式:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
 
    if (context == <#Player status context#>) {
        AVPlayer *thePlayer = (AVPlayer *)object;
        if ([thePlayer status] == AVPlayerStatusFailed) {
            NSError *error = [<#The AVPlayer object#> error];
            // Respond to error: for example, display an alert sheet.
            return;
        }
        // Deal with other status change if appropriate.
    }
    // Deal with other change notifications if appropriate.
    [super observeValueForKeyPath:keyPath ofObject:object
           change:change context:context];
    return;
}

監(jiān)聽視頻準(zhǔn)備播放的狀態(tài)

可以監(jiān)聽AVPlayerLayer的readyForDisplay屬性亭枷,當(dāng)layer有可顯示的內(nèi)容時袭艺,會發(fā)送一個notification。

跟蹤時間

可以使用addPeriodicTimeObserverForInterval:queue:usingBlock: 或者 addBoundaryTimeObserverForTimes:queue:usingBlock:來跟蹤播放的進(jìn)度叨粘,根據(jù)這個進(jìn)度猾编,你可以更新UI,比如播放了多少時間宣鄙,還剩多少時間袍镀,或者其他的UI狀態(tài)。

· addPeriodicTimeObserverForInterval:queue:usingBlock:冻晤,這個方法傳入一個CMTime結(jié)構(gòu)的時間區(qū)間苇羡,每隔這個時間段的時候,block會回調(diào)一次鼻弧,開始和結(jié)束播放的時候block也會回調(diào)一次设江。

· addBoundaryTimeObserverForTimes:queue:usingBlock:,這個放傳入一個CMTime結(jié)構(gòu)的數(shù)組,當(dāng)播放到數(shù)組里面的時間點(diǎn)的時候攘轩,block會回調(diào)叉存。

這兩個方法都返回一個id類型的對象,這個對象必須一直被持有度帮〖吣螅可以使用removeTimeObserver:取消這個觀察者。

對于這兩個方法笨篷,AVFoundation不會保證每次時間點(diǎn)到了的時候都會回調(diào)block瞳秽,如果前面回調(diào)的block沒有執(zhí)行完的時候,下一次就不會回調(diào)率翅。所以练俐,必須保證在block里面的邏輯不能太耗時。下面是使用的例子:

// Assume a property: @property (strong) id playerObserver;
Float64 durationSeconds = CMTimeGetSeconds([<#An asset#> duration]);
CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 1);
CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 1);
NSArray *times = @[[NSValue valueWithCMTime:firstThird], [NSValue valueWithCMTime:secondThird]];
self.playerObserver = [<#A player#> addBoundaryTimeObserverForTimes:times queue:NULL usingBlock:^{
    NSString *timeDescription = (NSString *)
        CFBridgingRelease(CMTimeCopyDescription(NULL, [self.player currentTime]));
    NSLog(@"Passed a boundary at %@", timeDescription);
}];

播放結(jié)束

可以向通知中心注冊 AVPlayerItemDidPlayToEndTimeNotification 通知冕臭,當(dāng)播放結(jié)束的時候可以收到一個結(jié)束的通知腺晾。代碼:

[[NSNotificationCenter defaultCenter] addObserver:<#The observer, typically self#>
                                         selector:@selector(<#The selector name#>)
                                             name:AVPlayerItemDidPlayToEndTimeNotification
                                           object:<#A player item#>];

完整的例子:用AVPlayerLayer播放一個video


下面的代碼向你展示如何使用AVPLayer播放一個video文件燕锥,步驟如下:

1、用AVPlayerLayer配置一個view

2悯蝉、創(chuàng)建一個AVPlayer

3归形、用video文件創(chuàng)建一個AVPlayerItem對象,并且用kvo觀察他的status

4泉粉、 當(dāng)收到item的狀態(tài)變成可播放的時候连霉,播放按鈕啟用

5、 播放嗡靡,結(jié)束之后把播放頭設(shè)置到起始位置

player view


為了播放一個視頻跺撼,需要個view,這個view的layer是AVPlayerLayer對象讨彼∏妇可以創(chuàng)建一個子類實現(xiàn)這個需求。代碼:

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface PlayerView : UIView
@property (nonatomic) AVPlayer *player;
@end
@implementation PlayerView
+ (Class)layerClass {
    return [AVPlayerLayer class];
}
- (AVPlayer*)player {
    return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player {
    [(AVPlayerLayer *)[self layer] setPlayer:player];
}
@end

View controller


同時你需要一個view controller哈误,代碼如下:

@class PlayerView;
@interface PlayerViewController : UIViewController
@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic, weak) IBOutlet PlayerView *playerView;
@property (nonatomic, weak) IBOutlet UIButton *playButton;
- (IBAction)loadAssetFromFile:sender;
- (IBAction)play:sender;
- (void)syncUI;
@end

syncUI方法的作用是根據(jù)player的status來更新播放按鈕哩至,實現(xiàn)如下:

-(void)syncUI {
    if ((self.player.currentItem != nil) &&
        ([self.player.currentItem status] == AVPlayerItemStatusReadyToPlay)) {
        self.playButton.enabled = YES;
    }
    else {
        self.playButton.enabled = NO;
    }
}

在viewdidload里面調(diào)用syncUI,確保剛開始顯示的頁面的時候按鈕是不可用的蜜自。代碼如下:

-(void)viewDidLoad {
    [super viewDidLoad];
    [self syncUI];
}

其他的屬性和方法在接下來的文字說明菩貌。

創(chuàng)建一個asset


通過URL創(chuàng)建一個AVURLAsset對象。代碼如下:

-(IBAction)loadAssetFromFile:sender {
    NSURL *fileURL = [[NSBundle mainBundle]
        URLForResource:<#@"VideoFileName"#> withExtension:<#@"extension"#>];
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
    NSString *tracksKey = @"tracks";
    [asset loadValuesAsynchronouslyForKeys:@[tracksKey] completionHandler:
     ^{
         // The completion block goes here.
     }];
}

在完成的block里面重荠,用asset創(chuàng)建一個AVPlayerItem對象和一個AVPlayer對象箭阶,并且把player設(shè)為player view的屬性。和創(chuàng)建一個asset一樣戈鲁,簡單的創(chuàng)建一個player item并不意味著可以馬上使用仇参,可以用kvo觀察player item 的status來判斷是否可以開始播放,這個kvo的設(shè)置應(yīng)該在player item和player關(guān)聯(lián)之前設(shè)置婆殿。代碼如下:

// Define this constant for the key-value observation context.
static const NSString *ItemStatusContext;
// Completion handler block.
         dispatch_async(dispatch_get_main_queue(),
            ^{
                NSError *error;
                AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];
                if (status == AVKeyValueStatusLoaded) {
                    self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
                     // ensure that this is done before the playerItem is associated with the player
                    [self.playerItem addObserver:self forKeyPath:@"status"
                                options:NSKeyValueObservingOptionInitial context:&ItemStatusContext];
                    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:)  
                                    name:AVPlayerItemDidPlayToEndTimeNotification
                                                                object:self.playerItem];
                    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
                    [self.playerView setPlayer:self.player];
                }
                else {
                    // You should deal with the error appropriately.
                    NSLog(@"The asset's tracks were not loaded:\n%@", [error localizedDescription]);
                }
            });

player item的status改變時的處理

當(dāng)player item的status改變時诈乒,view controller會收到一個通知消息,AVFoundation并不指定這個消息是由哪個線程發(fā)出的婆芦。如果你要更新ui怕磨,必須確保更新ui的代碼在主線程中。下面的代碼顯示更新ui的邏輯:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
 
    if (context == &ItemStatusContext) {
        dispatch_async(dispatch_get_main_queue(),
                       ^{
                           [self syncUI];
                       });
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object
           change:change context:context];
    return;
}

播放

播放很簡單消约,直接向player發(fā)送play消息即可癌压。代碼如下:

- (IBAction)play:sender {
    [player play];
}

這樣的情況只能播放一次,當(dāng)播放結(jié)束的時候荆陆,再調(diào)用play方法是沒有效果的,如果你要重新播放集侯,需要向通知中心注冊AVPlayerItemDidPlayToEndTimeNotification消息被啼,當(dāng)收到播放結(jié)束的消息的時候帜消,調(diào)用seekToTime:把播放頭移動到起始位置,這樣再調(diào)用play的時候就可以重新播放了浓体。代碼如下:

// Register with the notification center after creating the player item.
    [[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(playerItemDidReachEnd:)
        name:AVPlayerItemDidPlayToEndTimeNotification
        object:[self.player currentItem]];
 
- (void)playerItemDidReachEnd:(NSNotification *)notification {
    [self.player seekToTime:kCMTimeZero];
}

參考文獻(xiàn):
https://developer.apple.com/library/prerelease/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/02_Playback.html#//apple_ref/doc/uid/TP40010188-CH3-SW8
https://segmentfault.com/a/1190000004054258

程序員應(yīng)當(dāng)每日吃一個蘋果
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泡挺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子命浴,更是在濱河造成了極大的恐慌娄猫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件生闲,死亡現(xiàn)場離奇詭異媳溺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)碍讯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門悬蔽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捉兴,你說我怎么就攤上這事蝎困。” “怎么了倍啥?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵禾乘,是天一觀的道長。 經(jīng)常有香客問我虽缕,道長始藕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任彼宠,我火速辦了婚禮鳄虱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凭峡。我一直安慰自己拙已,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布摧冀。 她就那樣靜靜地躺著倍踪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪索昂。 梳的紋絲不亂的頭發(fā)上建车,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音椒惨,去河邊找鬼缤至。 笑死,一個胖子當(dāng)著我的面吹牛康谆,可吹牛的內(nèi)容都是我干的领斥。 我是一名探鬼主播嫉到,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼月洛!你這毒婦竟也來了何恶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤嚼黔,失蹤者是張志新(化名)和其女友劉穎细层,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唬涧,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疫赎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了爵卒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虚缎。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钓株,靈堂內(nèi)的尸體忽然破棺而出实牡,到底是詐尸還是另有隱情,我是刑警寧澤轴合,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布创坞,位于F島的核電站,受9級特大地震影響受葛,放射性物質(zhì)發(fā)生泄漏题涨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一总滩、第九天 我趴在偏房一處隱蔽的房頂上張望纲堵。 院中可真熱鬧,春花似錦闰渔、人聲如沸席函。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茂附。三九已至,卻和暖如春督弓,著一層夾襖步出監(jiān)牢的瞬間营曼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工愚隧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蒂阱,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像蒜危,于是被迫代替她去往敵國和親虱痕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345