[iOS][Swift]リモートコマンドイベントに対応する

(2019/11/29 更新 iOS 13, Xcode 11, Swift 5.1)

たいていのオーディオ系アプリでは、イヤホンやロック画面、コントロールセンターからの再生や停止ができます。

iPhoneのイヤホンではセンターボタンの操作でオーディオの操作ができます。
ss2014-10-30-01
再生・一時停止:センターボタンを1回押す
次の区間へ:センターボタンをすばやく2回押す
前の区間へ:センターボタンをすばやく3回押す

といった操作ができます。

コントロールセンターでは、赤丸で囲った部分でオーディオアプリの再生制御ができます。
ss2015-09-25-01

iPhone用イヤホン操作などのイベントはリモートコマンドイベントとよばれています。
このリモートコマンドイベントを受け取るために、必要な実装を紹介します。

Appleのドキュメントを見てみます。
Handling External Player Events Notifications | Apple Developer Documentation
Remote Command Center Events | Apple Developer Documentation

以前の版(2013年)では、remoteControlReceivedWithEvent:を使った方法が紹介されていましたが、
2015年版では、MPRemoteCommandオブジェクトを使ってハンドラを登録する方法が記されています。

Handling External Player Events Notificationsの記事に
“Your app must be the Now Playing app. An app does not receive remote control events until it begins playing audio. “
(アプリが再生中でなければなりません。アプリが、音声の再生を開始するまでリモートコントロールイベントを受け取りません)
とあります。アプリがオーディオ再生開始以降でないと、リモートコントロールイベント(リモートコマンドイベント)を受け取らないのでご注意ください。

* オーディオを使用せずにイヤホンからの操作でページをめくるアプリなどは製作できません

* オーディオを使用しているアプリでも、オーディオ再生が開始されたあとでないとイヤホンからの操作はできません

オーディオ再生に関しては以前の記事をご参照ください。
参考:[iOS][Swift]AVAudioPlayerを使う(リソースファイルを使う) – nackpan Blog

MPRemoteCommandにハンドラを登録してリモートコマンドイベントを制御

MPRemoteCommandCenter Class Reference
MPRemoteCommand Class Reference

MPRemoteCommandCenterがもつリモートコマンドが実行されたときに行う処理を登録します。
リモートコマンドには、
・togglePlayPauseCommand(イヤホンのセンターボタンを押した)
・playCommand(コントロールセンターのプレイボタンを押した)
・pauseCommand(コントロールセンターのポーズボタンを押した)
などがありますので、それぞれのコマンドにたいしてどのようなアクションを起こすかを記述します。

 

起動するといきなりサウンドが流れ、イヤホンやコントロールセンターから再生・停止ができるサンプルを作成しました。
Single View Appで作成。
File > Add Files to “プロジェクト” から、sound.mp3と名前をつけたオーディオファイルを追加。

ViewController.swift

import UIKit
import AVFoundation
import AVKit
import MediaPlayer


class ViewController: UIViewController {
    
    var audioPlayer:AVAudioPlayer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        addRemoteCommandEvent()
        
