ZFPlayer+KTVHTTPCache实现边下边播

YPDPlayer

一套支持边下边播的播放器方案:

·支持音/视频播放
·支持在线播放
·支持本地播放
·支持边下边播
·操作流畅

方案介绍

整体方案采用ZFPlayer + AVPlayer + KTVHTTPCacheZFPlayer是一个播放器壳子,支持自定义播放器和控制层。本项目的核心是解决视频缓存的问题,所以这里先介绍KTVHTTPCache这个库。

KTVHTTPCache

功能特点

  • 支持相同URL并发操作且线程安全;
  • 全路径Log,支持控制台打印和输出到文件,可准确定位问题;
  • 细粒度的缓存管理,可精确查看指定 URL 的完整缓存信息;
  • 模块相互独立,提供使用不同 Level 的接口;
  • 下载层高度可配置;
  • 低耦合,集成简单;

框架设计

KTVHTTPCache 由 HTTP Server 和 Data Storage 两大模块组成,前者负责与Client交互,后者负责资源加载及缓存处理。

工作流程图:


Workflow.jpg

工作流程简述:
1、Client 发出的请求被 HTTP Srever 接收到,HTTP Server 通过分析 HTTP Request 创建用于访问 Data Storage 的 Data Request 对象;
2、HTTP Server 使用 Data Request 创建 Data Reader,并以此作为从 Data Storage 获取数据的通道;
3、Data Reader 分析 Data Request 中的 Range 创建对应的网络数据源 Data Network Source 和文件数据源 Data File Source,并通过 Data Sourcer 进行管理;
4、Data Sourcer 开始加载数据;
5、Data Reader 从 Data Sourcer 读取数据并通过 HTTP Server 回传给 Client;

使用示例:

NSError *error = nil;
[KTVHTTPCache proxyStart:&error];//启动HttpServer,全局启动一次即可

//URL Encode:对中文、特殊符号、空格进行转译处理
NSString *URLString = [item.URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    
//将原视频url替换成自定义格式url,请求本地服务器
NSURL *URL = [KTVHTTPCache proxyURLWithOriginalURL:[NSURL URLWithString:URLString]];
    
YPDMediaVC *vc = [[YPDMediaVC alloc] initWithURLString:URL.absoluteString];
[self presentViewController:vc animated:YES completion:nil];

使用注意点:

当单个视频大小超过设置的最大缓存时,视频无法播放,解决办法:

  • 根据项目实际情况来设置最大缓存,尽量设置一个较大值;
  • 后台在返回视频url时带上视频长度参数,客户端在播放前先做判断,超过最大缓存的直接在线播放,不走本地服务器(实际中这种情况比较少,以防万一);

以下为框架中相关处理代码,只是抛出错误,没有做相关处理:

//KTVHCDownload.m

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    
    ...
    
    if (!error) {
        long long (^getDeletionLength)(long long) = ^(long long desireLength){
            return desireLength + [KTVHCDataStorage storage].totalCacheLength - [KTVHCDataStorage storage].maxCacheLength;
        };
        long long length = getDeletionLength(dataResponse.contentLength);
        if (length > 0) {
            [[KTVHCDataUnitPool pool] deleteUnitsWithLength:length];
            length = getDeletionLength(dataResponse.contentLength);
            if (length > 0) {
                error = [KTVHCError errorForNotEnoughDiskSpace:dataResponse.totalLength
                                                       request:dataResponse.contentLength
                                              totalCacheLength:[KTVHCDataStorage storage].totalCacheLength
                                                maxCacheLength:[KTVHCDataStorage storage].maxCacheLength];
            }
        }
    }
    if (error) {
        KTVHCLogDownload(@"%p, Invaild response\nError : %@", self, error);
        [self.errorDictionary setObject:error forKey:task];
        completionHandler(NSURLSessionResponseCancel);
    } else {
        id delegate = [self.delegateDictionary objectForKey:task];
        [delegate ktv_download:self didReceiveResponse:dataResponse];
        completionHandler(NSURLSessionResponseAllow);
    }
}

HttpServer

使用CocoaHTTPServer作为本地HttpServer,client发送数据请求,HttpServer先从本地获取数据,本地没有再从网络下载。主要类如下:

  • KTVHCHTTPServer:是一个单例类,用来管理 HttpServer 服务,负责开启或关闭服务;
  • KTVHCHTTPConnection:它继承于 HTTPConnection,表示一个连接,它主要为 HttpServer 提供 Response;
  • KTVHCHTTPResponse:一个遵循HTTPResponse协议的response类;

