82、AV Foundation 视频处理全解析

AV Foundation 视频处理全解析

1. AVPlayerViewController 基础属性

AVPlayerViewController 有几个重要属性,这些属性影响着视频播放的行为和信息展示:
| 属性 | 描述 |
| — | — |
| updatesNowPlayingInfoCenter | 若为 true(默认值),AVPlayerViewController 会将视频的时长和当前播放位置信息告知 MPNowPlayingInfoCenter;若为 false,则需开发者自行管理 MPNowPlayingInfoCenter。 |
| entersFullScreenWhenPlaybackBegins | 若为 true,子 AVPlayerViewController 的视图会在播放开始时自动切换到全屏模式。 |
| exitsFullScreenWhenPlaybackEnds | 若为 true,子 AVPlayerViewController 的视图会在播放结束时自动退出全屏模式。 |

2. 画中画模式

支持 iPad 多任务处理的 iPad 设备也支持画中画视频播放(除非用户在设置中关闭)。用户可以将视频移动到一个小的系统窗口中,该窗口会悬浮在屏幕上的其他内容之上,即使应用进入后台,该窗口依然存在。

要让 iPad 应用支持画中画,需满足以下条件:
1. 应用支持后台音频,在目标编辑器的功能选项卡中勾选相应复选框。
2. 音频会话策略必须处于活动状态且为播放模式。

若不想强制应用支持画中画,可将 AVPlayerViewController 的 allowsPictureInPicturePlayback 属性设置为 false。

当支持画中画时,播放控制栏上方会出现一个额外的按钮。用户点击该按钮后,视频会移动到系统窗口中,AVPlayerViewController 的视图会显示占位符。用户可以离开应用,同时继续观看和收听视频。若视频在全屏播放时用户离开应用,视频会自动进入画中画系统窗口。

用户可以将系统窗口移动到任意角落。通过点击系统窗口,可显示或隐藏窗口中的按钮,这些按钮可用于播放、暂停视频或关闭窗口。还有一个按钮可关闭窗口并返回应用,若在视频播放时点击该按钮,视频会继续在应用中播放。

若 AVPlayerViewController 在视频进入画中画模式时处于全屏展示状态,默认情况下,展示的视图控制器会被关闭。若用户尝试从系统画中画窗口返回应用,视频将无处可归。为处理这种情况,可给 AVPlayerViewController 设置一个代理(AVPlayerViewControllerDelegate),并在代理方法中处理:
- 不关闭展示的视图控制器 :实现 playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart( :) 方法并返回 false,这样展示的视图控制器会保留,视频有地方可以恢复。
- 重新创建展示的视图控制器 :实现 playerViewController(
:restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:) 方法,按方法名的提示恢复用户界面。示例代码如下:

func playerViewController(_ pvc: AVPlayerViewController,
    restoreUserInterfaceForPictureInPictureStopWithCompletionHandler
    ch: @escaping (Bool) -> ()) {
        self.present(pvc, animated:true) {
            ch(true)
        }
}
3. AV Foundation 核心类

AV Foundation 框架为 AVPlayerViewController 的视频显示提供支持。该框架包含众多类,以下是一些主要的类:
- AVPlayer :AV Foundation 视频播放的核心,它不是 UIView,而是视频传输的中心。实际视频若要显示,会出现在与 AVPlayer 关联的 AVPlayerLayer 中。例如,若想通过代码启动视频播放,可与 AVPlayerViewController 的 player(即 AVPlayer)交互,调用 play 方法或设置 rate 为 1。
- AVPlayerItem :AVPlayer 的视频由其 currentItem(即 AVPlayerItem)表示。虽然可以直接通过 URL 初始化 AVPlayer,但这只是快捷方式,AVPlayer 的真正初始化方法是 init(playerItem:),它接受一个 AVPlayerItem。同样,AVPlayerItem 也可以通过 URL 初始化,但真正的初始化方法是 init(asset:),它接受一个 AVAsset。
- AVAsset :实际的视频资源,有两个子类:
- AVURLAsset :通过 URL 指定的资源。
- AVComposition :通过代码编辑视频构建的资源。

以下是使用完整对象栈配置 AVPlayer 的示例代码:

let url = Bundle.main.url(forResource:"ElMirage", withExtension:"mp4")!
let asset = AVURLAsset(url:url)
let item = AVPlayerItem(asset:asset)
let player = AVPlayer(playerItem:item)
4. 视频处理的时间特性

处理视频是耗时的操作,给 AVPlayer 下达命令或设置属性并不意味着它会立即执行。从读取视频文件、获取元数据到转码和保存视频文件等各种操作都需要大量时间。为避免用户界面冻结,AV Foundation 大量依赖线程处理,开发者的代码也需要配合,经常使用键值观察和回调来在正确的时刻运行代码。

例如,创建嵌入式 AVPlayerViewController 时会存在两个问题:
- AVPlayerViewController 的视图最初在界面中显示为空,因为视频尚未准备好显示,视频准备好后会出现可见的闪烁。
- AVPlayerViewController 视图的建议框架与视频的实际纵横比不匹配,导致视频在该框架中出现黑边。

解决这些问题需要考虑视频准备好显示的时间和其纵横比。

4.1 键值观察属性

为防止闪烁,可先将 AVPlayerViewController 的视图隐藏,直到 isReadyForDisplay 属性为 true 时再显示。使用 KVO 注册为该属性的观察者,当属性变为 true 时会收到通知,此时取消 KVO 注册并显示视图。示例代码如下:

av.view.isHidden = true
var ob : NSKeyValueObservation!
ob = av.observe(\.isReadyForDisplay, options: .new) { vc, ch in
    guard let ok = ch.newValue, ok else {return}
    self.obs.remove(ob)
    DispatchQueue.main.async {
        vc.view.isHidden = false
    }
}
self.obs.insert(ob) // obs is a Set<NSKeyValueObservation>
4.2 异步属性加载

设置 AVPlayerViewController 的视图框架以匹配视频的纵横比时,需要获取视频轨道的 naturalSize 属性。但不能立即访问这些属性,因为 AV Foundation 对象的很多属性在未明确请求评估时没有值,请求评估需要时间。

AV Foundation 中符合 AVAsynchronousKeyValueLoading 协议的对象,可提前调用 loadValuesAsynchronously(forKeys:completionHandler:) 方法加载感兴趣的属性。当完成函数被调用时,检查键的状态,若状态为 .loaded,则可以访问该属性。

以下是获取视频纵横比的示例代码:

let url = Bundle.main.url(forResource:"ElMirage", withExtension:"mp4")!
let asset = AVURLAsset(url:url)
let track = #keyPath(AVURLAsset.tracks)
asset.loadValuesAsynchronously(forKeys:[track]) {
    let status = asset.statusOfValue(forKey:track, error: nil)
    if status == .loaded {
        DispatchQueue.main.async {
            self.getVideoTrack(asset)
        }
    }
}

func getVideoTrack(_ asset:AVAsset) {
    let visual = AVMediaCharacteristic.visual
    let vtrack = asset.tracks(withMediaCharacteristic: visual)[0]
    let size = #keyPath(AVAssetTrack.naturalSize)
    vtrack.loadValuesAsynchronously(forKeys: [size]) {
        let status = vtrack.statusOfValue(forKey: size, error: nil)
        if status == .loaded {
            DispatchQueue.main.async {
                self.getNaturalSize(vtrack, asset)
            }
        }
    }
}

func getNaturalSize(_ vtrack:AVAssetTrack, _ asset:AVAsset) {
    let sz = vtrack.naturalSize
    let item = AVPlayerItem(asset:asset)
    let player = AVPlayer(playerItem:item)
    let av = AVPlayerViewController()
    av.view.frame = AVMakeRect(
        aspectRatio: sz, insideRect: CGRect(10,10,300,200))
    av.player = player
    // ... and the rest is as before ...
}
4.3 远程资源处理