        setupPlayer()
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    
    func setupPlayer() {
        let fileName: String? = "sound"
        let fileExtension:String? = "mp3"
        if let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) {
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: url)
                audioPlayer?.prepareToPlay()
                audioPlayer?.play()
            } catch {
                // プレイヤー作成失敗
                // その場合は、プレイヤーをnilとする
                audioPlayer = nil
            }
            
        } else {
            // urlがnilなので再生できない
            fatalError("Url is nil.")
        }
    }
    
    
    
    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.togglePlayPauseCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remoteTogglePlayPause(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.playCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePlay(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.pauseCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePause(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.nextTrackCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remoteNextTrack(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })
        commandCenter.previousTrackCommand.addTarget(handler: { [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePrevTrack(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        })

        
    }
    
    func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        print("イヤホンのセンターボタンを押した時の処理")
        // (今回は再生中なら停止、停止中なら再生をおこなっています)
        if let player = audioPlayer {
            if player.isPlaying {
                player.stop()
            } else {
                player.play()
            }
        }
    }
    
    func remotePlay(_ event: MPRemoteCommandEvent) {
        // プレイボタンが押された時の処理
        print("プレイボタンが押された時の処理")
        // (今回は再生をおこなっています)
        if let player = audioPlayer {
            player.play()
        }
    }
    
    func remotePause(_ event: MPRemoteCommandEvent) {
        // ポーズボタンが押された時の処理
        print("ポーズが押された時の処理")
        // (今回は停止をおこなっています)
        if let player = audioPlayer {
            player.stop()
        }
    }
    
    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    func remotePrevTrack(_ event: MPRemoteCommandEvent) {
        // 「前へ」ボタンが押された時の処理
        // (略)
        
    }
}

AVPlayerViewControllerを使用した動画プレーヤーの場合

オーディオアプリの例では、リモートコマンドへハンドラを登録する必要がありました。
AVPlayerViewControllerを使用した動画プレーヤーの場合では、リモートコマンド関連のコードを一切追加せずに基本のプレーヤーを設定した段階で、リモートからの操作は可能です。

画面中央のボタンを押すと動画が再生され、イヤホン・コントロールセンターから一時停止・再生が可能なサンプルを作成します。

AppleDocumentationを参考にします。

Building a Basic Playback App

Appleの記事ではサーバー上の動画を再生していますが、今回は簡便のためにプロジェクトに追加した動画を再生します。

Single View Appで作成。
File > Add Files to “プロジェクト” から、movie01.mp4と名前をつけた動画ファイルを追加。
ボタンをMain.storyboard上に配置し、ActionでViewController.swiftと結びつける。
そのさいのメソッド名をsetupPlayer(_ sender: Any)としています。

Main.storyboard

ViewController.swift

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func setupPlayer(_ sender: Any) {
        guard let url = Bundle.main.url(forResource: "movie01", withExtension: "mp4") else {
            return
        }
        
        let player = AVPlayer(url: url)
        
        let controller = AVPlayerViewController()
        controller.player = player
        
        present(controller, animated: true) {
            player.play()
        }
    }
}

関連

[iOS]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応 – nackpan Blog
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) – nackpan Blog

投稿者:

nackpan

nackpan

iOSアプリを作っています。 リピーティングに便利な「語学学習支援プレイヤー」つくりました。

