SwiftUI | コントロールセンター使用方法 動画ファイル編

音声・動画

SwiftUIでコントロールセンターに動画ファイルのタイトル / アルバム名を表示し、再生 / 停止 / スキップ / シークを実行できるようにする方法を説明する。

Swift 5.7 / Xcode 14.0 / iOS 16.0

結論

タイトル / アルバム名を表示するには、MPNowPlayingInfoCenter を設定する。

再生 / 停止 / スキップ / シークを実行できるようにするには、MPRemoteCommand を設定する。

ただ、スマートな方法はわからなかった。多少ひねった方法はわかったので具体例に示す。

具体例

完成イメージ

動画を再生ボタンをタップすると動画が再生される。

コントロールセンターを表示すると

  1. タイトル
  2. アルバム名
  3. 再生 / 停止ボタン
  4. スキップボタン
  5. 逆スキップボタン
  6. シークバー/経過時間/残り時間

が表示される。

コードを示す。

  1. 動画ファイルのURLを指定
  2. 動画ファイルのタイトル、アルバム名、URLを扱いやすいようにまとめて変数に格納
import SwiftUI

struct ContentView: View {
    let fileUrl = Bundle.main.url(forResource: "sample1",
                                  withExtension: "mp4")!  // ? 1
    var body: some View {
        let avModel = AVModel(title: "タイトル", album: "アルバム", url: fileUrl)  // ? 2
        NavigationView {
            NavigationLink(destination: VideoPlayView(avModel: avModel)) {
                Image(systemName: "music.note")
                Text("動画を再生")
            }
        }
    }
}

struct AVModel {
    var title: String
    var album: String
    var url: URL
}
  1. タイトル、アルバム名、時間などをNowPlayingInfoに設定する。
  2. 10秒スキップ/逆スキップボタンを表示させ、早送り/巻き戻しボタンが表示されないようにする。
  3. 表示させたタイトル、アルバム名がリフレッシュされて消えないようにする。

ここはあまりスマートなコードではない。

※ 実はviewWillAppear()はなぜかわからないが(動画が全部読み込まれるまで?)繰り返し(4回くらい)実行され、最終的に動画ファイルの経過時間、トータル時間の情報が読み込まれた状態になる。動画ファイルの経過時間、トータル時間の情報が読み込まれる前にNowPlayingInfoに経過時間、トータル時間の情報を設定しに行くと、コントロールセンターの経過時間、残り時間が表示されなくなってしまう。ここではviewWillAppear()が繰り返し実行されて経過時間、トータル時間の情報が読み込まれた状態になるというよくわからない性質を利用して、NowPlayingInfoに経過時間、トータル時間の情報がきちんと設定される仕組みにしている。

import SwiftUI
import AVKit

let viewController = AVPlayerViewController()

struct VideoPlayView: View {
    var avPlayer = VideoPlayer()
    let avModel: AVModel

    // 動画の全画面表示切り替え用の変数
    @Environment(\.dismiss) private var dismiss
    @State var backButtonHidden: Bool = true
    @State var tabBarHidden: Visibility = .hidden
    
    var body: some View {
        viewWillAppear()  // ? 1
        
        return AVView(avViewModel: avPlayer)
                .onAppear {
                    avPlayer.videoPlayer!.play()
                    avPlayer.refreshMPRemoteCommandCenter()  // ? 2
                    // 表示させたタイトル、アルバム名がリフレッシュされて消えないようにする
                    viewController.updatesNowPlayingInfoCenter = false  // ? 3
                }
                // 動画の全画面表示切り替え
                .navigationBarBackButtonHidden(backButtonHidden)
                .edgesIgnoringSafeArea(.all)
                .toolbar(tabBarHidden, for: .tabBar)
                .gesture(
                    DragGesture()
                        .onEnded { value in
                            if value.translation.height > 10 {
                                backButtonHidden = false
                                tabBarHidden = .visible
                                dismiss()
                            }
                        }
                )

        func viewWillAppear() {  // ? 1
            print("▶viewWillAppear()")
            avPlayer.avModel = avModel
            if avPlayer.videoPlayer == nil {
                print("videoPlayer : nil")
                avPlayer.setVideoPlayer()
            } else {
                print("videoPlayer : \(avPlayer.videoPlayer!)")
            }
            // タイトル、アルバム名、時間などを設定する
            avPlayer.setNowPlayingInfo_video()
            // 試しにトータル時間のvalueをprint
            print("トータル時間 \(avPlayer.videoPlayer!.currentItem!.duration.value)")
        }
    }
}


