1. 简介


苹果目前提供两个框架用来处理音视频播放

1.AVFoundation

AVFoundation用于播放、处理音视频。可以通过结构图看到AVFoundation位于UIKit之下,很好理解AVFoundation并不提供用户界面,你可以自己自己构建用户界面来控制媒体的播放处理等功能。 但是苹果更推荐使用AVKit来构建用户界面

\2. AVKit

AVKit构建在 AVFoundation之上,可以简单的理解使用AVKit能够及其方便的使用系统为你提供的音视频播放界面。使用AVKit构建的播放界面能够随着苹果系统的更新自动更新。

如何选择

  • 如果你想让你的App拥有视频播放能力,并且不想自己创建控制界面,可以使用AVPlayerViewController(由AVKit提供)快速完成功能
  • 如果你想对视频进行编辑等处理,你需要使用 AVFoundation并且自己构建用户界面来处理音视频。

iOS9之前,你可以使用MPMoviePlayerViewController做一个简单的视频播放器,但是在iOS9这个类已经被弃用了。 取而代之的是AVPlayerViewController

2. 创建一个简单的视频播放App


1.新建工程, 命名为AVBasicPlayback,在Info.plist中添加

<key>NSAppTransportSecurity</key>
 <dict>
 <key>NSExceptionDomains</key>
 <dict>
 <key>devimages-cdn.apple.com</key>
 <dict>
 <key>NSExceptionRequiresForwardSecrecy</key>
 <false/>
 </dict>
 </dict>
 </dict>
目的是确保App能够从devimages-cdn.apple.com加载视频资源
2.在Appdelete中添加如下代码
importAVFoundation
 
 funcapplication(_application:UIApplication,didFinishLaunchingWithOptionslaunchOptions:[UIApplication.LaunchOptionsKey:Any]?)->Bool{
 // 1.初始化session
 letaudioSession=AVAudioSession.sharedInstance()
 do{
 //2.设置category确保app的音频能够正常播放
 tryaudioSession.setCategory(.playback)
 }catch {
 print("Setting category to AVAudioSessionCategoryPlayback failed.")
 }
 returntrue
 }
\3. 调整ViewController中的代码。
importUIKit
importAVKit
importAVFoundation

