(Xcode 11, Swift 5.1, iOS 13での実装)
iOSでの、動画を再生する機能の作成について前回 に引き続いて記します。
AVPlayer – AVFoundation | Apple Developer Documentation
AVPlayerクラスを用いることで、映像やオーディオの再生ができます。 AVPlayerには画面に表示する機能がないので、前回 はその部分をAVPLayerViewControllerがになう例を記しました。 今回は、AVPlayerLayerを使う例を記します。
AVPlayerLayer – AVFoundation | Apple Developer Documentation
AVPlayerLayerはplayerプロパティを持ち、そこにAVPlayerを渡すことで映像を表示できます。 実際に使う際には、このAVPlayerLayerのドキュメントに記されているプレイヤー用のUIViewサブクラスを用いる方法が扱いやすいです。
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
// Override UIView property
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
Appleのドキュメントに乗っているコード例です。layerClassをオーバーライドしてこのUIViewがもつlayerはAVPlayerLayerクラスを返すようにしています。プロパティとしてplayerがあり、そこにAVPlayerをセットします。
この記事では、画面表示にこのPlayerViewを用いて、プロジェクトのリソースに加えた動画ファイルを再生する方法を記します。
動画を再生するサンプル
プロジェクトに追加してある動画ファイルを再生・一時停止するサンプルを作成します。バックグラウンドへ移行してもオーディオの再生・一時停止ができます。バックグラウンド移行時に動画再生中であった場合、一時停止するものを作成しますが、一時停止せずにオーディオを再生し続ける方法についても記します。全体のソースコードは記事の最後にまとめてあります。 ポイントになる箇所について、説明をおこないます。
動画再生機能作成については、Appleのドキュメントが参考になります。Creating a Basic Video Player (iOS and tvOS) | Apple Developer Documentation
PlayerViewクラスを作成
今回は、ViewController.swiftのほかに、PlayerView.swiftを作成します。 File > New > File > Cocoa Touch Classから、UIViewをスーパークラスとするPlayerViewクラスを作成します。 内容は、前段に記したAppleのドキュメントに載っているPlayerViewコード例の通りです。記事の最後にも載せています。
動画ファイルをプロジェクトに加える
File > Add Files to <プロジェクト名>。 動画ファイルを選び、プロジェクトに加えます。 今回は、clip.mp4という名前の動画ファイルが加えられたとして話を進めます。
UI作成
Main.storyboard > View Controller。 UIViewとボタン二つを貼り付けます。 ボタンのテキストを「Play」「Pause」とします。 (画像ではUIViewのBackgroundはオレンジ色にしてありますが、これはどこに貼り付けたかを見やすくするためで、実際にはSystem Background Colorのままで構いません)
貼り付けたUIViewを選択し、Identity inspector > Custom Class > Classで、PlayerViewとします。
Player ViewとViewController.swiftをOutletで結び、名前をplayerViewとします。 一つのボタンのテキストを「Play」、もう一つのボタンのテキストを「Pause」とします。 PlayボタンとViewController.swiftをActionで結び、名前をplayBtnTappedとします。@IBAction func playBtnTapped(_ sender: Any) {}が生成されます。 PauseボタンとViewController.swiftをActionで結び、名前をpauseBtnTappedとします。@IBAction func pauseBtnTapped(_ sender: Any) {}が生成されます。
Audio Sessionの準備
AVAudioSession – AVFoundation | Apple Developer Documentation
Audio Sessionとはアプリがどのようにオーディオを扱うかを示すオブジェクトです。再生のみなのか、録音再生を行うのか、バックグラウンド再生ありにするのかなどを設定します。用意されたカテゴリ・モード・オプションから適切なものを選んで設定する形式になっています。 今回は、カテゴリをAVAudioSessionCategoryPlayback(オーディオ機能をメインとしたアプリ用カテゴリ。バックグラウンド再生可能)、モードをAVAudioSessionModeMoviePlayback(ムービー再生用)に設定します。
/// Audio sessionを動画再生向けのものに設定し、activeにします
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback, mode: .moviePlayback)
} catch {
print("Setting category to AVAudioSessionCategoryPlayback failed.")
}
do {
try audioSession.setActive(true)
print("Audio session set active !!")
} catch {
}
バックグラウンド再生の準備
今回のサンプルでは、バックグラウンドでのオーディオ再生を行うので、その設定を行います。 TARGET > Signing & Capabilitiesを選択。 「+ Capability」ボタンから-> Background Modes
Audio, AirPlay and Picture in Pictureをチェック。
これにより、info.plistにRequired background modesが加わり、そのitemがApp plays audio or streams audio/video using AirPlayとなります。バックグラウンド再生が可能になります。
動画プレイヤー
urlからAVPlayerItemを作成します。 AVPlayerに作成したAVPlayerItemをセットします。 AVPlayerを動画表示を担当するAVPlayerLayerと結びつけます。 PlayerViewのプロパティであるplayerにAVPlayerをセットします。これによって、PlayerViewの動画表示をになうlayerと結びつけることができます。 今回は、ViewControllerクラスに追加したitemURLプロパティに動画のurlも渡しています。これは、あとで、NowPlayingInfo(現在再生中のアプリの状況)を表示するさいに使用します。
let fileName = "clip"
let fileExtension = "mp4"
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
print("Url is nil")
return
}
itemURL = url
let item = AVPlayerItem(url: url)
player = AVPlayer(playerItem: item)
playerView.player = player
リモートコントロール対応
イヤホンからの操作や、バックグラウンド移行後のコントロールセンターやロックスクリーンの再生コントロールからの操作に対応します。MPRemoteCommandCenter
クラスを用います。 また、コントロールセンターやロックスクリーンの再生コントロールに再生中アプリの現在の状況を表示する必要があります。それには、MPNowPlayingInfoCenter
クラスを用います。 これらの実装については、Controlling Background Audio | Apple Developer Documentation が参考になります。
リモートコントロール対応部分
// MARK: Remote Command Event
func addRemoteCommandEvent() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.togglePlayPauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteTogglePlayPause(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remotePlay(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remotePause(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteNextTrack(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remotePrevTrack(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
}
func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
// イヤホンのセンターボタンを押した時の処理
// (略)
}
func remotePlay(_ event: MPRemoteCommandEvent) {
// 再生ボタンが押された時の処理
// (略)
}
func remotePause(_ event: MPRemoteCommandEvent) {
// ポーズボタンが押された時の処理
// (略)
}
func remoteNextTrack(_ event: MPRemoteCommandEvent) {
// 「次へ」ボタンが押された時の処理
// (略)
}
func remotePrevTrack(_ event: MPRemoteCommandEvent) {
// 「前へ」ボタンが押された時の処理
// (略)
}
NowPlayingInfo部分
この例では、動画ファイルのurlの末尾部分をタイトル表示に使用しています。 MPNowPlayingInfoCenterに示すことができるメタデータはほかにも様々なものがあります。Now Playing Metadata Properties | MPNowPlayingInfoCenter – Media Player | Apple Developer Documentation
func setupNowPlaying(url: URL) {
// Define Now Playing Info
var nowPlayingInfo = [String : Any]()
// ここでは、urlのlastPathComponentをtitleとして表示しています。
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = ""
// Set the metadata
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
バックグラウンド移行時の動作について
AVPlayerでは、動画再生中にバックグラウンドへ移行すると、一時停止するのがデフォルトの動作です。 もし、バックグラウンド移行時に再生しているオーディオを鳴らし続けたい場合は、追加の処理が必要になります。 これについては、Playing Audio from a Video Asset in the Background | Apple Developer Documentation のドキュメントに記されています。 バックグラウンド移行時にAVPlayerのインスタンスをplayerLayerから外す必要があります。
// フォアグラウンド移行時に呼び出されます
@objc func willEnterForeground(_ notification: Notification) {
playerView.player = player
}
// バックグラウンド移行時に呼び出されます
@objc func didEnterBackground(_ notification: Notification) {
playerView.player = nil
}
Playボタンを押すと動画が再生され、Pauseボタンを押すと動画が一時停止するサンプルを作成しました。イヤホン、コントロールセンターなどからの再生・一時停止もできます。
使用したソースコード
PlayerView.swift
import UIKit
import AVFoundation
class PlayerView: UIView {
// The player assigned to this view, if any.
var player: AVPlayer? {
get { return playerLayer.player }
set { playerLayer.player = newValue }
}
// The layer used by the player.
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
// Set the class of the layer for this view.
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
ViewController.swift
以下のソースコードでは、バックグラウンド移行時に一時停止する動作になっています。動画再生中のバックグラウンド移行のさいに、続けてオーディオ再生を行う場合は、willEnterForeground(_;)、didEnterBackground(_:)でのコメントアウトを外してください。
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class ViewController: UIViewController {
@IBOutlet weak var playerView: PlayerView!
var player = AVPlayer()
var itemURL: URL?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback, mode: .moviePlayback)
} catch {
print("Setting category to AVAudioSessionCategoryPlayback failed.")
}
do {
try audioSession.setActive(true)
print("audio session set active !!")
} catch {
}
setupPlayer()
addLifeCycleObserver()
addRemoteCommandEvent()
}
private func setupPlayer() {
let fileName = "clip"
let fileExtension = "mp4"
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
print("Url is nil")
return
}
itemURL = url
let item = AVPlayerItem(url: url)
player = AVPlayer(playerItem: item)
playerView.player = player
}
/// Playボタンが押された
@IBAction func playBtnTapped(_ sender: Any) {
player.play()
if let url = itemURL {
setupNowPlaying(url: url)
}
}
@IBAction func pauseBtnTapped(_ sender: Any) {
player.pause()
}
// MARK: Life Cycle
func addLifeCycleObserver() {
let center = NotificationCenter.default
center.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
// フォアグラウンド移行時に呼び出されます
@objc func willEnterForeground(_ notification: Notification) {
/// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
/// (バックグラウンド移行時に、playerLayerからplayerがはずされているので、再度つなげます。)
//playerView.player = player
}
// バックグラウンド移行時に呼び出されます
@objc func didEnterBackground(_ notification: Notification) {
/// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
//playerView.player = nil
}
// MARK: Now Playing Info
func setupNowPlaying(url: URL) {
// Define Now Playing Info
var nowPlayingInfo = [String : Any]()
// ここでは、urlのlastPathComponentを表示しています。
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = ""
// Set the metadata
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
// MARK: Remote Command Event
func addRemoteCommandEvent() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.togglePlayPauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteTogglePlayPause(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remotePlay(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remotePause(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteNextTrack(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remotePrevTrack(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
}
func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
// イヤホンのセンターボタンを押した時の処理
// (略)
}
func remotePlay(_ event: MPRemoteCommandEvent) {
player.play()
}
func remotePause(_ event: MPRemoteCommandEvent) {
player.pause()
}
func remoteNextTrack(_ event: MPRemoteCommandEvent) {
// 「次へ」ボタンが押された時の処理
// (略)
}
func remotePrevTrack(_ event: MPRemoteCommandEvent) {
// 「前へ」ボタンが押された時の処理
// (略)
}
}