iOS开发之视频播放功能、边播放边缓存

最近新做一个类似奖励视频的内置视频播放功能,并且实现边下载边播放,缓存后下次直接播放本地视频,自动适应横竖屏展示,给大家分享下核心代码

有不太清楚的地方可以加我微信一起探讨、主要六个文件如下

ECGRewardVideoView.h、

ECGRewardVideoView.m

ECGPlayVideoResourceLoaderDelegate.h

ECGPlayVideoResourceLoaderDelegate.m

ECGPlayVideoRequestTask.h 

ECGPlayVideoRequestTask.m

代码如下,直接复制粘贴会有报错地方,但是我觉得你们肯定一看就懂的哈,如有不懂记得微信交流。

外部调用代码

参数说明:url-视频url、duration-播放时间、width-视频宽度、height-视频高度。

[[ECGRewardVideoView sharedInstance] showInsideRewardVideoWithUrl:url duration:duration width height:height];

ECGRewardVideoView.h代码,这是一个继承单例类

#import <UIKit/UIKit.h>
#import "ECGBaseSingleInstance.h"

@interface ECGRewardVideoView : ECGBaseSingleInstance

/** 显示内置奖励视频*/
- (void)showInsideRewardVideoWithUrl:(NSString *)url duration:(NSInteger)duration (NSInteger)width height:(NSInteger)height;

@end

ECGRewardVideoView.m代码实现

#import "ECGRewardVideoView.h"
#import <AVKit/AVKit.h>
#import "CSUtility.h"
#import "ECGNCMultiLanguage.h"
#import "ECGPlayVideoUtility.h"
#import "ECGPlayVideoResourceLoaderDelegate.h"
#import "ECGNativeiOSAdapter.h"
#import "ECGCustomAlertView.h"
#import "ECGNCUtility.h"

@interface ECGRewardVideoView ()

/** 关闭视频按钮*/
@property (strong, nonatomic) UIButton *closeButton;
/** 关闭视频按钮*/
@property (strong, nonatomic) UIImageView *skipImageView;
/** 倒计时文本*/
@property (strong, nonatomic) UILabel *downTimeLabel;
/** 播放视频layer*/
@property (strong, nonatomic) AVPlayerLayer *playerLayer;
/** 播放视频背景*/
@property (strong, nonatomic) UIView *playBGView;
/** 播放器*/
@property (strong, nonatomic) AVPlayer *player;
/** 播放器*/
@property (strong, nonatomic) AVPlayerItem *playerItem;
/** 视频是否播放完毕*/
@property (assign, nonatomic) BOOL launchVideoIsFinish;
/** 是否横屏*/
@property (assign, nonatomic) BOOL isHorizontalScreen;
/** 视频播放urlasset*/
@property (strong, nonatomic) AVURLAsset *urlAsset;
/** 缓存代理*/
@property (strong, nonatomic) ECGPlayVideoResourceLoaderDelegate *resourceLoaderDelegate;
/** 监听播放进度*/
@property (strong, nonatomic) id timeObserverToken;
@property (strong, nonatomic) UIActivityIndicatorView *loadingView;

@end

@implementation ECGRewardVideoView

/** 显示内置奖励视频*/
- (void)showInsideRewardVideoWithUrl:(NSString *)urlStr duration:(NSInteger)duration (NSInteger)width height:(NSInteger)height
{
    self.isHorizontalScreen = NO;
    self.launchVideoIsFinish = NO;
    UIViewController *showVC = [CSUtility cs_getCurrentShowViewController];
    CGRect rect = [UIScreen mainScreen].bounds;
    CGSize size = rect.size;
    NSURL *url = nil;
    if ([urlStr hasPrefix:@"http://"] || [urlStr hasPrefix:@"https://"]) {
        url = [NSURL URLWithString:urlStr];
        
        NSString *strUrl = urlStr;
        //判断是否已经下载完毕,本地缓存路径
        NSString *strLocalPath = [ECGPlayVideoUtility pathForSaveFullVideoDataWithVideoFileName:strUrl.lastPathComponent];
        if ([ECGPlayVideoUtility isFileExistAtPath:strLocalPath]) {
            url = [NSURL fileURLWithPath:strLocalPath];
            _playerItem = [AVPlayerItem playerItemWithURL:url];
        } else {
            //实现的边下载边缓存
            NSURL *customUrl = [ECGPlayVideoUtility customUrlForStandardUrl:url];
           _urlAsset = [AVURLAsset assetWithURL:customUrl];
            self.resourceLoaderDelegate = [[ECGPlayVideoResourceLoaderDelegate alloc] initWithVideoOrigionalUrlString:strUrl];
            self.resourceLoaderDelegate.didFailLoadingBlock = ^(NSError *error) {
                ECGNativeLogHCL(@"HCL视频加载失败 - error %@", error);
            };
            [_urlAsset.resourceLoader setDelegate:self.resourceLoaderDelegate queue:dispatch_get_main_queue()];
            _playerItem = [AVPlayerItem playerItemWithAsset:_urlAsset];
        }
    } else {
        if ([CSUtility cs_isExistFileForPath:urlStr]) {
            url = [NSURL fileURLWithPath:urlStr];
            _playerItem = [AVPlayerItem playerItemWithURL:url];
        } else {
            return;
        }
    }
    AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:_playerItem];
    _player = player;
    _playBGView = [[UIView alloc] initWithFrame:rect];
    _playBGView.backgroundColor = [UIColor blackColor];
    _playBGView.userInteractionEnabled = YES;
    [[UIApplication sharedApplication].delegate.window addSubview:_playBGView];
    UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleDoubleTap:)];
    doubleTapGesture.numberOfTapsRequired = 2;
    [_playBGView addGestureRecognizer:doubleTapGesture];
    
    _playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    _playerLayer.frame = _playBGView.bounds;
    _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    [_playBGView.layer addSublayer:_playerLayer];
    
    [_playerLayer.player play];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(0, 0, 100, 50);
    button.center = CGPointMake(size.width-50, 50);
    [button addTarget:self action:@selector(closeVideoButtonClick) forControlEvents:UIControlEventTouchUpInside];
    [[UIApplication sharedApplication].delegate.window addSubview:button];
    self.closeButton = button;
    
    UIImageView *imageview = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ecg_video_close"]];
    imageview.frame = CGRectMake(40, 5, 35, 35);
    [button addSubview:imageview];
    _skipImageView = imageview;
    
    UILabel *label = [[UILabel alloc] init];
    label.font = [UIFont systemFontOfSize:15];
    label.textColor = [UIColor whiteColor];
    label.textAlignment = NSTextAlignmentCenter;
    label.frame = CGRectMake(0, 5, 40, 35);
    [button addSubview:label];
    _downTimeLabel = label;
    _downTimeLabel.text = @(duration).stringValue;
    
    if (height < width) {
        _playBGView.frame = CGRectMake(0, 0, size.height, size.width);
        _playBGView.center = CGPointMake(size.width/2, size.height/2);
        _playBGView.transform = CGAffineTransformRotate(_playBGView.transform, M_PI_2);
        _playerLayer.frame = _playBGView.bounds;
        
        button.center = CGPointMake(size.width - 50, size.height - 50);
        button.transform = CGAffineTransformRotate(button.transform, M_PI_2);
        
        self.isHorizontalScreen = YES;
    }
    
    [[UIApplication sharedApplication].delegate.window addSubview:self.loadingView];
    
    [_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:NULL];
    [_playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:NULL];
    [_playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:NULL];
    [self delayStartAnimatingLoadingView];
    
    //注册播放结束监听
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoPlayEndNeedCallBackGame) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    
    CMTime interval = CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    __weak typeof(self) weakSelf = self;
    //监听播放进度
    self.timeObserverToken = [self.player addPeriodicTimeObserverForInterval:interval queue:mainQueue usingBlock:^(CMTime time) {
        [weakSelf updatePlayProgressUIWithTime:time];
    }];
}

#pragma mark - 观察播放器状态
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        [self handlePlayerStatusChange:change];
    }else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {//正在缓冲
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayStartAnimatingLoadingView) object:nil];
        [self performSelector:@selector(delayStartAnimatingLoadingView) withObject:nil afterDelay:2];
    } else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {//缓冲到可播放程度了
        [self handlePlaybackLikelyToKeepUpWithChange:change];
    }
}

/** 双击手势处理*/
- (void)handleDoubleTap:(UITapGestureRecognizer *)tap
{
    if (_playerLayer.videoGravity == AVLayerVideoGravityResizeAspectFill) {
         _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    } else {
        _playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    }
}

/** 处理 playbackLikelyToKeepUp 变化*/
- (void)handlePlaybackLikelyToKeepUpWithChange:(NSDictionary<NSKeyValueChangeKey,id> *)change
{
    id newValue = [change objectForKey:NSKeyValueChangeNewKey];
    BOOL playbackLikelyToKeepUp = [newValue boolValue];
    if (!playbackLikelyToKeepUp) {
        return;
    }
    [self stopAnimatingLoadingView];
}

/** 处理播放器状态变化*/
- (void)handlePlayerStatusChange:(NSDictionary<NSKeyValueChangeKey,id> *)change
{
    NSInteger iNewStatus = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
    if (AVPlayerItemStatusReadyToPlay == iNewStatus) {
        ECGNativeLogHCL(@"HCL ============= 广告视频播放成功");
    } else if (AVPlayerItemStatusFailed == iNewStatus) {
        ECGNativeLogHCL(@"HCL ============= 广告视频播放失败!");
    } else {
        ECGNativeLogHCL(@"HCL ============= 广告视频播放状态异常:%@!", @(iNewStatus));
    }
}

/** 延迟显示loading动画*/
- (void)delayStartAnimatingLoadingView
{
    [self.loadingView startAnimating];
}

/** 停止loading动画*/
- (void)stopAnimatingLoadingView
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayStartAnimatingLoadingView) object:nil];
    [self.loadingView stopAnimating];
}

#pragma mark - 成员延迟初始化
- (UIActivityIndicatorView *)loadingView
{
    if (nil == _loadingView)
    {
        _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        _loadingView.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2);
        _loadingView.hidesWhenStopped = true;
    }
    return  _loadingView;
}

/** 更新播放倒计时UI*/
- (void)updatePlayProgressUIWithTime:(CMTime)time
{
    double total = CMTimeGetSeconds(_playerItem.duration);
    double current = CMTimeGetSeconds(time);
    double progress = current/total;
    int downTime = (int)(total-current);
    if (downTime > 0) {
        NSString *downTimeStr = [NSString stringWithFormat:@"%d", downTime];
        _downTimeLabel.text = downTimeStr;
    }
    if (current >= total) {
        _downTimeLabel.text = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"149906"]; //149906=关闭
    }
}

