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格式文件提示如下:
- 继续打开所下载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
#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
#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
- 调用了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;