遇到個需求需要涉及到視頻播放,那麼沒辦法,先找資料開始進一步了解下這個不熟悉的東西.一個是MP,一個是AV,MP是封裝好的,用起來非常簡單,但是自定義樣式就基本不可能了。AVPlayer存在於AVFundation中,更接近於底層,所以靈活性更強大,廢話不多說,咱們先簡單寫個Demo看下他的工作原理,然後模仿網易新聞寫個界面出來,這裡用到了一個封裝的框架,如果不熟悉內部原理的同學可以先看看我寫的第一個Demo,基本所有邏輯都有。
這裡容我啰嗦一句:
開發中,單純的使用AVPlayer類是無法播放視頻的,需要將視頻層添加到AVPLayerLayer層,這樣視頻才能顯示出來,Layer的定義方式有兩種,一種是下面這種直接使用PlayerLayer,還有一個就是自己做一個View,然後把他自身的Layer改成playerLayer
第一種方式:
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; self.playerLayer.frame = self.view.bounds; [self.view.layer addSublayer:self.playerLayer];
第二種方式:
//修改當前view的 layer的 class +(Class)layerClass { //AVPlayerLayer return [AVPlayerLayer class]; }
只能上傳2M的東東,這視頻一幀一幀消耗太快了,都不敢多錄了,各位大爺將就著看吧。。。。。。
不要來打我,不然我讓我表哥打死你
先簡單介紹下AVPlayer的用法
很多朋友應該和我一樣,一開始接觸視頻的時候都不知道用什麼東東來寫,如果是大神
就直接下載Demo吧。小白來介紹下,我也第一次用
第一:初始化播放器
// 初始化播放器item self.playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:@"http://flv2.bn.netease.com/videolib3/1608/30/zPuaL7429/SD/zPuaL7429-mobile.mp4"]]; self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem]; // 初始化播放器的Layer self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; // layer的frame self.playerLayer.frame = self.backView.bounds; // layer的填充屬性 和UIImageView的填充屬性類似 // AVLayerVideoGravityResizeAspect 等比例拉伸,會留白 // AVLayerVideoGravityResizeAspectFill // 等比例拉伸,會裁剪 // AVLayerVideoGravityResize // 保持原有大小拉伸 self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; // 把Layer加到底部View上 [self.backView.layer insertSublayer:self.playerLayer atIndex:0];
// 監聽播放器狀態變化 [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; // 監聽緩存進去,就是大家所看到的一開始進去底部灰色的View會迅速加載 [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; //旋轉屏幕通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onDeviceOrientationChange) name:UIDeviceOrientationDidChangeNotification object:nil ];
// 監聽播放器的變化屬性 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context { if ([keyPath isEqualToString:@"status"]) { AVPlayerItemStatus statues = [change[NSKeyValueChangeNewKey] integerValue]; switch (statues) { // 監聽到這個屬性的時候,理論上視頻就可以進行播放了 case AVPlayerItemStatusReadyToPlay: // 最大值直接用sec,以前都是 // CMTimeMake(幀數(slider.value * timeScale), 幀/sec) self.slider.maximumValue = CMTimeGetSeconds(self.playerItem.duration); [self initTimer]; // 啟動定時器 5秒自動隱藏 if (!self.autoDismissTimer) { self.autoDismissTimer = [NSTimer timerWithTimeInterval:8.0 target:self selector:@selector(autoDismissView:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.autoDismissTimer forMode:NSDefaultRunLoopMode]; } break; case AVPlayerItemStatusUnknown: break; // 這個就是不能播放喽,加載失敗了 case AVPlayerItemStatusFailed: // 這時可以通過`self.player.error.description`屬性來找出具體的原因 break; default: break; } } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) // 監聽緩存進度的屬性 { // 計算緩存進度 NSTimeInterval timeInterval = [self availableDuration]; // 獲取總長度 CMTime duration = self.playerItem.duration; CGFloat durationTime = CMTimeGetSeconds(duration); // 監聽到了給進度條賦值 [self.progressView setProgress:timeInterval / durationTime animated:NO]; } }
AVPlayerItemStatusReadyToPlay
AVPlayerItemStatusFailed
這兩個屬性還比較好理解,是個人都知道,但是這個是什麼鬼
AVPlayerItemStatusUnknown
內部是這麼解釋的
Indicates that the status of the player item is not yet known because it has not tried to load new media resources
for playback.
fk u 我英語不好,看不懂啊,估計是playerItem這個視頻資源對象掛了,識別不了,暫時不知道怎麼處理
來個官方的說話,顯得我比較牛B
// 調用plaer的對象進行UI更新 - (void)initTimer { // player的定時器 __weak typeof(self)weakSelf = self; // 每秒更新一次UI Slider [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { // 當前時間 CGFloat nowTime = CMTimeGetSeconds(weakSelf.playerItem.currentTime); // 總時間 CGFloat duration = CMTimeGetSeconds(weakSelf.playerItem.duration); // sec 轉換成時間點 weakSelf.nowLabel.text = [weakSelf convertToTime:nowTime]; weakSelf.remainLabel.text = [weakSelf convertToTime:(duration - nowTime)]; // 不是拖拽中的話更新UI if (!weakSelf.isDragSlider) { weakSelf.slider.value = CMTimeGetSeconds(weakSelf.playerItem.currentTime); } }]; }
// sec 轉換成指定的格式 - (NSString *)convertToTime:(CGFloat)time { // 初始化格式對象 NSDateFormatter *fotmmatter = [[NSDateFormatter alloc] init]; // 根據是否大於1H,進行格式賦值 if (time >= 3600) { [fotmmatter setDateFormat:@"HH:mm:ss"]; } else { [fotmmatter setDateFormat:@"mm:ss"]; } // 秒數轉換成NSDate類型 NSDate *date = [NSDate dateWithTimeIntervalSince1970:time]; // date轉字符串 return [fotmmatter stringFromDate:date]; }
// 啟動定時器 5秒自動隱藏 // 咱們這種初始化定時器的方式需要自己手動加到runloop上 // scheduledTimerWithTimeInterval用這個的時候就不需要手動加到runloop中 if (!self.autoDismissTimer) { self.autoDismissTimer = [NSTimer timerWithTimeInterval:8.0 target:self selector:@selector(autoDismissView:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.autoDismissTimer forMode:NSDefaultRunLoopMode]; }
#pragma mark - 自動隱藏bottom和top - (void)autoDismissView:(NSTimer *)timer { // player的屬性rate /* indicates the current rate of playback; 0.0 means "stopped", 1.0 means "play at the natural rate of the current item" */ if (self.player.rate == 0) { // 暫停狀態就不隱藏 } else if (self.player.rate == 1) { if (self.bottomView.alpha == 1) { [UIView animateWithDuration:1.0 animations:^{ self.bottomView.alpha = 0; self.topView.alpha = 0; }]; } } }
其實切換的時候就是把只之前的Layer移除,然後重新布局,加到KeyWindow中去
// 全屏顯示 -(void)toFullScreenWithInterfaceOrientation:(UIInterfaceOrientation )interfaceOrientation{ // 先移除之前的 [self.backView removeFromSuperview]; // 初始化 self.backView.transform = CGAffineTransformIdentity; if (interfaceOrientation==UIInterfaceOrientationLandscapeLeft) { self.backView.transform = CGAffineTransformMakeRotation(-M_PI_2); }else if(interfaceOrientation==UIInterfaceOrientationLandscapeRight){ self.backView.transform = CGAffineTransformMakeRotation(M_PI_2); } // BackView的frame能全屏 self.backView.frame = CGRectMake(0, 0, kScreenWidth, kScreenHeight); // layer的方向寬和高對調 self.playerLayer.frame = CGRectMake(0, 0, kScreenHeight, kScreenWidth); // remark 約束 [self.bottomView mas_remakeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(50); make.top.mas_equalTo(kScreenWidth-50); make.left.equalTo(self.backView).with.offset(0); make.width.mas_equalTo(kScreenHeight); }]; [self.topView mas_remakeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(50); make.left.equalTo(self.backView).with.offset(0); make.width.mas_equalTo(kScreenHeight); }]; [self.closeButton mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.backView).with.offset(5); make.height.mas_equalTo(30); make.width.mas_equalTo(30); make.top.equalTo(self.backView).with.offset(10); }]; [self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.topView).with.offset(45); make.right.equalTo(self.topView).with.offset(-45); make.center.equalTo(self.topView); make.top.equalTo(self.topView).with.offset(0); }]; [self.nowLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.slider.mas_left).with.offset(0); make.top.equalTo(self.slider.mas_bottom).with.offset(0); make.size.mas_equalTo(CGSizeMake(100, 20)); }]; [self.remainLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.slider.mas_right).with.offset(0); make.top.equalTo(self.slider.mas_bottom).with.offset(0); make.size.mas_equalTo(CGSizeMake(100, 20)); }]; // 加到window上面 [[UIApplication sharedApplication].keyWindow addSubview:self.backView]; } // 縮小到cell -(void)toCell{ // 先移除 [self.backView removeFromSuperview]; __weak typeof(self)weakSelf = self; [UIView animateWithDuration:0.5f animations:^{ weakSelf.backView.transform = CGAffineTransformIdentity; weakSelf.backView.frame = CGRectMake(0, 80, kScreenWidth, kScreenHeight / 2.5); weakSelf.playerLayer.frame = weakSelf.backView.bounds; // 再添加到View上 [weakSelf.view addSubview:weakSelf.backView]; // remark約束 [self.bottomView mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(weakSelf.backView).with.offset(0); make.right.equalTo(weakSelf.backView).with.offset(0); make.height.mas_equalTo(50); make.bottom.equalTo(weakSelf.backView).with.offset(0); }]; [self.topView mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(weakSelf.backView).with.offset(0); make.right.equalTo(weakSelf.backView).with.offset(0); make.height.mas_equalTo(50); make.top.equalTo(weakSelf.backView).with.offset(0); }]; [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(weakSelf.backView).with.offset(5); make.centerY.equalTo(weakSelf.topView); make.size.mas_equalTo(CGSizeMake(30, 30)); }]; }completion:^(BOOL finished) { }]; }
下面咱們試著寫個網易播放視頻的Demo,在tableView中使用下,效果圖已經在最上面了
這裡無非多了幾個屬性
@property (nonatomic,strong) NSIndexPath *currentIndexPath; // 當前播放的cell
@property (nonatomic,assign) BOOL isSmallScreen; // 是否放置在window上
@property(nonatomic,strong) ViedoTableViewCell *currentCell; // 當前cell
分析1:全屏小屏切換的時候回到指定的cell,那麼先點擊播放記錄位置
1.第一種cell播放:Layer是加載到cell上的背景圖片區域的 滾動的時候要記錄當前cell
2.第二種全屏播放:Layer是加載到Window上的 frame全屏
3.第三種小窗播放:它其實就是全屏播放的一個特例,也是加載到Window上的,frame自定義
其實不同狀態的切換無非就是Layer所在View的位置不停切換
下面這個方法就是記錄當前播放的cell下標
#pragma mark - 播放器播放 - (void)startPlayVideo:(UIButton *)sender { // 獲取當前的indexpath self.currentIndexPath = [NSIndexPath indexPathForRow:sender.tag inSection:0]; // iOS 7 和 8 以上獲取cell的方式不同 if ([UIDevice currentDevice].systemVersion.floatValue>=8||[UIDevice currentDevice].systemVersion.floatValue<7) { self.currentCell = (ViedoTableViewCell *)sender.superview.superview; }else{//ios7系統 UITableViewCell上多了一個層級UITableViewCellScrollView self.currentCell = (ViedoTableViewCell *)sender.superview.superview.subviews; } ViedoModel *model = [self.viedoLists objectAtIndex:sender.tag]; // 小窗口的時候點擊播放另一個 先移除掉 if (self.isSmallScreen) { [self releaseWMPlayer]; self.isSmallScreen = NO; } // 當有上一個在播放的時候 點擊 就先release if (self.wmPlayer) { [self releaseWMPlayer]; self.wmPlayer = [[WMPlayer alloc]initWithFrame:self.currentCell.mainImageView.bounds]; self.wmPlayer.delegate = self; self.wmPlayer.closeBtnStyle = CloseBtnStyleClose; self.wmPlayer.URLString = model.mp4URL; self.wmPlayer.titleLabel.text = model.title; // [wmPlayer play]; }else{ // 當沒有一個在播放的時候 self.wmPlayer = [[WMPlayer alloc]initWithFrame:self.currentCell.mainImageView.bounds]; self.wmPlayer.delegate = self; self.wmPlayer.closeBtnStyle = CloseBtnStyleClose; self.wmPlayer.titleLabel.text = model.title; self.wmPlayer.URLString = model.mp4URL; } // 把播放器加到當前cell的imageView上面 [self.currentCell.mainImageView addSubview:self.wmPlayer]; [self.currentCell.mainImageView bringSubviewToFront:self.wmPlayer]; [self.currentCell.playButton.superview sendSubviewToBack:self.currentCell.playButton]; [self.tableView reloadData]; }
分析2:上下滾動的時候根據坐標切換cell顯示還是小窗顯示
#pragma mark scrollView delegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if(scrollView ==self.tableView){ if (self.wmPlayer==nil) { return; } if (self.wmPlayer.superview) { // 當前cell在tableView中的frame // (lldb) po rectInTableView // (origin = (x = 0, y = 0), size = (width = 375, height = 300)) CGRect rectInTableView = [self.tableView rectForRowAtIndexPath:self.currentIndexPath]; // 把當前的frame從tableView轉換到屏幕View上面去 // (lldb) po rectInSuperview // (origin = (x = 0, y = 61), size = (width = 375, height = 300)) CGRect rectInSuperview = [self.tableView convertRect:rectInTableView toView:[self.tableView superview]]; NSLog(@"Y軸變化:%lf,currentCell:%lf",rectInSuperview.origin.y,self.currentCell.mainImageView.frame.size.height); // 當網上移出屏幕的時候或者往下移出屏幕的時候,根據邏輯是否加載到小窗上來 if (rectInSuperview.origin.y<-self.currentCell.mainImageView.frame.size.height ||rectInSuperview.origin.y>kScreenHeight-kNavbarHeight-kTabBarHeight) {//往上拖動 // 如果已經小屏幕顯示了,就不做任何操作 if ([[UIApplication sharedApplication].keyWindow.subviews containsObject:self.wmPlayer]&&self.isSmallScreen) { self.isSmallScreen = YES; }else{ //放widow上,小屏顯示 這裡的邏輯和展示到全屏是一樣的道理,只是位置和frame自己定義就好了,想放哪就放哪 [self toSmallScreen]; } }else{ // 如果已經在cell裡面了,那麼就不做任何操作 if ([self.currentCell.mainImageView.subviews containsObject:self.wmPlayer]) { }else{ // 如果進入屏幕,而且未在cell上,那麼動畫回currentCell [self toCell]; } } } } }
// 滾動的時候小屏幕,放window上顯示 -(void)toSmallScreen{ //放widow上 [self.wmPlayer removeFromSuperview]; __weak typeof(self)weakSelf = self; [UIView animateWithDuration:0.5f animations:^{ weakSelf.wmPlayer.transform = CGAffineTransformIdentity; // 設置window上的位置 weakSelf.wmPlayer.frame = CGRectMake(kScreenWidth/2,kScreenHeight-kTabBarHeight + 40 -(kScreenWidth/2)*0.75, kScreenWidth/2, (kScreenWidth/2)*0.75); weakSelf.wmPlayer.playerLayer.frame = weakSelf.wmPlayer.bounds; // 下面就是更新布局的代碼,此處省略了,需要的去下載Demo看看
分析3:用MJRefresh做個JD的加載動畫(隨便做的,大家隨便感受下)
MKJRefreshHeader * header = [MKJRefreshHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)]; header.stateLabel.hidden = YES; header.lastUpdatedTimeLabel.hidden = YES; header.mj_h = 80; self.tableView.mj_header = header;
這裡簡單的寫個重寫的方法示例,具體需要看的大家去下載Demo
- (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根據狀態做事情 // 刷新完畢 if (state == MJRefreshStateIdle) { if (oldState == MJRefreshStateRefreshing) { self.arrowView.transform = CGAffineTransformIdentity; [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ self.loadingView1.alpha = 0.0; } completion:^(BOOL finished) { // 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態 if (self.state != MJRefreshStateIdle) return; self.loadingView1.alpha = 1.0; [self.loadingView1 endRefresing]; self.arrowView.hidden = NO; }]; } else { // 拉倒即將刷新的時候,又往回縮,不進行刷新 [self.loadingView1 endRefresingDown]; self.arrowView.hidden = NO; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformIdentity; }]; } } else if (state == MJRefreshStatePulling) { // 繼續往下拉的時候 [self.loadingView1 refreing]; NSLog(@"連接點"); self.arrowView.hidden = NO; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI); }]; } else if (state == MJRefreshStateRefreshing) { // 刷新 self.loadingView1.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行 [self.loadingView1 refreing]; self.arrowView.hidden = YES; } }
篇幅有點多了,感覺沒必要什麼都寫出來,需要的同學去研究下Demo吧,感謝看到這
裡的小伙伴,你們都是好人,好人一生平安啊,要不再點個贊???!!!
簡單Demo示例地址:點擊打開簡單Demo鏈接
類似網易視頻播放最終Demo地址:點擊打開網易Demo鏈接
小白寫的東東,希望能幫到大家,大神的話可以給點意見,有問題留言哦