iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例)

1. 探究两种视频URL格式


分析网易新闻的视频接口时,单个视频数据其实会包含了两种视频URL格式地址,一个MP4视频URL,一个m3u8视频URL

1.1 建立视频模型

  • MVideo.h
//视频模型
@interface MVideo : MTLModel

@property (nonatomic, strong) NSString * title;       //标题,描述
@property (nonatomic, strong) NSString * content;     //内容
@property (nonatomic, strong) NSString * thumbImgUrl; //封面图片URL

//
@property (nonatomic, strong) NSString * videoUrl;    //MP4视频URL
@property (nonatomic, strong) NSString * m3u8Url;     //m3u8视频URL
@property (nonatomic, strong) NSString * updateTime;  //更新时间
@property (nonatomic, strong) NSString * videoSource; //更新时间
@property (nonatomic, strong) NSString * replyid;
@property (nonatomic, strong) NSString * video_id;
@property (nonatomic, strong) NSString * reply_id;     //跟帖id
@property (nonatomic, assign) NSInteger replyCount;   //跟帖人数
@property (nonatomic, assign) NSInteger playCount;    //播放次数
@property (nonatomic, assign) NSInteger length;       //时长

// layout size
@property (nonatomic, assign) CGFloat titleTop;
@property (nonatomic, assign) CGFloat titleHeight;
@property (nonatomic, assign) CGFloat contentHeight;
@property (nonatomic, assign) CGFloat videoTop;
@property (nonatomic, assign) CGFloat videoHeight;
@property (nonatomic, assign) CGFloat bottomBarHeight;
@property (nonatomic, assign) CGFloat marginTop;
@property (nonatomic, assign) CGFloat cellHeight;
@property (nonatomic, assign) CGFloat containerHeight;
// video status layout
@property (nonatomic, strong) VideoStatusLayout * statusLayout;

- (void)layout; // 计算布局

@end

  • MVideo.m
#import "MVideo.h"
#import "MTLValueTransformer.h"
#import "NSValueTransformer+MTLPredefinedTransformerAdditions.h"

@implementation MVideo

- (instancetype)init
{
    if (self == [super init]) {
        self.statusLayout = [[VideoStatusLayout alloc]init];
        self.videoHeight = kPlayViewHeight;
        self.bottomBarHeight = kBottomToolbarHeight;
        self.marginTop = kVideoCellTopMargin;
    }
    return self;
}

+ (NSDictionary *) JSONKeyPathsByPropertyKey {
    return @{
             @"videoUrl":@"mp4_url",
             @"m3u8Url":@"m3u8_url",
             @"thumbImgUrl":@"cover",
             @"updateTime":@"ptime",
             @"addTime":@"add_time",
             @"content":@"description",
             @"video_id":@"vid",
             @"videoSource":@"videosource",
             @"reply_id":@"replyid"
             };
}

// 由于网易的布局比较简单,数据基本上都是标题描述格式,字符串的高度只是做局部动态调整(动态调整适合复杂布局)
- (void)layout {
    if (nil != self.title) {
        self.titleTop = kTitleViewTopMargin;
        self.titleHeight = kTopViewTitleHeight;
    }
    if (nil != self.content) {
        self.contentHeight = kTopViewContentHeight;
    }
    self.videoTop = _titleTop + _titleHeight + _contentHeight + kPlayViewTopInset;
    self.containerHeight = _videoTop + _videoHeight + _bottomBarHeight;
    self.cellHeight = _marginTop + _containerHeight;
}

@end

1.2 mp4的Url格式

  • 实际数据

(lldb) po video.videoUrl
http://flv3.bn.netease.com/videolib3/1707/03/bGYNX4211/SD/bGYNX4211-mobile.mp4

  • 打开视频地址,视频共有1分22秒(82秒)

1.3 m3u8的Url格式

  • 实际数据