classViewController:UIViewController{
 
 letplayButton=UIButton(type:.system)

 overridefuncviewDidLoad(){
 super.viewDidLoad()
 setupUI()
 }
 
 @objcfuncplayVideo(){
 
 //1.这是一个HTTP Live Streaming流媒体链接,用于测试
 guardleturl=URL(string:"https://devimages-cdn.apple.com/samplecode/avfoundationMedia/AVFoundationQueuePlayer_HLS2/master.m3u8")else{
 return
 }
 //2. 创建AVPlayer
 letplayer=AVPlayer(url:url)
 
 //3. 创建AVPlayerViewController,并设置player
 letcontroller=AVPlayerViewController()
 controller.player=player
 
 //4. 显示
 present(controller,animated:true){
 player.play()
 }

 }
 
 funcsetupUI(){
 playButton.setTitle("Play Video",for:.normal)
 playButton.addTarget(self,action:#selector(playVideo),for:.touchUpInside)
 view.addSubview(playButton)
 playButton.translatesAutoresizingMaskIntoConstraints=false
 playButton.centerXAnchor.constraint(equalTo:view.centerXAnchor).isActive=true
 playButton.centerYAnchor.constraint(equalTo:view.centerYAnchor).isActive=true

 }
}

\4. 运行App,点击播放。 恭喜你,已经成功完成了一个简单的视频播放App。

如果你视频加载不出来,检查一下第一步是否完成。另外还可以在网上寻找一些HLS测试的URL或者本地资源对代码中URL的进行替换




ios播放器怎么用 苹果播放器如何使用_ios


3. 音频设置


上一步我们在AppDelegate中使用到了AVAudioSession类

AVAudioSession是App和操作系统的音频中介。我们使用这个类进行一些音频设置,无需进行复杂的配置, 系统就能为用户提供良好的音频体验

AVAudioSession 有一些默认的设置:

  1. 默认不允许录音
  2. 如果手机为静音模式,app播放的任何声音都会被静音
  3. 当手机锁屏的时候,app的音频会被静音
  4. 当你的App播放音频,其他背景音会被静音(比如你正在后台播放音乐)

如果想要调整默认设置,我们就需要配置AVAudioSession,在上一步中我们设置了.playback。

设置为.playback 能够使我们app具有后台音频播放的能力(需要配置),当你的App开始播放音频的时候系统会停止播放其他App的音频。想要了解更多可以仔细阅读下面这个链接

Introductiondeveloper.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40007875


ios播放器怎么用 苹果播放器如何使用_Powered by 金山文档_02


接下来我们设置允许后台音频播放


ios播放器怎么用 苹果播放器如何使用_swift_03


ios播放器怎么用 苹果播放器如何使用_ios_04


配置完成后App已经能够进行后台音频播放了。


4.深入了解AVFoundation


4.1AVAsset

我们使用AVFoundation主要目的就是对音视频进行处理, AVAsset就是这些媒体数据的抽象模型。 我们可以用本地的视频,例如一个mp4格式的视频, 或则通过一个HLS流媒体连接来构建一个AVAsset。 使用AVAsset能够使得我们不用关心视频的格式,编解码这些复杂的工作。可以简单的理解AVAsset提供给我们一套统一的接口用于处理不同格式的视频。

AVAsset中包含了多个AVAssetTrack。 AVAssetTrack是媒体流的抽象,例如音轨、视频轨道、字幕轨道。


ios播放器怎么用 苹果播放器如何使用_Powered by 金山文档_05


在大部分情况下,你只会对轨道的一部分进行处理,而不是处理整个轨道,所以AVAsset提供一些方法让你获取 AVAssetTrack的子集

4.2创建AVAsset

//URL可以是本地资源也可以是网络资源
leturl:URL=""
letasset=AVAsset(url:url)
实际上AVAsset是一个抽象类,你实际创建的是一个AVURLAsset实例。 你也可以通过AVURLAsset直接创建实例, 你还可以进行一些设置,比如我们不想在使用蜂窝网络的时候加载视频。
leturl:URL=// 网络URL资源
//不允许在蜂窝网络下加载
letoptions=[AVURLAssetAllowsCellularAccessKey:false]
letasset=AVURLAsset(url:url,options:options)
新建 AVAsset 的时候系统不会自动加载数据,直到需要对其进行操作(播放,导出等)。在新建AVAsset后,不要直接读取它的属性,这可能会造成阻塞。 比如你想知道一个视频是否可以播放,需要调用loadValuesAsynchronously异步加载playable
 funcloadPlayable(){
 //1. 加载本地视频
 leturl=Bundle.main.url(forResource:"sample-mp4-file",withExtension:"mp4")!
 letasset=AVAsset(url:url)
 letplayableKey="playable"
 
 //2 判断属性是否可用
 letstatus=asset.statusOfValue(forKey:playableKey,error:nil)
 ifstatus!=.loaded{
 print("playable unloaded")
 }
 //千万不要直接读取,这样可能会造成线程堵塞
 //asset.isPlayable
 
 //3. 加载"playable"属性
 asset.loadValuesAsynchronously(forKeys:[playableKey]){
 varerror:NSError?=nil
 letstatus=asset.statusOfValue(forKey:playableKey,error:&error)
 switchstatus{
 case.loaded:
 print("loaded, playable:\(asset.isPlayable)")
 case.failed:
 print("failed")
 case.cancelled:
 print("cancelled")
 default:break
 }
 }
 }

4.3处理元数据

AVAsset是媒体对象的抽象类 , AVMetadataItem是元数据的抽象类,提供了媒体文件关联的一些元数据,例如电影的标题或专辑的插图。你可以通过AVMetadataItem获取这些信息

AVFoundation把这些元数据分别关联到Format-specific key spaces 和Common key space.

funcloadMetaData(){
 //1. 加载本地视频
 leturl=Bundle.main.url(forResource:"sample-mp4-file",withExtension:"mp4")!
 letasset=AVAsset(url:url)
 letformatsKey="availableMetadataFormats"
 letcommonMetadataKey="commonMetadata"
 
 //2. 加载属性
 asset.loadValuesAsynchronously(forKeys:[formatsKey,commonMetadataKey]){
 varerror:NSError?=nil
 
 //3 获取Format-specific key spaces下的元数据
 letformatsStatus=asset.statusOfValue(forKey:formatsKey,error:&error)
 ifformatsStatus==.loaded{
 forformatinasset.availableMetadataFormats{
 letmetaData=asset.metadata(forFormat:format)
 print(metaData)
 }
 }
 
 //4 获取Common key space 下的元数据
 letcommonStatus=asset.statusOfValue(forKey:commonMetadataKey,error:&error)
 ifcommonStatus==.loaded{
 letmetadata=asset.commonMetadata
 print(metadata)
 
 //5 通过Identifier获取AVMetadataItem
 lettitleID:AVMetadataIdentifier=.commonIdentifierTitle
 lettitleItems=AVMetadataItem.metadataItems(from:metadata,filteredByIdentifier:titleID)
 ifletitem=titleItems.first{
 //6 处理title item
 print(item)
 }
 }
 }
 }

4.4视频播放

前面我们对AVAsset已经有了一个了解,它代表了一个媒体资源。如果你想播放视频,你还需要用到其他对象。


ios播放器怎么用 苹果播放器如何使用_Powered by 金山文档_06


\1. AVPlayer

AVPlayer 媒体播放的核心类,用它对媒体对象进行管理。

AVPlayer一次只能播放一个视频,你可以使用AVQueuePlayer (AVPlayer的子类)来创建播放队列,播放多个视频

2.AVPlayerItem

AVAsset只是对媒体对象的静态建模。当你播放它们的时候, 媒体对象会有播放时间等状态,所以你需要使用AVPlayerItem 对这些数据进行建模。 你可以使用AVPlayerItem控制播放时间等。

3.AVKit 和 AVPlayerLayer

AVPlayer和AVPlayerItem不负责视频的显示。

你可以使用AVKit下的AVPlayerController或者AVPlayerLayer来显示显示视频。 AVPlayerController 自带播放界面, AVPlayerLayer需要你自己创建控件控制视频的播放。

funcplayFlow() {
 // 1 获取URL
 leturl=Bundle.main.url(forResource:"sample-mp4-file",withExtension:"mp4")!

 // 2 创建AVAsset对象
 letasset=AVAsset(url:url)
 
 // 3 创建AVPlayerItem对象
 letplayerItem=AVPlayerItem(asset:asset)
 
 // 4 创建AVPlayer
 letplayer=AVPlayer(playerItem:playerItem)
 
 // 5 关联
 letcontroller=AVPlayerViewController()
 controller.player=player
 
 }
4.5监听播放状态

   AVPlayer 以及AVPlayerItem 的属性状态经常会发生变化。我们使用KVO来进行监听并进行业务处理 
 
其中AVPlayerItem 的status是一个非常重要的属性,我们可以通过这个属性判断视频是否可以进行播放
 leturl:URL=Bundle.main.url(forResource:"sample-mp4-file",withExtension:"mp4")!
 varasset:AVAsset!
 varplayer:AVPlayer!
 varplayerItem:AVPlayerItem!
 // Key-value observing context
 privatevarplayerItemContext=0
 // 需要加载的属性
 letrequiredAssetKeys=[
 "playable",
 "hasProtectedContent"
 ]
 
 funcprepareToPlay(){
 // 1.创建AVAsset
 asset=AVAsset(url:url)
 
 // 2.创建AVPlayerItem,并且在readyToPlay状态之前加载所有需要的属性
 playerItem=AVPlayerItem(asset:asset,
 automaticallyLoadedAssetKeys:requiredAssetKeys)
 
 // 3.KVO
 playerItem.addObserver(self,
 forKeyPath:#keyPath(AVPlayerItem.status),
 options:[.old,.new],
 context:&playerItemContext)
 
 // 4.创建AVPlayer
 player=AVPlayer(playerItem:playerItem)
 }
 
 overridefuncobserveValue(forKeyPathkeyPath:String?,
 ofobject:Any?,
 change:[NSKeyValueChangeKey:Any]?,
 context:UnsafeMutableRawPointer?){
 
 // 只对playerItemContext进行处理
 guardcontext==&playerItemContextelse{
 super.observeValue(forKeyPath:keyPath,
 of:object,
 change:change,
 context:context)
 return
 }
 
 ifkeyPath==#keyPath(AVPlayerItem.status){
 letstatus:AVPlayerItem.Status
 ifletstatusNumber=change?[.newKey]as?NSNumber{
 status=AVPlayerItem.Status(rawValue:statusNumber.intValue)!
 }else{
 status=.unknown
 }
 // Switch over status value
 switchstatus{
 case.readyToPlay:
 print("加载完成")
 case.failed:
 print("加载失败")
 case.unknown:
 print("未知状态")
 default:break
 }
 }
 }
