Exploring AVFoundation 문서읽기

kimseawater
daily-monster
Published in
31 min readApr 16, 2024

안녕하세요 화요일 kimseawater입니다. 오늘은 Exploring AVFoundation 문서 해석을 가지고 왔습니다!! 전반적인 내용을 모두 알려주는 문서였어요~

Understanding the Asset Model

많은 AVFoundation의 주요 기능은 media asset을 재생하고 처리하는 것이다. 프레임워크는 AVAsset 클래스를 이용해 에셋을 모델링하는데, 에셋은 추상적이고 단일 미디어 리소스를 나타내는 변하지 않는 타입이다. media asset의 합성 뷰를 제공하는데, 그건 미디어 전체의 정적인 부분을 모델링 한 것이다. AVAsset의 인스턴스는 local file-based 미디어(ex. QuickTime Movie, MP3 audio) 뿐만 아니라, remote host에서 다운받은거나 HLS stream도 나타낼 수 있다.

AVAsset은 두 가지 중요한 방식으로 미디어 작업을 단순화한다. 첫번째로, media format으로 부터 독립 수준을 제공한다. 타입에 상관없이 미디어와 상호작용할 때 일관된 인터페이스를 제공한다. container format, codec 타입에 관한 작업은 프레임워크에 남겨지고, 앱에서는 어떻게 에셋을 사용할지만 초점을 맞추면 된다.

두 번째로, AVAsset은 미디어의 위치와 독립적인 수준을 제공한다. asset 인스턴스를 미디어의 URL로 이니셜라이징해서 만들 수 있다. 이는 app bundle이나 file system내의 주소같은 local URL일 수도 있고, 서버에 있는 HLS stream 리소스가 될 수도 있다. 어느 쪽이든 간에, 프레임워크는 작업을 효율적으로 수행하고, 너를 대신해 적절한 시점에 미디어를 로드한다. 미디어 형식, 위치를 직접 처리하는 부담을 없애 시청각 미디어 작업을 크게 단순화시킬 수 있다.

AVAsset은 AVAsseetTrack의 인스턴스들이 합쳐진 것이다. AVAssetTrack은 에셋의 균일한 타입의 미디어 스트림을 모델링한다. 가장 일반적으로 사용되는 track type은 audio와 video track이다. 그러나 AVAssetTrack은 closed 캡션, 자막 및 시간 메타데이터와 같은 추가적인 트랙들도 모델링한다.

tracks 프로퍼티를 이용해 asset의 트랙 컬렉션들을 검색할 수 있다. 대부분의 경우, 전체 컬렉션에 대한 작업보다는 일부분의 트랙들에 대해 작업을 할 것이다. 이 상황에서 AVAsset은 또한 identifier, media type, characteristic에 기반한 일부 트랙을 검색하는 메서드도 제공한다.

Creating an Asset

아래처럼 local, remote URL을 초기화해서 AVAsset을 만들 수 있다.

let url: URL = // local or remote URL
let asset = AVAsset(url: url)

AVAsset은 추상클래스라서, 예시처럼 asset을 만들면 AVURLAsset이라 불리는 구체적인 서브 클래스의 인스턴스로 만들어진다. 많은 경우에 asset을 이런 방식으로 만들겠지만, 좀 더 구체적으로 컨트롤해야 하면 AVURLAsset의 생성자를 사용해도 된다. AVURLAsset의 생성자는 option 딕셔너리가 있는데, 이는 특정 사용 사례에 맞게 조정해줄 수 있다. 예를들어, HLS stream을 위한 asset을 만들었을 때, cellular network 연결시 미디어에 연결되지 않도록 하고 싶을 수 있다. 그럼 아래 예시처럼 하면 된다.

let url: URL = // remote Asset URL
let options = [AVURLAssetAllowsCellularAccessKey: false]
let asset = AVURLAsset(url: url, options: options)

AVURLAssetAllowsCellularAccessKey 옵션에 false를 넘기면, Wi-fi에 연결되었을 때만 미디어에 연결된다. AVURLAsset Class Reference를 더 보면 옵션 알 수 있다.

Preparing Asset for Use

playback, duraton, creation date, metadata와 같은 기능을 알기 위해 AVAsset의 프로퍼티를 사용한다. asset을 만드는 것이 자동으로 프로퍼티를 로드하고 특정 용도에 맞게 준비되는것은 아니다. 대신에, asset의 프로퍼티 값의 로딩은 그들이 요청될 때까지 지연된다. 왜냐하면, 프로퍼티 접근은 synchronous하고, 만약 요청된 프로퍼티가 로드되지 않았다면 프레임워크가 값을 리턴하기 위해 많은 작업을 해야할 수도 있다. macOS에서 만약 로드되지 않은 프로퍼티가 메인스레드에서 접근되었다면, user interface가 반응하지 않을 수도 있다. iOS나 tvOS에서, 상황은 더 심각해질 수 있다. 왜냐면 미디어 작업이 공유 미디어 서비스 데몬에 의해 수행되기 때문이다. 만약, 로드되지 않은 프로퍼티 값을 요청한다면, 굉장히 오래 block되어 있을 수 있고, 미디어 서비스에서 타임아웃이 일어날 수도 있다. 이를 막기 위해, asset 프로퍼티를 asynchronous하게 로드해야 한다.

AVAsset과 AVAssetTrack은 AVAsynchronousKeyValueLoading 프로토콜을 채택하는데, 이는 두가지 메서드를 정의한다. 현재 프로퍼티의 로드 상태를 가져오거나, 필요한 경우 프로퍼티 값을 가져올 수 있다.

public func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)?)
public func statusOfValue(forKey key: String, error outError: NSErrorPointer) -> AVKeyValueStatus

async하게 하나 혹은 그 이상의 프로퍼티 값을 가져오려면 loadValuesAsynchronously(forKeys: completionHandler:)를 사용해라. 가져올 프로퍼티 이름을 key에 배열로 넘기고, completion bock은 status가 결정되면 호출된다. 아래 예시는 asset의 **playable 프로퍼티**를 비동기적으로 로드하는 방식을 예시로 보여준다.

// bundle asset name => "example.mp4"
let url = Bundle.main.url(forResource: "example", withExtension: "mp4")
let asset = AVAsset(url: url)
let playableKey = "playable"
// bundle asset name => "example.mp4"
let url = Bundle.main.url(forResource: "example", withExtension: "mp4")
let asset = AVAsset(url: url)
let playableKey = "playable"

// playable 키 가져오기
asset.loadValuesAsynchronously(forKey: [playableKey]) {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: playableKey, error: &error)

switch status {
case .loaded:
// 성공적으로 로드됨.
case .failed:
// 에러 핸들링
case .cancelled:
// 프로세스 중단
default:
// 다른 경우 핸들링
}
}

프로퍼티 stauts를 completion callback에서 statusOfValue(forKey:error:) 메서드를 이용해 체크한다. AVKeyValueStatusLoaded의 상태는 프로퍼티 value가 성공적으로 로드되었는지, blocking없이 가져와졌는지를 나타낸다. AVKeyValueStatusFailed는 데이터를 로드할 때 에러가 생겨서 프로퍼티 값을 사용할 수 없음을 나태낸다. NSError 포인터를 출력해서 에러 이유를 볼 수 있다. 모든 클래스에서, completion callback은 임의적으로 background queue에서 실행된다. Dispatch 메서드는 user-interface 관련 작업을 수행하기 전에 호출을 main queue로 다시 보낸다.

Working with Metadata

media container formats는 미디어에 대한 설명 메타데이터를 저장할 수 있다. 개발자로서 각각의 container format이 각각 유니크한 metadata format을 가지고 있어서 메타데이터를 다루기 어려운 경우가 많다. 일반적으로 container의 메타데이터를 읽고 쓰기 위해서는 format에 대한 low-level을 이해하고 있어야 하지만, AVFoundation은 AVMetadataItem 클래스를 이용해 메타데이터 작업을 단순화시켰다.