(lldb) po video.m3u8Url
http://flv.bn.netease.com/videolib3/1707/03/bGYNX4211/SD/movie_index.m3u8

  • 打开效果,弹出下载.m3u8格式文件提示如下:
iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例)_第1张图片
  • 继续打开所下载m3u8格式文件,其内容如下
#EXTM3U
#EXT-X-TARGETDURATION:30
#EXTINF:30,
http://flv.bn.netease.com/videolib3///1707/03///bGYNX4211//SD/bGYNX4211-mobile-1.ts
#EXTINF:33,
http://flv.bn.netease.com/videolib3///1707/03///bGYNX4211//SD/bGYNX4211-mobile-2.ts
#EXTINF:18,
http://flv.bn.netease.com/videolib3///1707/03///bGYNX4211//SD/bGYNX4211-mobile-3.ts
#EXT-X-ENDLIST
  • 分别在浏览器输入如上三个地址,同样弹出下载.ts格式的文件提示如下:
iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例)_第2张图片
  • 分别保存上个地址下载后的文件
  • 打开第一个.ts文件:31秒
iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例)_第3张图片
  • 打开第二个.ts文件:34秒
iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例)_第4张图片
  • 打开第三个.ts文件:19秒
iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例)_第5张图片
  • m3u8Url所指向的三个.ts文件加起来共有84秒,接近videoUrl指向的视频时间82秒。

2.播放视频调用栈


2.1 调用处

  • 调用处

NeteaseNews/Scene/Video/VideoCells/VideoCellPlayVM.m

#pragma mark  - UserVideoCellDelegate

- (void)userVideoCell:(UserVideoCell *)cell startPlay:(MVideo *)video {
    @weakify(self);
    [self judgeNetStatusCompletion:^(BOOL shouldPlay) {
        @strongify(self);
        if (shouldPlay) {
            if (video.statusLayout.playStatus == VideoPlayStatusPause) {
                video.statusLayout.playStatus = VideoPlayStatusPlaying;
                [self.playerManger playContent];
                [cell refreshPlayViewBy:video isOnlyProgress:NO];
                return;
            }
            // old cell refresh
            if (self.playingIndexPath) {
                MVideo * oldVideo = [self.videoVMSource.videoSource objectAtIndex:self.playingIndexPath.row];
                oldVideo.statusLayout.playStatus = VideoPlayStatusNormal;
                oldVideo.statusLayout.totalTime = 0.0;
                oldVideo.statusLayout.progress = 0.0f;
                oldVideo.statusLayout.buffer = 0.0f;
                
                UserVideoCell * oldCell = (UserVideoCell *)[self.videoVMSource.tableView p_cellForRowAtIndexPath:self.playingIndexPath];
                [oldCell refreshPlayViewBy:oldVideo isOnlyProgress:NO];
            }
            // refresh cell
            NSIndexPath * indexPath = [self.videoVMSource.tableView p_indexPathForCell:cell];
            self.playingIndexPath = indexPath;
            self.videoVMSource.playingIndexPath = indexPath;
            video.statusLayout.playStatus = VideoPlayStatusBeginPlay;
            video.statusLayout.totalTime = [self.playerManger.player currentItemDuration];
            NSLog(@"totaltime === %.2f",video.statusLayout.totalTime);
            
            // play
            AVPlayerTrack * track = [[AVPlayerTrack alloc]initWithStreamURL:[NSURL URLWithString:video.m3u8Url]];
            [track setItemIndexPath:indexPath];
            [self.playerManger setCurrentPlayerView:cell.playView];
            [self.playerManger loadVideoWithTrack:track];
            
            // play count
            video.playCount ++;
            
            // reload data
            /*
             *  刷新不采用reloadata方法,而是个别位置的针对刷新,避免整体上UI的影响
             */
            [cell refreshPlayViewBy:video isOnlyProgress:NO];
        }
    }];
    
}

其中,有个属性:

@property (nonatomic, strong) AVPlayerManger * playerManger;

播放视频的关键方法为:

 [self.playerManger loadVideoWithTrack:track];

