iOS视频播放详解2-封装边下边播的播放器

引言

最近项目中需要一个播放器,并且要对视频进行缓存,那么最好的方式就是边下边播,播完之后如果数据完整就把视频数据保存到硬盘(沙盒中 ),显然用这些方法就不能满足要求了(总不能播完之后再去下载吧,呵呵), 所以就深入研究了一下AVFoundation框架(其实AVFoundation就是一个对多媒体操作的库),这个框架中的东东还是很多很复杂的,因为是相对较为底层的一些东西嘛。下面首先来看看跟我们要实现的播放器有关系的AVFoundation框架中的一些类

AVAsset

AVAsset是一个抽象的基类。由多种AVAssetTrack(音频轨道、字幕轨道、视频轨道等)集合组成,描述了视频内容的静态内容,例如时长创建日期等,不能直接使用,一般我们会使用它的子类AVURLAsset,AVURLAsset实现了AVAsset中的一些方法可以创建对象,一般我们用url对它进行初始化。 在音视频播放中AVURLAsset主要负责链接相关的服务器,从URL中请求音视频数据,下面来看看AVAsset中的一些熟悉:

// 媒体的时长。
@property (nonatomic, readonly) CMTime duration;
// 媒体的默认播放速度。这个值绝大部分时间为1.0。
@property (nonatomic, readonly) float preferredRate;
// 媒体的默认播放音量。这个值绝大部分时间为1.0。
@property (nonatomic, readonly) float preferredVolume;
// 媒体的旋转,缩放,平移量。 The identity transform: [ 1 0 0 1 0 0 ]。
@property (nonatomic, readonly) CGAffineTransform preferredTransform;
// 此资源中包含的所有的AVAssetTrack , AVAsset 可以通过标识符,媒体类型或媒体特征等信息找到相应的track。
@property (nonatomic, readonly) NSArray<AVAssetTrack *> *tracks;
// 包含着当前视频常见格式类型的元数据。
@property (nonatomic, readonly) NSArray<AVMetadataItem *> *commonMetadata;
// 包含当前视频所有格式类型的元数据。
@property (nonatomic, readonly) NSArray<AVMetadataItem *> *metadata;
// 包含当前视频所有可用元数据的格式类型。
@property (nonatomic, readonly) NSArray<NSString *> *availableMetadataFormats;

AVPlayerItem

AVPlayerItem用于统筹数据,用于管理视频的动态内容和在播放资源的呈现状态(即:视频播放着的各种状态如:播放器是否准备好要去播放,数据请求是否失败,本地数据是否已经播放完了等,后面我们即将详细讲解)。用AVURLAsset对它进行初始化, 它就如同MVC中的model。
如果把AVPlayer,AVPlayerLayer,AVPlayerItem按照MVC架构划分的话,我认为,AVPlayer是C,AVPlayerLayer是V,AVPlayerItem是M
下面我们来看看它的一些属性。

//视频的播放状态
@property (nonatomic, readonly) AVPlayerItemStatus status;
//视频的播放时长
@property (nonatomic, readonly) CMTime duration;
//视频缓存区域大小
@property (nonatomic, readonly) NSArray<NSValue *> *loadedTimeRanges;
//callback,缓冲区有足够数据可以播放
@property (nonatomic, readonly, getter=isPlaybackLikelyToKeepUp) BOOL playbackLikelyToKeepUp;
//callback,缓冲区满了
@property (nonatomic, readonly, getter=isPlaybackBufferFull) BOOL playbackBufferFull;
//callback,缓冲区空了,需要等待数据
@property (nonatomic, readonly, getter=isPlaybackBufferEmpty) BOOL playbackBufferEmpty;

以上控制属性在视频播放中很常用,我们通常使用 KVO 来监控视频播放状态和缓冲区的状态

AVPlayer

AVPlayer 是一个不可见组件(不能显示视频画面),其实最主要的功能是对视频进行解码,并对视频播放进行控制,提供了play,pause以及跳动某个时间点开始播放等功能。

AVPlayerLayer

AVPlayerLayer 是构建于 Core Animation 框架之上(注1)的图层类型,扩展了 Core Animation 的 CALayer 类,为 iOS 的视频渲染提供支持,其实它在视频播放中的功能最主要是显示视频数据。

下面我们开始播放器的详解

首先我们的需求是:

  • 支持正常播放器的一切功能,包括暂停、播放和拖拽

  • 如果视频加载完成且完整,将视频文件保存到本地cache,下一次播放本地cache中的视频,不再请求网络数据

  • 如果视频没有加载完(半路关闭或者拖拽)就不用保存到本地cache,因为数据不完整嘛