일반적인 형태에서, AVMetadataItem의 인스턴스는 movie 타이틀이나 앨범 아트워크 등 단일 메타데이터 값을 key-value 값으로 표현한다. 같은 방식으로, AVAsset은 미디어의 표준화된 뷰를 제공하고 AVMetadataItem은 메타데이터와 관련된 표준화 뷰를 제공한다.

Retriving a Collection of Metadata

AVMetaDataItem을 효과적으로 사용하기 위해, AVFoundation이 어떻게 metadata를 구성하는지를 알아야 한다. metadata item을 간단히 찾고 필터링하기 위해, framework는 metadata를 key space와 연관짓는다.

  • Format-specific key spaces: 프레임워크는 여러 format-specific key spaces를 정의한다. 이는 대략적으로 특정 container 혹은 file format (ex. QuickTime (QuickTime Metadata and UserData) or MP3 (ID3)과 연관된다. 그러나, 단일 에셋은 여러개의 key spaces에 걸쳐 메타데이터 값이 포함될 수 있다. metadata 프로퍼티를 이용해 asset의 완전한 format-specific metadata의 컬렉션을 가져올 수 있다.
  • Common key space: movie’s creation date나 description 같은 공통적인 metadata value들이 있다. 이는 여러 key space에 걸쳐있다. 이 common metadata에 접근하는 것을 표준화하기 위해, 프레임워크는 공통 메타데이터 의 제한된 집합에 대한 액세스를 제공하는 common key space를 제공한다. 이는 공통적으로 사용되는 메타데이터를 구체적인 포맷을 신경쓰지 않도로 쉽게 가져올 수 있게 한다. asset의 common metadata컬렉션은 commonMetadata프로퍼티를 통해 가져올 수 있다.

availableMetadataFormats프로퍼티를 호출해서 어떤 메타데이터 형식을 에셋이 가지고 있는지 볼 수 있다. 이 프로퍼티는 각각의 메타데이터 포맷이 포함한 identifiers를 string array로 리턴해준다. 그리고 그 metadata(Format:)메서드를 이용해 적절한 format identifier를 전달해서 특정한 format-specific metadata를 가져올 수 있다.

let url = Bundle.main.url(forResource: "audio", withExtension: "m4a")!
let asset = AVAsset(url: url)
let formatsKey = "availableMetadataFormats"
asset.loadValuesAsynchronously(forKeys: [formatsKey]) {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: formatKey, error: &error)
if status == .loaded {
for format in asset.availableMetadataFormats {
// 특정한 format-specific metadata를 가져온다.
let metadata = asset.metadata(forFormat: format)
// process format-specific metadata collecton
}
}
}

Finding and Using Metadata Values

메타데이터 컬렉션을 가지고 오고 난 후에는, 다음 스텝은 그 안에 있는 specific value를 찾은 것이다. metadata collecion을 필터링 하기 위해, 다양한 AVMetadataItem의 메서드를 사용한다. 가장 쉬운 방법은 identifier를 이용해 메타데이터 아이템을 필터링 하는것이다. identifier는 keyspace와 single unit의 키를 그룹화 한다. 아래 예시는 title item을 common key space에서 가져오는 예시이다.

let metadata = asset.commonMetadata
let titleID = AVMetadataCommonIdentifierTitle
// common metadata에서 특정 identifier로 필터링한 값
let titleItems = AVMetadataItem.metadataItems(form: metadata, filteredByIdentifier: titleID)
if let item = titleItems.first {
// process title item
}

AVMetadataItem의 필터링 메서드는 single instance대신 아이템의 컬렉션을 리턴한다. 많은 경우에 single element만 포함한 컬렉션을 리턴하지만, 만약 media가 localized metadata를 갖거나, common keyspace에서 검색하는 경우, 동일한 키값이 여러 공간에 존재하는 경우 등은 각각의 Locale이나 Keyspace에 연관된 구별된 값이 리턴된다.