生成response关键代码:

- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    KTVHCLogHTTPConnection(@"%p, Receive request\nmethod : %@\npath : %@\nURL : %@", self, method, path, request.url);
    NSDictionary *parameters = [[KTVHCURLTool tool] parseQuery:request.url.query];
    NSURL *URL = [NSURL URLWithString:[parameters objectForKey:@"url"]];
    KTVHCDataRequest *dataRequest = [[KTVHCDataRequest alloc] initWithURL:URL headers:request.allHeaderFields];
    KTVHCHTTPResponse *response = [[KTVHCHTTPResponse alloc] initWithConnection:self dataRequest:dataRequest];
    return response;
}

DataStroage

主要用来缓存数据,加载数据,也就是提供数据给 HttpServer。上面代码中关键的一句代码[[KTVHCHTTPResponse alloc] initWithConnection:self dataRequest:dataRequest],它会在这个方法的内部使用KTVHCDataStorage生成一个KTVHCDataReader,负责读取数据。生成KTVHCDataReader后通过[self.reader prepare]来准备数据源KTVHCDataSourceManager,这里主要有两个数据源,KTVHCDataFileSource和KTVHCDataNetworkSource,它们实现了协议KTVHCDataSource。KTVHCDataNetworkSource会通过KTVHCDownload下载数据。

DataStorage.png
  • KTVHCDataStorage:一个单利类,负责管理整个缓存,比如读取、保存、合并缓存;
  • KTVHCDataReader:负责读取数据;
  • KTVHCDataLoader:封装了reader的一个类,提供接口对外使用;
  • KTVHCDataRequest:数据请求;
  • KTVHCDataResponse:数据响应;
  • KTVHCDataCacheItem:缓存数据模型,表示一个缓存项;
  • KTVHCDataCacheItemZone:缓存区,一个缓存项中可能会有多个缓存区,比如099,100299等;
  • KTVHCDataSource:定义了一组属性、方法的协议,下面三个类都遵循了该协议;
  • KTVHCDataFileSource:本地数据源;
  • KTVHCDataNetworkSource:网络数据源;
  • KTVHCDataSourceManager:总数据源管理类;
  • KTVHCDataCallback:封装了两个类方法的回调类;
  • KTVHCDataUnit:数据单元,相当于一个缓存目录,比如一个视频的缓存;
  • KTVHCDataUnitItem:数据单元项,缓存目录下不同片段的缓存;
  • KTVHCDataUnitPool:数据单元池,它是一个单例,含有一个 KTVHCDataUnitQueue;
  • KTVHCDataUnitQueue:数据单元队列,保存了多个 KTVHCDataUnit,它会以 archive 的方式缓存到本地;

初始数据都来源于网络,这里介绍下KTVHCDataNetworkSource中处理数据核心代码:

- (void)ktv_download:(KTVHCDownload *)download didReceiveResponse:(KTVHCDataResponse *)response
{
    [self lock];
    if (self.isClosed || self.error) {
        [self unlock];
        return;
    }
    self->_response = response;
    NSString *path = [KTVHCPathTool filePathWithURL:self.request.URL offset:self.request.range.start];
    self.unitItem = [[KTVHCDataUnitItem alloc] initWithPath:path offset:self.request.range.start];
    KTVHCDataUnit *unit = [[KTVHCDataUnitPool pool] unitWithURL:self.request.URL];
    [unit insertUnitItem:self.unitItem];
    KTVHCLogDataNetworkSource(@"%p, Receive response\nResponse : %@\nUnit : %@\nUnitItem : %@", self, response, unit, self.unitItem);
    [unit workingRelease];
    
    //创建两个文件句柄,读和写
    self.writingHandle = [NSFileHandle fileHandleForWritingAtPath:self.unitItem.absolutePath];
    self.readingHandle = [NSFileHandle fileHandleForReadingAtPath:self.unitItem.absolutePath];
    
    [self callbackForPrepared];
    [self unlock];
}

