Help us understand the problem. What is going on with this article?

かゆいところに手が届きそうなAVFoundationとMediaPlayerによる音楽再生アプリ作成メモ

More than 5 years have passed since last update.

はじめに

「無料で音楽聴き放題!! - ネットラジオ」というアプリをリリースしました。AppleのiTunes Radioが日本ではリリースされないのでそれっぽいものをという感じで、ストリーミングで流れている楽曲で気に入ったものがあればiTunesで購入できるようにしています。

https://itunes.apple.com/jp/app/nettorajio-j-popmo-wu-liaode/id769979888?mt=8

以降のメモはそのとき調査して実装したものです。音楽プレイヤーアプリはゲームアプリともツール系アプリとも違う独特の処理を必要としているので、あれどうやるんだっけ的な物になっています。

IMG_1091.jpg

画面ロック時の処理

ロック時でも再生を続ける

プロジェクト設定からBackground Audio&AirPlayをONにする。また次のようなコードを実装する。

AVAudioSession *session = [AVAudioSession sharedInstance];

//ロック時も再生のカテゴリを指定
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
//オーディオセッションを有効化
[session setActive:YES error:nil];

上記は一度だけ最初に実行すればよい。再生を管理するシングルトンインスタンスの初期化時などにやればいい。

コントロールセンターに再生ボタンなどのコントロールを表示する

ロック画面やコントロールセンターの再生ボタンを表示するには次のようにする

[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];

再生ボタンなどのイベントを処理するには次のように実装する。

- (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent
{
    if (receivedEvent.type == UIEventTypeRemoteControl) {

        switch (receivedEvent.subtype) {

            case UIEventSubtypeRemoteControlPlay:
            case UIEventSubtypeRemoteControlPause:
            case UIEventSubtypeRemoteControlTogglePlayPause:
                [self playOrPause];
                break;

            default:
                break;
        }
    }
}

ただし、ViewControllerなどでこの処理を実装する場合、ファーストレスポンダにならなければいけないようなので、もし、上記のメソッドをViewControllerなどで実装するのであればファーストレスポンダの登録を行う。

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self becomeFirstResponder];
}

大抵の場合そうだと思うけど、ViewControllerにビジネスロジックを書きたくはないとおもうので、特定のオブジェクトにメッセージを送るようにするにはAppDelegate.mでリモートコントロールイベントを受け取るremoteControlReceivedWithEvent:メソッドをオーバーライドし、ビジネスロジックを記述するインスタンスに処理を渡すようにする。

AppDelegate.m
- (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent
{
    [[MusicPlayManager sharedManager] remoteControlReceivedWithEvent:receivedEvent];
}

例として、曲の再生管理用にMusicPlayManagerというクラスを作成している。

ロック画面にジャケット画像を表示する

MPNowPlayingInfoCenterのsetNowPlayingInfo:メソッドにDictionaryで画像を渡す。

MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:[UIImage imageNamed:@"image.png”]];

NSDictionary *songInfo = @{MPMediaItemPropertyArtwork, artwork};
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo];

このDictionaryでは曲名やアーティスト名なども渡すことが出来る。

電話などの割り込みから復帰

AVAudioSessionDelegateを実装すれば良い。

AVAudioSession *session = [AVAudioSession sharedInstance];
session.delegate = self;
#pragma mark -
#pragma mark Interruption event handling
- (void)beginInterruption
{
    //自動で再生中止するのでそのときの処理
}

- (void)endInterruptionWithFlags:(NSUInteger)flags
{
    //再生再開させるようにする
}

ヘッドフォンジャックを抜いた時

ヘッドフォンをジャックから抜くとAVFoundationのAVPlayerは再生が自動で止まる。ボタンも追随して再生中から停止中へと状態を変えてあげる必要があるので

自分の記事だけどこの記事が参考になる
http://qiita.com/yimajo/items/7f74536fa2ab3cc437f5

もしくはAVAudioSessionDelegateのinputIsAvailableChangedメソッドを使ってみるのもいいかもしれない。

AVPlayer再生

AVPlayerの状態を知る

AVPlayerの状態はAVPlayerStatus列挙体をもつstatusプロパティがあるが、再生中というステータスはない。

enum {
   AVPlayerStatusUnknown,
   AVPlayerStatusReadyToPlay,
   AVPlayerStatusFailed
};
typedef NSInteger AVPlayerStatus;

再生中かどうかを知りたい場合、rateプロパティが0かどうかを判断すれば良いとStackoverflowにある

if ([AVPlayer rate] != 0.0)

Check play state of AVPlayer
http://stackoverflow.com/questions/5655864/check-play-state-of-avplayer

たしかに停止中は0.0になるが、ストリーミングなど通信を介してAVPlayerで再生をする場合は再生していなくてもrateが0.0にならないことがあった。

タイムアウトを実装する

再生中のステータスが無いため、タイムアウトはReadyToPlayからタイマーを動かし、AVPlayerでメタデータが変わっていないかなどから判断してタイマーを止めるなどで対応するしかない。これはAVPlayerによるストリーミング音楽再生だけでなく映像再生も同じ。

再生させる

RadyToPlayの状態から再生メソッドplayを実行するようにする。ストリーミング再生などでURLから初期化後すぐにplayを実行することもできるが、再生中というステータス自体がないこともあり、再生開始の状態からメソッドを実行しそのとき内部の再生状態用のフラグを変更しつつplayメソッドを実行するほうがスマート。

音楽再生マネージャクラスの設計

シングルトンな再生用マネージャクラスを設計する

ストリーミング音楽再生はAVPlayerを使うが、ViewControllerから直接AVPlayerを監視して操作するのはよくない。曲の再生は画面に依存したくないだろうし、複数のAVPlayerを使うこともないだろう。なのでシングルトンな再生用マネージャクラスを設計する。

再生用マネージャクラスの状態通知はKVOよりNotificaitonのほうが便利

AVPlayerからの状態の監視はKVOを行うが、ViewControllerやViewなどに一方的に何かがONになった状態を通知したい場合はNotificationをPOSTするのが良いと思う。ただしON/OFFの状態がトグルになる場合はKVOのほうが分かりやすいかもしれない。blocksやdelegateで1対1になってしまうのも設計上よくない。

AVPlayer =>|KVO|=> 再生用マネージャインスタンス =>|Notification POST|=>ViewController

Notificationを使わずKVOで実装してみたがAVPlayerのKVOもあり混乱してしまった。

参考

Apple: AVFoundation Framework Reference
https://developer.apple.com/library/ios/DOCUMENTATION/AVFoundation/Reference/AVFoundationFramework/_index.html

yimajo
株式会社キュリオシティソフトウェアの代表です。iOSアプリを作っています。最近はCombine frameworkガイドブック / RxSwift研究読本などを書いてます。
https://swift.booth.pm/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away