특정 metadata item을 얻고 나서는, 다음단계는 그 value 프로퍼티를 호출하는 것이다. 값은 NSObject나 NSCopying 프로토콜을 채택한 객체 타입을 리턴한다. 값을 적절한 타입으로 수동으로 캐스팅할 수 있지만, 메타데이터 항목의 타입 강제 프로퍼티를 사용하는게 더 안전하고 쉽다. 그것의 stringValue, numberValue, dateValue 프로퍼티를 이용해서 값을 해당 유형에 더 쉽게 적용할 수 있다. 예를들어, 아래 예시는 iTunes music track과 관련된 atrwork를 얻는 예시이다.

// common metadata 컬렉션
let metadata = asset.commonMetaData
// 필터링
let artworkItems = AVMetadataItem.metadataItems(
from: metadata,
filteredByIdentifier: AVMetadataCommonIdentifierArtwork
)
if let artworkItem = artworkItem.first {
// dataValue를 이용해 NSData에 값을 강제로 적용
if let imageData = artworkItem.dataValue {
let image = UIImage(data: imageData)
} else {
// No image data found
}
}

메타데이터는 많은 미디어 앱에서 중요한 역할을 한다. 뒤 섹션에는 어떻게 재생 앱을 static, time 메타데이터를 기능을 강화하는지 볼것이다.

Playing Media

앞의 섹션에서 설명한 에셋 모델은 재생 사용 사례의 기반이다. 에셋은 재생을 하고자 하는 미디어를 나타낼 뿐만아니라, 그림 일부를 나타내기도 한다. 이 섹션에서는 미디어를 재생하고 보여줄때 필요한 추가적인 객체를 말할것이다. 그리고 어떻게 그들이 재생하도록 구성할 것인지 보여준다.

AVPlayer

AVPlayer는 재생을 위한 가장 중심 클래스이다. 플레이어는 컨트롤러 객체로, 재생과 미디어 에셋 타이밍을 관리한다. 이를 loacl 재생, 다운로드, 미디어 스트리밍에 사용할 수 있고 화면에 어떻게 나오는지 프로그램적으로 조정한다.

AVPlayer를 한번에 단일 미디어만 재생하도록 한다. 프레임워크는 AVQueuePlayer라는 AVPlayer의 하위 플레이어 객체를 재공하는데, 이는 미디어 에셋이 sequentially하게 재생되도록 미디어의 큐를 조정한다.

AVPlayerItem

AVAsset은 오직 미디어의 static한 측면만 모델링한다. (ex. duration, creation date..) 에셋을 재생하기 위해, dynimic 한 부분의 인스턴스를 만들어야 하는데, 이는 AVPlayerItem으로 한다. 이 객체는 timing과 AVPlayer에의해서 재생되는 에셋의 presentation state를 모델링한다. AVPlayerItem의 프로퍼티와 메서드를 이용해서 미디어의 다양한 시간들을 seek할 수 있고, presentation size를 결정하고, 현재 시간을 식별할 수 있고.. 더 많은 것들을 할 수 있다

AVKit and AVPlayerLayer

AVPlayer와 AVPlayerItem은 보이지 않는 객체고, 그 자체로 video에 보여지지 않는다. 비디오 컨텐츠를 앱에 보이게 하기 위한 두 다른 옵션이 있다.

  • AVKit: 가장 좋은 방법은 AVKit 프레임워크의 AVPlayerViewController(iOS, tvOS), AVPlayerView(macOS)를 사용해서 비디오 컨텐츠를 보여주는 것이다. 이 객체는 재생 컨트롤, 다른 미디어 기능들을 비디오 컨텐츠와 함께 제공해서 재생 경험의 전체 기능을 제공한다.
  • AVPlayerLayer: 만약 플레이어 인터페이스를 커스텀하려면, AVFoundation에서 제공된AVPlayerLayer라고 불리는 CALayer 서브클래스를 사용하면 된다. 플레이어 레이어는 view의 뒤쪽 레이어를 설정할 수 있고, layer 계층에 직접적으로 추가할 수 있다. AVPlayerView나 AVPlayerViewController와 다르게, AVPlayerLayer는 어떤 재생 컨트롤도 표시하지 않고, 단순히 visual content만 플레이어 화면에 표시한다. play, pause, seek 컨트롤을 하는걸 만드는건 개발자가..해야한다

Setting Up the Playback Objects

아래 example은 재생 시나리오의 전체 그래프를 만드는 단계를 보여준다. example은 iOS, tvOS 지만 macOS에도 기본적인 단계는 같다.

class PlayerViewController: UIViewController {
@IBOutlet weak var playerViewController: AVPlayerViewController!

var player: AVPlayer!
var playerItem: AVPlayerItem!

override func viewDidLoad() {
super.viewDidLoad()

// asset URL 정의
let url: URL = // url to local, streamed media
// asset instance 생성
let asset = AVAsset(url: url)
// playerItem 생성
playerItem = AVPlayerItem(asset: asset)
// player instance 생성
player = AVPlayer(playerItem: playerItem)

// player를 뷰컨에 연결
playerViewController.player = player
}
}

재생 오브젝트를 만들면, player의 play 메서드를 이용해 재생을 시작할 수 있다.

AVPlayer와 AVPlayerItem은 play item의 미디어가 사용이 준비되면, 다양한 재생 방법을 제공한다. 다음 단계는 어떻게 재생 오브젝트를 관찰하고, 재생준비가 되어있는지를 확인하는 것이다.

Observing Playback State

AVPlayer와 AVPlayerItem은 dynamic한 object여서, 그 상태가 빈번하게 바뀐다. 때때로 이러한 변화에 대해 적절하게 응답하길 원하는데, 이때 KVO(Key-value object)를 이용해서 변화에 응답할 수 있다. KVO를 이용하면, 객체가 다른 객체의 상태를 관찰할 수 있게 된다. 관찰된 객체의 상태에 변화가 생기면, 옵저버는 상태 변화와 관련된 세부사항을 받는다. KVO를 사용하는 것은 AVPlayer와 AVPlayerItem의 상태 변화를 쉽게 만들고, 거기에 적절한 응답 액션을 취할 수 있다.

관찰하기 위해 가장 중요한 AVPlayerItem 프로퍼티는 status이다. status는 만약 player item이 재생준비가 되었는지 그리고 일반적으로 사용할 수 있는 상태인지를 나타낸다. 처음 player item을 생성하면, status값은 AVPlayerItemStatusUnknown 상태이고, 이는 미디어가 로드되지 않았거나 재생을 위해 enqueue 되지 않았음을 나타낸다. player item과 AVPlayer를 연관시킬때, 그 즉시 item의 media가 enqueue되고, 재생을 준비한다. player item 재생이 준비되면 AVPlayerItemStatusReadyToPlay로 상태가 바뀐다. 아래 예시는 어떻게 상태 변화를 관찰하는지를 보여준다.

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() {
// play를 위한 에셋 만들기
asset = AVAsset(url: url)

// asset을 이용해 AVPlayerItem을 만든다.
// asset key들의 배열이 자동적으로 로드된다.
playerItem = AVPlayerItem(
asset: asset,
automaticallyLoadedAssetKeys: requiredAssetKeys
)

// player item의 status 프로퍼티에 옵저버 등록
playerItem.addObserver(
self,
forkeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new]
context: &playerItemContext
)

// player와 playerItem 연결
player = AVPlayer(playerItem: playerItem)
}

prepareToPlay() 메서드에서 addObserver(_:, forKeyPath:, options:, context:)를 이용해 player item의 status 프로퍼티를 관찰하도록 등록한다. 이 메서드는 player item이 player와 연결되기 전에 호출해서, item의 모든 status 변화를 확인해야 한다.

status 변화를 알리 받기 위해서, observeValue(forKeyPath:ofObject:change:context:) 메서드를 구현해야 한다. 이 메서드는 status가 변할 때마다, 액션을 취할 수 있도록 한다.

override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
// playerItemContext에 대해서만 observation
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 = .unknwon
}

switch status {
case .readyToPlay: // 성공
case .failed: // 실패
case .unknown: // 아직 준비 x
}
}
}

