【iOS】音频与视频播放
前言
在iOS应用中集成音频和视频播放功能是常见的需求。iOS提供了强大的框架来支持多媒体内容的播放,主要是AVFoundation框架。
视频播放
iOS中想播放视频,那么就要用到AVFoundation库,在AVFoundation框架中,视频播放主要由数据层AVAsset、播放单元层AVPlayerItem和播放器层AVPlayer构成。
AVPlayer、AVPlayerLayer、AVPlayerItem
- AVPlayer:是播放媒体内容的核心对象,负责播放控制逻辑(播放、暂停、跳转、速率等)。
- AVPlayerLayer:是CALayer的子类,显示视频画面的图层,是AVPlayer的可视化层。
- AVPlayerItem:是连接媒体文件和播放器的中间层,起承上启下作用。

具体实现
具体实现有两种方式:
- 直接使用AVPlayer+AVPlayerLayer手动构建播放器UI。
- 使用系统自带完整播放器界面。
AVPlayer+AVPlayerLayer手动自定义播放器界面
- 初始化一个播放单元
这里我们的URL要明确视频的路径是本地还是网络。
-
网络URL:
objcNSURL *url = [NSURL URLWithString:@"/Users/mac/Desktop/技能五子棋.mp4"]; -
本地文件:
- 如果视频在本地某个位置:
objcNSString *path = @"/Users/mac/Desktop/技能五子棋.mp4"; NSURL *url = [NSURL fileURLWithPath:path];- 如果视频在项目Bundle里:
objcNSString *path = [[NSBundle mainBundle] pathForResource:@"技能五子棋" ofType:@"mp4"]; NSURL *url = [NSURL fileURLWithPath:path];
objc
self.item = [AVPlayerItem playerItemWithURL:url];
- 初始化一个对象播放器
objc
self.player = [AVPlayer playerWithPlayerItem:self.item];
- 初始化一个播放界面
objc
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.frame = CGRectMake(100, 200, screenWidth - 200, 500);
我们会发现无论如何修改self.playerLayer.frame,视频宽高比例永远不会变化,这是因为self.playerLayer.frame控制的是显示在屏幕上的矩形区域大小。
而self.playerLayer.videoGravity控制视频内容如何在外框中铺放。它有以下三种常用值:
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;:按比例缩放,不裁剪、不拉伸,默认
self.playerLayer.videoGravity = AVLayerVideoGravityResize;:不保持比例直接拉伸
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;:按比例缩放,充满视图,可能裁剪部分画面
- 开始播放视频
objc
[self.view.layer addSublayer:self.playerLayer];
[self.player play];
这里有一个问题,为什么不是直接把playerLayer作为一个控件直接加到self.view上?
我们会发现系统给出警告:类型不兼容,不能把CALayer当作UIView添加到视图层级中。

这是因为AVPlayerLayer是CALayer的子类,它本身不是UIView,就不能直接加到视图层级上。
在iOS绘图系统中,每一个UIView背后都有一个CALyer,称作宿主图层,所有视图的绘制其实都是它的layer在负责显示。UIView负责响应点击、滑动等事件和布局,CALayer负责显示颜色、图片、视频、动画等内容。形象地说,UIView是一块透明玻璃,view.layer是玻璃上能显示内容的那层薄膜,AVPlayerLayer是贴上去播放视频的胶片。
这样,就实现了视频的播放。

系统自带完整播放器界面
我们先了解一下AVPlayerViewController。
AVPlayerViewController属于AVKit,是苹果官方提供的系统控件。其作用是帮我们快速创建带系统自带UI的视频播放器,包括播放/暂停按钮、时间进度条、音量控制、全屏切换、AirPlay支持,我们自己是不用写这些控件的。
AVPlayerViewController是UIViewController的子类,这意味着它与普通ViewController一样,能被presentViewController弹出,它拥有UIViewController的所有生命周期。它可以自动处理旋转、全屏、播放结束通知。
值得注意的:AVPlayerViewController本身不负责解码视频,它只是显示和控制。解码和播放由AVPlayer完成。
objc
-(void)playVideoWithViewController {
NSLog(@"开始");
NSString *path = [[NSBundle mainBundle] pathForResource:@"技能五子棋" ofType:@"mp4"];
NSLog(@"%@", path);
NSURL *url = [NSURL fileURLWithPath:path];
//创建一个系统自带的视频播放控制器
//将AVPlayer交给它负责
AVPlayer *player = [AVPlayer playerWithURL:url];
AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
playerVC.player = player;
[self presentViewController:playerVC animated:YES completion:^{
[player play];
}];
}
然而视频却没有显示在屏幕上,这是什么原因?
这是因为根据先前了解的UIViewController生命周期:loadView → viewDidLoad → viewWillAppear → viewDidAppear,在viewDidload中调用playVideoWithViewController时,当前视图控制器还未执行到viewDidAppear,即当前视图控制器ViewController没有真正显示在屏幕上,这时候present的视图控制器也不会显示,因此我们将调用playVideoWithViewController写在viewDidAppear:中。
objc
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self playVideoWithViewController];
}

