LoginSignup
79
77

More than 5 years have passed since last update.

iOSでヘッドフォンを抜いた時のイベントをフックする

Last updated at Posted at 2013-03-10

AVFoundationフレームワークのAVPlayerを使った動画再生などでは、ヘッドフォンやイヤフォンを抜いた際、動画の再生が強制的に止まるようになっており、このイベントをフックできないと動画プレイヤーのボタンの状態は動画が再生されている状態のままになってしまいます。

Googleの純正YouTubeアプリではこのような仕様にしっかりと対応しています。

Googleの純正YouTubeアプリ
https://itunes.apple.com/jp/app/youtube/id544007664

ソースコード

ヘッドフォンを抜いた時のイベントをフックするクラスをARCで作成し、ソースコードはgithubにおいています
https://github.com/yimajo/HeadphonesPluggedDemo

下記の説明はまず動作させてから読むことで理解が深まると思います。

ヘッドフォンジャックの情報を取得

現在の情報はAudioToolboxフレームワークの、
AudioSessionGetProperty関数を使います。

YIHeadphonesDetector.m
- (BOOL)headphonesArePlugged
{
    BOOL result = NO;
    CFStringRef route;
    UInt32 propertySize = sizeof(CFStringRef);

    if (AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route) == 0)  {
        //__bridge_transferにしてrouteがreleaseされるようにする
        NSString *routeString = (__bridge_transfer NSString *)route;

        if ([routeString isEqualToString:@"Headphone"] == YES) {
            result = YES;
        }
    }
    return result;
}

__bridge_transferキャストについて

ARCが有効な場合、CFStringRefのようなC言語のオブジェクトから、Objective-Cのオブジェクトにキャストするには__bridgeキャストを行う必要があります。

ヘッドフォンジャックの情報を取得するAudioSessionGetProperty関数の引数にしたCFStringRefのrouteオブジェクトは、自動でreleaseされるわけではなく、明示的にreleaseすることが必要になります。ここではNSStringオブジェクトへキャストする際にbridgeではなく、キャスト元をreleaseすることができるbridge_transferを使っています。

ここでbridge_transferではなくbridgeにした場合、おそらくrouteオブジェクトは下記のようにreleaseする必要が出てくるでしょう。

NSString *routeString = (__bridge NSString *)route;
CFRelease(route); //CFStringRefはreleaseにCFReleaseを使う

ヘッドフォンジャックの抜き差し時のコールバック

ヘッドフォンジャックの抜き差しについては、「オーディオハードウェア経路の変化」を見ることによって実現出来ます。

そのためには、オーディオセッションに対するプロパティリスナーコールバック関数を登録します。
下記はinitメソッドの中で登録している例です。

YIHeadphonesDetector.m
- (id)init
{
    if (self = [super init]) {
        AudioSessionInitialize(NULL, NULL, NULL, NULL);

        //プロパティリスナーコールバック関数を登録します
        AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, //監視したいプロパティの識別子
                                        audioRouteChangeListenerCallback,       //コールバック関数への参照
                                        (__bridge void *)self);  //呼び出されるコールバック関数に渡されるためのデータです

    }
    return self;
}

initメソッドでコールバック関数を登録した場合は、deallocメソッドでコールバック関数を削除しておくのが良いと思います。明示的に削除しない場合、宣言したオブジェクトが削除されても実行され続けます。

YIHeadphonesDetector.m
- (void)dealloc
{
    //プロパティリスナーコールバック関数を削除します
    AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_AudioRouteChange,
                                                   audioRouteChangeListenerCallback,
                                                   (__bridge void *)self);
}

コールバック関数の実装は次のような感じ。delegateでViewControllerに処理を委譲しています。

YIHeadphonesDetector.m
/*
 @param inUserData オーディオセッションを初期化する際に指定したデータへのポインタです。
 このcallbackはObjctive-Cのクラス外で実装されているため、callback内で特定のオブジェクトにメッセージを送るためにはObjective-Cのポインタを渡す必要があります。
 @param inPropertyID このコールバック関数で通知を受ける対象プロパティの識別子です。
 @param inPropertyValueSize inPropertyValueパラメータに渡されたデータのサイズ(バイト単位)です
 @param inPropertyValue 監視しているプロパティの値です。
 kAudioSessionProperty_AudioRouteChangeを指定すると型はCFDictionaryRefとなります。
 */
void audioRouteChangeListenerCallback(void *inUserData,
                                       AudioSessionPropertyID inPropertyID,
                                       UInt32 inPropertyValueSize,
                                       const void *inPropertyValue)
{
    if (inPropertyID != kAudioSessionProperty_AudioRouteChange) {
        return;
    }

    CFDictionaryRef routeChangeDictionary = inPropertyValue;
    CFNumberRef routeChangeReasonRef = CFDictionaryGetValue (routeChangeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_Reason));
    SInt32 routeChangeReason;
    CFNumberGetValue(routeChangeReasonRef, kCFNumberSInt32Type, &routeChangeReason);

    if (routeChangeReason == kAudioSessionRouteChangeReason_OldDeviceUnavailable
        || routeChangeReason == kAudioSessionRouteChangeReason_NewDeviceAvailable) {

        YIHeadphonesDetector *headphonesDetector = (__bridge YIHeadphonesDetector *)inUserData;

        if ([headphonesDetector.delegate respondsToSelector:@selector(headphonesDetectorStateChanged:) ]) {
            [headphonesDetector.delegate headphonesDetectorStateChanged:headphonesDetector];
        }
    }
}

__bridgeキャストについて

上の例ではコールバック関数登録にselfを__bridgeでキャストしています。

//プロパティリスナーコールバック関数を登録します
AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange,
                                audioRouteChangeListenerCallback,
                                (__bridge void *)self);

コールバック関数内でも

YIHeadphonesDetector *headphonesDetector = (__bridge YIHeadphonesDetector *)inUserData;

としていますが、これはキャスト時にキャスト元オブジェクトをretainもreleaseもしたくないためです。

該当のオブジェクトがdealloc時にコールバック関数を削除する仕組みにしているため、コールバック関数に渡す該当のオブジェクトselfの参照カウンタの管理(+1にしたり-1にしたり)を明示的にする必要がないためとなります。

参考

オーディオセッションプログラミングガイド
https://developer.apple.com/jp/devcenter/ios/library/documentation/AudioSessionProgrammingGuide.pdf

端末で音声再生が可能かチェックする
http://shiffon.dtiblog.com/blog-entry-47.html

[iOS5] ARC : Autorelease, キャスト, 環境設定
http://blog.natsuapps.com/2011/11/ios5-arc-autorelease-bridge-xcode.html

79
77
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
79
77