はじめに
この記事では、Nexus PlayerやFire TVなどのAndroid OS(またはFire OS)を搭載したAndroid TV対応端末使用時における、TV端末の電源ON/OFF、入力切替を検知する方法を紹介します。
用語定義
似たような用語が多く出てくるので、わかりやすいよう、本記事では以下のように定義して使用させていただきます🙇♂️
用語 | 概要 |
---|---|
外部端末 | Nexus Player/Fire TVなどの付属のHDMIをテレビ端末に接続する端末 |
内蔵端末 | BRAVIAなどのAndroid OSを内蔵するテレビ端末 |
TV端末 | Android OSの内蔵有無に関わらない全てのテレビ端末やディスプレイ端末 |
オーディオ端末 | 外付けスピーカーなどの付属のHDMIをテレビ端末に接続する端末 |
アプリ | 開発対象のAndroid TVアプリケーション |
背景
Nexus PlayerやFire TVなどを使って、TV端末で映像コンテンツを楽しんでいる方も多いと思います。
みなさん、これらの端末ってどうやって使ってますか?
見終わったら、TV端末のリモコンぽちーってやってませんか?
そうなんです!実は、TV端末の電源を消しただけでは外部端末の電源が供給されている限り、外部端末は起動しっぱなしなんですよ!😨
これじゃあ、実際にユーザがいつアプリの使用をやめたかっていうのが分からないですよね...
ということで、何かいい方法はないかなーと調べた結果を紹介します🙋♀️
実は、AmazonのDeveloperサイトにFire TV上のHDMIイベントの処理という項目で、説明がされています。
しかし、このドキュメントだけだと分からない様々なユースケースがあるので、今回はその検証結果についてもまとめておきます。
使用するAPIについて
BroadcastReceiver
を使用して、取得できるイベントの一つに ACTION_HDMI_AUDIO_PLUG
というものが存在します。
このイベントは AudioManager
クラスに定義されており、映像や音声を出力するHDMIの接続状態を通知するイベントです。
ここで、出力を強調している訳は、本記事を読み進めていくと理解できると思います。
この ACTION_HDMI_AUDIO_PLUG
というイベントは、物理的にHDMIコードを接続/切断する場合だけでなく、TV端末の電源をON/OFFにした場合などにも通知されます。
これを利用して、TV端末の電源ON/OFF、入力切替を検知します。
出力HDMIの接続/切断状態について
ACTION_HDMI_AUDIO_PLUG
を利用して、出力HDMIの接続状態変化を通知することは分かったと思います。
では、出力HDMIの接続状態変化が起こった時の状態を取得する方法を説明します。
AudioManager
クラスの中には EXTRA_AUDIO_PLUG_STATE
というものが存在します。ACTION_HDMI_AUDIO_PLUG
イベントが飛んできた時に、この EXTRA_AUDIO_PLUG_STATE
の値を Intent
から取得することができます。
EXTRA_AUDIO_PLUG_STATE
の値は、出力HDMI接続時に 1
となり、出力HDMI切断時に 0
となります。
つまり、TV端末の電源がONになった時
や入力切替によって、別の入力からアプリを起動している入力に切り替えられた時
には 1
になります。
逆に、TV端末の電源がOFFになった時
や入力切替によって、アプリを起動している入力から別の入力に切り替えられた時
には 0
になります。
実装
説明はこのくらいにして、実装を見ていきましょう!
https://github.com/ronnnnn/HDMIPlugStateSample
こちらにサンプルアプリを作成したので、ぜひ参考にしてください🙏
BroadcastReceiver
について
ACTION_HDMI_AUDIO_PLUG
イベントを処理するには、BroadcastReceiver
を継承したクラスを作成します。
そして、BroadcastReceiver#onReceiver
をOverrideして、その中にイベントを受け取った時にやりたいことを実装していきます。
この辺は基本的な BroadcastReceiver
による Intent
アクションのハンドリング方法と同じですね🙆♀️
class HDMIPlugStateBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val PLUGGED_IN: Int = 1
private const val UNPLUGGED: Int = 0
}
override fun onReceive(context: Context?, intent: Intent?) {
// ここで ACTION_HDMI_AUDIO_PLUG イベントを処理する
intent ?: return
if (intent.action == AudioManager.ACTION_HDMI_AUDIO_PLUG) {
val plugState = intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, -1) // こいつが現在の接続状態
val address = intent.getStringExtra("address")
val state = intent.getIntExtra("state", -1)
val portName = intent.getStringExtra("portName")
when (plugState) {
UNPLUGGED -> {
Timber.d(StringBuilder(" \n")
.append("--------------- HDMI is unplugged ---------------\n")
.append("address is ${address}\n")
.append("state is ${state}\n")
.append("port name is ${portName}\n")
.append("---------------------------------------------------")
.toString()
)
}
PLUGGED_IN -> {
Timber.d(StringBuilder(" \n")
.append("--------------- HDMI is plugged in ---------------\n")
.append("address is ${address}\n")
.append("state is ${state}\n")
.append("port name is ${portName}\n")
.append("---------------------------------------------------")
.toString()
)
}
else -> Timber.e("invalid ACTION_HDMI_AUDIO_PLUG value")
}
}
}
}
ここで注目したいのが、 Intent
を経由して取得できる値についてです。
EXTRA_AUDIO_PLUG_STATE
の値を取得できるのは先で説明しましたが、このイベントの Intent
には、これ以外にも "address"
、"state"
、"portName"
というkeyの値が付随してくるようです。
私の環境では "state"
の値は EXTRA_AUDIO_PLUG_STATE
の値と同じで、"state"
、"portName"
の値は空でした。ドキュメントにもこれらのkeyについての説明を見つけられなかったので、何かわかる方がいましたら教えてください🙋♀️
BroadcastReceiver
の登録について
ドキュメントにも記載されていますが、今回説明している ACTION_HDMI_AUDIO_PLUG
のイベントは、Context#registerReceiver
を使用して BroadcastReceiver
を継承したクラスを登録する必要があります。
AndroidManifest.xml
にこのクラスを登録してもイベントを受け取ることができませんので、注意してください。
今回は、Application
クラスを継承したクラスで登録、解除を行っています。
class App : Application() {
private val hdmiPlugStateBroadcastReceiver: HDMIPlugStateBroadcastReceiver =
HDMIPlugStateBroadcastReceiver()
override fun onCreate() {
super.onCreate()
initializeReceiver()
}
override fun onTerminate() {
terminateReceiver()
super.onTerminate()
}
private fun initializeReceiver() {
registerReceiver(
hdmiPlugStateBroadcastReceiver,
IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)
)
}
private fun terminateReceiver() {
unregisterReceiver(hdmiPlugStateBroadcastReceiver)
}
}
Context#registerReceiver
の第二引数に ACTION_HDMI_AUDIO_PLUG
の IntentFilter
を渡すことをお忘れなく!
EXTRA_AUDIO_PLUG_STATE
の値について
これまで説明してきた、EXTRA_AUDIO_PLUG_STATE
の値が、どういった場合に変化するかを検証したので、下にまとめておきます。
他にもこのパターンの時はどうなんだ🤔などありましたら、教えてください!
↓の値が取得できるのは基本的に、外部端末の電源が入っており(コンセント電源供給)、アプリが起動していることが前提です。
パターン |
EXTRA_AUDIO_PLUG_STATE の値 |
備考 |
---|---|---|
外部端末付属のHDMIをTV端末から切断 | 0 |
テレビ端末でないディスプレイ端末での検証では 0 のみが取れたが、BRAVIAでの検証では 0 -> 1 -> 0 を確認 |
外部端末付属のHDMIをTV端末に接続 | 1 |
|
TV端末の入力切替で別の入力へ遷移 | 0 |
|
TV端末の入力切替で別の入力から移動 | 1 |
|
TV端末の電源off (TV端末の主電源on) | 0 |
|
TV端末の電源off (TV端末の主電源も同時にoff) | - | アプリが強制終了されるため何も取れない |
TV端末の電源off (外部端末の電源も同時にoff) | - | TV端末のUSBポートを電源供給源としている場合には、TV端末の電源offと同時にアプリも強制終了されるため何も取れない |
TV端末の電源on | 1 |
|
アプリ起動中に外部端末の電源off->電源on | 1 |
外部端末の再起動時にアプリが裏で起動され、 1 のイベントが発火する (FireTVのみでしか検証してない) |
ターゲットとする外部端末が刺さっているHDMIポート以外のHDMIポートへのHDMI接続/切断 | - | 別の外部端末がアプリを所有し、起動していてもイベントは発火されない |
外部端末でアプリの起動 | 1 |
ただし、アプリが起動中の場合はイベントは発火されない |
オーディオ端末の接続・切断 | - |
おまけ
今回はNexus PlayerやFire TVなどのTV端末に接続して使用するAndroid TV端末の場合を説明してきました。
ですが、BRAVIAなどのAndroid OS内蔵端末はどうなんだ🤔と思った方もいるかと思います。
私が行った検証の限りだと、内蔵端末の場合は、基本的に外部端末のように出力HDMIを持たないため、TV端末のHDMIポートにHDMIが接続/切断されても ACTION_HDMI_AUDIO_PLUG
イベントは発火されません。
ただし、オーディオ端末を使用して、内蔵端末の音声をHDMI経由で出力している場合には、オーディオ端末のHDMI接続/切断により ACTION_HDMI_AUDIO_PLUG
イベントが発火されてしまいます😭
そのため、内蔵端末において、TV端末の電源ON/OFF、入力切替を検知するには、Application
や Activity
などのライフサイクルを利用するのがいいかと考えています。
ちょっとニッチな内容だったかもしれませんが、最後までお読みいただきありがとうございます!
何かありましたら、ぜひコメントください🙏