Added camera functionality

This commit is contained in:
Thomas Lovén 2022-07-13 23:05:27 +00:00
parent 466a5eb5e7
commit e4a65f3077
18 changed files with 355 additions and 223 deletions

View File

@ -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

View File

@ -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

View File

@ -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("-", "_");

View File

@ -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 $ `
<ha-app-layout>
<app-header slot="header" fixed>
@ -137,7 +140,13 @@ loadDevTools().then(() => {
></ha-icon>
`}
</h1>
<div class="card-content">Browser-mod not connected.</div>
<div class="card-content">
<p>Settings that apply to this browser.</p>
<p>
It is strongly recommended to refresh your browser window
after any change to those settings.
</p>
</div>
<div class="card-content">
<ha-settings-row>
<span slot="heading">Enable</span>
@ -168,7 +177,10 @@ loadDevTools().then(() => {
>Get camera input from this device (hardware
dependent)</span
>
<ha-switch> </ha-switch>
<ha-switch
.checked=${(_d = window.browser_mod) === null || _d === void 0 ? void 0 : _d.cameraEnabled}
@change=${this.toggleCameraEnabled}
></ha-switch>
</ha-settings-row>
</div>
</ha-card>
@ -184,7 +196,10 @@ loadDevTools().then(() => {
.datetime=${window.browser_mod.devices[d].last_seen}
></ha-relative-time>
</span>
<ha-icon-button .deviceID=${d} @click=${this.unregister_device}>
<ha-icon-button
.deviceID=${d}
@click=${this.unregister_device}
>
<ha-icon .icon=${"mdi:delete"}></ha-icon>
</ha-icon-button>
<ha-icon-button>

View File

@ -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])

View File

@ -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(

View File

@ -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]
return devices[deviceID]
def deleteDevice(hass, deviceID):
devices = hass.data[DOMAIN]["devices"]
if deviceID in devices:
devices["deviceID"].delete()
del devices["deviceID"]

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
return retval

View File

@ -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):

View File

@ -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(() => {
></ha-icon>
`}
</h1>
<div class="card-content">Browser-mod not connected.</div>
<div class="card-content">
<p>Settings that apply to this browser.</p>
<p>
It is strongly recommended to refresh your browser window
after any change to those settings.
</p>
</div>
<div class="card-content">
<ha-settings-row>
<span slot="heading">Enable</span>
@ -99,7 +108,10 @@ loadDevTools().then(() => {
>Get camera input from this device (hardware
dependent)</span
>
<ha-switch> </ha-switch>
<ha-switch
.checked=${window.browser_mod?.cameraEnabled}
@change=${this.toggleCameraEnabled}
></ha-switch>
</ha-settings-row>
</div>
</ha-card>
@ -116,7 +128,10 @@ loadDevTools().then(() => {
.datetime=${window.browser_mod.devices[d].last_seen}
></ha-relative-time>
</span>
<ha-icon-button .deviceID=${d} @click=${this.unregister_device}>
<ha-icon-button
.deviceID=${d}
@click=${this.unregister_device}
>
<ha-icon .icon=${"mdi:delete"}></ha-icon>
</ha-icon-button>
<ha-icon-button>

View File

@ -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);
}
};
};

View File

@ -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) {

View File

@ -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("-", "_");

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "browser_mod",
"version": "1.5.3",
"version": "2.0.0b0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -25,4 +25,4 @@
"dependencies": {
"card-tools": "github:thomasloven/lovelace-card-tools"
}
}
}