AVPlayer
文章目录
- AVPlayer
AVFoundation 是苹果提供的音视频处理框架,是 iOS/macOS 开发中处理媒体的核心框架
其中主要包括
| 功能 | 相关类 |
|---|---|
| 播放音频/视频 | AVPlayer、AVAudioPlayer |
| 录音 | AVAudioRecorder |
| 录像/拍照 | AVCaptureSession |
| 音频混合 | AVAudioMix |
| 视频合成/剪辑 | AVAssetExportSession |
| 读取媒体信息 | AVAsset |
这里我主要讲一下音乐播放板块 AVPlayer
- AVAudioPlayer 用来播放本地音乐
- AVPlayer 本地和网络都可以
AVPlayer 一般由四个核心对象组成
- AVAsset 媒体资源本身
- AVPlayerItem 把资源包装成"可播放条目",表示一个 asset 在播放时的时间和呈现状态
- AVPlayer 播放器
- AVPlayerLayer 视频画面渲染层(纯音频播放不会用到)
类比一下:
比如你点了一首歌《晴天》,AVAsset 负责歌曲地址,总时长,音频轨道,元数据:歌名、歌手、专辑、封面等,AVPlayerItem 负责当前是否缓冲中,能不能继续流畅播放,当前状态等,AVPlayer 负责播放,暂停,定位等
AVAsset
AVAsset 不负责播放,只负责描述一个媒体文件,可以把它理解成文件的说明书,里面记录了时长、轨道信息、是否可播放等元数据
一个 mp3 文件内部其实包含两部分内容:真正的音频波形数据,以及一段叫做 ID3 标签的附加信息区域,用来存放歌名、歌手、专辑封面这些文字和图片
AVAsset 既能读到声音轨道,也能读到这部分标签信息:
objc
NSURL *url = [NSURL URLWithString:@"https://example.com/song.mp3"];
AVAsset *asset = [AVAsset assetWithURL:url];
// 对于网络歌曲这些属性是异步加载的,不能直接读取
// @[@"duration", @"tracks"]告诉 AVAsset 你想加载哪些属性,叫做 keys(键)
[asset loadValuesAsynchronouslyForKeys:@[@"duration", @"tracks", @"commonMetadata"] completionHandler:^{
CGFloat duration = CMTimeGetSeconds(asset.duration);
for (AVMetadataItem *item in asset.commonMetadata) {
if ([item.commonKey isEqualToString:@"artwork"]) {
UIImage *cover = [UIImage imageWithData:(NSData *)item.dataValue];
}
}
}];
AVAsset 的属性默认是异步加载 的,不能创建完就直接读取,必须通过 loadValuesAsynchronouslyForKeys 显式声明需要哪些属性,等加载完成的回调触发后才能安全访问
AVAsset 主要可以读取duration总时长,playable 是否可播放,tracks 轨道,metadata 元数据(标题、歌手、专辑、封面等)
AVPlayerItem
把 AVAsset 包装成"动态的播放单元",负责记录播放状态、缓冲进度、当前位置等运行时信息
AVAsset 管资源本身,AVPlayerItem 管播放过程
同一个 AVAsset ,可以创建不同的 AVPlayerItem ,它们指向的是同一首歌资源,但播放状态可以不同
objc
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
可以在创建 AVPlayer 后直接调用 play,但是对于网络媒体,播放器会进行KVO监听,根据资源状态决定立即开始还是等待缓冲
objc
[item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItem *item = (AVPlayerItem *)object;
if (item.status == AVPlayerItemStatusReadyToPlay) {
[self.player play];
} else if (item.status == AVPlayerItemStatusFailed) {
NSLog(@"加载失败: %@", item.error);
}
}
}
这里主要值得关注的除了status就是以下属性:
objc
// 已缓冲的时间范围,可用于绘制灰色缓冲进度条
item.loadedTimeRanges
// 缓冲是否耗尽(true 时应该显示 loading 转圈)
item.playbackBufferEmpty
// 是否已经缓冲到足以流畅播放
item.playbackLikelyToKeepUp
当然也可以获取到当前播放时长,总时长,已缓存的范围等
AVPlayer
AVPlayer 持有 AVPlayerItem,负责实际的播放控制,是真正的播放器
objc
AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
[player play];
[player pause];
player.volume = 0.8;
player.rate = 1.5; // 倍速播放
// 跳转进度
CMTime target = CMTimeMakeWithSeconds(30.0, NSEC_PER_SEC);
[player seekToTime:target];
// 切歌:替换当前播放内容
AVPlayerItem *newItem = [AVPlayerItem playerItemWithURL:newURL];
[player replaceCurrentItemWithPlayerItem:newItem];
这里主要就是调用一些函数管理你的音乐播放
AVPlayerLayer
纯音频播放完全用不到这一层,只有播放视频时才需要把画面渲染出来
objc
AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:player];
layer.frame = self.view.bounds;
layer.videoGravity = AVLayerVideoGravityResizeAspect;
[self.view.layer addSublayer:layer];
CMTime
这是苹果专门处理时间和媒体数据的底层框架
由于普通的 float/double 类型在大量加减运算后会产生累积误差,而音视频处理对时间精度要求很高,尤其是要做帧级对齐、音画同步的时候,一点点误差累积起来就会导致明显的不同步
CMTime 用分数的形式表示时间就会避免这个问题
objc
// CMTime 的本质是 value / timescale
CMTimeMake(90, 30); // 90/30 = 3.0 秒
CMTimeMake(1, 2); // 1/2 = 0.5 秒
// 如果已知秒数,用这个更直观
CMTimeMakeWithSeconds(30.5, NSEC_PER_SEC); // 30.5秒,纳秒级精度
两者都是用来保存某个数据的,区别在于创建方式不同:
- 知道帧数或采样数时用 CMTimeMake
- 知道具体秒数时用 CMTimeMakeWithSeconds
底层的精确计算全程用 CMTime 保证不丢精度
但是在最后一步展示给用户看的时候,或者赋值给 UISlider 这类只接受 float 的 UI 控件时,就要讲将 CMTimeGetSeconds 转换成普通的浮点数
objc
CGFloat seconds = CMTimeGetSeconds(player.currentTime);
self.timeLabel.text = [self formatTime:seconds];
TabBar附加视图

如图,UITabAccessory附加视图是iOS26新推出的系统控件,可以用于音乐播放展示
objc
UITabAccessory *accessory = [[UITabAccessory alloc] initWithContentView:yourMiniPlayerView];
self.tabBarController.bottomAccessory = accessory;
TabBar 滚动收起时,迷你条会跟着变化,但是空间变小时需要自己调整内容
因为收起后空间变少,所以要隐藏一部分播放控制按钮来适应
objc
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection