iOS 音頻視頻播放器實(shí)現(xiàn)邊下載邊播放緩存視頻

前言

  • 本文主要介紹基于AVPlayer實(shí)現(xiàn)邊下邊播邊存處理棵红,核心其實(shí)就是基于AVPlayerAVAssetResourceLoaderDelegate然后對(duì)FILE文件實(shí)現(xiàn)邊下邊播方案,

AVPlayer的基本知識(shí)

單純使用AVPlayer類是無(wú)法顯示視頻的卫枝,要將視頻層添加至AVPlayerLayer中掂榔,這樣才能將視頻顯示出來(lái)疼蛾,簡(jiǎn)單總結(jié)播放視頻就是這三者的使用枯途,AVPlayer教寂、AVPlayerLayer页慷、AVPlayerItem

  • AVPlayer:負(fù)責(zé)控制播放器的播放憔足,暫停,播放速度等
  • AVPlayerLayer:負(fù)責(zé)管理資源對(duì)象酒繁,提供播放數(shù)據(jù)源
  • AVPlayerItem:負(fù)責(zé)顯示視頻滓彰,如果沒(méi)有添加該類,只有聲音沒(méi)有畫面

簡(jiǎn)單理解州袒,你可以把這三者理解為我們常用的MVC揭绑,AVPlayer就對(duì)應(yīng)C,AVPlayerLayer對(duì)應(yīng)V郎哭,AVPlayerItem對(duì)應(yīng)M

關(guān)于這些的介紹使用他匪,我就不介紹了弓叛,網(wǎng)上資料一大堆。本文主要介紹邊下邊播邊存方案

AVPlayer詳解系列(一)參數(shù)設(shè)置

邊下邊播方案

再介紹之前诚纸,我們?cè)賮?lái)了解AVPlayer的一個(gè)類AVAsset撰筷,該類主要用于獲取多媒體信息,再接著往下了解畦徘,AVURLAsset該類是AVAsset的子類毕籽,主要可以根據(jù)URL路徑創(chuàng)建包含媒體信息的AVURLAsset對(duì)象,AVURLAsset通過(guò)委托AVAssetResourceLoader去加載所需文件井辆,同時(shí)可以進(jìn)行數(shù)據(jù)的緩存和讀取操作关筒,這樣就實(shí)現(xiàn)邊下邊播邊存的功能。

大致流程圖杯缺,


流程圖

初始化AVURLAsset

// 判斷是否含有視頻軌道
NS_INLINE BOOL kPlayerHaveTracks(NSURL *videoURL, void(^assetblock)(AVURLAsset *), NSDictionary *requestHeader){
    if (videoURL == nil) return NO;
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:videoURL options:requestHeader];
    if (assetblock) assetblock(asset);
    NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
    return [tracks count] > 0;
}

這里我們就得到AVURLAsset蒸播,接下來(lái)就是設(shè)置委托引出主人公AVAssetResourceLoaderDelegate,這個(gè)就是我們實(shí)現(xiàn)邊下邊播的中間橋梁

NSURL * URL = weakself.connection.kj_createSchemeURL(tempURL);
weakself.asset = [AVURLAsset URLAssetWithURL:URL options:weakself.requestHeader];
[weakself.asset.resourceLoader setDelegate:weakself.connection queue:dispatch_get_main_queue()];

AVAssetResourceLoaderDelegate實(shí)現(xiàn)

下面先來(lái)介紹AVAssetResourceLoaderDelegate的委托方法萍肆,

/*  連接視頻播放和視頻斷點(diǎn)下載的橋梁
 *  必須返回Yes袍榆,如果返回NO,則resourceLoader將會(huì)加載出現(xiàn)故障的數(shù)據(jù)
 *  這里會(huì)出現(xiàn)很多個(gè)loadingRequest請(qǐng)求,需要為每一次請(qǐng)求作出處理
 *  該接口會(huì)被調(diào)用多次塘揣,請(qǐng)求不同片段的視頻數(shù)據(jù)包雀,應(yīng)當(dāng)保存這些請(qǐng)求,在請(qǐng)求的數(shù)據(jù)全部響應(yīng)完畢才銷毀該請(qǐng)求
 *  @param resourceLoader 資源管理器
 *  @param loadingRequest 每一小塊數(shù)據(jù)的請(qǐng)求
 */
