iOS 앱에서 동영상 재생 화면을 구현할 때, AVPlayerLayer를 어떻게 사용하는지가 사용자 경험에 큰 영향을 미칩니다.
AVPlayerLayer는 Core Animation을 통해 비디오를 직접 렌더링하는데,
이를 어떻게 활용하느냐에 따라 화면 전환과 애니메이션 성능이 달라질 수 있습니다.
이 글에서는 AVPlayerLayer를 이용해 동영상을 표시하는 두 가지 방법을 소개하고, 각각의 장단점을 비교하겠습니다.
AVPlayer는 비디오 파일을 로드하고 재생하는데 필요한 기능을 제공합니다.
하지만, 실제로 비디오를 화면에 렌더링하기 위해서는 AVPlayerLayer가 필요합니다.
AVPlayerLayer는 Core Animation을 사용해 비디오를 렌더링합니다.
또한, 비율(aspect ratio)과 정렬(videoGravity) 설정을 지원하여 UIView의 영역에 맞게 조정할 수 있습니다.
비디오를 그릴 때 사용할 Core Image 필터나 애니메이션 효과를 적용할 수 있어 추가적인 시각 효과도 제공합니다.
AVPlayerLayer는 CALayer를 상속합니다.
따라서 AVPlayerLayer를 UIViewController의 view에 addSubLayer하여 미디어를 재생할 layer를 추가할 수 있습니다.
그런데, CALayer는 Autolayout을 지원하지 않기 때문에 layout의 변경이 필요한 경우 수동으로 frame 값을 조정해 주어야 합니다.
그래서 viewDidLayoutSubviews()
함수에서 AVplayerLayer의 frame을 수동으로 변경시켜 주어야 합니다.
import UIKit
import AVFoundation
final class ContentViewController: UIViewController {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
override func viewDidLoad() {
setupPlayer()
}
func setupPlayer() {
guard let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4") else { return }
player = AVPlayer(url: consume url)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = view.bounds
playerLayer?.videoGravity = .resizeAspect
if let playerLayer {
view.layer.addSublayer(playerLayer)
}
player?.play()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerLayer?.frame = view.bounds
}
}
UIView의 기본 CALayer는 더 전문화된 기능을 사용하기 위해 다른 타입의 layer로 치환할 수 있습니다.
예를 들면 CAShapeLayer, CAGradientLayer, CAEmitterLayer 등을 사용하여 더 화려한 View를 그릴 수 있죠.
그리고 AVPlayerLayer도 CALayer를 상속하는 만큼 동일하게 치환할 수 있습니다.
이렇게 치환하게 되면 AVPlayerLayer가 UIView에 적용된 AutoLayout을 그대로 따라가게 됩니다.
layout이 바뀔 때마다 수동으로 frame을 조정할 필요가 없기 때문에 가독성이 높고 유지 보수가 편리한 코드가 만들어 집니다.
import UIKit
import AVFoundation
final class AVPlayerView: UIView {
override class var layerClass: AnyClass { AVPlayerLayer.self }
private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
private(set) var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
override init(frame: CGRect) {
super.init(frame: frame)
playerLayer.videoGravity = .resizeAspect
}
required init?(coder: NSCoder) {
super.init(coder: coder)
playerLayer.videoGravity = .resizeAspect
}
func loadVideo(from url: URL) {
player = AVPlayer(url: url)
player?.play()
}
}
final class ContentViewController: UIViewController {
private var avPlayerView: AVPlayerView?
override func viewDidLoad() {
super.viewDidLoad()
setUpPlayer()
}
func setUpPlayer() {
let avPlayerView = AVPlayerView()
avPlayerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(avPlayerView)
NSLayoutConstraint.activate([
avPlayerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
avPlayerView.topAnchor.constraint(equalTo: view.topAnchor),
avPlayerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
avPlayerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4") {
avPlayerView.loadVideo(from: consume url)
}
self.avPlayerView = avPlayerView
}
}
두 가지 방법 중 더 권장되는 방식은 두 번째 방법입니다.
첫 번째 방법은 viewDidLayoutSubviews()
에서 frame을 수동으로 조정해야 하므로 성능에 부담이 될 수 있습니다.
layout 계산을 수동으로 처리해야 하며, 반복적으로 호출되고, 최적화가 되지 않습니다.
하지만 NSLayoutConstraint를 활용해 AutoLayout을 적용한 경우는 위의 경우보다 더 좋은 성능을 제공합니다.
또한 코드가 간결해지고 유지 보수가 간편해집니다.
그리고, 위에 첨부한 영상을 자세히 보면 두 영상의 애니메이션이 다름을 확인할 수 있습니다.
첫 번째 영상의 애니메이션은 무언가가 밀어 넣는듯한 느낌이 있지만, 두 번째 영상은 자연스럽게 전환이 됩니다.
AutoLayout을 활용했을 때 훨씬 부드럽고 자연스러운 애니메이션 효과가 나타남을 확인할 수 있습니다.
우리는 UIKit에서 AVPlayerLayer, 또는 SwiftUI에서 VideoPlayer를 사용할 수 있습니다.
그러나 단순한 View를 구성하는 경우가 아니라면 성능과 커스터마이징의 용이함으로 인해 AVPlayerLayer를 사용하는게 유리합니다.