根据需求我们的需要实现的功能是:

  • 有开始暂停按钮

  • 显示播放进度及总时长

  • 可以通过拖拽从任意位置开始播放视频

  • 视频加载中的过程和加载失败需要有相应的提示

先看几张图:
                        正常使用AVPlayer`只播放视频`的流程是这样:
正常播放流程

所以我们的方案是

                    但是这样我们得不到播放中缓存的视频数据,所以我们需要这样做:
边下边播,播完将数据写的本地的播放流程
                我们的播放器代码流程是这样:
自定义播放器流程

即:

  1. 当开始播放视频时,通过视频url判断本地cache中是否已经缓存当前视频,如果有,则直接播放本地cache中视频

  2. 如果本地cache中没有视频,则视频播放器向代理请求数据

  3. 加载视频时展示正在加载的提示(菊花转)

  4. 如果可以正常播放视频,则去掉加载提示,播放视频,如果加载失败,去掉加载提示并显示失败提示

  5. 在播放过程中如果由于网络过慢或拖拽原因导致没有播放数据时,要展示加载提示,跳转到第4步

代理对象处理流程
  1. 当视频播放器向代理请求dataRequest时,判断代理是否已经向服务器发起了请求,如果没有,则发起下载整个视频文件的请求

  2. 如果代理已经和服务器建立链接,则判断当前的dataRequest请求的offset是否大于当前已经缓存的文件的offset,如果大于则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向后拖拽,并且超过了已缓存的数据时才会出现)

  3. 如果当前的dataRequest请求的offset小于已经缓存的文件的offset,同时大于代理向服务器请求的range的offset,说明有一部分已经缓存的数据可以传给播放器,则将这部分数据返回给播放器(此时应该是由于播放器向前拖拽,请求的数据已经缓存过才会出现)

  4. 如果当前的dataRequest请求的offset小于代理向服务器请求的range的offset,则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向前拖拽,并且超过了已缓存的数据时才会出现)

  5. 只要代理重新向服务器发起请求,就会导致缓存的数据不连续,则加载结束后不用将缓存的数据放入本地cache

  6. 如果代理和服务器的链接超时,重试一次,如果还是错误则通知播放器网络错误

  7. 如果服务器返回其他错误,则代理通知播放器网络错误

自此实现原理及其业务逻辑已经讲完,下面我们开始代码实现:

首先我们新建四个类如图:

播放器类

注:SPPlayer类主要处理一些界面上的东西,如播放,暂停,进度显示,监听播放器的各种状态。
SPPlayerProxyServer类就是我们上面所讲的播放器的代理类,它遵守AVAssetResourceLoaderDelegate协议,主要处理来自播放器的数据请求,并将已经请求到的数据实时传给播放器。
SPPlayerRequestTask类主要完成SPPlayerProxyServer类交给它的请求数据指令,完成数据请求,并实时将已经请求到的数据写入缓存并通知给SPPlayerProxyServer类,SPPlayerProxyServer类从缓存中读到数据后给SPPlayer。SPPlayerTool是个单例类主要完成创建文件目录,获取缓存数据文件目录等功能。

具体如下:
  • 1 根据URL创建播放器对象,设置AVAsset的数据请求代理并添加相应的监听 ,并处理监听
