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

具体实现
具体实现有两种方式:
- 直接使用AVPlayer+AVPlayerLayer手动构建播放器UI。
- 使用系统自带完整播放器界面。
AVPlayer+AVPlayerLayer手动自定义播放器界面
- 初始化一个播放单元
这里我们的URL要明确视频的路径是本地还是网络。
- 网络URL:
NSURL *url = [NSURL URLWithString:@"/Users/mac/Desktop/技能五子棋.mp4"]; - 本地文件:
- 如果视频在本地某个位置:
NSString *path = @"/Users/mac/Desktop/技能五子棋.mp4"; NSURL *url = [NSURL fileURLWithPath:path];- 如果视频在项目Bundle里:
NSString *path = [[NSBundle mainBundle] pathForResource:@"技能五子棋" ofType:@"mp4"];NSURL *url = [NSURL fileURLWithPath:path];
self.item = [AVPlayerItem playerItemWithURL:url];
- 初始化一个对象播放器
self.player = [AVPlayer playerWithPlayerItem:self.item];
- 初始化一个播放界面
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;:按比例缩放,充满视图,可能裁剪部分画面
- 开始播放视频
[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完成。
-(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:中。
-(void)viewDidAppear:(BOOL)animated {[super viewDidAppear:animated];[self playVideoWithViewController];
}

更多功能
- UISlider实时显示和调整进度
//实时刷新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中表示时间的结构体。
typedef 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 becauseof 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(时间刻度)秒
- 实时显示播放时间
-(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传值的方式获取视频总时长。(不要忘了销毁监听!)
[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秒) 、文件较小、不需要精确控制播放进度的音频文件。
具体步骤如下:
- 导入头文件
#import <AudioToolbox/AudioToolbox.h>
- 获取音频文件URL
NSString *path = [[NSBundle mainBundle] pathForResource:@"ding" ofType:@"wav"];
NSURL *soundURL = [NSURL fileURLWithPath:path];
- 创建系统声音ID
AudioServicesCreateSystemSoundID:根据URL创建一个系统声音对象,并返回一个可用ID。执行成功后,系统会将音频文件加载成可播放的系统声音。
extern 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管理声音的播放和释放等。
//创建SystemSoundID
SystemSoundID soundID;//声明SystemSoundID类型变量
//__bridge CFURLRef:ARC环境下,只进行类型转换,不会改变对象引用计数,使用桥接关键字保证内存安全
AudioServicesCreateSystemSoundID((__bridge CFURLRef)soundURL, &soundID);//创建SystemSoundID
- 播放声音
//播放声音
AudioServicesPlaySystemSound(soundID);
//播放完成后释放
//AudioServicesDisposeSystemSoundID(soundID);
通过一个按钮展示一下效果:

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

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

长音效播放
适用于播放时长较长、需要精确控制播放进度、音量、循环、支持后台播放的音频文件。常用于播放背景音乐、录音回放等。
具体步骤如下:
- 导入头文件
#import <AVFoundation/AVFoundation.h>
- 创建音频URL
NSString *path = [[NSBundle mainBundle] pathForResource:@"很久很久" ofType:@"mp3"];
NSURL *url = [NSURL fileURLWithPath:path];
- 初始化AVAudioPlayer,并设置部分属性
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。
-(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];
}
- 准备并播放
if ([self.audioPlayer prepareToPlay])
{[self.audioPlayer play];
} else {NSLog(@"播放失败");
}
- 更多功能
- 中途暂停、停止播放冲头开始
-(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(@"当前未在播放状态");}
}
- 代理协议实现回调
@interface ViewController ()<AVAudioPlayerDelegate>
self.audioPlayer.delegate = self;
//播放完成回调
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {if (flag) {NSLog(@"音频播放完成");} else {NSLog(@"音频播放中断");}
}//解码错误回调
-(void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {NSLog(@"解码错误:%@", error.localizedDescription);
}

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

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