「[iOS][Swift]リモートコマンドイベントに対応する」への6件のフィードバック

  1. 私はKuwabara Kazutoと申します。swift初心者で、現在、リモコンのセンターボタンでpause_playが可能な、動画(mp4)再生アプリを作っており、Xcode:Version 10.1 、swift4を使用してます。
    リモートコントロールイベント対応の記事掲載、ありがとうございます。この内容について、可能であれば教えて頂きたく、よろしくお願いします。
    早速、下記の1)2)ように書いてみましたが、
    1)addRemoteControlEvent()を、viewDidLoadに記載
    2)classの下に、下記の①②を記載
    ①func addRemoteControlEvent() {
    let commandCenter =
      MPRemoteCommandCenter.sharedCommandCenter()
     commandCenter.togglePlayPauseCommand.addTarget(self, action: “remoteTogglePlayPause:”)
    commandCenter.playCommand.addTarget(self, action: “remotePlay:”)
    }
    ②func remoteTogglePlayPause(event: MPRemoteCommandEvent) {
    print(“remote”)
    }
    ViewControllerを起動すると、appDelegate.swiftのclassのところで、以下の3)のようにSIGABRTエラー表示が出ます。
    3)class AppDelegate: UIResponder, UIApplicationDelegate, AVAudioPlayerDelegate{ =Thread 1: signal SIGABRT
    Xcodeのprint表示エリアには、以下のエラーが表示されます。
    2019-02-16 ***MP4 playback[1160:248790] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘+[NSInvocation _invocationWithMethodSignature:frame:]: method signature argument cannot be nil’
    *** First throw call stack:
    (0x183869ea4 0x182a39a50 0x18375b428 0x194f5cfd4 0x1009876bc 0x1009a1e18 0x1009a3e78 0x1b0441890 0x1b0441cc0 0x1b0374838
    ・・・(途中省略)0x1009ad12c 0x1832b2bb4)
    libc++abi.dylib: terminating with uncaught exception of type NSException
    (lldb)
    私のプログラムのclassは、
     class ViewController: UIViewController, AVAudioPlayerDelegate{
    importは、
     UIKit、AVKit、AVFoundation、MediaPlayer
    を記載しております。

    何か、不足、間違い等をご指摘頂ければ、ありがたいです。
    どうか、よろしくお願い致します。

  2. Kuwabara Kazutoさん、こんにちは。
    コメントありがとうございます。
    情報が古くなっていて申し訳ないです。

    Xcode 10.1 + Swift 4.2の環境でのコードに書き改めましたので、参考にしてください。
    また、記事に追記したのですが、リモートコントロールイベントはアプリでの音声が再生されてからでないとイベントを受け取らないのでご注意ください。

  3. nackpan様
    返信ありがとうございます。早速、試してみたところ、エラーは出なくなりましたが、動作させることができておりません。
    追記の、「アプリでの音声が再生されてから***」に従い、viewDidLoadに記載していた、”addRemoteCommandEvent()”を、再生コマンドの
    ・playerLayer.player?.play()
    の後、つまり再生開始後に実行できるように変更し、動画再生後、リモコンのセンターボタンを押しましたが、何も反応がありません。そこで、以下の質問があります。
    (1)何か、間違っていること、不足していること、注意点等はありますでしょうか。
    (2)動画プレーヤには、無効なのでしょうか。
    すみませんが、ご教示頂ければ、幸いです。
    どうか、よろしくお願いします。

  4. Kuwabara Kazutoさん、こんにちは。
    コメントありがとうございます。

    「アプリでの音声が再生されてから***」の部分、誤解を与えて申し訳ありません。
    addRemoteCommandEvent()は、viewDidLoadに記載していただいてOKです。
    * オーディオを使用せずにイヤホンからの操作でページをめくるアプリなどは製作できない
    * オーディオを使用しているアプリでも、オーディオ再生が開始されたあとでないとイヤホンからの操作はできない
    と伝えたかったのでした。

    オーディオアプリでのリモートコマンドの例でコードが断片的でわかりづらかったので、ViewController.swift全体を記載しました。参考になればさいわいです。

    また、AVPlayerViewControllerを実験してみたところ、こちらではもともとリモートコマンドをカバーしているようです。リモートコマンド関連を登録するコードがなくても、イヤホンからの操作ができます。
    AppleのDocumentationを参考にしたサンプルを記載したので、参考になれば幸いです。

  5. nackpan様、Kuwahara Kazutoです。
    アドバイス、ありがとうございました。
    “addRemoteCommandEvent( )”を、viewDidLoad( )に戻し、
    動画再生後に、リモコンセンターボタンを押すと、動作しました。

    しかしながら、Stopボタンを実装し、イニシャル画面へ移動(同時に録画停止)後、
    もう一度、この画面へ戻ってきて動画再生をした時に、
    ・タップによるplay/pauseは動作する
    のですが、
    ・リモコンセンターボタンを押すとハングする
    という現象になってしまします。
    一度、再生画面から、別の画面へ遷移する時に、何らかの
    処置が必要なのでしょうか。
    ・遷移時には、performSegue()
    ・戻る(Stop)時は、dismiss()
    を使用しております。

    Stopボタンを押した時に、色々と設定をしてみたのですが、同じ現象が
    起きてしまい、解決策がわからない状態です。

    下記に、
    ①initialViewController.swift:
     起動画面で、ボタンを押せば、ViewController画面へ移動します。
    ②ViewController.swift:
     AVPlayerを使った、シンプルな動画再生プログラムで、ツールバーでの
     タップによるplay/pauseボタン対応と、リモコンセンターボタンによる
     play/pauseボタン対応を実装しています。
    の2つのソースコードを、記載しております。
    ③は、エラーの内容です。

    もしお時間あるときに、アドバイスを頂ければありがたいです。
    よろしくお願い致します。

    ①InitialViewController.swift:
    import UIKit
    import AVFoundation
    import AVKit
    import MediaPlayer

    class InitialViewController: UIViewController {
    @IBAction func start(_ sender: Any) {
    performSegue(withIdentifier: “viewController”, sender: nil)
    }
    override func viewDidLoad() {
    super.viewDidLoad()
    }
    }

    ②ViewController.swift
    import AVFoundation
    import UIKit
    import AVKit
    import MediaPlayer

    // Bundle Resourcesから***.mp4を読み込んで再生の準備
    var path = Bundle.main.path(forResource: “Sample”, ofType: “mp4”)!
    //var player = AVPlayer(url: URL(fileURLWithPath: path))

    class ViewController: UIViewController/*, AVAudioPlayerDelegate*/{ //class

    //var path = Bundle.main.path(forResource: “Sample”, ofType: “mp4”)!
    var player = AVPlayer(url: URL(fileURLWithPath: path))
    var statusIndicator:Int = 1

    func addRemoteCommandEvent() {
    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
    }
    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    print(“イヤホンのセンターボタンを押した時の処理”)
    var items = toolBar.items!
    var toggleButton:UIBarButtonItem
    if statusIndicator == 1/*音楽を再生中*/ {//if(1)
    // 音楽の一時停止処理
    toggleButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.play, target: self, action: #selector(ViewController.PlayPauseButton(_:)))//playボタン表示の実施
    //一時停止
    player.pause()
    print(“pause”)
    statusIndicator = 2
    PlayPauseButton = toggleButton
    }//if(1)
    else {//else(1)
    // 音楽の再生処理
    toggleButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.pause, target: self, action: #selector(ViewController.PlayPauseButton(_:)))//pauseボタン表示の実施
    //再生
    player.play()
    print(“play”)
    statusIndicator = 1
    PlayPauseButton = toggleButton
    }//else(1)
    items[0] = toggleButton
    toolBar.setItems(items, animated: false)
    }

    @IBAction func stop(_ sender: Any) { //Stopボタン
    self.dismiss(animated: true, completion: nil)
    //player.rate = 0.0
    //player.pause()
    //statusIndicator = 1
    //addRemoteCommandEvent()
    } //Stopボタン

    @IBOutlet weak var toolBar: UIToolbar!
    @IBOutlet weak var PlayPauseButton: UIBarButtonItem!
    @IBAction func PlayPauseButton(_ sender: UIBarButtonItem) { //func(1)
    var items = toolBar.items!
    var toggleButton:UIBarButtonItem
    if statusIndicator == 1/*音楽を再生中の場合*/ { //if(1)
    // 音楽の一時停止処理
    toggleButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.play, target: self, action: #selector(ViewController.PlayPauseButton(_:)))//playボタン表示の実施
    //一時停止
    player.pause()
    print(“pause”)
    statusIndicator = 2 //<=音楽を一時停止の場合
    PlayPauseButton = toggleButton

    } //if(1)
    else { //else(1)
    // 音楽の再生処理
    toggleButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.pause, target: self, action: #selector(ViewController.PlayPauseButton(_:)))//pauseボタン表示の実施
    //再生
    player.play()
    print("play")
    statusIndicator = 1
    PlayPauseButton = toggleButton
    }//else(1)
    items[0] = toggleButton
    toolBar.setItems(items, animated: false)
    } //func(1)

    override func viewDidLoad() {
    super.viewDidLoad()
    addRemoteCommandEvent()
    // Bundle Resourcesから***.mp4を読み込んで再生
    //var path = Bundle.main.path(forResource: "Sample", ofType: "mp4")!
    //var player = AVPlayer(url: URL(fileURLWithPath: path))
    player.actionAtItemEnd = .none // default: pause
    print("playerplay")
    player.play()

    // AVPlayer用のLayerを生成
    let playerLayer = AVPlayerLayer(player: player)
    playerLayer.frame = self.view.bounds
    self.view.layer.addSublayer(playerLayer)
    view.layer.insertSublayer(playerLayer, at: 0)// 動画をレイヤーとして追加
    }
    } //class

    ③エラー:unrecognized selector ・・・
    =Thread 1: signal SIGABRT
    2019-02-20 MP4 playback] -[_MPWeakInvocationTarget remoteTogglePlayPause:]: unrecognized selector sent to instance 0x2824d1b40
    2019-02-20 MP4 playback[***] Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_MPWeakInvocationTarget remoteTogglePlayPause:]: unrecognized selector sent to instance 0x2824d1b40'
    *** First throw call stack:
    (0x183869ea4 0x182a39a50 0x183782b14 0x18386f7bc 0x18387146c 0x183871610 0x18374f340 0x194f5c694 0x194ed70e8 0x194ed6f38 0x194ed86cc 0x18f1bbf08 0x1019a322c 0x101994dc8 0x1019a2a78 0x1837f9ce4 0x1837f4bac 0x1837f40e0 0x185a6d584 0x1b0a08c00 0x1009e8618 0x1832b2bb4)
    libc++abi.dylib: terminating with uncaught exception of type NSException
    (lldb)

  6. nackpan様、Kuwabara Kazutoです。
    2/20に送信した、エラーの件、自己解決しましたので、報告します。
    ・遷移時には、performSegue()
    ・戻る(Stop)時は、dismiss()
    としておりましたが、戻る(Stop)時の方法を、performSegue()に
    することで、エラーがなくなりました。
    引き続き、検討継続しますが、取り急ぎ報告とさせて頂きます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください