Compare commits

..

10 Commits

Author SHA1 Message Date
Koushik Dutta
b2a05c099d prepublish 2023-03-16 20:37:42 -07:00
Koushik Dutta
898331da4c Merge branch 'main' of github.com:koush/scrypted 2023-03-16 20:37:33 -07:00
Koushik Dutta
9044e782b2 python-codecs: add gray decoding support 2023-03-16 20:37:28 -07:00
Koushik Dutta
aedb985941 detect: support motion on new pipeline 2023-03-16 20:37:12 -07:00
Koushik Dutta
9ba22e4058 server: fix python rpc kill handling 2023-03-16 20:33:09 -07:00
Alex Leeds
ab0afb61ae ring: add video clips support (#635)
* ring: add video clips support

* fix merge
2023-03-16 18:40:36 -07:00
Alex Leeds
bf00ba0adc ring: add support for locks (#634) 2023-03-16 18:32:20 -07:00
Koushik Dutta
d564cf1b62 server: update package lock 2023-03-16 11:13:24 -07:00
Koushik Dutta
544dfb3b24 Update rtsp-proxy.ts 2023-03-16 10:40:19 -07:00
Koushik Dutta
cf9af910be rtsp: rtsp proxy example 2023-03-16 10:03:24 -07:00
17 changed files with 383 additions and 117 deletions

56
common/test/rtsp-proxy.ts Normal file
View File

@@ -0,0 +1,56 @@
import net from 'net';
import { listenZero } from '../src/listen-cluster';
import { RtspClient, RtspServer } from '../src/rtsp-server';
async function main() {
const server = net.createServer(async serverSocket => {
const client = new RtspClient('rtsp://localhost:57594/911db962087f904d');
await client.options();
const describeResponse = await client.describe();
const sdp = describeResponse.body.toString();
const server = new RtspServer(serverSocket, sdp, true);
const setupResponse = await server.handlePlayback();
if (setupResponse !== 'play') {
serverSocket.destroy();
client.client.destroy();
return;
}
console.log('playback handled');
let channel = 0;
for (const track of Object.keys(server.setupTracks)) {
const setupTrack = server.setupTracks[track];
await client.setup({
// type: 'udp',
type: 'tcp',
port: channel,
path: setupTrack.control,
onRtp(rtspHeader, rtp) {
server.sendTrack(setupTrack.control, rtp, false);
},
});
channel += 2;
}
await client.play();
console.log('client playing');
await client.readLoop();
});
let port: number;
if (false) {
port = await listenZero(server);
}
else {
port = 5555;
server.listen(5555)
}
console.log(`rtsp://127.0.0.1:${port}`);
}
main();

View File

@@ -1,4 +1,4 @@
import sdk, { ScryptedMimeTypes, Image, VideoFrame, VideoFrameGenerator, Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import sdk, { ScryptedMimeTypes, Image, VideoFrame, VideoFrameGenerator, Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, MediaStreamDestination } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import cloneDeep from 'lodash/cloneDeep';
@@ -53,13 +53,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
newPipeline: {
title: 'Video Pipeline',
description: 'Configure how frames are provided to the video analysis pipeline.',
async onGet() {
onGet: async () => {
const choices = [
'Default',
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
if (!this.hasMotionType)
choices.push('Snapshot');
return {
choices: [
'Default',
'Snapshot',
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
],
choices,
}
},
defaultValue: 'Default',
@@ -142,7 +144,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
analyzeStop = 0;
lastDetectionInput = 0;
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public hasMotionType: boolean, public settings: Setting[]) {
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, public model: ObjectDetectionModel, group: string, public hasMotionType: boolean, public settings: Setting[]) {
super({
mixinDevice, mixinDeviceState,
mixinProviderNativeId: providerNativeId,
@@ -153,7 +155,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
});
this.cameraDevice = systemManager.getDeviceById<Camera & VideoCamera & MotionSensor & ObjectDetector>(this.id);
this.detectionId = modelName + '-' + this.cameraDevice.id;
this.detectionId = model.name + '-' + this.cameraDevice.id;
this.bindObjectDetection();
this.register();
@@ -171,7 +173,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (this.hasMotionType) {
// force a motion detection restart if it quit
if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_REPLACE)
await this.startVideoDetection();
await this.startStreamAnalysis();
return;
}
}, this.storageSettings.values.detectionInterval * 1000);
@@ -224,7 +226,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
if (this.motionSensorSupplementation !== BUILTIN_MOTION_SENSOR_REPLACE)
return;
await this.startVideoDetection();
await this.startStreamAnalysis();
}
endObjectDetection() {
@@ -310,7 +312,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
if (!this.detectorRunning)
this.console.log('built in motion sensor started motion, starting video detection.');
await this.startVideoDetection();
await this.startStreamAnalysis();
return;
}
@@ -491,8 +493,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
this.analyzeStop = Date.now() + this.getDetectionDuration();
const newPipeline = this.newPipeline;
let generator : () => Promise<AsyncGenerator<VideoFrame & MediaObject>>;
if (newPipeline === 'Snapshot') {
let generator: () => Promise<AsyncGenerator<VideoFrame & MediaObject>>;
if (newPipeline === 'Snapshot' && !this.hasMotionType) {
const self = this;
generator = async () => (async function* gen() {
try {
@@ -528,20 +530,26 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
})();
}
else {
const destination: MediaStreamDestination = this.hasMotionType ? 'low-resolution' : 'local-recorder';
const videoFrameGenerator = systemManager.getDeviceById<VideoFrameGenerator>(newPipeline);
if (!videoFrameGenerator)
throw new Error('invalid VideoFrameGenerator');
const stream = await this.cameraDevice.getVideoStream({
destination: 'local-recorder',
destination,
// ask rebroadcast to mute audio, not needed.
audio: null,
});
generator = async () => videoFrameGenerator.generateVideoFrames(stream);
generator = async () => videoFrameGenerator.generateVideoFrames(stream, {
resize: this.model?.inputSize ? {
width: this.model.inputSize[0],
height: this.model.inputSize[1],
} : undefined,
format: this.model?.inputFormat,
});
}
try {
const start = Date.now();
let detections = 0;
for await (const detected
of await this.objectDetection.generateObjectDetections(await generator(), {
@@ -588,6 +596,9 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
// this.handleDetectionEvent(detected.detected);
}
}
catch (e) {
this.console.error('video pipeline ended with error', e);
}
finally {
this.endObjectDetection();
}
@@ -1190,7 +1201,7 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
const settings = this.model.settings;
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model.name, group, hasMotionType, settings);
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model, group, hasMotionType, settings);
this.currentMixins.add(ret);
return ret;
}

View File

@@ -16,6 +16,6 @@
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/scrypted_python"
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}

View File

@@ -13,38 +13,40 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.1.17",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.74.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.15"
"typedoc": "^0.23.21"
}
},
"../sdk": {
@@ -59,12 +61,12 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
@@ -72,9 +74,11 @@
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.4.0",
"typedoc": "^0.23.15",
"webpack": "^5.74.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
}

View File

@@ -3,6 +3,7 @@ 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
@@ -10,7 +11,7 @@ try:
from gi.repository import Gst
except:
pass
from scrypted_sdk.types import ObjectDetectionModel, ObjectDetectionResult, ObjectsDetected, Setting
from scrypted_sdk.types import ObjectDetectionModel, ObjectDetectionResult, ObjectsDetected, Setting, VideoFrame
from PIL import Image
class OpenCVDetectionSession(DetectionSession):
@@ -93,6 +94,9 @@ class OpenCVPlugin(DetectPlugin):
def get_pixel_format(self):
return self.pixelFormat
def get_input_format(self) -> str:
return 'gray'
def parse_settings(self, settings: Any):
area = defaultArea
@@ -106,7 +110,8 @@ class OpenCVPlugin(DetectPlugin):
blur = int(settings.get('blur', blur))
return area, threshold, interval, blur
def detect(self, detection_session: OpenCVDetectionSession, frame, settings: Any, src_size, convert_to_src_size) -> ObjectsDetected:
def detect(self, detection_session: OpenCVDetectionSession, frame, src_size, convert_to_src_size) -> ObjectsDetected:
settings = detection_session.settings
area, threshold, interval, blur = self.parse_settings(settings)
# see get_detection_input_size on undocumented size requirements for GRAY8
@@ -119,10 +124,15 @@ class OpenCVPlugin(DetectPlugin):
detection_session.curFrame = cv2.GaussianBlur(
gray, (blur, blur), 0, dst=detection_session.curFrame)
detections: List[ObjectDetectionResult] = []
detection_result: ObjectsDetected = {}
detection_result['detections'] = detections
detection_result['inputDimensions'] = src_size
if detection_session.previous_frame is None:
detection_session.previous_frame = detection_session.curFrame
detection_session.curFrame = None
return
return detection_result
detection_session.frameDelta = cv2.absdiff(
detection_session.previous_frame, detection_session.curFrame, dst=detection_session.frameDelta)
@@ -138,10 +148,6 @@ class OpenCVPlugin(DetectPlugin):
detection_session.dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(fcontours)
detections: List[ObjectDetectionResult] = []
detection_result: ObjectsDetected = {}
detection_result['detections'] = detections
detection_result['inputDimensions'] = src_size
for c in contours:
x, y, w, h = cv2.boundingRect(c)
@@ -163,6 +169,9 @@ class OpenCVPlugin(DetectPlugin):
detections.append(detection)
return detection_result
def get_input_details(self) -> Tuple[int, int, int]:
return (300, 300, 1)
def get_detection_input_size(self, src_size):
# The initial implementation of this plugin used BGRA
@@ -197,11 +206,45 @@ class OpenCVPlugin(DetectPlugin):
detection_session.cap = None
return super().end_session(detection_session)
def run_detection_image(self, detection_session: DetectionSession, image: Image.Image, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
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:
width = videoFrame.width
height = videoFrame.height
def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
aspectRatio = width / height
# dont bother resizing if its already fairly small
if width <= 640 and height < 640:
scale = 1
resize = None
elif aspectRatio > 1:
scale = height / 300
resize = {
'height': 300,
'width': int(300 * aspectRatio)
}
else:
scale = width / 300
resize = {
'width': 300,
'height': int(300 / aspectRatio)
}
buffer = await videoFrame.toBuffer({
'resize': resize,
})
def convert_to_src_size(point, normalize = False):
return point[0] * scale, point[1] * scale, True
mat = np.ndarray((videoFrame.height, videoFrame.width, self.pixelFormatChannelCount), buffer=buffer, dtype=np.uint8)
detections = self.detect(
detection_session, mat, (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:
@@ -209,11 +252,11 @@ class OpenCVPlugin(DetectPlugin):
detections = self.detect(
detection_session, mat, settings, src_size, convert_to_src_size)
if not detections or not len(detections['detections']):
self.detection_sleep(settings)
await self.detection_sleep(settings)
return None, None
return detections, None
def run_detection_gstsample(self, detection_session: OpenCVDetectionSession, gst_sample, settings: Any, src_size, convert_to_src_size) -> ObjectsDetected:
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
@@ -236,18 +279,18 @@ class OpenCVPlugin(DetectPlugin):
buf.unmap(info)
if not detections or not len(detections['detections']):
self.detection_sleep(settings)
await self.detection_sleep(settings)
return None, None
return detections, None
def create_detection_session(self):
return OpenCVDetectionSession()
def detection_sleep(self, settings: Any):
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
sleep(interval / 1000)
await asyncio.sleep(interval / 1000)
def detection_event_notified(self, settings: Any):
self.detection_sleep(settings)
return super().detection_event_notified(settings)
async def detection_event_notified(self, settings: Any):
await self.detection_sleep(settings)
return await super().detection_event_notified(settings)

View File

@@ -219,7 +219,22 @@ async def generateVideoFramesGstreamer(mediaObject: scrypted_sdk.MediaObject, op
if videoCodec == 'h264':
videosrc += ' ! rtph264depay ! h264parse'
videosrc += ' ! decodebin ! queue leaky=downstream max-size-buffers=0 ! videoconvert ! video/x-raw,format=RGB'
videocaps = 'video/x-raw'
# if options and options.get('resize'):
# videocaps = 'videoscale ! video/x-raw,width={width},height={height}'.format(width=options['resize']['width'], height=options['resize']['height'])
format = options and options.get('format')
# I420 is a cheap way to get gray out of an h264 stream without color conversion.
if format == 'gray':
format = 'I420'
bands = 1
else:
format = 'RGB'
bands = 3
videocaps += ',format={format}'.format(format=format)
videosrc += ' ! decodebin ! queue leaky=downstream max-size-buffers=0 ! videoconvert ! ' + videocaps
gst, gen = createPipelineIterator(videosrc)
async for gstsample in gen():
@@ -232,8 +247,7 @@ async def generateVideoFramesGstreamer(mediaObject: scrypted_sdk.MediaObject, op
continue
try:
# pyvips.Image.new_from_memory(info.data, width, height, 3, pyvips.BandFormat.UCHAR)
vips = pyvips.Image.new_from_memory(info.data, width, height, 3, pyvips.BandFormat.UCHAR)
vips = pyvips.Image.new_from_memory(info.data, width, height, bands, pyvips.BandFormat.UCHAR)
vipsImage = VipsImage(vips)
try:
mo = await createVipsMediaObject(VipsImage(vips))

View File

@@ -9,7 +9,9 @@ Do not enable prebuffer on Ring cameras and doorbells.
* The persistent live stream will drain the battery faster than it can charge.
* The persistent live stream will also count against ISP bandwidth limits.
## Supported Cameras
## Supported Devices
### Cameras
- Ring Video Doorbell Wired, Pro, Pro 2, 4, 3, 2nd Gen
- Ring Floodlight Cam Wired Plus
- Ring Floodlight Cam Wired Pro
@@ -17,6 +19,15 @@ Do not enable prebuffer on Ring cameras and doorbells.
- Ring Indoor Cam
- Ring Stick-Up Cam (Wired and Battery)
### Other Devices
- Security Panel
- Location Modes
- Contact Sensor / Retrofit Alarm Zones / Tilt Sensor
- Motion Sensor
- Flood / Freeze Sensor
- Water Sensor
- Smart Locks
## Problems and Solutions
I can see artifacts in HKSV recordings

View File

@@ -1,17 +1,17 @@
{
"name": "@scrypted/ring",
"version": "0.0.98",
"version": "0.0.100",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ring",
"version": "0.0.98",
"version": "0.0.100",
"dependencies": {
"@koush/ring-client-api": "file:../../external/ring-client-api",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.14.5",
"@types/node": "^18.15.3",
"axios": "^1.3.4",
"rxjs": "^7.8.0"
},
@@ -49,7 +49,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.82",
"version": "0.2.85",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -148,9 +148,9 @@
}
},
"node_modules/@types/node": {
"version": "18.14.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.5.tgz",
"integrity": "sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw=="
"version": "18.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz",
"integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw=="
},
"node_modules/@types/responselike": {
"version": "1.0.0",

View File

@@ -36,7 +36,7 @@
"@koush/ring-client-api": "file:../../external/ring-client-api",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.14.5",
"@types/node": "^18.15.3",
"axios": "^1.3.4",
"rxjs": "^7.8.0"
},
@@ -44,5 +44,5 @@
"got": "11.8.6",
"socket.io-client": "^2.5.0"
},
"version": "0.0.98"
"version": "0.0.100"
}

View File

@@ -3,15 +3,15 @@ import { RefreshPromise } from "@scrypted/common/src/promise-utils";
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
import { RtspServer } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp, replacePorts } from '@scrypted/common/src/sdp-utils';
import sdk, { Battery, BinarySensor, Camera, Device, DeviceProvider, EntrySensor, FFmpegInput, FloodSensor, Lock, LockState, MediaObject, MediaStreamUrl, MotionSensor, OnOff, PictureOptions, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SecuritySystem, SecuritySystemMode, Setting, Settings, SettingValue, TamperSensor, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import sdk, { Battery, BinarySensor, Camera, Device, DeviceProvider, EntrySensor, FFmpegInput, FloodSensor, MediaObject, MediaStreamUrl, MotionSensor, OnOff, PictureOptions, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SecuritySystem, SecuritySystemMode, Setting, Settings, SettingValue, TamperSensor, VideoCamera } from '@scrypted/sdk';
import child_process, { ChildProcess } from 'child_process';
import dgram from 'dgram';
import { RtcpReceiverInfo, RtcpRrPacket } from '../../../external/werift/packages/rtp/src/rtcp/rr';
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
import { ProtectionProfileAes128CmHmacSha1_80 } from '../../../external/werift/packages/rtp/src/srtp/const';
import { SrtcpSession } from '../../../external/werift/packages/rtp/src/srtp/srtcp';
import { Location, LocationMode, RingDevice, isStunMessage, RtpDescription, SipSession, BasicPeerConnection, CameraData, clientApi, generateUuid, RingBaseApi, RingRestClient, rxjs, SimpleWebRtcSession, StreamingSession, RingDeviceType, RingDeviceData } from './ring-client-api';
import { BasicPeerConnection, CameraData, clientApi, generateUuid, isStunMessage, Location, LocationMode, RingBaseApi, RingDevice, RingDeviceData, RingDeviceType, RingRestClient, RtpDescription, rxjs, SimpleWebRtcSession, SipSession, StreamingSession } from './ring-client-api';
import { encodeSrtpOptions, getPayloadType, getSequenceNumber, isRtpMessagePayloadType } from './srtp-utils';
const STREAM_TIMEOUT = 120000;
@@ -79,7 +79,7 @@ class RingCameraSiren extends ScryptedDeviceBase implements OnOff {
}
}
class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Camera, MotionSensor, BinarySensor, RTCSignalingChannel {
class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Camera, MotionSensor, BinarySensor, RTCSignalingChannel, VideoClips {
buttonTimeout: NodeJS.Timeout;
session: SipSession;
rtpDescription: RtpDescription;
@@ -89,6 +89,7 @@ class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Cam
currentMediaMimeType: string;
refreshTimeout: NodeJS.Timeout;
picturePromise: RefreshPromise<Buffer>;
videoClips = new Map<string, VideoClip>();
constructor(public plugin: RingPlugin, public location: RingLocationDevice, nativeId: string) {
super(nativeId);
@@ -98,7 +99,6 @@ class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Cam
this.batteryLevel = this.findCamera()?.batteryLevel;
}
async startIntercom(media: MediaObject): Promise<void> {
if (!this.session)
throw new Error("not in call");
@@ -659,9 +659,100 @@ class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Cam
siren.on = data.siren_status.seconds_remaining > 0 ? true : false;
}
}
async getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
this.videoClips = new Map<string, VideoClip>;
const response = await this.findCamera().videoSearch({
dateFrom: options.startTime,
dateTo: options.endTime,
});
return response.video_search.map((result) => {
const videoClip = {
id: result.ding_id,
startTime: result.created_at,
duration: Math.round(result.duration * 1000),
event: result.kind.toString(),
description: result.kind.toString(),
thumbnailId: result.ding_id,
resources: {
thumbnail: {
href: result.thumbnail_url
},
video: {
href: result.hq_url
}
}
}
this.videoClips.set(result.ding_id, videoClip)
return videoClip;
});
}
async getVideoClip(videoId: string): Promise<MediaObject> {
if (this.videoClips.has(videoId)) {
return mediaManager.createMediaObjectFromUrl(this.videoClips.get(videoId).resources.video.href);
}
throw new Error('Failed to get video clip.')
}
async getVideoClipThumbnail(thumbnailId: string): Promise<MediaObject> {
if (this.videoClips.has(thumbnailId)) {
return mediaManager.createMediaObjectFromUrl(this.videoClips.get(thumbnailId).resources.thumbnail.href);
}
throw new Error('Failed to get video clip thumbnail.')
}
async removeVideoClips(...videoClipIds: string[]): Promise<void> {
throw new Error('Removing video clips not supported.');
}
}
class RingLock extends ScryptedDeviceBase implements Battery, Lock {
device: RingDevice
constructor(nativeId: string, device: RingDevice) {
super(nativeId);
this.device = device;
device.onData.subscribe(async (data: RingDeviceData) => {
this.updateState(data);
});
}
async lock(): Promise<void> {
return this.device.sendCommand('lock.lock');
}
async unlock(): Promise<void> {
return this.device.sendCommand('lock.unlock');
}
updateState(data: RingDeviceData) {
this.batteryLevel = data.batteryLevel;
switch (data.locked) {
case 'locked':
this.lockState = LockState.Locked;
break;
case 'unlocked':
this.lockState = LockState.Unlocked;
break;
case 'jammed':
this.lockState = LockState.Jammed;
break;
default:
this.lockState = undefined;
}
}
}
class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, EntrySensor, MotionSensor, FloodSensor {
constructor(nativeId: string, device: RingDevice) {
super(nativeId);
device.onData.subscribe(async (data: RingDeviceData) => {
this.updateState(data);
});
}
updateState(data: RingDeviceData) {
this.tampered = data.tamperStatus === 'tamper';
this.batteryLevel = data.batteryLevel;
@@ -673,6 +764,7 @@ class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, En
export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProvider, SecuritySystem {
devices = new Map<string, any>();
locationDevices = new Map<string, RingDevice>();
constructor(public plugin: RingPlugin, nativeId: string) {
super(nativeId);
@@ -762,11 +854,21 @@ export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProv
return this.plugin.locations.find(l => l.id === this.nativeId);
}
async findRingDeviceAtLocation(id: string): Promise<RingDevice> {
const location = this.findLocation();
return (await location.getDevices()).find((x) => x.id === id);
}
async getDevice(nativeId: string) {
if (!this.devices.has(nativeId)) {
if (nativeId.endsWith('-sensor')) {
const sensor = new RingSensor(nativeId);
this.devices.set(nativeId, sensor);
const ringRevice = await this.findRingDeviceAtLocation(nativeId.replace('-sensor', ''));
const device = new RingSensor(nativeId, ringRevice);
this.devices.set(nativeId, device);
} else if (nativeId.endsWith('-lock')) {
const ringRevice = await this.findRingDeviceAtLocation(nativeId.replace('-lock', ''));
const device = new RingLock(nativeId, ringRevice);
this.devices.set(nativeId, device);
} else {
const camera = new RingCameraDevice(this.plugin, this, nativeId);
this.devices.set(nativeId, camera);
@@ -967,6 +1069,7 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
interfaces.push(
ScryptedInterface.VideoCamera,
ScryptedInterface.Intercom,
ScryptedInterface.VideoClips,
);
}
if (camera.operatingOnBattery)
@@ -1028,41 +1131,52 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
});
}
const sensors = (await location.getDevices()).filter(x => {
const supportedSensors = [
RingDeviceType.ContactSensor,
RingDeviceType.RetrofitZone,
RingDeviceType.TiltSensor,
RingDeviceType.MotionSensor,
RingDeviceType.FloodFreezeSensor,
RingDeviceType.WaterSensor,
]
return x.data.status !== 'disabled' && (supportedSensors.includes(x.data.deviceType))
});
for (const sensor of sensors) {
const nativeId = sensor.id.toString() + '-sensor';
const data: RingDeviceData = sensor.data;
// add location devices
const locationDevices = await location.getDevices();
for (const locationDevice of locationDevices) {
const data: RingDeviceData = locationDevice.data;
let nativeId: string;
let type: ScryptedDeviceType;
let interfaces: ScryptedInterface[] = [];
const interfaces = [ScryptedInterface.TamperSensor];
switch (data.deviceType){
if (data.status === 'disabled') {
continue;
}
switch (data.deviceType) {
case RingDeviceType.ContactSensor:
case RingDeviceType.RetrofitZone:
case RingDeviceType.TiltSensor:
interfaces.push(ScryptedInterface.EntrySensor);
nativeId = locationDevice.id.toString() + '-sensor';
type = ScryptedDeviceType.Sensor
interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.EntrySensor);
break;
case RingDeviceType.MotionSensor:
interfaces.push(ScryptedInterface.MotionSensor);
nativeId = locationDevice.id.toString() + '-sensor';
type = ScryptedDeviceType.Sensor
interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.MotionSensor);
break;
case RingDeviceType.FloodFreezeSensor:
case RingDeviceType.WaterSensor:
interfaces.push(ScryptedInterface.FloodSensor);
nativeId = locationDevice.id.toString() + '-sensor';
type = ScryptedDeviceType.Sensor
interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.FloodSensor);
break;
default: break;
default:
if (/^lock($|\.)/.test(data.deviceType)) {
nativeId = locationDevice.id.toString() + '-lock';
type = ScryptedDeviceType.Lock
interfaces.push(ScryptedInterface.Lock);
break;
} else {
this.console.debug(`discovered and ignoring unsupported '${locationDevice.deviceType}' device: '${locationDevice.name}'`)
continue;
}
}
if (data.batteryStatus !== 'none')
interfaces.push(ScryptedInterface.Battery);
const device: Device = {
info: {
model: data.deviceType,
@@ -1071,22 +1185,11 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
},
providerNativeId: location.id,
nativeId: nativeId,
name: sensor.name,
type: ScryptedDeviceType.Sensor,
name: locationDevice.name,
type: type,
interfaces,
};
devices.push(device);
const getScryptedDevice = async () => {
const locationDevice = await this.getDevice(location.id);
const scryptedDevice = await locationDevice?.getDevice(nativeId);
return scryptedDevice as RingSensor;
}
sensor.onData.subscribe(async (data: RingDeviceData) => {
const scryptedDevice = await getScryptedDevice();
scryptedDevice?.updateState(data)
});
}
await deviceManager.onDevicesChanged({

View File

@@ -122,6 +122,9 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
def get_input_details(self) -> Tuple[int, int, int]:
pass
def get_input_format(self) -> str:
pass
def getModelSettings(self, settings: Any = None) -> list[Setting]:
return []
@@ -131,6 +134,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
'classes': self.getClasses(),
'triggerClasses': self.getTriggerClasses(),
'inputSize': self.get_input_details(),
'inputFormat': self.get_input_format(),
'settings': [],
}
@@ -206,7 +210,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
def run_detection_gstsample(self, detection_session: DetectionSession, gst_sample, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
pass
async def run_detection_videoframe(self, videoFrame: scrypted_sdk.VideoFrame) -> ObjectsDetected:
async def run_detection_videoframe(self, videoFrame: scrypted_sdk.VideoFrame, detection_session: DetectionSession) -> ObjectsDetected:
pass
async def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
@@ -288,13 +292,17 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
async def generateObjectDetections(self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None) -> Any:
try:
videoFrames = await scrypted_sdk.sdk.connectRPCObject(videoFrames)
detection_session = self.create_detection_session()
detection_session.plugin = self
detection_session.settings = session and session.get('settings')
async for videoFrame in videoFrames:
detected = await self.run_detection_videoframe(videoFrame, session and session.get('settings'))
detected = await self.run_detection_videoframe(videoFrame, detection_session)
yield {
'__json_copy_serialize_children': True,
'detected': detected,
'videoFrame': videoFrame,
}
await self.detection_event_notified(detection_session.settings)
except:
raise
finally:
@@ -456,7 +464,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
return ret
def detection_event_notified(self, settings: Any):
async def detection_event_notified(self, settings: Any):
pass
async def createMedia(self, data: Any) -> MediaObject:
@@ -527,7 +535,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
self.invalidateMedia(detection_session, data)
# asyncio.run_coroutine_threadsafe(, loop = self.loop).result()
self.detection_event_notified(detection_session.settings)
await self.detection_event_notified(detection_session.settings)
if not detection_session or duration == None:
safe_set_result(detection_session.loop,

View File

@@ -272,7 +272,8 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
pass
async def run_detection_videoframe(self, videoFrame: scrypted_sdk.VideoFrame, settings: Any) -> ObjectsDetected:
async def run_detection_videoframe(self, videoFrame: scrypted_sdk.VideoFrame, detection_session: PredictSession) -> ObjectsDetected:
settings = detection_session.settings
src_size = videoFrame.width, videoFrame.height
w, h = self.get_input_size()
iw, ih = src_size

View File

@@ -233,7 +233,7 @@ class HttpResponseOptions(TypedDict):
class ImageOptions(TypedDict):
crop: Any
format: Any | Any | Any
format: Any | Any | Any | Any
resize: Any
pass
@@ -486,6 +486,7 @@ class ObjectDetectionGeneratorSession(TypedDict):
class ObjectDetectionModel(TypedDict):
classes: list[str]
inputFormat: Any | Any | Any
inputSize: list[float]
name: str
settings: list[Setting]
@@ -693,7 +694,7 @@ class VideoClipOptions(TypedDict):
class VideoFrameGeneratorOptions(TypedDict):
crop: Any
format: Any | Any | Any
format: Any | Any | Any | Any
resize: Any
pass

View File

@@ -1296,6 +1296,7 @@ export interface ObjectDetectionSession extends ObjectDetectionGeneratorSession
export interface ObjectDetectionModel extends ObjectDetectionTypes {
name: string;
inputSize?: number[];
inputFormat?: 'gray' | 'rgb' | 'rgba';
settings: Setting[];
triggerClasses?: string[];
}
@@ -1328,7 +1329,7 @@ export interface ImageOptions {
width?: number,
height?: number,
};
format?: 'rgba' | 'rgb' | 'jpg';
format?: 'gray' | 'rgba' | 'rgb' | 'jpg';
}
export interface Image {
width: number;

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ class RPCResultError(Exception):
def __init__(self, caught, message):
self.caught = caught
self.message = message
self.name = None
self.stack = None
class RpcSerializer:
@@ -121,6 +123,16 @@ class RpcPeer:
self.killed = False
def __apply__(self, proxyId: str, oneWayMethods: List[str], method: str, args: list):
oneway = oneWayMethods and method in oneWayMethods
if self.killed:
future = Future()
if oneway:
future.set_result(None)
return future
future.set_exception(RPCResultError(None, 'RpcPeer has been killed (apply) ' + str(method)))
return future
serializationContext: Dict = {}
serializedArgs = []
for arg in args:
@@ -134,7 +146,7 @@ class RpcPeer:
'method': method,
}
if oneWayMethods and method in oneWayMethods:
if oneway:
rpcApply['oneway'] = True
self.send(rpcApply, None, serializationContext)
future = Future()
@@ -472,12 +484,13 @@ class RpcPeer:
pass
async def createPendingResult(self, cb: Callable[[str, Callable[[Exception], None]], None]):
# if (Object.isFrozen(this.pendingResults))
# return Promise.reject(new RPCResultError('RpcPeer has been killed'));
future = Future()
if self.killed:
future.set_exception(RPCResultError(None, 'RpcPeer has been killed (createPendingResult)'))
return future
id = str(self.idCounter)
self.idCounter = self.idCounter + 1
future = Future()
self.pendingResults[id] = future
await cb(id, lambda e: future.set_exception(RPCResultError(e, None)))
return await future