iOS实现边下边播

作者: 留个念想给昨天 | 来源:发表于2018-03-30 16:43 被阅读148次
    image.png

    最近公司项目要开发一个TCP上传下载文件并且能边下边播的功能。我这里主要分为三个部分:

    1.文件的分段读取,与分段写入。详情请查看NSFileHandle完成分段读写数据

    2.交互协议中的字节处理。详情请查看iOS基础之字节处理

    3.边下边播思路分析

    mp4文件基本知识:

    对于播放器而言,只要视频文件的头信息(时长,帧率,码率,视频数据偏移量等)解析到了,然后根据视频播放的当前时间对应的内容数据就可以播放视频,mp4的基本格式可参考http://www.jianshu.com/p/3ab4bd0d4219。基于以上,只要解析到视频的头信息,然后缓存视频数据内容就可以实现缓存播放及seek播放。

    两种方案

    视频的缓存播放目前有两种方案,

    1、通过解析mp4的格式,将mp4的数据直接下载并写入文件,然后让播放器直接播放的是本地的视频文件;

    image.png

    2、使用本地代理服务器进行文件缓存,并将视频url地址转换成本地代理服务器地址来实现视频的缓存播放。


    image

    正常情况下推荐方式二:有url的时候用方式二
    但本文是在TCP的情况下,没有url,只能分段读取视频数据。

    技术难点:

    1.解析视频的头信息(mp4)
    2.播放优化(播放到未下载的地方的处理方式)

    4.解析视频的头信息

    mp4文件其实有两种格式的数据,一种是头信息(即moov)在视频头部,一种是在视频尾部。在头部就可以正常处理,在尾部就困难了。

    初步方案:第一段下开头,第二段下结尾,然后再下载中间的内容。

    小试牛刀:砰的一声。撞车了。用普通的播放器搞不定。
    继续找资料,终于经过半个月的研究,终于让我找到了一个播放器,迅雷的APlayer,棒棒的,这种数据他尽然能解析出来,不得不说迅雷很牛

    image.png image.png

    在讨论区有APlayer代码。

    5.播放优化(播放到未下载的地方的处理方式)

    5.1获取到当前下载部分对应的播放时长

    没有找到相应的API,想了一个死办法,但也能很完美的解决问题。

    //返回当前文件大小
    unsigned long long currentSize = [self.readHandle seekToEndOfFile];
    //self.totalSeconds为视频的总时长,self.totalDataSize视频的总大小
    //粗略计算当前能播放的时长
    self.needBuff = self.totalSeconds*currentSize/self.totalDataSize;
    

    5.2优化

    //手动计算是否需要缓冲
    *calculationBuff*每秒中调用一次
    -(void)calculationBuff{
        //当前正在播放的时间
        NSInteger current =  CMTimeGetSeconds(self.player.currentTime);
        //self.needBuff为上次计算的最大能播放的量
        if (self.needBuff<=current) {//如果不够,重新计算self.needBuff,因为播放过程中一直在下载
            //查看当前下载量
            unsigned long long currentSize = [self.readHandle seekToEndOfFile];//返回当前文件大小
            NSLog(@"currentSize %lld",currentSize);
            
            if (currentSize==self.totalDataSize) {//判断是否下载完成,下载完成,直接播放,以后都不用调用calculationBuff来判断了
                
                self.needBuff = self.totalSeconds;
                self.downloadOver = YES;
            }else{
                //重新计算self.needBuff
                //粗略计算当前能播放的时长
                self.needBuff = self.totalSeconds*currentSize/self.totalDataSize - self.buff;
            }
            
            //输出当前播放的时间
            NSLog(@"正在缓冲now %ld total %ld",(long)current,(long)self.needBuff);
            if (self.needBuff<current) {
                
                if (self.player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
                    [SVProgressHUD showWithStatus:@"正在缓冲..."];
                    [self.player pause];
                    
                }
                [self performSelector:@selector(calculationBuff) withObject:nil afterDelay:3];
                
            }else{
                if (self.player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
                    NSLog(@"正在播放now %ld total %ld",(long)current,(long)self.needBuff);
                    [self.player play];
                    [SVProgressHUD dismiss];
                }
            }
            
        }else{
            NSLog(@"不需要缓冲");
        }
        
    }
    

    完美播放
    瞬间心情舒服多了
    以上是边下边播的简单实现方案,像读数据,先读头和尾,再读中间。我觉得不是太好,有没有什么办法可以直接转换mp4数据的方法呢?

    6. mp4转换

    前面说到mp4包括两种,一种是头信息(即moov)在视频头部,一种是在视频尾部。有什么办法可以直接转换mp4的数据吗

    一般通过FFmpeg生成的MP4文件如果没有经过特殊处理在播放的时候是要下载完整个文件才能播放,但是我们想将文件用于点播,比如放到Darwin RTSP Server的媒体目录里让访问的客户端播放,必须让它支持边下载边播放。将MP4文件转成可以逐渐播放的操作叫做“流化”,那如何对一个MP4文件进行“流化”呢?我们可以借助ffmpeg带的一个命令行工具--qt-faststart。

    6.1先下载并安装FFmpeg

    因为在ffmpeg解压完的文件中存在qt-faststart的源码,所以直接使用,位置在解压路径/tools/qt-faststart.c

    6.2安装qt-faststart

    进入ffmpeg解压路径执行命令

    cd 到下载解压的FFmpeg的路径
    make tools/qt-faststart 
    

    会看到在tools中会出现一个qt-faststart文件(还有一个.c文件)

    6.3对MP4进行操作

    6.3.1.ffmpeg将元数据转移至末尾
    cd ffmpeg安装路径/bin
    ./ffmpeg -i /opt/mp4test.mp4 -acodec copy -vcodec copy /opt/1.mp4
    // /opt/mp4test.mp4为原始MP4文件路径,/opt/1.mp4为生成文件的存放路径
    
    6.3.2.qt-faststart操作
    cd ffmpeg压缩包解压路径/tools
    ./qt-faststart /opt/1.mp4 /opt/2.mp4
    

    //路径如上解释
    可以尝试播放1.MP4和2.MP4,前者需要加载完毕才能播放,后者可以边加载边播放。

    6.4 代码实现解析视频的头信息

    经过半个月的研究,我找到了一个完美的方法并写成framework供大家使用,详情请参考iOS实现边下边播之mp4的moov置前

    7. 发现新大陆,

    因为手机录制的视频都比较大,1s就得占用1M,需要压缩一下,所以上传视频给服务器的时候我们必须要压缩。

    //压缩
    - (void)compression{
        
        // 创建AVAsset对象
        AVAsset* asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:_path]];
        NSLog(@"asset:%@",asset);
        /*
         创建AVAssetExportSession对象
         压缩的质量
         AVAssetExportPresetLowQuality 最low的画质最好不要选择实在是看不清楚
         AVAssetExportPresetMediumQuality 使用到压缩的话都说用这个
         AVAssetExportPresetHighestQuality 最清晰的画质
         */
        AVAssetExportSession * session = [[AVAssetExportSession alloc]
                                          initWithAsset:asset presetName:AVAssetExportPresetMediumQuality];
        
        //优化网络
        session.shouldOptimizeForNetworkUse = YES;
        //转换后的格式
        //拼接输出文件路径 为了防止同名 可以根据日期拼接名字 或者对名字进行MD5加密
        //目标
        NSString * docpath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
        NSString *path =[docpath stringByAppendingPathComponent:@"apple1.mp4"];
        //判断文件是否存在,如果已经存在删除
        [[NSFileManager defaultManager]removeItemAtPath:path error:nil];
        //设置输出路径
        session.outputURL = [NSURL fileURLWithPath:path];
        //设置输出类型 这里可以更改输出的类型 具体可以看文档描述
        session.outputFileType = AVFileTypeMPEG4;
        
        __weak typeof(self) ws = self;
        
        [session exportAsynchronouslyWithCompletionHandler:^{
            
            __strong typeof(ws) strongSelf = ws;
            NSLog(@"%@",[NSThread currentThread]);
            //压缩完成
            if(session.status==AVAssetExportSessionStatusCompleted) {
                //在主线程中刷新UI界面,弹出控制器通知用户压缩完成
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"导出完成");
                    NSURL* CompressURL = session.outputURL;
                    strongSelf.totalRet = [strongSelf fileSize:CompressURL];
                    NSLog(@"压缩完毕,压缩后大小 %llu ",strongSelf.totalRet);
                    strongSelf.path = path;
                    strongSelf.readHandle = [NSFileHandle fileHandleForReadingAtPath:path];//读到内存
                    strongSelf.needCompress = NO;
                    [strongSelf readAndWriteData];
                });
            }
        }];
    }
    

    一次偶然的机会,发现视频经过压缩后用系统的播放器AVPlayer可以播放(这算不算一个晴天霹雳,我之前的功夫白费了一半)。还好,果断用上系统的播放器。

    8.AVPlayerViewController

        //设置本地视频路径
        
        NSURL *url=[NSURL fileURLWithPath:self.urlString];
        
        AVAsset *asset = [AVAsset assetWithURL:url];
    AVPlayerItem *item=[AVPlayerItem playerItemWithAsset:asset];
        
        //设置流媒体视频路径
        //self.item=[AVPlayerItem playerItemWithURL:movieURL];
        
        //设置AVPlayer中的AVPlayerItem
        self.player=[AVPlayer playerWithPlayerItem:item];
        
        
        //监听status属性,注意监听的是AVPlayerItem
        [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
        
        //监听loadedTimeRanges属性
        [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
        
        //设置监听函数,监听视频播放进度的变化,每播放一秒,回调此函数
        __weak __typeof(self) weakSelf = self;
        [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            __strong __typeof(weakSelf) strongSelf = weakSelf;
            
            if (!strongSelf.downloadOver) {
                [strongSelf calculationBuff];
            }
            
        }];
        _session = [AVAudioSession sharedInstance];
        [_session setCategory:AVAudioSessionCategoryPlayback error:nil];
        self.videoGravity = AVLayerVideoGravityResizeAspect;
        self.allowsPictureInPicturePlayback = true;    //画中画,iPad可用
        self.showsPlaybackControls = true;
        self.view.translatesAutoresizingMaskIntoConstraints = true;
    }
    - (void)removeObserverFromPlayerItem:(AVPlayerItem *)playerItem {
        [playerItem removeObserver:self forKeyPath:@"status"];
        [playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
        //    [playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
    }
    -(void)dealloc{
        NSLog(@"AVPlayerChildController Dealloc");
        if (!self.downloadOver) {
            [self.readHandle closeFile];
        }
        [self removeObserverFromPlayerItem:self.player.currentItem];
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    

    这样就完成了边下边播功能。
    资料请参考faststarVideoframework

    写在最后:

    希望这篇文章对您有帮助,最好就是实操一边,这样才能理解更深入。
    当然如果您发现有可以优化的地方,希望您能慷慨的提出来。
    最后祝您工作愉快!
    

    相关文章

      网友评论

      • 鹏鹏2015:楼主您好,请问你的第一种方案“第一段下开头,第二段下结尾,然后再下载中间的内容”,这种封装格式出了用迅雷的APlayer可以播放,用IJKPlayer可以播放吗?
        鹏鹏2015:@采釆一叶秋的iOS漫步 请问“解析mp4的格式”,客户端可以直接通过一个url来解析服务器的mp4文件吗,是怎么解析的啊?期望楼主的答复。
        鹏鹏2015:谢谢楼主!请问第二段下结尾,结尾这部分的offset和contentLength分别是多少呢,这个是怎么算的啊?
        留个念想给昨天:@鹏鹏2015我试过IJK,播不了。
      • ptlCoder:有demo莫
      • IT人故事会:写的很用心,你的文章我收藏了啊

      本文标题:iOS实现边下边播

      本文链接:https://www.haomeiwen.com/subject/cflvqftx.html