- (BOOL)resourceLoader:(AVAssetResourceLoader*)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest*)loadingRequest{
    // TODO:在這里面開(kāi)始我們的網(wǎng)絡(luò)下載請(qǐng)求亲铡,也就是得到AVAssetResourceLoadingRequest對(duì)象
}

這里在提一下才写,由于會(huì)調(diào)用很多次,得到很多個(gè)分片信息奖蔓,所以我選擇用一個(gè)字典來(lái)將這些分片信息存儲(chǔ)起來(lái)赞草,然后逐一下載使用

NSString *key = kGetRequestKey(loadingRequest.request.URL);
if (key == nil) return NO;
KJResourceLoaderManager *manager = self.loaderMap[key];
if (manager == nil){
    NSURL *resourceURL = loadingRequest.request.URL;
    NSString *string = [resourceURL.absoluteString stringByReplacingOccurrencesOfString:kCustomVideoScheme withString:@""];
    NSURL *videoURL = [NSURL URLWithString:string];
    manager = [[KJResourceLoaderManager alloc] initWithVideoURL:videoURL];
    manager.delegate = self;
    self.loaderMap[key] = manager;
}
[manager kj_addRequest:loadingRequest];
/*  當(dāng)視頻播放器要取消請(qǐng)求時(shí),相應(yīng)的吆鹤,也應(yīng)該停止下載這部分?jǐn)?shù)據(jù)厨疙。
 *  通常在拖拽視頻進(jìn)度時(shí)調(diào)這方法
 *  @param resourceLoader 資源管理器
 *  @param loadingRequest 每一小塊數(shù)據(jù)的請(qǐng)求
 */
- (void)resourceLoader:(AVAssetResourceLoader*)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest*)loadingRequest{
    // TODO:停止下載請(qǐng)求
}

下面我們來(lái)一步一步講解得到AVAssetResourceLoadingRequest之后,怎么去開(kāi)啟一個(gè)請(qǐng)求

第一步:獲取請(qǐng)求長(zhǎng)度檀头,文件類型等信息

這里開(kāi)啟一個(gè)小分片去獲取視頻數(shù)據(jù)信息轰异,然后配置正確的信息

/* 對(duì)請(qǐng)求加上長(zhǎng)度,文件類型等信息暑始,必須設(shè)置正確否則會(huì)報(bào)播放器Failed */
NS_INLINE void kSetDownloadConfiguration(KJDownloader *downloader, AVAssetResourceLoadingRequest *loadingRequest){
    AVAssetResourceLoadingContentInformationRequest *request = loadingRequest.contentInformationRequest;
    if (downloader.fileHandleManager.cacheInfo.contentType) {
        request.contentType = downloader.fileHandleManager.cacheInfo.contentType;
    }else{
        CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(@"video/mp4"), NULL);
        request.contentType = CFBridgingRelease(type);
    }
    request.byteRangeAccessSupported = YES;
    request.contentLength = downloader.fileHandleManager.cacheInfo.contentLength;
}

第二步:將下載的NSData傳給播放器

總結(jié)其實(shí)就下面這一句代碼搭独,

[request.dataRequest respondWithData:data];

第三步:請(qǐng)求完成

取消并移除請(qǐng)求

if (error.code == KJPlayerCustomCodeCachedComplete) {
    [weakself kj_cancelLoading];
}else if (error){
    [request finishLoadingWithError:error];
}else{
    [request finishLoading];
    [weakself.requests removeObject:request];
}

到此拋開(kāi)下載器部分處理不說(shuō),簡(jiǎn)單的邊下邊播就已經(jīng)實(shí)現(xiàn)廊镜,下面我們就來(lái)說(shuō)說(shuō)下載器部分

