[SwiftUI] VideoPlayer の使い方

SwiftUI2021

     
⌛️ 4 min.
VideoPlayer の使い方を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Ventura 13.2 Beta
  • Xcode 14.2
  • iOS 16.0

VideoPlayer

SwiftUI 向けの View で動画が再生できるようになる View です。

Apple のドキュメントは、こちら

iOS14/macOS11 から用意されていたようです。
自分は、あまり興味がなかったので、確認してませんでした。

ふと、興味があって、簡単な再生アプリを作ろうとして、手間取ったので、VideoPlayer の使い方を説明します。

サンプルコードが動かない

Apple のドキュメントを見て最初に困るのが、サンプルコードがコンパイルできないことです。

PlayerViewModel という class(?) が使われているのですが、そのコードはありません。

サンプルコードの中では、player, .play(), .isPlaying というプロパティとメソッドが使用されていることがわかりますが、それぞれの意味は推測する必要があります。

シンプルに作る

本格的に作るなら、ViewModel を作って・・・となるとは思いますが、最初は、できるだけ少ないコードで動くものを作ってみたくなります。

VideoPlayer とその周辺の要素を整理してみます。

VideoPlayer

VideoPlayer は、player からの コンテンツを表示し、再生のためのインターフェースを提供する View と説明されています。

Apple のドキュメントは、こちら

ドキュメントを確認してみると、VideoPlayer は、初期化時に、AVPlayer を必要とします。

videoOverlay として overlay 要素を View として設定できるようですが、この記事では、扱いません。

AVPlayer

AVPlayer は、Player の再生をコントロルするためのインターフェースを提供するオブジェクトと説明されています。

Apple のドキュメントは、こちら

ドキュメントを確認してみると、この AVPlayer を使うことで、再生をコントロールできるようです。
play() や pause() のメソッドもそれっぽい(?) です。

この AVPlayer を初期化するには、url か AVPlayerItem が必要です。

AVPlayerItem

AVPlayerItem は、再生途中のコンテンツについてのタイミングや状態を表すオブジェクトのようです。

凝ったことをするためには必要になりそうですが、必要になるまで扱いません。

AVPlayer を初期化する時の url は、リソースの URL を渡せば良いようです。

iOS でファイル選択しようとすると少し手間ですが、macOS であれば、ファイルダイアログを出して簡単に選択することができます。

@MainActor
func openPanelForFilename(_ folder: URL? = nil) async -> [URL] {
#if os(macOS)
    let panel = NSOpenPanel()
    panel.canChooseDirectories = false
    panel.canCreateDirectories = false
    panel.allowsMultipleSelection = true
    panel.showsTagField = true
    panel.isExtensionHidden = true
    panel.level = NSWindow.Level.modalPanel
    let result = await panel.beginSheetModal(for: NSApp.mainWindow!)
    if result == .OK {
        return panel.urls
    }
#endif
    return []
}

上記のコードは、macOS/iOS のどちらでもコンパイルできるように#if/#endif を使っていますが、NSOpenPanel を使って、ファイル選択するコードです。

最初の実装

VideoPlayer, AVPlayer の意味がわかったので、実装していきます。

struct ContentView: View {
    @State private var fileURL: URL? = nil
    @State private var avPlayer: AVPlayer? = nil

    var body: some View {
        VStack {
            if let avPlayer = avPlayer {
                // (1)
                VideoPlayer(player: avPlayer)
            } else {
                Text("No resource")
            }
            // (2)
            Button(action: {
                Task {
                    let urls = await openPanelForFilename()
                    if let url = urls.first {
                        fileURL = url
                    }
                }
            }, label: { Text("select file") })
            // (3)
            .onChange(of: fileURL) { newURL in
                guard let newURL = newURL else { return }
                avPlayer = AVPlayer(url: newURL)
            }
        }
    }
}
コード解説
  1. AVPlayer が用意できているようであれば、VideoPlayer を表示します。
  2. ファイル選択ダイアログを出すボタンです。選択したファイルを VideoPlayer を使って表示します
  3. ファイル選択ダイアログで、fileURL がセットされたときに、AVPlayer を用意します。
    AVPlayer が用意されると、VideoPlayer が表示されます

以下のような動作になります。

動画が読み込まれていることがわかりますし、VideoPlayer が表示してくれる 再生ボタン等で操作することもできます。

少し拡張してみる

再生ボタン

せっかくなので(?)、Apple のサンプルコードにもあるような、再生・停止ボタンを作ってみます。

再生、停止は、AVPlayer に play(), pause() のメソッドとして用意されていました。

再生状態は、AVPlayer からは取得できず、Notification を使用することが必要です。
Apple のドキュメントでは、currentItem: AVPlayerItem や rate: Float を 監視するように書かれています。

stackoverflow をみてみると、rate が、0 であれば停止中、 0 でなければ再生中 ということのようです。(rate が負のときは、逆方向の再生中です)

ということで、以下のようなコードで、再生中かどうか判断できます。以下の例では、isPlyaing: Bool を @State 変数として持ち、”Play” ボタンの disable 制御をしています。

struct ContentView: View {
    @State private var fileURL: URL? = nil
    @State private var avPlayer: AVPlayer? = nil
    @State private var cancellable: Cancellable? = nil
    @State private var isPlaying = false

    var body: some View {
        VStack {
            if let avPlayer = avPlayer {
                VideoPlayer(player: avPlayer)
            } else {
                Text("No resource")
            }

            Button(action: {
                // (1)
                avPlayer?.play()
            }, label: {Text("Play")})
            // (2)
            .disabled(isPlaying || (avPlayer == nil))

            Button(action: {
                Task {
                    let urls = await openPanelForFilename()
                    if let url = urls.first {
                        fileURL = url
                    }
                }
            }, label: { Text("select file") })
            .onChange(of: fileURL) { newURL in
                guard let newURL = newURL else { return }
                avPlayer = AVPlayer(url: newURL)
                // (3)
                cancellable = avPlayer?.publisher(for: \.rate)
                    .sink(receiveValue: { newRate in
                        if newRate != 0 {
                            isPlaying = true
                        } else {
                            isPlaying = false
                        }
                    })

            }
        }
    }
}
コード解説
  1. Play ボタンを押下されたら、AVPlayer の機能で再生を開始します。
  2. AVPlayer の用意ができていて、再生途中でなければ、Playボタンを enable します
  3. 作成した AVPlayer の rate プロパティを監視し、変更があれば、isPlaying を更新します

再生/停止 ボタンにする

再生中か判断できるようになったので、状態に応じて “再生ボタン” / “停止ボタン” を切り替えるようにしてみます。

struct ContentView: View {
    @State private var fileURL: URL? = nil
    @State private var avPlayer: AVPlayer? = nil
    @State private var cancellable: Cancellable? = nil
    @State private var isPlaying = false

    var body: some View {
        VStack {
            if let avPlayer = avPlayer {
                VideoPlayer(player: avPlayer)
            } else {
                Text("No resource")
            }

            Button(action: {
                if isPlaying {
                    avPlayer?.pause()
                } else {
                    avPlayer?.play()
                }
            }, label: {Text(isPlaying ? "Pause" : "Play")})
            .disabled(avPlayer==nil)

            Button(action: {
                Task {
                    let urls = await openPanelForFilename()
                    if let url = urls.first {
                        fileURL = url
                    }
                }
            }, label: { Text("select file") })
            .onChange(of: fileURL) { newURL in
                guard let newURL = newURL else { return }
                avPlayer = AVPlayer(url: newURL)
                cancellable = avPlayer?.publisher(for: \.rate)
                    .sink(receiveValue: { newRate in
                        isPlaying = (newRate != 0)
                    })

            }
        }
    }
}

動作確認していると、再生を途中で Pause して再開すると停止箇所から再開してくれますが、再生が終わってしまうと、Play ボタンを押下しても再生してくれません。

最後まで再生したことを検知して、最初から再生するように修正してみます。

先頭から再生

再生後に Play ボタンを押下されたときに先頭から再生するためには、どこまで再生されたかを知ることが必要となります。