예시는 매개변로 받은 change dictionary에서 상태 값을 검색하고, 해당 값을 전환한다. 만약 Player item의 status가 AVPlayerItemStatusReadyToPlay라면, 사용 준비가 완료된 것이다. 만약 player item의 미디어를 로드할 때 문제가 있다면, status가 AVPlayerItemStatusFailed일 것이다. 만약 Failed가 발생하면, player item의 error 프로퍼티를 가져와 NSError 객체를 이용해 실패 이유를 찾을 수 있다.

Performing Time-Based Operations

미디어 재생은 time 기반의 활동이다. 그래서 특정 기간동안 시간이 지정된 미디어 샘플을 고정 속도로 표시한다. Time-based 작업(ex. media에서 seeking)은 미디어 재생 앱을 만들 때 중요한 역할이다. 많은 AVPlayer와 AVPlayerItem의 중요한 기능들은 미디어 timing과 많은 관련이 있다. 이 기능을 효과적으로 배우기 위해, AVFoundaion에서 시간이 어떻게 나타나는지를 이해해야 한다.

AVFoundation을 포함한 몇몇 Apple framework 에서는 시간을 floating-point NSTimeInterval 값으로 나타낸다. 여러 경우에 이는 시간을 생각하고 나타내는 자연스러운 방식을 제공하는데, 시간이 지정된 미디어 작업을 수행할 때 자주 문제가 생긴다. 미디어를 작업할 때 sample-accurate timing을 유지하는것이 중요한데, floating-point 부정확성은 종종 timing draft를 발생시킬 수 있다. 이 불확실성을 없애기 위해, AVFoundation은 time을 Core Media Framework의 CMTime 데이터 타입을 이용해 해결한다.

Core Media는 low-level의 C 프레임워크이다. Core Media는 AVFoundation과 애플 플랫폼의 상위 레벨의 미디어 프레임워크에서 볼 수 있는 많은 기능을 제공한다. 많은경우, CoreMedia를 AVFoundation에서 제공하는 더 높은 레벨의 인터페이스로 작업할 것이지만, CMTime이라 불리는 데이터 타입은 자주 쓸 것이다.

public struct CMTime {
public var value: CMTime
public var timescale: CMTimeScale
public var flags: CMTimeFlags
public var epoch: CMTimeEpoch
}

이 구조체는 시간에 대한 합리적(또는 분수적..)인 표현을 정의한다. 여기서 가장 중요한 두 필드는 value와 timescale이다. CMTimeValue는 64 bit의 정수로, 분수에서 분자를 담당한다. 그리고 CMTimeScale은 32bit의 정수로, 분모를 담당한다. 이 구조체는 time을 미디어 frame rate, 혹은 sample rate에 기반해서 시간을 표현한다.

// 0.25 second
let quarterSecond = CMTime(value: 1, timescale: 4)

// 10 second mark in a 44.1 kBz audio file
let tenSeconds = CMTime(value: 441000, timescale: 44100)

// 3 seconds into a 30fps video
let cursor = CMTime(value: 90, timescale: 30)

Core Media는 CMTime 값을 만들고 산술, 비교, 검증, 변환 작업을 수행하는 여러 방법을 제공한다. Swift를 사용한다면, Core Media는 CMTime에 여러 extension과 연산자 overloads를 추가해서 더 쉽게 작업을 수행할 수 있다. 자세한 내용은 Core Media Reference ㄱ ㄱ

Observing Time

재생 시간 시간이 진행되면서 이를 관찰해서 재생 위치나 UI 동기화를 하고 싶을 것이다. 앞에서 KVO를 이용해 재생 상태를 관찰하는 걸 봤다. KVO는 일반적인 관찰을 할 때는 잘 동작하지만, player timing을 관찰하는 데에 적합하진 않다. 왜냐면 계속 바뀌는 변화를 관찰하는거랑 잘 안맞기 때문이다. 대신에, player 시간 변화를 관찰하기 위해 AVPlayer에서 제공하는 두 방법을 사용해라: periodic observation과 boundary observation

periodic observations

time을 규칙적, 주기적인 간격으로 관찰할 수 있다. 만약 custom player를 만들었다면, 가장 빈번하게 사용하는 경우가 ui에 time 을 보여주는 것을 업데이트 할 때 일 것이다.

