在iOS平台上,AVFoundation.framework
共同完成了一系列的任务,包括捕捉、处理、合成、控制、导入和导出视听媒体。
Topics
Media Assets, Playback, and Editing:: 媒体资源获取,播放,编辑功能
- 获取并检查媒体资产
- 排队播放媒体资源并自定义播放行为
- 编辑合并资产
- 导入和导出原始媒体流。(CVPixelBuffer,CVSimpleBuffer)
AirPay2
- 使用AirPlay 2,您可以将内容从任何Apple设备无线发送到启用AirPlay的设备。
Cameras and Media Capture
- 拍摄照片并录制视频和音频;配置内置摄像头和麦克风或外部捕获设备。
System Audio Interaction
- 将系统音频集成到你的应用程序中
Audio Track Engineering:
- 音轨工程, 播放、录制、混音和处理音频。
Speech Synthesis
- 语音合成
AV Video Composition Render Hint
Creating a Basic Video Player (iOS and tvOS)
- 通过配置
AVAudioSession
和使用系统提供的AVPlayer
,AVPlayerViewController
,可以快速的通过一个指定的url播放视屏
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback, mode: .moviePlayback)
}
catch {
print("Setting category to AVAudioSessionCategoryPlayback failed.")
}
return true
}
//播放视频.
@IBAction func playVideo(_ sender: UIButton) {
guard let url = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8") else {
return
}
// Create an AVPlayer, passing it the HTTP Live Streaming URL.
let player = AVPlayer(url: url)
// Create a new AVPlayerViewController and pass it a reference to the player.
let controller = AVPlayerViewController()
controller.player = player
// Modally present the player and call the player's play() method when complete.
present(controller, animated: true) {
player.play()
}
}
About the Asset Model
AVAsset的实例可以表示基于本地文件的媒体,例如QuickTime电影或MP3音频文件,还可以表示从远程主机逐步下载或使用HTTP Live Streaming(HLS)流式传输的资产。
- AVAsset在两个重要方面简化了与媒体的工作。首先,它提供了一个独立于媒体格式的级别。它为您提供了一个一致的界面,用于管理和与媒体交互,而不管其底层类型如何。该框架管理着使用容器格式和编解码器类型到框架的详细信息,让您专注于如何使用应用程序中的媒体资产。
- 第二,AVAsset提供了一个独立于媒体位置的级别。通过使用媒体的URL初始化资产实例来创建它。这可能是一个本地URL,例如包含在应用程序包中或文件系统上其他地方的URL,也可能是一个资源,例如远程服务器上托管的HLS流。在这两种情况下,框架都会代表您执行必要的工作,以高效地检索和加载介质。消除处理媒体格式和位置的负担大大简化了视听媒体的工作。
- AVAsset是一个容器对象,由一个或多个AVAssetTrack实例组成,它为资产的统一类型的媒体流建模。最常用的轨迹类型是音频和视频轨迹,但资产轨迹也会对其他辅助轨迹进行建模,例如闭合字幕、字幕和定时元数据。
总体来看,它就是一个资源托管对象,常用的类及其功能如下:
Asset Manipulation
检索资源以进行播放或收集有关资源的信息。
- AVAssetCache: 用于检查资源本地缓存媒体数据状态的对象。有个重要属性
playableOffline
,配置是否允许脱机访问。 - AVFragmentedAsset: 可以在不修改以前存在的数据结构的情况下延长其总持续时间的资产。
- AVFragmentedAssetMinder: 一个对象,定期检查碎片化资产是否附加了额外的碎片。
- AVAsynchronousKeyValueLoading: 异步加载访问资信息,这个类非常常用。
- AVAssetTrackGroup: 资产中的一组相关轨迹。
- AvassetTrackSegment: 资产跟踪的一段,由从源到资产跟踪时间轴的时间映射组成
- AVFragmentedAssetTrack: 用于处理零碎资产轨迹的对象。
Asset Retrieval
- AVURLAsset: AVAsset的具体子类,用于从本地或远程URL初始化资产。
- AVAssetDownloadURLSession: 用于支持创建和执行资产下载任务的URL会话。
- AVAssetResourceLoader: 一个对象,用于调解来自URL资产的资源请求。
- AVAssetResourceLoadingRequest: 封装由资源装入器对象发出的有关资源请求的信息的对象。
- URLResponse: 加载请求的URL响应。
- AVAssetResourceRenewalRequest: 一个对象,它封装有关由资源加载器发出的资源请求的信息,以更新以前发出的请求。
- AVAssetDownloadStorageManager: 用于自动清除已下载策略的资产管理器。
- AVAssetDownloadStorageManagementPolicy: 一组属性,用于定义自动清除已下载资产的策略。
- AVMutableAssetDownloadStorageManagementPolicy: 一组可变属性,用于定义自动清除已下载资产的策略。
Asset File Import and Export
- AVAssetReader: 一种读卡器对象,用于获取资产的媒体数据,可以基于文件,也可以由来自多个源的媒体数据的集合组成。
- AVAssetReaderAudioMixOutput: 定义一个接口的对象,用于读取从一个或多个音轨混合产生的音频样本。
- AVAssetReaderTrackOutput: 定义从资产读取器资产的单轨读取媒体数据的接口的对象。
- AVAssetReaderSampleReferenceOutput: 定义用于从单个资源轨迹读取示例引用的接口的对象。
- AVAssetReaderVideoCompositionOutput: 一种对象,它读取由阅读器资源的一个或多个轨迹中的帧合成的视频帧。
- AVAssetReaderOutput: 一个抽象类,它定义了一个接口,用于从资产读取器对象读取公共媒体类型的单个样本集合。
- AVAssetReaderOutputMetadataAdaptor: 定义用于读取元数据的接口的对象。
- AVAssetImageGenerator: 提供资源缩略图或预览图像的对象,与播放无关。
Export the New Video into the Desired Format
通过将资源导出为所需的文件类型来转换电影文件。从AVFoundation提供的AVFileType预设列表中选择您想要的最终视频类型。您将使用该类型配置一个AVAssetExportSession对象,然后该对象管理现有类型的导出过程。
- 如下示例,为导出
H.264
格式的视频,通过指定输出文件的类型,和编码格式,将配置信息写入到Session,由系统API去处理复杂的逻辑。
#import <AVFoundation/AVFoundation.h>
//准备输入源,输出目标
AVAsset* hevcAsset = // Your source AVAsset movie in HEVC format //
NSURL* outputURL = // URL of your exported output //
// These settings will encode using H.264.
NSString* preset = AVAssetExportPresetHighestQuality;
AVFileType outFileType = AVFileTypeQuickTimeMovie;
//检查配置信息是否可以执行
[AVAssetExportSession determineCompatibilityOfExportPreset:preset withAsset:hevcAsset outputFileType:outFileType completionHandler:^(BOOL compatible) {
if (!compatible) {
return;
}
}];
//异步导出
AVAssetExportSession* exportSession = [AVAssetExportSession exportSessionWithAsset:hevcAsset presetName:preset];
if (exportSession) {
exportSession.outputFileType = outFileType;
exportSession.outputURL = outputURL;
[exportSession exportAsynchronouslyWithCompletionHandler:^{
// Handle export results.
}];
}
- AVAssetExportSession: 对资源源对象的内容进行代码转换以创建由指定的导出预置描述的窗体的输出的对象。
- AVAssetWriter: 用于将媒体数据写入指定视听容器类型的新文件的对象。
- AVAssetWriterInput: 用于将媒体示例附加到资产写入程序输出文件的单轨的写入程序。
- AVVideoTransferFunction_ITU_R_2100_HLG:
ITU_R BT.2100
颜色空间 - AVOutputSettingsAssistant: 指定一组参数的对象,用于配置使用输出设置词典的对象。
- AVAssetWriterInputGroup: 相互排斥关系中的一组轨迹。
- AVAssetWriterInputMetadataAdaptor: 定义接口的对象,用于将打包为定时元数据组的元数据写入单个资产写入器输入。
- AVAssetWriterInputPassDescription: 一个对象,定义用于查询当前过程的要求的接口。
- AVAssetWriterInputPixelBufferAdaptor: 一种缓冲区,用于将打包为像素缓冲区的视频样本附加到单个资产写入器输入。
Media Playback-状态监听
响应播放状态更改: 下面的例子通过KVO的方式进行监听
let url: URL = // Asset URL var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
// Key-value observing context
private var playerItemContext = 0
let requiredAssetKeys = [
"playable",
"hasProtectedContent"
]
func prepareToPlay() {
// Create the asset to play
asset = AVAsset(url: url)
// Create a new AVPlayerItem with the asset and an
// array of asset keys to be automatically loaded
playerItem = AVPlayerItem(asset: asset,
automaticallyLoadedAssetKeys: requiredAssetKeys)
// Register as an observer of the player item's status property
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
// Associate the player item with the player
player = AVPlayer(playerItem: playerItem)
}
[
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
]() guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
// Switch over status value
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}
}
}
Media PlayBack-播放时间监听
注册监听的时间回调时间
```Objective-C var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?
func addPeriodicTimeObserver() {
// Notify every half second
let timeScale = CMTimeScale(NSEC_PER_SEC)
let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time,
queue: .main) {
[weak self] time in
// update player transport UI
}
}
func removePeriodicTimeObserver() {
if let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}
```固定时间分割点监听
var asset: AVAsset! var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?
func addBoundaryTimeObserver() {
// Divide the asset's duration into quarters.
let interval = CMTimeMultiplyByFloat64(asset.duration, 0.25)
var currentTime = kCMTimeZero
var times = [NSValue]()
// Calculate boundary times
while currentTime < asset.duration {
currentTime = currentTime + interval
times.append(NSValue(time:currentTime))
}
timeObserverToken = player.addBoundaryTimeObserver(forTimes: times,
queue: .main) {
// Update UI
}
}
func removeBoundaryTimeObserver() {
if let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}
Media-Player Seeking
视频播放的进度条移动
// Seek to the 2 minute mark let time = CMTime(value: 120, timescale: 1)
player.seek(to: time)
// Seek to the first frame at 3:25 mark,时间的精度更高,
let seekTime = CMTime(seconds: 205, preferredTimescale: Int32(NSEC_PER_SEC))
player.seek(to: seekTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)注意,提高精度对解码有影响
Asset Playback
- 通过Asset去播放资源,这也是实际开发中比较常用的方式
Sample Buffer Playback
使用自定义播放器播放音频和视频示例缓冲区。
- AVSampleBufferAudioRenderer: 用于解压缩音频并播放压缩或未压缩音频的对象。
- AVSampleBufferDisplayLayer: 显示压缩或未压缩视频帧的对象。
- AVSampleBufferRenderSynchronizer: 用于将多个排队的样本缓冲区同步到单个时间轴的对象。
构建一个音频播放器来播放您的自定义音频数据,还可以选择利用AirPlay2的高级功能
指定长格式音频,配置会话
do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, policy: .longForm)
} catch {
print("Failed to set audio session route sharing policy: (error)")
}注意:
.longForm is renamed to .longFormAudio in iOS 13 and above.
管理Playlist
private struct Playlist { // Items in the playlist.
var items: [SampleBufferItem] = [] //通过源数据来播放
// The current item index, or nil if the player is stopped.
var currentIndex: Int?
}log输出,信号量控制时序
func insertItem(_ newItem: PlaylistItem, at index: Int) { playbackSerializer.printLog(component: .player, message: "inserting item at playlist#(index)")
atomicitySemaphore.wait()
defer { atomicitySemaphore.signal() }
playlist.items.insert(playbackSerializer.sampleBufferItem(playlistItem: newItem, fromOffset: .zero), at: index)
// Adjust the current index, if necessary.
if let currentIndex = playlist.currentIndex, index <= currentIndex {
playlist.currentIndex = currentIndex + 1
}
// Let the current item continue playing.
continueWithCurrentItems()
}注意修改时的线程安全问题
重启
private func restartWithItems(fromIndex proposedIndex: Int?, atOffset offset: CMTime) { // Stop the player if there is no current item.
guard let currentIndex = proposedIndex,
(0 ..< playlist.items.count).contains(currentIndex) else { stopCurrentItems(); return }
// Start playing the requested items.
playlist.currentIndex = currentIndex
let playbackItems = Array(playlist.items [currentIndex ..< playlist.items.count])
playbackSerializer.restartQueue(with: playbackItems, atOffset: offset)
}Schedule Playback: 一旦SampleBufferSerializer对象接收到要播放的项目队列,它将继续执行两项任务:将项目转换为一系列包含音频数据的示例缓冲区,并将缓冲区排队进行渲染。SampleBufferSerializer使AVSampleBufferRenderSynchronizer对象在正确的时间播放音频,并使AVSampleBufferAudioRenderer对象及时呈现排队的音频采样缓冲区以便播放。具体参考Demo。
静态图像及视频的采集
为了管理一个设备,如相机或者麦克风.你可以弄一些对象来描绘输入设备和输出设备.同时用一个AVCaptureSession的实例来协调二者之间的数据流传递.你至少需要如下实例:
- AVCaptureDevice: 用来表示输入设备.如相机或者麦克风
- AVCaptureInput: 用来配置输入设备的端口
- AVCaptureOutput: 用来管理是输出成一个视频文件还是静态图片
- AVCaptureSession: 协调输入设备和输出设备之间数据流的传递
运行流程如下:
输入源和输出源通过Session进行传输,最终通过解码之后呈现成我们需要的数据,输入源是我们的硬件设备,相机,话筒,输出则是图片,视频流,资源文件。
上图可以看到通常一个输出对应多个输入,为了方便管理,于是引入了AVCaptureConnection,它可以将多个Input和对应的output进行关联。
相机麦克风授权
- 配置plist
- NSCameraUsageDescription
- NSMicrophoneUsageDescription
- 如果需要存储图片和视频到照片库则需额外添加
- NSPhotoLibraryUsageDescription
- NSMicrophoneUsageDescription
请求授权
switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: // The user has previously granted access to the camera.
self.setupCaptureSession()
case .notDetermined: // The user has not yet been asked for camera access.
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
self.setupCaptureSession()
}
}
case .denied: // The user has previously denied access.
return
case .restricted: // The user can't grant access due to restrictions.
return
}音视频获取-配置Session
它是iOS和macOS中所有媒体捕获的基础。管理应用程序对操作系统捕获基础设施和捕获设备的独占访问,以及从输入设备到媒体输出的数据流。管理输入和输出流,以及它们直接的连接对象。例如,下图显示了一个捕捉会话,可以捕捉照片和电影,并提供了一个摄像头预览,使用iPhone背摄像头和麦克风。
配置时需要显示的调用beginConfiguration
///配置input
captureSession.beginConfiguration()
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video, position: .unspecified)
guard
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!),
captureSession.canAddInput(videoDeviceInput)
else { return }
captureSession.addInput(videoDeviceInput)
//配置output
let photoOutput = AVCapturePhotoOutput()
guard captureSession.canAddOutput(photoOutput) else { return }
captureSession.sessionPreset = .photo
captureSession.addOutput(photoOutput)
captureSession.commitConfiguration()
- 定义一个预览视图层,用来向用户展示捕捉的数据
class PreviewView: UIView {
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
/// Convenience wrapper to get layer as its statically known type.
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
///预览层实时的获取session的数据
self.previewView.videoPreviewLayer.session = self.captureSession
注意session的方向:
运行AVCapatureSession
配置好输入、输出和预览后,调用startRunning()让数据从输入流到输出。
对于一些捕获输出,运行会话就是开始媒体捕获所需的全部内容。例如,如果会话包含AVCaptureVideoDataOutput,则在会话运行后立即开始接收传送的视频帧。
对于其他捕获输出,首先启动会话运行,然后使用capture输出类本身来启动捕获。例如,在摄影应用程序中,运行会话将启用取景器样式的预览,但您使用的是
AVCapturePhotoOutput capturePhoto(with:delegate:)
捕捉图片的方法。
Demo参考:
- https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/avmulticampip_capturing_from_multiple_cameras
- https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/avcam_building_a_camera_app
TODO
- 音视频合成,此部分通常采用三方库合成如
ffmpeg/ijkplayer