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閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容