下載器

下載器我這邊采用的是NSURLSession牙肝,然后實(shí)現(xiàn)NSURLSessionDelegate委托協(xié)議
主要就是這三個(gè)方法

- (void)URLSession:(NSURLSession*)session
          dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{

}
- (void)URLSession:(NSURLSession*)session dataTask:(NSURLSessionDataTask*)dataTask didReceiveData:(NSData*)data{

}
- (void)URLSession:(NSURLSession*)session task:(NSURLSessionDataTask*)task didCompleteWithError:(nullable NSError*)error{

}

這里關(guān)于下載就不做多余贅述,接著說(shuō)說(shuō)分片下載處理

NSUInteger fromOffset = fragment.range.location;
NSUInteger endOffset  = fragment.range.location + fragment.range.length - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL];
request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
self.startOffset = fragment.range.location;
self.task = [self.session dataTaskWithRequest:request];
[self.task resume];

到此分片下載我們也就實(shí)現(xiàn)完成,

文件管理

文件管理這邊配椭,我們聲明兩個(gè)NSFileHandle虫溜,一個(gè)用來(lái)寫入分片資源,一個(gè)用來(lái)讀取已下載分片資源

寫入已下載分片文件

[self.writeHandle seekToFileOffset:range.location];
[self.writeHandle writeData:data];
[self.cacheInfo kj_continueCacheFragmentRange:range];

讀取已下載分片緩存數(shù)據(jù)

/* 讀取已下載分片緩存數(shù)據(jù) */
- (NSData*)kj_readCachedDataWithRange:(NSRange)range{
    @synchronized(self.readHandle) {
        [self.readHandle seekToFileOffset:range.location];
        return [self.readHandle readDataOfLength:range.length];
    }
}

這里還值得一提的就是股缸,我們有可能數(shù)據(jù)并沒(méi)有下載完成就就取消等等衡楞,這時(shí)候就選擇了歸檔的方式來(lái)存儲(chǔ)下載文件,然后下次進(jìn)入優(yōu)先讀取歸檔信息敦姻,接著繼續(xù)下載緩存這樣子

歸檔解檔處理

這里采用runtime結(jié)合kvc的方式獲取處理Ivar瘾境,快捷簡(jiǎn)便