4.5基于时间对视频进行操作
首先我们了解一下CMTime
publicstructCMTime{
 publicvarvalue:CMTimeValue
 publicvartimescale:CMTimeScale
 publicvarflags:CMTimeFlags
 publicvarepoch:CMTimeEpoch
}
其中比较重要的是value和timescale 。当我们构建一个CMTime的时候,我们需要知道视频的帧率。 计算CMTime的时候,我们只要把 value作为分子和timescale作为分母,就能计算出时长。

   value/timescale = seconds 
 
 // 60/60 = 1秒
 letoneSecond=CMTime(value:60,timescale:60)
 // 1/4 = 0.25秒
 letquarterSecond=CMTime(value:1,timescale:4)
 // 441000/44100 = 10秒
 lettenSeconds=CMTime(value:441000,timescale:44100)
 // 90/30 = 3秒
 letcursor=CMTime(value:90,timescale:30)
如果你想对视频的播放时间进监听,首先肯定想到的是KVO,但KVO不不适合用来对时间进行监听,因为时间是连续变化的(想象一下KVO一直回调)。
官方推荐了2种方式对时间进行监听
1. 周期监测

   比如你想根据视频播放的时间刷新界面上的时间显示 
 
 vartimeObserverToken:Any?
 
 funcaddPeriodicTimeObserver(){
 // 每半秒回调一次
 lettimeScale=CMTimeScale(NSEC_PER_SEC)
 lettime=CMTime(seconds:0.5,preferredTimescale:timeScale)
 
 timeObserverToken=player.addPeriodicTimeObserver(forInterval:time,
 queue:.main){
 [weakself]timein
 guardletself=selfelse{return}
 //在这里进行你的业务逻辑
 print("\(self.player.currentTime())")
 }
 }
 
 funcremovePeriodicTimeObserver(){
 iflettimeObserverToken=timeObserverToken{
 player.removeTimeObserver(timeObserverToken)
 self.timeObserverToken=nil
 }
 }