- (void)ktv_download:(KTVHCDownload *)download didReceiveData:(NSData *)data
{
    [self lock];
    if (self.isClosed || self.error) {
        [self unlock];
        return;
    }
    @try {
        //接收到数据后,写入文件
        [self.writingHandle writeData:data];
        self.downloadLength += data.length;
        [self.unitItem updateLength:self.downloadLength];
        KTVHCLogDataNetworkSource(@"%p, Receive data : %lld, %lld, %lld", self, (long long)data.length, self.downloadLength, self.unitItem.length);
        //有可用数据,需要回调通知
        [self callbackForHasAvailableData];
    } @catch (NSException *exception) {
        NSError *error = [KTVHCError errorForException:exception];
        KTVHCLogDataNetworkSource(@"%p, write exception\nError : %@", self, error);
        [self callbackForFailed:error];
        if (!self.downloadCalledComplete) {
            KTVHCLogDataNetworkSource(@"%p, Cancel download task when write exception", self);
            [self.downlaodTask cancel];
            self.downlaodTask = nil;
        }
    }
    [self unlock];
}

缓存策略

以网络使用最小化为原则,设计了分片加载数据的功能。有 Network Source 和 File Source 两种用于加载数据的 Source,分别用于下载网络数据和读取本地数据。通过分析 Data Request 的 Range 和本地缓存状态来对应创建。

例如一次请求的 Range 为 0-999,本地缓存中已有 200-499 和 700-799 两段数据。那么会对应生成 5 个 Source,分别是:

  • 网络:0~199
  • 本地:200~499
  • 网络:500~699
  • 本地:700~799
  • 网络:800~999

ZFPlayer

功能特点

  • 普通模式的播放,类似于腾讯视频、爱奇艺等APP;
  • 列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;
  • 列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;
  • 列表视频滑出屏幕后停止播放、滑出屏幕后小窗播放;
  • 优雅的全屏,支持横屏和竖屏全屏模式;

使用介绍

实际项目中,如果要自定义播放器和控制层,只需要播放器SDK遵守ZFPlayerMediaPlayback协议,控制层遵守ZFPlayerMediaControl协议。

如何导入库:(CocoaPods)
pod 'ZFPlayer', '~> 4.0'
使用默认控制层:
pod 'ZFPlayer/ControlView', '~> 4.0'
使用AVPlayer播放器:
pod 'ZFPlayer/AVPlayer', '~> 4.0'
使用ijkplayer播放器:
pod 'ZFPlayer/ijkplayer', '~> 4.0'

本项目采用ZFPlayer+AVPlayer+KTVHTTPCache实现边下边播,关键代码如下:

- (void)configPlayer{
    
    ZFAVPlayerManager *playerManager = [[ZFAVPlayerManager alloc] init];
    
    if (@available(iOS 10.0, *)) {
        //关闭AVPlayer默认的缓冲延迟播放策略,提高首屏播放速度
        playerManager.player.automaticallyWaitsToMinimizeStalling = NO;
    }
    
    //初始化时设置containerViewTag,根据此tag在cell上找到播放器view显示的位置
    self.player = [[ZFPlayerController alloc] initWithScrollView:self.tableView playerManager:playerManager containerViewTag:kContainerViewTag];
    
    self.player.controlView = self.controlView;
    /// 0.4是消失40%时候
    self.player.playerDisapperaPercent = 0.4;
    /// 0.6是出现60%时候
    self.player.playerApperaPercent = 0.6;
    /// 移动网络依然自动播放
    self.player.WWANAutoPlay = YES;
    /// 设置是否续播
//    self.player.resumePlayRecord = YES;
    
    @weakify(self)
    //播放完当前视频自动播放下一个
    self.player.playerDidToEnd = ^(id _Nonnull asset){
        @strongify(self)
        if (self.player.playingIndexPath.row < self.dataSource.count - 1) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.player.playingIndexPath.row+1 inSection:0];
            [self playTheVideoAtIndexPath:indexPath scrollAnimated:YES];
        } else {
            //停止当前在cell上的播放并移除播放器view
            [self.player stopCurrentPlayingCell];
        }
    };
    
    /// 停止的时候找出最合适的播放
    self.player.zf_scrollViewDidEndScrollingCallback = ^(NSIndexPath * _Nonnull indexPath) {
        @strongify(self)
        if (!self.player.playingIndexPath) {
            [self playTheVideoAtIndexPath:indexPath scrollAnimated:NO];
        }
    };
}

