SwiftUIでコントロールセンターに動画ファイルのタイトル / アルバム名を表示し、再生 / 停止 / スキップ / シークを実行できるようにする方法を説明する。
Swift 5.7 / Xcode 14.0 / iOS 16.0
結論
タイトル / アルバム名を表示するには、MPNowPlayingInfoCenter を設定する。
再生 / 停止 / スキップ / シークを実行できるようにするには、MPRemoteCommand を設定する。
ただ、スマートな方法はわからなかった。多少ひねった方法はわかったので具体例に示す。
具体例
完成イメージ
動画を再生ボタンをタップすると動画が再生される。
コントロールセンターを表示すると
- タイトル
- アルバム名
- 再生 / 停止ボタン
- スキップボタン
- 逆スキップボタン
- シークバー/経過時間/残り時間
が表示される。
コードを示す。
- 動画ファイルのURLを指定
- 動画ファイルのタイトル、アルバム名、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
}
- タイトル、アルバム名、時間などをNowPlayingInfoに設定する。
- 10秒スキップ/逆スキップボタンを表示させ、早送り/巻き戻しボタンが表示されないようにする。
- 表示させたタイトル、アルバム名がリフレッシュされて消えないようにする。
ここはあまりスマートなコードではない。
※ 実は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) { }
}
- タイトルを設定する。
- アルバム名を設定する。
- 経過時間を設定する。
- トータル時間を設定する。
- 10秒スキップ/逆スキップボタンを表示させ、早送り/巻き戻しボタンが表示されないようにする。
- 再生/停止/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でコントロールセンターに動画ファイルのタイトル / アルバム名を表示し、再生 / 停止 / スキップ / シークを実行できるようにする方法を説明した。
コメント