Swift. Video Streaming에 대해서 알아봅니다.

Video Streaming Tutorial for iOS: Getting Started, Part 1

Posted by MinJun Ju on Tuesday, October 30, 2018 Tags: Swift Multi-media Raywenderlich   19 minute read

Table of contents


Video Streaming Tutorial for iOS: Getting Started 에서 필요한 부분을 의역 했습니다.

이 비디오 스트리밍 튜토리얼 에서 AVKitAVFoundation 프레임 워크를 사용하는 비디오 스트리밍 앱을 어떻게 만드는지 배울 것입니다.

iOS 앱으로 계속 작업했고 자신이 꾀 영리하다고 생각하고 있습니다.

좋습니다. 아마 기본적인 네트워킹을 할수 있죠? 그러면 JSON을 가져와서 텍스트와 이미지가 있는 테이블뷰에 데이터를 구성할수 있습니다.

분명히 좋은 성취 목록이지만.

이것을 할수 있나요?

이제 앱의 수준을 올리고 비디오 스트리밍을 어떻게 추가하는지 배우게 됩니다.

이번 시간에, 해외에 있는 블로거들을 위한 여행용 앱을 만들 것입니다. 어떤 사람들은 여행에 관한 영화를 만들고 싶어 하고 어떤 사람은 편안한 침대에서 이들의 경험을 즐기고 싶어합니다.

이 두가지 꿈을 실현 시켜 볼것입니다.

이 과정에서 AVKit, AVFoundation 프레임 워크의 기본을 배울 것입니다.


Getting Started

Note: 이 튜토리얼을 진행하면서 실험 할때, 브레이크 포인트를 끄거나 스킵 버튼을 많이 누를 준비를 해야합니다. AudioHAL_Client, CUICatalog로 시작 하는 경고를 본다면 이들을 무시하고 계속 진행해도 괜찮습니다.

여기에서 시작 프로젝트를 다운받고 프로젝트를 열고 VideoFeedViewController.swift로 이동합니다.


Introduction to AVKit

개발시 유용한 팁: 가능한 가장 높은 추상화를 선호합니다. 하지만 사용해온것이 더이상 적합하지 않으면 낮은 수준으로 내려갈수 있습니다. 이 방침에 따라서, 가장 높은 수준의 비디오 프레임 워크에서 여행을 시작합니다.

AVKit은 video와 상호작용 하기위해 제공된 모든 필요한 UI와 AVFoundation의 상위에 위치합니다.

앱을 빌드하고 실행하면 비디오를 시청하기 위한 비디오가 있는 테이블뷰를 볼수 있습니다.


유저가 셀중 하나를 탭할때 비디오를 실행하여 보여주는것이 목표입니다.


Adding Local Playback

이들은 사실 두가지 타입의 재생할수 있는 비디오가 있습니다. 첫번째는 현재 아이폰의 하드 드라이브에 놓여 있는 타입이고 두번째는 서버에서 스트리밍하는 동영상을 재생하는 방법을 배울 것입니다.

시작하려면 VideoFeedViewController.swift로 이동하고 import UIKit 아래에 다음을 추가합니다.

import AVKit

이것의 아래를 보면, 이미 테이블 뷰와 정의된 Video의 배열을 볼수 있습니다. 이것은 기존 테이블뷰에 데이터가 얼마나 채워져 있나 입니다. 이 비디오들은 비디오 관리 class에서 온 것 처럼 보입니다. 이들을 어떻게 가져오는지 AppDelegate.swift에서 볼수 있습니다.

다음으로 tableView(_ tableView:didSelectRowAt:)를 찾을때까지 스크롤을 내리고 다음 코드를 추가합니다.

//1
let video = videos[indexPath.row]

//2
let videoURL = video.url
let player = AVPlayer(url: videoURL)
  1. 먼저 비디오 모델 객체를 가져야합니다.
  2. 모든 Video객체는 비디오 파일의 경로를 나타내는 url 속성을 가집니다. url을 가지고 AVPlayer 객체를 생성합니다.

AVPlayer은 iOS 에서 비디오 플레이의 중심 입니다.

player 객체는 비디오를 시작하고 멈출수 있고, 이들의 재생 속도를 변경하고 볼륨도 높이고 내릴수 있습니다. 한번에 하나의 미디어 재생을 관리할수 있는 컨트롤러 개체로 생각할수 있습니다.

메소드의 끝에 뷰 컨트롤러 설정하기 위해 다음 코드를 추가합니다.

let playerViewController = AVPlayerViewController()
playerViewController.player = player

present(playerViewController, animated: true) {
  player.play()
}

AVPlayerViewController는 유용한 player객체가 필요한 편리한 뷰 컨트롤러 입니다. 일단 이것이 하나 있으면 풀화면 비디오 플레이어로 화면에 표시합니다.

화면 표시 애니메이션이 끝나면 비디오를 시작하기 위해 play()를 호출합니다.

그리고 빌드하고 실행하여 이것이 어떻게 동작하는지 봅니다.

이 뷰 컨트롤러는 기존 컨트롤러 세트를 보여줍니다. 그리고 이 뷰 컨트롤러는 플레이 버튼, 음소거 버튼, 15초 앞, 뒤 이동 버튼을 포함하고 있습니다.

Adding Remote Playback

정말 쉽습니다. 원격 URL에서 비디오 재생을 추가하는것은 어때? 그것은 확실히 힘들거야..

AppDelegate.swift로 가고 feed.videos가 설정된 곳을 찾습니다. 디바이스의 하드디스크에 있는 비디오를 로드 하신 대신, 다음으로 코드를 교체하여 모든 비디오를 가져옵니다.

feed.videos = Video.allVideos()

이게 다입니다. Video.swift로 가서 allVideos()는 단순하게 한개의 추가 비디오를 로딩하는것을 볼수 있습니다. 유일한 차이점은 이것의 url 속성이 파일의 경로 대신 웹 주소를 라는것 입니다.

앱을 빌드하고 실행하여 キツネ村 (kitsune-mura) 또는 Fox Village 비디오를 찾기 위해 피드의 스크롤을 아래로 내립니다.

이것은 AVPlayerViewController의 아름 다움 입니다 ; 필요한것은 URL 뿐이고 실행하는것입니다.

사실은 allVideos()로 가서 다음 라인을

let videoURLString = 
  "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.mp4"

다음과같이 교체합니다.

let videoURLString = 
1.   "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.m3u8"aa

앱을 빌드하고 실행하면 여전히 동작하는 fox village 비디오를 볼수 있습니다.

유일한 차이점은 두번쨰 URL은 HLS Livestream을 나타낸다는 것 입니다. HLS livestreaming은 비디오를 10초짜리 뭉치(chunks)로 나누어 동작시키는 것입니다. 이들은 매 시간 클라이언트에게 한 덩어리로 전달되어 집니다. Gif 예제 에서 본것과 같이 MP4 버전을 사용할때 보다 비디오가 훨씬 빠르게 재생되기 시작 했습니다.


Adding a Looping Video Preview

오른쪽 하단에 검은색 뷰가 있는것을 알아 차렸을 것입니다. 그 검정색 뷰를 둥둥 떠다니는 비디오 플레이어로 사용할것입니다. 이것의 목적은 사용자가 이러한 모든 비디오에 흥미를 느끼도록 끊임없이 클립이 설정되어 비디오르는 재생하기 위함힙니다.

그 후 소리를 키는 탭 재스쳐나 2배 속도로 변경하는 더블탭 같은 사용자화된 제스쳐를 추가하는게 필요합니다. 어떻게 이것들이 동작하는지에 대해 구조체적으로 조작하고 싶을때, 자신만의 비디오 뷰를 작성하는게 좋습니다.

VideoFeedViewController.swift로 돌아가서 속성 정의를 확인합니다. 이 클레스의 겉모습이 이미 존재하고 비디오 클립의 세트가 생성되어 있음을 알수 있습니다.