/** 关闭视频按钮点击*/
- (void)closeVideoButtonClick
{
    if (self.launchVideoIsFinish) {
        [self removeVideoView];
        //视频播放完成,满足奖励
        [ECGNativeiOSAdapter playInsideRewerdVideoFinishCallBackResult:1];
    } else {
        __weak typeof(self) weakSelf = self;
        NSString *cancelStr = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"108532"];  //108532=取消
        NSString *confirmStr = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"140369"]; //140369=退出
        NSString *message = [ECGNCMultiLanguage multiplyLanguageForDialogId:@"660505"]; //660505=当前视频未播放完毕无法获得奖励,您确定要退出吗?
        ECGCustomAlertView *alertView = [[ECGCustomAlertView alloc] initWithTitle:nil message:message cancelTitle:cancelStr cancelClickBlock:^{
            weakSelf.closeButton.userInteractionEnabled = YES;
        } confirmTitle:confirmStr confirmClickBlock:^{
            weakSelf.closeButton.userInteractionEnabled = YES;
            [weakSelf removeVideoView];
            if (weakSelf.launchVideoIsFinish) {
                //视频播放完成,奖励
                [ECGNativeiOSAdapter playInsideRewerdVideoFinishCallBackResult:1];
            } else {
                //视频未播放完成,没有奖励
                [ECGNativeiOSAdapter playInsideRewerdVideoFinishCallBackResult:0];
            }
        }];
        [alertView showInWindow];
        if (self.isHorizontalScreen) {
            alertView.transform = CGAffineTransformRotate(alertView.transform, M_PI_2);
        }
        //让关闭按钮不能点击
        _closeButton.userInteractionEnabled = NO;
    }
}

/** 播放结束*/
- (void)videoPlayEndNeedCallBackGame
{
    self.launchVideoIsFinish = YES;
    [self stopAnimatingLoadingView];
}

/** 播放视频结束*/
- (void)removeVideoView
{
    if (nil != _timeObserverToken && nil != _player) {
        [_player removeTimeObserver:_timeObserverToken];
    }
    //播放器结束
    [_player pause];
    [_player replaceCurrentItemWithPlayerItem:nil];
    //移除视频
    [_playerLayer removeFromSuperlayer];
    _playerLayer = nil;
    [_playBGView removeFromSuperview];
    _playBGView = nil;
    _playerItem = nil;
    _player = nil;
    _urlAsset = nil;
    _skipImageView = nil;
    _downTimeLabel = nil;
    //移除button
    [_closeButton removeFromSuperview];
    _closeButton = nil;
    [self stopAnimatingLoadingView];
}

@end    

实现边下载边播放的类

ECGPlayVideoResourceLoaderDelegate.h代码

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

typedef void(^ECGPlayVideoResourceLoaderDelegateVoidBlock)(void);
typedef void(^ECGPlayVideoResourceLoaderDelegateErrorBlock)(NSError *error);

@interface ECGPlayVideoResourceLoaderDelegate : NSObject <AVAssetResourceLoaderDelegate>

@property (readonly, nonatomic) NSString *videoOrigionalUrlString;//视频原始url,区别于替换成自定义url Scheme后的url
@property (assign,   nonatomic) BOOL isUserChangePlayProgress;//用户是否改变了播放进度,比如滑动进度条

@property (copy, nonatomic) ECGPlayVideoResourceLoaderDelegateErrorBlock didFailLoadingBlock;
@property (copy, nonatomic) ECGPlayVideoResourceLoaderDelegateVoidBlock didFinishLoadingBlock;

- (instancetype)initWithVideoOrigionalUrlString:(NSString *)strUrl;

@end

ECGPlayVideoResourceLoaderDelegate.m实现代码

#import <MobileCoreServices/MobileCoreServices.h>
#import "ECGPlayVideoResourceLoaderDelegate.h"
#import "ECGPlayVideoRequestTask.h"
#import "ECGPlayVideoUtility.h"

@interface ECGPlayVideoResourceLoaderDelegate ()

@property (copy,   nonatomic) NSString *videoOrigionalUrlString;
@property (strong, nonatomic) NSMutableArray<AVAssetResourceLoadingRequest *> *loadingRequestArray;
@property (strong, nonatomic) ECGPlayVideoRequestTask *requestTask;

@end

@implementation ECGPlayVideoResourceLoaderDelegate

- (instancetype)initWithVideoOrigionalUrlString:(NSString *)strUrl
{
    self = [super init];
    if (self) {
        self.videoOrigionalUrlString = strUrl;
    }
    return self;
}