AVURLAsset 的 URL 可以指向互联网上的资源,但管理此类资源较为棘手,因为资源需要通过网络传输,可能会遇到网络缓慢、中断等问题。若缓冲区数据不足,播放会卡顿或停止。

在 iOS 10 之前,管理此类资源较为复杂,需要使用 AVPlayer 的 AVPlayerItem 跟踪资源的到达和播放情况,关注 playbackLikelyToKeepUp 和 accessLog 等属性,以及 AVPlayerItemPlaybackStalled 等通知,通过暂停和恢复播放来优化用户体验。

从 iOS 10 开始,处理变得简单,只需告诉 AVPlayer 播放即可。播放会在缓冲区填满到可以流畅播放整个视频时开始,若播放卡顿会自动恢复。可通过检查 AVPlayer 的 timeControlStatus 了解播放状态,若为 .waitingToPlayAtSpecifiedRate,可检查 reasonForWaitingToPlay。若要了解实际当前播放速率,可使用 CMTimebaseGetRate 结合 AVPlayerItem 的 timebase。

5. 时间测量方式

AV Foundation 中时间的测量方式不常见,使用普通的内置数值类(如 CGFloat)进行计算会有轻微的舍入误差,在处理大量媒体时这些误差会变得重要。因此,Core Media 框架提供了 CMTime 类,它本质上是一对整数,分别为 value 和 timescale,相当于一个有理数的分子和分母。

  • init(value:timescale:) :提供分子和分母,分母表示粒度,常见值为 600,足以指定常见视频格式中的单个帧。
  • init(seconds:preferredTimescale:) :提供时间的秒数和分母,例如 CMTime(seconds:2.5, preferredTimescale:600) 会得到 CMTime (1500,600)。
6. 媒体构建

AV Foundation 允许通过代码构建自己的媒体资源,作为 AVComposition(AVAsset 的子类),使用其子类 AVMutableComposition。

6.1 剪辑和拼接

以下示例从一个 AVAsset(asset1,视频文件)中提取前 5 秒和后 5 秒的视频,并将它们组合成一个 AVMutableComposition(comp):

let type = AVMediaType.video
let arr = asset1.tracks(withMediaType: type)
let track = arr.last!
let duration : CMTime = track.timeRange.duration
let comp = AVMutableComposition()
let comptrack = comp.addMutableTrack(withMediaType: type,
    preferredTrackID: Int32(kCMPersistentTrackID_Invalid))!
try! comptrack.insertTimeRange(CMTimeRange(
    start: CMTime(seconds:0, preferredTimescale:600),
    duration: CMTime(seconds:5, preferredTimescale:600)),
    of:track, at:CMTime(seconds:0, preferredTimescale:600))
try! comptrack.insertTimeRange(CMTimeRange(
    start: duration - CMTime(seconds:5, preferredTimescale:600),
    duration: CMTime(seconds:5, preferredTimescale:600)),
    of:track, at:CMTime(seconds:5, preferredTimescale:600))

但上述代码忽略了对应的音频轨道,下面将音频轨道添加到 AVMutableComposition 中:

let type2 = AVMediaType.audio
let arr2 = asset1.tracks(withMediaType: type2)
let track2 = arr2.last!
let comptrack2 = comp.addMutableTrack(withMediaType: type2,
    preferredTrackID:Int32(kCMPersistentTrackID_Invalid))!
try! comptrack2.insertTimeRange(CMTimeRange(
    start: CMTime(seconds:0, preferredTimescale:600),
    duration: CMTime(seconds:5, preferredTimescale:600)),
    of:track2, at:CMTime(seconds:0, preferredTimescale:600))
