【iOS】音频与视频播放

【iOS】音频与视频播放

前言

在iOS应用中集成音频和视频播放功能是常见的需求。iOS提供了强大的框架来支持多媒体内容的播放,主要是AVFoundation框架。

视频播放

iOS中想播放视频,那么就要用到AVFoundation库,在AVFoundation框架中,视频播放主要由数据层AVAsset、播放单元层AVPlayerItem和播放器层AVPlayer构成。

AVPlayer、AVPlayerLayer、AVPlayerItem

  • AVPlayer:是播放媒体内容的核心对象,负责播放控制逻辑(播放、暂停、跳转、速率等)。
  • AVPlayerLayer:是CALayer的子类,显示视频画面的图层,是AVPlayer的可视化层。
  • AVPlayerItem:是连接媒体文件和播放器的中间层,起承上启下作用。

具体实现

具体实现有两种方式:

  • 直接使用AVPlayer+AVPlayerLayer手动构建播放器UI。
  • 使用系统自带完整播放器界面。

AVPlayer+AVPlayerLayer手动自定义播放器界面

  1. 初始化一个播放单元

这里我们的URL要明确视频的路径是本地还是网络。

  • 网络URL:

    objc 复制代码
    NSURL *url = [NSURL URLWithString:@"/Users/mac/Desktop/技能五子棋.mp4"];
  • 本地文件:

    • 如果视频在本地某个位置:
    objc 复制代码
    NSString *path = @"/Users/mac/Desktop/技能五子棋.mp4";
    NSURL *url = [NSURL fileURLWithPath:path];
    • 如果视频在项目Bundle里:
    objc 复制代码
    NSString *path = [[NSBundle mainBundle] pathForResource:@"技能五子棋" ofType:@"mp4"];
        NSURL *url = [NSURL fileURLWithPath:path];
objc 复制代码
self.item = [AVPlayerItem playerItemWithURL:url];
  1. 初始化一个对象播放器
objc 复制代码
self.player = [AVPlayer playerWithPlayerItem:self.item];
  1. 初始化一个播放界面
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;:按比例缩放,充满视图,可能裁剪部分画面
  1. 开始播放视频
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];
}

更多功能

  1. 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中表示时间的结构体。

objc 复制代码
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 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(时间刻度)秒

  1. 实时显示播放时间
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];
}
  1. 获取视频总时长

这里使用了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秒) 、文件较小、不需要精确控制播放进度的音频文件。

具体步骤如下:

  1. 导入头文件
objc 复制代码
#import <AudioToolbox/AudioToolbox.h>
  1. 获取音频文件URL
objc 复制代码
NSString *path = [[NSBundle mainBundle] pathForResource:@"ding" ofType:@"wav"];
NSURL *soundURL = [NSURL fileURLWithPath:path];
  1. 创建系统声音ID

AudioServicesCreateSystemSoundID:根据URL创建一个系统声音对象,并返回一个可用ID。执行成功后,系统会将音频文件加载成可播放的系统声音。

objc 复制代码
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管理声音的播放和释放等。

objc 复制代码
//创建SystemSoundID
SystemSoundID soundID;//声明SystemSoundID类型变量
//__bridge CFURLRef:ARC环境下,只进行类型转换,不会改变对象引用计数,使用桥接关键字保证内存安全
AudioServicesCreateSystemSoundID((__bridge CFURLRef)soundURL, &soundID);//创建SystemSoundID
  1. 播放声音
objc 复制代码
//播放声音
AudioServicesPlaySystemSound(soundID);
//播放完成后释放
//AudioServicesDisposeSystemSoundID(soundID);

通过一个按钮展示一下效果:

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

objc 复制代码
AudioServicesPlaySystemSound(1007);
AudioServicesPlaySystemSound(1022);

这些ID在系统内部预定义,可直接使用。

长音效播放

适用于播放时长较长、需要精确控制播放进度、音量、循环、支持后台播放的音频文件。常用于播放背景音乐、录音回放等。

具体步骤如下:

  1. 导入头文件
objc 复制代码
#import <AVFoundation/AVFoundation.h>
  1. 创建音频URL
objc 复制代码
NSString *path = [[NSBundle mainBundle] pathForResource:@"很久很久" ofType:@"mp3"];
NSURL *url = [NSURL fileURLWithPath:path];
  1. 初始化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];
}
  1. 准备并播放
objc 复制代码
if ([self.audioPlayer prepareToPlay])
{
  [self.audioPlayer play];
} else {
  NSLog(@"播放失败");
}
  1. 更多功能
  • 中途暂停、停止播放冲头开始
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,才会回到开头。

这样,我们就实现了长音频播放、暂停、停止功能:

总结

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

相关推荐
小小测试开发8 小时前
Playwright进阶:录制视频与追踪功能,让自动化过程“看得见、可分析”
自动化·音视频
开开心心就好10 小时前
微软官方出品:免费数据恢复工具推荐
网络·笔记·microsoft·pdf·word·音视频·symfony
大熊猫侯佩10 小时前
黑衣人档案:用 Apple Foundation Models + SwiftUI 打造 AI 聊天机器人全攻略
ios·swiftui·ai编程
大熊猫侯佩10 小时前
侠客行・iOS 26 Liquid Glass TabBar 破阵记
ios·swiftui·swift
懷淰メ11 小时前
python3GUI--短视频社交软件 By:Django+PyQt5(前后端分离项目)
后端·python·django·音视频·pyqt·抖音·前后端
小马过河R11 小时前
AIGC首帧图尾帧图生成视频案例教程
aigc·音视频·ai视频
causaliy11 小时前
实践六:防盗链知识点——视频
爬虫·音视频
戴草帽的大z11 小时前
使用V4L2工具验证RK3588平台视频设备节点数据有效性
ffmpeg·音视频·rk3588·nv12·v4l2-ctl
2501_9160074712 小时前
手机使用过的痕迹能查到吗?完整查询指南与步骤
android·ios·智能手机·小程序·uni-app·iphone·webview