そこで AVPlayerItem の登場となります。

AVPlayerItem

AVPlayer は、再生途中の情報を AVPlayerItem として持っていて、AVPlayer から currentItem プロパティとして取得することができます。

AVPlayerItem には、多くのプロパティ/メソッドが定義されていますが、どこまで再生されたかを確認するためには、以下のプロパティを使います。

・currentTime : 現在の時間(CMTime)
・seekableTimeRange: 開始終了時間(CMTimeRange)

CMTime は、CoreMedia で使用されている時間を表現するための型です。
Apple のドキュメントは、こちら

この AVPlayerItem の seekableTimeRange プロパティは、CMTimeRange 型で、メディアの開始時間終了時間を持っています。
AVPlayerItem は複数の CMTimeRange を持つことができ、seekableTimeRange は、CMTimeRange の配列を返してきます。

なお、CMTime は、comparable なので、同値判定できます。

これらの情報を使い、再生終了時の(時間的)位置が、メディアの終了時間と一致しているかを確認することができます。

特定箇所へ移動する

最終的にやりたいことは、「停止位置が終了位置だったときは、開始位置から再生する」なので、次の課題(?)は、「開始位置から再生する」です。もう少し分割すると、「指定時間位置から再生する」です。
(開始時間の位置は、すでにわかっています。)

特定の時間位置に移動するメソッドは、AVPlayer の持つ seek メソッドです。
Apple のドキュメントは、こちら

指定時間位置からの再生は、「指定時間位置に移動する」+「再生する」で実現できます。

struct ContentView: View {
    @State private var fileURL: URL? = nil
    @State private var avPlayer: AVPlayer? = nil
    @State private var cancellable: Cancellable? = nil
    @State private var isPlaying = false
    var body: some View {
        VStack {
            if let avPlayer = avPlayer {
                VideoPlayer(player: avPlayer)
            } else {
                Text("No resource")
            }
            Button(action: {
                if isPlaying {
                    avPlayer?.pause()
                } else {
                    // (1)
                    if let currentItem = avPlayer?.currentItem,
                       let contentTime = currentItem.seekableTimeRanges.first as? CMTimeRange,
                       contentTime.end == currentItem.currentTime() {
                        Task {
                            // (2)
                            await avPlayer?.seek(to: contentTime.start)
                            avPlayer?.play()
                        }
                    } else {
                        avPlayer?.play()
                    }
                }
            }, label: {Text(isPlaying ? "Pause" : "Play")})
            .disabled(avPlayer==nil)

            Button(action: {
                Task {
                    let urls = await openPanelForFilename()
                    if let url = urls.first {
                        fileURL = url
                    }
                }
            }, label: { Text("select file") })
            .onChange(of: fileURL) { newURL in
                guard let newURL = newURL else { return }
                avPlayer = AVPlayer(url: newURL)
                cancellable = avPlayer?.publisher(for: \.rate)
                    .sink(receiveValue: { newRate in
                        isPlaying = (newRate != 0)
                    })

            }
        }
    }
}
コード解説
  1. AVPlayer が再生情報を持っていて、再生停止時の位置が、メディアの終了位置と一致しているかチェック
  2. 条件が一致していれば、開始位置まで移動してから 再生を開始する

完成したアプリ

最終的に以下のような機能を持つアプリになってます。

・選択したファイルを読み込む
・Play ボタンで再生開始
・Pause ボタンで再生停止
・最後まで再生して停止されているときに Play ボタンを押すと最初から再生開始

実装メモ

1ファイル(ContentView.swift)で実装したかったので、@State でいろいろな変数を作っていますが、実際にアプリを作るとすると、ViewModel を作って そちらに 各種変数を持たせるのが自然だと思います。

今回の構成は あくまで、動作確認用の構成ということです。

まとめ

VideoPlayer の使い方を説明してきました。

VideoPlayer の使い方
  • VidePlayer が AVPlayer を表示するための (SwiftUI の) View
  • 実際の再生制御は、AVPlayer で行う
  • 再生途中の情報は、AVPlayerItem を参照する

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です