try! comptrack2.insertTimeRange(CMTimeRange(
    start: duration - CMTime(seconds:5, preferredTimescale:600),
    duration: CMTime(seconds:5, preferredTimescale:600)),
    of:track2, at:CMTime(seconds:5, preferredTimescale:600))

若要在 AVPlayerViewController 中显示编辑后的视频,可将其 player 的 playerItem 替换为新的 playerItem:

let item = AVPlayerItem(asset:comp)
let p = vc.player! // vc is an AVPlayerViewController
p.replaceCurrentItem(with: item)
6.2 添加轨道

可以使用相同的技术将另一个资产的音频轨道叠加到现有视频上,例如添加一段来自音频文件的额外旁白:

let type3 = AVMediaType.audio
let s = Bundle.main.url(forResource:"aboutTiagol", withExtension:"m4a")!
let asset2 = AVURLAsset(url:s)
let arr3 = asset2.tracks(withMediaType: type3)
let track3 = arr3.last!
let comptrack3 = comp.addMutableTrack(withMediaType: type3,
    preferredTrackID:Int32(kCMPersistentTrackID_Invalid))!
try! comptrack3.insertTimeRange(CMTimeRange(
    start: CMTime(seconds:0, preferredTimescale:600),
    duration: CMTime(seconds:10, preferredTimescale:600)),
    of:track3, at:CMTime(seconds:0, preferredTimescale:600))
6.3 过渡效果

可以对单个轨道的播放应用音频音量变化、视频不透明度和变换变化。以下示例对旁白轨道(comptrack3)的后半部分应用淡出效果:

let params = AVMutableAudioMixInputParameters(track:comptrack3)
params.setVolume(1, at:CMTime(seconds:0, preferredTimescale:600))
params.setVolumeRamp(fromStartVolume: 1, toEndVolume:0,
    timeRange:CMTimeRange(
        start: CMTime(seconds:5, preferredTimescale:600),
        duration: CMTime(seconds:5, preferredTimescale:600)))
let mix = AVMutableAudioMix()
mix.inputParameters = [params]

let item = AVPlayerItem(asset:comp)
item.audioMix = mix

类似地,可以使用 AVVideoComposition 来指定视频轨道的合成方式。

6.4 滤镜应用

可以为视频添加 CIFilter。以下示例对整个编辑后的视频(comp)应用棕褐色滤镜:

let vidcomp = AVVideoComposition(asset: comp) { req in
    // req is an AVAsynchronousCIImageFilteringRequest
    let f = "CISepiaTone"
    let im = req.sourceImage.applyingFilter(
        f, parameters: ["inputIntensity":0.95])
    req.finish(with: im, context: nil)
}

let item = AVPlayerItem(asset:comp)
item.videoComposition = vidcomp
6.5 动画与视频同步

AV Foundation 的 AVSynchronizedLayer 是 CALayer 的子类,它可以将视频时间(电影进度中的 CMTime)和 Core Animation 时间(动画进度中的时间)联系起来,从而可以将界面中的动画与电影播放同步。

以下示例展示如何将一个小黑色方块的水平位置与电影播放头位置同步:

let vc = self.children[0] as! AVPlayerViewController
let p = vc.player!
// create synch layer, put it in the interface
let item = p.currentItem!
let syncLayer = AVSynchronizedLayer(playerItem:item)
syncLayer.frame = CGRect(10,220,300,10)
syncLayer.backgroundColor = UIColor.lightGray.cgColor
self.view.layer.addSublayer(syncLayer)
// give synch layer a sublayer
let subLayer = CALayer()
subLayer.backgroundColor = UIColor.black.cgColor
subLayer.frame = CGRect(0,0,10,10)
syncLayer.addSublayer(subLayer)
// animate the sublayer
let anim = CABasicAnimation(keyPath:#keyPath(CALayer.position))
anim.fromValue = subLayer.position
anim.toValue = CGPoint(295,5)
anim.isRemovedOnCompletion = false
anim.beginTime = AVCoreAnimationBeginTimeAtZero // important trick
anim.duration = item.asset.duration.seconds
subLayer.add(anim, forKey:nil)
6.6 向视频添加图层

可以将 CALayer 渲染到视频中,将视频本身视为一个图层,与其他图层组合成新的视频。

以下示例展示如何将一个 AVMutableComposition(comp)导出到文件并加载到 AVPlayerViewController 中:

let pre = AVAssetExportPresetHighestQuality
guard let exporter = AVAssetExportSession(asset:comp, presetName:pre) else {
    print("oops")
    return
}
// create a URL to export to
let fm = FileManager.default
var url = fm.temporaryDirectory
let uuid = UUID().uuidString
url.appendPathComponent(uuid + ".mov")
exporter.outputURL = url
exporter.outputFileType = AVFileType.mov
// warning: this can take a long time!
exporter.exportAsynchronously() {
    DispatchQueue.main.async {
        let item = AVPlayerItem(url: url)
        let p = vc.player! // vc is an AVPlayerViewController
        p.replaceCurrentItem(with:item)
    }
}

若要对视频进行更改,例如将视频作为图层与其他图层组合,需要为导出器附加一个 AVVideoComposition:

exporter.videoComposition = vidcomp

let vidtrack = comp.tracks(withMediaType: .video)[0]
let sz = vidtrack.naturalSize
let parent = CALayer()
parent.frame = CGRect(origin: .zero, size: sz)
let child = CALayer()
child.frame = parent.bounds
parent.addSublayer(child)

let tool = AVVideoCompositionCoreAnimationTool(
    postProcessingAsVideoLayer: child, in: parent)
let vidcomp = AVMutableVideoComposition()
vidcomp.animationTool = tool
vidcomp.renderSize = sz
vidcomp.frameDuration = CMTime(value: 1, timescale: 30)
let inst = AVMutableVideoCompositionInstruction()
let dur = comp.duration
inst.timeRange = CMTimeRange(start: .zero, duration: dur)
let layinst = AVMutableVideoCompositionLayerInstruction(assetTrack: vidtrack)
inst.layerInstructions = [layinst]
vidcomp.instructions = [inst]

综上所述,AV Foundation 提供了丰富的功能和类,可用于处理视频播放、编辑、合成等各种任务。通过合理使用这些功能和类,开发者可以创建出功能强大、体验良好的视频应用。

AV Foundation 视频处理全解析

7. 画中画模式的状态管理

在使用画中画模式时,除了前面提到的处理视图控制器的问题外,还需要关注画中画模式的不同阶段。当进入画中画模式时,应用实际上处于后台状态,此时应减少资源使用和活动,只专注于视频播放,直到画中画模式结束。可以通过实现 AVPlayerViewControllerDelegate 的其他方法来了解画中画模式的开始和结束阶段。

以下是一个简单的状态管理流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;

    A([开始画中画模式]):::startend --> B(AVPlayerViewControllerDelegate 方法被调用):::process
    B --> C(减少资源和活动):::process
    C --> D(专注于视频播放):::process
    D --> E([结束画中画模式]):::startend
    E --> F(恢复资源和活动):::process
    F --> G(继续正常应用操作):::process
8. 视频处理中的错误处理

在视频处理过程中,可能会遇到各种错误,如资源加载失败、导出失败等。因此,需要在代码中进行适当的错误处理,以提高应用的健壮性。

8.1 资源加载错误处理

在异步加载资源时,需要检查状态并处理可能的错误。例如,在加载 AVAsset 的 tracks 属性时:

let url = Bundle.main.url(forResource:"ElMirage", withExtension:"mp4")!
let asset = AVURLAsset(url:url)
let track = #keyPath(AVURLAsset.tracks)
asset.loadValuesAsynchronously(forKeys:[track]) {
    let status = asset.statusOfValue(forKey:track, error: &error)
    if status == .failed {
        if let error = error {
            print("加载 tracks 属性失败: \(error.localizedDescription)")
        }
    } else if status == .loaded {
        DispatchQueue.main.async {
            self.getVideoTrack(asset)
        }
    }
}
8.2 导出错误处理

在导出视频时,也需要检查导出状态并处理错误:

let pre = AVAssetExportPresetHighestQuality
guard let exporter = AVAssetExportSession(asset:comp, presetName:pre) else {
    print("无法创建导出会话")
    return
}
// create a URL to export to
let fm = FileManager.default
var url = fm.temporaryDirectory
let uuid = UUID().uuidString
url.appendPathComponent(uuid + ".mov")
exporter.outputURL = url
exporter.outputFileType = AVFileType.mov
exporter.exportAsynchronously() {
    DispatchQueue.main.async {
        switch exporter.status {
        case .completed:
            let item = AVPlayerItem(url: url)
            let p = vc.player! // vc is an AVPlayerViewController
            p.replaceCurrentItem(with: item)
        case .failed:
            if let error = exporter.error {
                print("导出视频失败: \(error.localizedDescription)")
            }
        case .cancelled:
            print("导出被取消")
        default:
            break
        }
    }
}
9. 视频处理的性能优化

为了提高视频处理的性能,可采取以下措施:

9.1 合理选择导出预设

在导出视频时,选择合适的导出预设可以平衡视频质量和导出时间。例如,若对视频质量要求不高,可以选择较低质量的预设:

let pre = AVAssetExportPresetMediumQuality
guard let exporter = AVAssetExportSession(asset:comp, presetName:pre) else {
    print("oops")
    return
}
9.2 减少不必要的处理

在进行视频编辑和合成时,避免进行不必要的操作,如重复添加滤镜或过渡效果。只在必要时进行处理,以减少处理时间和资源消耗。

9.3 异步处理

尽量使用异步方法进行资源加载和导出操作,避免阻塞主线程,保证用户界面的流畅性。例如,在加载资源和导出视频时都使用了异步方法。

10. 视频处理的兼容性考虑

不同的设备和 iOS 版本可能对视频处理有不同的支持。在开发视频应用时,需要考虑以下兼容性问题:

10.1 设备支持

某些功能可能只在特定的设备上支持,如画中画模式只在支持 iPad 多任务处理的 iPad 设备上可用。在使用这些功能时,需要进行设备检查:

if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.isMultitaskingSupported {
    // 支持画中画模式
    let av = AVPlayerViewController()
    av.allowsPictureInPicturePlayback = true
}
10.2 iOS 版本支持

某些功能可能只在特定的 iOS 版本中可用,如 iOS 10 及以上版本对远程资源的处理更加简单。在使用这些功能时,需要进行版本检查:

if #available(iOS 10.0, *) {
    // 使用 iOS 10 及以上版本的功能
    let player = AVPlayer()
    player.play()
} else {
    // 处理旧版本的情况
}
11. 总结

AV Foundation 为视频处理提供了丰富的功能和类,涵盖了视频播放、编辑、合成、同步等多个方面。通过合理使用这些功能和类,开发者可以创建出功能强大、体验良好的视频应用。

在实际开发中,需要注意以下几点:
- 了解 AVPlayerViewController 的属性和画中画模式的使用方法,处理好视图控制器的管理和状态变化。
- 掌握 AV Foundation 核心类的使用,如 AVPlayer、AVPlayerItem 和 AVAsset,以及它们之间的关系。
- 处理好视频处理中的时间特性,使用键值观察和异步加载方法,避免界面冻结。
- 进行适当的错误处理和性能优化,提高应用的健壮性和流畅性。
- 考虑设备和 iOS 版本的兼容性,确保应用在不同环境下都能正常工作。

通过遵循这些原则和方法,开发者可以更好地利用 AV Foundation 进行视频处理开发,为用户带来优质的视频体验。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值