// View
struct AVView: UIViewControllerRepresentable {
    var avViewModel: VideoPlayer
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        viewController.player = avViewModel.videoPlayer
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: AVPlayerViewController,
                                context: Context) { }
    
}
  1. タイトルを設定する。
  2. アルバム名を設定する。
  3. 経過時間を設定する。
  4. トータル時間を設定する。
  5. 10秒スキップ/逆スキップボタンを表示させ、早送り/巻き戻しボタンが表示されないようにする。
  6. 再生/停止/10秒スキップ/逆スキップボタンを表示させ、それぞれの機能を実装。シーク機能も実装する。
import SwiftUI
import MediaPlayer


class VideoPlayer {
    let commandCenter = MPRemoteCommandCenter.shared()
    var avModel: AVModel = AVModel(title: "", album: "", url: URL(fileURLWithPath: ""))
    var videoPlayer: AVPlayer?
    
    init() {
        setMPRemoteCommandCenter()        
    }

    func setNowPlayingInfo_video() {
        var nowPlayingInfo = [String : Any]()
        nowPlayingInfo[MPMediaItemPropertyTitle] = avModel.title  // ? 1
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = avModel.album  // ? 2
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(videoPlayer!.currentTime().value)
                                                                    / Double(videoPlayer!.currentTime().timescale)  // ? 3
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(videoPlayer!.currentItem!.duration.value)
                                                            / Double(videoPlayer!.currentItem!.duration.timescale)  // ? 4
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = videoPlayer!.rate
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
    
    func refreshMPRemoteCommandCenter() {  // ? 5
        commandCenter.nextTrackCommand.isEnabled = false
        commandCenter.previousTrackCommand.isEnabled = false
        commandCenter.skipBackwardCommand.isEnabled = true
        commandCenter.skipForwardCommand.isEnabled = true
    }
        
    func setMPRemoteCommandCenter() {  // ? 6
        commandCenter.playCommand.addTarget { [unowned self] event in
            print("play")
            videoPlayer!.play()
            return .success
        }
        commandCenter.pauseCommand.addTarget { [unowned self] event in
            print("pause")
            videoPlayer!.pause()
            return .success
        }
        commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: 10)]
        commandCenter.skipBackwardCommand.addTarget{ [unowned self] event in
            print("skipBackward")
            let currentTime = Double(videoPlayer!.currentTime().value)
                            / Double(videoPlayer!.currentTime().timescale)
            var skippedTime = currentTime - 10
            if skippedTime < 0 {
                skippedTime = 0
            }
            self.videoPlayer!.seek(to: CMTime(value: Int64(skippedTime), timescale: 1))
            MPNowPlayingInfoCenter.default().nowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = skippedTime
            return .success
        }
        commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: 10)]
        commandCenter.skipForwardCommand.addTarget{ [unowned self] event in
            print("skipForward")
            let currentTime = Double(videoPlayer!.currentTime().value)
                            / Double(videoPlayer!.currentTime().timescale)
            var skippedTime = currentTime + 10
            let duration = Double(videoPlayer!.currentItem!.duration.value)
                         / Double(videoPlayer!.currentItem!.duration.timescale)
            if skippedTime > duration {
                skippedTime = duration
            }
            self.videoPlayer!.seek(to: CMTime(value: Int64(skippedTime), timescale: 1))
            MPNowPlayingInfoCenter.default().nowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = skippedTime
            return .success
        }
        commandCenter.changePlaybackPositionCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
            print("seek")
            if let changePlaybackPositionCommandEvent = event as? MPChangePlaybackPositionCommandEvent {
                let positionTime = changePlaybackPositionCommandEvent.positionTime
                self.videoPlayer!.seek(to: CMTime(value: Int64(positionTime), timescale: 1))
                MPNowPlayingInfoCenter.default().nowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionTime
            }
            return .success
        }
    }

    func setVideoPlayer() {
        if videoPlayer == nil {
            videoPlayer = AVPlayer(url: avModel.url)
        } else {
            videoPlayer!.replaceCurrentItem(with: AVPlayerItem(url: avModel.url))
        }
    }    
}

まとめ

SwiftUIでコントロールセンターに動画ファイルのタイトル / アルバム名を表示し、再生 / 停止 / スキップ / シークを実行できるようにする方法を説明した。

コメント

タイトルとURLをコピーしました