更多功能
- UISlider实时显示和调整进度
objc
//实时刷新slider和当前时间
-(void)updateSlider {
CGFloat time = self.item.currentTime.value / self.item.currentTime.timescale;
self.slider.value = time;
self.leftLb.text = [self formatTime:time];
}
//格式化时间
-(NSString*)formatTime:(NSInteger)time {
NSInteger min = time / 60;
NSInteger sec = time % 60;
return [NSString stringWithFormat:@"%02ld:%02ld", (long)min, (long)sec];
}
//滑动调节播放进度
-(void)silderValueChanged {
CGFloat seconds = self.slider.value;
self.leftLb.text = [self formatTime:(NSInteger)seconds];
CMTime startTime = CMTimeMakeWithSeconds(seconds, self.item.currentTime.timescale);
//让播放器跳转到startTime处,completionHandler在跳转完成后回调
[self.player seekToTime:startTime completionHandler:^(BOOL finished) {
if (self.isPlaying) {
[self.player play];
}
}];
}
CMTime:AVFoundation中表示时间的结构体。
objctypedef struct { CMTimeValue value; /*!< The value of the CMTime. value/timescale = seconds */ CMTimeScale timescale; /*!< The timescale of the CMTime. value/timescale = seconds. */ CMTimeFlags flags; /*!< The flags, eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity, etc. */ CMTimeEpoch epoch; /*!< Differentiates between equal timestamps that are actually different because of looping, multi-item sequencing, etc. Will be used during comparison: greater epochs happen after lesser ones. Additions/subtraction is only possible within a single epoch, however, since epoch length may be unknown/variable */ }简单来说,CMTime = value(时间的数值)/timescale(时间刻度)秒
- 实时显示播放时间
objc
-(void)pressBtn {
if (self.isPlaying) {
[self.player pause];
[self.btn setTitle:@"播放" forState:UIControlStateNormal];
//暂停定时器
[self.timer invalidate];
self.timer = nil;
} else {
[self.player play];
[self.btn setTitle:@"暂停" forState:UIControlStateNormal];
[self startTimer];
}
self.isPlaying = !self.isPlaying;
}
-(void)startTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateSlider) userInfo:nil repeats:YES];
//将定时器加到NSRunLoopCommonModes模式中,避免用户拖动silder或滚动界面时RunLoop切换模式,定时器暂停,界面进度条卡住
//NSDefaultRunLoopMode:普通状态,用户未滚动UI
//UITrackingRunLoopMode:用户正在滑动UIScollView或拖动UISlider
//NSRunLoopCommonModes:集中常用模式的集合模式
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- 获取视频总时长
这里使用了KVO传值的方式获取视频总时长。(不要忘了销毁监听!)
objc
[self.item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status = [change[NSKeyValueChangeNewKey] integerValue];
if (status == AVPlayerStatusReadyToPlay) {
NSLog(@"视频好了");
CGFloat duration = self.item.duration.value / self.item.duration.timescale;
self.slider.maximumValue = duration;
self.rightLb.text = [self formatTime:duration];
} else if (status == AVPlayerStatusFailed) {
NSLog(@"视频加载失败");
}
}
}
-(void)dealloc {
[self.item removeObserver:self forKeyPath:@"status"];
[self.timer invalidate];
}
展示一下实现功能的效果:

音频播放
iOS中播放音频主要有两种场景:
- 短音频:音效、提示音
- 长音频:音乐、博客
短音频播放
适用于播放时长较短(小于30秒) 、文件较小、不需要精确控制播放进度的音频文件。
具体步骤如下:
- 导入头文件
objc
#import <AudioToolbox/AudioToolbox.h>
- 获取音频文件URL
objc
NSString *path = [[NSBundle mainBundle] pathForResource:@"ding" ofType:@"wav"];
NSURL *soundURL = [NSURL fileURLWithPath:path];
- 创建系统声音ID
AudioServicesCreateSystemSoundID:根据URL创建一个系统声音对象,并返回一个可用ID。执行成功后,系统会将音频文件加载成可播放的系统声音。
objcextern OSStatus AudioServicesCreateSystemSoundID( CFURLRef inFileURL, SystemSoundID* outSystemSoundID) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) ;参数1:音频文件的路径(要先转化为CFURLRef)
参数2:一个输出参数的地址,填入创建成功后的SystemSoundID
SystemSoundID:是一个系统定义的整型标识符,用来表示某个音效,系统通过上面返回的ID管理声音的播放和释放等。
objc
//创建SystemSoundID
SystemSoundID soundID;//声明SystemSoundID类型变量
//__bridge CFURLRef:ARC环境下,只进行类型转换,不会改变对象引用计数,使用桥接关键字保证内存安全
AudioServicesCreateSystemSoundID((__bridge CFURLRef)soundURL, &soundID);//创建SystemSoundID
- 播放声音
objc
//播放声音
AudioServicesPlaySystemSound(soundID);
//播放完成后释放
//AudioServicesDisposeSystemSoundID(soundID);
通过一个按钮展示一下效果:

除此之外,Apple为开发者保留了一些可公开调用的系统音效ID,同样可以通过AudioServicesPlaySystemSound播放,常用的如下:

objc
AudioServicesPlaySystemSound(1007);
AudioServicesPlaySystemSound(1022);
这些ID在系统内部预定义,可直接使用。

长音效播放
适用于播放时长较长、需要精确控制播放进度、音量、循环、支持后台播放的音频文件。常用于播放背景音乐、录音回放等。
具体步骤如下:
- 导入头文件
objc
#import <AVFoundation/AVFoundation.h>
- 创建音频URL
objc
NSString *path = [[NSBundle mainBundle] pathForResource:@"很久很久" ofType:@"mp3"];
NSURL *url = [NSURL fileURLWithPath:path];
- 初始化AVAudioPlayer,并设置部分属性
objc
NSError *error = nil;
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
//默认播放一次:self.audioPlayer.numberOfLoops = 0;
//self.audioPlayer.numberOfLoops = 3;播放3次,加上初始1次,共4次
self.audioPlayer.numberOfLoops = -1;//表示无限循环
self.audioPlayer.volume = 0.5;//设置音量(0.0-1.0)
AVAudioPlayer是AVFoundation框架中专门用于播放本地音频文件的类。
而播放网络音频,则应该使用AVPlayer或更高级的AVQueuePlayer、AVPlayerItem。
objc
-(void)playOnlineMusic {
NSURL *url = [NSURL URLWithString:@"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
self.player = [AVPlayer playerWithPlayerItem:item];
//创建音频会话
AVAudioSession *session = [AVAudioSession sharedInstance];
//设置会话类别
//AVAudioSessionCategoryAmbient:可播放音频,与系统或其他App的声音混合
//AVAudioSessionCategorySoloAmbient:播放音频,但独占输出,打断其他App
//AVAudioSessionCategoryPlayback:播放音频,可后台播放,不被静音键影响
//AVAudioSessionCategoryRecord:录音专用
//AVAudioSessionCategoryPlayAndRecord:同时播放和录音,适用于语音通话聊天等
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
//激活音频会话,让设置立即生效
[session setActive:YES error:nil];
[self.player play];
}
- 准备并播放
objc
if ([self.audioPlayer prepareToPlay])
{
[self.audioPlayer play];
} else {
NSLog(@"播放失败");
}
- 更多功能
- 中途暂停、停止播放冲头开始
objc
-(void)pauseSound {
if (self.audioPlayer.isPlaying) {
[self.audioPlayer pause];
NSLog(@"暂停");
} else {
NSLog(@"当前未在播放状态");
}
}
-(void)stopSound {
if (self.audioPlayer.isPlaying) {
[self.audioPlayer stop];
self.audioPlayer.currentTime = 0;//停止并回到开头
NSLog(@"音频已停止");
} else {
NSLog(@"当前未在播放状态");
}
}
- 代理协议实现回调
objc
@interface ViewController ()<AVAudioPlayerDelegate>
self.audioPlayer.delegate = self;
objc
//播放完成回调
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
if (flag) {
NSLog(@"音频播放完成");
} else {
NSLog(@"音频播放中断");
}
}
//解码错误回调
-(void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {
NSLog(@"解码错误:%@", error.localizedDescription);
}

但是我们发现一个问题,暂停后继续播放时又会重新开始。我们解决这个问题:
objc
if (!self.audioPlayer) {
//初始化播放器
}
if (!self.audioPlayer.isPlaying) {
[self.audioPlayer play];
NSLog(@"继续播放");
} else {
NSLog(@"已经在播放中");
}
通过判断播放器是否已经创建,防止反复初始化,并且判断是否是暂停状态,是则继续播放。
AVAudioPlayer内部会自动维护播放进度,调用pause暂停后,再次调用play会从上次暂停出继续播放,只有调用stop并将currentTime设置为0,才会回到开头。
这样,我们就实现了长音频播放、暂停、停止功能:

总结
对音频和视频播放的学习将会对笔者后续写项目有很大的帮助。除此之外,对后台播放、中断处理等笔者学习后将补充完善该博客。