- (void)playTheVideoAtIndexPath:(NSIndexPath *)indexPath scrollAnimated:(BOOL)animated {
    ZFTableViewCellLayout *layout = self.dataSource[indexPath.row];
    if (animated) {
        [self.player playTheIndexPath:indexPath assetURL:[NSURL URLWithString:[self convertToProxyUrlString:layout.data.video_url]] scrollPosition:ZFPlayerScrollViewScrollPositionCenteredVertically animated:YES];
    } else {
        [self.player playTheIndexPath:indexPath assetURL:[NSURL URLWithString:[self convertToProxyUrlString:layout.data.video_url]]];
    }
    [self.controlView showTitle:layout.data.title
                 coverURLString:layout.data.thumbnail_url
                 fullScreenMode:layout.isVerticalVideo?ZFFullScreenModePortrait:ZFFullScreenModeLandscape];
}

//转换成请求本地服务器的url
- (NSString *)convertToProxyUrlString:(NSString *)urlString {
    
    NSString *URLString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    NSURL *URL = [KTVHTTPCache proxyURLWithOriginalURL:[NSURL URLWithString:URLString]];
    return  URL.absoluteString;
}

ZFPlayerControllerZFPlayer框架的核心类。

ZFPlayerController.png

属性

  • containerView:初始化时传递的容器视图,用来显示播放器view,和播放器view同等大小;
  • currentPlayerManager:初始化时传递的播放器manager,必须遵守ZFPlayerMediaPlayback协议
  • controlView:设置显示的控制层,遵守ZFPlayerMediaControl协议,可自定义;
  • notification:通知的管理类;
  • containerType:容器的类型(cell和普通View);
  • smallFloatView:播放器小窗的容器View;
  • isSmallFloatViewShow:播放器小窗是否正在显示;

初始化方式

/// 普通播放的初始化
+ (instancetype)playerWithPlayerManager:(id)playerManager containerView:(UIView *)containerView;

/// 普通播放的初始化
- (instancetype)initWithPlayerManager:(id)playerManager containerView:(UIView *)containerView;

/// UITableView、UICollectionView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerViewTag:(NSInteger)containerViewTag;

/// UITableView、UICollectionView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerViewTag:(NSInteger)containerViewTag;

/// UIScrollView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerView:(UIView *)containerView;

/// UIScrollView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id)playerManager containerView:(UIView *)containerView;

ZFPlayerMediaPlayback-播放器SDK遵守的协议

  • 协议属性
///  播放器视图继承于ZFPlayerView,处理一些手势冲突
@property (nonatomic) ZFPlayerView *view;

///  0...1.0,播放器音量,不影响设备的音量大小
@property (nonatomic) float volume;

///  播放器是否静音,不影响设备静音
@property (nonatomic, getter=isMuted) BOOL muted;

///  0.5...2,播放速率,正常速率为 1
@property (nonatomic) float rate;

///  当前播放时间
@property (nonatomic, readonly) NSTimeInterval currentTime;

///  播放总时间
@property (nonatomic, readonly) NSTimeInterval totalTime;

///  缓冲时间
@property (nonatomic, readonly) NSTimeInterval bufferTime;

///  视频播放定位时间
@property (nonatomic) NSTimeInterval seekTime;

///  视频是否正在播放中
@property (nonatomic, readonly) BOOL isPlaying;

///  视频播放视图的填充模式,默认不做任何拉伸
@property (nonatomic) ZFPlayerScalingMode scalingMode;

///  检查视频播放是否准备就绪,返回YES,调用play方法直接播放视频;返回NO,调用play方法内部自动调用prepareToPlay方法进行视频播放准备工作
@property (nonatomic, readonly) BOOL isPreparedToPlay;

///  媒体播放资源URL
@property (nonatomic) NSURL *assetURL;

///  视频的尺寸
@property (nonatomic, readonly) CGSize presentationSize;

///  视频播放状态
@property (nonatomic, readonly) ZFPlayerPlaybackState playState;

///  视频的加载状态
@property (nonatomic, readonly) ZFPlayerLoadState loadState;

///------------------------------------
///如果没有指定controlView,可以调用以下块。
///如果你指定了controlView,下面的代码块不能在外部调用,只能用于“ZFPlayerController”调用。
///------------------------------------

///  准备播放
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id asset, NSURL *assetURL);

///  开始播放了
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id asset, NSURL *assetURL);

///  播放进度改变
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id asset, NSTimeInterval currentTime, NSTimeInterval duration);

///  视频缓冲进度改变
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id asset, NSTimeInterval bufferTime);

///  视频播放状态改变
@property (nonatomic, copy, nullable) void(^playerPlayStatChanged)(id asset, ZFPlayerPlaybackState playState);

///  视频加载状态改变
@property (nonatomic, copy, nullable) void(^playerLoadStatChanged)(id asset, ZFPlayerLoadState loadState);

///  视频播放已经结束
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id asset);

///  视频的尺寸改变了
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id asset, CGSize size);

  • 协议方法:
///  视频播放准备,中断除non-mixible之外的任何音频会话
- (void)prepareToPlay;

///  重新进行视频播放准备
- (void)reloadPlayer;

///  视频播放
- (void)play;

///  视频暂停
- (void)pause;

///  视频重新播放
- (void)replay;

///  视频播放停止
- (void)stop;

///  视频播放当前时间的画面截图
- (UIImage *)thumbnailImageAtCurrentTime;

///  替换当前媒体资源地址
- (void)replaceCurrentAssetURL:(NSURL *)assetURL;

///  调节播放进度
- (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;

ZFPlayerMediaControl-控制层遵守的协议

  • 视频状态相关
///  视频播放准备就绪
- (void)videoPlayer:(ZFPlayerController *)videoPlayer prepareToPlay:(NSURL *)assetURL;

///  视频播放状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer playStateChanged:(ZFPlayerPlaybackState)state;

///  视频加载状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state;
  • 播放进度
///  视频播放时间进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
        currentTime:(NSTimeInterval)currentTime
          totalTime:(NSTimeInterval)totalTime;

///  视频缓冲进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
         bufferTime:(NSTimeInterval)bufferTime;

///  视频定位播放时间
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
       draggingTime:(NSTimeInterval)seekTime
          totalTime:(NSTimeInterval)totalTime;

///  视频播放结束
- (void)videoPlayerPlayEnd:(ZFPlayerController *)videoPlayer;
  • 锁屏
/// 设置播放器锁屏时的协议方法
- (void)lockedVideoPlayer:(ZFPlayerController *)videoPlayer lockedScreen:(BOOL)locked;
  • 屏幕旋转
///  播放器全屏模式即将改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationWillChange:(ZFOrientationObserver *)observer;

///  播放器全屏模式已经改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationDidChanged:(ZFOrientationObserver *)observer;

///  当前网络状态发生变化
- (void)videoPlayer:(ZFPlayerController *)videoPlayer reachabilityChanged:(ZFReachabilityStatus)status;
  • 手势方法
///  相关手势设置
- (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl
                    gestureType:(ZFPlayerGestureType)gestureType
              gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
                          touch:(UITouch *)touch;

///  单击
- (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl;

///  双击
- (void)gestureDoubleTapped:(ZFPlayerGestureControl *)gestureControl;

///  开始拖拽
- (void)gestureBeganPan:(ZFPlayerGestureControl *)gestureControl
           panDirection:(ZFPanDirection)direction
            panLocation:(ZFPanLocation)location;

///  拖拽中
- (void)gestureChangedPan:(ZFPlayerGestureControl *)gestureControl
             panDirection:(ZFPanDirection)direction
              panLocation:(ZFPanLocation)location
             withVelocity:(CGPoint)velocity;

///  拖拽结束
- (void)gestureEndedPan:(ZFPlayerGestureControl *)gestureControl
           panDirection:(ZFPanDirection)direction
            panLocation:(ZFPanLocation)location;

///  捏合手势变化
- (void)gesturePinched:(ZFPlayerGestureControl *)gestureControl
        scale:(float)scale;
  • scrollView上的播放器视图方法
/**
scrollView中的播放器视图将要出现的回调
 */
- (void)playerWillAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图已经出现的回调
 */
- (void)playerDidAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图即将消失的回调
 */
- (void)playerWillDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图已经消失的回调
 */
- (void)playerDidDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图正在显示的回调
 */
- (void)playerAppearingInScrollView:(ZFPlayerController *)videoPlayer playerApperaPercent:(CGFloat)playerApperaPercent;

/**
scrollView中的播放器视图正在消失的回调
 */
- (void)playerDisappearingInScrollView:(ZFPlayerController *)videoPlayer playerDisapperaPercent:(CGFloat)playerDisapperaPercent;

/**
小窗视图显示隐藏的回调
 */
- (void)videoPlayer:(ZFPlayerController *)videoPlayer floatViewShow:(BOOL)show;

关于ZFPlayer框架更详细介绍请参考ZFPlayer 3.0解析。 Demo地址

你可能感兴趣的:(ZFPlayer+KTVHTTPCache实现边下边播)