【OC】AVPlayer

AVPlayer

文章目录

AVFoundation 是苹果提供的音视频处理框架,是 iOS/macOS 开发中处理媒体的核心框架

其中主要包括

功能 相关类
播放音频/视频 AVPlayerAVAudioPlayer
录音 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