Compare commits

..

28 Commits

Author SHA1 Message Date
Koushik Dutta
fa86c31340 prerelease 2023-03-29 12:41:56 -07:00
Koushik Dutta
94ded75d40 docker: fix watchtower token 2023-03-29 12:17:05 -07:00
Koushik Dutta
887b61cd7a prebeta 2023-03-29 11:58:54 -07:00
Koushik Dutta
48e3d30987 server: output docker flavor to logs 2023-03-29 11:58:43 -07:00
Koushik Dutta
02dba3cd71 docker: include flavor in env variable 2023-03-29 11:57:11 -07:00
Koushik Dutta
195769034d docker: include flavor in env variable 2023-03-29 11:56:50 -07:00
Koushik Dutta
39c08aa378 prebeta 2023-03-29 10:19:18 -07:00
Koushik Dutta
fa8056d38e python: purge packages on update 2023-03-29 10:18:34 -07:00
Koushik Dutta
145f116c68 webrtc/h264: reset stapa sent flag after every idr frame 2023-03-29 09:37:41 -07:00
Koushik Dutta
15b6f336e4 common: add h264 fragment information parsing 2023-03-29 08:18:13 -07:00
Koushik Dutta
8b46f0a466 openv: use new pipieline 2023-03-29 08:17:52 -07:00
Koushik Dutta
a20cc5cd89 docker: always install packages for arm 2023-03-29 08:01:08 -07:00
Koushik Dutta
3d068929fd predict: publish 2023-03-28 19:40:14 -07:00
Koushik Dutta
928f9b7579 prerelease 2023-03-28 19:36:48 -07:00
Koushik Dutta
c1c5a42645 server: fixup versioned prefix/node_modules path 2023-03-28 19:36:39 -07:00
Koushik Dutta
12643cdde2 Merge branch 'main' of github.com:koush/scrypted 2023-03-28 19:27:26 -07:00
Koushik Dutta
0bff96a6e6 python-codecs: pil crop is not thread safe https://github.com/python-pillow/Pillow/issues/4848 2023-03-28 19:27:22 -07:00
TA2k
4e7e67de54 Enable ipv6 for avahi (#670)
Enable ipv6 for avahi to allow multiple mdns server on one host
2023-03-28 13:32:14 -07:00
Koushik Dutta
65c4a30004 rebroadcast: use regular file open flags for truncate 2023-03-28 12:45:52 -07:00
Koushik Dutta
309a1dc11f rebroadcast: add truncation error logging 2023-03-28 12:43:07 -07:00
Koushik Dutta
b7904b73b2 Merge branch 'main' of github.com:koush/scrypted 2023-03-28 12:20:14 -07:00
Koushik Dutta
9e9ddbc5f3 rebroadcast: catch various unhandled errors 2023-03-28 12:20:07 -07:00
Koushik Dutta
ceda54f91b rebroadcast: support recording truncation 2023-03-28 12:19:38 -07:00
Koushik Dutta
1d4052b839 common: simplify some socket utils 2023-03-28 10:26:48 -07:00
Koushik Dutta
6a5d6e6617 predict: cleanups 2023-03-28 10:26:43 -07:00
Koushik Dutta
f55cc6066f common: simplify some socket utils 2023-03-28 10:25:50 -07:00
Brett Jia
527714e434 arlo: camera [spot,flood]lights, sirens + only use interfaces when hardware supports it (#660)
* only create vss and siren for supported basestation models

* VideoClips only if camera has cloud recording + start of Cameras as DeviceProviders

* make verbose logging a boolean toggle

* camera spotlights and floodlights

* tweak video clip delete warning

* bump 0.7.5 for beta

* bump 0.7.6 for release + pin deps

* expose sirens on supported cameras

* bump 0.7.7 for release
2023-03-27 16:43:23 -07:00
Koushik Dutta
8a1633ffa3 tensorflow: reduce dependencies for new pipeline 2023-03-27 12:23:44 -07:00
52 changed files with 555 additions and 219 deletions

View File

@@ -361,8 +361,7 @@ export interface RebroadcasterOptions {
},
}
export async function handleRebroadcasterClient(duplex: Promise<Duplex> | Duplex, options?: RebroadcasterOptions) {
const socket = await duplex;
export function handleRebroadcasterClient(socket: Duplex, options?: RebroadcasterOptions) {
const firstWriteData = (data: StreamChunk) => {
if (data.startStream) {
socket.write(data.startStream)

View File

@@ -62,4 +62,4 @@ export async function bind(server: dgram.Socket, port: number) {
}
}
export { listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
export { listenZero, listenZeroSingleClient, ListenZeroSingleClientTimeoutError } from "@scrypted/server/src/listen-zero";

View File

@@ -129,6 +129,16 @@ export function getNaluTypes(streamChunk: StreamChunk) {
return getNaluTypesInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12))
}
export function getNaluFragmentInformation(nalu: Buffer) {
const naluType = nalu[0] & 0x1f;
const fua = naluType === H264_NAL_TYPE_FU_A;
return {
fua,
fuaStart: fua && !!(nalu[1] & 0x80),
fuaEnd: fua && !!(nalu[1] & 0x40),
}
}
export function getNaluTypesInNalu(nalu: Buffer, fuaRequireStart = false, fuaRequireEnd = false) {
const ret = new Set<number>();
const naluType = nalu[0] & 0x1f;

View File

@@ -63,7 +63,7 @@ RUN apt-get -y install \
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
RUN if [ "$(uname -m)" = "armv7l" ]; \
RUN if [ "$(uname -m)" != "x86_64" ]; \
then \
apt-get -y install \
python3-matplotlib \
@@ -95,7 +95,8 @@ ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230322
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=full
################################################################
# End section generated from template/Dockerfile.full.footer

View File

@@ -42,4 +42,5 @@ ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230322
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=lite

View File

@@ -21,4 +21,5 @@ ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230322
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=thin

View File

@@ -1,7 +1,7 @@
[server]
#host-name=
use-ipv4=yes
use-ipv6=no
use-ipv6=yes
enable-dbus=yes
ratelimit-interval-usec=1000000
ratelimit-burst=1000
@@ -14,4 +14,4 @@ rlimit-core=0
rlimit-data=4194304
rlimit-fsize=0
rlimit-nofile=768
rlimit-stack=4194304
rlimit-stack=4194304

View File

@@ -42,7 +42,7 @@ fi
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
echo "Created $DOCKER_COMPOSE_YML"
curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum)"/g > $DOCKER_COMPOSE_YML
curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
echo "Setting permissions on $SCRYPTED_HOME"
chown -R $SERVICE_USER $SCRYPTED_HOME

View File

@@ -10,7 +10,8 @@ ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230322
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=full
################################################################
# End section generated from template/Dockerfile.full.footer

View File

@@ -60,7 +60,7 @@ RUN apt-get -y install \
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
RUN if [ "$(uname -m)" = "armv7l" ]; \
RUN if [ "$(uname -m)" != "x86_64" ]; \
then \
apt-get -y install \
python3-matplotlib \

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/arlo",
"version": "0.7.4",
"version": "0.7.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.7.4",
"version": "0.7.7",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.7.4",
"version": "0.7.7",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",

View File

@@ -711,7 +711,20 @@ class Arlo(object):
callback,
)
def SirenOn(self, basestation):
def SirenOn(self, basestation, camera=None):
if camera is not None:
resource = f"siren/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"sirenState": "on",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
return self.Notify(basestation, {
"action": "set",
"resource": "siren",
@@ -724,7 +737,20 @@ class Arlo(object):
}
})
def SirenOff(self, basestation):
def SirenOff(self, basestation, camera=None):
if camera is not None:
resource = f"siren/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"sirenState": "off",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
return self.Notify(basestation, {
"action": "set",
"resource": "siren",
@@ -737,6 +763,58 @@ class Arlo(object):
}
})
def SpotlightOn(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"spotlight": {
"enabled": True,
},
},
})
def SpotlightOff(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"spotlight": {
"enabled": False,
},
},
})
def FloodlightOn(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"floodlight": {
"on": True,
},
},
})
def FloodlightOff(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"floodlight": {
"on": False,
},
},
})
def GetLibrary(self, device, from_date: datetime, to_date: datetime):
"""
This call returns the following:
@@ -784,4 +862,13 @@ class Arlo(object):
'dateFrom': from_date,
'dateTo': to_date
}
)
)
def GetSmartFeatures(self, device):
smart_features = self._getSmartFeaturesCached()
key = f"{device['owner']['ownerId']}_{device['deviceId']}"
return smart_features["features"].get(key)
@cached(cache=TTLCache(maxsize=1, ttl=60))
def _getSmartFeaturesCached(self):
return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/subscription/smart/features')

View File

@@ -14,11 +14,20 @@ if TYPE_CHECKING:
class ArloBasestation(ArloDeviceBase, DeviceProvider):
MODELS_WITH_SIRENS = [
"vmb4000",
"vmb4500"
]
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_basestation, arlo_basestation=arlo_basestation, provider=provider)
@property
def has_siren(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS])
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.DeviceProvider.value]
@@ -26,8 +35,11 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider):
return ScryptedDeviceType.DeviceProvider.value
def get_builtin_child_device_manifests(self) -> List[Device]:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
vss = self.get_or_create_vss(vss_id)
if not self.has_siren:
# this basestation has no builtin siren, so no manifests to return
return []
vss = self.get_or_create_vss()
return [
{
"info": {
@@ -36,7 +48,7 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider):
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": vss_id,
"nativeId": vss.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
"interfaces": vss.get_applicable_interfaces(),
"type": vss.get_device_type(),
@@ -48,11 +60,12 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider):
if not nativeId.startswith(self.nativeId):
# must be a camera, so get it from the provider
return await self.provider.getDevice(nativeId)
return self.get_or_create_vss(nativeId)
def get_or_create_vss(self, nativeId: str) -> ArloSirenVirtualSecuritySystem:
if not nativeId.endswith("vss"):
return None
return self.get_or_create_vss()
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
if not self.vss:
self.vss = ArloSirenVirtualSecuritySystem(nativeId, self.arlo_device, self.arlo_basestation, self.provider)
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.vss

View File

@@ -10,9 +10,11 @@ from typing import List, TYPE_CHECKING
import scrypted_arlo_go
import scrypted_sdk
from scrypted_sdk.types import Setting, Settings, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
from scrypted_sdk.types import Setting, Settings, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .spotlight import ArloSpotlight, ArloFloodlight
from .vss import ArloSirenVirtualSecuritySystem
from .child_process import HeartbeatChildProcess
from .util import BackgroundTaskMixin
@@ -21,9 +23,43 @@ if TYPE_CHECKING:
from .provider import ArloProvider
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, MotionSensor, Battery):
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, Battery):
MODELS_WITH_SPOTLIGHTS = [
"vmc4040p",
"vmc2030",
"vmc2032",
"vmc4041p",
"vmc4050p",
"vmc5040",
"vml2030",
"vml4030",
]
MODELS_WITH_FLOODLIGHTS = ["fb1001"]
MODELS_WITH_SIRENS = [
"vmb4000",
"vmb4500",
"vmb4540",
"vmb5000",
"vmc4040p",
"fb1001",
"vmc2030",
"vmc2020",
"vmc2032",
"vmc4041p",
"vmc4050p",
"vmc5040",
"vml2030",
"vmc4030",
"vml4030",
"vmc4030p",
]
timeout: int = 30
intercom_session = None
light: ArloSpotlight = None
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
@@ -55,7 +91,6 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, Moti
ScryptedInterface.MotionSensor.value,
ScryptedInterface.Battery.value,
ScryptedInterface.Settings.value,
ScryptedInterface.VideoClips.value,
])
if self.two_way_audio:
@@ -66,6 +101,12 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, Moti
results.add(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)
if self.has_siren or self.has_spotlight or self.has_floodlight:
results.add(ScryptedInterface.DeviceProvider.value)
if self.has_cloud_recording:
results.add(ScryptedInterface.VideoClips.value)
if not self._can_push_to_talk():
results.discard(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)
@@ -75,6 +116,42 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, Moti
def get_device_type(self) -> str:
return ScryptedDeviceType.Camera.value
def get_builtin_child_device_manifests(self) -> List[Device]:
results = []
if self.has_spotlight or self.has_floodlight:
light = self.get_or_create_spotlight_or_floodlight()
results.append({
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": light.nativeId,
"name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}',
"interfaces": light.get_applicable_interfaces(),
"type": light.get_device_type(),
"providerNativeId": self.nativeId,
})
if self.has_siren:
vss = self.get_or_create_vss()
results.extend([
{
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": vss.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
"interfaces": vss.get_applicable_interfaces(),
"type": vss.get_device_type(),
"providerNativeId": self.nativeId,
},
] + vss.get_builtin_child_device_manifests())
return results
@property
def webrtc_emulation(self) -> bool:
if self.storage:
@@ -92,6 +169,22 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, Moti
else:
return True
@property
def has_cloud_recording(self) -> bool:
return self.provider.arlo.GetSmartFeatures(self.arlo_device)["planFeatures"]["eventRecording"]
@property
def has_spotlight(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SPOTLIGHTS])
@property
def has_floodlight(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_FLOODLIGHTS])
@property
def has_siren(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS])
async def getSettings(self) -> List[Setting]:
if self._can_push_to_talk():
return [
@@ -116,7 +209,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, Moti
async def putSetting(self, key, value) -> None:
if key in ["webrtc_emulation", "two_way_audio"]:
self.storage.setItem(key, value == "true")
await self.provider.discoverDevices()
await self.provider.discover_devices()
async def getPictureOptions(self) -> List[ResponsePictureOptions]:
return []
@@ -281,11 +374,36 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, Moti
clips.reverse()
return clips
@ArloDeviceBase.async_print_exception_guard
async def removeVideoClips(self, videoClipIds: List[str]) -> None:
# Arlo does support deleting, but let's be safe and disable that
self.logger.error("deleting Arlo video clips is not implemented by this plugin")
raise Exception("deleting Arlo video clips is not implemented by this plugin")
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight):
return self.get_or_create_spotlight_or_floodlight()
if nativeId.endswith("vss") and self.has_siren:
return self.get_or_create_vss()
return None
def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight:
if self.has_spotlight:
light_id = f'{self.arlo_device["deviceId"]}.spotlight'
if not self.light:
self.light = ArloSpotlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
elif self.has_floodlight:
light_id = f'{self.arlo_device["deviceId"]}.floodlight'
if not self.light:
self.light = ArloFloodlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.light
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
if self.has_siren:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
if not self.vss:
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.vss
class ArloCameraRTCSignalingSession(BackgroundTaskMixin):
def __init__(self, camera):

View File

@@ -10,7 +10,7 @@ from typing import List
import scrypted_sdk
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, DeviceDiscovery, ScryptedInterface
from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, ScryptedInterface
from .arlo import Arlo
from .arlo.arlo_async import change_stream_class
@@ -23,7 +23,7 @@ from .basestation import ArloBasestation
from .base import ArloDeviceBase
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
arlo_cameras = None
arlo_basestations = None
_arlo_mfa_code = None
@@ -188,7 +188,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
async def do_arlo_setup(self) -> None:
try:
await self.discoverDevices()
await self.discover_devices()
await self.arlo.Subscribe([
(self.arlo_basestations[camera["parentId"]], camera) for camera in self.arlo_cameras.values()
])
@@ -472,11 +472,10 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
{
"group": "General",
"key": "plugin_verbosity",
"title": "Plugin Verbosity",
"description": "Select the verbosity of this plugin. 'Verbose' will show debugging messages, "
"including events received from connected Arlo cameras.",
"value": self.plugin_verbosity,
"choices": sorted(self.plugin_verbosity_choices.keys()),
"title": "Verbose Logging",
"description": "Enable this option to show debug messages, including events received from connected Arlo cameras.",
"value": self.plugin_verbosity == "Verbose",
"type": "boolean",
},
])
@@ -493,13 +492,14 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
elif key == "force_reauth":
# force arlo client to be invalidated and reloaded
self.invalidate_arlo_client()
elif key == "plugin_verbosity":
self.storage.setItem(key, "Verbose" if value == "true" else "Normal")
self.propagate_verbosity()
skip_arlo_client = True
else:
self.storage.setItem(key, value)
if key == "plugin_verbosity":
self.propagate_verbosity()
skip_arlo_client = True
elif key == "arlo_transport":
if key == "arlo_transport":
self.propagate_transport()
# force arlo client to be invalidated and reloaded, but
# keep any mfa codes
@@ -558,7 +558,8 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return False
return True
async def discoverDevices(self, duration: int = 0) -> None:
@ArloDeviceBase.async_print_exception_guard
async def discover_devices(self, duration: int = 0) -> None:
if not self.arlo:
raise Exception("Arlo client not connected, cannot discover devices")
@@ -573,6 +574,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
basestations = self.arlo.GetDevices(['basestation', 'siren'])
for basestation in basestations:
nativeId = basestation["deviceId"]
self.logger.debug(f"Adding {nativeId}")
if nativeId in self.arlo_basestations:
self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added")
@@ -582,7 +584,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
device = await self.getDevice(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.info(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}")
self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}")
# for basestations, we want to add them to the top level DeviceProvider
provider_to_device_map.setdefault(None, []).append(manifest)
@@ -601,11 +603,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"])
for camera in cameras:
nativeId = camera["deviceId"]
self.logger.debug(f"Adding {nativeId}")
if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations:
self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because its basestation was not found")
continue
nativeId = camera["deviceId"]
if nativeId in self.arlo_cameras:
self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added")
continue
@@ -619,7 +623,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
device: ArloDeviceBase = await self.getDevice(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.info(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}")
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}")
if camera["deviceId"] == camera["parentId"]:
provider_to_device_map.setdefault(None, []).append(manifest)

View File

@@ -27,6 +27,7 @@ class ArloSiren(ArloDeviceBase, OnOff):
@ArloDeviceBase.async_print_exception_guard
async def turnOn(self) -> None:
from .basestation import ArloBasestation
self.logger.info("Turning on")
if self.vss.securitySystemState["mode"] == SecuritySystemMode.Disarmed.value:
@@ -42,7 +43,12 @@ class ArloSiren(ArloDeviceBase, OnOff):
}
return
self.provider.arlo.SirenOn(self.arlo_device)
if isinstance(self.vss.parent, ArloBasestation):
self.logger.debug("Parent device is a basestation")
self.provider.arlo.SirenOn(self.arlo_basestation)
else:
self.logger.debug("Parent device is a camera")
self.provider.arlo.SirenOn(self.arlo_basestation, self.arlo_device)
self.on = True
self.vss.securitySystemState = {
@@ -52,8 +58,12 @@ class ArloSiren(ArloDeviceBase, OnOff):
@ArloDeviceBase.async_print_exception_guard
async def turnOff(self) -> None:
from .basestation import ArloBasestation
self.logger.info("Turning off")
self.provider.arlo.SirenOff(self.arlo_device)
if isinstance(self.vss.parent, ArloBasestation):
self.provider.arlo.SirenOff(self.arlo_basestation)
else:
self.provider.arlo.SirenOff(self.arlo_basestation, self.arlo_device)
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import OnOff, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .camera import ArloCamera
class ArloSpotlight(ArloDeviceBase, OnOff):
camera: ArloCamera = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, camera: ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.camera = camera
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.OnOff.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.Light.value
@ArloDeviceBase.async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.SpotlightOn(self.arlo_basestation, self.arlo_device)
self.on = True
@ArloDeviceBase.async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.SpotlightOff(self.arlo_basestation, self.arlo_device)
self.on = False
class ArloFloodlight(ArloSpotlight):
@ArloDeviceBase.async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.FloodlightOn(self.arlo_basestation, self.arlo_device)
self.on = True
@ArloDeviceBase.async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.FloodlightOff(self.arlo_basestation, self.arlo_device)
self.on = False

View File

@@ -11,6 +11,8 @@ from .siren import ArloSiren
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .basestation import ArloBasestation
from .camera import ArloCamera
class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, DeviceProvider):
@@ -19,9 +21,11 @@ class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, DeviceProvi
SUPPORTED_MODES = [SecuritySystemMode.AwayArmed.value, SecuritySystemMode.HomeArmed.value, SecuritySystemMode.Disarmed.value]
siren: ArloSiren = None
parent: ArloBasestation | ArloCamera = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, parent: ArloBasestation | ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.parent = parent
self.create_task(self.delayed_init())
@property
@@ -56,7 +60,7 @@ class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, DeviceProvi
}
return
except Exception as e:
self.logger.info(f"Delayed init failed, will try again: {e}")
self.logger.debug(f"Delayed init failed, will try again: {e}")
await asyncio.sleep(0.1)
iterations += 1

View File

@@ -1,7 +1,7 @@
paho-mqtt==1.6.1
sseclient==0.0.22
requests
cachetools
requests==2.28.2
cachetools==5.3.0
scrypted-arlo-go==0.0.1
--extra-index-url=https://www.piwheels.org/simple/
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/coreml",
"version": "0.1.5",
"version": "0.1.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.5",
"version": "0.1.8",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -41,5 +41,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.5"
"version": "0.1.8"
}

View File

@@ -9,7 +9,7 @@ from PIL import Image
import asyncio
import concurrent.futures
predictExecutor = concurrent.futures.ThreadPoolExecutor(2, "CoreML-Predict")
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "CoreML-Predict")
def parse_label_contents(contents: str):
lines = contents.splitlines()

View File

@@ -1,4 +1,4 @@
coremltools~=6.1
coremltools
# pillow for anything not intel linux, pillow-simd is available on x64 linux
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'

View File

@@ -64,6 +64,9 @@ export class H264Repacketizer {
extraPackets = 0;
fuaMax: number;
pendingFuA: RtpPacket[];
// log whether a stapa sps/pps has been seen.
// resets on every idr frame, to trigger codec information
// to be resent.
seenStapASps = false;
fuaMin: number;
@@ -402,8 +405,12 @@ export class H264Repacketizer {
// if this is an idr frame, but no sps has been sent via a stapa, dummy one up.
// the stream may not contain codec information in stapa or may be sending it
// in separate sps/pps packets which is not supported by homekit.
if (originalNalType === NAL_TYPE_IDR && !this.seenStapASps)
this.maybeSendSpsPps(packet, ret);
if (originalNalType === NAL_TYPE_IDR) {
if (!this.seenStapASps)
this.maybeSendSpsPps(packet, ret);
this.seenStapASps = false;
}
}
else {
if (this.pendingFuA) {
@@ -486,10 +493,12 @@ export class H264Repacketizer {
return;
}
if (nalType === NAL_TYPE_IDR && !this.seenStapASps) {
if (nalType === NAL_TYPE_IDR) {
// if this is an idr frame, but no sps has been sent, dummy one up.
// the stream may not contain sps.
this.maybeSendSpsPps(packet, ret);
if (!this.seenStapASps)
this.maybeSendSpsPps(packet, ret);
this.seenStapASps = false;
}
this.fragment(packet, ret);

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/opencv",
"version": "0.0.70",
"version": "0.0.72",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/opencv",
"version": "0.0.70",
"version": "0.0.72",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -36,5 +36,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.70"
"version": "0.0.72"
}

View File

@@ -1,22 +1,21 @@
from __future__ import annotations
from time import sleep
from detect import DetectionSession, DetectPlugin
from typing import Any, List, Tuple
import numpy as np
import asyncio
import cv2
import imutils
Gst = None
try:
from gi.repository import Gst
except:
pass
from scrypted_sdk.types import ObjectDetectionModel, ObjectDetectionResult, ObjectsDetected, Setting, VideoFrame
from PIL import Image
import numpy as np
class OpenCVDetectionSession(DetectionSession):
from detect import DetectPlugin
import scrypted_sdk
from scrypted_sdk.types import (ObjectDetectionGeneratorSession,
ObjectDetectionResult,
ObjectsDetected, Setting, VideoFrame)
class OpenCVDetectionSession:
def __init__(self) -> None:
super().__init__()
self.cap: cv2.VideoCapture = None
self.previous_frame: Any = None
self.curFrame = None
@@ -110,8 +109,7 @@ class OpenCVPlugin(DetectPlugin):
blur = int(settings.get('blur', blur))
return area, threshold, interval, blur
def detect(self, detection_session: OpenCVDetectionSession, frame, src_size, convert_to_src_size) -> ObjectsDetected:
settings = detection_session.settings
def detect(self, frame, settings: Any, detection_session: OpenCVDetectionSession, src_size, convert_to_src_size) -> ObjectsDetected:
area, threshold, interval, blur = self.parse_settings(settings)
# see get_detection_input_size on undocumented size requirements for GRAY8
@@ -154,8 +152,8 @@ class OpenCVPlugin(DetectPlugin):
# if w * h != contour_area:
# print("mismatch w/h", contour_area - w * h)
x2, y2, _ = convert_to_src_size((x + w, y + h))
x, y, _ = convert_to_src_size((x, y))
x2, y2 = convert_to_src_size((x + w, y + h))
x, y = convert_to_src_size((x, y))
w = x2 - x + 1
h = y2 - y + 1
@@ -206,11 +204,24 @@ class OpenCVPlugin(DetectPlugin):
detection_session.cap = None
return super().end_session(detection_session)
async def run_detection_image(self, detection_session: DetectionSession, image: Image.Image, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
# todo
raise Exception('can not run motion detection on image')
async def run_detection_videoframe(self, videoFrame: VideoFrame, detection_session: OpenCVDetectionSession) -> ObjectsDetected:
async def generateObjectDetections(self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None) -> Any:
try:
ds = OpenCVDetectionSession()
videoFrames = await scrypted_sdk.sdk.connectRPCObject(videoFrames)
async for videoFrame in videoFrames:
detected = await self.run_detection_videoframe(videoFrame, session and session.get('settings'), ds)
yield {
'__json_copy_serialize_children': True,
'detected': detected,
'videoFrame': videoFrame,
}
finally:
try:
await videoFrames.aclose()
except:
pass
async def run_detection_videoframe(self, videoFrame: VideoFrame, settings: Any, detection_session: OpenCVDetectionSession) -> ObjectsDetected:
width = videoFrame.width
height = videoFrame.height
@@ -238,60 +249,8 @@ class OpenCVPlugin(DetectPlugin):
'resize': resize,
})
def convert_to_src_size(point, normalize = False):
return point[0] * scale, point[1] * scale, True
def convert_to_src_size(point):
return point[0] * scale, point[1] * scale
mat = np.ndarray((height, width, self.pixelFormatChannelCount), buffer=buffer, dtype=np.uint8)
detections = self.detect(
detection_session, mat, (width, height), convert_to_src_size)
detections = self.detect(mat, settings, detection_session, (width, height), convert_to_src_size)
return detections
async def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
if avframe.format.name != 'yuv420p' and avframe.format.name != 'yuvj420p':
mat = avframe.to_ndarray(format='gray8')
else:
mat = np.ndarray((avframe.height, avframe.width, self.pixelFormatChannelCount), buffer=avframe.planes[0], dtype=np.uint8)
detections = self.detect(
detection_session, mat, src_size, convert_to_src_size)
if not detections or not len(detections['detections']):
await self.detection_sleep(settings)
return None, None
return detections, None
async def run_detection_gstsample(self, detection_session: OpenCVDetectionSession, gst_sample, settings: Any, src_size, convert_to_src_size) -> ObjectsDetected:
buf = gst_sample.get_buffer()
caps = gst_sample.get_caps()
# can't trust the width value, compute the stride
height = caps.get_structure(0).get_value('height')
width = caps.get_structure(0).get_value('width')
result, info = buf.map(Gst.MapFlags.READ)
if not result:
return None, None
try:
mat = np.ndarray(
(height,
width,
self.pixelFormatChannelCount),
buffer=info.data,
dtype=np.uint8)
detections = self.detect(
detection_session, mat, src_size, convert_to_src_size)
# no point in triggering empty events.
finally:
buf.unmap(info)
if not detections or not len(detections['detections']):
await self.detection_sleep(settings)
return None, None
return detections, None
def create_detection_session(self):
return OpenCVDetectionSession()
async def detection_sleep(self, settings: Any):
area, threshold, interval, blur = self.parse_settings(settings)
# it is safe to block here because gstreamer creates a queue thread
await asyncio.sleep(interval / 1000)
async def detection_event_notified(self, settings: Any):
await self.detection_sleep(settings)
return await super().detection_event_notified(settings)

View File

@@ -1 +0,0 @@
../../tensorflow-lite/src/pipeline

View File

@@ -3,10 +3,6 @@ numpy>=1.16.2
# pillow for anything not intel linux
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'
pillow-simd; sys_platform == 'linux' and platform_machine == 'x86_64'
PyGObject>=3.30.4; sys_platform != 'win32'
imutils>=0.5.0
# not available on armhf
av>=10.0.0; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
# not available on armhf
opencv-python; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
opencv-python; sys_platform != 'linux' or platform_machine == 'x86_64'

View File

@@ -10,7 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"**/plugin-console.*",
"<node_internals>/**"
],
"autoAttachChildProcesses": true,

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.9.77",
"version": "0.9.79",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/prebuffer-mixin",
"version": "0.9.77",
"version": "0.9.79",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.9.77",
"version": "0.9.79",
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -1,5 +1,7 @@
import { Deferred } from "@scrypted/common/src/deferred";
import { Headers, RtspServer } from "@scrypted/common/src/rtsp-server";
import fs from 'fs';
import { format } from "path";
import { Duplex } from "stream";
// non standard extension that dumps the rtp payload to a file.
@@ -28,17 +30,49 @@ export class FileRtspServer extends RtspServer {
ws?.end(() => ws?.destroy());
}
write(url: string, requestHeaders: Headers) {
this.cleanup();
this.segmentBytesWritten = 0;
async write(url: string, requestHeaders: Headers) {
const file = requestHeaders['x-scrypted-rtsp-file'];
if (!file)
return this.respond(400, 'Bad Request', requestHeaders, {});
const truncate = requestHeaders['x-scrypted-rtsp-file-truncate'];
// this.writeConsole?.log('RTSP WRITE file', file);
this.writeStream = fs.createWriteStream(file);
// truncation preparation must happen before cleanup.
let truncateWriteStream: fs.WriteStream;
if (truncate) {
try {
const d = new Deferred<number>();
fs.open(truncate, 'w', (e, fd) => {
if (e)
d.reject(e);
else
d.resolve(fd);
});
const fd = await d.promise;
try {
await fs.promises.rename(truncate, file);
truncateWriteStream = fs.createWriteStream(undefined, {
fd,
})
// this.writeConsole?.log('truncating', truncate);
}
catch (e) {
throw e;
}
}
catch (e) {
this.writeConsole?.error('RTSP WRITE error during truncate file', truncate, e);
}
}
// everything after this point must be sync due to cleanup potentially causing dangling state.
this.cleanup();
this.segmentBytesWritten = 0;
this.writeStream = truncateWriteStream || fs.createWriteStream(file);
this.writeStream.on('error', e => {
this.writeConsole?.error('RTSP WRITE error', e);
});

View File

@@ -3,7 +3,7 @@ import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-p
import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
import { handleRebroadcasterClient, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
import { closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { closeQuiet, listenZeroSingleClient, ListenZeroSingleClientTimeoutError } from '@scrypted/common/src/listen-cluster';
import { readLength } from '@scrypted/common/src/read-stream';
import { createRtspParser, findH264NaluType, getNaluTypes, H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_STAP_B, listenSingleRtspClient, RtspServer, RtspTrack } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
@@ -946,23 +946,35 @@ class PrebufferSession {
const { isActiveClient, container, session, socketPromise, requestedPrebuffer } = options;
this.console.log('sending prebuffer', requestedPrebuffer);
// in case the client never connects, do an inactivity check.
socketPromise.catch(() => this.inactivityCheck(session, false));
socketPromise.then(socket => {
let socket: Duplex;
try {
socket = await socketPromise;
}
catch (e) {
// in case the client never connects, do an inactivity check.
this.inactivityCheck(session, false);
if (e instanceof ListenZeroSingleClientTimeoutError)
this.console.warn('client connection timed out');
else
this.console.error('client connection error', e);
return;
}
if (isActiveClient) {
this.activeClients++;
this.printActiveClients();
}
socket.once('close', () => {
if (isActiveClient) {
this.activeClients++;
this.activeClients--;
this.printActiveClients();
}
socket.once('close', () => {
if (isActiveClient) {
this.activeClients--;
this.printActiveClients();
}
this.inactivityCheck(session, isActiveClient);
})
this.inactivityCheck(session, isActiveClient);
});
handleRebroadcasterClient(socketPromise, {
handleRebroadcasterClient(socket, {
// console: this.console,
connect: (connection) => {
const now = Date.now();
@@ -1138,7 +1150,7 @@ class PrebufferSession {
}
// server.console = this.console;
await server.handlePlayback();
server.handleTeardown().finally(() => server.client.destroy());
server.handleTeardown().catch(() => {}).finally(() => server.client.destroy());
for (const track of Object.values(server.setupTracks)) {
if (track.protocol === 'udp') {
serverPortMap.set(track.codec, track);

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.22",
"version": "0.1.25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/python-codecs",
"version": "0.1.22",
"version": "0.1.25",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.22",
"version": "0.1.25",
"description": "Python Codecs for Scrypted",
"keywords": [
"scrypted",

View File

@@ -26,7 +26,7 @@ class PILImage(scrypted_sdk.VideoFrame):
elif options['format'] == 'rgb':
def format():
rgbx = pilImage.pilImage
if rgbx.format != 'RGBA':
if rgbx.mode != 'RGBA':
return rgbx.tobytes()
rgb = rgbx.convert('RGB')
try:
@@ -96,6 +96,7 @@ class ImageReader(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.BufferConverter)
async def convert(self, data: Any, fromMimeType: str, toMimeType: str, options: scrypted_sdk.MediaObjectOptions = None) -> Any:
pil = Image.open(io.BytesIO(data))
pil.load()
return await createPILMediaObject(PILImage(pil))
class ImageWriter(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.BufferConverter):

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import concurrent.futures
import os
import re
import urllib.request
@@ -16,15 +17,24 @@ from detect import DetectPlugin
from .rectangle import (Rectangle, combine_rect, from_bounding_box,
intersect_area, intersect_rect, to_bounding_box)
def ensureRGBData(data: bytes, size: Tuple[int, int], format: str):
if format == 'rgba':
# vips is already multithreaded, but needs to be kicked off the python asyncio thread.
toThreadExecutor = concurrent.futures.ThreadPoolExecutor(max_workers=2, thread_name_prefix="image")
async def to_thread(f):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(toThreadExecutor, f)
async def ensureRGBData(data: bytes, size: Tuple[int, int], format: str):
if format != 'rgba':
return Image.frombuffer('RGB', size, data)
def convert():
rgba = Image.frombuffer('RGBA', size, data)
try:
return rgba.convert('RGB')
finally:
rgba.close()
else:
return Image.frombuffer('RGB', size, data)
return await to_thread(convert)
def parse_label_contents(contents: str):
lines = contents.splitlines()
@@ -190,12 +200,9 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
detection_result['detections'] = []
for detection in detections:
bb = detection['boundingBox']
x, y, valid = convert_to_src_size((bb[0], bb[1]), True)
x2, y2, valid2 = convert_to_src_size(
(bb[0] + bb[2], bb[1] + bb[3]), True)
if not valid or not valid2:
# print("filtering out", detection['className'])
continue
x, y = convert_to_src_size((bb[0], bb[1]))
x2, y2 = convert_to_src_size(
(bb[0] + bb[2], bb[1] + bb[3]))
detection['boundingBox'] = (x, y, x2 - x + 1, y2 - y + 1)
detection_result['detections'].append(detection)
@@ -224,13 +231,13 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
hs = h / ih
s = max(ws, hs)
if ws == 1 and hs == 1:
def cvss(point, normalize=False):
return point[0], point[1], True
def cvss(point):
return point[0], point[1]
data = await videoFrame.toBuffer({
'format': videoFrame.format or 'rgb',
})
image = ensureRGBData(data, (w, h), videoFrame.format)
image = await ensureRGBData(data, (w, h), videoFrame.format)
try:
ret = await self.detect_once(image, settings, src_size, cvss)
return ret
@@ -275,13 +282,15 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
})
)
first = ensureRGBData(firstData, (w, h), videoFrame.format)
second = ensureRGBData(secondData, (w, h), videoFrame.format)
first, second = await asyncio.gather(
ensureRGBData(firstData, (w, h), videoFrame.format),
ensureRGBData(secondData, (w, h), videoFrame.format)
)
def cvss1(point, normalize=False):
return point[0] / s, point[1] / s, True
def cvss2(point, normalize=False):
return point[0] / s + ow, point[1] / s + oh, True
def cvss1(point):
return point[0] / s, point[1] / s
def cvss2(point):
return point[0] / s + ow, point[1] / s + oh
ret1 = await self.detect_once(first, settings, src_size, cvss1)
first.close()

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/tensorflow-lite",
"version": "0.1.7",
"version": "0.1.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/tensorflow-lite",
"version": "0.1.7",
"version": "0.1.8",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -41,5 +41,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.7"
"version": "0.1.8"
}

View File

@@ -1 +0,0 @@
../../tensorflow-lite/src/pipeline

View File

@@ -1,14 +1,8 @@
# plugin
numpy>=1.16.2
# pillow for anything not intel linux
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'
pillow-simd; sys_platform == 'linux' and platform_machine == 'x86_64'
tensorflow-macos; sys_platform == 'darwin'
tensorflow; sys_platform != 'darwin'
PyGObject>=3.30.4; sys_platform != 'win32'
# not available on armhf
av>=10.0.0; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
# sort_oh
scipy
filterpy
numpy>=1.16.2
# pillow for anything not intel linux, pillow-simd is available on x64 linux
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'
pillow-simd; sys_platform == 'linux' and platform_machine == 'x86_64'

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/webrtc",
"version": "0.1.37",
"version": "0.1.38",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/webrtc",
"version": "0.1.37",
"version": "0.1.38",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/webrtc",
"version": "0.1.37",
"version": "0.1.38",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.7.36",
"version": "0.7.39",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.7.36",
"version": "0.7.39",
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.7.37",
"version": "0.7.41",
"description": "",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",

View File

@@ -397,11 +397,13 @@ class PluginRemote:
python_versioned_directory = '%s-%s-%s' % (python_version, platform.system(), platform.machine())
SCRYPTED_BASE_VERSION = os.environ.get('SCRYPTED_BASE_VERSION')
if SCRYPTED_BASE_VERSION:
python_versioned_directory += SCRYPTED_BASE_VERSION
python_versioned_directory += '-' + SCRYPTED_BASE_VERSION
python_prefix = os.path.join(
plugin_volume, python_versioned_directory)
print('python prefix: %s' % python_prefix)
if not os.path.exists(python_prefix):
os.makedirs(python_prefix)
@@ -421,7 +423,18 @@ class PluginRemote:
pass
if need_pip:
shutil.rmtree(python_prefix)
try:
for de in os.listdir(plugin_volume):
if de.startswith('linux') or de.startswith('darwin') or de.startswith('win32') or de.startswith('python') or de.startswith('node'):
filePath = os.path.join(plugin_volume, de)
print('Removing old dependencies: %s' % filePath)
try:
shutil.rmtree(filePath)
except:
pass
except:
pass
os.makedirs(python_prefix)
print('requirements.txt (outdated)')

View File

@@ -1,6 +1,12 @@
import { once } from 'events';
import net from 'net';
export class ListenZeroSingleClientTimeoutError extends Error {
constructor() {
super('timeout waiting for client')
}
}
export async function listenZero(server: net.Server, hostname?: string) {
server.listen(0, hostname);
await once(server, 'listening');
@@ -14,7 +20,7 @@ export async function listenZeroSingleClient(hostname?: string) {
const clientPromise = new Promise<net.Socket>((resolve, reject) => {
const timeout = setTimeout(() => {
server.close();
reject(new Error('timeout waiting for client'));
reject(new ListenZeroSingleClientTimeoutError());
}, 30000)
server.on('connection', client => {
server.close();

View File

@@ -311,7 +311,9 @@ export class PluginHost {
this.worker.stdout.on('data', data => console.log(data.toString()));
this.worker.stderr.on('data', data => console.error(data.toString()));
const consoleHeader = `${os.platform()} ${os.arch()} ${os.version()}\nserver version: ${serverVersion}\nplugin version: ${this.pluginId} ${this.packageJson.version}\n`;
let consoleHeader = `${os.platform()} ${os.arch()} ${os.version()}\nserver version: ${serverVersion}\nplugin version: ${this.pluginId} ${this.packageJson.version}\n`;
if (process.env.SCRYPTED_DOCKER_FLAVOR)
consoleHeader += `${process.env.SCRYPTED_DOCKER_FLAVOR}\n`;
this.consoleServer = createConsoleServer(this.worker.stdout, this.worker.stderr, consoleHeader);
const disconnect = () => {

View File

@@ -15,7 +15,7 @@ export function getPluginNodePath(name: string) {
let nodeVersionedDirectory = `node${nodeMajorVersion}-${process.platform}-${process.arch}`;
const scryptedBase = process.env.SCRYPTED_BASE_VERSION;
if (scryptedBase)
nodeVersionedDirectory += '-' + nodeVersionedDirectory;
nodeVersionedDirectory += '-' + scryptedBase;
const nodePrefix = path.join(pluginVolume, nodeVersionedDirectory);
return nodePrefix;
}

View File

@@ -176,6 +176,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const pluginConsole = getPluginConsole?.();
params.console = pluginConsole;
const pnp = getPluginNodePath(pluginId);
pluginConsole?.log('node modules', pnp);
params.require = (name: string) => {
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
return volume;