스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
더욱 응답성 높은 미디어 앱 만들기
AVFoundation을 사용하여 사용자가 로딩 스피너가 아닌 여러분의 미디어 앱 콘텐츠에 더욱 집중할 수 있도록 하는 방법을 확인하세요. 풍부한 오디오 비주얼 구성을 만들고, 오디오 비주얼 자산을 로드하고, 미디어 썸네일을 준비하는 동시에 앱에서 응답성이 높고 유연한 인터페이스를 지원하는 방법을 보여드립니다. I/O 프로세스가 병렬로 진행되는 동안 앱의 메인 스레드에서 이러한 작업을 수행하는 방법을 알아보고, 맞춤형 저장 공간에서 데이터를 로드할 때 최고의 재생 성능을 얻는 방법 등을 확인할 수 있습니다. 이 세션을 최대한 활용하려면 WWDC21에서 ‘What's new in AVFoundation(AVFoundation의 새로운 기능)'을 시청하시기 바랍니다.
리소스
관련 비디오
WWDC21
-
다운로드
♪ ♪ 안녕하세요 Jeremy입니다 AVFoundation을 이용해 더 즉각 반응하는 앱을 만드는 방법을 알아 보려고 합니다 앱에서 미디어 자산을 이용할 때 재생 그 이상을 하고 싶으실 텐데요 썸네일도 보여주고 싶고 미디어를 새 구성과 결합하고도 싶고 자산에서 정보를 얻고도 싶으실 겁니다 그러려면 영상과 같은 대용량 파일과 데이터 로딩이 필요한데 완료에 시간이 좀 걸립니다 하지만 메인 스레드에서 비동기로 작업한다면 앱에 대기 시간이 쉽게 발생할 수 있죠 앱을 반응형으로 유지하려면 데이터 비동기 로딩과 완료 시 UI 업데이트가 최고의 방안입니다 AVFoundation에서는 쉽게 가능합니다 자, 오늘 말씀드릴 내용은 다음과 같습니다 먼저, AVFoundation 내 새 비동기 API를 소개할게요 지난 해 도입한 비동기 로드 메소드로 자산 검사에 대한 최신 정보를 알려드리겠습니다 AVAssetResourceLoader로 로컬 및 캐시된 미디어에 대해 커스텀 데이터 로딩을 최적화하는 방식도 알아보죠 먼저, 새 비동기 API부터 살펴 봅시다 AVAssetImageGenerator를 통한 영상 스틸 이미지 그래빙은 썸네일을 만들 수 있는 최고의 방법인데요 하지만 이미지 생성이 바로 되지 않죠
이미지 제너레이터는 이미지 생성을 위해 영상 파일 프레임 데이터를 로딩해야 합니다 원격 서버나 인터넷에 저장된 미디어의 경우에는 로딩 속도가 훨씬 느리죠 그래서 이미지 생성 방식이 중요한 겁니다 메인 스레드에서 copyCGImage와 같이 데이터를 비동기로 로딩하는 메소드를 사용하면 UI는 영상 로딩을 기다리려고 잠시 멈출 수 있습니다 올해 image(at: time) 메소드가 추가되었는데요 비동기나 대기를 사용해 호출 스레드를 풀어주죠 이미지 제너레이터가 데이터를 로딩하는 동안요 이미지 제너레이터는 자산 내 이미지와 실제 시간으로 이루어진 튜플을 반환합니다 실제 시간이 요청 시간에 따라 다양한 이유가 몇 가지 있는데요 하지만 이미지만 원할 경우 .image 프로퍼티로 직접 이미지 액세스가 가능하죠 압축 영상 프레임 일부는 비교적 로딩하기 쉬운데요 I 프레임은 독립적인 디코딩이 가능하나 다른 프레임은 디코잉되는 근처 프레임에 의존합니다 요청 시간을 위해 제너레이터는 이미지 생성에 기본적으로 가장 가까운 I 프레임을 사용할 겁니다 요청 시간 동안 정확한 프레임을 얻기 위해 허용 오차를 0으로 설정하고 싶으실 겁니다 하지만 저 프레임은 제너레이터가 로딩해야 하는 근처 프레임에 의존할 거라는 사실을 기억하세요 대신 찾고 있는 결과를 줄 수 있는 광범위한 허용 오차 설정을 고려하세요 광범위한 오차는 선택 가능한 프레임을 더 줌으로써 이미지 제너레이터의 데이터 로딩 최소화를 돕죠 로딩해야 할 프레임이 적어지면 이미지 반환이 더 빨라집니다
자산에서 연속 이미지를 여러 번 얻기 위해 이미지 제너레이터는 generateCGImagesAsynchronously(forTimes:)를 가집니다 하지만 Swift에서 사용 시 어느 정도 유의하셔야 하죠 올해 images(for: times) 메소드를 추가했는데요 이제 CMTime 배열을 취하기 때문에 먼저 NSValue로 매핑하지 않아도 됩니다 AsyncSequence를 사용한 결과도 제공하고요 Swift 시퀀스는 for-in 루프로 항목을 반복합니다 한 번에 준비되지 않는 항목의 시퀀스를 위해 비동기 시퀀스는 각 반복마다 다음 요소를 기다리게 해주죠 성공적으로 생성된 이미지 각각에 대해 해당 결과값에는 기존 요청 시간과 이미지 옆에 실제 시간이 포함됩니다 실패할 경우의 결과값은 이유를 설명하는 오류를 가지죠
이미지에만 관심이 있다면 값에 직접 액세스할 수 있는 프로퍼티가 나타납니다 생성이 실패할 경우 오류를 던질 수도 있죠 비동기 시퀀스에 관심이 있으시다면 다음 세션을 확인해 보세요 이미지 생성과 같은 태스크의 경우 로딩 데이터 포함 방법을 보기가 더 쉽습니다 하지만 AVFoundation의 동기 영역 일부는 문제 지점으로 골라내기가 더 어렵습니다 AVMutableComposition도 그런 영역 중 하나죠 구성 내 레퍼런스를 추가하려면 자산에 대한 삽입 시간 범위엔 자산 트랙에 대한 정보가 필요합니다 동시에 트랙을 검사하기 때문에 트랙이 로딩되어 있지 않은 경우 새 구성 트랙을 만들기 위해 동시에 로딩될 겁니다
이전에는 구성에 삽입하기 전 자산 트랙 로딩을 기다리는 게 솔루션이었겠지만 올해 insertTimeRange 비동기 버전이 도입됐기 때문에 이제 필요할 경우 트랙을 비동기 로딩할 수 있습니다
영상 구성과 가변적 영상 구성에는 자산 프로퍼티도 로딩해야 하는 또다른 메소드가 있는데요 올해 'propertiesOf asset' 생성자와 isValid(for:timeRange:) 메소드도 비동기 방식을 갖게 됐습니다 트랙과 자산 지속 시간을 비동기 로딩하기 때문에 둘 중 하나를 미리 로딩할 필요가 없습니다 이들은 필요한 프로퍼티를 비동기 로딩함으로써 자산과의 상호 작용을 더 쉽게 만들죠 하지만 자산 프로퍼티를 직접 로딩해야 할 경우를 위해 비동기 자산 검사를 한 번 살펴 봅시다 자산 프로퍼티 검사 방식에는 두 가지가 있잖아요 AVFoundation이 도입되었을 땐 비동기 키값 로딩이 최선이었습니다 그리고 지난 해 async load(_:)가 도입됐죠 이건 로딩될 프로퍼티 식별에 타임세이프 키를 사용합니다 기존에는 하드 코딩 문자열을 키로 사용했고요 이런 문자열 키 속 타이포는 찾기가 더 어렵죠 키를 잘못 입력하면 비동기 로딩이 막힙니다 프로퍼티가 나중에 사용될 때도 로딩을 블로킹할 거고요
새 프로퍼티를 로딩될 키에 추가하는 걸 잊거나 비동기 로딩하는 것 자체를 잊어버리기도 쉽죠 이런 이유로 비동기 로드에 Swift 내 비동기 키값 로딩과 동기 프로퍼티 사용을 권장하지 않고 있습니다 비동기 로드는 타이포를 막고자 타입세이프 식별자를 사용합니다 프로퍼티 값을 요청대로 직접 반환하죠 로딩되지 않은 프로퍼티 액세스를 막기 위해서요 그리고 이 모든 것은 컴파일 타임에 확인되기에 새로운 I/O 바운드 퍼포먼스 문제 발생이 예방됩니다 비동기 로드는 AVAsset, AVAssetTrack AVMetadataItem, 기타 하위 클래스에서만 권장되는 비동기 프로퍼티 검사 방식입니다 그러나 이 클래스들 중 일부는 여전히 동기 프로퍼티 검사도 제공합니다 메모리에서 프로퍼티 데이터를 이미 이용할 수 있기 때문이죠 왜 그런지 가변 구성을 다시 한 번 살펴 봅시다
기존 영상 트랙 2개의 세그먼트 연결을 위해 가변 구성을 사용할 건데요 우선 빈 구성 만들기와 빈 영상 트랙 추가하기로 시작해 보죠 그 다음 첫 영상 트랙의 일부를 구성 트랙에 동시에 삽입할 수 있습니다 하지만 이 단계에서는 어떤 데이터도 로딩되지 않죠 대신 원하는 트랙을 지정하는 새 트랙 세그먼트를 추가합니다
두 번째 트랙의 일부도 동일한 방식으로 진행합니다
구성은 파일이 아닌 인메모리 구조의 지원을 받아 프로퍼티 동기 검사를 안전하게 할 수 있습니다 먼저 로딩할 필요 없이요 바로 이런 이유로 동기 프로퍼티 검사가 이 클래스에서도 가능하며 모든 클래스에서 비동기 검사에 비동기 로드를 사용할 겁니다
AVFoundation의 새 비동기 메소드 모두 미디어 데이터 로딩 중 블로킹 예방을 쉽게 하죠 하지만 앱에 동시성을 도입하는 건 까다로울 수 있습니다 동시성과 AVFoundation의 비동기 로드 마이그레이션은 다음 WWDC21의 두 세션으로 시작해 보시길 추천드립니다 마지막으로 자산에 대한 커스텀 데이터 로딩을 최적화하는 방법을 살펴 보죠 먼저 AVAsset의 기본적인 데이터 로딩 방식을 봅시다 URL로 AVAsset을 만들 때 미디어는 네트워크에 있거나 로컬 장치에 저장될 수 있습니다 네트워크에 있다면 AVAsset은 부드러운 재생을 위해 특정 양의 데이터를 역동적으로 캐시에 저장합니다 로컬 장치에 저장된 경우 캐시를 건너뛰고 재생에 필요한 데이터를 로딩할 수 있고요 간혹 다이렉트 포인터를 미디어에 직접 주는 것이 불가능할 수 있습니다 커스텀 프로젝트 파일 내부에 mp4의 로바이트를 저장한다면 AVAsset는 AVAssetResourceLoader를 사용할 수 있습니다 리소스 로더는 자산에 특수 로딩 방식의 미디어에서 임의 바이트를 요청하는 방법을 제공합니다 하지만 자산은 더이상 데이터 읽기를 처리하지 않아 각 청크가 로딩되는 시간이 얼마나 걸릴지 예상할 수 없죠 미디어 액세스에 네트워크 통신이 포함된다 치고 재생 준비 전 데이터를 캐시에 저장할 때까지 기다립니다 미디어를 로컬 장치에서 이용 가능할 경우 이제 리소스 로더에 대해 entireLengthAvailableOnDemand를 작동시킬 수 있습니다 이 플래그는 자산에게 요청대로 데이터를 받을 것이라 말해서 캐싱을 계속할 수 있습니다
로컬 미디어에서는 entireLengthAvailableOnDemand가 재생 동안 앱 메모리 사용량을 줄일 수 있습니다 추가 데이터를 캐시에 저장할 필요가 없어서요 재생 시작에 걸리는 시간도 줄여줄 수 있는데요 캐시가 먼저 채워질 때까지 자산이 기다릴 필요가 없거든요 하지만 이 플래그 작동 시 유의하셔야 합니다 로딩에 네트워크 파일 저장 등 네트워크 실행이 필요한 경우 적당히 재생은 되지만 불안하거든요
여기까지 세 번째 내용이었습니다 이제 몇 가지 사항과 함께 전체를 정리해 보죠
미디어 작업 시 로딩을 지속하면서 반응형을 유지하려면 비동기/대기를 사용하세요 이미지 제너레이터 사용 시 허용 오차를 늘리세요 로컬 장치 미디어에서 리소스 로더를 사용한다면 퍼포먼스 향상에 도움이 되는 모든 것을 자동시키세요 제 세션은 여기까지입니다 시청해 주셔서 감사합니다 WWDC 22를 즐겨보세요
-
-
1:41 - Generate a thumbnail
func thumbnail() async throws -> UIImage { let generator = AVAssetImageGenerator(asset: asset) generator.requestedTimeToleranceBefore = .zero generator.requestedTimeToleranceAfter = CMTime(seconds: 3, preferredTimescale: 600) let thumbnail = try await generator.image(at: time).image return UIImage(cgImage: thumbnail) }
-
2:56 - Generate a series of thumbnails
func timelineThumbnails(for times: [CMTime]) async { for await result in generator.images(for: times) { switch result { case .success(requestedTime: let requestedTime, image: let image, actualTime: _): updateThumbnail(for: requestedTime, with: image) case .failed(requestedTime: let requestedTime, error: _): updateThumbnail(for: requestedTime, with: placeholder) } } }
-
3:49 - Generate a series of thumbnails
func timelineThumbnails(for times: [CMTime]) async { for await result in generator.images(for: times) { updateThumbnail(for: result.requestedTime, with: (try? result.image) ?? placeholder) } }
-
4:40 - AVMutableComposition
let composition = AVMutableComposition() try await composition.insertTimeRange(timeRange, of: asset, at: startTime)
-
4:57 - AVVideoComposition
let videoComposition = try await AVVideoComposition .videoComposition(withPropertiesOf: asset) try await videoComposition.isValid(for: asset, timeRange: range, validationDelegate: delegate)
-
5:33 - Asset inspection
asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) { guard asset.statusOfValue(forKey: "duration", error: &error) == .loaded else { ... } guard asset.statusOfValue(forKey: "tracks", error: &error) == .loaded else { ... } myFunction(thatUses: asset.duration, and: asset.tracks) } let (duration, tracks) = try await asset.load(.duration, .tracks) myFunction(thatUses: duration, and: tracks)
-
7:06 - Synchronously insert track segments into a composition
// videoTrack1: AVAssetTrack, videoTrack2: AVAssetTrack // Create a composition and add an empty track let composition = AVMutableComposition() guard let compositionTrack = composition .addMutableTrack(withMediaType: .video, preferredTrackID: 1) else { return } // Append the first 5 seconds of track 1 try compositionTrack .insertTimeRange(firstFiveSeconds, of: videoTrack1, at: .zero) // Append the first 5 seconds of track 2 try compositionTrack .insertTimeRange(firstFiveSeconds, of: videoTrack2, at: fiveSeconds) myFunction(thatUses: composition.duration, and: composition.tracks)
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.