diff --git a/.gitignore b/.gitignore index 67da198..191f6d6 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json # Certificates generated by demo.py *.pem + +# recorded voice command with demo app +voice_command.wav diff --git a/README.md b/README.md index 9b8daf0..10f1077 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ For a list of the most common commands you can send to the Android TV see: [TvKe For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/b4c49ac03043b1b9c40c2f2960e466d5a3b8bd67/src/androidtvremote2/remotemessage.proto#L90). In addition to commands you can send URLs to open apps registered to handle them. See [this guide](https://community.home-assistant.io/t/android-tv-remote-app-links-deep-linking-guide/567921) for how to find deep links for apps. +Voice commands can also be sent as PCM 16-bit mono 8 kHz audio data. + ## Credits - Official [implementation](https://android.googlesource.com/platform/external/google-tv-pairing-protocol/+/refs/heads/master) of the pairing protocol in Java @@ -52,3 +54,7 @@ python src/demo.py python -m pip install build python -m build ``` + +The voice demo requires the [PyAudio](https://pypi.org/project/PyAudio/) library. +Depending on the target platform, [PortAudio](https://www.portaudio.com/) might have to be installed manually, +see [PyAudio installation](https://people.csail.mit.edu/hubert/pyaudio/) for more information. diff --git a/pyproject.toml b/pyproject.toml index 631bee4..b990dc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ test = [ "pytest", ] demo = [ + "pyaudio", "pynput", "zeroconf", ] @@ -87,6 +88,10 @@ show_error_codes = true warn_incomplete_stub = true enable_error_code = ["ignore-without-code", "redundant-self", "truthy-iterable"] +[[tool.mypy.overrides]] +module = ["pyaudio"] +ignore_missing_imports = true + [tool.codespell] skip = "*.json,*.csv,*.lock,./.git/*,./.venv/*" check-filenames = true diff --git a/src/androidtvremote2/__init__.py b/src/androidtvremote2/__init__.py index bf6c514..98a4434 100644 --- a/src/androidtvremote2/__init__.py +++ b/src/androidtvremote2/__init__.py @@ -3,6 +3,7 @@ from .androidtv_remote import AndroidTVRemote from .exceptions import CannotConnect, ConnectionClosed, InvalidAuth from .model import DeviceInfo, VolumeInfo +from .voice_stream import VoiceStream __all__ = [ "AndroidTVRemote", @@ -10,5 +11,6 @@ "ConnectionClosed", "DeviceInfo", "InvalidAuth", + "VoiceStream", "VolumeInfo", ] diff --git a/src/androidtvremote2/androidtv_remote.py b/src/androidtvremote2/androidtv_remote.py index a29cb98..ff0e4be 100644 --- a/src/androidtvremote2/androidtv_remote.py +++ b/src/androidtvremote2/androidtv_remote.py @@ -15,8 +15,9 @@ from .const import LOGGER from .exceptions import CannotConnect, ConnectionClosed, InvalidAuth from .pairing import PairingProtocol -from .remote import RemoteProtocol +from .remote import VOICE_SESSION_TIMEOUT, RemoteProtocol from .remotemessage_pb2 import RemoteDirection +from .voice_stream import VoiceStream if TYPE_CHECKING: from collections.abc import Callable @@ -45,6 +46,7 @@ def __init__( pair_port: int = 6467, loop: asyncio.AbstractEventLoop | None = None, enable_ime: bool = True, + enable_voice: bool = False, ) -> None: """Initialize. @@ -57,6 +59,7 @@ def __init__( :param loop: event loop. Used for connections and futures. :param enable_ime: Needed for getting current_app. Disable for devices that show 'Use keyboard on mobile device screen'. + :param enable_voice: Enable sending voice commands to the device. """ self._client_name = client_name self._certfile = certfile @@ -66,6 +69,7 @@ def __init__( self._pair_port = pair_port self._loop = loop or asyncio.get_running_loop() self._enable_ime = enable_ime + self._enable_voice = enable_voice self._transport: asyncio.Transport | None = None self._remote_message_protocol: RemoteProtocol | None = None self._pairing_message_protocol: PairingProtocol | None = None @@ -125,6 +129,17 @@ def volume_info(self) -> VolumeInfo | None: return None return self._remote_message_protocol.volume_info + @property + def is_voice_enabled(self) -> bool | None: + """Whether voice commands are enabled on the Android TV. + + Depends on the requested feature at AndroidTVRemote initialization and the supported + features of the device. + """ + if not self._remote_message_protocol: + return None + return self._remote_message_protocol.is_voice_enabled + def add_is_on_updated_callback(self, callback: Callable[[bool], None]) -> None: """Add a callback for when is_on is updated.""" self._is_on_updated_callbacks.append(callback) @@ -217,6 +232,7 @@ async def async_connect(self) -> None: self._on_volume_info_updated, self._loop, self._enable_ime, + self._enable_voice, ), self.host, self._api_port, @@ -400,3 +416,21 @@ def send_launch_app_command(self, app_link_or_app_id: str) -> None: raise ConnectionClosed("Called send_launch_app_command after disconnect") prefix = "" if urlparse(app_link_or_app_id).scheme else "market://launch?id=" self._remote_message_protocol.send_launch_app_command(f"{prefix}{app_link_or_app_id}") + + async def start_voice(self, timeout: float = VOICE_SESSION_TIMEOUT) -> VoiceStream: + """Start a streaming voice session. + + A ``VoiceStream`` session wrapper is returned if the voice session can be established + within the given timeout. The session needs to be closed with ``end()`` (or through the + asynchronous context manager) before a new session is started. + + :param timeout: optional timeout for session readiness. Defaults to 2 seconds. + :raises ConnectionClosed: if client is disconnected. + :raises asyncio.TimeoutError: if the device does not begin voice in time, or a voice + session is already in progress. + """ + if not self._remote_message_protocol: + LOGGER.debug("Called start_voice after disconnect") + raise ConnectionClosed("Called start_voice after disconnect") + session_id = await self._remote_message_protocol.start_voice(timeout) + return VoiceStream(self._remote_message_protocol, session_id) diff --git a/src/androidtvremote2/remote.py b/src/androidtvremote2/remote.py index 5dc545f..ae2d137 100644 --- a/src/androidtvremote2/remote.py +++ b/src/androidtvremote2/remote.py @@ -8,13 +8,14 @@ import asyncio from enum import IntFlag -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from google.protobuf import text_format from google.protobuf.message import DecodeError from .base import ProtobufProtocol from .const import LOGGER +from .exceptions import ConnectionClosed from .remotemessage_pb2 import ( RemoteDirection, RemoteEditInfo, @@ -38,6 +39,13 @@ KEYCODE_PREFIX = "KEYCODE_" TEXT_PREFIX = "text:" +# Timeout in seconds to wait for `remote_voice_begin` after sending KEYCODE_SEARCH. +VOICE_SESSION_TIMEOUT = 2.0 +# Voice data chunk size in bytes for the `remote_voice_payload` message. +VOICE_CHUNK_SIZE = 20 * 1024 +# Minimum voice data chunk size in bytes. Shield TV did not accept lower chunk sizes. +VOICE_CHUNK_MIN_SIZE = 8 * 1024 + class Feature(IntFlag): """Supported features.""" @@ -45,6 +53,9 @@ class Feature(IntFlag): PING = 2**0 KEY = 2**1 IME = 2**2 + VOICE = 2**3 + """Enables remote_voice_begin after sending KEYCODE_SEARCH""" + UNKNOWN_1 = 2**4 POWER = 2**5 VOLUME = 2**6 APP_LINK = 2**9 @@ -67,6 +78,7 @@ def __init__( on_volume_info_updated: Callable[[VolumeInfo], None], loop: asyncio.AbstractEventLoop, enable_ime: bool, + enable_voice: bool, ) -> None: """Initialize. @@ -78,6 +90,7 @@ def __init__( :param loop: event loop. :param enable_ime: Needed for getting current_app. Disable for devices that show 'Use keyboard on mobile device screen'. + :param enable_voice: Enable sending voice commands to the device. """ super().__init__(on_con_lost) self._on_remote_started = on_remote_started @@ -85,7 +98,13 @@ def __init__( self._on_current_app_updated = on_current_app_updated self._on_volume_info_updated = on_volume_info_updated self._active_features = ( - Feature.PING | Feature.KEY | Feature.POWER | Feature.VOLUME | Feature.APP_LINK | (Feature.IME if enable_ime else 0) + Feature.PING + | Feature.KEY + | Feature.POWER + | Feature.VOLUME + | Feature.APP_LINK + | (Feature.IME if enable_ime else 0) + | (Feature.VOICE if enable_voice else 0) ) self.is_on = False self.current_app = "" @@ -96,6 +115,17 @@ def __init__( self._loop = loop self._idle_disconnect_task: asyncio.Task[None] | None = None self._reset_idle_disconnect_task() + self._voice_lock = asyncio.Lock() + self._on_voice_begin: asyncio.Future[int] | None = None + + @property + def is_voice_enabled(self) -> bool: + """Voice commands enabled. + + Determined from requested features at initialization and the supported features on the + device. + """ + return self._active_features & Feature.VOICE == Feature.VOICE def send_key_command(self, key_code: int | str, direction: int | str = RemoteDirection.SHORT) -> None: """Send a key press to Android TV. @@ -158,7 +188,75 @@ def send_launch_app_command(self, app_link: str) -> None: msg.remote_app_link_launch_request.app_link = app_link self._send_message(msg) - def _handle_message(self, raw_msg: bytes) -> None: # noqa: PLR0912 + async def start_voice(self, timeout: float = VOICE_SESSION_TIMEOUT) -> int: + """Initiate a voice session and return the session id when ready. + + Sends ``KEYCODE_SEARCH`` and waits for ``remote_voice_begin``. Also sends the + initial ``remote_voice_begin`` message back to the device, as required by the + protocol, so the caller can start streaming audio chunks. + + :param timeout: Optional timeout in seconds for session readiness. Defaults to 2 seconds. + :raises ConnectionClosed: If the connection is lost. + :raises asyncio.TimeoutError: If the operation times out or a voice session is already in + progress. + :return: The voice session id, which must be used in later calls to ``send_voice_chunk``. + """ + if self.transport is None or self.transport.is_closing(): + raise ConnectionClosed("Connection has been lost") + + if self._voice_lock.locked(): + raise asyncio.TimeoutError("Voice session already in progress") + + await self._voice_lock.acquire() + + self._on_voice_begin = self._loop.create_future() + try: + self.send_key_command(RemoteKeyCode.KEYCODE_SEARCH) + session_id = await self._async_wait_for_future_or_con_lost(self._on_voice_begin, timeout) + if session_id is None: + raise ConnectionClosed("No voice session available") + + begin = RemoteMessage() + begin.remote_voice_begin.session_id = session_id + self._send_message(begin) + return int(session_id) + except: + self._on_voice_begin = None + raise + finally: + self._voice_lock.release() + + def send_voice_chunk(self, chunk: bytes, session_id: int) -> None: + """Send a chunk of PCM audio for the active voice session. + + :param chunk: The PCM audio data chunk to be sent. + :param session_id: The voice session id. + :raises ConnectionClosed: If the connection is lost. + """ + if self.transport is None or self.transport.is_closing(): + raise ConnectionClosed("Connection has been lost") + + # Pad chunk to minimum chunk size + if len(chunk) < VOICE_CHUNK_MIN_SIZE: + chunk = chunk + b"\x00" * (VOICE_CHUNK_MIN_SIZE - len(chunk)) + + # Limit chunk size, otherwise Android TV will close the connection + for i in range(0, len(chunk), VOICE_CHUNK_SIZE): + msg = RemoteMessage() + msg.remote_voice_payload.session_id = session_id + msg.remote_voice_payload.samples = chunk[i : i + VOICE_CHUNK_SIZE] + self._send_message(msg, False) # disable logging of voice data + + def end_voice(self, session_id: int) -> None: + """End the specified voice session. + + :param session_id: The voice session id. + """ + end = RemoteMessage() + end.remote_voice_end.session_id = session_id + self._send_message(end) + + def _handle_message(self, raw_msg: bytes) -> None: # noqa: PLR0912,PLR0915 """Handle a message from the server.""" self._reset_idle_disconnect_task() msg = RemoteMessage() @@ -215,6 +313,11 @@ def _handle_message(self, raw_msg: bytes) -> None: # noqa: PLR0912 elif msg.HasField("remote_ping_request"): new_msg.remote_ping_response.val1 = msg.remote_ping_request.val1 log_send = LOG_PING_REQUESTS + elif msg.HasField("remote_voice_begin"): + if self._on_voice_begin and not self._on_voice_begin.done(): + self._on_voice_begin.set_result(msg.remote_voice_begin.session_id) + else: + LOGGER.debug("Ignoring remote_voice_begin: no client request available") else: LOGGER.debug("Unhandled: %s", text_format.MessageToString(msg, as_one_line=True)) @@ -237,3 +340,31 @@ async def _async_idle_disconnect(self) -> None: self.transport.close() if not self.on_con_lost.done(): self.on_con_lost.set_result(Exception("Closed idle connection")) + + async def _async_wait_for_future_or_con_lost(self, future: asyncio.Future[Any], timeout: float) -> Any: + """Wait for the future to finish, connection to be lost, or timeout occurs. + + :param future: The future to wait for. + :param timeout: Timeout in seconds. + + :raises ConnectionClosed: If the connection is lost or the future has an exception. + :raises asyncio.TimeoutError: If timeout is reached before completion. + """ + tasks = {self.on_con_lost, future} + + done, _pending = await asyncio.wait(tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED) + + # Check if timeout occurred (no tasks completed) + if not done: + if not future.done(): + future.cancel() + LOGGER.debug("Timeout reached after %s seconds", timeout) + raise asyncio.TimeoutError(f"Operation timed out after {timeout} seconds") + + # Check if future completed successfully + if future.done(): + if future.exception(): + raise ConnectionClosed("Waiting for future failed") from future.exception() + return future.result() + + raise ConnectionClosed("Connection has been lost") diff --git a/src/androidtvremote2/remotemessage.proto b/src/androidtvremote2/remotemessage.proto index 1fd63fb..8925fa9 100644 --- a/src/androidtvremote2/remotemessage.proto +++ b/src/androidtvremote2/remotemessage.proto @@ -36,15 +36,20 @@ message RemoteStart { } message RemoteVoiceEnd { - + int32 session_id = 1; } message RemoteVoicePayload { - + int32 session_id = 1; + // Audio configuration in RemoteVoiceBegin is unknown. + // Default audio sample payload is a sequence of 16-bit PCM, 8 kHz, mono samples, split into 20 KB messages. + bytes samples = 2; } message RemoteVoiceBegin { - + int32 session_id = 1; + // Package name is sent from the Android device as a response to sending KEYCODE_SEARCH and not required when sending audio. + string package_name = 2; } message RemoteTextFieldStatus { diff --git a/src/androidtvremote2/remotemessage_pb2.py b/src/androidtvremote2/remotemessage_pb2.py index 3f6941f..e8127e3 100644 --- a/src/androidtvremote2/remotemessage_pb2.py +++ b/src/androidtvremote2/remotemessage_pb2.py @@ -13,17 +13,17 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13remotemessage.proto\x12\x06remote\".\n\x1aRemoteAppLinkLaunchRequest\x12\x10\n\x08\x61pp_link\x18\x01 \x01(\t\"!\n\x1fRemoteResetPreferredAudioDevice\"\x1f\n\x1dRemoteSetPreferredAudioDevice\"\x19\n\x17RemoteAdjustVolumeLevel\"\xb4\x01\n\x14RemoteSetVolumeLevel\x12\x10\n\x08unknown1\x18\x01 \x01(\r\x12\x10\n\x08unknown2\x18\x02 \x01(\r\x12\x14\n\x0cplayer_model\x18\x03 \x01(\t\x12\x10\n\x08unknown4\x18\x04 \x01(\r\x12\x10\n\x08unknown5\x18\x05 \x01(\r\x12\x12\n\nvolume_max\x18\x06 \x01(\r\x12\x14\n\x0cvolume_level\x18\x07 \x01(\r\x12\x14\n\x0cvolume_muted\x18\x08 \x01(\x08\"\x1e\n\x0bRemoteStart\x12\x0f\n\x07started\x18\x01 \x01(\x08\"\x10\n\x0eRemoteVoiceEnd\"\x14\n\x12RemoteVoicePayload\"\x12\n\x10RemoteVoiceBegin\"v\n\x15RemoteTextFieldStatus\x12\x15\n\rcounter_field\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t\x12\r\n\x05start\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x12\x0c\n\x04int5\x18\x05 \x01(\x05\x12\r\n\x05label\x18\x06 \x01(\t\"W\n\x14RemoteImeShowRequest\x12?\n\x18remote_text_field_status\x18\x02 \x01(\x0b\x32\x1d.remote.RemoteTextFieldStatus\"T\n\x0eRemoteEditInfo\x12\x0e\n\x06insert\x18\x01 \x01(\x05\x12\x32\n\x11text_field_status\x18\x02 \x01(\x0b\x32\x17.remote.RemoteImeObject\"<\n\x0fRemoteImeObject\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\r\n\x05value\x18\x03 \x01(\t\"k\n\x12RemoteImeBatchEdit\x12\x13\n\x0bime_counter\x18\x01 \x01(\x05\x12\x15\n\rfield_counter\x18\x02 \x01(\x05\x12)\n\tedit_info\x18\x03 \x03(\x0b\x32\x16.remote.RemoteEditInfo\"\x99\x01\n\rRemoteAppInfo\x12\x0f\n\x07\x63ounter\x18\x01 \x01(\x05\x12\x0c\n\x04int2\x18\x02 \x01(\x05\x12\x0c\n\x04int3\x18\x03 \x01(\x05\x12\x0c\n\x04int4\x18\x04 \x01(\t\x12\x0c\n\x04int7\x18\x07 \x01(\x05\x12\x0c\n\x04int8\x18\x08 \x01(\x05\x12\r\n\x05label\x18\n \x01(\t\x12\x13\n\x0b\x61pp_package\x18\x0c \x01(\t\x12\r\n\x05int13\x18\r \x01(\x05\"w\n\x12RemoteImeKeyInject\x12\'\n\x08\x61pp_info\x18\x01 \x01(\x0b\x32\x15.remote.RemoteAppInfo\x12\x38\n\x11text_field_status\x18\x02 \x01(\x0b\x32\x1d.remote.RemoteTextFieldStatus\"f\n\x0fRemoteKeyInject\x12\'\n\x08key_code\x18\x01 \x01(\x0e\x32\x15.remote.RemoteKeyCode\x12*\n\tdirection\x18\x02 \x01(\x0e\x32\x17.remote.RemoteDirection\"\"\n\x12RemotePingResponse\x12\x0c\n\x04val1\x18\x01 \x01(\x05\"/\n\x11RemotePingRequest\x12\x0c\n\x04val1\x18\x01 \x01(\x05\x12\x0c\n\x04val2\x18\x02 \x01(\x05\"!\n\x0fRemoteSetActive\x12\x0e\n\x06\x61\x63tive\x18\x01 \x01(\x05\"\x80\x01\n\x10RemoteDeviceInfo\x12\r\n\x05model\x18\x01 \x01(\t\x12\x0e\n\x06vendor\x18\x02 \x01(\t\x12\x10\n\x08unknown1\x18\x03 \x01(\x05\x12\x10\n\x08unknown2\x18\x04 \x01(\t\x12\x14\n\x0cpackage_name\x18\x05 \x01(\t\x12\x13\n\x0b\x61pp_version\x18\x06 \x01(\t\"O\n\x0fRemoteConfigure\x12\r\n\x05\x63ode1\x18\x01 \x01(\x05\x12-\n\x0b\x64\x65vice_info\x18\x02 \x01(\x0b\x32\x18.remote.RemoteDeviceInfo\"D\n\x0bRemoteError\x12\r\n\x05value\x18\x01 \x01(\x08\x12&\n\x07message\x18\x02 \x01(\x0b\x32\x15.remote.RemoteMessage\"\xc1\x08\n\rRemoteMessage\x12\x31\n\x10remote_configure\x18\x01 \x01(\x0b\x32\x17.remote.RemoteConfigure\x12\x32\n\x11remote_set_active\x18\x02 \x01(\x0b\x32\x17.remote.RemoteSetActive\x12)\n\x0cremote_error\x18\x03 \x01(\x0b\x32\x13.remote.RemoteError\x12\x36\n\x13remote_ping_request\x18\x08 \x01(\x0b\x32\x19.remote.RemotePingRequest\x12\x38\n\x14remote_ping_response\x18\t \x01(\x0b\x32\x1a.remote.RemotePingResponse\x12\x32\n\x11remote_key_inject\x18\n \x01(\x0b\x32\x17.remote.RemoteKeyInject\x12\x39\n\x15remote_ime_key_inject\x18\x14 \x01(\x0b\x32\x1a.remote.RemoteImeKeyInject\x12\x39\n\x15remote_ime_batch_edit\x18\x15 \x01(\x0b\x32\x1a.remote.RemoteImeBatchEdit\x12=\n\x17remote_ime_show_request\x18\x16 \x01(\x0b\x32\x1c.remote.RemoteImeShowRequest\x12\x34\n\x12remote_voice_begin\x18\x1e \x01(\x0b\x32\x18.remote.RemoteVoiceBegin\x12\x38\n\x14remote_voice_payload\x18\x1f \x01(\x0b\x32\x1a.remote.RemoteVoicePayload\x12\x30\n\x10remote_voice_end\x18 \x01(\x0b\x32\x16.remote.RemoteVoiceEnd\x12)\n\x0cremote_start\x18( \x01(\x0b\x32\x13.remote.RemoteStart\x12=\n\x17remote_set_volume_level\x18\x32 \x01(\x0b\x32\x1c.remote.RemoteSetVolumeLevel\x12\x43\n\x1aremote_adjust_volume_level\x18\x33 \x01(\x0b\x32\x1f.remote.RemoteAdjustVolumeLevel\x12P\n!remote_set_preferred_audio_device\x18< \x01(\x0b\x32%.remote.RemoteSetPreferredAudioDevice\x12T\n#remote_reset_preferred_audio_device\x18= \x01(\x0b\x32\'.remote.RemoteResetPreferredAudioDevice\x12J\n\x1eremote_app_link_launch_request\x18Z \x01(\x0b\x32\".remote.RemoteAppLinkLaunchRequest*\xe7\x37\n\rRemoteKeyCode\x12\x13\n\x0fKEYCODE_UNKNOWN\x10\x00\x12\x15\n\x11KEYCODE_SOFT_LEFT\x10\x01\x12\x16\n\x12KEYCODE_SOFT_RIGHT\x10\x02\x12\x10\n\x0cKEYCODE_HOME\x10\x03\x12\x10\n\x0cKEYCODE_BACK\x10\x04\x12\x10\n\x0cKEYCODE_CALL\x10\x05\x12\x13\n\x0fKEYCODE_ENDCALL\x10\x06\x12\r\n\tKEYCODE_0\x10\x07\x12\r\n\tKEYCODE_1\x10\x08\x12\r\n\tKEYCODE_2\x10\t\x12\r\n\tKEYCODE_3\x10\n\x12\r\n\tKEYCODE_4\x10\x0b\x12\r\n\tKEYCODE_5\x10\x0c\x12\r\n\tKEYCODE_6\x10\r\x12\r\n\tKEYCODE_7\x10\x0e\x12\r\n\tKEYCODE_8\x10\x0f\x12\r\n\tKEYCODE_9\x10\x10\x12\x10\n\x0cKEYCODE_STAR\x10\x11\x12\x11\n\rKEYCODE_POUND\x10\x12\x12\x13\n\x0fKEYCODE_DPAD_UP\x10\x13\x12\x15\n\x11KEYCODE_DPAD_DOWN\x10\x14\x12\x15\n\x11KEYCODE_DPAD_LEFT\x10\x15\x12\x16\n\x12KEYCODE_DPAD_RIGHT\x10\x16\x12\x17\n\x13KEYCODE_DPAD_CENTER\x10\x17\x12\x15\n\x11KEYCODE_VOLUME_UP\x10\x18\x12\x17\n\x13KEYCODE_VOLUME_DOWN\x10\x19\x12\x11\n\rKEYCODE_POWER\x10\x1a\x12\x12\n\x0eKEYCODE_CAMERA\x10\x1b\x12\x11\n\rKEYCODE_CLEAR\x10\x1c\x12\r\n\tKEYCODE_A\x10\x1d\x12\r\n\tKEYCODE_B\x10\x1e\x12\r\n\tKEYCODE_C\x10\x1f\x12\r\n\tKEYCODE_D\x10 \x12\r\n\tKEYCODE_E\x10!\x12\r\n\tKEYCODE_F\x10\"\x12\r\n\tKEYCODE_G\x10#\x12\r\n\tKEYCODE_H\x10$\x12\r\n\tKEYCODE_I\x10%\x12\r\n\tKEYCODE_J\x10&\x12\r\n\tKEYCODE_K\x10\'\x12\r\n\tKEYCODE_L\x10(\x12\r\n\tKEYCODE_M\x10)\x12\r\n\tKEYCODE_N\x10*\x12\r\n\tKEYCODE_O\x10+\x12\r\n\tKEYCODE_P\x10,\x12\r\n\tKEYCODE_Q\x10-\x12\r\n\tKEYCODE_R\x10.\x12\r\n\tKEYCODE_S\x10/\x12\r\n\tKEYCODE_T\x10\x30\x12\r\n\tKEYCODE_U\x10\x31\x12\r\n\tKEYCODE_V\x10\x32\x12\r\n\tKEYCODE_W\x10\x33\x12\r\n\tKEYCODE_X\x10\x34\x12\r\n\tKEYCODE_Y\x10\x35\x12\r\n\tKEYCODE_Z\x10\x36\x12\x11\n\rKEYCODE_COMMA\x10\x37\x12\x12\n\x0eKEYCODE_PERIOD\x10\x38\x12\x14\n\x10KEYCODE_ALT_LEFT\x10\x39\x12\x15\n\x11KEYCODE_ALT_RIGHT\x10:\x12\x16\n\x12KEYCODE_SHIFT_LEFT\x10;\x12\x17\n\x13KEYCODE_SHIFT_RIGHT\x10<\x12\x0f\n\x0bKEYCODE_TAB\x10=\x12\x11\n\rKEYCODE_SPACE\x10>\x12\x0f\n\x0bKEYCODE_SYM\x10?\x12\x14\n\x10KEYCODE_EXPLORER\x10@\x12\x14\n\x10KEYCODE_ENVELOPE\x10\x41\x12\x11\n\rKEYCODE_ENTER\x10\x42\x12\x0f\n\x0bKEYCODE_DEL\x10\x43\x12\x11\n\rKEYCODE_GRAVE\x10\x44\x12\x11\n\rKEYCODE_MINUS\x10\x45\x12\x12\n\x0eKEYCODE_EQUALS\x10\x46\x12\x18\n\x14KEYCODE_LEFT_BRACKET\x10G\x12\x19\n\x15KEYCODE_RIGHT_BRACKET\x10H\x12\x15\n\x11KEYCODE_BACKSLASH\x10I\x12\x15\n\x11KEYCODE_SEMICOLON\x10J\x12\x16\n\x12KEYCODE_APOSTROPHE\x10K\x12\x11\n\rKEYCODE_SLASH\x10L\x12\x0e\n\nKEYCODE_AT\x10M\x12\x0f\n\x0bKEYCODE_NUM\x10N\x12\x17\n\x13KEYCODE_HEADSETHOOK\x10O\x12\x11\n\rKEYCODE_FOCUS\x10P\x12\x10\n\x0cKEYCODE_PLUS\x10Q\x12\x10\n\x0cKEYCODE_MENU\x10R\x12\x18\n\x14KEYCODE_NOTIFICATION\x10S\x12\x12\n\x0eKEYCODE_SEARCH\x10T\x12\x1c\n\x18KEYCODE_MEDIA_PLAY_PAUSE\x10U\x12\x16\n\x12KEYCODE_MEDIA_STOP\x10V\x12\x16\n\x12KEYCODE_MEDIA_NEXT\x10W\x12\x1a\n\x16KEYCODE_MEDIA_PREVIOUS\x10X\x12\x18\n\x14KEYCODE_MEDIA_REWIND\x10Y\x12\x1e\n\x1aKEYCODE_MEDIA_FAST_FORWARD\x10Z\x12\x10\n\x0cKEYCODE_MUTE\x10[\x12\x13\n\x0fKEYCODE_PAGE_UP\x10\\\x12\x15\n\x11KEYCODE_PAGE_DOWN\x10]\x12\x17\n\x13KEYCODE_PICTSYMBOLS\x10^\x12\x1a\n\x16KEYCODE_SWITCH_CHARSET\x10_\x12\x14\n\x10KEYCODE_BUTTON_A\x10`\x12\x14\n\x10KEYCODE_BUTTON_B\x10\x61\x12\x14\n\x10KEYCODE_BUTTON_C\x10\x62\x12\x14\n\x10KEYCODE_BUTTON_X\x10\x63\x12\x14\n\x10KEYCODE_BUTTON_Y\x10\x64\x12\x14\n\x10KEYCODE_BUTTON_Z\x10\x65\x12\x15\n\x11KEYCODE_BUTTON_L1\x10\x66\x12\x15\n\x11KEYCODE_BUTTON_R1\x10g\x12\x15\n\x11KEYCODE_BUTTON_L2\x10h\x12\x15\n\x11KEYCODE_BUTTON_R2\x10i\x12\x19\n\x15KEYCODE_BUTTON_THUMBL\x10j\x12\x19\n\x15KEYCODE_BUTTON_THUMBR\x10k\x12\x18\n\x14KEYCODE_BUTTON_START\x10l\x12\x19\n\x15KEYCODE_BUTTON_SELECT\x10m\x12\x17\n\x13KEYCODE_BUTTON_MODE\x10n\x12\x12\n\x0eKEYCODE_ESCAPE\x10o\x12\x17\n\x13KEYCODE_FORWARD_DEL\x10p\x12\x15\n\x11KEYCODE_CTRL_LEFT\x10q\x12\x16\n\x12KEYCODE_CTRL_RIGHT\x10r\x12\x15\n\x11KEYCODE_CAPS_LOCK\x10s\x12\x17\n\x13KEYCODE_SCROLL_LOCK\x10t\x12\x15\n\x11KEYCODE_META_LEFT\x10u\x12\x16\n\x12KEYCODE_META_RIGHT\x10v\x12\x14\n\x10KEYCODE_FUNCTION\x10w\x12\x11\n\rKEYCODE_SYSRQ\x10x\x12\x11\n\rKEYCODE_BREAK\x10y\x12\x15\n\x11KEYCODE_MOVE_HOME\x10z\x12\x14\n\x10KEYCODE_MOVE_END\x10{\x12\x12\n\x0eKEYCODE_INSERT\x10|\x12\x13\n\x0fKEYCODE_FORWARD\x10}\x12\x16\n\x12KEYCODE_MEDIA_PLAY\x10~\x12\x17\n\x13KEYCODE_MEDIA_PAUSE\x10\x7f\x12\x18\n\x13KEYCODE_MEDIA_CLOSE\x10\x80\x01\x12\x18\n\x13KEYCODE_MEDIA_EJECT\x10\x81\x01\x12\x19\n\x14KEYCODE_MEDIA_RECORD\x10\x82\x01\x12\x0f\n\nKEYCODE_F1\x10\x83\x01\x12\x0f\n\nKEYCODE_F2\x10\x84\x01\x12\x0f\n\nKEYCODE_F3\x10\x85\x01\x12\x0f\n\nKEYCODE_F4\x10\x86\x01\x12\x0f\n\nKEYCODE_F5\x10\x87\x01\x12\x0f\n\nKEYCODE_F6\x10\x88\x01\x12\x0f\n\nKEYCODE_F7\x10\x89\x01\x12\x0f\n\nKEYCODE_F8\x10\x8a\x01\x12\x0f\n\nKEYCODE_F9\x10\x8b\x01\x12\x10\n\x0bKEYCODE_F10\x10\x8c\x01\x12\x10\n\x0bKEYCODE_F11\x10\x8d\x01\x12\x10\n\x0bKEYCODE_F12\x10\x8e\x01\x12\x15\n\x10KEYCODE_NUM_LOCK\x10\x8f\x01\x12\x15\n\x10KEYCODE_NUMPAD_0\x10\x90\x01\x12\x15\n\x10KEYCODE_NUMPAD_1\x10\x91\x01\x12\x15\n\x10KEYCODE_NUMPAD_2\x10\x92\x01\x12\x15\n\x10KEYCODE_NUMPAD_3\x10\x93\x01\x12\x15\n\x10KEYCODE_NUMPAD_4\x10\x94\x01\x12\x15\n\x10KEYCODE_NUMPAD_5\x10\x95\x01\x12\x15\n\x10KEYCODE_NUMPAD_6\x10\x96\x01\x12\x15\n\x10KEYCODE_NUMPAD_7\x10\x97\x01\x12\x15\n\x10KEYCODE_NUMPAD_8\x10\x98\x01\x12\x15\n\x10KEYCODE_NUMPAD_9\x10\x99\x01\x12\x1a\n\x15KEYCODE_NUMPAD_DIVIDE\x10\x9a\x01\x12\x1c\n\x17KEYCODE_NUMPAD_MULTIPLY\x10\x9b\x01\x12\x1c\n\x17KEYCODE_NUMPAD_SUBTRACT\x10\x9c\x01\x12\x17\n\x12KEYCODE_NUMPAD_ADD\x10\x9d\x01\x12\x17\n\x12KEYCODE_NUMPAD_DOT\x10\x9e\x01\x12\x19\n\x14KEYCODE_NUMPAD_COMMA\x10\x9f\x01\x12\x19\n\x14KEYCODE_NUMPAD_ENTER\x10\xa0\x01\x12\x1a\n\x15KEYCODE_NUMPAD_EQUALS\x10\xa1\x01\x12\x1e\n\x19KEYCODE_NUMPAD_LEFT_PAREN\x10\xa2\x01\x12\x1f\n\x1aKEYCODE_NUMPAD_RIGHT_PAREN\x10\xa3\x01\x12\x18\n\x13KEYCODE_VOLUME_MUTE\x10\xa4\x01\x12\x11\n\x0cKEYCODE_INFO\x10\xa5\x01\x12\x17\n\x12KEYCODE_CHANNEL_UP\x10\xa6\x01\x12\x19\n\x14KEYCODE_CHANNEL_DOWN\x10\xa7\x01\x12\x14\n\x0fKEYCODE_ZOOM_IN\x10\xa8\x01\x12\x15\n\x10KEYCODE_ZOOM_OUT\x10\xa9\x01\x12\x0f\n\nKEYCODE_TV\x10\xaa\x01\x12\x13\n\x0eKEYCODE_WINDOW\x10\xab\x01\x12\x12\n\rKEYCODE_GUIDE\x10\xac\x01\x12\x10\n\x0bKEYCODE_DVR\x10\xad\x01\x12\x15\n\x10KEYCODE_BOOKMARK\x10\xae\x01\x12\x15\n\x10KEYCODE_CAPTIONS\x10\xaf\x01\x12\x15\n\x10KEYCODE_SETTINGS\x10\xb0\x01\x12\x15\n\x10KEYCODE_TV_POWER\x10\xb1\x01\x12\x15\n\x10KEYCODE_TV_INPUT\x10\xb2\x01\x12\x16\n\x11KEYCODE_STB_POWER\x10\xb3\x01\x12\x16\n\x11KEYCODE_STB_INPUT\x10\xb4\x01\x12\x16\n\x11KEYCODE_AVR_POWER\x10\xb5\x01\x12\x16\n\x11KEYCODE_AVR_INPUT\x10\xb6\x01\x12\x15\n\x10KEYCODE_PROG_RED\x10\xb7\x01\x12\x17\n\x12KEYCODE_PROG_GREEN\x10\xb8\x01\x12\x18\n\x13KEYCODE_PROG_YELLOW\x10\xb9\x01\x12\x16\n\x11KEYCODE_PROG_BLUE\x10\xba\x01\x12\x17\n\x12KEYCODE_APP_SWITCH\x10\xbb\x01\x12\x15\n\x10KEYCODE_BUTTON_1\x10\xbc\x01\x12\x15\n\x10KEYCODE_BUTTON_2\x10\xbd\x01\x12\x15\n\x10KEYCODE_BUTTON_3\x10\xbe\x01\x12\x15\n\x10KEYCODE_BUTTON_4\x10\xbf\x01\x12\x15\n\x10KEYCODE_BUTTON_5\x10\xc0\x01\x12\x15\n\x10KEYCODE_BUTTON_6\x10\xc1\x01\x12\x15\n\x10KEYCODE_BUTTON_7\x10\xc2\x01\x12\x15\n\x10KEYCODE_BUTTON_8\x10\xc3\x01\x12\x15\n\x10KEYCODE_BUTTON_9\x10\xc4\x01\x12\x16\n\x11KEYCODE_BUTTON_10\x10\xc5\x01\x12\x16\n\x11KEYCODE_BUTTON_11\x10\xc6\x01\x12\x16\n\x11KEYCODE_BUTTON_12\x10\xc7\x01\x12\x16\n\x11KEYCODE_BUTTON_13\x10\xc8\x01\x12\x16\n\x11KEYCODE_BUTTON_14\x10\xc9\x01\x12\x16\n\x11KEYCODE_BUTTON_15\x10\xca\x01\x12\x16\n\x11KEYCODE_BUTTON_16\x10\xcb\x01\x12\x1c\n\x17KEYCODE_LANGUAGE_SWITCH\x10\xcc\x01\x12\x18\n\x13KEYCODE_MANNER_MODE\x10\xcd\x01\x12\x14\n\x0fKEYCODE_3D_MODE\x10\xce\x01\x12\x15\n\x10KEYCODE_CONTACTS\x10\xcf\x01\x12\x15\n\x10KEYCODE_CALENDAR\x10\xd0\x01\x12\x12\n\rKEYCODE_MUSIC\x10\xd1\x01\x12\x17\n\x12KEYCODE_CALCULATOR\x10\xd2\x01\x12\x1c\n\x17KEYCODE_ZENKAKU_HANKAKU\x10\xd3\x01\x12\x11\n\x0cKEYCODE_EISU\x10\xd4\x01\x12\x15\n\x10KEYCODE_MUHENKAN\x10\xd5\x01\x12\x13\n\x0eKEYCODE_HENKAN\x10\xd6\x01\x12\x1e\n\x19KEYCODE_KATAKANA_HIRAGANA\x10\xd7\x01\x12\x10\n\x0bKEYCODE_YEN\x10\xd8\x01\x12\x0f\n\nKEYCODE_RO\x10\xd9\x01\x12\x11\n\x0cKEYCODE_KANA\x10\xda\x01\x12\x13\n\x0eKEYCODE_ASSIST\x10\xdb\x01\x12\x1c\n\x17KEYCODE_BRIGHTNESS_DOWN\x10\xdc\x01\x12\x1a\n\x15KEYCODE_BRIGHTNESS_UP\x10\xdd\x01\x12\x1e\n\x19KEYCODE_MEDIA_AUDIO_TRACK\x10\xde\x01\x12\x12\n\rKEYCODE_SLEEP\x10\xdf\x01\x12\x13\n\x0eKEYCODE_WAKEUP\x10\xe0\x01\x12\x14\n\x0fKEYCODE_PAIRING\x10\xe1\x01\x12\x1b\n\x16KEYCODE_MEDIA_TOP_MENU\x10\xe2\x01\x12\x0f\n\nKEYCODE_11\x10\xe3\x01\x12\x0f\n\nKEYCODE_12\x10\xe4\x01\x12\x19\n\x14KEYCODE_LAST_CHANNEL\x10\xe5\x01\x12\x1c\n\x17KEYCODE_TV_DATA_SERVICE\x10\xe6\x01\x12\x19\n\x14KEYCODE_VOICE_ASSIST\x10\xe7\x01\x12\x1d\n\x18KEYCODE_TV_RADIO_SERVICE\x10\xe8\x01\x12\x18\n\x13KEYCODE_TV_TELETEXT\x10\xe9\x01\x12\x1c\n\x17KEYCODE_TV_NUMBER_ENTRY\x10\xea\x01\x12\"\n\x1dKEYCODE_TV_TERRESTRIAL_ANALOG\x10\xeb\x01\x12#\n\x1eKEYCODE_TV_TERRESTRIAL_DIGITAL\x10\xec\x01\x12\x19\n\x14KEYCODE_TV_SATELLITE\x10\xed\x01\x12\x1c\n\x17KEYCODE_TV_SATELLITE_BS\x10\xee\x01\x12\x1c\n\x17KEYCODE_TV_SATELLITE_CS\x10\xef\x01\x12!\n\x1cKEYCODE_TV_SATELLITE_SERVICE\x10\xf0\x01\x12\x17\n\x12KEYCODE_TV_NETWORK\x10\xf1\x01\x12\x1d\n\x18KEYCODE_TV_ANTENNA_CABLE\x10\xf2\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_1\x10\xf3\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_2\x10\xf4\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_3\x10\xf5\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_4\x10\xf6\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPOSITE_1\x10\xf7\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPOSITE_2\x10\xf8\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPONENT_1\x10\xf9\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPONENT_2\x10\xfa\x01\x12\x1b\n\x16KEYCODE_TV_INPUT_VGA_1\x10\xfb\x01\x12!\n\x1cKEYCODE_TV_AUDIO_DESCRIPTION\x10\xfc\x01\x12(\n#KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP\x10\xfd\x01\x12*\n%KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN\x10\xfe\x01\x12\x19\n\x14KEYCODE_TV_ZOOM_MODE\x10\xff\x01\x12\x1d\n\x18KEYCODE_TV_CONTENTS_MENU\x10\x80\x02\x12\"\n\x1dKEYCODE_TV_MEDIA_CONTEXT_MENU\x10\x81\x02\x12!\n\x1cKEYCODE_TV_TIMER_PROGRAMMING\x10\x82\x02\x12\x11\n\x0cKEYCODE_HELP\x10\x83\x02\x12\x1e\n\x19KEYCODE_NAVIGATE_PREVIOUS\x10\x84\x02\x12\x1a\n\x15KEYCODE_NAVIGATE_NEXT\x10\x85\x02\x12\x18\n\x13KEYCODE_NAVIGATE_IN\x10\x86\x02\x12\x19\n\x14KEYCODE_NAVIGATE_OUT\x10\x87\x02\x12\x19\n\x14KEYCODE_STEM_PRIMARY\x10\x88\x02\x12\x13\n\x0eKEYCODE_STEM_1\x10\x89\x02\x12\x13\n\x0eKEYCODE_STEM_2\x10\x8a\x02\x12\x13\n\x0eKEYCODE_STEM_3\x10\x8b\x02\x12\x19\n\x14KEYCODE_DPAD_UP_LEFT\x10\x8c\x02\x12\x1b\n\x16KEYCODE_DPAD_DOWN_LEFT\x10\x8d\x02\x12\x1a\n\x15KEYCODE_DPAD_UP_RIGHT\x10\x8e\x02\x12\x1c\n\x17KEYCODE_DPAD_DOWN_RIGHT\x10\x8f\x02\x12\x1f\n\x1aKEYCODE_MEDIA_SKIP_FORWARD\x10\x90\x02\x12 \n\x1bKEYCODE_MEDIA_SKIP_BACKWARD\x10\x91\x02\x12\x1f\n\x1aKEYCODE_MEDIA_STEP_FORWARD\x10\x92\x02\x12 \n\x1bKEYCODE_MEDIA_STEP_BACKWARD\x10\x93\x02\x12\x17\n\x12KEYCODE_SOFT_SLEEP\x10\x94\x02\x12\x10\n\x0bKEYCODE_CUT\x10\x95\x02\x12\x11\n\x0cKEYCODE_COPY\x10\x96\x02\x12\x12\n\rKEYCODE_PASTE\x10\x97\x02\x12!\n\x1cKEYCODE_SYSTEM_NAVIGATION_UP\x10\x98\x02\x12#\n\x1eKEYCODE_SYSTEM_NAVIGATION_DOWN\x10\x99\x02\x12#\n\x1eKEYCODE_SYSTEM_NAVIGATION_LEFT\x10\x9a\x02\x12$\n\x1fKEYCODE_SYSTEM_NAVIGATION_RIGHT\x10\x9b\x02\x12\x15\n\x10KEYCODE_ALL_APPS\x10\x9c\x02\x12\x14\n\x0fKEYCODE_REFRESH\x10\x9d\x02\x12\x16\n\x11KEYCODE_THUMBS_UP\x10\x9e\x02\x12\x18\n\x13KEYCODE_THUMBS_DOWN\x10\x9f\x02\x12\x1b\n\x16KEYCODE_PROFILE_SWITCH\x10\xa0\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_1\x10\xa1\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_2\x10\xa2\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_3\x10\xa3\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_4\x10\xa4\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_5\x10\xa5\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_6\x10\xa6\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_7\x10\xa7\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_8\x10\xa8\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_1\x10\xa9\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_2\x10\xaa\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_3\x10\xab\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_4\x10\xac\x02\x12\x17\n\x12KEYCODE_DEMO_APP_1\x10\xad\x02\x12\x17\n\x12KEYCODE_DEMO_APP_2\x10\xae\x02\x12\x17\n\x12KEYCODE_DEMO_APP_3\x10\xaf\x02\x12\x17\n\x12KEYCODE_DEMO_APP_4\x10\xb0\x02*Q\n\x0fRemoteDirection\x12\x15\n\x11UNKNOWN_DIRECTION\x10\x00\x12\x0e\n\nSTART_LONG\x10\x01\x12\x0c\n\x08\x45ND_LONG\x10\x02\x12\t\n\x05SHORT\x10\x03\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13remotemessage.proto\x12\x06remote\".\n\x1aRemoteAppLinkLaunchRequest\x12\x10\n\x08\x61pp_link\x18\x01 \x01(\t\"!\n\x1fRemoteResetPreferredAudioDevice\"\x1f\n\x1dRemoteSetPreferredAudioDevice\"\x19\n\x17RemoteAdjustVolumeLevel\"\xb4\x01\n\x14RemoteSetVolumeLevel\x12\x10\n\x08unknown1\x18\x01 \x01(\r\x12\x10\n\x08unknown2\x18\x02 \x01(\r\x12\x14\n\x0cplayer_model\x18\x03 \x01(\t\x12\x10\n\x08unknown4\x18\x04 \x01(\r\x12\x10\n\x08unknown5\x18\x05 \x01(\r\x12\x12\n\nvolume_max\x18\x06 \x01(\r\x12\x14\n\x0cvolume_level\x18\x07 \x01(\r\x12\x14\n\x0cvolume_muted\x18\x08 \x01(\x08\"\x1e\n\x0bRemoteStart\x12\x0f\n\x07started\x18\x01 \x01(\x08\"$\n\x0eRemoteVoiceEnd\x12\x12\n\nsession_id\x18\x01 \x01(\x05\"9\n\x12RemoteVoicePayload\x12\x12\n\nsession_id\x18\x01 \x01(\x05\x12\x0f\n\x07samples\x18\x02 \x01(\x0c\"<\n\x10RemoteVoiceBegin\x12\x12\n\nsession_id\x18\x01 \x01(\x05\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\"v\n\x15RemoteTextFieldStatus\x12\x15\n\rcounter_field\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t\x12\r\n\x05start\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x12\x0c\n\x04int5\x18\x05 \x01(\x05\x12\r\n\x05label\x18\x06 \x01(\t\"W\n\x14RemoteImeShowRequest\x12?\n\x18remote_text_field_status\x18\x02 \x01(\x0b\x32\x1d.remote.RemoteTextFieldStatus\"T\n\x0eRemoteEditInfo\x12\x0e\n\x06insert\x18\x01 \x01(\x05\x12\x32\n\x11text_field_status\x18\x02 \x01(\x0b\x32\x17.remote.RemoteImeObject\"<\n\x0fRemoteImeObject\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\r\n\x05value\x18\x03 \x01(\t\"k\n\x12RemoteImeBatchEdit\x12\x13\n\x0bime_counter\x18\x01 \x01(\x05\x12\x15\n\rfield_counter\x18\x02 \x01(\x05\x12)\n\tedit_info\x18\x03 \x03(\x0b\x32\x16.remote.RemoteEditInfo\"\x99\x01\n\rRemoteAppInfo\x12\x0f\n\x07\x63ounter\x18\x01 \x01(\x05\x12\x0c\n\x04int2\x18\x02 \x01(\x05\x12\x0c\n\x04int3\x18\x03 \x01(\x05\x12\x0c\n\x04int4\x18\x04 \x01(\t\x12\x0c\n\x04int7\x18\x07 \x01(\x05\x12\x0c\n\x04int8\x18\x08 \x01(\x05\x12\r\n\x05label\x18\n \x01(\t\x12\x13\n\x0b\x61pp_package\x18\x0c \x01(\t\x12\r\n\x05int13\x18\r \x01(\x05\"w\n\x12RemoteImeKeyInject\x12\'\n\x08\x61pp_info\x18\x01 \x01(\x0b\x32\x15.remote.RemoteAppInfo\x12\x38\n\x11text_field_status\x18\x02 \x01(\x0b\x32\x1d.remote.RemoteTextFieldStatus\"f\n\x0fRemoteKeyInject\x12\'\n\x08key_code\x18\x01 \x01(\x0e\x32\x15.remote.RemoteKeyCode\x12*\n\tdirection\x18\x02 \x01(\x0e\x32\x17.remote.RemoteDirection\"\"\n\x12RemotePingResponse\x12\x0c\n\x04val1\x18\x01 \x01(\x05\"/\n\x11RemotePingRequest\x12\x0c\n\x04val1\x18\x01 \x01(\x05\x12\x0c\n\x04val2\x18\x02 \x01(\x05\"!\n\x0fRemoteSetActive\x12\x0e\n\x06\x61\x63tive\x18\x01 \x01(\x05\"\x80\x01\n\x10RemoteDeviceInfo\x12\r\n\x05model\x18\x01 \x01(\t\x12\x0e\n\x06vendor\x18\x02 \x01(\t\x12\x10\n\x08unknown1\x18\x03 \x01(\x05\x12\x10\n\x08unknown2\x18\x04 \x01(\t\x12\x14\n\x0cpackage_name\x18\x05 \x01(\t\x12\x13\n\x0b\x61pp_version\x18\x06 \x01(\t\"O\n\x0fRemoteConfigure\x12\r\n\x05\x63ode1\x18\x01 \x01(\x05\x12-\n\x0b\x64\x65vice_info\x18\x02 \x01(\x0b\x32\x18.remote.RemoteDeviceInfo\"D\n\x0bRemoteError\x12\r\n\x05value\x18\x01 \x01(\x08\x12&\n\x07message\x18\x02 \x01(\x0b\x32\x15.remote.RemoteMessage\"\xc1\x08\n\rRemoteMessage\x12\x31\n\x10remote_configure\x18\x01 \x01(\x0b\x32\x17.remote.RemoteConfigure\x12\x32\n\x11remote_set_active\x18\x02 \x01(\x0b\x32\x17.remote.RemoteSetActive\x12)\n\x0cremote_error\x18\x03 \x01(\x0b\x32\x13.remote.RemoteError\x12\x36\n\x13remote_ping_request\x18\x08 \x01(\x0b\x32\x19.remote.RemotePingRequest\x12\x38\n\x14remote_ping_response\x18\t \x01(\x0b\x32\x1a.remote.RemotePingResponse\x12\x32\n\x11remote_key_inject\x18\n \x01(\x0b\x32\x17.remote.RemoteKeyInject\x12\x39\n\x15remote_ime_key_inject\x18\x14 \x01(\x0b\x32\x1a.remote.RemoteImeKeyInject\x12\x39\n\x15remote_ime_batch_edit\x18\x15 \x01(\x0b\x32\x1a.remote.RemoteImeBatchEdit\x12=\n\x17remote_ime_show_request\x18\x16 \x01(\x0b\x32\x1c.remote.RemoteImeShowRequest\x12\x34\n\x12remote_voice_begin\x18\x1e \x01(\x0b\x32\x18.remote.RemoteVoiceBegin\x12\x38\n\x14remote_voice_payload\x18\x1f \x01(\x0b\x32\x1a.remote.RemoteVoicePayload\x12\x30\n\x10remote_voice_end\x18 \x01(\x0b\x32\x16.remote.RemoteVoiceEnd\x12)\n\x0cremote_start\x18( \x01(\x0b\x32\x13.remote.RemoteStart\x12=\n\x17remote_set_volume_level\x18\x32 \x01(\x0b\x32\x1c.remote.RemoteSetVolumeLevel\x12\x43\n\x1aremote_adjust_volume_level\x18\x33 \x01(\x0b\x32\x1f.remote.RemoteAdjustVolumeLevel\x12P\n!remote_set_preferred_audio_device\x18< \x01(\x0b\x32%.remote.RemoteSetPreferredAudioDevice\x12T\n#remote_reset_preferred_audio_device\x18= \x01(\x0b\x32\'.remote.RemoteResetPreferredAudioDevice\x12J\n\x1eremote_app_link_launch_request\x18Z \x01(\x0b\x32\".remote.RemoteAppLinkLaunchRequest*\xe7\x37\n\rRemoteKeyCode\x12\x13\n\x0fKEYCODE_UNKNOWN\x10\x00\x12\x15\n\x11KEYCODE_SOFT_LEFT\x10\x01\x12\x16\n\x12KEYCODE_SOFT_RIGHT\x10\x02\x12\x10\n\x0cKEYCODE_HOME\x10\x03\x12\x10\n\x0cKEYCODE_BACK\x10\x04\x12\x10\n\x0cKEYCODE_CALL\x10\x05\x12\x13\n\x0fKEYCODE_ENDCALL\x10\x06\x12\r\n\tKEYCODE_0\x10\x07\x12\r\n\tKEYCODE_1\x10\x08\x12\r\n\tKEYCODE_2\x10\t\x12\r\n\tKEYCODE_3\x10\n\x12\r\n\tKEYCODE_4\x10\x0b\x12\r\n\tKEYCODE_5\x10\x0c\x12\r\n\tKEYCODE_6\x10\r\x12\r\n\tKEYCODE_7\x10\x0e\x12\r\n\tKEYCODE_8\x10\x0f\x12\r\n\tKEYCODE_9\x10\x10\x12\x10\n\x0cKEYCODE_STAR\x10\x11\x12\x11\n\rKEYCODE_POUND\x10\x12\x12\x13\n\x0fKEYCODE_DPAD_UP\x10\x13\x12\x15\n\x11KEYCODE_DPAD_DOWN\x10\x14\x12\x15\n\x11KEYCODE_DPAD_LEFT\x10\x15\x12\x16\n\x12KEYCODE_DPAD_RIGHT\x10\x16\x12\x17\n\x13KEYCODE_DPAD_CENTER\x10\x17\x12\x15\n\x11KEYCODE_VOLUME_UP\x10\x18\x12\x17\n\x13KEYCODE_VOLUME_DOWN\x10\x19\x12\x11\n\rKEYCODE_POWER\x10\x1a\x12\x12\n\x0eKEYCODE_CAMERA\x10\x1b\x12\x11\n\rKEYCODE_CLEAR\x10\x1c\x12\r\n\tKEYCODE_A\x10\x1d\x12\r\n\tKEYCODE_B\x10\x1e\x12\r\n\tKEYCODE_C\x10\x1f\x12\r\n\tKEYCODE_D\x10 \x12\r\n\tKEYCODE_E\x10!\x12\r\n\tKEYCODE_F\x10\"\x12\r\n\tKEYCODE_G\x10#\x12\r\n\tKEYCODE_H\x10$\x12\r\n\tKEYCODE_I\x10%\x12\r\n\tKEYCODE_J\x10&\x12\r\n\tKEYCODE_K\x10\'\x12\r\n\tKEYCODE_L\x10(\x12\r\n\tKEYCODE_M\x10)\x12\r\n\tKEYCODE_N\x10*\x12\r\n\tKEYCODE_O\x10+\x12\r\n\tKEYCODE_P\x10,\x12\r\n\tKEYCODE_Q\x10-\x12\r\n\tKEYCODE_R\x10.\x12\r\n\tKEYCODE_S\x10/\x12\r\n\tKEYCODE_T\x10\x30\x12\r\n\tKEYCODE_U\x10\x31\x12\r\n\tKEYCODE_V\x10\x32\x12\r\n\tKEYCODE_W\x10\x33\x12\r\n\tKEYCODE_X\x10\x34\x12\r\n\tKEYCODE_Y\x10\x35\x12\r\n\tKEYCODE_Z\x10\x36\x12\x11\n\rKEYCODE_COMMA\x10\x37\x12\x12\n\x0eKEYCODE_PERIOD\x10\x38\x12\x14\n\x10KEYCODE_ALT_LEFT\x10\x39\x12\x15\n\x11KEYCODE_ALT_RIGHT\x10:\x12\x16\n\x12KEYCODE_SHIFT_LEFT\x10;\x12\x17\n\x13KEYCODE_SHIFT_RIGHT\x10<\x12\x0f\n\x0bKEYCODE_TAB\x10=\x12\x11\n\rKEYCODE_SPACE\x10>\x12\x0f\n\x0bKEYCODE_SYM\x10?\x12\x14\n\x10KEYCODE_EXPLORER\x10@\x12\x14\n\x10KEYCODE_ENVELOPE\x10\x41\x12\x11\n\rKEYCODE_ENTER\x10\x42\x12\x0f\n\x0bKEYCODE_DEL\x10\x43\x12\x11\n\rKEYCODE_GRAVE\x10\x44\x12\x11\n\rKEYCODE_MINUS\x10\x45\x12\x12\n\x0eKEYCODE_EQUALS\x10\x46\x12\x18\n\x14KEYCODE_LEFT_BRACKET\x10G\x12\x19\n\x15KEYCODE_RIGHT_BRACKET\x10H\x12\x15\n\x11KEYCODE_BACKSLASH\x10I\x12\x15\n\x11KEYCODE_SEMICOLON\x10J\x12\x16\n\x12KEYCODE_APOSTROPHE\x10K\x12\x11\n\rKEYCODE_SLASH\x10L\x12\x0e\n\nKEYCODE_AT\x10M\x12\x0f\n\x0bKEYCODE_NUM\x10N\x12\x17\n\x13KEYCODE_HEADSETHOOK\x10O\x12\x11\n\rKEYCODE_FOCUS\x10P\x12\x10\n\x0cKEYCODE_PLUS\x10Q\x12\x10\n\x0cKEYCODE_MENU\x10R\x12\x18\n\x14KEYCODE_NOTIFICATION\x10S\x12\x12\n\x0eKEYCODE_SEARCH\x10T\x12\x1c\n\x18KEYCODE_MEDIA_PLAY_PAUSE\x10U\x12\x16\n\x12KEYCODE_MEDIA_STOP\x10V\x12\x16\n\x12KEYCODE_MEDIA_NEXT\x10W\x12\x1a\n\x16KEYCODE_MEDIA_PREVIOUS\x10X\x12\x18\n\x14KEYCODE_MEDIA_REWIND\x10Y\x12\x1e\n\x1aKEYCODE_MEDIA_FAST_FORWARD\x10Z\x12\x10\n\x0cKEYCODE_MUTE\x10[\x12\x13\n\x0fKEYCODE_PAGE_UP\x10\\\x12\x15\n\x11KEYCODE_PAGE_DOWN\x10]\x12\x17\n\x13KEYCODE_PICTSYMBOLS\x10^\x12\x1a\n\x16KEYCODE_SWITCH_CHARSET\x10_\x12\x14\n\x10KEYCODE_BUTTON_A\x10`\x12\x14\n\x10KEYCODE_BUTTON_B\x10\x61\x12\x14\n\x10KEYCODE_BUTTON_C\x10\x62\x12\x14\n\x10KEYCODE_BUTTON_X\x10\x63\x12\x14\n\x10KEYCODE_BUTTON_Y\x10\x64\x12\x14\n\x10KEYCODE_BUTTON_Z\x10\x65\x12\x15\n\x11KEYCODE_BUTTON_L1\x10\x66\x12\x15\n\x11KEYCODE_BUTTON_R1\x10g\x12\x15\n\x11KEYCODE_BUTTON_L2\x10h\x12\x15\n\x11KEYCODE_BUTTON_R2\x10i\x12\x19\n\x15KEYCODE_BUTTON_THUMBL\x10j\x12\x19\n\x15KEYCODE_BUTTON_THUMBR\x10k\x12\x18\n\x14KEYCODE_BUTTON_START\x10l\x12\x19\n\x15KEYCODE_BUTTON_SELECT\x10m\x12\x17\n\x13KEYCODE_BUTTON_MODE\x10n\x12\x12\n\x0eKEYCODE_ESCAPE\x10o\x12\x17\n\x13KEYCODE_FORWARD_DEL\x10p\x12\x15\n\x11KEYCODE_CTRL_LEFT\x10q\x12\x16\n\x12KEYCODE_CTRL_RIGHT\x10r\x12\x15\n\x11KEYCODE_CAPS_LOCK\x10s\x12\x17\n\x13KEYCODE_SCROLL_LOCK\x10t\x12\x15\n\x11KEYCODE_META_LEFT\x10u\x12\x16\n\x12KEYCODE_META_RIGHT\x10v\x12\x14\n\x10KEYCODE_FUNCTION\x10w\x12\x11\n\rKEYCODE_SYSRQ\x10x\x12\x11\n\rKEYCODE_BREAK\x10y\x12\x15\n\x11KEYCODE_MOVE_HOME\x10z\x12\x14\n\x10KEYCODE_MOVE_END\x10{\x12\x12\n\x0eKEYCODE_INSERT\x10|\x12\x13\n\x0fKEYCODE_FORWARD\x10}\x12\x16\n\x12KEYCODE_MEDIA_PLAY\x10~\x12\x17\n\x13KEYCODE_MEDIA_PAUSE\x10\x7f\x12\x18\n\x13KEYCODE_MEDIA_CLOSE\x10\x80\x01\x12\x18\n\x13KEYCODE_MEDIA_EJECT\x10\x81\x01\x12\x19\n\x14KEYCODE_MEDIA_RECORD\x10\x82\x01\x12\x0f\n\nKEYCODE_F1\x10\x83\x01\x12\x0f\n\nKEYCODE_F2\x10\x84\x01\x12\x0f\n\nKEYCODE_F3\x10\x85\x01\x12\x0f\n\nKEYCODE_F4\x10\x86\x01\x12\x0f\n\nKEYCODE_F5\x10\x87\x01\x12\x0f\n\nKEYCODE_F6\x10\x88\x01\x12\x0f\n\nKEYCODE_F7\x10\x89\x01\x12\x0f\n\nKEYCODE_F8\x10\x8a\x01\x12\x0f\n\nKEYCODE_F9\x10\x8b\x01\x12\x10\n\x0bKEYCODE_F10\x10\x8c\x01\x12\x10\n\x0bKEYCODE_F11\x10\x8d\x01\x12\x10\n\x0bKEYCODE_F12\x10\x8e\x01\x12\x15\n\x10KEYCODE_NUM_LOCK\x10\x8f\x01\x12\x15\n\x10KEYCODE_NUMPAD_0\x10\x90\x01\x12\x15\n\x10KEYCODE_NUMPAD_1\x10\x91\x01\x12\x15\n\x10KEYCODE_NUMPAD_2\x10\x92\x01\x12\x15\n\x10KEYCODE_NUMPAD_3\x10\x93\x01\x12\x15\n\x10KEYCODE_NUMPAD_4\x10\x94\x01\x12\x15\n\x10KEYCODE_NUMPAD_5\x10\x95\x01\x12\x15\n\x10KEYCODE_NUMPAD_6\x10\x96\x01\x12\x15\n\x10KEYCODE_NUMPAD_7\x10\x97\x01\x12\x15\n\x10KEYCODE_NUMPAD_8\x10\x98\x01\x12\x15\n\x10KEYCODE_NUMPAD_9\x10\x99\x01\x12\x1a\n\x15KEYCODE_NUMPAD_DIVIDE\x10\x9a\x01\x12\x1c\n\x17KEYCODE_NUMPAD_MULTIPLY\x10\x9b\x01\x12\x1c\n\x17KEYCODE_NUMPAD_SUBTRACT\x10\x9c\x01\x12\x17\n\x12KEYCODE_NUMPAD_ADD\x10\x9d\x01\x12\x17\n\x12KEYCODE_NUMPAD_DOT\x10\x9e\x01\x12\x19\n\x14KEYCODE_NUMPAD_COMMA\x10\x9f\x01\x12\x19\n\x14KEYCODE_NUMPAD_ENTER\x10\xa0\x01\x12\x1a\n\x15KEYCODE_NUMPAD_EQUALS\x10\xa1\x01\x12\x1e\n\x19KEYCODE_NUMPAD_LEFT_PAREN\x10\xa2\x01\x12\x1f\n\x1aKEYCODE_NUMPAD_RIGHT_PAREN\x10\xa3\x01\x12\x18\n\x13KEYCODE_VOLUME_MUTE\x10\xa4\x01\x12\x11\n\x0cKEYCODE_INFO\x10\xa5\x01\x12\x17\n\x12KEYCODE_CHANNEL_UP\x10\xa6\x01\x12\x19\n\x14KEYCODE_CHANNEL_DOWN\x10\xa7\x01\x12\x14\n\x0fKEYCODE_ZOOM_IN\x10\xa8\x01\x12\x15\n\x10KEYCODE_ZOOM_OUT\x10\xa9\x01\x12\x0f\n\nKEYCODE_TV\x10\xaa\x01\x12\x13\n\x0eKEYCODE_WINDOW\x10\xab\x01\x12\x12\n\rKEYCODE_GUIDE\x10\xac\x01\x12\x10\n\x0bKEYCODE_DVR\x10\xad\x01\x12\x15\n\x10KEYCODE_BOOKMARK\x10\xae\x01\x12\x15\n\x10KEYCODE_CAPTIONS\x10\xaf\x01\x12\x15\n\x10KEYCODE_SETTINGS\x10\xb0\x01\x12\x15\n\x10KEYCODE_TV_POWER\x10\xb1\x01\x12\x15\n\x10KEYCODE_TV_INPUT\x10\xb2\x01\x12\x16\n\x11KEYCODE_STB_POWER\x10\xb3\x01\x12\x16\n\x11KEYCODE_STB_INPUT\x10\xb4\x01\x12\x16\n\x11KEYCODE_AVR_POWER\x10\xb5\x01\x12\x16\n\x11KEYCODE_AVR_INPUT\x10\xb6\x01\x12\x15\n\x10KEYCODE_PROG_RED\x10\xb7\x01\x12\x17\n\x12KEYCODE_PROG_GREEN\x10\xb8\x01\x12\x18\n\x13KEYCODE_PROG_YELLOW\x10\xb9\x01\x12\x16\n\x11KEYCODE_PROG_BLUE\x10\xba\x01\x12\x17\n\x12KEYCODE_APP_SWITCH\x10\xbb\x01\x12\x15\n\x10KEYCODE_BUTTON_1\x10\xbc\x01\x12\x15\n\x10KEYCODE_BUTTON_2\x10\xbd\x01\x12\x15\n\x10KEYCODE_BUTTON_3\x10\xbe\x01\x12\x15\n\x10KEYCODE_BUTTON_4\x10\xbf\x01\x12\x15\n\x10KEYCODE_BUTTON_5\x10\xc0\x01\x12\x15\n\x10KEYCODE_BUTTON_6\x10\xc1\x01\x12\x15\n\x10KEYCODE_BUTTON_7\x10\xc2\x01\x12\x15\n\x10KEYCODE_BUTTON_8\x10\xc3\x01\x12\x15\n\x10KEYCODE_BUTTON_9\x10\xc4\x01\x12\x16\n\x11KEYCODE_BUTTON_10\x10\xc5\x01\x12\x16\n\x11KEYCODE_BUTTON_11\x10\xc6\x01\x12\x16\n\x11KEYCODE_BUTTON_12\x10\xc7\x01\x12\x16\n\x11KEYCODE_BUTTON_13\x10\xc8\x01\x12\x16\n\x11KEYCODE_BUTTON_14\x10\xc9\x01\x12\x16\n\x11KEYCODE_BUTTON_15\x10\xca\x01\x12\x16\n\x11KEYCODE_BUTTON_16\x10\xcb\x01\x12\x1c\n\x17KEYCODE_LANGUAGE_SWITCH\x10\xcc\x01\x12\x18\n\x13KEYCODE_MANNER_MODE\x10\xcd\x01\x12\x14\n\x0fKEYCODE_3D_MODE\x10\xce\x01\x12\x15\n\x10KEYCODE_CONTACTS\x10\xcf\x01\x12\x15\n\x10KEYCODE_CALENDAR\x10\xd0\x01\x12\x12\n\rKEYCODE_MUSIC\x10\xd1\x01\x12\x17\n\x12KEYCODE_CALCULATOR\x10\xd2\x01\x12\x1c\n\x17KEYCODE_ZENKAKU_HANKAKU\x10\xd3\x01\x12\x11\n\x0cKEYCODE_EISU\x10\xd4\x01\x12\x15\n\x10KEYCODE_MUHENKAN\x10\xd5\x01\x12\x13\n\x0eKEYCODE_HENKAN\x10\xd6\x01\x12\x1e\n\x19KEYCODE_KATAKANA_HIRAGANA\x10\xd7\x01\x12\x10\n\x0bKEYCODE_YEN\x10\xd8\x01\x12\x0f\n\nKEYCODE_RO\x10\xd9\x01\x12\x11\n\x0cKEYCODE_KANA\x10\xda\x01\x12\x13\n\x0eKEYCODE_ASSIST\x10\xdb\x01\x12\x1c\n\x17KEYCODE_BRIGHTNESS_DOWN\x10\xdc\x01\x12\x1a\n\x15KEYCODE_BRIGHTNESS_UP\x10\xdd\x01\x12\x1e\n\x19KEYCODE_MEDIA_AUDIO_TRACK\x10\xde\x01\x12\x12\n\rKEYCODE_SLEEP\x10\xdf\x01\x12\x13\n\x0eKEYCODE_WAKEUP\x10\xe0\x01\x12\x14\n\x0fKEYCODE_PAIRING\x10\xe1\x01\x12\x1b\n\x16KEYCODE_MEDIA_TOP_MENU\x10\xe2\x01\x12\x0f\n\nKEYCODE_11\x10\xe3\x01\x12\x0f\n\nKEYCODE_12\x10\xe4\x01\x12\x19\n\x14KEYCODE_LAST_CHANNEL\x10\xe5\x01\x12\x1c\n\x17KEYCODE_TV_DATA_SERVICE\x10\xe6\x01\x12\x19\n\x14KEYCODE_VOICE_ASSIST\x10\xe7\x01\x12\x1d\n\x18KEYCODE_TV_RADIO_SERVICE\x10\xe8\x01\x12\x18\n\x13KEYCODE_TV_TELETEXT\x10\xe9\x01\x12\x1c\n\x17KEYCODE_TV_NUMBER_ENTRY\x10\xea\x01\x12\"\n\x1dKEYCODE_TV_TERRESTRIAL_ANALOG\x10\xeb\x01\x12#\n\x1eKEYCODE_TV_TERRESTRIAL_DIGITAL\x10\xec\x01\x12\x19\n\x14KEYCODE_TV_SATELLITE\x10\xed\x01\x12\x1c\n\x17KEYCODE_TV_SATELLITE_BS\x10\xee\x01\x12\x1c\n\x17KEYCODE_TV_SATELLITE_CS\x10\xef\x01\x12!\n\x1cKEYCODE_TV_SATELLITE_SERVICE\x10\xf0\x01\x12\x17\n\x12KEYCODE_TV_NETWORK\x10\xf1\x01\x12\x1d\n\x18KEYCODE_TV_ANTENNA_CABLE\x10\xf2\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_1\x10\xf3\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_2\x10\xf4\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_3\x10\xf5\x01\x12\x1c\n\x17KEYCODE_TV_INPUT_HDMI_4\x10\xf6\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPOSITE_1\x10\xf7\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPOSITE_2\x10\xf8\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPONENT_1\x10\xf9\x01\x12!\n\x1cKEYCODE_TV_INPUT_COMPONENT_2\x10\xfa\x01\x12\x1b\n\x16KEYCODE_TV_INPUT_VGA_1\x10\xfb\x01\x12!\n\x1cKEYCODE_TV_AUDIO_DESCRIPTION\x10\xfc\x01\x12(\n#KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP\x10\xfd\x01\x12*\n%KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN\x10\xfe\x01\x12\x19\n\x14KEYCODE_TV_ZOOM_MODE\x10\xff\x01\x12\x1d\n\x18KEYCODE_TV_CONTENTS_MENU\x10\x80\x02\x12\"\n\x1dKEYCODE_TV_MEDIA_CONTEXT_MENU\x10\x81\x02\x12!\n\x1cKEYCODE_TV_TIMER_PROGRAMMING\x10\x82\x02\x12\x11\n\x0cKEYCODE_HELP\x10\x83\x02\x12\x1e\n\x19KEYCODE_NAVIGATE_PREVIOUS\x10\x84\x02\x12\x1a\n\x15KEYCODE_NAVIGATE_NEXT\x10\x85\x02\x12\x18\n\x13KEYCODE_NAVIGATE_IN\x10\x86\x02\x12\x19\n\x14KEYCODE_NAVIGATE_OUT\x10\x87\x02\x12\x19\n\x14KEYCODE_STEM_PRIMARY\x10\x88\x02\x12\x13\n\x0eKEYCODE_STEM_1\x10\x89\x02\x12\x13\n\x0eKEYCODE_STEM_2\x10\x8a\x02\x12\x13\n\x0eKEYCODE_STEM_3\x10\x8b\x02\x12\x19\n\x14KEYCODE_DPAD_UP_LEFT\x10\x8c\x02\x12\x1b\n\x16KEYCODE_DPAD_DOWN_LEFT\x10\x8d\x02\x12\x1a\n\x15KEYCODE_DPAD_UP_RIGHT\x10\x8e\x02\x12\x1c\n\x17KEYCODE_DPAD_DOWN_RIGHT\x10\x8f\x02\x12\x1f\n\x1aKEYCODE_MEDIA_SKIP_FORWARD\x10\x90\x02\x12 \n\x1bKEYCODE_MEDIA_SKIP_BACKWARD\x10\x91\x02\x12\x1f\n\x1aKEYCODE_MEDIA_STEP_FORWARD\x10\x92\x02\x12 \n\x1bKEYCODE_MEDIA_STEP_BACKWARD\x10\x93\x02\x12\x17\n\x12KEYCODE_SOFT_SLEEP\x10\x94\x02\x12\x10\n\x0bKEYCODE_CUT\x10\x95\x02\x12\x11\n\x0cKEYCODE_COPY\x10\x96\x02\x12\x12\n\rKEYCODE_PASTE\x10\x97\x02\x12!\n\x1cKEYCODE_SYSTEM_NAVIGATION_UP\x10\x98\x02\x12#\n\x1eKEYCODE_SYSTEM_NAVIGATION_DOWN\x10\x99\x02\x12#\n\x1eKEYCODE_SYSTEM_NAVIGATION_LEFT\x10\x9a\x02\x12$\n\x1fKEYCODE_SYSTEM_NAVIGATION_RIGHT\x10\x9b\x02\x12\x15\n\x10KEYCODE_ALL_APPS\x10\x9c\x02\x12\x14\n\x0fKEYCODE_REFRESH\x10\x9d\x02\x12\x16\n\x11KEYCODE_THUMBS_UP\x10\x9e\x02\x12\x18\n\x13KEYCODE_THUMBS_DOWN\x10\x9f\x02\x12\x1b\n\x16KEYCODE_PROFILE_SWITCH\x10\xa0\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_1\x10\xa1\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_2\x10\xa2\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_3\x10\xa3\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_4\x10\xa4\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_5\x10\xa5\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_6\x10\xa6\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_7\x10\xa7\x02\x12\x18\n\x13KEYCODE_VIDEO_APP_8\x10\xa8\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_1\x10\xa9\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_2\x10\xaa\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_3\x10\xab\x02\x12\x1b\n\x16KEYCODE_FEATURED_APP_4\x10\xac\x02\x12\x17\n\x12KEYCODE_DEMO_APP_1\x10\xad\x02\x12\x17\n\x12KEYCODE_DEMO_APP_2\x10\xae\x02\x12\x17\n\x12KEYCODE_DEMO_APP_3\x10\xaf\x02\x12\x17\n\x12KEYCODE_DEMO_APP_4\x10\xb0\x02*Q\n\x0fRemoteDirection\x12\x15\n\x11UNKNOWN_DIRECTION\x10\x00\x12\x0e\n\nSTART_LONG\x10\x01\x12\x0c\n\x08\x45ND_LONG\x10\x02\x12\t\n\x05SHORT\x10\x03\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'remotemessage_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _globals['_REMOTEKEYCODE']._serialized_start=2791 - _globals['_REMOTEKEYCODE']._serialized_end=9934 - _globals['_REMOTEDIRECTION']._serialized_start=9936 - _globals['_REMOTEDIRECTION']._serialized_end=10017 +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_REMOTEKEYCODE']._serialized_start=2890 + _globals['_REMOTEKEYCODE']._serialized_end=10033 + _globals['_REMOTEDIRECTION']._serialized_start=10035 + _globals['_REMOTEDIRECTION']._serialized_end=10116 _globals['_REMOTEAPPLINKLAUNCHREQUEST']._serialized_start=31 _globals['_REMOTEAPPLINKLAUNCHREQUEST']._serialized_end=77 _globals['_REMOTERESETPREFERREDAUDIODEVICE']._serialized_start=79 @@ -37,39 +37,39 @@ _globals['_REMOTESTART']._serialized_start=357 _globals['_REMOTESTART']._serialized_end=387 _globals['_REMOTEVOICEEND']._serialized_start=389 - _globals['_REMOTEVOICEEND']._serialized_end=405 - _globals['_REMOTEVOICEPAYLOAD']._serialized_start=407 - _globals['_REMOTEVOICEPAYLOAD']._serialized_end=427 - _globals['_REMOTEVOICEBEGIN']._serialized_start=429 - _globals['_REMOTEVOICEBEGIN']._serialized_end=447 - _globals['_REMOTETEXTFIELDSTATUS']._serialized_start=449 - _globals['_REMOTETEXTFIELDSTATUS']._serialized_end=567 - _globals['_REMOTEIMESHOWREQUEST']._serialized_start=569 - _globals['_REMOTEIMESHOWREQUEST']._serialized_end=656 - _globals['_REMOTEEDITINFO']._serialized_start=658 - _globals['_REMOTEEDITINFO']._serialized_end=742 - _globals['_REMOTEIMEOBJECT']._serialized_start=744 - _globals['_REMOTEIMEOBJECT']._serialized_end=804 - _globals['_REMOTEIMEBATCHEDIT']._serialized_start=806 - _globals['_REMOTEIMEBATCHEDIT']._serialized_end=913 - _globals['_REMOTEAPPINFO']._serialized_start=916 - _globals['_REMOTEAPPINFO']._serialized_end=1069 - _globals['_REMOTEIMEKEYINJECT']._serialized_start=1071 - _globals['_REMOTEIMEKEYINJECT']._serialized_end=1190 - _globals['_REMOTEKEYINJECT']._serialized_start=1192 - _globals['_REMOTEKEYINJECT']._serialized_end=1294 - _globals['_REMOTEPINGRESPONSE']._serialized_start=1296 - _globals['_REMOTEPINGRESPONSE']._serialized_end=1330 - _globals['_REMOTEPINGREQUEST']._serialized_start=1332 - _globals['_REMOTEPINGREQUEST']._serialized_end=1379 - _globals['_REMOTESETACTIVE']._serialized_start=1381 - _globals['_REMOTESETACTIVE']._serialized_end=1414 - _globals['_REMOTEDEVICEINFO']._serialized_start=1417 - _globals['_REMOTEDEVICEINFO']._serialized_end=1545 - _globals['_REMOTECONFIGURE']._serialized_start=1547 - _globals['_REMOTECONFIGURE']._serialized_end=1626 - _globals['_REMOTEERROR']._serialized_start=1628 - _globals['_REMOTEERROR']._serialized_end=1696 - _globals['_REMOTEMESSAGE']._serialized_start=1699 - _globals['_REMOTEMESSAGE']._serialized_end=2788 + _globals['_REMOTEVOICEEND']._serialized_end=425 + _globals['_REMOTEVOICEPAYLOAD']._serialized_start=427 + _globals['_REMOTEVOICEPAYLOAD']._serialized_end=484 + _globals['_REMOTEVOICEBEGIN']._serialized_start=486 + _globals['_REMOTEVOICEBEGIN']._serialized_end=546 + _globals['_REMOTETEXTFIELDSTATUS']._serialized_start=548 + _globals['_REMOTETEXTFIELDSTATUS']._serialized_end=666 + _globals['_REMOTEIMESHOWREQUEST']._serialized_start=668 + _globals['_REMOTEIMESHOWREQUEST']._serialized_end=755 + _globals['_REMOTEEDITINFO']._serialized_start=757 + _globals['_REMOTEEDITINFO']._serialized_end=841 + _globals['_REMOTEIMEOBJECT']._serialized_start=843 + _globals['_REMOTEIMEOBJECT']._serialized_end=903 + _globals['_REMOTEIMEBATCHEDIT']._serialized_start=905 + _globals['_REMOTEIMEBATCHEDIT']._serialized_end=1012 + _globals['_REMOTEAPPINFO']._serialized_start=1015 + _globals['_REMOTEAPPINFO']._serialized_end=1168 + _globals['_REMOTEIMEKEYINJECT']._serialized_start=1170 + _globals['_REMOTEIMEKEYINJECT']._serialized_end=1289 + _globals['_REMOTEKEYINJECT']._serialized_start=1291 + _globals['_REMOTEKEYINJECT']._serialized_end=1393 + _globals['_REMOTEPINGRESPONSE']._serialized_start=1395 + _globals['_REMOTEPINGRESPONSE']._serialized_end=1429 + _globals['_REMOTEPINGREQUEST']._serialized_start=1431 + _globals['_REMOTEPINGREQUEST']._serialized_end=1478 + _globals['_REMOTESETACTIVE']._serialized_start=1480 + _globals['_REMOTESETACTIVE']._serialized_end=1513 + _globals['_REMOTEDEVICEINFO']._serialized_start=1516 + _globals['_REMOTEDEVICEINFO']._serialized_end=1644 + _globals['_REMOTECONFIGURE']._serialized_start=1646 + _globals['_REMOTECONFIGURE']._serialized_end=1725 + _globals['_REMOTEERROR']._serialized_start=1727 + _globals['_REMOTEERROR']._serialized_end=1795 + _globals['_REMOTEMESSAGE']._serialized_start=1798 + _globals['_REMOTEMESSAGE']._serialized_end=2887 # @@protoc_insertion_point(module_scope) diff --git a/src/androidtvremote2/remotemessage_pb2.pyi b/src/androidtvremote2/remotemessage_pb2.pyi index 80c1a2e..f6eda85 100644 --- a/src/androidtvremote2/remotemessage_pb2.pyi +++ b/src/androidtvremote2/remotemessage_pb2.pyi @@ -1863,9 +1863,14 @@ global___RemoteStart = RemoteStart class RemoteVoiceEnd(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + SESSION_ID_FIELD_NUMBER: builtins.int + session_id: builtins.int def __init__( self, + *, + session_id: builtins.int = ..., ) -> None: ... + def ClearField(self, field_name: typing.Literal["session_id", b"session_id"]) -> None: ... global___RemoteVoiceEnd = RemoteVoiceEnd @@ -1873,9 +1878,20 @@ global___RemoteVoiceEnd = RemoteVoiceEnd class RemoteVoicePayload(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + SESSION_ID_FIELD_NUMBER: builtins.int + SAMPLES_FIELD_NUMBER: builtins.int + session_id: builtins.int + samples: builtins.bytes + """Audio configuration in RemoteVoiceBegin is unknown. + Default audio sample payload is a sequence of 16-bit PCM, 8 kHz, mono samples, split into 20 KB messages. + """ def __init__( self, + *, + session_id: builtins.int = ..., + samples: builtins.bytes = ..., ) -> None: ... + def ClearField(self, field_name: typing.Literal["samples", b"samples", "session_id", b"session_id"]) -> None: ... global___RemoteVoicePayload = RemoteVoicePayload @@ -1883,9 +1899,18 @@ global___RemoteVoicePayload = RemoteVoicePayload class RemoteVoiceBegin(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor + SESSION_ID_FIELD_NUMBER: builtins.int + PACKAGE_NAME_FIELD_NUMBER: builtins.int + session_id: builtins.int + package_name: builtins.str + """Package name is sent from the Android device as a response to sending KEYCODE_SEARCH and not required when sending audio.""" def __init__( self, + *, + session_id: builtins.int = ..., + package_name: builtins.str = ..., ) -> None: ... + def ClearField(self, field_name: typing.Literal["package_name", b"package_name", "session_id", b"session_id"]) -> None: ... global___RemoteVoiceBegin = RemoteVoiceBegin diff --git a/src/androidtvremote2/voice_stream.py b/src/androidtvremote2/voice_stream.py new file mode 100644 index 0000000..10ab8f2 --- /dev/null +++ b/src/androidtvremote2/voice_stream.py @@ -0,0 +1,74 @@ +"""High-level voice streaming session wrapper.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from androidtvremote2.const import LOGGER + +if TYPE_CHECKING: + from types import TracebackType + + from androidtvremote2.remote import RemoteProtocol + + +class VoiceStream: + """High-level voice streaming session wrapper. + + Obtained from AndroidTVRemote.start_voice(). + """ + + def __init__(self, proto: RemoteProtocol, session_id: int) -> None: + """Initialize. + + param proto: RemoteProtocol instance. + param session_id: voice session id. + """ + self._proto = proto + self.session_id = session_id + self._closed = False + + def send_chunk(self, chunk: bytes) -> bool: + """Send a chunk of audio data. + + - The audio data must be 16-bit PCM, mono, 8000 Hz. + - The chunk size should be at least 8 KB. Smaller chunks will be padded with zeros. + - Chunk sizes larger than 20 KB will be split into multiple chunks. + + :param chunk: The PCM audio data chunk to be sent. Should be a multiple of 8 KB. + :return: False if a voice session is closed. + :raises ConnectionClosed: If the connection is lost. + """ + if self._closed: + LOGGER.debug("VoiceStream already closed") + return False + self._proto.send_voice_chunk(chunk, self.session_id) + return True + + def end(self) -> None: + """End the voice stream.""" + if not self._closed: + self._proto.end_voice(self.session_id) + self._closed = True + + async def __aenter__(self) -> VoiceStream: + """Enter async context manager.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + """End the asynchronous context manager session. + + This method implements the asynchronous exit for a context manager. It is called + when execution leaves the `async with` block that the instance of the class + is managing. + + :param exc_type: The exception type raised within the context block if an exception occurred. + :param exc: The exception object raised within the context block if an exception was raised. + :param tb: The traceback object associated with the raised exception, if any. + """ + self.end() diff --git a/src/demo.py b/src/demo.py index 6af4845..34604c6 100644 --- a/src/demo.py +++ b/src/demo.py @@ -1,12 +1,16 @@ -# ruff: noqa: T201, PLR0912 +# ruff: noqa: T201, PLR0912, PLR0915 """Demo usage of AndroidTVRemote.""" import argparse import asyncio import logging +import os import sys +import time +import wave from typing import cast +import pyaudio from pynput import keyboard from zeroconf import ServiceStateChange, Zeroconf from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf @@ -21,6 +25,14 @@ _LOGGER = logging.getLogger(__name__) +VOICE_ENABLED = True +VOICE_RECORD_SECONDS = 5 +VOICE_STREAM_SECONDS = 10 +VOICE_FORMAT = pyaudio.paInt16 +VOICE_CHANNELS = 1 +VOICE_RATE = 8000 +VOICE_FILE = "voice_command.wav" + async def _bind_keyboard(remote: AndroidTVRemote) -> None: print( @@ -39,7 +51,11 @@ async def _bind_keyboard(remote: AndroidTVRemote) -> None: "\n- 'a': Amazon Prime Video" "\n- 'k': Kodi" "\n- 'q': quit" - "\n- 't': send text 'Hello world' to Android TV\n\n" + "\n- 't': send text 'Hello world' to Android TV" + "\n- 'v': stream a voice command from the default audio input. Press v again to stop streaming." + "\n- 'r': record a " + str(VOICE_RECORD_SECONDS) + "s voice command" + "\n- 'p': play back pre-recorded voice command" + "\n- 'w': send pre-recorded voice command in " + VOICE_FILE + " to Android TV\n\n" ) key_mappings = { keyboard.Key.up: "DPAD_UP", @@ -65,6 +81,8 @@ def on_press(key: keyboard.Key | keyboard.KeyCode | None) -> None: return queue key_queue = transmit_keys() + voice_task: asyncio.Task[None] | None = None + voice_stop_event = asyncio.Event() while True: key = await key_queue.get() if key is None: @@ -94,6 +112,20 @@ def on_press(key: keyboard.Key | keyboard.KeyCode | None) -> None: remote.send_launch_app_command("org.xbmc.kodi") elif key.char == "t": remote.send_text("Hello World!") + if key.char == "r": + _record_voice_command(VOICE_FILE) + elif key.char == "p": + _play_voice_command(VOICE_FILE) + elif key.char == "v": + if voice_task is not None and not voice_task.done(): + print("Stopping voice recording") + voice_stop_event.set() + continue + print("Starting voice recording. Press v again to stop. Auto stop after " + str(VOICE_STREAM_SECONDS) + "s") + voice_stop_event.clear() + voice_task = asyncio.get_event_loop().create_task(_stream_voice(remote, voice_stop_event)) + elif key.char == "w": + await _send_voice(VOICE_FILE, remote) async def _host_from_zeroconf(timeout: float) -> str: @@ -186,7 +218,7 @@ async def _main() -> None: host = args.host or await _host_from_zeroconf(args.scan_timeout) - remote = AndroidTVRemote(args.client_name, args.certfile, args.keyfile, host) + remote = AndroidTVRemote(args.client_name, args.certfile, args.keyfile, host, enable_voice=VOICE_ENABLED) if await remote.async_generate_cert_if_missing(): _LOGGER.info("Generated new certificate") @@ -209,6 +241,7 @@ async def _main() -> None: _LOGGER.info("is_on: %s", remote.is_on) _LOGGER.info("current_app: %s", remote.current_app) _LOGGER.info("volume_info: %s", remote.volume_info) + _LOGGER.info("voice enabled: %s", remote.is_voice_enabled) def is_on_updated(is_on: bool) -> None: _LOGGER.info("Notified that is_on: %s", is_on) @@ -230,4 +263,146 @@ def is_available_updated(is_available: bool) -> None: await _bind_keyboard(remote) +async def _send_voice(wav_file: str, remote: AndroidTVRemote) -> None: + """Send a WAV file as a voice command.""" + if not os.path.isfile(wav_file): + _LOGGER.error("WAV file not found: %s", wav_file) + return + if not remote.is_voice_enabled: + _LOGGER.warning("Voice feature is not enabled in the client or not supported on the device") + return + + try: + with wave.open(wav_file, "rb") as wf: + if wf.getnchannels() != 1: + _LOGGER.error("Only mono WAV files are supported") + return + if wf.getsampwidth() != 2: + _LOGGER.error("Only 16-bit WAV files are supported") + return + if wf.getframerate() != 8000: + _LOGGER.error("Only 8 kHz WAV files are supported") + return + nframes = wf.getnframes() + pcm_data = wf.readframes(nframes) + + _LOGGER.debug("Loaded WAV file '%s': frames=%d, bytes=%d", wav_file, nframes, len(pcm_data)) + + async with await remote.start_voice() as session: + session.send_chunk(pcm_data) + except FileNotFoundError: + _LOGGER.exception("WAV file not found") + except wave.Error: + _LOGGER.exception("Invalid/unsupported WAV file %s", wav_file) + except asyncio.TimeoutError: + _LOGGER.warning("Timeout: could not start voice session") + except Exception: + _LOGGER.exception("Unexpected error in send_voice") + + +def _record_voice_command(wav_file: str) -> None: + """Record a WAV file from the default audio input.""" + with wave.open(wav_file, "wb") as wf: + p = pyaudio.PyAudio() + wf.setnchannels(VOICE_CHANNELS) + wf.setsampwidth(p.get_sample_size(VOICE_FORMAT)) + wf.setframerate(VOICE_RATE) + + def callback(in_data: bytes, frame_count: object, time_info: object, status: object) -> tuple[bytes | None, int]: + wf.writeframes(in_data) + return None, pyaudio.paContinue + + stream = p.open(format=VOICE_FORMAT, channels=VOICE_CHANNELS, rate=VOICE_RATE, input=True, stream_callback=callback) + + print("Recording " + str(VOICE_RECORD_SECONDS) + "s voice command to:", wav_file) + start = time.time() + while stream.is_active() and (time.time() - start) < VOICE_RECORD_SECONDS: + time.sleep(0.1) + print("Recording stopped") + + stream.close() + p.terminate() + + +def _play_voice_command(wav_file: str) -> None: + """Play a WAV file on the default audio output.""" + if not os.path.isfile(wav_file): + _LOGGER.error("WAV file not found: %s", wav_file) + return + print("Playing back recorded voice command:", wav_file) + with wave.open(wav_file, "rb") as wf: + p = pyaudio.PyAudio() + stream = p.open( + format=p.get_format_from_width(wf.getsampwidth()), channels=wf.getnchannels(), rate=wf.getframerate(), output=True + ) + + chunk_size = 1024 + while len(data := wf.readframes(chunk_size)): + stream.write(data) + + stream.close() + p.terminate() + + +async def _stream_voice(remote: AndroidTVRemote, stop_event: asyncio.Event) -> None: + """Record from the default audio input and stream as a voice command.""" + chunk_size = 8 * 1024 + + if not remote.is_voice_enabled: + _LOGGER.warning("Voice feature is not enabled in the client or not supported on the device") + return + + # Start a streaming voice session + # Context manager calls session.end() automatically + try: + async with await remote.start_voice() as session: + if not remote.is_voice_enabled: + _LOGGER.warning("Voice feature is not enabled in the client or not supported on the device") + return + + def callback(in_data: bytes, frame_count: object, time_info: object, status: object) -> tuple[bytes | None, int]: + _LOGGER.debug("MIC callback: frame_count=%d, time_info=%s, status=%s", frame_count, time_info, status) + session.send_chunk(in_data) + return None, pyaudio.paContinue + + _LOGGER.info("Voice session established, opening microphone...") + p = pyaudio.PyAudio() + stream = p.open( + format=VOICE_FORMAT, + channels=VOICE_CHANNELS, + rate=VOICE_RATE, + input=True, + frames_per_buffer=chunk_size, + stream_callback=callback, + ) + + _LOGGER.info("Recording started, sending data to Android TV...") + start = time.time() + + # Use run_in_executor to check stream status without blocking + loop = asyncio.get_event_loop() + # Wait until timeout, stop_event is set, or stream becomes inactive + while (time.time() - start) < VOICE_RECORD_SECONDS: + if stop_event.is_set(): + _LOGGER.debug("Recording stopped by external event") + break + + if not await loop.run_in_executor(None, stream.is_active): + _LOGGER.debug("Recording stopped: stream became inactive") + break + + try: + await asyncio.wait_for(stop_event.wait(), timeout=0.25) + break + except asyncio.TimeoutError: + pass + + print("Voice data sent, closing microphone") + + stream.close() + p.terminate() + except asyncio.TimeoutError as e: + print("Timeout: could not start voice session.", e) + + asyncio.run(_main(), debug=True)