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;