1 """Expose Reolink IP camera VODs as media sources."""
3 from __future__
import annotations
8 from reolink_aio.api
import DUAL_LENS_MODELS
9 from reolink_aio.enums
import VodRequestType
25 from .const
import DOMAIN
26 from .host
import ReolinkHost
27 from .util
import ReolinkConfigEntry
29 _LOGGER = logging.getLogger(__name__)
33 """Set up camera media source."""
38 """Return the user friendly name for a stream."""
43 return "Autotrack low res."
44 case
"autotrack_main":
45 return "Autotrack high res."
50 def get_host(hass: HomeAssistant, config_entry_id: str) -> ReolinkHost:
51 """Return the Reolink host from the config entry id."""
52 config_entry: ReolinkConfigEntry |
None = hass.config_entries.async_get_entry(
55 assert config_entry
is not None
56 return config_entry.runtime_data.host
60 """Provide Reolink camera VODs as media sources."""
64 def __init__(self, hass: HomeAssistant) ->
None:
65 """Initialize ReolinkVODMediaSource."""
70 """Resolve media to a url."""
71 identifier = [
"UNKNOWN"]
72 if item.identifier
is not None:
73 identifier = item.identifier.split(
"|", 5)
74 if identifier[0] !=
"FILE":
75 raise Unresolvable(f
"Unknown media item '{item.identifier}'.")
77 _, config_entry_id, channel_str, stream_res, filename = identifier
78 channel =
int(channel_str)
82 def get_vod_type() -> VodRequestType:
83 if filename.endswith(
".mp4"):
84 return VodRequestType.PLAYBACK
86 return VodRequestType.FLV
87 return VodRequestType.RTMP
89 vod_type = get_vod_type()
91 mime_type, url = await host.api.get_vod_source(
92 channel, filename, stream_res, vod_type
94 if _LOGGER.isEnabledFor(logging.DEBUG):
96 if "&user=" in url_log:
97 url_log = f
"{url_log.split('&user=')[0]}&user=xxxxx&password=xxxxx"
98 elif "&token=" in url_log:
99 url_log = f
"{url_log.split('&token=')[0]}&token=xxxxx"
101 "Opening VOD stream from %s: %s", host.api.camera_name(channel), url_log
104 if mime_type ==
"video/mp4":
108 stream.add_provider(
"hls", timeout=3600)
109 stream_url: str = stream.endpoint_url(
"hls")
110 stream_url = stream_url.replace(
"master_",
"")
115 item: MediaSourceItem,
116 ) -> BrowseMediaSource:
118 if not item.identifier:
121 identifier = item.identifier.split(
"|", 7)
122 item_type = identifier[0]
124 if item_type ==
"CAM":
125 _, config_entry_id, channel_str = identifier
127 config_entry_id,
int(channel_str)
129 if item_type ==
"RES":
130 _, config_entry_id, channel_str, stream = identifier
132 config_entry_id,
int(channel_str), stream
134 if item_type ==
"DAY":
153 raise Unresolvable(f
"Unknown media item '{item.identifier}' during browsing.")
156 """Return all available reolink cameras as root browsing structure."""
157 children: list[BrowseMediaSource] = []
159 entity_reg = er.async_get(self.
hasshass)
160 device_reg = dr.async_get(self.
hasshass)
161 for config_entry
in self.
hasshass.config_entries.async_entries(DOMAIN):
162 if config_entry.state != ConfigEntryState.LOADED:
164 channels: list[str] = []
165 host = config_entry.runtime_data.host
166 entities = er.async_entries_for_config_entry(
167 entity_reg, config_entry.entry_id
169 for entity
in entities:
172 or entity.device_id
is None
173 or entity.domain != CAM_DOMAIN
177 device = device_reg.async_get(entity.device_id)
178 ch_id = entity.unique_id.split(
"_")[1]
179 if ch_id
in channels
or device
is None:
181 channels.append(ch_id)
183 ch: int | str = ch_id
185 ch = host.api.channel_for_uid(ch_id)
187 if not host.api.supported(
int(ch),
"replay")
or not host.api.hdd_info:
191 device_name = device.name
192 if device.name_by_user
is not None:
193 device_name = device.name_by_user
195 if host.api.model
in DUAL_LENS_MODELS:
196 device_name = f
"{device_name} lens {ch}"
201 identifier=f
"CAM|{config_entry.entry_id}|{ch}",
202 media_class=MediaClass.CHANNEL,
203 media_content_type=MediaType.PLAYLIST,
205 thumbnail=f
"/api/camera_proxy/{entity.entity_id}",
214 media_class=MediaClass.APP,
215 media_content_type=
"",
223 self, config_entry_id: str, channel: int
224 ) -> BrowseMediaSource:
225 """Allow the user to select the high or low playback resolution, (low loads faster)."""
228 main_enc = await host.api.get_encoding(channel,
"main")
229 if main_enc ==
"h265":
231 "Reolink camera %s uses h265 encoding for main stream,"
232 "playback only possible using sub stream",
233 host.api.camera_name(channel),
239 identifier=f
"RES|{config_entry_id}|{channel}|sub",
240 media_class=MediaClass.CHANNEL,
241 media_content_type=MediaType.PLAYLIST,
242 title=
"Low resolution",
247 if main_enc !=
"h265":
251 identifier=f
"RES|{config_entry_id}|{channel}|main",
252 media_class=MediaClass.CHANNEL,
253 media_content_type=MediaType.PLAYLIST,
254 title=
"High resolution",
260 if host.api.supported(channel,
"autotrack_stream"):
264 identifier=f
"RES|{config_entry_id}|{channel}|autotrack_sub",
265 media_class=MediaClass.CHANNEL,
266 media_content_type=MediaType.PLAYLIST,
267 title=
"Autotrack low resolution",
272 if main_enc !=
"h265":
276 identifier=f
"RES|{config_entry_id}|{channel}|autotrack_main",
277 media_class=MediaClass.CHANNEL,
278 media_content_type=MediaType.PLAYLIST,
279 title=
"Autotrack high resolution",
285 if len(children) == 1:
287 config_entry_id, channel,
"sub"
290 title = host.api.camera_name(channel)
291 if host.api.model
in DUAL_LENS_MODELS:
292 title = f
"{host.api.camera_name(channel)} lens {channel}"
296 identifier=f
"RESs|{config_entry_id}|{channel}",
297 media_class=MediaClass.CHANNEL,
298 media_content_type=MediaType.PLAYLIST,
306 self, config_entry_id: str, channel: int, stream: str
307 ) -> BrowseMediaSource:
308 """Return all days on which recordings are available for a reolink camera."""
312 now = host.api.time()
or await host.api.async_get_time()
313 start = now - dt.timedelta(days=31)
316 if _LOGGER.isEnabledFor(logging.DEBUG):
318 "Requesting recording days of %s from %s to %s",
319 host.api.camera_name(channel),
323 statuses, _ = await host.api.request_vod_files(
324 channel, start, end, status_only=
True, stream=stream
326 children: list[BrowseMediaSource] = [
329 identifier=f
"DAY|{config_entry_id}|{channel}|{stream}|{status.year}|{status.month}|{day}",
330 media_class=MediaClass.DIRECTORY,
331 media_content_type=MediaType.PLAYLIST,
332 title=f
"{status.year}/{status.month}/{day}",
336 for status
in statuses
337 for day
in status.days
340 title = f
"{host.api.camera_name(channel)} {res_name(stream)}"
341 if host.api.model
in DUAL_LENS_MODELS:
342 title = f
"{host.api.camera_name(channel)} lens {channel} {res_name(stream)}"
346 identifier=f
"DAYS|{config_entry_id}|{channel}|{stream}",
347 media_class=MediaClass.CHANNEL,
348 media_content_type=MediaType.PLAYLIST,
357 config_entry_id: str,
363 ) -> BrowseMediaSource:
364 """Return all recording files on a specific day of a Reolink camera."""
367 start = dt.datetime(year, month, day, hour=0, minute=0, second=0)
368 end = dt.datetime(year, month, day, hour=23, minute=59, second=59)
370 children: list[BrowseMediaSource] = []
371 if _LOGGER.isEnabledFor(logging.DEBUG):
373 "Requesting VODs of %s on %s/%s/%s",
374 host.api.camera_name(channel),
379 _, vod_files = await host.api.request_vod_files(
380 channel, start, end, stream=stream
382 for file
in vod_files:
383 file_name = f
"{file.start_time.time()} {file.duration}"
384 if file.triggers != file.triggers.NONE:
385 file_name +=
" " +
" ".join(
386 str(trigger.name).title()
387 for trigger
in file.triggers
388 if trigger != trigger.NONE
394 identifier=f
"FILE|{config_entry_id}|{channel}|{stream}|{file.file_name}",
395 media_class=MediaClass.VIDEO,
396 media_content_type=MediaType.VIDEO,
404 f
"{host.api.camera_name(channel)} {res_name(stream)} {year}/{month}/{day}"
406 if host.api.model
in DUAL_LENS_MODELS:
407 title = f
"{host.api.camera_name(channel)} lens {channel} {res_name(stream)} {year}/{month}/{day}"
411 identifier=f
"FILES|{config_entry_id}|{channel}|{stream}",
412 media_class=MediaClass.CHANNEL,
413 media_content_type=MediaType.PLAYLIST,
Stream create_stream(HomeAssistant hass, str stream_source, Mapping[str, str|bool|float] options, DynamicStreamSettings dynamic_stream_settings, str|None stream_label=None)