#pragma mark - NSCopying
- (id)copyWithZone:(nullable NSZone *)zone {
    KJFileHandleInfo *info = [[[self class] allocWithZone:zone] init];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++){
        const char *name = ivar_getName(ivars[i]);
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        if ([value respondsToSelector:@selector(copyWithZone:)]) {
            [info setValue:[value copy] forKey:key];
        }else{
            [info setValue:value forKey:key];
        }
    }
    free(ivars);
    return info;
}
/* 歸檔 */
- (void)encodeWithCoder:(NSCoder*)aCoder{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++){
        const char *name = ivar_getName(ivars[i]);
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);
}
/* 解檔 */
- (instancetype)initWithCoder:(NSCoder*)aDecoder{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i<count; i++){
            const char *name = ivar_getName(ivars[i]);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

到此,其實(shí)我們的邊下邊播邊存就基本上完成

存入信息到Database

為了更方便更好的管理存儲(chǔ)數(shù)據(jù)镰惦,我還定義了一個(gè)數(shù)據(jù)庫(kù)迷守,然后我們將下載的信息存儲(chǔ)至數(shù)據(jù)庫(kù)當(dāng)中,

//存儲(chǔ)到本地?cái)?shù)據(jù)庫(kù)
- (BOOL)kj_saveDatabaseVideoIntact:(BOOL)videoIntact{
    PLAYER_WEAKSELF;
    NSError *__error;
    [DBPlayerDataInfo kj_insertData:self.cacheInfo.fileName Data:^(DBPlayerData * data){
        data.dbid = weakself.cacheInfo.fileName;
        data.videoUrl = weakself.cacheInfo.videoURL.absoluteString;
        data.videoFormat = weakself.cacheInfo.fileFormat;
        data.sandboxPath = [weakself.cacheInfo.fileName stringByAppendingPathExtension:weakself.cacheInfo.fileFormat];
        data.saveTime = NSDate.date.timeIntervalSince1970;
        data.videoIntact = videoIntact;
        data.videoContentLength = weakself.cacheInfo.contentLength;
    } error:&__error];
    if (__error) {
        return YES;
    }else if (videoIntact) {
        kGCD_player_main(^{
            weakself.playError = [DBPlayerDataInfo kj_errorSummarizing:KJPlayerCustomCodeSaveDatabase];
        });
    }
    return NO;
}

緩存管理器

提供了文件的增刪改查等旺入,資源文件管理等等

#pragma mark - NSFileManager
/* 刪除指定文件 */
+ (BOOL)kj_removeFilePath:(NSString*)path;
/* 創(chuàng)建文件夾 */
+ (BOOL)kj_createFilePath:(NSString*)path;
/* 目錄下有用的文件路徑兑凿,排除臨時(shí)文件 */
+ (NSArray*)kj_videoFilePaths;
/* 目錄下的全部文件名,包含臨時(shí)文件 */
+ (NSArray*)kj_videoAllFileNames;
/* 刪除指定完整路徑數(shù)據(jù) */
+ (void)kj_removeAimPath:(NSString*)path,...;
/* 判斷文件是否存在茵瘾,存在拼接完整路徑 */
+ (BOOL)kj_haveFileSandboxPath:(NSString * _Nonnull __strong * _Nonnull)path;
/* 清除視頻緩存文件和數(shù)據(jù)庫(kù)數(shù)據(jù) */
+ (BOOL)kj_crearVideoCachedAndDatabase:(DBPlayerData*)data;

#pragma mark - Sandbox板塊
/* 判斷是否有緩存礼华,返回緩存鏈接 */
@property(nonatomic,copy,class,readonly)void(^kJudgeHaveCacheURL)(void(^)(BOOL locality), NSURL * _Nonnull __strong * _Nonnull);
/* 創(chuàng)建視頻緩存文件完整路徑 */
+ (NSString*)kj_createVideoCachedPath:(NSURL*)url;
/* 追加視頻臨時(shí)緩存路徑,用于播放器讀取 */
+ (NSString*)kj_appendingVideoTempPath:(NSURL*)url;
/* 獲取視頻緩存大小 */
+ (int64_t)kj_videoCachedSize;
/* 清除全部視頻緩存龄捡,暴露當(dāng)前正在下載數(shù)據(jù) */
+ (void)kj_clearAllVideoCache;
/* 清除指定視頻緩存 */
+ (BOOL)kj_clearVideoCacheWithURL:(NSURL*)url;
/* 存入視頻封面圖 */
+ (void)kj_saveVideoCoverImage:(UIImage*)image VideoURL:(NSURL*)url;
/* 讀取視頻封面圖 */
+ (UIImage*)kj_getVideoCoverImageWithURL:(NSURL*)url;
/* 清除視頻封面圖 */
+ (void)kj_clearVideoCoverImageWithURL:(NSURL*)url;
/* 清除全部封面緩存 */
+ (void)kj_clearAllVideoCoverImage;

關(guān)于seek處理

這里再說(shuō)說(shuō)卓嫂,關(guān)于我們seek的時(shí)候的處理,大致分3種情況聘殖,

第一種:seek處視頻已經(jīng)下載好

I Like 這種是最中規(guī)中矩的只需要直接讀取緩存播放即可

第二種:seek到視頻未下載部分

這時(shí)就需要先取消正在下載的數(shù)據(jù),然后從seek處開(kāi)始重新下載數(shù)據(jù)行瑞,只需要下載器支持分片指定位置下載即可實(shí)現(xiàn)該需求

NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];

第三種:seek來(lái)回多次數(shù)據(jù)就會(huì)包含已下載部分和未下載部分奸腺,斷斷續(xù)續(xù)

