Support multiple camera streams in HomeKit (#37968)
* Support multiple camera stream in HomeKit
* Update homeassistant/components/homekit/type_cameras.py
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
* Revert "Update homeassistant/components/homekit/type_cameras.py"
This reverts commit d7624c5bff
.
* Update homeassistant/components/homekit/type_cameras.py
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
* Update homeassistant/components/homekit/type_cameras.py
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
* black
* bump pyhap
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/38212/head
parent
bbc8748e3b
commit
3206f4dc83
|
@ -55,6 +55,7 @@ CONF_SUPPORT_AUDIO = "support_audio"
|
||||||
CONF_VIDEO_CODEC = "video_codec"
|
CONF_VIDEO_CODEC = "video_codec"
|
||||||
CONF_VIDEO_MAP = "video_map"
|
CONF_VIDEO_MAP = "video_map"
|
||||||
CONF_VIDEO_PACKET_SIZE = "video_packet_size"
|
CONF_VIDEO_PACKET_SIZE = "video_packet_size"
|
||||||
|
CONF_STREAM_COUNT = "stream_count"
|
||||||
|
|
||||||
# #### Config Defaults ####
|
# #### Config Defaults ####
|
||||||
DEFAULT_SUPPORT_AUDIO = False
|
DEFAULT_SUPPORT_AUDIO = False
|
||||||
|
@ -72,6 +73,7 @@ DEFAULT_SAFE_MODE = False
|
||||||
DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264
|
DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264
|
||||||
DEFAULT_VIDEO_MAP = "0:v:0"
|
DEFAULT_VIDEO_MAP = "0:v:0"
|
||||||
DEFAULT_VIDEO_PACKET_SIZE = 1316
|
DEFAULT_VIDEO_PACKET_SIZE = 1316
|
||||||
|
DEFAULT_STREAM_COUNT = 3
|
||||||
|
|
||||||
# #### Features ####
|
# #### Features ####
|
||||||
FEATURE_ON_OFF = "on_off"
|
FEATURE_ON_OFF = "on_off"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "HomeKit",
|
"name": "HomeKit",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homekit",
|
"documentation": "https://www.home-assistant.io/integrations/homekit",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"HAP-python==2.9.2",
|
"HAP-python==3.0.0",
|
||||||
"fnvhash==0.1.0",
|
"fnvhash==0.1.0",
|
||||||
"PyQRCode==1.2.1",
|
"PyQRCode==1.2.1",
|
||||||
"base36==0.1.1",
|
"base36==0.1.1",
|
||||||
|
|
|
@ -5,7 +5,6 @@ import logging
|
||||||
|
|
||||||
from haffmpeg.core import HAFFmpeg
|
from haffmpeg.core import HAFFmpeg
|
||||||
from pyhap.camera import (
|
from pyhap.camera import (
|
||||||
STREAMING_STATUS,
|
|
||||||
VIDEO_CODEC_PARAM_LEVEL_TYPES,
|
VIDEO_CODEC_PARAM_LEVEL_TYPES,
|
||||||
VIDEO_CODEC_PARAM_PROFILE_ID_TYPES,
|
VIDEO_CODEC_PARAM_PROFILE_ID_TYPES,
|
||||||
Camera as PyhapCamera,
|
Camera as PyhapCamera,
|
||||||
|
@ -24,7 +23,6 @@ from homeassistant.util import get_local_ip
|
||||||
from .accessories import TYPES, HomeAccessory
|
from .accessories import TYPES, HomeAccessory
|
||||||
from .const import (
|
from .const import (
|
||||||
CHAR_MOTION_DETECTED,
|
CHAR_MOTION_DETECTED,
|
||||||
CHAR_STREAMING_STRATUS,
|
|
||||||
CONF_AUDIO_CODEC,
|
CONF_AUDIO_CODEC,
|
||||||
CONF_AUDIO_MAP,
|
CONF_AUDIO_MAP,
|
||||||
CONF_AUDIO_PACKET_SIZE,
|
CONF_AUDIO_PACKET_SIZE,
|
||||||
|
@ -33,6 +31,7 @@ from .const import (
|
||||||
CONF_MAX_HEIGHT,
|
CONF_MAX_HEIGHT,
|
||||||
CONF_MAX_WIDTH,
|
CONF_MAX_WIDTH,
|
||||||
CONF_STREAM_ADDRESS,
|
CONF_STREAM_ADDRESS,
|
||||||
|
CONF_STREAM_COUNT,
|
||||||
CONF_STREAM_SOURCE,
|
CONF_STREAM_SOURCE,
|
||||||
CONF_SUPPORT_AUDIO,
|
CONF_SUPPORT_AUDIO,
|
||||||
CONF_VIDEO_CODEC,
|
CONF_VIDEO_CODEC,
|
||||||
|
@ -44,11 +43,11 @@ from .const import (
|
||||||
DEFAULT_MAX_FPS,
|
DEFAULT_MAX_FPS,
|
||||||
DEFAULT_MAX_HEIGHT,
|
DEFAULT_MAX_HEIGHT,
|
||||||
DEFAULT_MAX_WIDTH,
|
DEFAULT_MAX_WIDTH,
|
||||||
|
DEFAULT_STREAM_COUNT,
|
||||||
DEFAULT_SUPPORT_AUDIO,
|
DEFAULT_SUPPORT_AUDIO,
|
||||||
DEFAULT_VIDEO_CODEC,
|
DEFAULT_VIDEO_CODEC,
|
||||||
DEFAULT_VIDEO_MAP,
|
DEFAULT_VIDEO_MAP,
|
||||||
DEFAULT_VIDEO_PACKET_SIZE,
|
DEFAULT_VIDEO_PACKET_SIZE,
|
||||||
SERV_CAMERA_RTP_STREAM_MANAGEMENT,
|
|
||||||
SERV_MOTION_SENSOR,
|
SERV_MOTION_SENSOR,
|
||||||
)
|
)
|
||||||
from .img_util import scale_jpeg_camera_image
|
from .img_util import scale_jpeg_camera_image
|
||||||
|
@ -121,6 +120,7 @@ CONFIG_DEFAULTS = {
|
||||||
CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC,
|
CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC,
|
||||||
CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE,
|
CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE,
|
||||||
CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE,
|
CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE,
|
||||||
|
CONF_STREAM_COUNT: DEFAULT_STREAM_COUNT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,7 +131,6 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||||
def __init__(self, hass, driver, name, entity_id, aid, config):
|
def __init__(self, hass, driver, name, entity_id, aid, config):
|
||||||
"""Initialize a Camera accessory object."""
|
"""Initialize a Camera accessory object."""
|
||||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||||
self._cur_session = None
|
|
||||||
for config_key in CONFIG_DEFAULTS:
|
for config_key in CONFIG_DEFAULTS:
|
||||||
if config_key not in config:
|
if config_key not in config:
|
||||||
config[config_key] = CONFIG_DEFAULTS[config_key]
|
config[config_key] = CONFIG_DEFAULTS[config_key]
|
||||||
|
@ -178,6 +177,7 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||||
"audio": audio_options,
|
"audio": audio_options,
|
||||||
"address": stream_address,
|
"address": stream_address,
|
||||||
"srtp": True,
|
"srtp": True,
|
||||||
|
"stream_count": config[CONF_STREAM_COUNT],
|
||||||
}
|
}
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@ -313,51 +313,42 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||||
if not opened:
|
if not opened:
|
||||||
_LOGGER.error("Failed to open ffmpeg stream")
|
_LOGGER.error("Failed to open ffmpeg stream")
|
||||||
return False
|
return False
|
||||||
session_info["stream"] = stream
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"[%s] Started stream process - PID %d",
|
"[%s] Started stream process - PID %d",
|
||||||
session_info["id"],
|
session_info["id"],
|
||||||
stream.process.pid,
|
stream.process.pid,
|
||||||
)
|
)
|
||||||
|
|
||||||
ffmpeg_watcher = async_track_time_interval(
|
session_info["stream"] = stream
|
||||||
self.hass, self._async_ffmpeg_watch, FFMPEG_WATCH_INTERVAL
|
session_info[FFMPEG_PID] = stream.process.pid
|
||||||
|
|
||||||
|
async def watch_session(_):
|
||||||
|
await self._async_ffmpeg_watch(session_info["id"])
|
||||||
|
|
||||||
|
session_info[FFMPEG_WATCHER] = async_track_time_interval(
|
||||||
|
self.hass, watch_session, FFMPEG_WATCH_INTERVAL,
|
||||||
)
|
)
|
||||||
self._cur_session = {
|
|
||||||
FFMPEG_WATCHER: ffmpeg_watcher,
|
|
||||||
FFMPEG_PID: stream.process.pid,
|
|
||||||
SESSION_ID: session_info["id"],
|
|
||||||
}
|
|
||||||
|
|
||||||
return await self._async_ffmpeg_watch(0)
|
return await self._async_ffmpeg_watch(session_info["id"])
|
||||||
|
|
||||||
async def _async_ffmpeg_watch(self, _):
|
async def _async_ffmpeg_watch(self, session_id):
|
||||||
"""Check to make sure ffmpeg is still running and cleanup if not."""
|
"""Check to make sure ffmpeg is still running and cleanup if not."""
|
||||||
ffmpeg_pid = self._cur_session[FFMPEG_PID]
|
ffmpeg_pid = self.sessions[session_id][FFMPEG_PID]
|
||||||
session_id = self._cur_session[SESSION_ID]
|
|
||||||
if pid_is_alive(ffmpeg_pid):
|
if pid_is_alive(ffmpeg_pid):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_LOGGER.warning("Streaming process ended unexpectedly - PID %d", ffmpeg_pid)
|
_LOGGER.warning("Streaming process ended unexpectedly - PID %d", ffmpeg_pid)
|
||||||
self._async_stop_ffmpeg_watch()
|
self._async_stop_ffmpeg_watch(session_id)
|
||||||
self._async_set_streaming_available(session_id)
|
self.set_streaming_available(self.sessions[session_id]["stream_idx"])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_stop_ffmpeg_watch(self):
|
def _async_stop_ffmpeg_watch(self, session_id):
|
||||||
"""Cleanup a streaming session after stopping."""
|
"""Cleanup a streaming session after stopping."""
|
||||||
if not self._cur_session:
|
if FFMPEG_WATCHER not in self.sessions[session_id]:
|
||||||
return
|
return
|
||||||
self._cur_session[FFMPEG_WATCHER]()
|
self.sessions[session_id].pop(FFMPEG_WATCHER)()
|
||||||
self._cur_session = None
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_set_streaming_available(self, session_id):
|
|
||||||
"""Free the session so they can start another."""
|
|
||||||
self.streaming_status = STREAMING_STATUS["AVAILABLE"]
|
|
||||||
self.get_service(SERV_CAMERA_RTP_STREAM_MANAGEMENT).get_characteristic(
|
|
||||||
CHAR_STREAMING_STRATUS
|
|
||||||
).notify()
|
|
||||||
|
|
||||||
async def stop_stream(self, session_info):
|
async def stop_stream(self, session_info):
|
||||||
"""Stop the stream for the given ``session_id``."""
|
"""Stop the stream for the given ``session_id``."""
|
||||||
|
@ -367,7 +358,7 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||||
_LOGGER.debug("No stream for session ID %s", session_id)
|
_LOGGER.debug("No stream for session ID %s", session_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._async_stop_ffmpeg_watch()
|
self._async_stop_ffmpeg_watch(session_id)
|
||||||
|
|
||||||
if not pid_is_alive(stream.process.pid):
|
if not pid_is_alive(stream.process.pid):
|
||||||
_LOGGER.info("[%s] Stream already stopped", session_id)
|
_LOGGER.info("[%s] Stream already stopped", session_id)
|
||||||
|
|
|
@ -41,6 +41,7 @@ from .const import (
|
||||||
CONF_MAX_HEIGHT,
|
CONF_MAX_HEIGHT,
|
||||||
CONF_MAX_WIDTH,
|
CONF_MAX_WIDTH,
|
||||||
CONF_STREAM_ADDRESS,
|
CONF_STREAM_ADDRESS,
|
||||||
|
CONF_STREAM_COUNT,
|
||||||
CONF_STREAM_SOURCE,
|
CONF_STREAM_SOURCE,
|
||||||
CONF_SUPPORT_AUDIO,
|
CONF_SUPPORT_AUDIO,
|
||||||
CONF_VIDEO_CODEC,
|
CONF_VIDEO_CODEC,
|
||||||
|
@ -53,6 +54,7 @@ from .const import (
|
||||||
DEFAULT_MAX_FPS,
|
DEFAULT_MAX_FPS,
|
||||||
DEFAULT_MAX_HEIGHT,
|
DEFAULT_MAX_HEIGHT,
|
||||||
DEFAULT_MAX_WIDTH,
|
DEFAULT_MAX_WIDTH,
|
||||||
|
DEFAULT_STREAM_COUNT,
|
||||||
DEFAULT_SUPPORT_AUDIO,
|
DEFAULT_SUPPORT_AUDIO,
|
||||||
DEFAULT_VIDEO_CODEC,
|
DEFAULT_VIDEO_CODEC,
|
||||||
DEFAULT_VIDEO_MAP,
|
DEFAULT_VIDEO_MAP,
|
||||||
|
@ -112,6 +114,9 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||||
vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int,
|
vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int,
|
||||||
vol.Optional(CONF_AUDIO_MAP, default=DEFAULT_AUDIO_MAP): cv.string,
|
vol.Optional(CONF_AUDIO_MAP, default=DEFAULT_AUDIO_MAP): cv.string,
|
||||||
vol.Optional(CONF_VIDEO_MAP, default=DEFAULT_VIDEO_MAP): cv.string,
|
vol.Optional(CONF_VIDEO_MAP, default=DEFAULT_VIDEO_MAP): cv.string,
|
||||||
|
vol.Optional(CONF_STREAM_COUNT, default=DEFAULT_STREAM_COUNT): vol.All(
|
||||||
|
vol.Coerce(int), vol.Range(min=1, max=10)
|
||||||
|
),
|
||||||
vol.Optional(CONF_VIDEO_CODEC, default=DEFAULT_VIDEO_CODEC): vol.In(
|
vol.Optional(CONF_VIDEO_CODEC, default=DEFAULT_VIDEO_CODEC): vol.In(
|
||||||
VALID_VIDEO_CODECS
|
VALID_VIDEO_CODECS
|
||||||
),
|
),
|
||||||
|
|
|
@ -18,7 +18,7 @@ Adafruit-SHT31==1.0.2
|
||||||
# Adafruit_BBIO==1.1.1
|
# Adafruit_BBIO==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
HAP-python==2.9.2
|
HAP-python==3.0.0
|
||||||
|
|
||||||
# homeassistant.components.mastodon
|
# homeassistant.components.mastodon
|
||||||
Mastodon.py==1.5.1
|
Mastodon.py==1.5.1
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
-r requirements_test.txt
|
-r requirements_test.txt
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
HAP-python==2.9.2
|
HAP-python==3.0.0
|
||||||
|
|
||||||
# homeassistant.components.plugwise
|
# homeassistant.components.plugwise
|
||||||
Plugwise_Smile==1.1.0
|
Plugwise_Smile==1.1.0
|
||||||
|
|
Loading…
Reference in New Issue