#pragma mark - 业务函数

/** 给请求填充信息*/
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
{
    NSString *mimeType = self.requestTask.mimeType;
    CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
    contentInformationRequest.byteRangeAccessSupported = YES;
    contentInformationRequest.contentType = CFBridgingRelease(contentType);
    contentInformationRequest.contentLength = self.requestTask.videoLength;
}

/** 响应请求数据*/
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
    long long startOffset = dataRequest.requestedOffset;
    
    if (dataRequest.currentOffset != 0) {
        startOffset = dataRequest.currentOffset;
    }
    
    if ((self.requestTask.offset +self.requestTask.downLoadingOffset) < startOffset)
    {
        //NSLog(@"NO DATA FOR REQUEST");
        return NO;
    }
    
    if (startOffset < self.requestTask.offset) {
        return NO;
    }
    
    NSAssert([self.videoOrigionalUrlString hasPrefix:@"http"], @"respondWithDataForRequest:self.videoOrigionalUrlString异常,应该是http开头的网络url");
    NSString *strTempPath = [ECGPlayVideoUtility pathForTempVideoDataWithVideoFileName:[_videoOrigionalUrlString lastPathComponent]];
    
    NSData *filedata = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:strTempPath] options:NSDataReadingMappedIfSafe error:nil];
    
    // This is the total data we have from startOffset to whatever has been downloaded so far
    NSUInteger unreadBytes = self.requestTask.downLoadingOffset - ((NSInteger)startOffset - self.requestTask.offset);
    
    // Respond with whatever is available if we can't satisfy the request fully yet
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
    
    [dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.requestTask.offset, (NSUInteger)numberOfBytesToRespondWith)]];
    
    long long endOffset = startOffset + dataRequest.requestedLength;
    BOOL didRespondFully = (self.requestTask.offset + self.requestTask.downLoadingOffset) >= endOffset;
    
    return didRespondFully;
}

/** 处理请求数组*/
- (void)processLoadingRequestArray
{
    NSMutableArray<AVAssetResourceLoadingRequest *> *finishRequests = [NSMutableArray array];
    for (AVAssetResourceLoadingRequest *loadingRequest in self.loadingRequestArray)
    {
        [self fillInContentInformation:loadingRequest.contentInformationRequest]; //对每次请求加上长度,文件类型等信息
        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判断此次请求的数据是否处理完全
        if (didRespondCompletely) {
            [finishRequests addObject:loadingRequest];
            [loadingRequest finishLoading];
        }
    }
    
    [self.loadingRequestArray removeObjectsInArray:finishRequests];
    finishRequests = nil;
}

/** 处理视频数据请求*/
- (void)processLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
    if (![loadingRequest.request.URL.lastPathComponent isEqualToString:_videoOrigionalUrlString.lastPathComponent]) {
        NSAssert(NO, @"processLoadingRequest:当前请求的视频不是期望请求的视频?");
        return;
    }
    
    if (_requestTask.downLoadingOffset > 0) {
        [self processLoadingRequestArray];
    }
    
    if (nil == _requestTask || _isUserChangePlayProgress) {
        [self.requestTask setVideoUrl:[NSURL URLWithString:_videoOrigionalUrlString] offset:0];
    }
}

#pragma mark - AVAssetResourceLoaderDelegate
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
    NSAssert([loadingRequest.request.URL.absoluteString hasPrefix:[ECGPlayVideoUtility customUrlScheme]], @"没有自定义url scheme?");
    [self.loadingRequestArray addObject:loadingRequest];
    [self processLoadingRequest:loadingRequest];
    return YES;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
    [self.loadingRequestArray removeObject:loadingRequest];
}

#pragma mark - 成员延迟初始化
- (NSMutableArray<AVAssetResourceLoadingRequest *> *)loadingRequestArray
{
    if (nil == _loadingRequestArray) {
        _loadingRequestArray = [NSMutableArray array];
    }
    return _loadingRequestArray;
}

