【iOS】AVPlayer 播放音视频

article/2025/10/2 14:42:05

1、常见的音视频播放器

iOS开发中不可避免地会遇到音视频播放方面的需求。

常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是,AVAudioPlayer 只支持本地音频的播放,而 AVPlayer 既支持本地音频播放,也支持网络音频播放。

常用的视频播放器有 MPMoviePlayerController、AVPlayer 等。不同的是,MPMoviePlayerController 内部做了高度封装,包含了播放控件,几乎不用写几行代码就能完成一个播放器,但是正是由于它的高度封装使得要自定义这个播放器变得很复杂,甚至是不可能完成。而 AVPlayer 更加接近于底层,所以灵活性也更强,更加方便自定义。

今天我们要介绍的主角就是强大的 AVPlayer。

2、AVPlayer

AVPlayer 存在于 AVFoundation 框架中,所以要使用 AVPlayer,要先在工程中导入 AVFoundation 框架。

AVPlayer 播放界面中不带播放控件,想要播放视频,必须要加入 AVPlayerLayer 中,并添加到其他能显示的 layer 当中。

AVPlayer 中音视频的播放、暂停功能对应着两个方法 playpause 来实现。

大多播放器都是通过通知来获取播放器的播放状态、加载状态等,而 AVPlayer 中对于获得播放状态和加载状态有用的通知只有一个:AVPlayerItemDidPlayToEndTimeNotification(播放完成通知) 。播放器的播放状态判断可以通过播放器的播放速度 rate 来获得,如果 rate 为0说明是停止状态,为1时则是正常播放状态。想要获取视频播放情况、缓冲情况等的实时变化,可以通过 KVO 监控 AVPlayerItem 的 statusloadedTimeRanges 等属性来获得。当 AVPlayerItem 的 status 属性为 AVPlayerStatusReadyToPlay 时说明可以开始播放,只有处于这个状态时才能获得视频时长等信息;当 loadedTimeRanges 改变时(每缓冲一部分数据就会更新此属性),可以获得本次缓冲加载的视频范围(包含起始时间、本次加载时长),这样一来就可以实时获得缓冲情况。

AVPlayer 中播放进度的获取通常是通过:- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block 方法。这个方法会在设定的时间间隔内定时更新播放进度,通过 time 参数通知客户端。至于播放进度的跳转则是依靠 - (void)seekToTime:(CMTime)time 方法。

