ピクミンのスマホゲームに歩かされ続けている福山です(現在Lv51 - 合計歩数1,415,248)
iOS 16で音楽や動画を流しているときにロック画面がいい感じになりましたよね!
正式名称はわかりませんが、ここに表示される画像はアルバムアートやアートワークなどと呼ばれているようです。
Mirrativ iOSアプリの配信視聴時もいい感じになっているはず... と思ったのですが、ただの灰色の画面になっていました😨 「なんとかしたいなぁ」と思い既存のコードとリソースをほんの少しいじった結果、以下のようになりました!Mirrativアプリv9.88.0から反映されています。
大画面で画像を表示したくない or 他の通知もすぐ見たいという場合には画像部分をタップすることでコンパクトに表示されます。
コントロールセンターの右上のブロックを押すことでも画像を見ることができます。
iPhone 14 Pro, iPhone 14 Pro Maxに搭載された Dynamic Island にも自動で適用されます。 縮小時は左側に画像が小さく表示され、長押しして展開するとクルッと回るアニメーション付きで画像がより大きく表示されます。
Appleの違和感のないアニメーションも相まって良いですね!
iPhone 14 Pro, iPhone 14 Pro Maxの常時表示ディスプレイにもバッチリ表示されます!
サンプルコード
再生している動画のスクショを作成してアートワークにするサンプルコードです。
AVPlayerViewController
と AVPlayerLayer
の両方のケースを想定した作りになっています。
XcodeのSigning & Capabilities
で Background Modes
の Audio, AirPlay, and Picture in Picture
にチェックを入れるのを忘れないでください。
// // ViewController.swift // import UIKit import AVKit import MediaPlayer enum PlayerMode { case avPlayerViewController case avPlayerLayer } class ViewController: UIViewController { private let playerViewController = AVPlayerViewController() private let playerView = PlayerView() private let player = AVPlayer() private var observer: NSKeyValueObservation? private let playerMode: PlayerMode = .avPlayerViewController override func viewDidLoad() { super.viewDidLoad() setupAppStatusObserver() setupPlayer() } } // MARK: Player extension ViewController { private func setupPlayer() { guard let url = Bundle.main.url(forResource: "video", withExtension: "mp4") else { print("video not found") return } let item = AVPlayerItem(url: url) // KVOでstatusを監視し準備ができたら再生開始 observer = item.observe(\.status) { [weak self] item, _ in switch item.status { case .readyToPlay: self?.play() case .failed, .unknown: break @unknown default: break } } player.replaceCurrentItem(with: item) switch playerMode { case .avPlayerViewController: playerViewController.willMove(toParent: self) addChild(playerViewController) view.addSubview(playerViewController.view) playerViewController.view.frame = view.bounds playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] playerViewController.didMove(toParent: self) // ピクチャー・イン・ピクチャーを無効化 playerViewController.allowsPictureInPicturePlayback = false // アルバムアート/アートワーク表示が上書きされないようにする playerViewController.updatesNowPlayingInfoCenter = false playerViewController.player = player case .avPlayerLayer: view.addSubview(playerView) playerView.frame = view.bounds playerView.player = player } } private func play() { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) try AVAudioSession.sharedInstance().setActive(true) } catch { print("audio session error: \(error.localizedDescription)") return } player.play() setupNowPlayingInfo() // ロック画面のアルバムアート/アートワーク表示を利用するためには、 // UIApplication.shared.beginReceivingRemoteControlEvents() を呼ぶ // もしくはリモートコントロールをセットする必要がある setupRemoteControl() } /// 再生情報をセット private func setupNowPlayingInfo() { var nowPlayingInfo: [String: Any] = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] // タイトル nowPlayingInfo[MPMediaItemPropertyTitle] = "My Video" // アーティスト (投稿者・撮影者など) nowPlayingInfo[MPMediaItemPropertyArtist] = "someone" // ライブストリーミングかどうか nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false if let duration = player.currentItem?.duration { // 動画全体の時間 nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = "\(CMTimeGetSeconds(duration))" // 再生位置 nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = "0" // 再生速度 nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = "1" } MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo // 数秒後の動画を静止画にして、アルバムアート/アートワーク表示に利用する DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.setAlbumArt() } } /// アルバムアート/アートワーク表示に利用する画像をセット private func setAlbumArt() { takeScreenshot { image in // 画像を正方形に切り抜き (ここでは適当に、一片600px以下にしている) guard let croppedImage = image?.cropToSquare(desiredDemension: 600) else { return } let item = MPMediaItemArtwork(boundsSize: croppedImage.size) { _ in croppedImage } var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] info[MPMediaItemPropertyArtwork] = item MPNowPlayingInfoCenter.default().nowPlayingInfo = info } } /// 再生中の動画のスクリーンショットを撮る /// - Parameter completion: UIImage? を返すクロージャ private func takeScreenshot(completion: @escaping ((UIImage?) -> Void)) { guard let asset = player.currentItem?.asset else { completion(nil) return } let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true let times = [NSValue(time: player.currentTime())] imageGenerator.generateCGImagesAsynchronously(forTimes: times) { _, cgImage, _, _, _ in DispatchQueue.main.async { if let cgImage { completion(UIImage(cgImage: cgImage)) } else { completion(nil) } } } } /// ロック画面やコントロールセンターからの操作を受け付ける private func setupRemoteControl() { let remoteCommandCenter = MPRemoteCommandCenter.shared() [ remoteCommandCenter.playCommand, remoteCommandCenter.pauseCommand ].forEach { $0.addTarget(self, action: #selector(handleRemoteCommand(_:))) } } /// ロック画面やコントロールセンターからの操作を処理する @objc private func handleRemoteCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { let remoteCommandCenter = MPRemoteCommandCenter.shared() switch event.command { case remoteCommandCenter.playCommand: player.play() case remoteCommandCenter.pauseCommand: player.pause() default: return .commandFailed } return .success } } // MARK: Notification extension ViewController { /// バックグラウンド再生のための通知 /// https://developer.apple.com/documentation/avfoundation/media_playback/creating_a_basic_video_player_ios_and_tvos/playing_audio_from_a_video_asset_in_the_background private func setupAppStatusObserver() { [ UIApplication.willEnterForegroundNotification, UIApplication.didEnterBackgroundNotification ].forEach { NotificationCenter.default.addObserver(self, selector: #selector(appStateChange(_:)), name: $0, object: nil) } } @objc private func appStateChange(_ notification: Notification) { switch notification.name { case UIApplication.willEnterForegroundNotification: switch playerMode { case .avPlayerViewController: playerViewController.player = player case .avPlayerLayer: playerView.player = player } case UIApplication.didEnterBackgroundNotification: playerViewController.player = nil playerView.player = nil default: break } } } // MARK: etc extension UIImage { func cropToSquare(desiredDemension: CGFloat) -> UIImage? { guard size.width > 0, size.height > 0 else { return nil } let dimension = min(desiredDemension, size.width, size.height) let targetSize = CGSize(width: dimension, height: dimension) UIGraphicsBeginImageContextWithOptions(targetSize, false, 0) if size.height > size.width { let (width, height) = (dimension, size.height * dimension / size.width) draw(in: CGRect(x: 0, y: (height - dimension) * -0.5, width: width, height: height)) } else { let (width, height) = (size.width * dimension / size.height, dimension) draw(in: CGRect(x: (width - dimension) * -0.5, y: 0, width: width, height: height)) } let outputImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return outputImage } } /// AVPlayerLayer /// https://developer.apple.com/documentation/avfoundation/avplayerlayer class PlayerView: UIView { override static var layerClass: AnyClass { AVPlayerLayer.self } var player: AVPlayer? { get { playerLayer.player } set { playerLayer.player = newValue } } private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } }
音楽や動画の再生機能があるけど画像はまだ指定していないといったアプリの開発の参考になれば幸いです!
We are hiring!
ミラティブでは一緒に開発してくれるiOS開発者を募集しています!少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、気軽にご連絡ください。
www.mirrativ.co.jp