- (ECGPlayVideoRequestTask *)requestTask
{
    if (nil == _requestTask) {
        _requestTask = [[ECGPlayVideoRequestTask alloc] init];
        __weak typeof(self) weakSelf = self;
        _requestTask.didReceiveVideoDataBlock = ^{
            [weakSelf processLoadingRequestArray];
        };
        _requestTask.didFailLoadingBlock = ^(NSError *error) {
            if (weakSelf.didFailLoadingBlock) {
                weakSelf.didFailLoadingBlock(error);
            }
        };
        _requestTask.didFinishLoadingBlock = ^{
            if (weakSelf.didFinishLoadingBlock) {
                weakSelf.didFinishLoadingBlock();
            }
        };
    }
    return _requestTask;
}

#pragma mark - 属性设置方法
- (void)setIsUserChangePlayProgress:(BOOL)isUserChangePlayProgress
{
    _isUserChangePlayProgress = isUserChangePlayProgress;
    _requestTask.isUserChangePlayProgress = isUserChangePlayProgress;
}

@end

ECGPlayVideoRequestTask.h代码

#import <Foundation/Foundation.h>

typedef void(^ECGPlayVideoRequestTaskVoidBlock)(void);
typedef void(^ECGPlayVideoRequestTaskErrorBlock)(NSError *error);

@interface ECGPlayVideoRequestTask : NSObject

@property (readonly, nonatomic) NSURL *url;
@property (readonly, nonatomic) NSUInteger offset;
@property (readonly, nonatomic) NSUInteger videoLength;
@property (readonly, nonatomic) NSUInteger downLoadingOffset;
@property (readonly, nonatomic) NSString *mimeType;
@property (readonly, nonatomic) BOOL isFinishLoad;

@property (copy,     nonatomic) ECGPlayVideoRequestTaskVoidBlock didReceiveVideoDataBlock;
@property (copy,     nonatomic) ECGPlayVideoRequestTaskVoidBlock didFinishLoadingBlock;
@property (copy,     nonatomic) ECGPlayVideoRequestTaskErrorBlock didFailLoadingBlock;

@property (assign,   nonatomic) BOOL isUserChangePlayProgress;//用户是否改变了播放进度,比如滑动进度条

- (void)setVideoUrl:(NSURL *)url offset:(NSUInteger)offset;
- (void)cancel;
- (void)clearData;

@end

ECGPlayVideoRequestTask.m代码

#import "ECGPlayVideoRequestTask.h"
#import "ECGPlayVideoUtility.h"

@interface ECGPlayVideoRequestTask () <NSURLSessionDataDelegate>

@property (strong, nonatomic) NSURL *url;
@property (assign, nonatomic) NSUInteger offset;
@property (assign, nonatomic) NSUInteger videoLength;
@property (assign, nonatomic) NSUInteger downLoadingOffset;
@property (copy,   nonatomic) NSString *mimeType;
@property (assign, nonatomic) BOOL isFinishLoad;

@property (copy,   nonatomic) NSString *tempFilePath;
@property (strong, nonatomic) NSURLSessionDataTask *dataTask;
@property (strong, nonatomic) NSFileHandle *fileHandle;

@end

@implementation ECGPlayVideoRequestTask

#pragma mark - 内部函数

/** 处理用户修改播放进度*/
- (void)handleUserChangePlayProgress
{
    [self clearData];
    self.offset = 0;
    self.videoLength = 0;
    self.downLoadingOffset = 0;
    self.mimeType = nil;
    self.isFinishLoad = NO;
    self.tempFilePath = nil;
    self.fileHandle = nil;
    
    //删除临时文件
    [[NSFileManager defaultManager] removeItemAtPath:_tempFilePath error:nil];
}