이것을 사용하는게 일입니다.


Introduction to AVFoundation

AVFoundation이 어렵게 느껴질때, 함께 다루어야 하는 대부분의 객체는 여전히 높은 수준의 추상화가 되어 있습니다.

익숙 해져야 하는 주요 class는 다음과 같습니다.

  • AVPlayerLayer : 이 특정 CALayer 하위 클레스는 주어진 AVplayer 객체의 재생화면을 볼수 있습니다.
  • AVAsset: media asset의 정적 표현(static representations of a media asset) 입니다. asset 객체는 기간, 생성 날짜 같은 정보를 포함합니다.
  • AVPlayerItem: AVAsset의 동적 대응(dynamic counterpart) 관계 입니다. 이 객체는 재생 가능한 비디오의 현재 상태를 나타냅니다. 이것은 AVPlayer에게 제공해야 하는것입니다.

AVFoundation은 큰 몇개의 클레스를 넘어서는 큰 프레임 워크 입니다. 운 좋게 이것은 looping 비디오 플레이어를 생성하기 위해 필요한 모든것입니다.

차례로 이들 각각 되돌아 갈것입니다. 그리고 이들과 어떤것들을 기억하는것에 대해서 걱정하지마세요.


Writing a Custom Video View with AVPlayerLayer

첫번째 클레스는 AVPlayerLayer 입니다. 이 CALayer의 하위 클레스는 다른 레이어와 같습니다: 이것의 contents 속성에 있는 것을 화면에 보여줍니다.

이 레이어는 player속성을 통해 주어진 비디오에서 프레임과 함께 그것의 콘텐츠를 채우는 행동을 합니다.

VideoPlayerView.swfit로 가서 비디오를 보여주기 위한 빈 뷰를 찾을수 있습니다.

먼저 해야하는것은 다음을 추가합니다

import AVFoundation

좋은 시작입니다. 이제 AVPlayerLayer를 섞을수 있습니다.

UIView는 사실 CALayer를 렙핑한 것입니다. 이것은 터치 처리, 접근이 쉬운 기능을 제공하지만 이것은 하위 클레스는 아닙니다. 대신 기본 레이어 속성을 소유하고 관리합니다. 한가지 멋진 트릭은 원하는 뷰의 하위 클레스가 소유하길 원하는 layer 타입을 지정할수 있다는 것입니다.

다으 속성을 override 하여 이 클레에 기본 CALayer 대신 AVPlayerLayer를 사용해야 한다고 이야기 알려줍니다.

override class var layerClass: AnyClass {
  return AVPlayerLayer.self
}

뷰에 player 레이어를 랩핑하여 player 속성을 드러내는게 필요합니다.

이것을 하려면 먼저 연산 프로퍼티를 추가하여 항상 하위 클래스를 캐스팅할 필요가 없게 만듭니다.

var playerLayer: AVPlayerLayer {
  return layer as! AVPlayerLayer
}

다음 실제 getter, setter가 있는 player를 추가합니다.

var player: AVPlayer? {
  get {
    return playerLayer.player
  }

  set {
    playerLayer.player = newValue
  }
}

단지 playerLayer의 player 객체를 setting, getting로 설정했습니다. 이 UIView는 단순히 매개자 입니다.

다시 한번 player 자체와 상호작용을 시작할때 마술을 부립니다.

앱을 빌드하고 실행합니다.


아직 새로운것을 볼수는 없지만 목적지의 중간입니다.


Writing the Looping Video View

VideoLooperview.swift 다시 살피고 VideoPlayerView를 잘 사용하기 위한 준비를 합니다. 이 클레스는 이미 VideoClip의 세트를 가지고 VideoPlayerView 속성을 초기화 합니다.

해야하는 것은 클립을 가져오고 지속적인 루프에서 이들을 어떻게 재생시킬지 이해하는 것입니다.

시작하려면 다음과 같은 player 속성을 추가합니다.

private let player = AVQueuePlayer()

