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


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

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

![](http://a67.phobos.apple.com/us/r1000/102/Purple/v4/f6/53/32/f653322a-2196-1e93-8169-a649571935c6/mzl.phgdfmid.320x480-75.jpg)

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


## ソースコード

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

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

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

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

```objc: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する必要が出てくるでしょう。

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

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

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

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

```objc:YIHeadphonesDetector.m
- (id)init
{
	if (self = [super init]) {
		AudioSessionInitialize(NULL, NULL, NULL, NULL);
		
		//プロパティリスナーコールバック関数を登録します
		AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, //監視したいプロパティの識別子
										audioRouteChangeListenerCallback,       //コールバック関数への参照
										(__bridge void *)self);  //呼び出されるコールバック関数に渡されるためのデータです

	}
	return self;
}
```

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

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


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

```objc: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でキャストしています。

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

コールバック関数内でも

```objc
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