/** 处理收到Response*/
- (void)handleDidReceiveResponse:(NSURLResponse *)response
{
    _isFinishLoad = NO;
    NSAssert([response isKindOfClass:[NSHTTPURLResponse class]], @"didReceiveResponse:不是 NSHTTPURLResponse");
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
    NSDictionary *dicAllHeaderFields = [httpResponse allHeaderFields] ;
    
    NSString *content = [dicAllHeaderFields valueForKey:@"Content-Range"];
    NSArray *array = [content componentsSeparatedByString:@"/"];
    NSString *strLength = array.lastObject;
    
    NSUInteger videoLength = 0;
    
    if (strLength.length < 1) {
        videoLength = (NSUInteger)httpResponse.expectedContentLength;
    } else {
        videoLength = [strLength integerValue];
    }
    
    self.videoLength = videoLength;
    
    NSString *strContentType = [dicAllHeaderFields objectForKey:@"Content-Type"];
    if ([strContentType isKindOfClass:[NSString class]] && strContentType.length > 0) {
        self.mimeType = strContentType;
    }else {
        self.mimeType = @"video/mp4";
    }
    
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:_tempFilePath];
}

/** 处理收到Data*/
- (void)handleDidReceiveData:(NSData *)data
{
    [self.fileHandle seekToEndOfFile];
    [self.fileHandle writeData:data];
    _downLoadingOffset += data.length;
    
    if (self.didReceiveVideoDataBlock) {
        self.didReceiveVideoDataBlock();
    }
}

/** 处理数据下载完毕*/
- (void)handleFinishReceiveData
{
    _isFinishLoad = YES;
    
    //如果用户没有修改播放进度,则保存临时文件
    if (!_isUserChangePlayProgress) {
        NSString *moveToPath = [ECGPlayVideoUtility pathForSaveFullVideoDataWithVideoFileName:_url.lastPathComponent];
        
        //这里不要用moveItemToPath,因为视频可能正在播放
        [[NSFileManager defaultManager] copyItemAtPath:_tempFilePath toPath:moveToPath error:nil];
    }
    
    if (self.didFinishLoadingBlock) {
        self.didFinishLoadingBlock();
    }
}

#pragma mark - 属性设置方法
- (void)setIsUserChangePlayProgress:(BOOL)isUserChangePlayProgress
{
    _isUserChangePlayProgress = isUserChangePlayProgress;
    if (_isUserChangePlayProgress) {
        //删除临时文件
        [self handleUserChangePlayProgress];
    }
}

#pragma mark - 对外接口
- (void)setVideoUrl:(NSURL *)url offset:(NSUInteger)offset
{
    _url = url;
    _offset = offset;
    
    self.tempFilePath = [ECGPlayVideoUtility pathForTempVideoDataWithVideoFileName:[_url lastPathComponent]];
    if (![ECGPlayVideoUtility isFileExistAtPath:_tempFilePath]) {
        [[NSFileManager defaultManager] createFileAtPath:_tempFilePath contents:nil attributes:nil];
    }
    
    _downLoadingOffset = 0;
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15];
    
    if (offset > 0 && self.videoLength > 0) {
        [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
    }
    
    [self cancel];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    self.dataTask = [session dataTaskWithRequest:request];
    [self.dataTask resume];
    [session finishTasksAndInvalidate];
}

- (void)cancel
{
    [self.dataTask cancel];
}

- (void)clearData
{
    [self cancel];
    //移除文件
    [[NSFileManager defaultManager] removeItemAtPath:_tempFilePath error:nil];
}

#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    [self handleDidReceiveResponse:response];
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [self handleDidReceiveData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (nil != error) {
        [session finishTasksAndInvalidate];
        if (self.didFailLoadingBlock) {
            self.didFailLoadingBlock(error);
        }
    }else {
        [self handleFinishReceiveData];
    }
}

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error
{
    [session finishTasksAndInvalidate];
}

@end
原文地址:https://www.cnblogs.com/hecanlin/p/11541890.html