안목이 있다면 이것은 평범한 AVPlayer 인스턴스가 아니라는 것을 알수 있습니다. 맞습니다. 이것은 AVQueuePlayer라고 불리는 특정 하위클레스 입니다. 이름에서 적절하게 추측할수 있는것처럼 이 클레스는 재생 아이템들의 큐를 제공합니다.

player 설정을 시작하기 위해 다음 매소드를 추가합니다.

private func initializePlayer() {
  videoPlayerView.player = player
}

기초인 AVPlayerLayer로 연결하기 위해 playervideoPlayerView로 전달합니다.

이제 player로 비디오 클립의 목록을 추가합니다. 그후 이들을 재생할수 있습니다. 다음 매소드를 추가합니다.

private func addAllVideosToPlayer() {
  for video in clips {
    //1
    let asset = AVURLAsset(url: video.url)
    let item = AVPlayerItem(asset: asset)

    //2
    player.insert(item, after: player.items().last)
  }
}

모든 클립들을 통하여 반복합니다. 다음은 각각의 설명입니다.

  1. 각 비디오 클립 객체의 URL에서 AVURLAsset를 생성 합니다.
  2. 그후 player가 재생을 관리할수 있는 asset과 함께 AVPlayerItem을 생성합니다.
  3. 마지막으로 각 아이템을 큐로 추가하기 위해 insert(_after:)매소드를 사용합니다.

initializePlayer()로 돌아가고 다음 매소드를 호출합니다.

addAllVideosToPlayer()

그후 player set을 가지고, 어떤것을 구성할수 있는 시간입니다.

이것을 하기위해 다음 두개의 라인을 추가 합니다.

player.volume = 0.0
player.play()

이것은 기본적으로 반복하는 클립이 자동 재생, 오디오 off를 설정합니다.

마지막으로 이 메소드를 작업하는 곳에서 호출해야합니다.

init(clips:) 매소드로 가서 하단에 다음을 추가합니다.

initializePlayer()

앱을 빌드하고 실행하면 잘 작동하는 clip show를 볼수 있습니다.

불행히도, 마지막 클립은 재생될떄 끝납니다. 이 비디오 플레이어는 검정색으로 사라집니다.


Doing the Actual Looping

Apple은 AVPlayerLooper라고 불리는 새로운 클레스를 작성했었습니다. 이 클레스는 단일 player 아이템을 가져오고 반복적으로 이 아이템들을 재생하기 위하 필요한 모든 로직을 처리합니다. 불행히도 이것은 여기에서는 도움이 되지 않습니다.


원하는 것은 이들의 모든 비디오를 반복적으로 실행하는 것입니다. 어떤것을 수동적인 방법으로 해야 하는것 처럼 보입니다. 해야 하는 모든것은 player를 추적하고 현재 재생되는 아이템을 추적 관리 하는것입니다. 마지막 비디오를 얻었을때 모든 비디오 클립을 큐에 다시 추가합니다.

플레이어의 정보를 추적(keeping track of a player’s information)하는데 있어서 유일한 방법은 Key-Value Observing을 사용하는 것입니다.


이것은 Apple이 내놓은 API 중 하나 입니다. 그럼에도 불구하고 조금 주의를 기울이면 실시간으로 상태 변화를 관찰하는 강력한 방법입니다. KVO에 친숙하지 않다면, 여기에 빠른 정답이 있습니다. 기본 개념은 특정 속성의 값이 변경 될때마다 알림에 등록하는 것입니다. 우리의 경우 player’s의 현재 아이템이 변경되는 때를 알고 싶습니다. 알람을 받을 대마다 player는 다음 비디오로 진행한다는것을 알게 됩니다.

먼저 이전에 정의한 player 속성을 변경 합니다. file의 상단으로 이동하고 오래된 정의를 다음과같이 변경합니다.

@objc private let player = AVQueuePlayer()

유일한 차이는 @objc를 추가했습니다. 이것은 Swift에게 KVO같이 Objective-C로 속성을 드러낼것을 알립니다. Swift에서 Objective-C 보다 좋은 KVO를 사용하려면 - 옵저버로 참조를 유지하는게 필요합니다. player 이후에 다음 속성을 추가합니다.

