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

1. 探究两种视频URL格式


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

1.1 建立视频模型

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

@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格式文件提示如下:
  • 继续打开所下载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格式的文件提示如下:
  • 分别保存上个地址下载后的文件
  • 打开第一个.ts文件:31秒
  • 打开第二个.ts文件:34秒
  • 打开第三个.ts文件:19秒
  • 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<AVPlayerTrackProtocol>)track

#pragma mark - Resource
- (void)loadVideoWithTrack:(id<AVPlayerTrackProtocol>)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<AVPlayerTrackProtocol>)track

#pragma mark - play
- (void)playVideoTrack:(id<AVPlayerTrackProtocol>)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<AVPlayerViewDelegate>)playerLayerView track:(id<AVPlayerTrackProtocol>)track

  • 调用了AVFoundation框架
- (void)playAVPlayer:(NSURL*)streamURL playerLayerView:(id<AVPlayerViewDelegate>)playerLayerView track:(id<AVPlayerTrackProtocol>)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<MangerPlayer>)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<NSString *, id> *)options NS_DESIGNATED_INITIALIZER;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容