From e4a65f307768d71791f89c274a6f9c977d8f1339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Wed, 13 Jul 2022 23:05:27 +0000 Subject: [PATCH] Added camera functionality --- custom_components/browser_mod/__init__.py | 15 +-- .../browser_mod/binary_sensor.py | 10 +- custom_components/browser_mod/browser_mod.js | 84 ++++++++++++-- .../browser_mod/browser_mod_panel.js | 23 +++- custom_components/browser_mod/camera.py | 47 ++++---- custom_components/browser_mod/connection.py | 30 +++-- custom_components/browser_mod/device.py | 63 ++++++++--- custom_components/browser_mod/helpers.py | 48 +------- custom_components/browser_mod/light.py | 14 ++- custom_components/browser_mod/media_player.py | 22 ++-- custom_components/browser_mod/sensor.py | 31 ++++-- custom_components/browser_mod/store.py | 26 +++-- js/config_panel/main.ts | 21 +++- js/plugin/camera.ts | 103 ++++++++++-------- js/plugin/connection.ts | 31 ++++-- js/plugin/main.ts | 6 +- package-lock.json | 2 +- package.json | 2 +- 18 files changed, 355 insertions(+), 223 deletions(-) diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py index 74ea48e..420f520 100644 --- a/custom_components/browser_mod/__init__.py +++ b/custom_components/browser_mod/__init__.py @@ -3,14 +3,7 @@ import logging from .store import BrowserModStore from .mod_view import async_setup_view from .connection import async_setup_connection -from .const import ( - DOMAIN, - DATA_DEVICES, - DATA_ADDERS, - DATA_STORE -) - -from .coordinator import Coordinator +from .const import DOMAIN, DATA_DEVICES, DATA_ADDERS, DATA_STORE _LOGGER = logging.getLogger(__name__) @@ -35,13 +28,9 @@ async def async_setup_entry(hass, config_entry): await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "light") await hass.config_entries.async_forward_entry_setup(config_entry, "media_player") + await hass.config_entries.async_forward_entry_setup(config_entry, "camera") await async_setup_connection(hass) await async_setup_view(hass) return True - for component in COMPONENTS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) - return True diff --git a/custom_components/browser_mod/binary_sensor.py b/custom_components/browser_mod/binary_sensor.py index 537e87b..692412d 100644 --- a/custom_components/browser_mod/binary_sensor.py +++ b/custom_components/browser_mod/binary_sensor.py @@ -1,18 +1,20 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from .const import DOMAIN, DATA_ADDERS -from .helpers import BrowserModEntity2 +from .helpers import BrowserModEntity -async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): +async def async_setup_platform( + hass, config_entry, async_add_entities, discoveryInfo=None +): hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities + async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserBinarySensor(BrowserModEntity2, BinarySensorEntity): - +class BrowserBinarySensor(BrowserModEntity, BinarySensorEntity): def __init__(self, coordinator, deviceID, parameter, name): super().__init__(coordinator, deviceID, name) self.parameter = parameter diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index eeb66d5..166fba0 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -732,19 +732,28 @@ const ConnectionMixin = (SuperClass) => { } })(); } + async _reregister(newData = {}) { + await this.connection.sendMessage({ + type: "browser_mod/reregister", + deviceID: this.deviceID, + data: Object.assign(Object.assign({}, this.devices[this.deviceID]), newData), + }); + } get meta() { if (!this.registered) return null; return this.devices[this.deviceID].meta; } set meta(value) { - (async () => { - await this.connection.sendMessage({ - type: "browser_mod/reregister", - deviceID: this.deviceID, - data: Object.assign(Object.assign({}, this.devices[this.deviceID]), { meta: value }) - }); - })(); + this._reregister({ meta: value }); + } + get cameraEnabled() { + if (!this.registered) + return null; + return this.devices[this.deviceID].camera; + } + set cameraEnabled(value) { + this._reregister({ camera: value }); } sendUpdate(data) { if (!this.connected || !this.registered) @@ -923,6 +932,65 @@ const MediaPlayerMixin = (SuperClass) => { }; }; +const CameraMixin = (SuperClass) => { + return class CameraMixinClass extends SuperClass { + constructor() { + super(); + this._framerate = 2; + window.addEventListener("pointerdown", () => { + this._setup_camera(); + }, { once: true }); + } + async _setup_camera() { + if (this._video) + return; + await this.connectionPromise; + if (!this.cameraEnabled) + return; + const video = (this._video = document.createElement("video")); + video.autoplay = true; + video.playsInline = true; + video.style.display = "none"; + const canvas = (this._canvas = document.createElement("canvas")); + canvas.style.display = "none"; + document.body.appendChild(video); + document.body.appendChild(canvas); + if (!navigator.mediaDevices) + return; + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: false, + }); + video.srcObject = stream; + video.play(); + this.update_camera(); + } + async update_camera() { + var _a; + if (!this.cameraEnabled) { + const stream = (_a = this._video) === null || _a === void 0 ? void 0 : _a.srcObject; + if (stream) { + stream.getTracks().forEach((t) => t.stop()); + this._video.scrObject = undefined; + } + return; + } + const video = this._video; + const width = video.videoWidth; + const height = video.videoHeight; + this._canvas.width = width; + this._canvas.height = height; + const context = this._canvas.getContext("2d"); + context.drawImage(video, 0, 0, width, height); + this.sendUpdate({ + camera: this._canvas.toDataURL("image/jpeg"), + }); + const interval = Math.round(1000 / this._framerate); + setTimeout(() => this.update_camera(), interval); + } + }; +}; + var name = "browser_mod"; var version = "2.0.0b0"; var description = ""; @@ -970,7 +1038,7 @@ var pjson = { // FullyKioskMixin, // BrowserModMediaPlayerMixin, // ]) { -class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) { +class BrowserMod extends CameraMixin(MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget)))) { constructor() { super(); this.entity_id = deviceID.replace("-", "_"); diff --git a/custom_components/browser_mod/browser_mod_panel.js b/custom_components/browser_mod/browser_mod_panel.js index 81ad178..1a9e7df 100644 --- a/custom_components/browser_mod/browser_mod_panel.js +++ b/custom_components/browser_mod/browser_mod_panel.js @@ -90,6 +90,9 @@ loadDevTools().then(() => { changeDeviceID(ev) { window.browser_mod.deviceID = ev.target.value; } + toggleCameraEnabled() { + window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled; + } unregister_device(ev) { const deviceID = ev.currentTarget.deviceID; if (deviceID === window.browser_mod.deviceID) @@ -104,7 +107,7 @@ loadDevTools().then(() => { window.browser_mod.addEventListener("browser-mod-config-update", () => this.requestUpdate()); } render() { - var _a, _b, _c; + var _a, _b, _c, _d; return $ ` @@ -137,7 +140,13 @@ loadDevTools().then(() => { > `} -
Browser-mod not connected.
+
+

Settings that apply to this browser.

+

+ It is strongly recommended to refresh your browser window + after any change to those settings. +

+
Enable @@ -168,7 +177,10 @@ loadDevTools().then(() => { >Get camera input from this device (hardware dependent) - +
@@ -184,7 +196,10 @@ loadDevTools().then(() => { .datetime=${window.browser_mod.devices[d].last_seen} > - + diff --git a/custom_components/browser_mod/camera.py b/custom_components/browser_mod/camera.py index 43a57be..9028e8f 100644 --- a/custom_components/browser_mod/camera.py +++ b/custom_components/browser_mod/camera.py @@ -1,45 +1,40 @@ -from datetime import datetime import base64 from homeassistant.components.camera import Camera -from .helpers import setup_platform, BrowserModEntity +from .helpers import BrowserModEntity +from .const import DOMAIN, DATA_ADDERS import logging -PLATFORM = "camera" - LOGGER = logging.Logger(__name__) -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModCamera) +async def async_setup_platform( + hass, config_entry, async_add_entities, discoveryInfo=None +): + hass.data[DOMAIN][DATA_ADDERS]["camera"] = async_add_entities async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModCamera(Camera, BrowserModEntity): - domain = PLATFORM - - def __init__(self, hass, connection, deviceID, alias=None): +class BrowserModCamera(BrowserModEntity, Camera): + def __init__(self, coordinator, deviceID): + BrowserModEntity.__init__(self, coordinator, deviceID, None) Camera.__init__(self) - BrowserModEntity.__init__(self, hass, connection, deviceID, alias) - self.last_seen = None - - def updated(self): - if self.last_seen is None or (datetime.now() - self.last_seen).seconds > 15: - self.last_seen = datetime.now() - self.schedule_update_ha_state() - - def camera_image(self, width=None, height=None): - return base64.b64decode(self.data.split(",")[-1]) @property - def extra_state_attributes(self): - return { - "type": "browser_mod", - "deviceID": self.deviceID, - "last_seen": self.last_seen, - } + def unique_id(self): + return f"{self.deviceID}-camera" + + @property + def entity_registry_visible_default(self): + return True + + def camera_image(self, width=None, height=None): + if "camera" not in self._data: + LOGGER.error(self._data) + return None + return base64.b64decode(self._data["camera"].split(",")[-1]) diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index 3260834..d125ed9 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -15,7 +15,7 @@ from .const import WS_CONNECT, WS_REGISTER, WS_UNREGISTER, WS_REREGISTER, WS_UPD from .helpers import get_devices, create_entity, get_config, is_setup_complete from .coordinator import Coordinator -from .device import getDevice +from .device import getDevice, deleteDevice _LOGGER = logging.getLogger(__name__) @@ -40,6 +40,7 @@ async def async_setup_connection(hass): if store.get_device(deviceID).enabled: dev = getDevice(hass, deviceID) + dev.update_settings(hass, store.get_device(deviceID).asdict()) dev.connection = (connection, msg["id"]) await store.set_device(deviceID, last_seen=datetime.now( @@ -75,12 +76,8 @@ async def async_setup_connection(hass): async def handle_unregister(hass, connection, msg): deviceID = msg["deviceID"] store = hass.data[DOMAIN]["store"] - devices = hass.data[DOMAIN]["devices"] - - if deviceID in devices: - devices[deviceID].delete(hass) - del devices[deviceID] + deleteDevice(hass, deviceID) await store.delete_device(deviceID) connection.send_result(msg["id"]) @@ -96,28 +93,29 @@ async def async_setup_connection(hass): async def handle_reregister(hass, connection, msg): deviceID = msg["deviceID"] store = hass.data[DOMAIN]["store"] - devices = hass.data[DOMAIN]["devices"] data = msg["data"] del data["last_seen"] - device = {} + deviceSettings = {} + if "deviceID" in data: newDeviceID = data["deviceID"] del data["deviceID"] - oldDevice = store.get_device(deviceID) - if oldDevice: - device = oldDevice.asdict() + oldDeviceSettings = store.get_device(deviceID) + if oldDeviceSettings: + deviceSettings = oldDeviceSettings.asdict() await store.delete_device(deviceID) - if deviceID in devices: - devices[deviceID].delete(hass) - del devices[deviceID] + deleteDevice(hass, deviceID) deviceID = newDeviceID - device.update(data) - await store.set_device(deviceID, **device) + if (dev := getDevice(hass, deviceID, create=False)) is not None: + dev.update_settings(hass, data) + + deviceSettings.update(data) + await store.set_device(deviceID, **deviceSettings) @websocket_api.websocket_command( diff --git a/custom_components/browser_mod/device.py b/custom_components/browser_mod/device.py index a9fbcce..7a32e43 100644 --- a/custom_components/browser_mod/device.py +++ b/custom_components/browser_mod/device.py @@ -9,29 +9,32 @@ from .sensor import BrowserSensor from .light import BrowserModLight from .binary_sensor import BrowserBinarySensor from .media_player import BrowserModPlayer +from .camera import BrowserModCamera _LOGGER = logging.getLogger(__name__) -BROWSER_SENSORS = { - "battery_level", () -} - - class BrowserModDevice: - """ A Browser_mod device. """ + """A Browser_mod device.""" def __init__(self, hass, deviceID): """ """ self.deviceID = deviceID self.coordinator = Coordinator(hass, deviceID) self.entities = [] + self.camera_entity = None self.data = {} self.setup_sensors(hass) self.connection = None + def update_settings(self, hass, settings): + if settings.get("camera", False) and self.camera_entity is None: + self.add_camera(hass) + elif self.camera_entity and not settings.get("camera", False): + self.remove_camera(hass) + def setup_sensors(self, hass): - """ Create all entities associated with the device. """ + """Create all entities associated with the device.""" coordinator = self.coordinator deviceID = self.deviceID @@ -70,9 +73,30 @@ class BrowserModDevice: adder(new) self.entities += new + def add_camera(self, hass): + if self.camera_entity is not None: + return + coordinator = self.coordinator + deviceID = self.deviceID + adder = hass.data[DOMAIN][DATA_ADDERS]["camera"] + self.camera_entity = BrowserModCamera(coordinator, deviceID) + adder([self.camera_entity]) + self.entities.append(self.camera_entity) + pass + + def remove_camera(self, hass): + if self.camera_entity is None: + return + er = entity_registry.async_get(hass) + er.async_remove(self.camera_entity.entity_id) + self.entities.remove(self.camera_entity) + self.camera_entity = None + pass + def send(self, command, **kwargs): - """ Send a command to this device. """ - if self.connection is None: return + """Send a command to this device.""" + if self.connection is None: + return connection, cid = self.connection @@ -87,22 +111,35 @@ class BrowserModDevice: ) def delete(self, hass): - """ Delete device and associated entities. """ + """Delete device and associated entities.""" dr = device_registry.async_get(hass) er = entity_registry.async_get(hass) for e in self.entities: er.async_remove(e.entity_id) + self.entities = [] + self.camera_entity = None + device = dr.async_get_device({(DOMAIN, self.deviceID)}) dr.async_remove_device(device.id) -def getDevice(hass, deviceID): - """ Get or create device by deviceID. """ +def getDevice(hass, deviceID, *, create=True): + """Get or create device by deviceID.""" devices = hass.data[DOMAIN]["devices"] if deviceID in devices: return devices[deviceID] + if not create: + return None + devices[deviceID] = BrowserModDevice(hass, deviceID) - return devices[deviceID] \ No newline at end of file + return devices[deviceID] + + +def deleteDevice(hass, deviceID): + devices = hass.data[DOMAIN]["devices"] + if deviceID in devices: + devices["deviceID"].delete() + del devices["deviceID"] diff --git a/custom_components/browser_mod/helpers.py b/custom_components/browser_mod/helpers.py index e60a2a1..9a51ded 100644 --- a/custom_components/browser_mod/helpers.py +++ b/custom_components/browser_mod/helpers.py @@ -72,8 +72,8 @@ def setup_platform(hass, config, async_add_devices, platform, cls): def is_setup_complete(hass): return hass.data[DOMAIN][DATA_SETUP_COMPLETE] -class BrowserModEntity2(CoordinatorEntity): +class BrowserModEntity(CoordinatorEntity): def __init__(self, coordinator, deviceID, name): super().__init__(coordinator) self.deviceID = deviceID @@ -101,55 +101,15 @@ class BrowserModEntity2(CoordinatorEntity): @property def name(self): return self._name + @property def has_entity_name(self): return True + @property def entity_registry_visible_default(self): return False + @property def unique_id(self): return f"{self.deviceID}-{self._name.replace(' ','_')}" - - -class BrowserModEntity(Entity): - def __init__(self, hass, connection, deviceID, alias=None): - self.hass = hass - self.connection = connection - self.deviceID = deviceID - self._data = {} - self._alias = alias - prefix = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_PREFIX, "") - self.entity_id = async_generate_entity_id( - self.domain + ".{}", alias or f"{prefix}{deviceID}", hass=hass - ) - - def updated(self): - pass - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.deviceID)}, - "name": self._alias or self.deviceID, - } - - @property - def unique_id(self): - return f"{self.domain}-{self.deviceID}" - - @property - def data(self): - return self._data - - @data.setter - def data(self, data): - self._data = data - self.updated() - - @property - def device_id(self): - return self.deviceID - - def send(self, command, **kwargs): - self.connection.send(command, **kwargs) diff --git a/custom_components/browser_mod/light.py b/custom_components/browser_mod/light.py index f86bcab..67e946a 100644 --- a/custom_components/browser_mod/light.py +++ b/custom_components/browser_mod/light.py @@ -1,20 +1,23 @@ from homeassistant.components.light import LightEntity, ColorMode -from .helpers import setup_platform, BrowserModEntity2 +from .helpers import setup_platform, BrowserModEntity from .const import DOMAIN, DATA_ADDERS -async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): +async def async_setup_platform( + hass, config_entry, async_add_entities, discoveryInfo=None +): hass.data[DOMAIN][DATA_ADDERS]["light"] = async_add_entities + async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModLight(BrowserModEntity2, LightEntity): - +class BrowserModLight(BrowserModEntity, LightEntity): def __init__(self, coordinator, deviceID, device): - super().__init__(coordinator, deviceID, "Screen") + BrowserModEntity.__init__(self, coordinator, deviceID, "Screen") + LightEntity.__init__(self) self.device = device @property @@ -28,6 +31,7 @@ class BrowserModLight(BrowserModEntity2, LightEntity): @property def supported_color_modes(self): return {ColorMode.BRIGHTNESS} + @property def color_mode(self): return ColorMode.BRIGHTNESS diff --git a/custom_components/browser_mod/media_player.py b/custom_components/browser_mod/media_player.py index 7d9695d..66bd5ae 100644 --- a/custom_components/browser_mod/media_player.py +++ b/custom_components/browser_mod/media_player.py @@ -25,27 +25,34 @@ from homeassistant.const import ( STATE_UNKNOWN, ) -from .helpers import BrowserModEntity2 +from .helpers import BrowserModEntity from .const import DOMAIN, DATA_ADDERS -async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): +async def async_setup_platform( + hass, config_entry, async_add_entities, discoveryInfo=None +): hass.data[DOMAIN][DATA_ADDERS]["media_player"] = async_add_entities + async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity): - +class BrowserModPlayer(BrowserModEntity, MediaPlayerEntity): def __init__(self, coordinator, deviceID, device): - super().__init__(coordinator, deviceID, None) + BrowserModEntity.__init__(self, coordinator, deviceID, None) + MediaPlayerEntity.__init__(self) self.device = device @property def unique_id(self): return f"{self.deviceID}-player" + @property + def entity_registry_visible_default(self): + return True + @property def state(self): state = self._data.get("player", {}).get("state") @@ -76,7 +83,6 @@ class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity): def is_volume_muted(self): return self._data.get("player", {}).get("muted", False) - def set_volume_level(self, volume): self.device.send("player-set-volume", volume_level=volume) @@ -86,7 +92,9 @@ class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id, self.entity_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): media_id = async_process_play_media_url(self.hass, media_id) diff --git a/custom_components/browser_mod/sensor.py b/custom_components/browser_mod/sensor.py index 8df9f99..21f6012 100644 --- a/custom_components/browser_mod/sensor.py +++ b/custom_components/browser_mod/sensor.py @@ -1,22 +1,29 @@ from homeassistant.components.sensor import SensorEntity from .const import DOMAIN, DATA_ADDERS -from .helpers import BrowserModEntity2 +from .helpers import BrowserModEntity -async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): +async def async_setup_platform( + hass, config_entry, async_add_entities, discoveryInfo=None +): hass.data[DOMAIN][DATA_ADDERS]["sensor"] = async_add_entities + async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserSensor(BrowserModEntity2, SensorEntity): - def __init__(self, coordinator, deviceID, parameter, - name, - unit_of_measurement = None, - device_class = None, - ): +class BrowserSensor(BrowserModEntity, SensorEntity): + def __init__( + self, + coordinator, + deviceID, + parameter, + name, + unit_of_measurement=None, + device_class=None, + ): super().__init__(coordinator, deviceID, name) self.parameter = parameter self._device_class = device_class @@ -28,9 +35,11 @@ class BrowserSensor(BrowserModEntity2, SensorEntity): data = data.get("browser", {}) data = data.get(self.parameter, None) return data + @property def device_class(self): return self._device_class + @property def native_unit_of_measurement(self): return self._unit_of_measurement @@ -41,8 +50,10 @@ class BrowserSensor(BrowserModEntity2, SensorEntity): if self.parameter == "currentUser": retval["userData"] = self._data.get("browser", {}).get("userData") if self.parameter == "path": - retval["pathSegments"] = self._data.get("browser", {}).get("path", "").split("/") + retval["pathSegments"] = ( + self._data.get("browser", {}).get("path", "").split("/") + ) if self.parameter == "userAgent": retval["userAgent"] = self._data.get("browser", {}).get("userAgent") - return retval \ No newline at end of file + return retval diff --git a/custom_components/browser_mod/store.py b/custom_components/browser_mod/store.py index cb5f8cc..d5dd0c3 100644 --- a/custom_components/browser_mod/store.py +++ b/custom_components/browser_mod/store.py @@ -26,22 +26,28 @@ class DeviceStoreData: def asdict(self): return attr.asdict(self) + @attr.s class ConfigStoreData: - devices = attr.ib(type=dict[str: DeviceStoreData], factory=dict) + devices = attr.ib(type=dict[str:DeviceStoreData], factory=dict) version = attr.ib(type=str, default="2.0") @classmethod - def from_dict(cls, data): - devices = {k: DeviceStoreData.from_dict(v) for k,v in data["devices"].items()} - return cls(**(data | { - "devices": devices, - } - )) + def from_dict(cls, data={}): + devices = {k: DeviceStoreData.from_dict(v) for k, v in data["devices"].items()} + return cls( + **( + data + | { + "devices": devices, + } + ) + ) def asdict(self): return attr.asdict(self) + class BrowserModStore: def __init__(self, hass): self.store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -55,10 +61,12 @@ class BrowserModStore: self.dirty = False async def load(self): - self.data = ConfigStoreData.from_dict(await self.store.async_load()) + stored = await self.store.async_load() + if stored: + self.data = ConfigStoreData.from_dict(stored) if self.data is None: self.data = ConfigStoreData() - self.save() + await self.save() self.dirty = False async def updated(self): diff --git a/js/config_panel/main.ts b/js/config_panel/main.ts index 21566c6..6887d48 100644 --- a/js/config_panel/main.ts +++ b/js/config_panel/main.ts @@ -17,6 +17,9 @@ loadDevTools().then(() => { changeDeviceID(ev) { window.browser_mod.deviceID = ev.target.value; } + toggleCameraEnabled() { + window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled; + } unregister_device(ev) { const deviceID = ev.currentTarget.deviceID; @@ -68,7 +71,13 @@ loadDevTools().then(() => { > `} -
Browser-mod not connected.
+
+

Settings that apply to this browser.

+

+ It is strongly recommended to refresh your browser window + after any change to those settings. +

+
Enable @@ -99,7 +108,10 @@ loadDevTools().then(() => { >Get camera input from this device (hardware dependent) - +
@@ -116,7 +128,10 @@ loadDevTools().then(() => { .datetime=${window.browser_mod.devices[d].last_seen} > - + diff --git a/js/plugin/camera.ts b/js/plugin/camera.ts index ea8c230..48fdb77 100644 --- a/js/plugin/camera.ts +++ b/js/plugin/camera.ts @@ -1,63 +1,72 @@ -export const BrowserModCameraMixin = (C) => - class extends C { - setup_camera() { - console.log("Starting camera"); +export const CameraMixin = (SuperClass) => { + return class CameraMixinClass extends SuperClass { + private _video; + private _canvas; + private _framerate; - if (this._video) return; - this._video = document.createElement("video"); - this._video.autoplay = true; - this._video.playsInline = true; - this._video.style.display = "none"; - - this._canvas = document.createElement("canvas"); - this._canvas.style.display = "none"; - - document.body.appendChild(this._video); - document.body.appendChild(this._canvas); - - if (!navigator.mediaDevices) return; - - console.log("Starting devices"); - navigator.mediaDevices - .getUserMedia({ video: true, audio: false }) - .then((stream) => { - this._video.srcObject = stream; - this._video.play(); - this.update_camera(); - }); - - this._camera_framerate = 2; + constructor() { + super(); + this._framerate = 2; window.addEventListener( - "click", + "pointerdown", () => { - if (this._video.ended || this._video.paused) this._video.play(); + this._setup_camera(); }, - { - once: true, - } + { once: true } ); } - update_camera() { - this._canvas.width = this._video.videoWidth; - this._canvas.height = this._video.videoHeight; + async _setup_camera() { + if (this._video) return; + await this.connectionPromise; + if (!this.cameraEnabled) return; + const video = (this._video = document.createElement("video")); + video.autoplay = true; + video.playsInline = true; + video.style.display = "none"; + const canvas = (this._canvas = document.createElement("canvas")); + canvas.style.display = "none"; + + document.body.appendChild(video); + document.body.appendChild(canvas); + + if (!navigator.mediaDevices) return; + + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: false, + }); + + video.srcObject = stream; + video.play(); + this.update_camera(); + } + + async update_camera() { + if (!this.cameraEnabled) { + const stream = this._video?.srcObject; + if (stream) { + stream.getTracks().forEach((t) => t.stop()); + this._video.scrObject = undefined; + } + return; + } + const video = this._video; + const width = video.videoWidth; + const height = video.videoHeight; + this._canvas.width = width; + this._canvas.height = height; const context = this._canvas.getContext("2d"); - context.drawImage( - this._video, - 0, - 0, - this._video.videoWidth, - this._video.videoHeight - ); + context.drawImage(video, 0, 0, width, height); this.sendUpdate({ camera: this._canvas.toDataURL("image/jpeg"), }); - setTimeout( - () => this.update_camera(), - Math.round(1000 / this._camera_framerate) - ); + + const interval = Math.round(1000 / this._framerate); + setTimeout(() => this.update_camera(), interval); } }; +}; diff --git a/js/plugin/connection.ts b/js/plugin/connection.ts index 58ce150..9bb5f61 100644 --- a/js/plugin/connection.ts +++ b/js/plugin/connection.ts @@ -103,21 +103,32 @@ export const ConnectionMixin = (SuperClass) => { })(); } + + private async _reregister(newData = {}) { + await this.connection.sendMessage({ + type: "browser_mod/reregister", + deviceID: this.deviceID, + data: { + ...this.devices[this.deviceID], + ...newData, + }, + }); + } + get meta() { if (!this.registered) return null; return this.devices[this.deviceID].meta; } set meta(value) { - (async () => { - await this.connection.sendMessage({ - type: "browser_mod/reregister", - deviceID: this.deviceID, - data: { - ...this.devices[this.deviceID], - meta: value, - } - }) - })() + this._reregister({ meta: value }); + } + + get cameraEnabled() { + if (!this.registered) return null; + return this.devices[this.deviceID].camera; + } + set cameraEnabled(value) { + this._reregister({ camera: value }); } sendUpdate(data) { diff --git a/js/plugin/main.ts b/js/plugin/main.ts index d720734..9a07175 100644 --- a/js/plugin/main.ts +++ b/js/plugin/main.ts @@ -9,8 +9,8 @@ import "./browser-player"; import { ConnectionMixin } from "./connection"; import { ScreenSaverMixin } from "./screensaver"; import { MediaPlayerMixin } from "./mediaPlayer"; +import { CameraMixin } from "./camera"; import { FullyKioskMixin } from "./fullyKiosk"; -import { BrowserModCameraMixin } from "./camera"; import { BrowserModScreensaverMixin } from "./screensaver"; import { BrowserModPopupsMixin } from "./popups"; import { BrowserModBrowserMixin } from "./browser"; @@ -27,7 +27,9 @@ const ext = (baseClass, mixins) => // FullyKioskMixin, // BrowserModMediaPlayerMixin, // ]) { -export class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) { +export class BrowserMod extends CameraMixin( + MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) +) { constructor() { super(); this.entity_id = deviceID.replace("-", "_"); diff --git a/package-lock.json b/package-lock.json index 3df62a2..ae4ae87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "browser_mod", - "version": "1.5.3", + "version": "2.0.0b0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0c5b1e4..8808ad6 100644 --- a/package.json +++ b/package.json @@ -25,4 +25,4 @@ "dependencies": { "card-tools": "github:thomasloven/lovelace-card-tools" } -} \ No newline at end of file +}