Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for FFmpeg."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import re
7 
8 from haffmpeg.core import HAFFmpeg
9 from haffmpeg.tools import IMAGE_JPEG, FFVersion, ImageFrame
10 from propcache import cached_property
11 import voluptuous as vol
12 
13 from homeassistant.const import (
14  ATTR_ENTITY_ID,
15  CONTENT_TYPE_MULTIPART,
16  EVENT_HOMEASSISTANT_START,
17  EVENT_HOMEASSISTANT_STOP,
18 )
19 from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
22  async_dispatcher_connect,
23  async_dispatcher_send,
24 )
25 from homeassistant.helpers.entity import Entity
26 from homeassistant.helpers.system_info import is_official_image
27 from homeassistant.helpers.typing import ConfigType
28 from homeassistant.loader import bind_hass
29 from homeassistant.util.signal_type import SignalType
30 
31 DOMAIN = "ffmpeg"
32 
33 SERVICE_START = "start"
34 SERVICE_STOP = "stop"
35 SERVICE_RESTART = "restart"
36 
37 SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
38 SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
39 SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
40 
41 DATA_FFMPEG = "ffmpeg"
42 
43 CONF_INITIAL_STATE = "initial_state"
44 CONF_INPUT = "input"
45 CONF_FFMPEG_BIN = "ffmpeg_bin"
46 CONF_EXTRA_ARGUMENTS = "extra_arguments"
47 CONF_OUTPUT = "output"
48 
49 DEFAULT_BINARY = "ffmpeg"
50 
51 # Currently we only care if the version is < 3
52 # because we use a different content-type
53 # It is only important to update this version if the
54 # content-type changes again in the future
55 OFFICIAL_IMAGE_VERSION = "6.0"
56 
57 CONFIG_SCHEMA = vol.Schema(
58  {
59  DOMAIN: vol.Schema(
60  {vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string}
61  )
62  },
63  extra=vol.ALLOW_EXTRA,
64 )
65 
66 SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
67 
68 
69 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
70  """Set up the FFmpeg component."""
71  conf = config.get(DOMAIN, {})
72 
73  manager = FFmpegManager(hass, conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY))
74 
75  await manager.async_get_version()
76 
77  # Register service
78  async def async_service_handle(service: ServiceCall) -> None:
79  """Handle service ffmpeg process."""
80  entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
81 
82  if service.service == SERVICE_START:
83  async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
84  elif service.service == SERVICE_STOP:
85  async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
86  else:
87  async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
88 
89  hass.services.async_register(
90  DOMAIN, SERVICE_START, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
91  )
92 
93  hass.services.async_register(
94  DOMAIN, SERVICE_STOP, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
95  )
96 
97  hass.services.async_register(
98  DOMAIN, SERVICE_RESTART, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
99  )
100 
101  hass.data[DATA_FFMPEG] = manager
102  return True
103 
104 
105 @bind_hass
106 def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager:
107  """Return the FFmpegManager."""
108  if DATA_FFMPEG not in hass.data:
109  raise ValueError("ffmpeg component not initialized")
110  return hass.data[DATA_FFMPEG]
111 
112 
113 @bind_hass
114 async def async_get_image(
115  hass: HomeAssistant,
116  input_source: str,
117  output_format: str = IMAGE_JPEG,
118  extra_cmd: str | None = None,
119  width: int | None = None,
120  height: int | None = None,
121 ) -> bytes | None:
122  """Get an image from a frame of an RTSP stream."""
123  manager = hass.data[DATA_FFMPEG]
124  ffmpeg = ImageFrame(manager.binary)
125 
126  if width and height and (extra_cmd is None or "-s" not in extra_cmd):
127  size_cmd = f"-s {width}x{height}"
128  if extra_cmd is None:
129  extra_cmd = size_cmd
130  else:
131  extra_cmd += " " + size_cmd
132 
133  return await asyncio.shield(
134  ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd)
135  )
136 
137 
139  """Helper for ha-ffmpeg."""
140 
141  def __init__(self, hass: HomeAssistant, ffmpeg_bin: str) -> None:
142  """Initialize helper."""
143  self.hasshass = hass
144  self._cache_cache = {} # type: ignore[var-annotated]
145  self._bin_bin = ffmpeg_bin
146  self._version_version: str | None = None
147  self._major_version_major_version: int | None = None
148 
149  @cached_property
150  def binary(self) -> str:
151  """Return ffmpeg binary from config."""
152  return self._bin_bin
153 
154  async def async_get_version(self) -> tuple[str | None, int | None]:
155  """Return ffmpeg version."""
156  if self._version_version is None:
157  if is_official_image():
158  self._version_version = OFFICIAL_IMAGE_VERSION
159  self._major_version_major_version = int(self._version_version.split(".")[0])
160  elif (
161  (version := await FFVersion(self._bin_bin).get_version())
162  and (result := re.search(r"(\d+)\.", version))
163  and (major_version := int(result.group(1)))
164  ):
165  self._version_version = version
166  self._major_version_major_version = major_version
167 
168  return self._version_version, self._major_version_major_version
169 
170  @cached_property
171  def ffmpeg_stream_content_type(self) -> str:
172  """Return HTTP content type for ffmpeg stream."""
173  if self._major_version_major_version is not None and self._major_version_major_version > 3:
174  return CONTENT_TYPE_MULTIPART.format("ffmpeg")
175 
176  return CONTENT_TYPE_MULTIPART.format("ffserver")
177 
178 
179 class FFmpegBase[_HAFFmpegT: HAFFmpeg](Entity): # pylint: disable=hass-enforce-class-module
180  """Interface object for FFmpeg."""
181 
182  _attr_should_poll = False
183 
184  def __init__(self, ffmpeg: _HAFFmpegT, initial_state: bool = True) -> None:
185  """Initialize ffmpeg base object."""
186  self.ffmpeg = ffmpeg
187  self.initial_state = initial_state
188 
189  async def async_added_to_hass(self) -> None:
190  """Register dispatcher & events.
191 
192  This method is a coroutine.
193  """
194  self.async_on_remove(
196  self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg
197  )
198  )
199  self.async_on_remove(
201  self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg
202  )
203  )
204  self.async_on_remove(
206  self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg
207  )
208  )
209 
210  # register start/stop
211  self._async_register_events()
212 
213  @property
214  def available(self) -> bool:
215  """Return True if entity is available."""
216  return self.ffmpeg.is_running
217 
218  async def _async_start_ffmpeg(self, entity_ids: list[str] | None) -> None:
219  """Start a FFmpeg process.
220 
221  This method is a coroutine.
222  """
223  raise NotImplementedError
224 
225  async def _async_stop_ffmpeg(self, entity_ids: list[str] | None) -> None:
226  """Stop a FFmpeg process.
227 
228  This method is a coroutine.
229  """
230  if entity_ids is None or self.entity_id in entity_ids:
231  await self.ffmpeg.close()
232 
233  async def _async_restart_ffmpeg(self, entity_ids: list[str] | None) -> None:
234  """Stop a FFmpeg process.
235 
236  This method is a coroutine.
237  """
238  if entity_ids is None or self.entity_id in entity_ids:
239  await self._async_stop_ffmpeg(None)
240  await self._async_start_ffmpeg(None)
241 
242  @callback
243  def _async_register_events(self) -> None:
244  """Register a FFmpeg process/device."""
245 
246  async def async_shutdown_handle(event: Event) -> None:
247  """Stop FFmpeg process."""
248  await self._async_stop_ffmpeg(None)
249 
250  self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown_handle)
251 
252  # start on startup
253  if not self.initial_state:
254  return
255 
256  async def async_start_handle(event: Event) -> None:
257  """Start FFmpeg process."""
258  await self._async_start_ffmpeg(None)
259  self.async_write_ha_state()
260 
261  self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_handle)
None __init__(self, HomeAssistant hass, str ffmpeg_bin)
Definition: __init__.py:141
tuple[str|None, int|None] async_get_version(self)
Definition: __init__.py:154
None __init__(self, _HAFFmpegT ffmpeg, bool initial_state=True)
Definition: __init__.py:184
None _async_restart_ffmpeg(self, list[str]|None entity_ids)
Definition: __init__.py:233
None _async_stop_ffmpeg(self, list[str]|None entity_ids)
Definition: __init__.py:225
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:69
None _async_start_ffmpeg(self, list[str]|None entity_ids)
Definition: __init__.py:218
FFmpegManager get_ffmpeg_manager(HomeAssistant hass)
Definition: __init__.py:106
bytes|None async_get_image(HomeAssistant hass, str input_source, str output_format=IMAGE_JPEG, str|None extra_cmd=None, int|None width=None, int|None height=None)
Definition: __init__.py:121
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193