你咋這么煩呢?搞事情Q谩M徽铡!
這時(shí)候就需要對(duì)這段分片做個(gè)標(biāo)記氧吐,它到底屬于已下載分片讹蘑,還是未下載分片

1、如果為未下載分片數(shù)據(jù)筑舅,執(zhí)行分片下載

if (fragment.type){// 遠(yuǎn)端碎片座慰,即開(kāi)始下載
    NSUInteger fromOffset = fragment.range.location;
    NSUInteger endOffset  = fragment.range.location + fragment.range.length - 1;
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL];
    request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
    NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
    [request setValue:range forHTTPHeaderField:@"Range"];
    self.startOffset = fragment.range.location;
    self.task = [self.session dataTaskWithRequest:request];
    [self.task resume];
}

2、如果是已下載分片數(shù)據(jù)翠拣,則讀取分片數(shù)據(jù)

NSData *data = [self.fileHandleManager kj_readCachedDataWithRange:fragment.range];

3版仔、如果讀取不成功,給一次機(jī)會(huì)再讀,好好珍惜 - -!

self.once = YES;
data = [self.fileHandleManager kj_readCachedDataWithRange:fragment.range];

4蛮粮、如果還是不成功益缎,則將此分片標(biāo)記為未下載分片,然后重新下載

if (data == nil) {
    fragment.type = 1;
    NSUInteger fromOffset = fragment.range.location;
    NSUInteger endOffset  = fragment.range.location + fragment.range.length - 1;
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL];
    request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
    NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
    [request setValue:range forHTTPHeaderField:@"Range"];
    self.startOffset = fragment.range.location;
    self.task = [self.session dataTaskWithRequest:request];
    [self.task resume];
}

到此然想,關(guān)于邊下邊播邊存莺奔,并且斷點(diǎn)讀取播放繼續(xù)緩存處理也就介紹的差不多了,至于詳細(xì)信息变泄,我Dmeo里面寫的也很詳細(xì)弊仪,感興趣的朋友可以去下載 Demo地址:KJPlayerDemo

文章關(guān)聯(lián)

關(guān)于播放器其他相關(guān)文章

后續(xù)該播放器殼子我會(huì)慢慢補(bǔ)充完善,老哥覺(jué)得好用還請(qǐng)幫我點(diǎn)個(gè)小星星傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杖刷,一起剝皮案震驚了整個(gè)濱河市励饵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌滑燃,老刑警劉巖役听,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異表窘,居然都是意外死亡典予,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門乐严,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瘤袖,“玉大人,你說(shuō)我怎么就攤上這事昂验∥娴校” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵既琴,是天一觀的道長(zhǎng)占婉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)甫恩,這世上最難降的妖魔是什么逆济? 我笑而不...
    開(kāi)封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮磺箕,結(jié)果婚禮上奖慌,老公的妹妹穿的比我還像新娘。我一直安慰自己松靡,他們只是感情好简僧,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著击困,像睡著了一般涎劈。 火紅的嫁衣襯著肌膚如雪广凸。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天蛛枚,我揣著相機(jī)與錄音谅海,去河邊找鬼。 笑死蹦浦,一個(gè)胖子當(dāng)著我的面吹牛扭吁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播盲镶,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼侥袜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了溉贿?” 一聲冷哼從身側(cè)響起枫吧,我...
    開(kāi)封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宇色,沒(méi)想到半個(gè)月后九杂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宣蠕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年例隆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抢蚀。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镀层,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出皿曲,到底是詐尸還是另有隱情唱逢,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布谷饿,位于F島的核電站惶我,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏博投。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一盯蝴、第九天 我趴在偏房一處隱蔽的房頂上張望毅哗。 院中可真熱鬧,春花似錦捧挺、人聲如沸虑绵。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)翅睛。三九已至声搁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捕发,已是汗流浹背疏旨。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扎酷,地道東北人檐涝。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像法挨,于是被迫代替她去往敵國(guó)和親谁榜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

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