• 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
  • 相关阅读:
    adb稳定性monkey测试(转载)
    Cookie、sessionStorage、localStorage的异同
    Vue-eBookReader 学习笔记(初始化部分)
    ValueError: Max value is 14 解决方案
    Chrome 报错: Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
    Bootstrap使用方法
    Vue笔记
    3D相册 复仇者联盟
    奔跑的少年
    钟表练习 html+css实现
  • 原文地址:https://www.cnblogs.com/hecanlin/p/11541890.html
Copyright © 2020-2023  润新知