1 """Support for the Roku media player."""
3 from __future__
import annotations
10 from rokuecp.helpers
import guess_stream_format
11 import voluptuous
as vol
18 MediaPlayerDeviceClass,
20 MediaPlayerEntityFeature,
23 async_process_play_media_url,
33 from .browse_media
import async_browse_media
44 from .coordinator
import RokuDataUpdateCoordinator
45 from .entity
import RokuEntity
46 from .helpers
import format_channel_name, roku_exception_handler
48 _LOGGER = logging.getLogger(__name__)
51 STREAM_FORMAT_TO_MEDIA_TYPE = {
52 "dash": MediaType.VIDEO,
53 "hls": MediaType.VIDEO,
54 "ism": MediaType.VIDEO,
55 "m4a": MediaType.MUSIC,
56 "m4v": MediaType.VIDEO,
57 "mka": MediaType.MUSIC,
58 "mkv": MediaType.VIDEO,
59 "mks": MediaType.VIDEO,
60 "mp3": MediaType.MUSIC,
61 "mp4": MediaType.VIDEO,
64 ATTRS_TO_LAUNCH_PARAMS = {
65 ATTR_CONTENT_ID:
"contentID",
66 ATTR_MEDIA_TYPE:
"mediaType",
69 ATTRS_TO_PLAY_ON_ROKU_PARAMS = {
70 ATTR_NAME:
"videoName",
71 ATTR_FORMAT:
"videoFormat",
75 ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS = {
76 ATTR_NAME:
"songName",
77 ATTR_FORMAT:
"songFormat",
78 ATTR_ARTIST_NAME:
"artistName",
79 ATTR_THUMBNAIL:
"albumArtUrl",
82 SEARCH_SCHEMA: VolDictType = {vol.Required(ATTR_KEYWORD): str}
86 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
88 """Set up the Roku config entry."""
89 coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
94 coordinator=coordinator,
100 platform = entity_platform.async_get_current_platform()
102 platform.async_register_entity_service(
110 """Representation of a Roku media player on the network."""
113 _attr_supported_features = (
114 MediaPlayerEntityFeature.PREVIOUS_TRACK
115 | MediaPlayerEntityFeature.NEXT_TRACK
116 | MediaPlayerEntityFeature.VOLUME_STEP
117 | MediaPlayerEntityFeature.VOLUME_MUTE
118 | MediaPlayerEntityFeature.SELECT_SOURCE
119 | MediaPlayerEntityFeature.PAUSE
120 | MediaPlayerEntityFeature.PLAY
121 | MediaPlayerEntityFeature.PLAY_MEDIA
122 | MediaPlayerEntityFeature.TURN_ON
123 | MediaPlayerEntityFeature.TURN_OFF
124 | MediaPlayerEntityFeature.BROWSE_MEDIA
127 def __init__(self, coordinator: RokuDataUpdateCoordinator) ->
None:
128 """Initialize the Roku device."""
129 super().
__init__(coordinator=coordinator)
130 if coordinator.data.info.device_type ==
"tv":
136 """Detect if we have enough media data to track playback."""
137 if self.coordinator.data.media
is None or self.coordinator.data.media.live:
140 return self.coordinator.data.media.duration > 0
143 def state(self) -> MediaPlayerState | None:
144 """Return the state of the device."""
145 if self.coordinator.data.state.standby:
146 return MediaPlayerState.STANDBY
148 if self.coordinator.data.app
is None:
152 self.coordinator.data.app.name
in {
"Power Saver",
"Roku"}
153 or self.coordinator.data.app.screensaver
155 return MediaPlayerState.IDLE
157 if self.coordinator.data.media:
158 if self.coordinator.data.media.paused:
159 return MediaPlayerState.PAUSED
160 return MediaPlayerState.PLAYING
162 if self.coordinator.data.app.name:
163 return MediaPlayerState.ON
169 """Content type of current playing media."""
174 return MediaType.CHANNEL
180 """Image url of current playing media."""
188 """Name of the current running app."""
189 if self.coordinator.data.app
is not None:
190 return self.coordinator.data.app.name
196 """Return the ID of the current running app."""
197 if self.coordinator.data.app
is not None:
198 return self.coordinator.data.app.app_id
204 """Return the TV channel currently tuned."""
205 if self.
app_idapp_idapp_idapp_id !=
"tvinput.dtv" or self.coordinator.data.channel
is None:
208 channel = self.coordinator.data.channel
214 """Return the title of current playing media."""
215 if self.
app_idapp_idapp_idapp_id !=
"tvinput.dtv" or self.coordinator.data.channel
is None:
218 if self.coordinator.data.channel.program_title
is not None:
219 return self.coordinator.data.channel.program_title
225 """Duration of current playing media in seconds."""
227 return self.coordinator.data.media.duration
233 """Position of current playing media in seconds."""
235 return self.coordinator.data.media.position
241 """When was the position of the current playing media valid."""
243 return self.coordinator.data.media.at
249 """Return the current input source."""
250 if self.coordinator.data.app
is not None:
251 return self.coordinator.data.app.name
257 """List of available input sources."""
261 app.name
for app
in self.coordinator.data.apps
if app.name
is not None
265 @roku_exception_handler()
266 async
def search(self, keyword: str) ->
None:
267 """Emulate opening the search screen and entering the search keyword."""
268 await self.coordinator.roku.search(keyword)
272 media_content_type: MediaType | str,
273 media_content_id: str,
274 media_image_id: str |
None =
None,
275 ) -> tuple[bytes |
None, str |
None]:
276 """Fetch media browser image to serve via proxy."""
277 if media_content_type == MediaType.APP
and media_content_id:
278 image_url = self.coordinator.roku.app_icon_url(media_content_id)
285 media_content_type: MediaType | str |
None =
None,
286 media_content_id: str |
None =
None,
288 """Implement the websocket media browsing helper."""
297 @roku_exception_handler()
299 """Turn on the Roku."""
300 await self.coordinator.roku.remote(
"poweron")
301 await self.coordinator.async_request_refresh()
303 @roku_exception_handler(ignore_timeout=True)
305 """Turn off the Roku."""
306 await self.coordinator.roku.remote(
"poweroff")
307 await self.coordinator.async_request_refresh()
309 @roku_exception_handler()
311 """Send pause command."""
313 await self.coordinator.roku.remote(
"play")
314 await self.coordinator.async_request_refresh()
316 @roku_exception_handler()
318 """Send play command."""
320 await self.coordinator.roku.remote(
"play")
321 await self.coordinator.async_request_refresh()
323 @roku_exception_handler()
325 """Send play/pause command."""
327 await self.coordinator.roku.remote(
"play")
328 await self.coordinator.async_request_refresh()
330 @roku_exception_handler()
332 """Send previous track command."""
333 await self.coordinator.roku.remote(
"reverse")
334 await self.coordinator.async_request_refresh()
336 @roku_exception_handler()
338 """Send next track command."""
339 await self.coordinator.roku.remote(
"forward")
340 await self.coordinator.async_request_refresh()
342 @roku_exception_handler()
344 """Mute the volume."""
345 await self.coordinator.roku.remote(
"volume_mute")
346 await self.coordinator.async_request_refresh()
348 @roku_exception_handler()
350 """Volume up media player."""
351 await self.coordinator.roku.remote(
"volume_up")
353 @roku_exception_handler()
355 """Volume down media player."""
356 await self.coordinator.roku.remote(
"volume_down")
358 @roku_exception_handler()
360 self, media_type: MediaType | str, media_id: str, **kwargs: Any
362 """Play media from a URL or file, launch an application, or tune to a channel."""
363 extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA)
or {}
364 original_media_type: str = media_type
365 original_media_id: str = media_id
366 mime_type: str |
None =
None
367 stream_name: str |
None =
None
368 stream_format: str |
None = extra.get(ATTR_FORMAT)
371 if media_source.is_media_source_id(media_id):
372 sourced_media = await media_source.async_resolve_media(
375 media_type = MediaType.URL
376 media_id = sourced_media.url
377 mime_type = sourced_media.mime_type
378 stream_name = original_media_id
379 stream_format = guess_stream_format(media_id, mime_type)
381 if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]:
382 media_type = MediaType.VIDEO
383 mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER]
384 stream_name =
"Camera Stream"
385 stream_format =
"hls"
387 if media_type
in {MediaType.MUSIC, MediaType.URL, MediaType.VIDEO}:
391 parsed = yarl.URL(media_id)
393 if mime_type
is None:
394 mime_type, _ = mimetypes.guess_type(parsed.path)
396 if stream_format
is None:
397 stream_format = guess_stream_format(media_id, mime_type)
399 if extra.get(ATTR_FORMAT)
is None:
400 extra[ATTR_FORMAT] = stream_format
402 if extra[ATTR_FORMAT]
not in STREAM_FORMAT_TO_MEDIA_TYPE:
404 "Media type %s is not supported with format %s (mime: %s)",
412 media_type == MediaType.URL
413 and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MediaType.MUSIC
415 media_type = MediaType.MUSIC
417 if media_type == MediaType.MUSIC
and "tts_proxy" in media_id:
418 stream_name =
"Text to Speech"
419 elif stream_name
is None:
420 if stream_format ==
"ism":
421 stream_name = parsed.parts[-2]
423 stream_name = parsed.name
425 if extra.get(ATTR_NAME)
is None:
426 extra[ATTR_NAME] = stream_name
428 if media_type == MediaType.APP:
431 for attr, param
in ATTRS_TO_LAUNCH_PARAMS.items()
435 await self.coordinator.roku.launch(media_id, params)
436 elif media_type == MediaType.CHANNEL:
437 await self.coordinator.roku.tune(media_id)
438 elif media_type == MediaType.MUSIC:
439 if extra.get(ATTR_ARTIST_NAME)
is None:
440 extra[ATTR_ARTIST_NAME] =
"Home Assistant"
444 for (attr, param)
in ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS.items()
448 params = {
"u": media_id,
"t":
"a", **params}
450 await self.coordinator.roku.launch(
451 self.coordinator.play_media_app_id,
454 elif media_type
in {MediaType.URL, MediaType.VIDEO}:
457 for (attr, param)
in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
460 params[
"u"] = media_id
463 await self.coordinator.roku.launch(
464 self.coordinator.play_media_app_id,
468 _LOGGER.error(
"Media type %s is not supported", original_media_type)
471 await self.coordinator.async_request_refresh()
473 @roku_exception_handler()
475 """Select input source."""
477 await self.coordinator.roku.remote(
"home")
482 for app
in self.coordinator.data.apps
483 if source
in (app.name, app.app_id)
488 if appl
is not None and appl.app_id
is not None:
489 await self.coordinator.roku.launch(appl.app_id)
490 await self.coordinator.async_request_refresh()
str format_channel_name(str channel_number, str|None channel_name=None)