iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

Swiftでショートムービ(動画)を再生してみる

この記事でできるもの

f:id:yum_fishing:20200726213207g:plain

どうやるの?

動画を再生する方法は現在だと、3つ方法があります。この他にも MPMoviePlayerController が ありますが、iOS9以降は Deprecated です。今回はこの内の AVPlayer を使った実装を行っていき ます。

  • AVPlayerViewController
  • AVPlayer => 今回はこれで実装します!
    • 正しく書くと AVPlayerLayerlayerClass とする UIView のサブクラス
  • WKWebView

ざっくり手順

  1. 動画を表示するための UIView サブクラスを作成します。

    • layerClass を Override して AVPlayerLayer を使用できるようにセットします。
    • 公式ドキュメント
  2. 作成した動画 View で動画を再生できるようにします。

  3. 最後に AVPlayer を View に渡して動画を再生できるようにします。

    • AVPlayer を再生する際には下記の3つの種類がありますが、今回はその内のストリーミングでの動画再生で実装していきます。
      • ストリーミングでの動画再生
      • ダウンロードして動画再生
      • Bundle Resource にある動画ファイルの再生

実装してみる

動画を表示するためのクラス作成

まずは動画を再生するために、AVPlayerLayerlayerClass とする UIView のサブクラスを 作成していきましょう。公式のドキュメント にもありますが、プロパティとして playerLayer を持っておくことで後々使う AVPlayer の取り 回しが楽になります。

class VideoPlayerView: UIView {
    private var playerLayer: AVPlayerLayer? {
        return layer as? AVPlayerLayer
    }

    // Override UIView property
    override static var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
}

VideoPlayerView の動画を簡単に再生できるようにする

基本的には動画の制御に必要な APIAVPlayer が全て持ってますが、今回は動画広告のを流すとい うことで AVPlayer広告ID を一緒にセットしたいので player プロパティは private(set) にしておきます。もし、特にそのような事情がなければ player プロパティは private にしなくても大 丈夫です。

isPlaying に関しては AVPlayer に特にそれっぽい API が無かったのでプロパティを追加しま した。

class VideoPlayerView: UIView {
    private(set) var player: AVPlayer? {
        get {
            return playerLayer?.player
        }
        set {
            playerLayer?.player = newValue
        }
    }

    var isPlaying: Bool {
        guard let player = player else {
            return false
        }
        return player.rate != 0 && player.error == nil
    }

    private var playerLayer: AVPlayerLayer? {
        return layer as? AVPlayerLayer
    }

    // Override UIView property
    override static var layerClass: AnyClass {
        return AVPlayerLayer.self
    }

    private var currentId: Int32?

    func setPlayer(id: Int32, player: AVPlayer) {
        currentId = id
        self.player = player
    }

}

動画 View を使用する View に追加して再生してみる

これで一通り基本的な動画が再生はできるようになりました。

class VideoAdView: UIView {
    let videoView = VideoPlayerView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        videoView.frame = frame
        addSubview(videoView)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setPlayer(id: Int32, url: URL) {
        let player = AVPlayer(url: url)
        videoView.setPlayer(id: id, player: player)
    }

    // 再生
    func play() {
        videoView.player?.play()
    }

    // 停止
    func pause() {
        videoView.player?.pause()
    }

    // ミュート
    func mute(isMute: Bool) {
        videoView.player?.isMuted = isMute
    }
}

余談

今回は動画広告ということで、いくつかの動画を順番に再生したかったので動画の終了イベントを検知でき るようにしてみます。それとついでに動画の再生回数のイベントも送信したいので、1秒以上再生された場合 にイベントを送信できるように処理を書いていきます。動画の終了イベントに関しては AVPlayerDelegate が用意されているのかと思いましたがそういうわけではありませんでした、😓

class VideoPlayerView: UIView {
    private(set) var player: AVPlayer? {
        get {
            return playerLayer?.player
        }
        set {
            playerLayer?.player = newValue
        }
    }

    private var timeObserverToken: Any?

    var isPlaying: Bool {
        guard let player = player else {
            return false
        }
        return player.rate != 0 && player.error == nil
    }

    private var playerLayer: AVPlayerLayer? {
        return layer as? AVPlayerLayer
    }

    // Override UIView property
    override static var layerClass: AnyClass {
        return AVPlayerLayer.self
    }

    private var currentId: Int32?
    private var stopObserveClosure: (() -> ())?
    weak var delegate: VideoPlayerViewDelegate?

    deinit {
        stopObserveClosure?()
    }

    func setPlayer(id: Int32, player: AVPlayer) {
        stopObserveClosure?()
        setTimeObserver(player: player)
        if let currentItem = player.currentItem {
            stopObserveClosure = observeNotification(currentItem: currentItem)
        }
        currentId = id
        self.player = player
    }

    // 動画の再生イベント送信
    private func setTimeObserver(player: AVPlayer) {
        timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: {[weak self] time in
            guard let self = self, let timeObserverToken = self.timeObserverToken else {
                return
            }
            let seconds = CMTimeGetSeconds(time)
            if seconds >= 1.0 {
                // TODO: Add tracking event.

                self.player?.removeTimeObserver(timeObserverToken)
                self.timeObserverToken = nil
            }
        })
    }

    // 動画の終了イベントを通知
    @objc private func playerDidFinishPlaying(_ sender: Any) {
        guard let id = currentId else {
            return
        }
        // TODO: Handle Player Finish Playing
    }

    private func observeNotification(currentItem: AVPlayerItem) -> () -> Void {
        NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: currentItem)

        return {[weak self] in
            guard let self = self else {
                return
            }
            NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: currentItem)
        }
    }
}