private var token: NSKeyValueObservation?

관찰하는 속성을 시작 하려면 initializePlayer()로 돌아가서 끝에 다음을 추가합니다.

token = player.observe(\.currentItem) { [weak self] player, _ in
  if player.items().count == 1 {
    self?.addAllVideosToPlayer()
  }
}

player의 currentItem 속성이 변경될때마다 실행될 block을 등록합니다. 현재 비디오가 변경될때 player가 마지막 비디오로 이동 했는지 확인하려고 합니다. 만약 이동 했다면 큐로 모든 비디오 클릭을 다시 추가할 시간입니다.

앱을 빌드하고 실행하여 클립이 무한반복하는지 확인합니다.


Playing Video Efficiently

계속 진행하기 전에 알아야 하는것은 비디오를 재생하는것이 자원소모가 많은 작업 이라는 것입니다. 전체 화면 비디오를 재생을 시작할때에도 이 클립들은 계속 재성되어집니다.

이것을 수정하려면 먼저 VideoLooperView.swift의 하단에 다음 두개의 메소드를 추가합니다.

func pause() {
  player.pause()
}

func play() {
  player.play()
}

보이는 것과 같이, play(), pause() 매소드를 노출시키고 뷰의 player로 메시지를 전달합니다.

VideoFeedViewController.swift로 가서 viewWillDisappear(_:)를 찾고 거기에서 비디오 반복 재생을 정지하기 위해 다음을 추가합니다.

videoPreviewLooper.pause()

그후 viewWillAppear(_:)로 가고 유저가 돌아왔을때 다시 재생시키기 위해 호출을 일치시킵니다.

videoPreviewLooper.play()

앱을 빌드하고 실행합니다. 풀화면 비디오로 가세요. 미리보기는 유저가 feed로 돌아왔을때 중단된 곳에서 다시 시작 됩니다.


Playing with Player Controls

다음으로, 몇가지 제어를 추가할 시간입니다.

  1. 탭했을때 음소거 하지 않는 비디오
  2. 더블 탭했을때 1배속, 2배속 전환

이것들을 완수하기 위해 필요한 실제 매소드들과 함께 시작합니다. 먼저 VideoLooperView.swift로 돌아가고 play, pause 매소드를 추가했던 장소를 찾습니다.

0.0~1.0 소리를 전환하기 위한 단일 탭 handler를 추가합니다.

@objc func wasTapped() {
  player.volume = player.volume == 1.0 ? 0.0 : 1.0
}

다음으로 더블 탭 handler를 추가합니다.

@objc func wasDoubleTapped() {
  player.rate = player.rate == 1.0 ? 2.0 : 1.0
}

이것은 1.0 과 2.0 사이의 배속 전환에서도 비슷합니다.

다음으로 제스쳐들을 생성하기위한 매소드 정의를 추가합니다.