\2. 边界监测

   比如你想在某个时间节点对视频进行处理 
 
 funcaddBoundaryTimeObserver(){
 // 视频每播放1/4我们进行一次回调
 letinterval=CMTimeMultiplyByFloat64(asset.duration,multiplier:0.25)
 varcurrentTime=CMTime.zero
 vartimes=[NSValue]()
 
 // 添加时间节点
 whilecurrentTime<asset.duration{
 currentTime=currentTime+interval
 times.append(NSValue(time:currentTime))
 }
 
 timeObserverToken=player.addBoundaryTimeObserver(forTimes:times,
 queue:.main){
 //在这里进行你的业务逻辑
 print(self.player.currentTime())
 }
 }
 
 funcremoveBoundaryTimeObserver(){
 iflettimeObserverToken=timeObserverToken{
 player.removeTimeObserver(timeObserverToken)
 self.timeObserverToken=nil
 }
 }
调整视频播放时间

   如果你想快速调整视频到某个时间点 
 
 funcseekToTime(){
 // 2分钟
 lettime=CMTime(value:120,timescale:1)
 player.seek(to:time)
 }
如果你想非常精确的调整视频到某个时间节点,使用seekToTime:toleranceBefore:toleranceAfter:方法。
 funcseekToTimeAccuracy(){
 // 10秒的第一帧。 这里不用觉得CMTime计算的时间不对,视频的帧率由视频本身决定,preferredTimescale设置一个极大的值就可以了
 letseekTime=CMTime(seconds:10,preferredTimescale:Int32(NSEC_PER_SEC))
 // 设置tolerance 为CMTime.zero 不允许有误差
 player.seek(to:seekTime,toleranceBefore:CMTime.zero,toleranceAfter:CMTime.zero)
 }