periodic timing을 관찰하기 위해, player의 addPeriodicTimeObserver(forInterval:queue:usingBlock:)메서드를 사용하면 된다. 이 메서드는 CMTime을 매개변수로 받아서 시간 간격을 나타내고, serial dispatch queue를 받고, 특정 시간 간격에 따라 호출될 callback block을 받는다. 아래 example은 어떻게 0.5초마다 부르는지에 대한 코드이다.

var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?

func addPeriodicTimeObserver() {
// 0.5 초마다 노티ㄱ ㄱ
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
// 플레이어 UI 업데이트
}
}

func removePeriodicTimeObserver() {
if let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}

Boundary Observation

time을 관찰할 수 있는 다른 방법은 “boundary”이다. 미디어 타임라인 내에서 여러 관심 포인트들을 정의하고, 재생 중에 해당 시간이 경과하면 프레임워크에서 호출한다. Boundary observation은 periodic 보다는 덜 빈번하게 발생하는데, 이는 특정한 상황에서만 유용하다. 예를들어, 재생 컨트롤이 없는 비디오를 표시하고, 해당 시간이 경과할때 UI 요소를 동기화 시킨다거나, 보충 내용을 표시하려는 경우 사용할 수 있다.

boundary time을 관찰하기 위해, player의 addBoundaryTimeObserver(forTimes:queue:usingBlock:)을 사용한다. 이 메서드는 NSValue 객체를 CMTime값으로 래핑한 배열로 boundary time을 걸정하고, serial dispatchqueue와 callback block 을 받는다. 아래 예시는 1/4 재생에 대한 경계시간을 정의하는 방법이다.

var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?

func addBoundaryTimeObserver() {
// 에셋의 duration을 1/4로 쪼갬
let interval = CMTimeMultiplyByFloat64(asset.duration, 0.25)
var currentTime = kCMTimeZero
var times = [NSValue]()

// boundary time 계산
while currentTime < asset.durtaion {
currentTime = currentTime + interval
times.append(NSValue(time: currentTime))
}

timeObserverToken = player.addBoundaryTimeObserver(
forTimes: times,
queue: .main
) {
// UI 업데이트
}

}

func removeBoundaryTimeObserver() {
if let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}

Seeking Media

일반적으로 그냥 쭉 진행되는 재생에 더해서, 유저는 nonlinear 하게 수동적으로 빠르게 미디어의 원하는 위치를 탐색하고 싶을수도 있다. AVKit은 자동으로 scribbing control을 제공한다. 그러나, custom player를 만들면, 이 기능(seeking)을 직접 만들어야 한다. AVKit을 사용하더라도, 미디어의 다양한 위치로 빨리 skip하는 추가적인 user interface를 구현하고 싶을 수 있다. (ex. tableview, collectionview)

AVPlayer와 AVPlayerItem의 메서드를 이용해 여러 방법으로 seek 할 수 있다. 가장 일반적인 방법은 player의 seek(to time:) 메서드를 이용하는 것이다. 여기에는 목적지 CMTime을 넘기면 된다.

// Seek to the 2 minute mark
let time = CMTime(value: 120, timescale: 1)
player.seek(to time:)

seek(to: time)메서드는 화면에서 가장 빨리 편하게 seek할 수 있는 함수이다. 그러나 정확성 보다는 속도에 더 초점이 맞춰져 있다. 이 말은, player가 요청한 시간이랑 조금 다르게 움직일 수도 있다는 것이다. 만약 정확한 seeking을 원하면, seek(to: time. toleranceBefore:toleranceAfter:)를 사용해라. 이를 통해 목표 시간(전후)에서 허용되는 편차의 양을 지정할 수 있다. 만약 정확한 탐색 동작을 제공해야 한다면 zero tolerance를 하면된다.

seek(to time: toleranceBefore: toleranceAfter:) 메서드는 작거나 0 값으로 오차를 설정하면, 앱 seeking 동작에 decoding delay가 일어날 수 있다.

--

--