self.resouerLoader          = [[SPPlayerProxyServer alloc] init];
        NSURL *playUrl              = [_resouerLoader getSchemeVideoURL:[NSURL URLWithString:urlStr]];
        self.videoUrlAsset             = [AVURLAsset URLAssetWithURL:playUrl options:nil];
        [_videoUrlAsset.resourceLoader setDelegate:_resouerLoader queue:dispatch_get_main_queue()];
        self.playerItem          = [AVPlayerItem playerItemWithAsset:_videoUrlAsset];
        
        if (!self.player) {
            self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
        } else {
            [self.player replaceCurrentItemWithPlayerItem:self.playerItem];
        }
        
        //播放状态通知
    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //监听播放器的下载进度
    [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //播放数据为空 此时应该去请求数据
    [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    //缓冲区有足够数据可以播放
    [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
    

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    AVPlayerItem *playerItem = (AVPlayerItem *)object;
    
    if ([keyPath isEqualToString:@"status"]) {
        if ([playerItem status] == AVPlayerStatusReadyToPlay) {
            [self showTime];
            [self.player play];
            [self progressTimer];
            
        } else if ([playerItem status] == AVPlayerStatusFailed || [playerItem status] == AVPlayerStatusUnknown) {
            self.toolView.alpha = 1;
            self.backButton.alpha = 1;
            [self removeShowTime];
            [self.player pause];
            [self removeProgressTimer];
        }
        
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {  //监听播放器的下载进度
        
      //  [self calculateDownloadProgress:playerItem];
        
    } else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) { //监听播放器在缓冲数据的状态
        NSLog(@"缓存数据已经播放完毕,开始下载数据");
        if (playerItem.isPlaybackBufferEmpty) {
            self.state = SPPlayerStateBuffering;
          //  [self bufferingSomeSecond];
        }
    }else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {  //监听播放器的下载进度 即将要播放
    
        
    }
}

  • 2 通过NSURLComponents修改URL,因为只有这样播放器请求数据的时候才会调用我们给它设置的代理,否则,播放器不会调用我们的代理。
- (NSURL *)getSchemeVideoURL:(NSURL *)url{
    // NSURLComponents用来替代NSMutableURL,可以readwrite修改URL
    // AVAssetResourceLoader通过你提供的委托对象去调节AVURLAsset所需要的加载资源。
    // 而很重要的一点是,AVAssetResourceLoader仅在AVURLAsset不知道如何去加载这个URL资源时才会被调用
    // 就是说你提供的委托对象在AVURLAsset不知道如何加载资源时才会得到调用。
    // 所以我们又要通过一些方法来曲线解决这个问题,把我们目标视频URL地址的scheme替换为系统不能识别的scheme
    
    NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    components.scheme = @"systemCannotRecognition";
    
    return [components URL];
}
  • 3 在SPPlayerProxyServer实现AVAssetResourceLoaderDelegate代理中的某些方法
#pragma mark - AVAssetResourceLoaderDelegate
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest{
    
    [self.pendingRequests addObject:loadingRequest];
    
    [self dealWithLoadingRequest:loadingRequest];
    
    return YES;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{

    [self.pendingRequests removeObject:loadingRequest];

}

  • 4通过以上三步我们就可以得到播放器每次请求数据的AVAssetResourceLoadingRequest,然后我们可以通过SPPlayerRequestTask来请求数据,请求到数据后写入缓存并传给SPPlayerProxyServer类
//处理每一次的播放器数据请求
- (void)dealWithLoadingRequest:(AVAssetResourceLoadingRequest *) loadingRequest{

    NSURL *interceptedURL = [loadingRequest.request URL];
    NSRange range = NSMakeRange((NSUInteger)loadingRequest.dataRequest.currentOffset, NSUIntegerMax);
    
    if (self.task.downLoadingOffset > 0) {
        [self processPendingRequests];
    }
    
    if (!self.task) {
        self.task = [[SPPlayerRequestTask alloc] init];
        self.task.delegate = self;
        [self.task setUrl:interceptedURL offset:0];
    } else {
        // 如果新的rang的起始位置比当前缓存的位置还大300k,则重新按照range请求数据
        if (self.task.offset + self.task.downLoadingOffset + 1024 * 300 < range.location ||
            // 如果往回拖也重新请求
            range.location < self.task.offset) {
            [self.task setUrl:interceptedURL offset:range.location];
        }
    }
}

- (void)setUrl:(NSURL *)url offset:(NSUInteger)offset{

    _url = url;
    _offset = offset;
    [[SPPlayerTool sharedInstance] createFileCachePath];
    
    // 替代NSMutableURL, 可以动态修改scheme
    NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    actualURLComponents.scheme = @"http";
    
    // 创建请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[actualURLComponents URL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0];
   
    _downLoadingOffset = 0;

    // fix offset of request(第二次及其以上发起请求时需要修改range哦)
    if (offset > 0 && self.videoLength > 0) {
        [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
    }
    
    // 重置(取消上次请求)
    [self.session invalidateAndCancel];
    
    // 创建Session,并设置代理
    self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    // 创建会话对象
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
    
    // 开始下载
    [dataTask resume];

}

// ReceiveData
// 接收到服务器返回数据的时候调用,会调用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    
    if (data.length>0) {
        _downLoadingOffset += data.length;
//        [self.outputStream write:data.bytes maxLength:data.length];
        [self.fileHandle seekToEndOfFile];
        [self.fileHandle writeData:data];
        
        // For Test
         NSLog(@"loading ... 正在下载");
        if ([self.delegate respondsToSelector:@selector(requestTask:didReceiveData:downloadOffset:tempFilePath:)]) {
            [self.delegate requestTask:self didReceiveData:data downloadOffset:_downLoadingOffset tempFilePath:[[SPPlayerTool sharedInstance] getFileCachePath]];
        }
    }
}

  • 5 SPPlayerProxyServer类得到数据后传给SPPlayer,此时SPPlayer类中会监听到数据已经准备就绪,他就可以开始播放,
#pragma mark - SPPlayerRequestTaskDelegate
// 正在下载(传递获取到的数据和下载的偏移量以及临时文件存储路径)
-(void)requestTask:(SPPlayerRequestTask *)requesttask didReceiveData:(NSData *)data downloadOffset:(NSInteger)offset tempFilePath:(NSString *)filePath{

    [self processPendingRequests];

}

- (void)processPendingRequests{

    // 遍历所有的请求, 为每个请求加上请求的数据长度和文件类型等信息.
    // 在判断当前下载完的数据长度中有没有要请求的数据, 如果有,就把这段数据取出来,并且把这段数据填充给请求, 然后关闭这个请求
    // 如果没有, 继续等待下载完成.
    NSMutableArray *requestsCompleted = [NSMutableArray array];
    for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests) {
        
        [self fillInContentInformation:loadingRequest.contentInformationRequest];
        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest];
        
        if (didRespondCompletely) {
            [requestsCompleted addObject:loadingRequest];
            [loadingRequest finishLoading];
        }
    }
    [self.pendingRequests removeObjectsInArray:[requestsCompleted copy]];
}

//将此次下载到的数据传给此次的请求
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{
    
    long long startOffset = dataRequest.requestedOffset;
    if (dataRequest.currentOffset != 0)
        startOffset = dataRequest.currentOffset;
    
    NSData *fileData = [NSData dataWithContentsOfFile:[[SPPlayerTool sharedInstance] getFileCachePath] options:NSDataReadingMappedIfSafe error:nil];
    NSInteger unreadBytes = self.task.downLoadingOffset - self.task.offset - (NSInteger)startOffset;
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
    if (fileData.length != 0) {
        [dataRequest respondWithData:[fileData subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.task.offset, (NSUInteger)numberOfBytesToRespondWith)]];
    }
    
    long long endOffset = startOffset + dataRequest.requestedLength;
    BOOL didRespondFully = (self.task.offset + self.task.downLoadingOffset) >= endOffset;
    
    return didRespondFully;
}