AVPlayer 还提供了 - (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item 方法用于在不同视频之间的切换(事实上在AVFoundation内部还有一个AVQueuePlayer专门处理播放列表切换,有兴趣的朋友可以自行研究,这里不再赘述)。

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

 

3、自定义AVPlayer

下面是我自己在项目中封装的音视频播放器,贴上代码,大家可以参考一下。

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
​
/**播放器开始播放的通知当存在多个播放器,可使用该通知在其他播放器播放时暂停当前播放器*/
extern NSString * const YDPlayerDidStartPlayNotification;
​
/**enum 播放器状态
​- YDPlayerStatusUnknown: 未知- YDPlayerStatusPlaying: 播放中- YDPlayerStatusLoading: 加载中- YDPlayerStatusPausing: 暂停中- YDPlayerStatusFailed: 播放失败- YDPlayerStatusFinished: 播放完成*/
typedef NS_ENUM(NSInteger, YDPlayerStatus) {YDPlayerStatusUnknown,YDPlayerStatusPlaying,YDPlayerStatusLoading,YDPlayerStatusPausing,YDPlayerStatusFailed,YDPlayerStatusFinished
};
​
​
@interface YDPlayerMananger : NSObject
​
/**播放器*/
@property (nonatomic, strong) AVPlayer *player;
​
/**播放器layer层*/
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
​
/**当前PlayerItem*/
@property (nonatomic, strong) AVPlayerItem *currentItem;
​
/**播放器状态*/
@property (nonatomic, assign) YDPlayerStatus playStatus;
​
/**Item总时长回调*/
@property (nonatomic, copy) void(^currentItemDurationCallBack)(AVPlayer *player, CGFloat duration);
​
/**Item播放进度回调*/
@property (nonatomic, copy) void(^currentPlayTimeCallBack)(AVPlayer *player, CGFloat time);
​
/**Item缓冲进度回调*/
@property (nonatomic, copy) void(^currentLoadedTimeCallBack)(AVPlayer *player, CGFloat time);
​
/**Player状态改变回调*/
@property (nonatomic, copy) void(^playStatusChangeCallBack)(AVPlayer *player, YDPlayerStatus status);
​
​
/**初始化方法@param url 播放链接@return YDPlayerMananger对象*/
- (instancetype)initWithURL:(NSURL *)url;
​
​
/**创建单例对象
​@return YDPlayerMananger单例对象*/
+ (instancetype)shareManager;
​
​
/**将播放器展示在某个View@param view 展示播放器的View*/
- (void)showPlayerInView:(UIView *)view withFrame:(CGRect)frame;
​
​
/**替换PlayerItem
​@param url 需要播放的链接*/
- (void)replaceCurrentItemWithURL:(NSURL *)url;
​
​
/**播放某个链接
​@param urlStr 需要播放的链接*/
- (void)playWithUrl:(NSString *)urlStr;
​
​
/**开始播放*/
- (void)play;
​
​
/**暂停播放*/
- (void)pause;
​
​
/**停止播放*/
- (void)stop;
​
​
/**跳转到指定时间
​@param time 指定的时间*/
- (void)seekToTime:(CGFloat)time;
​
@end#import "YDPlayerMananger.h"
​
NSString * const YDPlayerDidStartPlayNotification = @"YDPlayerDidStartPlayNotification";
​
@interface YDPlayerMananger ()
@property (nonatomic, strong) id timeObserver; // 监控播放进度的观察者
​
@end
​
@implementation YDPlayerMananger
​
#pragma mark - 生命周期
​
- (instancetype)init
{if (self = [super init]) {AVAudioSession *audioSession = [AVAudioSession sharedInstance];[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];[audioSession setActive:YES error:nil];self.player = [[AVPlayer alloc] init];[self addNotificationAndObserver];}return self;
}
​
- (instancetype)initWithURL:(NSURL *)url
{if (self = [self init]) {[self replaceCurrentItemWithURL:url];}return self;
}
​
+ (instancetype)shareManager
{static YDPlayerMananger *manager = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{manager = [[self alloc] init];});return manager;
}
​
- (void)dealloc
{[self removeNotificationAndObserver];
}
​
#pragma mark - 公开方法
​
- (void)showPlayerInView:(UIView *)view withFrame:(CGRect)frame
{self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];_playerLayer.frame = frame;_playerLayer.backgroundColor = [UIColor blackColor].CGColor;_playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;[view.layer addSublayer:_playerLayer];
}
​
- (void)replaceCurrentItemWithURL:(NSURL *)url
{// 移除当前观察者if (_currentItem) {[_currentItem removeObserver:self forKeyPath:@"status"];[_currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];}_currentItem = [[AVPlayerItem alloc] initWithURL:url];[self.player replaceCurrentItemWithPlayerItem:_currentItem];// 重新添加观察者[_currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];[_currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
​
- (void)playWithUrl:(NSString *)urlStr
{[self replaceCurrentItemWithURL:[NSURL URLWithString:urlStr]];[self play];
}
​
- (void)play
{[self.player play];self.playStatus = YDPlayerStatusPlaying;// 发起开始播放的通知[[NSNotificationCenter defaultCenter] postNotificationName:YDPlayerDidStartPlayNotification object:_player];
}
​
- (void)pause
{[self.player pause];self.playStatus = YDPlayerStatusPausing;
}
​
- (void)stop
{[self.player pause];[_currentItem cancelPendingSeeks];self.playStatus = YDPlayerStatusFinished;
}
​
- (void)seekToTime:(CGFloat)time
{[_currentItem seekToTime:CMTimeMakeWithSeconds(time, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
​
#pragma mark - 私有方法
​
// 添加通知、观察者
- (void)addNotificationAndObserver
{// 添加播放完成通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];// 添加打断播放的通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionComing:) name:AVAudioSessionInterruptionNotification object:nil];// 添加插拔耳机的通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChanged:) name:AVAudioSessionRouteChangeNotification object:nil];// 添加观察者监控播放器状态[self addObserver:self forKeyPath:@"playStatus" options:NSKeyValueObservingOptionNew context:nil];// 添加观察者监控进度__weak typeof(self) weakSelf = self;_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {__strong typeof(self) strongSelf = weakSelf;if (strongSelf.currentPlayTimeCallBack) {float currentPlayTime = (double)strongSelf.currentItem.currentTime.value / strongSelf.currentItem.currentTime.timescale;strongSelf.currentPlayTimeCallBack(strongSelf.player, currentPlayTime);}}];
}
​
// 移除通知、观察者
- (void)removeNotificationAndObserver
{[[NSNotificationCenter defaultCenter] removeObserver:self];[self removeObserver:self forKeyPath:@"playStatus"];[_player removeTimeObserver:_timeObserver];if (_currentItem) {[_currentItem removeObserver:self forKeyPath:@"status"];[_currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];}
}
​
#pragma mark - 观察者
​
// 观察者
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{if ([keyPath isEqualToString:@"status"]) {AVPlayerStatus status = [[change objectForKey:@"new"] intValue];if (status == AVPlayerStatusReadyToPlay) {// 获取视频长度if (self.currentItemDurationCallBack) {CGFloat duration = CMTimeGetSeconds(_currentItem.duration);self.currentItemDurationCallBack(_player, duration);}} else if (status == AVPlayerStatusFailed) {self.playStatus = YDPlayerStatusFailed;} else {self.playStatus = YDPlayerStatusUnknown;}} else if ([keyPath isEqualToString:@"playStatus"]) {if (self.playStatusChangeCallBack) {self.playStatusChangeCallBack(_player, _playStatus);}} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {// 计算缓冲总进度NSArray *loadedTimeRanges = [_currentItem loadedTimeRanges];CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];float startSeconds = CMTimeGetSeconds(timeRange.start);float durationSeconds = CMTimeGetSeconds(timeRange.duration);NSTimeInterval loadedTime = startSeconds + durationSeconds;
​if (self.playStatus == YDPlayerStatusPlaying && self.player.rate <= 0) {self.playStatus = YDPlayerStatusLoading;}// 卡顿时缓冲完成后自动播放if (self.playStatus == YDPlayerStatusLoading) {NSTimeInterval currentTime = self.player.currentTime.value / self.player.currentTime.timescale;if (loadedTime > currentTime + 5) {[self play];}}if (self.currentLoadedTimeCallBack) {self.currentLoadedTimeCallBack(_player, loadedTime);}}
}
​
#pragma mark - 通知
​
// 播放完成通知
- (void)playbackFinished:(NSNotification *)notification
{AVPlayerItem *playerItem = (AVPlayerItem *)notification.object;if (playerItem == _currentItem) {self.playStatus = YDPlayerStatusFinished;}
}
​
// 插拔耳机通知
- (void)routeChanged:(NSNotification *)notification
{NSDictionary *dic = notification.userInfo;int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];// 旧输出不可用if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];// 原设备为耳机则暂停if ([portDescription.portType isEqualToString:@"Headphones"]) {[self pause];}}
}
​
// 来电、闹铃打断播放通知
- (void)interruptionComing:(NSNotification *)notification
{NSDictionary *userInfo = notification.userInfo;AVAudioSessionInterruptionType type = [userInfo[AVAudioSessionInterruptionTypeKey] intValue];if (type == AVAudioSessionInterruptionTypeBegan) {[self pause];}
}
​
@end

4、注意点

在使用 AVPlayer 时需要注意的是,由于播放状态、缓冲状态等是通过 KVO 监控 AVPlayerItem 的 status、loadedTimeRanges 等属性来获得的,在使用 - (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item 切换视频后,当前的 AVPlayerItem 实际上已经被释放掉了,所以一定要及时移除观察者并重新添加,否则会引起崩溃。

如果有大神发现文章中的错误,欢迎指正。有兴趣下载文中 Demo 的朋友,可以前往我的GitHub:GitHud地址

原文链接:【iOS】AVPlayer 播放音视频 - 掘金


http://chatgpt.dhexx.cn/article/v0wvxXue.shtml

相关文章

【iOS】AVPlayer 视频播放

视频播放器的类别 iOS开发中不可避免地会遇到音视频播放方面的需求。 常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是&#xff0c;AVAudioPlayer 只支持本地音频的播放&#xff0c;而 AVPlayer 既支持本地音频播放&#xff0c;也支持网络音频播放。 常用的视频播放…

iOS音视频播放-AVPlayer简单使用

按公司需求需要对音频文件进行后台播放,借此机会对音频播放做了个总结.主要针对 AVPlayer 进行详细说明. iOS 各播放器比较 名称使用环境优点确点System Sound ServicesAVFoundationC语言的底层写法&#xff0c;节省内存支持的格式有限&#xff0c;音量无法通过音量键控制&…

iOS音视频播放指南(二)

1. 让你的App支持画中画 画中画指可以让视频在小窗中播放,可以一边看视频一边刷知乎 你可以使用AVPlayerViewController或者AVPictureInPictureController来实现画中画播放。 其中AVPictureInPictureController支持你自定义一些播放控件 在支持画中画播放之前,确保你按照iOS音视…

iOS音视频播放指南(一)

1. 简介 苹果目前提供两个框架用来处理音视频播放 1.AVFoundation AVFoundation用于播放、处理音视频。可以通过结构图看到AVFoundation位于UIKit之下,很好理解AVFoundation并不提供用户界面,你可以自己自己构建用户界面来控制媒体的播放处理等功能。 但是苹果更推荐使用AVKit来…

iOS视频播放的基本方法

本文总结了iOS中最常见的视频播放方法&#xff0c;不同的方法都各具特点&#xff0c;我希望能够总结它们的不同&#xff0c;方便在开发中选择合适的技术方案。 Apple为我们提供了多种方法来实现视频播放&#xff0c;包括MPMoviePlayerController&#xff0c;MPMoviePlayerView…

【计算机系统1】4 Nim游戏

目录 目的与要求 内容与方法 步骤与过程 程序总体设计 核心数据结构及算法流程 核心代码 调试过程 界面展示子程序DISPLAY&#xff08;嵌套&#xff1a;球数展示子程序PUTBALL&#xff09; 游戏子程序GAME&#xff08;嵌套&#xff1a;单人每轮子程序PLAY&#xff09; 结论或体…

java nim游戏_LeetCode 292. Nim游戏

题目描述&#xff1a; 你和你的朋友&#xff0c;两个人一起玩 Nim游戏&#xff1a;桌子上有一堆石头&#xff0c;每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。 你们是聪明人&#xff0c;每一步都是最优解。 编写一个函数&#xff0c;来判断你…

Nim游戏、3的幂、4的幂

&#x1f345; Java学习路线&#xff1a;Java学习路线 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、CSDN哪吒公众号作者✌ 、Java架构师奋斗者&#x1f4aa; &#x1f345; 百日刷题计划&#xff1a;第 12 / 100 天。 &#x1f345; 扫描主页左侧二维码&a…

【数论】博弈论 —— nim游戏

知识点 一 . nim游戏的数学定义 Nim游戏是博弈论中最经典的模型&#xff0c;它又有着十分简单的规则和无比优美的结论 。 Nim游戏是组合游戏(Combinatorial Games)的一种&#xff0c;准确来说&#xff0c;属于“Impartial Combinatorial Games”&#xff08;以下简称ICG&#…

【模板题】几种常见的Nim游戏(博弈论)

一、AcWing 891. Nim游戏 【题目描述】 给定 n n n堆石子&#xff0c;两位玩家轮流操作&#xff0c;每次操作可以从任意一堆石子中拿走任意数量的石子&#xff08;可以拿完&#xff0c;但不能不拿&#xff09;&#xff0c;最后无法进行操作的人视为失败。 问如果两人都采用最优…

Kafka 为什么能那么快 | Kafka高效读写数据的原因

点击上方“服务端思维”&#xff0c;选择“设为星标” 回复”669“获取独家整理的精选资料集 回复”加群“加入全国服务端高端社群「后端圈」 无论 kafka 作为 MQ 也好&#xff0c;作为存储层也罢&#xff0c;无非就是两个功能&#xff08;好简单的样子&#xff09;&#xff0c…

【人人都懂密码学】一篇最易懂的Java密码学入门教程

密码与我们的生活息息相关&#xff0c;远到国家机密&#xff0c;近到个人账户&#xff0c;我们每天都在跟密码打交道&#xff1a; 那么&#xff0c;密码从何而来&#xff1f;生活中常见的加密是怎么实现的&#xff1f;怎么保证个人信息安全&#xff1f;本文将从这几方面进行浅谈…

Kafka必须掌握的核心技术--为什么吞吐量大、速度快?

点击上方“服务端思维”&#xff0c;选择“设为星标” 回复”669“获取独家整理的精选资料集 回复”加群“加入全国服务端高端社群「后端圈」 Kafka是大数据领域无处不在的消息中间件&#xff0c;目前广泛使用在企业内部的实时数据管道&#xff0c;并帮助企业构建自己的流计算应…

哪些软件问题也可导致硬盘录像机死机

硬盘录像机死机除了一些硬件上的问题之外&#xff0c;也有不少是由软件引起的。如&#xff1a; 1、病毒感染 病毒是计算机操作的大患&#xff0c;几乎人人恶之。病毒可以使计算机工作效率急剧下降&#xff0c;造成频繁死机、数据丢失、系统崩溃&#xff0c;甚至损坏主板、硬盘、…

导致硬盘录像机卡死的十大原因分析

硬盘录像机卡死除了一些技术上的问题之外&#xff0c;也有不少是由软件引起的。如&#xff1a; 1、病毒感染 病毒是硬盘录像机操作的大患&#xff0c;几乎人人恶之。病毒可以使硬盘录像机工作效率急剧下降&#xff0c;造成频繁死机、数据丢失、系统崩溃&#xff0c;甚至损坏主板…

谈项目管理和软件测试过程

谈项目管理和软件测试过程&#xff08;一&#xff09; 1. 软件测试在公司的组织保障是基础 1.1 研发部组织结构介绍 以华友公司研发部的组织结构为例&#xff0c;测试部门属于研发部副总裁直接管理&#xff0c;见如下结构图 公司研发部的组织结构图 …

NoSQL初探之人人都爱Redis:(1)Redis简介与简单安装

一、NoSQL的风生水起 1.1 后Web2.0时代的发展要求 随着互联网Web2.0网站的兴起&#xff0c;传统的关系数据库在应付Web2.0网站&#xff0c;特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心&#xff0c;暴露了很多难以克服的问题&#xff1a; &#xff08;1…

软件公司面试总结

文章目录 面试1面试2面试3面试4面试5面试6 面试1 1、你先做个简单的自我介绍吧 我叫张三&#xff0c;2015年在重庆邮电大学毕业。我读的专业是电子信息。工作已经快5年了。 我上家公司的主营业务是在柬埔寨做移动支付钱包。 最近做的一个项目是聚合支付的项目&#xff0c;主要…

腾讯的硬盘里,有互联网的昨天今天和明天

作者&#xff1a;史中 来源&#xff1a;浅黑科技&#xff08;qianheikeji&#xff09; 2018年1月1日&#xff0c;太阳照常升起。 世界上所有的时钟合谋&#xff0c;把最后一个90后推过了18岁的门槛。对于这些年轻的面孔来说&#xff0c;自由的风终于如期而至&#xff0c;只是其…

机械硬盘与SSD固态硬盘性能的深度

从7200转硬盘升级到10000转的迅猛龙&#xff0c;那叫量变。从10000转的迅猛龙升级到SSD&#xff0c;这个叫质变。2者的差距是有些地方相当大&#xff0c;而有些却很接近&#xff0c;主要是难比较。经常听到有人说&#xff1a;我买2个黑盘组RAID 0&#xff0c;传输率也有接近250…