前回は、動画を再生・ポーズ・早送り・早戻し・シークする機能を実装しました。
今回は、バックグラウンドで、動画ファイルのオーディオを再生・ポーズ・早送り・早戻し・シークする機能を追加します。
以前、バックグラウンドで動画ファイルのオーディオを再生・ポーズする記事([iOS]動画を再生する(AVPlayerLayer使用))を記しました。今回は、早送り・早戻し・シークが加わっています。
前回の記事で作成したプロジェクトに書き加える形で作成します。
ポイントになる箇所について、説明をおこないました。
全体のソースコードは記事の最後にまとめてあります。
バックグラウンド再生の準備
今回のサンプルでは、バックグラウンドでのオーディオ再生を行うので、その設定を行います。
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となります。バックグラウンド再生が可能になります。
リモートコントロール対応
イヤホンからの操作や、バックグラウンド移行後のコントロールセンターやロックスクリーンの再生コントロールからの操作に対応します。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
}
// 早送り
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: skipInterval)]
commandCenter.skipForwardCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteSkipForward(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
// 早戻し
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: skipInterval)]
commandCenter.skipBackwardCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteSkipBackward(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
// ポジション移動(バーのボタン位置変更)
commandCenter.changePlaybackPositionCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteChangePlaybackPosition(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
commandCenter.skipBackwardCommand.isEnabled = false
}
/// イヤホンのセンターボタンを押した時の処理
func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
// (略)
}
func remotePlay(_ event: MPRemoteCommandEvent) {
player.play()
}
func remotePause(_ event: MPRemoteCommandEvent) {
player.pause()
}
/// リモートコマンドで「次へ」ボタンが押された時の処理
func remoteNextTrack(_ event: MPRemoteCommandEvent) {
// (略)
}
/// リモートコマンドで「前へ」ボタンが押された時の処理
func remotePrevTrack(_ event: MPRemoteCommandEvent) {
// (略)
}
/// リモートコマンドで「早送り」ボタンが押された時の処理
func remoteSkipForward(_ event: MPRemoteCommandEvent) {
skipForward()
}
/// リモートコマンドで「早戻し」ボタンが押された時の処理
func remoteSkipBackward(_ event: MPRemoteCommandEvent) {
skipBackward()
}
/// リモートコマンドでシークバー(プログレスバー)の位置を変更した時の処理
func remoteChangePlaybackPosition(_ event: MPRemoteCommandEvent) {
if let evt = event as? MPChangePlaybackPositionCommandEvent {
let timeScale = CMTimeScale(NSEC_PER_SEC)
let time = CMTime(seconds: evt.positionTime, preferredTimescale: timeScale)
changePosition(time: time)
}
}
NowPlayingInfo部分
今回のサンプルでは、動画ファイルのurlの末尾部分がタイトルとして表示されます。
また、再生に連れてバーが進み、現在の再生時刻が表示されます。再生に連れて変化する箇所は、addPeriodicTimeObserver(forInterval:queue:using:)のblockに記述して、周期的に更新しています。
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を表示しています。
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = ""
// Set the metadata
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func updateNowPlaying(time: Double) {
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = itemURL?.lastPathComponent ?? ""
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time
if let duration = player.currentItem?.duration {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(duration)
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
再生コントロール
今回のサンプルでのロックスクリーン上の再生コントロール
再生ボタン、早戻しボタン、早送りボタンが表示されています。
ここで、「次のトラック」「前のトラック」ボタンのほうを表示したい時には、
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.skipForwardCommand.isEnabled = false
commandCenter.skipBackwardCommand.isEnabled = false
とすると、skipForwardCommandとskipBackwardCommandが使用不可になり、再生コントロールにも表示されません。以下のようになります。
代わって、previousTrackCommandとnextTrackCommandを示すボタンが表示されています。
使用したソースコード
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
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class ViewController: UIViewController {
@IBOutlet weak var playerView: PlayerView!
@IBOutlet weak var slider: UISlider!
let skipInterval: Double = 10
var player = AVPlayer()
var timeObserverToken: Any?
var itemURL: URL?
var itemDuration: Double = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupAudioSession()
setupPlayer()
addRemoteCommandEvent()
}
private func setupAudioSession() {
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 {
}
}
private func setupPlayer() {
playerView.player = player
addPeriodicTimeObserver()
replacePlayerItem(fileName: "clip", fileExtension: "mp4")
}
private func replacePlayerItem(fileName: String, fileExtension: String) {
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
print("Url is nil")
return
}
itemURL = url
let asset = AVAsset(url: url)
itemDuration = CMTimeGetSeconds(asset.duration)
let item = AVPlayerItem(url: url)
player.replaceCurrentItem(with: item)
setupNowPlaying(url: url)
}
@IBAction func playBtnTapped(_ sender: Any) {
player.play()
}
@IBAction func pauseBtnTapped(_ sender: Any) {
player.pause()
}
@IBAction func sliderValueChanged(_ sender: UISlider) {
let seconds = Double(sender.value) * itemDuration
let timeScale = CMTimeScale(NSEC_PER_SEC)
let time = CMTime(seconds: seconds, preferredTimescale: timeScale)
changePosition(time: time)
}
@IBAction func skipForwardBtnTapped(_ sender: Any) {
skipForward()
}
@IBAction func skipBackwardBtnTapped(_ sender: Any) {
skipBackward()
}
private func skipForward() {
skip(interval: skipInterval)
}
private func skipBackward() {
skip(interval: -skipInterval)
}
private func skip(interval: Double) {
let timeScale = CMTimeScale(NSEC_PER_SEC)
let rhs = CMTime(seconds: interval, preferredTimescale: timeScale)
let time = CMTimeAdd(player.currentTime(), rhs)
changePosition(time: time)
}
private func updateSlider() {
let time = player.currentItem?.currentTime() ?? CMTime.zero
if itemDuration != 0 {
slider.value = Float(CMTimeGetSeconds(time) / itemDuration)
}
}
private func changePosition(time: CMTime) {
let rate = player.rate
// いったんplayerをとめる
player.rate = 0
// 指定した時間へ移動
player.seek(to: time, completionHandler: {_ in
// playerをもとのrateに戻す(0より大きいならrateの速度で再生される)
self.player.rate = rate
})
}
// 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
}
func updateNowPlaying(time: Double) {
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = itemURL?.lastPathComponent ?? ""
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time
if let duration = player.currentItem?.duration {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(duration)
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
// MARK: Periodic Time Observer
func addPeriodicTimeObserver() {
// Notify every half second
let timeScale = CMTimeScale(NSEC_PER_SEC)
let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time,
queue: .main)
{ [weak self] time in
// update player transport UI
DispatchQueue.main.async {
// sliderを更新
self?.updateSlider()
// NowPlayingInfoCenterを更新
self?.updateNowPlaying(time: CMTimeGetSeconds(time))
}
}
}
func removePeriodicTimeObserver() {
if let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}
// 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
}
// 早送り
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: skipInterval)]
commandCenter.skipForwardCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteSkipForward(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
// 早戻し
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: skipInterval)]
commandCenter.skipBackwardCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteSkipBackward(commandEvent)
return MPRemoteCommandHandlerStatus.success
}
// ポジション移動(バーのボタン位置変更)
commandCenter.changePlaybackPositionCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
self.remoteChangePlaybackPosition(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) {
// (略)
}
/// リモートコマンドで「早送り」ボタンが押された時の処理
func remoteSkipForward(_ event: MPRemoteCommandEvent) {
skipForward()
}
/// リモートコマンドで「早戻し」ボタンが押された時の処理
func remoteSkipBackward(_ event: MPRemoteCommandEvent) {
skipBackward()
}
/// リモートコマンドでシークバー(プルグレスバー)の位置を変更した時の処理
func remoteChangePlaybackPosition(_ event: MPRemoteCommandEvent) {
if let evt = event as? MPChangePlaybackPositionCommandEvent {
let timeScale = CMTimeScale(NSEC_PER_SEC)
let time = CMTime(seconds: evt.positionTime, preferredTimescale: timeScale)
changePosition(time: time)
}
}
}