//此时监听到playerItem的状态为AVPlayerStatusReadyToPlay,开始播放。
 if ([keyPath isEqualToString:@"status"]) {
        if ([playerItem status] == AVPlayerStatusReadyToPlay) {
            [self showTime];
            [self.player play];
            [self progressTimer];
            

  • 6 数据下载完毕如果数据完整,就写入沙盒

-(void)downloadSuccessWithURLSession:(NSURLSession *)session task:(NSURLSessionTask *)task{
   
 //   If download success, then move the complete file from temporary path to cache path
 //   如果下载完成, 就把文件移到缓存文件夹
   if (self.taskArray.count < 2) { //注:如果中间有快进或者快退 则表示下载数据并不完整 就不保存
       NSFileManager *fileManager = [NSFileManager defaultManager];
       if ([fileManager fileExistsAtPath:[[SPPlayerTool sharedInstance] getFileSavePath]]) {
           dispatch_async(dispatch_get_global_queue(0, 0), ^{
               
               [fileManager moveItemAtPath:[[SPPlayerTool sharedInstance] getFileCachePath] toPath:[[SPPlayerTool sharedInstance] getFileSavePath] error:nil];
               dispatch_async(dispatch_get_main_queue(), ^{
                   if ([self.delegate respondsToSelector:@selector(didFinishLoadingWithManager:fileSavePath:)]) {
                       [self.delegate didFinishLoadingWithManager:self fileSavePath:[[SPPlayerTool sharedInstance] getFileSavePath]];
                   }
               });
           });
       }

至此,主要代码已经完成,滑动进度条从某个时间点开始播放,也是通过3--5步骤完成数据请求的,注意还有一点,视频数据请求需要给request设置一个偏移量和长度,所以从第二次请求开始,这个range得计算好。

代码整理一下随后上传

本播放器参考了NewPan夜千寻墨两位大神的博客,在此表示敬意。

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

推荐阅读更多精彩内容

  • google搜索“iOS视频变下边播”,有好几篇博客写到了实现方法,其实只有一篇,其他都是copy的,不过他们都是...
    夜千寻墨阅读 85,463评论 261 907
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • 我们在项目中有时会碰到视频相关的需求,一般的可以分为几种情况: 1. 简单的视频开发,对界面无要求,可直接使用系统...
    纯情_小火鸡阅读 20,649评论 2 102
  • 无疆:佛度有缘人,度是人,何不度我,我最忠诚的佛祖,可曾听到我内心独白? 万寿:我为兽王,却在面临族人生死时,无可...
    鹿未尽阅读 1,741评论 0 3
  • 爱的警报响了,遗失。 爱在哪里?以为在抽屉里,打开它,不见。以为在床底下,寻它,不见。以为在棉被里,拆开,没有。以...
    子非鱼pai阅读 347评论 0 3