2.2 自定义AVPlayerManger类

NeteaseNews/Scene/Video/VideoPlayer/AVPlayerManger.m

- (void)loadVideoWithTrack:(id)track

#pragma mark - Resource
- (void)loadVideoWithTrack:(id)track
{
    self.track = track;
    self.state = AVPlayerStateContentLoading;
    
    void(^completionHandler)() = ^{
        [self playVideoTrack:self.track];
    };
    switch (self.state) {
        case AVPlayerStateError:
        case AVPlayerStateContentPaused:
        case AVPlayerStateContentLoading:
            completionHandler();
            break;
        case AVPlayerStateContentPlaying:
            [self pauseContentWithCompletionHandler:completionHandler];
            break;
        default:
            break;
    };
}

- (void)playVideoTrack:(id)track

#pragma mark - play
- (void)playVideoTrack:(id)track
{
    [self clearPlayer];
    
    NSURL *streamURL = [track streamURL];
    if (!streamURL) {
        return;
    }
    
    if (_delegate && [_delegate respondsToSelector:@selector(videoPlayer:willStartVideo:)]) {
        [_delegate videoPlayer:self willStartVideo:track];
        self.state = AVPlayerStateContentLoading;
    }
    [self playAVPlayer:streamURL playerLayerView:self.playerView track:track];
}

- (void)playAVPlayer:(NSURL*)streamURL playerLayerView:(id)playerLayerView track:(id)track

  • 调用了AVFoundation框架
- (void)playAVPlayer:(NSURL*)streamURL playerLayerView:(id)playerLayerView track:(id)track {
    
    if (!track.isVideoLoadedBefore) {
        track.isVideoLoadedBefore = YES;
    }
    
    NSAssert(self.playerView.superview, @"you must setup current playerview as a container view!");
        
    AVURLAsset* asset = [[AVURLAsset alloc] initWithURL:streamURL options:@{ AVURLAssetPreferPreciseDurationAndTimingKey : @YES }];
    [asset loadValuesAsynchronouslyForKeys:@[kTracksKey, kPlayableKey] completionHandler:^{
        // Completion handler block.
        RUN_ON_UI_THREAD(^{
            if (![asset.URL.absoluteString isEqualToString:streamURL.absoluteString]) {
                NSLog(@"Ignore stream load success. Requested to load: %@ but the current stream should be %@.", asset.URL.absoluteString, streamURL.absoluteString);
                return;
            }
            NSError *error = nil;
            AVKeyValueStatus status = [asset statusOfValueForKey:kTracksKey error:&error];
            if (status == AVKeyValueStatusLoaded) {
                self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
                self.avPlayer = [self playerWithPlayerItem:self.playerItem];
                self.player = (id)self.avPlayer;
                [playerLayerView setPlayer:self.avPlayer];
                
            } else {
                // You should deal with the error appropriately.
                [self handleErrorCode:kVideoPlayerErrorAssetLoadError track:track];
                NSLog(@"The asset's tracks were not loaded:\n%@", error);
            }
        });
    }];  
}

其中

 AVURLAsset* asset = [[AVURLAsset alloc] initWithURL:streamURL options:@{ AVURLAssetPreferPreciseDurationAndTimingKey : @YES }];

的AVURLAsset属于AVFoudation框架:

AVFoudation>Headers>AVAsset.h

运行的时候,查看streamURL实际数据:

(lldb) po streamURL
http://flv.bn.netease.com/videolib3/1707/03/UdTtq1944/SD/movie_index.m3u8

2.3 AVFoundation框架头文件

AVFoundation>Headers>AVAsset.h

- (instancetype)initWithURL:(NSURL *)URL options:(nullable NSDictionary *)options NS_DESIGNATED_INITIALIZER;

你可能感兴趣的:(iOS网络视频下载与播放:两种视频URL格式(m3u8 & mp4)(AVFoundation框架篇·以网易视频为例))