func addGestureRecognizers() {
  // 1
  let tap = UITapGestureRecognizer(target: self, action: #selector(VideoLooperView.wasTapped))
  let doubleTap = UITapGestureRecognizer(target: self,
                                         action: #selector(VideoLooperView.wasDoubleTapped))
  doubleTap.numberOfTapsRequired = 2
  
  // 2
  tap.require(toFail: doubleTap)

  // 3
  addGestureRecognizer(tap)
  addGestureRecognizer(doubleTap)
}

주석 단위로 설명합니다.

  1. 먼저, 두개의 제스쳐 인식기를 생성하고 이들의 각 매소드를 호출합니다. 또한 더블 탭은 두번의 탭이 필요하다고 이야기합니다
  2. 다음, 단일 탭은 더블탭이 발생하지 않는걸 보장하기 위해 기다립니다. 만약 이것을 하지 않으면 단일 탭은 항상 호출되어집니다.
  3. 그후, 제스쳐 인식기를 비디오 뷰에 추가합니다.

이것을 끝내기 위해 init(clips:)로 가고 하단에 다음 매소드를 호출을 추가합니다.

addGestureRecognizers()

다시 앱을 빌드하고 실행합니다. 클립의 속도와 볼륨을 다룰수 있는 탭과 더블탭을 사용할수 있습니다. 사용자화한 비디오 뷰와 그 사이에 대해 사용자화한 컨트롤을 추가하는게 얼마나 쉬운지 보여줍니다.

이제, 볼륨을 올리고 속도 조절을 할수 있습니다.


Trying Not to Steal the Show

마지막으로, 비디오가 있는 앱을 만들 것이라면, 유저에게 어떤 영향을 미칠지 생각 해보는것이 중요합니다.

너무 뻔하지만 얼마나 많은 소리가 나지 않는 비디오를 시작하고 음악을 껏나요?

이 첫번째 엉망진창인 세계를 경험해보지 않았다면, 가서 해드폰을 꼽아 보세요. 오.. 죄송합니다 2018년 버전은 블루투스로 해드폰을 연결합니다. 어떤 음악을 켜고 앱을 실행하세요. 그러면 오른쪽 하단의 반복하는 비디오는 아무 소리도 내지 않을뿐만 아니라 음악도 꺼집니다.

오른쪽 하단의 반복하는 비디오가 소리를 내지 않는것 대신 사용자에게 그들 자체의 음악을 끄는것을 허용 해야 한다는것이 나의 의견 입니다. 다행히도 AVAudioSession의 설정을 조절 하는 것으로 이것을 쉽게 고칠수 있습니다.

AppDelegate.swift로 가서 상단에 다음을 추가하고

import AVFoundation

다음으로 application(_:didFinishLaunchingWithOptions:)의 상단에 다음 라인을 추가합니다.

try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient,
                                                 mode: AVAudioSessionModeMoviePlayback,
                                                 options: [.mixWithOthers])

여기, 공유된 AVAudioSession에게 오디오가 AVAudioSessionCategoryAmbient 카테고리에 포함되라고 이야기 합니다. 기본은 AVAudioSessionCategorySoloAmbient 이고 이것은 다른 앱에서의 오디오를 차단 하는 이유 입니다.

또한 앱이 오디오를 영화 재생(movie playback)을 위해 사용하고 다른 소스와 소리가 섞여도 괜찮다고 이야기 합니다.

마지막으로 빌드하고 실행하여 음악기능이 잘 동작하는지 확인합니다.


비디오 앱을 완성 했습니다


Where to Go From Here?

이 튜토리얼의 상단의 링크를 사용하여 최종 프로젝트를 다운 받을수 있습니다.

로컬과 원격에 있는 비디오를 재생하는 앱을 성공적으로 만들었습니다.

비디오 재생에 대해 더 많은 것을 알고 싶다면 이것은 빙산의 일각 입니다. AVFoundation은 다음과 같은 것들을 처리할 수 있는 강대한 프레임 워크 입니다.

  • 내장된 카메라로 비디오를 캡쳐하고
  • 비디오 포맷들을 부호화 합니다
  • 실시간 비디오에 필터 적용

항상 그렇듯이, 특정 주제에 대해 더 자세히 알아보려고 할때 WWDC 비디오를 보는것이 좋습니다.

특히 이 튜토리얼에서 다루지 않은 AvplayerItem’s 상태 속성에 반응하는 것입니다. 원격 비디오의 상태를 관찰하는것은 네트워크 상태와 비디오의 재생 품질에 대해서 알수 있습니다.

이 상태 변화에 어떻게 반응할지 더 자세히 알아 보기 위해 Advances in AVFoundation Playback을 추천합니다.

또한 HLS Live Streaming에 대해서 언급했지만. 이 주제애 대해서는 많이 다루지 않았습니다. 이것에 흥미를 느낀다면, HTTP Live Streaming을 추천 합니다. 이 페이지에는 HLS Live Streaming에 대해서 더 많이 배우기 위해 사용할수 있는 리소스 목록들이 있습니다.