mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fae66619fb | ||
|
|
d979b9ec0c | ||
|
|
975319a65d | ||
|
|
7b5aa4ba2d | ||
|
|
670739c82b | ||
|
|
8511bd15a8 | ||
|
|
06d3c89274 | ||
|
|
e13f3eb2f1 | ||
|
|
001918d613 | ||
|
|
c859c3aa40 | ||
|
|
2bce019677 | ||
|
|
6ba3386157 | ||
|
|
51e66d98f9 | ||
|
|
6484804649 | ||
|
|
b2a05c099d | ||
|
|
898331da4c | ||
|
|
9044e782b2 | ||
|
|
aedb985941 | ||
|
|
9ba22e4058 | ||
|
|
ab0afb61ae | ||
|
|
bf00ba0adc | ||
|
|
d564cf1b62 | ||
|
|
544dfb3b24 | ||
|
|
cf9af910be | ||
|
|
e2e65f93af | ||
|
|
b271567428 | ||
|
|
a88a295d9a | ||
|
|
38ba31ca7d | ||
|
|
1c8ff2493b | ||
|
|
5c9f62e6b6 | ||
|
|
6fd8018c52 | ||
|
|
d900ddf5f1 | ||
|
|
e3a8d311ce | ||
|
|
8bbc3d5470 | ||
|
|
00cf987cec | ||
|
|
7e5dcae64a | ||
|
|
cb67237d7c |
56
common/test/rtsp-proxy.ts
Normal file
56
common/test/rtsp-proxy.ts
Normal 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();
|
||||
@@ -103,7 +103,11 @@ then
|
||||
fi
|
||||
|
||||
RUN python$PYTHON_VERSION -m pip install --upgrade pip
|
||||
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions typing opencv-python psutil
|
||||
if [ "$PYTHON_VERSION" != "3.10" ]
|
||||
then
|
||||
RUN python$PYTHON_VERSION -m pip install typing
|
||||
fi
|
||||
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions opencv-python psutil
|
||||
|
||||
echo "Installing Scrypted Launch Agent..."
|
||||
|
||||
|
||||
2836
plugins/alexa/package-lock.json
generated
2836
plugins/alexa/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -39,6 +39,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.4.2",
|
||||
"@scrypted/sdk": "^0.2.70"
|
||||
"@scrypted/sdk": "../../sdk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
sdp: this.directive.payload.offer.value,
|
||||
},
|
||||
disableTrickle: true,
|
||||
// this could be a low resolutions creen, no way of knowning, so never send a 1080p stream
|
||||
disableTurn: true,
|
||||
// this could be a low resolution screen, no way of knowning, so never send a 1080p stream
|
||||
screen: {
|
||||
devicePixelRatio: 1,
|
||||
width: 1280,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright 2019 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
rm -rf all_models
|
||||
mkdir -p all_models
|
||||
cd all_models
|
||||
wget https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel
|
||||
wget https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt
|
||||
@@ -1 +0,0 @@
|
||||
../all_models/MobileNetV2_SSDLite.mlmodel
|
||||
@@ -1 +0,0 @@
|
||||
../all_models/coco_labels.txt
|
||||
4
plugins/coreml/package-lock.json
generated
4
plugins/coreml/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.0.26",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.0.26",
|
||||
"version": "0.1.2",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.26"
|
||||
"version": "0.1.2"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ from predict import PredictPlugin, Prediction, Rectangle
|
||||
import coremltools as ct
|
||||
import os
|
||||
from PIL import Image
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(2, "CoreML-Predict")
|
||||
|
||||
def parse_label_contents(contents: str):
|
||||
lines = contents.splitlines()
|
||||
@@ -25,17 +29,19 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(MIME_TYPE, nativeId=nativeId)
|
||||
|
||||
modelPath = os.path.join(os.environ['SCRYPTED_PLUGIN_VOLUME'], 'zip', 'unzipped', 'fs', 'MobileNetV2_SSDLite.mlmodel')
|
||||
self.model = ct.models.MLModel(modelPath)
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt', 'coco_labels.txt')
|
||||
modelFile = self.downloadFile('https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel', 'MobileNetV2_SSDLite.mlmodel')
|
||||
|
||||
self.model = ct.models.MLModel(modelFile)
|
||||
|
||||
self.modelspec = self.model.get_spec()
|
||||
self.inputdesc = self.modelspec.description.input[0]
|
||||
self.inputheight = self.inputdesc.type.imageType.height
|
||||
self.inputwidth = self.inputdesc.type.imageType.width
|
||||
|
||||
labels_contents = scrypted_sdk.zip.open(
|
||||
'fs/coco_labels.txt').read().decode('utf8')
|
||||
labels_contents = open(labelsFile, 'r').read()
|
||||
self.labels = parse_label_contents(labels_contents)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
# width, height, channels
|
||||
def get_input_details(self) -> Tuple[int, int, int]:
|
||||
@@ -44,8 +50,12 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
def get_input_size(self) -> Tuple[float, float]:
|
||||
return (self.inputwidth, self.inputheight)
|
||||
|
||||
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
# run in executor if this is the plugin loop
|
||||
if asyncio.get_event_loop() is self.loop:
|
||||
out_dict = await asyncio.get_event_loop().run_in_executor(predictExecutor, lambda: self.model.predict({'image': input, 'confidenceThreshold': .2 }))
|
||||
else:
|
||||
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
|
||||
|
||||
coordinatesList = out_dict['coordinates']
|
||||
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.103",
|
||||
"version": "0.0.108",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.103",
|
||||
"version": "0.0.108",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.103",
|
||||
"version": "0.0.108",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -38,7 +38,10 @@
|
||||
"Settings",
|
||||
"MixinProvider"
|
||||
],
|
||||
"realfs": true
|
||||
"realfs": true,
|
||||
"pluginDependencies": [
|
||||
"@scrypted/python-codecs"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { 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';
|
||||
@@ -50,6 +50,22 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
detections = new Map<string, MediaObject>();
|
||||
cameraDevice: ScryptedDevice & Camera & VideoCamera & MotionSensor & ObjectDetector;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
newPipeline: {
|
||||
title: 'Video Pipeline',
|
||||
description: 'Configure how frames are provided to the video analysis pipeline.',
|
||||
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,
|
||||
}
|
||||
},
|
||||
defaultValue: 'Default',
|
||||
},
|
||||
motionSensorSupplementation: {
|
||||
title: 'Built-In Motion Sensor',
|
||||
description: `This camera has a built in motion sensor. Using ${this.objectDetection.name} may be unnecessary and will use additional CPU. Replace will ignore the built in motion sensor. Filter will verify the motion sent by built in motion sensor. The Default is ${BUILTIN_MOTION_SENSOR_REPLACE}.`,
|
||||
@@ -59,6 +75,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
BUILTIN_MOTION_SENSOR_REPLACE,
|
||||
],
|
||||
defaultValue: "Default",
|
||||
onPut: () => {
|
||||
this.endObjectDetection();
|
||||
this.maybeStartMotionDetection();
|
||||
}
|
||||
},
|
||||
captureMode: {
|
||||
title: 'Capture Mode',
|
||||
@@ -128,7 +148,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,
|
||||
@@ -139,7 +159,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();
|
||||
@@ -157,7 +177,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);
|
||||
@@ -210,7 +230,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return;
|
||||
if (this.motionSensorSupplementation !== BUILTIN_MOTION_SENSOR_REPLACE)
|
||||
return;
|
||||
await this.startVideoDetection();
|
||||
await this.startStreamAnalysis();
|
||||
}
|
||||
|
||||
endObjectDetection() {
|
||||
@@ -296,7 +316,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;
|
||||
}
|
||||
|
||||
@@ -473,22 +493,70 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (this.detectorRunning)
|
||||
return;
|
||||
|
||||
const stream = await this.cameraDevice.getVideoStream({
|
||||
destination: 'local-recorder',
|
||||
// ask rebroadcast to mute audio, not needed.
|
||||
audio: null,
|
||||
});
|
||||
|
||||
this.detectorRunning = true;
|
||||
this.analyzeStop = Date.now() + this.getDetectionDuration();
|
||||
|
||||
const videoFrameGenerator = this.newPipeline as VideoFrameGenerator;
|
||||
const newPipeline = this.newPipeline;
|
||||
let generator: () => Promise<AsyncGenerator<VideoFrame & MediaObject>>;
|
||||
if (newPipeline === 'Snapshot' && !this.hasMotionType) {
|
||||
const self = this;
|
||||
generator = async () => (async function* gen() {
|
||||
try {
|
||||
while (self.detectorRunning) {
|
||||
const now = Date.now();
|
||||
const sleeper = async () => {
|
||||
const diff = now + 1100 - Date.now();
|
||||
if (diff > 0)
|
||||
await sleep(diff);
|
||||
};
|
||||
let image: MediaObject & VideoFrame;
|
||||
try {
|
||||
const mo = await self.cameraDevice.takePicture({
|
||||
reason: 'event',
|
||||
});
|
||||
image = await sdk.mediaManager.convertMediaObject(mo, ScryptedMimeTypes.Image);
|
||||
}
|
||||
catch (e) {
|
||||
self.console.error('Video analysis snapshot failed. Will retry in a moment.');
|
||||
await sleeper();
|
||||
continue;
|
||||
}
|
||||
|
||||
// self.console.log('yield')
|
||||
yield image;
|
||||
// self.console.log('done yield')
|
||||
await sleeper();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
self.console.log('Snapshot generation finished.');
|
||||
}
|
||||
})();
|
||||
}
|
||||
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,
|
||||
// ask rebroadcast to mute audio, not needed.
|
||||
audio: null,
|
||||
});
|
||||
|
||||
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 videoFrameGenerator.generateVideoFrames(stream), {
|
||||
of await this.objectDetection.generateObjectDetections(await generator(), {
|
||||
settings: this.getCurrentSettings(),
|
||||
})) {
|
||||
if (!this.detectorRunning) {
|
||||
@@ -529,9 +597,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// this.console.log('image saved', detected.detected.detections);
|
||||
}
|
||||
this.reportObjectDetections(detected.detected);
|
||||
if (this.hasMotionType) {
|
||||
await sleep(250);
|
||||
}
|
||||
// this.handleDetectionEvent(detected.detected);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('video pipeline ended with error', e);
|
||||
}
|
||||
finally {
|
||||
this.endObjectDetection();
|
||||
}
|
||||
@@ -610,6 +684,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
}
|
||||
|
||||
normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]) {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x * 100 / inputDimensions[0];
|
||||
y = y * 100 / inputDimensions[1];
|
||||
x2 = x2 * 100 / inputDimensions[0];
|
||||
y2 = y2 * 100 / inputDimensions[1];
|
||||
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
return box;
|
||||
}
|
||||
|
||||
getDetectionDuration() {
|
||||
// when motion type, the detection interval is a keepalive reset.
|
||||
// the duration needs to simply be an arbitrarily longer time.
|
||||
@@ -626,15 +713,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
continue;
|
||||
|
||||
o.zones = []
|
||||
let [x, y, width, height] = o.boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x * 100 / detection.inputDimensions[0];
|
||||
y = y * 100 / detection.inputDimensions[1];
|
||||
x2 = x2 * 100 / detection.inputDimensions[0];
|
||||
y2 = y2 * 100 / detection.inputDimensions[1];
|
||||
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
const box = this.normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
|
||||
let included: boolean;
|
||||
for (const [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
@@ -674,6 +753,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
}
|
||||
|
||||
// if this is a motion sensor and there are no inclusion zones set up,
|
||||
// use a default inclusion zone that crops the top and bottom to
|
||||
// prevents errant motion from the on screen time changing every second.
|
||||
if (this.hasMotionType && included === undefined) {
|
||||
const defaultInclusionZone = [[0, 10], [100, 10], [100, 90], [0, 90]];
|
||||
included = polygonOverlap(box, defaultInclusionZone);
|
||||
}
|
||||
|
||||
// if there are inclusion zones and this object
|
||||
// was not in any of them, filter it out.
|
||||
if (included === false)
|
||||
@@ -849,8 +936,18 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
get newPipeline() {
|
||||
return this.plugin.storageSettings.values.newPipeline;
|
||||
}
|
||||
if (!this.plugin.storageSettings.values.newPipeline)
|
||||
return;
|
||||
|
||||
const newPipeline = this.storageSettings.values.newPipeline;
|
||||
if (!newPipeline)
|
||||
return newPipeline;
|
||||
if (newPipeline === 'Snapshot')
|
||||
return newPipeline;
|
||||
const pipelines = getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
|
||||
const found = pipelines.find(p => p.name === newPipeline);
|
||||
return found?.id || pipelines[0]?.id;
|
||||
}
|
||||
|
||||
async getMixinSettings(): Promise<Setting[]> {
|
||||
const settings: Setting[] = [];
|
||||
@@ -872,7 +969,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
|
||||
this.storageSettings.settings.captureMode.hide = this.hasMotionType || !!this.newPipeline;
|
||||
this.storageSettings.settings.captureMode.hide = this.hasMotionType || !!this.plugin.storageSettings.values.newPipeline;
|
||||
this.storageSettings.settings.newPipeline.hide = this.hasMotionType || !this.plugin.storageSettings.values.newPipeline;
|
||||
this.storageSettings.settings.detectionDuration.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.detectionTimeout.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
|
||||
@@ -1123,7 +1221,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;
|
||||
}
|
||||
@@ -1141,8 +1239,7 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings
|
||||
newPipeline: {
|
||||
title: 'New Video Pipeline',
|
||||
description: 'WARNING! DO NOT ENABLE: Use the new video pipeline. Leave blank to use the legacy pipeline.',
|
||||
type: 'device',
|
||||
deviceFilter: `interfaces.includes('${ScryptedInterface.VideoFrameGenerator}')`,
|
||||
type: 'boolean',
|
||||
},
|
||||
activeMotionDetections: {
|
||||
title: 'Active Motion Detection Sessions',
|
||||
|
||||
@@ -9,3 +9,4 @@ dist/*.js
|
||||
dist/*.txt
|
||||
__pycache__
|
||||
all_models
|
||||
.venv
|
||||
|
||||
2
plugins/opencv/.vscode/settings.json
vendored
2
plugins/opencv/.vscode/settings.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
32
plugins/opencv/package-lock.json
generated
32
plugins/opencv/package-lock.json
generated
@@ -1,50 +1,52 @@
|
||||
{
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.64",
|
||||
"version": "0.0.66",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.64",
|
||||
"version": "0.0.66",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.64"
|
||||
"version": "0.0.66"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -230,24 +273,24 @@ class OpenCVPlugin(DetectPlugin):
|
||||
buffer=info.data,
|
||||
dtype=np.uint8)
|
||||
detections = self.detect(
|
||||
detection_session, mat, settings, src_size, convert_to_src_size)
|
||||
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']):
|
||||
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)
|
||||
|
||||
4
plugins/python-codecs/package-lock.json
generated
4
plugins/python-codecs/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.8",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.8",
|
||||
"description": "Python Codecs for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
@@ -25,7 +25,7 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"VideoFrameGenerator"
|
||||
"DeviceProvider"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
136
plugins/python-codecs/src/gst_generator.py
Normal file
136
plugins/python-codecs/src/gst_generator.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import concurrent.futures
|
||||
import threading
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
gi.require_version('GstBase', '1.0')
|
||||
|
||||
from gi.repository import GLib, GObject, Gst
|
||||
GObject.threads_init()
|
||||
Gst.init(None)
|
||||
except:
|
||||
pass
|
||||
|
||||
class Callback:
|
||||
def __init__(self, callback) -> None:
|
||||
if callback:
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.callback = callback
|
||||
else:
|
||||
self.loop = None
|
||||
self.callback = None
|
||||
|
||||
def createPipelineIterator(pipeline: str):
|
||||
pipeline = '{pipeline} ! queue leaky=downstream max-size-buffers=0 ! appsink name=appsink emit-signals=true sync=false max-buffers=-1 drop=true'.format(pipeline=pipeline)
|
||||
print(pipeline)
|
||||
gst = Gst.parse_launch(pipeline)
|
||||
bus = gst.get_bus()
|
||||
|
||||
def on_bus_message(bus, message):
|
||||
t = str(message.type)
|
||||
# print(t)
|
||||
if t == str(Gst.MessageType.EOS):
|
||||
finish()
|
||||
elif t == str(Gst.MessageType.WARNING):
|
||||
err, debug = message.parse_warning()
|
||||
print('Warning: %s: %s\n' % (err, debug))
|
||||
elif t == str(Gst.MessageType.ERROR):
|
||||
err, debug = message.parse_error()
|
||||
print('Error: %s: %s\n' % (err, debug))
|
||||
finish()
|
||||
|
||||
def stopGst():
|
||||
bus.remove_signal_watch()
|
||||
bus.disconnect(watchId)
|
||||
gst.set_state(Gst.State.NULL)
|
||||
|
||||
def finish():
|
||||
nonlocal hasFinished
|
||||
hasFinished = True
|
||||
callback = Callback(None)
|
||||
callbackQueue.put(callback)
|
||||
if not asyncFuture.done():
|
||||
asyncFuture.set_result(None)
|
||||
if not finished.done():
|
||||
finished.set_result(None)
|
||||
|
||||
watchId = bus.connect('message', on_bus_message)
|
||||
bus.add_signal_watch()
|
||||
|
||||
finished = concurrent.futures.Future()
|
||||
finished.add_done_callback(lambda _: threading.Thread(target=stopGst, name="StopGst").start())
|
||||
hasFinished = False
|
||||
|
||||
appsink = gst.get_by_name('appsink')
|
||||
callbackQueue = Queue()
|
||||
asyncFuture = asyncio.Future()
|
||||
|
||||
async def gen():
|
||||
try:
|
||||
while True:
|
||||
nonlocal asyncFuture
|
||||
asyncFuture = asyncio.Future()
|
||||
yieldFuture = asyncio.Future()
|
||||
async def asyncCallback(sample):
|
||||
asyncFuture.set_result(sample)
|
||||
await yieldFuture
|
||||
callbackQueue.put(Callback(asyncCallback))
|
||||
sample = await asyncFuture
|
||||
if not sample:
|
||||
yieldFuture.set_result(None)
|
||||
break
|
||||
try:
|
||||
yield sample
|
||||
finally:
|
||||
yieldFuture.set_result(None)
|
||||
finally:
|
||||
finish()
|
||||
print('gstreamer finished')
|
||||
|
||||
|
||||
def on_new_sample(sink, preroll):
|
||||
nonlocal hasFinished
|
||||
|
||||
sample = sink.emit('pull-preroll' if preroll else 'pull-sample')
|
||||
|
||||
callback: Callback = callbackQueue.get()
|
||||
if not callback.callback or hasFinished:
|
||||
hasFinished = True
|
||||
if callback.callback:
|
||||
asyncio.run_coroutine_threadsafe(callback.callback(None), loop = callback.loop)
|
||||
return Gst.FlowReturn.OK
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(callback.callback(sample), loop = callback.loop)
|
||||
try:
|
||||
future.result()
|
||||
except:
|
||||
pass
|
||||
return Gst.FlowReturn.OK
|
||||
|
||||
appsink.connect('new-preroll', on_new_sample, True)
|
||||
appsink.connect('new-sample', on_new_sample, False)
|
||||
|
||||
gst.set_state(Gst.State.PLAYING)
|
||||
return gst, gen
|
||||
|
||||
def mainThread():
|
||||
async def asyncMain():
|
||||
gst, gen = createPipelineIterator('rtspsrc location=rtsp://localhost:59668/18cc179a814fd5b3 ! rtph264depay ! h264parse ! vtdec_hw ! videoconvert ! video/x-raw')
|
||||
i = 0
|
||||
async for sample in gen():
|
||||
print('sample')
|
||||
i = i + 1
|
||||
if i == 10:
|
||||
break
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.ensure_future(asyncMain(), loop = loop)
|
||||
loop.run_forever()
|
||||
|
||||
if __name__ == "__main__":
|
||||
threading.Thread(target = mainThread).start()
|
||||
mainLoop = GLib.MainLoop()
|
||||
mainLoop.run()
|
||||
@@ -1,132 +1,87 @@
|
||||
import concurrent.futures
|
||||
import threading
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
from gst_generator import createPipelineIterator
|
||||
from util import optional_chain
|
||||
import scrypted_sdk
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
import pyvips
|
||||
from vips import createVipsMediaObject, VipsImage
|
||||
import platform
|
||||
|
||||
Gst = None
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
gi.require_version('GstBase', '1.0')
|
||||
|
||||
from gi.repository import GLib, GObject, Gst
|
||||
GObject.threads_init()
|
||||
Gst.init(None)
|
||||
from gi.repository import Gst
|
||||
except:
|
||||
pass
|
||||
|
||||
class Callback:
|
||||
def __init__(self, callback) -> None:
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.callback = callback
|
||||
async def generateVideoFramesGstreamer(mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None, h264Decoder: str = None) -> scrypted_sdk.VideoFrame:
|
||||
ffmpegInput: scrypted_sdk.FFmpegInput = await scrypted_sdk.mediaManager.convertMediaObjectToJSON(mediaObject, scrypted_sdk.ScryptedMimeTypes.FFmpegInput.value)
|
||||
container = ffmpegInput.get('container', None)
|
||||
videosrc = ffmpegInput.get('url')
|
||||
videoCodec = optional_chain(ffmpegInput, 'mediaStreamOptions', 'video', 'codec')
|
||||
|
||||
def createPipelineIterator(pipeline: str):
|
||||
pipeline = '{pipeline} ! queue leaky=downstream max-size-buffers=0 ! appsink name=appsink emit-signals=true sync=false max-buffers=-1 drop=true'.format(pipeline=pipeline)
|
||||
print(pipeline)
|
||||
gst = Gst.parse_launch(pipeline)
|
||||
bus = gst.get_bus()
|
||||
if videosrc.startswith('tcp://'):
|
||||
parsed_url = urlparse(videosrc)
|
||||
videosrc = 'tcpclientsrc port=%s host=%s' % (
|
||||
parsed_url.port, parsed_url.hostname)
|
||||
if container == 'mpegts':
|
||||
videosrc += ' ! tsdemux'
|
||||
elif container == 'sdp':
|
||||
videosrc += ' ! sdpdemux'
|
||||
else:
|
||||
raise Exception('unknown container %s' % container)
|
||||
elif videosrc.startswith('rtsp'):
|
||||
videosrc = 'rtspsrc buffer-mode=0 location=%s protocols=tcp latency=0 is-live=false' % videosrc
|
||||
if videoCodec == 'h264':
|
||||
videosrc += ' ! rtph264depay ! h264parse'
|
||||
|
||||
def on_bus_message(bus, message):
|
||||
t = str(message.type)
|
||||
# print(t)
|
||||
if t == str(Gst.MessageType.EOS):
|
||||
finish()
|
||||
elif t == str(Gst.MessageType.WARNING):
|
||||
err, debug = message.parse_warning()
|
||||
print('Warning: %s: %s\n' % (err, debug))
|
||||
elif t == str(Gst.MessageType.ERROR):
|
||||
err, debug = message.parse_error()
|
||||
print('Error: %s: %s\n' % (err, debug))
|
||||
finish()
|
||||
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'])
|
||||
|
||||
def stopGst():
|
||||
bus.remove_signal_watch()
|
||||
bus.disconnect(watchId)
|
||||
gst.set_state(Gst.State.NULL)
|
||||
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)
|
||||
|
||||
def finish():
|
||||
nonlocal hasFinished
|
||||
hasFinished = True
|
||||
callback = Callback(None)
|
||||
callbackQueue.put(callback)
|
||||
if not asyncFuture.done():
|
||||
asyncFuture.set_result(None)
|
||||
if not finished.done():
|
||||
finished.set_result(None)
|
||||
decoder = 'decodebin'
|
||||
if videoCodec == 'h264':
|
||||
decoder = h264Decoder or 'Default'
|
||||
if decoder == 'Default':
|
||||
if platform.system() == 'Darwin':
|
||||
decoder = 'vtdec_hw'
|
||||
else:
|
||||
decoder = 'decodebin'
|
||||
|
||||
watchId = bus.connect('message', on_bus_message)
|
||||
bus.add_signal_watch()
|
||||
videosrc += ' ! {decoder} ! queue leaky=downstream max-size-buffers=0 ! videoconvert ! {videocaps}'.format(decoder=decoder, videocaps=videocaps)
|
||||
|
||||
finished = concurrent.futures.Future()
|
||||
finished.add_done_callback(lambda _: threading.Thread(target=stopGst, name="StopGst").start())
|
||||
hasFinished = False
|
||||
gst, gen = createPipelineIterator(videosrc)
|
||||
async for gstsample in gen():
|
||||
caps = gstsample.get_caps()
|
||||
height = caps.get_structure(0).get_value('height')
|
||||
width = caps.get_structure(0).get_value('width')
|
||||
gst_buffer = gstsample.get_buffer()
|
||||
result, info = gst_buffer.map(Gst.MapFlags.READ)
|
||||
if not result:
|
||||
continue
|
||||
|
||||
appsink = gst.get_by_name('appsink')
|
||||
callbackQueue = Queue()
|
||||
asyncFuture = asyncio.Future()
|
||||
|
||||
async def gen():
|
||||
try:
|
||||
while True:
|
||||
nonlocal asyncFuture
|
||||
asyncFuture = asyncio.Future()
|
||||
yieldFuture = asyncio.Future()
|
||||
async def asyncCallback(sample):
|
||||
asyncFuture.set_result(sample)
|
||||
await yieldFuture
|
||||
callbackQueue.put(Callback(asyncCallback))
|
||||
sample = await asyncFuture
|
||||
if not sample:
|
||||
yieldFuture.set_result(None)
|
||||
break
|
||||
try:
|
||||
yield sample
|
||||
finally:
|
||||
yieldFuture.set_result(None)
|
||||
finally:
|
||||
finish()
|
||||
print('gstreamer finished')
|
||||
|
||||
|
||||
def on_new_sample(sink, preroll):
|
||||
nonlocal hasFinished
|
||||
|
||||
sample = sink.emit('pull-preroll' if preroll else 'pull-sample')
|
||||
|
||||
callback: Callback = callbackQueue.get()
|
||||
if not callback.callback or hasFinished:
|
||||
hasFinished = True
|
||||
if callback.callback:
|
||||
asyncio.run_coroutine_threadsafe(callback.callback(None), loop = callback.loop)
|
||||
return Gst.FlowReturn.OK
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(callback.callback(sample), loop = callback.loop)
|
||||
try:
|
||||
future.result()
|
||||
except:
|
||||
pass
|
||||
return Gst.FlowReturn.OK
|
||||
|
||||
appsink.connect('new-preroll', on_new_sample, True)
|
||||
appsink.connect('new-sample', on_new_sample, False)
|
||||
|
||||
gst.set_state(Gst.State.PLAYING)
|
||||
return gst, gen
|
||||
|
||||
def mainThread():
|
||||
async def asyncMain():
|
||||
gst, gen = createPipelineIterator('rtspsrc location=rtsp://localhost:59668/18cc179a814fd5b3 ! rtph264depay ! h264parse ! vtdec_hw ! videoconvert ! video/x-raw')
|
||||
i = 0
|
||||
async for sample in gen():
|
||||
print('sample')
|
||||
i = i + 1
|
||||
if i == 10:
|
||||
break
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.ensure_future(asyncMain(), loop = loop)
|
||||
loop.run_forever()
|
||||
|
||||
if __name__ == "__main__":
|
||||
threading.Thread(target = mainThread).start()
|
||||
mainLoop = GLib.MainLoop()
|
||||
mainLoop.run()
|
||||
vips = pyvips.Image.new_from_memory(info.data, width, height, bands, pyvips.BandFormat.UCHAR)
|
||||
vipsImage = VipsImage(vips)
|
||||
try:
|
||||
mo = await createVipsMediaObject(VipsImage(vips))
|
||||
yield mo
|
||||
finally:
|
||||
vipsImage.vipsImage.invalidate()
|
||||
vipsImage.vipsImage = None
|
||||
finally:
|
||||
gst_buffer.unmap(info)
|
||||
|
||||
51
plugins/python-codecs/src/libav.py
Normal file
51
plugins/python-codecs/src/libav.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import time
|
||||
from gst_generator import createPipelineIterator
|
||||
import scrypted_sdk
|
||||
from typing import Any
|
||||
import pyvips
|
||||
from vips import createVipsMediaObject, VipsImage
|
||||
|
||||
av = None
|
||||
try:
|
||||
import av
|
||||
av.logging.set_level(av.logging.PANIC)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def generateVideoFramesLibav(mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
ffmpegInput: scrypted_sdk.FFmpegInput = await scrypted_sdk.mediaManager.convertMediaObjectToJSON(mediaObject, scrypted_sdk.ScryptedMimeTypes.FFmpegInput.value)
|
||||
videosrc = ffmpegInput.get('url')
|
||||
container = av.open(videosrc)
|
||||
# none of this stuff seems to work. might be libav being slow with rtsp.
|
||||
# container.no_buffer = True
|
||||
# container.gen_pts = False
|
||||
# container.options['-analyzeduration'] = '0'
|
||||
# container.options['-probesize'] = '500000'
|
||||
stream = container.streams.video[0]
|
||||
# stream.codec_context.thread_count = 1
|
||||
# stream.codec_context.low_delay = True
|
||||
# stream.codec_context.options['-analyzeduration'] = '0'
|
||||
# stream.codec_context.options['-probesize'] = '500000'
|
||||
|
||||
start = 0
|
||||
try:
|
||||
for idx, frame in enumerate(container.decode(stream)):
|
||||
now = time.time()
|
||||
if not start:
|
||||
start = now
|
||||
elapsed = now - start
|
||||
if (frame.time or 0) < elapsed - 0.500:
|
||||
# print('too slow, skipping frame')
|
||||
continue
|
||||
# print(frame)
|
||||
vips = pyvips.Image.new_from_array(frame.to_ndarray(format='rgb24'))
|
||||
vipsImage = VipsImage(vips)
|
||||
try:
|
||||
mo = await createVipsMediaObject(VipsImage(vips))
|
||||
yield mo
|
||||
finally:
|
||||
vipsImage.vipsImage.invalidate()
|
||||
vipsImage.vipsImage = None
|
||||
|
||||
finally:
|
||||
container.close()
|
||||
@@ -1,165 +1,113 @@
|
||||
from gstreamer import createPipelineIterator
|
||||
import asyncio
|
||||
from util import optional_chain
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk import Setting, SettingValue
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
import pyvips
|
||||
import threading
|
||||
import traceback
|
||||
import concurrent.futures
|
||||
import gstreamer
|
||||
import libav
|
||||
|
||||
Gst = None
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
gi.require_version('GstBase', '1.0')
|
||||
|
||||
from gi.repository import Gst
|
||||
except:
|
||||
pass
|
||||
|
||||
vipsExecutor = concurrent.futures.ThreadPoolExecutor(max_workers=2, thread_name_prefix="vips")
|
||||
av = None
|
||||
try:
|
||||
import av
|
||||
except:
|
||||
pass
|
||||
|
||||
async def to_thread(f):
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(vipsExecutor, f)
|
||||
class LibavGenerator(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.VideoFrameGenerator):
|
||||
async def generateVideoFrames(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
worker = scrypted_sdk.fork()
|
||||
forked: CodecFork = await worker.result
|
||||
return await forked.generateVideoFramesLibav(mediaObject, options, filter)
|
||||
|
||||
class VipsImage(scrypted_sdk.VideoFrame):
|
||||
def __init__(self, vipsImage: pyvips.Image) -> None:
|
||||
super().__init__()
|
||||
self.vipsImage = vipsImage
|
||||
self.width = vipsImage.width
|
||||
self.height = vipsImage.height
|
||||
|
||||
async def toBuffer(self, options: scrypted_sdk.ImageOptions = None) -> bytearray:
|
||||
vipsImage: VipsImage = await self.toVipsImage(options)
|
||||
|
||||
if not options or not options.get('format', None):
|
||||
def format():
|
||||
return memoryview(vipsImage.vipsImage.write_to_memory())
|
||||
return await to_thread(format)
|
||||
elif options['format'] == 'rgb':
|
||||
def format():
|
||||
if vipsImage.vipsImage.hasalpha():
|
||||
rgb = vipsImage.vipsImage.extract_band(0, vipsImage.vipsImage.bands - 1)
|
||||
else:
|
||||
rgb = vipsImage.vipsImage
|
||||
mem = memoryview(rgb.write_to_memory())
|
||||
return mem
|
||||
return await to_thread(format)
|
||||
|
||||
return await to_thread(lambda: vipsImage.vipsImage.write_to_buffer('.' + options['format']))
|
||||
|
||||
async def toVipsImage(self, options: scrypted_sdk.ImageOptions = None):
|
||||
return await to_thread(lambda: toVipsImage(self, options))
|
||||
|
||||
async def toImage(self, options: scrypted_sdk.ImageOptions = None) -> Any:
|
||||
if options and options.get('format', None):
|
||||
raise Exception('format can only be used with toBuffer')
|
||||
newVipsImage = await self.toVipsImage(options)
|
||||
return await createVipsMediaObject(newVipsImage)
|
||||
|
||||
def toVipsImage(vipsImageWrapper: VipsImage, options: scrypted_sdk.ImageOptions = None) -> VipsImage:
|
||||
vipsImage = vipsImageWrapper.vipsImage
|
||||
if not vipsImage:
|
||||
raise Exception('Video Frame has been invalidated')
|
||||
options = options or {}
|
||||
crop = options.get('crop')
|
||||
if crop:
|
||||
vipsImage = vipsImage.crop(int(crop['left']), int(crop['top']), int(crop['width']), int(crop['height']))
|
||||
|
||||
resize = options.get('resize')
|
||||
if resize:
|
||||
xscale = None
|
||||
if resize.get('width'):
|
||||
xscale = resize['width'] / vipsImage.width
|
||||
scale = xscale
|
||||
yscale = None
|
||||
if resize.get('height'):
|
||||
yscale = resize['height'] / vipsImage.height
|
||||
scale = yscale
|
||||
|
||||
if xscale and yscale:
|
||||
scale = min(yscale, xscale)
|
||||
|
||||
xscale = xscale or yscale
|
||||
yscale = yscale or xscale
|
||||
vipsImage = vipsImage.resize(xscale, vscale=yscale, kernel='linear')
|
||||
|
||||
return VipsImage(vipsImage)
|
||||
|
||||
async def createVipsMediaObject(image: VipsImage):
|
||||
ret = await scrypted_sdk.mediaManager.createMediaObject(image, scrypted_sdk.ScryptedMimeTypes.Image.value, {
|
||||
'width': image.width,
|
||||
'height': image.height,
|
||||
'toBuffer': lambda options = None: image.toBuffer(options),
|
||||
'toImage': lambda options = None: image.toImage(options),
|
||||
})
|
||||
return ret
|
||||
|
||||
class PythonCodecs(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.VideoFrameGenerator):
|
||||
class GstreamerGenerator(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.VideoFrameGenerator, scrypted_sdk.Settings):
|
||||
async def generateVideoFrames(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
worker = scrypted_sdk.fork()
|
||||
forked: CodecFork = await worker.result
|
||||
return await forked.generateVideoFramesGstreamer(mediaObject, options, filter, self.storage.getItem('h264Decoder'))
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
return [
|
||||
{
|
||||
'key': 'h264Decoder',
|
||||
'title': 'H264 Decoder',
|
||||
'description': 'The Gstreamer pipeline to use to decode H264 video.',
|
||||
'value': self.storage.getItem('h264Decoder') or 'Default',
|
||||
'choices': [
|
||||
'Default',
|
||||
'decodebin',
|
||||
'vtdec_hw',
|
||||
'nvh264dec',
|
||||
'vaapih264dec',
|
||||
],
|
||||
'combobox': True,
|
||||
}
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
self.storage.setItem(key, value)
|
||||
await scrypted_sdk.deviceManager.onDeviceEvent(self.nativeId, scrypted_sdk.ScryptedInterface.Settings.value, None)
|
||||
|
||||
class PythonCodecs(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.DeviceProvider):
|
||||
def __init__(self, nativeId = None):
|
||||
super().__init__(nativeId)
|
||||
|
||||
async def generateVideoFrames(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
worker = scrypted_sdk.fork()
|
||||
forked = await worker.result
|
||||
return await forked.generateVideoFrames(mediaObject, options, filter)
|
||||
asyncio.ensure_future(self.initialize())
|
||||
|
||||
async def initialize(self):
|
||||
manifest: scrypted_sdk.DeviceManifest = {
|
||||
'devices': [],
|
||||
}
|
||||
if Gst:
|
||||
gstDevice: scrypted_sdk.Device = {
|
||||
'name': 'Gstreamer',
|
||||
'nativeId': 'gstreamer',
|
||||
'interfaces': [
|
||||
scrypted_sdk.ScryptedInterface.VideoFrameGenerator.value,
|
||||
scrypted_sdk.ScryptedInterface.Settings.value,
|
||||
],
|
||||
'type': scrypted_sdk.ScryptedDeviceType.API.value,
|
||||
}
|
||||
manifest['devices'].append(gstDevice)
|
||||
|
||||
if av:
|
||||
avDevice: scrypted_sdk.Device = {
|
||||
'name': 'Libav',
|
||||
'nativeId': 'libav',
|
||||
'interfaces': [
|
||||
scrypted_sdk.ScryptedInterface.VideoFrameGenerator.value,
|
||||
],
|
||||
'type': scrypted_sdk.ScryptedDeviceType.API.value,
|
||||
}
|
||||
manifest['devices'].append(avDevice)
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged(manifest)
|
||||
|
||||
def getDevice(self, nativeId: str) -> Any:
|
||||
if nativeId == 'gstreamer':
|
||||
return GstreamerGenerator('gstreamer')
|
||||
if nativeId == 'libav':
|
||||
return LibavGenerator('libav')
|
||||
|
||||
def create_scrypted_plugin():
|
||||
return PythonCodecs()
|
||||
|
||||
|
||||
async def generateVideoFrames(mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
ffmpegInput: scrypted_sdk.FFmpegInput = await scrypted_sdk.mediaManager.convertMediaObjectToJSON(mediaObject, scrypted_sdk.ScryptedMimeTypes.FFmpegInput.value)
|
||||
container = ffmpegInput.get('container', None)
|
||||
videosrc = ffmpegInput.get('url')
|
||||
videoCodec = optional_chain(ffmpegInput, 'mediaStreamOptions', 'video', 'codec')
|
||||
|
||||
if videosrc.startswith('tcp://'):
|
||||
parsed_url = urlparse(videosrc)
|
||||
videosrc = 'tcpclientsrc port=%s host=%s' % (
|
||||
parsed_url.port, parsed_url.hostname)
|
||||
if container == 'mpegts':
|
||||
videosrc += ' ! tsdemux'
|
||||
elif container == 'sdp':
|
||||
videosrc += ' ! sdpdemux'
|
||||
else:
|
||||
raise Exception('unknown container %s' % container)
|
||||
elif videosrc.startswith('rtsp'):
|
||||
videosrc = 'rtspsrc buffer-mode=0 location=%s protocols=tcp latency=0 is-live=false' % videosrc
|
||||
if videoCodec == 'h264':
|
||||
videosrc += ' ! rtph264depay ! h264parse'
|
||||
|
||||
videosrc += ' ! decodebin ! queue leaky=downstream max-size-buffers=0 ! videoconvert ! video/x-raw,format=RGB'
|
||||
|
||||
gst, gen = createPipelineIterator(videosrc)
|
||||
async for gstsample in gen():
|
||||
caps = gstsample.get_caps()
|
||||
height = caps.get_structure(0).get_value('height')
|
||||
width = caps.get_structure(0).get_value('width')
|
||||
gst_buffer = gstsample.get_buffer()
|
||||
result, info = gst_buffer.map(Gst.MapFlags.READ)
|
||||
if not result:
|
||||
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)
|
||||
vipsImage = VipsImage(vips)
|
||||
try:
|
||||
mo = await createVipsMediaObject(VipsImage(vips))
|
||||
yield mo
|
||||
finally:
|
||||
vipsImage.vipsImage.invalidate()
|
||||
vipsImage.vipsImage = None
|
||||
finally:
|
||||
gst_buffer.unmap(info)
|
||||
|
||||
class CodecFork:
|
||||
async def generateVideoFrames(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
async def generateVideoFramesGstreamer(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None, h264Decoder: str = None) -> scrypted_sdk.VideoFrame:
|
||||
try:
|
||||
async for data in generateVideoFrames(mediaObject, options, filter):
|
||||
async for data in gstreamer.generateVideoFramesGstreamer(mediaObject, options, filter, h264Decoder):
|
||||
yield data
|
||||
finally:
|
||||
import os
|
||||
os._exit(os.EX_OK)
|
||||
pass
|
||||
|
||||
async def generateVideoFramesLibav(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
|
||||
try:
|
||||
async for data in libav.generateVideoFramesLibav(mediaObject, options, filter):
|
||||
yield data
|
||||
finally:
|
||||
import os
|
||||
|
||||
89
plugins/python-codecs/src/vips.py
Normal file
89
plugins/python-codecs/src/vips.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import time
|
||||
from gst_generator import createPipelineIterator
|
||||
import asyncio
|
||||
from util import optional_chain
|
||||
import scrypted_sdk
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
import pyvips
|
||||
import concurrent.futures
|
||||
|
||||
# vips is already multithreaded, but needs to be kicked off the python asyncio thread.
|
||||
vipsExecutor = concurrent.futures.ThreadPoolExecutor(max_workers=2, thread_name_prefix="vips")
|
||||
|
||||
async def to_thread(f):
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(vipsExecutor, f)
|
||||
|
||||
class VipsImage(scrypted_sdk.VideoFrame):
|
||||
def __init__(self, vipsImage: pyvips.Image) -> None:
|
||||
super().__init__()
|
||||
self.vipsImage = vipsImage
|
||||
self.width = vipsImage.width
|
||||
self.height = vipsImage.height
|
||||
|
||||
async def toBuffer(self, options: scrypted_sdk.ImageOptions = None) -> bytearray:
|
||||
vipsImage: VipsImage = await self.toVipsImage(options)
|
||||
|
||||
if not options or not options.get('format', None):
|
||||
def format():
|
||||
return memoryview(vipsImage.vipsImage.write_to_memory())
|
||||
return await to_thread(format)
|
||||
elif options['format'] == 'rgb':
|
||||
def format():
|
||||
if vipsImage.vipsImage.hasalpha():
|
||||
rgb = vipsImage.vipsImage.extract_band(0, vipsImage.vipsImage.bands - 1)
|
||||
else:
|
||||
rgb = vipsImage.vipsImage
|
||||
mem = memoryview(rgb.write_to_memory())
|
||||
return mem
|
||||
return await to_thread(format)
|
||||
|
||||
return await to_thread(lambda: vipsImage.vipsImage.write_to_buffer('.' + options['format']))
|
||||
|
||||
async def toVipsImage(self, options: scrypted_sdk.ImageOptions = None):
|
||||
return await to_thread(lambda: toVipsImage(self, options))
|
||||
|
||||
async def toImage(self, options: scrypted_sdk.ImageOptions = None) -> Any:
|
||||
if options and options.get('format', None):
|
||||
raise Exception('format can only be used with toBuffer')
|
||||
newVipsImage = await self.toVipsImage(options)
|
||||
return await createVipsMediaObject(newVipsImage)
|
||||
|
||||
def toVipsImage(vipsImageWrapper: VipsImage, options: scrypted_sdk.ImageOptions = None) -> VipsImage:
|
||||
vipsImage = vipsImageWrapper.vipsImage
|
||||
if not vipsImage:
|
||||
raise Exception('Video Frame has been invalidated')
|
||||
options = options or {}
|
||||
crop = options.get('crop')
|
||||
if crop:
|
||||
vipsImage = vipsImage.crop(int(crop['left']), int(crop['top']), int(crop['width']), int(crop['height']))
|
||||
|
||||
resize = options.get('resize')
|
||||
if resize:
|
||||
xscale = None
|
||||
if resize.get('width'):
|
||||
xscale = resize['width'] / vipsImage.width
|
||||
scale = xscale
|
||||
yscale = None
|
||||
if resize.get('height'):
|
||||
yscale = resize['height'] / vipsImage.height
|
||||
scale = yscale
|
||||
|
||||
if xscale and yscale:
|
||||
scale = min(yscale, xscale)
|
||||
|
||||
xscale = xscale or yscale
|
||||
yscale = yscale or xscale
|
||||
vipsImage = vipsImage.resize(xscale, vscale=yscale, kernel='linear')
|
||||
|
||||
return VipsImage(vipsImage)
|
||||
|
||||
async def createVipsMediaObject(image: VipsImage):
|
||||
ret = await scrypted_sdk.mediaManager.createMediaObject(image, scrypted_sdk.ScryptedMimeTypes.Image.value, {
|
||||
'width': image.width,
|
||||
'height': image.height,
|
||||
'toBuffer': lambda options = None: image.toBuffer(options),
|
||||
'toImage': lambda options = None: image.toImage(options),
|
||||
})
|
||||
return ret
|
||||
@@ -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
|
||||
|
||||
14
plugins/ring/package-lock.json
generated
14
plugins/ring/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright 2019 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
mkdir -p all_models
|
||||
wget https://dl.google.com/coral/canned_models/all_models.tar.gz
|
||||
tar -C all_models -xvzf all_models.tar.gz
|
||||
rm -f all_models.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
../all_models/coco_labels.txt
|
||||
@@ -1 +0,0 @@
|
||||
../all_models/mobilenet_ssd_v2_coco_quant_postprocess.tflite
|
||||
@@ -1 +0,0 @@
|
||||
../all_models/mobilenet_ssd_v2_coco_quant_postprocess_edgetpu.tflite
|
||||
4
plugins/tensorflow-lite/package-lock.json
generated
4
plugins/tensorflow-lite/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.0.112",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.0.112",
|
||||
"version": "0.1.2",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.112"
|
||||
"version": "0.1.2"
|
||||
}
|
||||
|
||||
@@ -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,14 +210,14 @@ 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
|
||||
|
||||
def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
|
||||
async def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
|
||||
pil: Image.Image = avframe.to_image()
|
||||
return self.run_detection_image(detection_session, pil, settings, src_size, convert_to_src_size)
|
||||
return await self.run_detection_image(detection_session, pil, settings, src_size, convert_to_src_size)
|
||||
|
||||
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]:
|
||||
pass
|
||||
|
||||
def run_detection_crop(self, detection_session: DetectionSession, sample: Any, settings: Any, src_size, convert_to_src_size, bounding_box: Tuple[float, float, float, float]) -> ObjectsDetected:
|
||||
@@ -288,17 +292,21 @@ 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,
|
||||
}
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
await videoFrames.aclose()
|
||||
try:
|
||||
await videoFrames.aclose()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def detectObjects(self, mediaObject: MediaObject, session: ObjectDetectionSession = None, callbacks: ObjectDetectionCallbacks = None) -> ObjectsDetected:
|
||||
is_image = mediaObject and (mediaObject.mimeType.startswith('image/') or mediaObject.mimeType.endswith('/x-raw-image'))
|
||||
@@ -335,7 +343,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
|
||||
finally:
|
||||
detection_session.running = False
|
||||
else:
|
||||
return self.run_detection_jpeg(detection_session, bytes(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(mediaObject, 'image/jpeg')), settings)
|
||||
return await self.run_detection_jpeg(detection_session, bytes(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(mediaObject, 'image/jpeg')), settings)
|
||||
|
||||
if not create:
|
||||
# a detection session may have been created, but not started
|
||||
@@ -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:
|
||||
@@ -479,7 +487,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
|
||||
if not current_data:
|
||||
raise Exception('no sample')
|
||||
|
||||
detection_result = self.run_detection_crop(
|
||||
detection_result = await self.run_detection_crop(
|
||||
detection_session, current_data, detection_session.settings, current_src_size, current_convert_to_src_size, boundingBox)
|
||||
|
||||
return detection_result['detections']
|
||||
@@ -493,7 +501,7 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
|
||||
first_frame = False
|
||||
print("first frame received", detection_session.id)
|
||||
|
||||
detection_result, data = run_detection(
|
||||
detection_result, data = await run_detection(
|
||||
detection_session, sample, detection_session.settings, src_size, convert_to_src_size)
|
||||
if detection_result:
|
||||
detection_result['running'] = True
|
||||
@@ -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,
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any, List, Tuple, Mapping
|
||||
import asyncio
|
||||
import time
|
||||
from .rectangle import Rectangle, intersect_area, intersect_rect, to_bounding_box, from_bounding_box, combine_rect
|
||||
import urllib.request
|
||||
import os
|
||||
|
||||
from detect import DetectionSession, DetectPlugin
|
||||
|
||||
@@ -126,6 +128,17 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.call_later(4 * 60 * 60, lambda: self.requestRestart())
|
||||
|
||||
def downloadFile(self, url: str, filename: str):
|
||||
filesPath = os.path.join(os.environ['SCRYPTED_PLUGIN_VOLUME'], 'files')
|
||||
fullpath = os.path.join(filesPath, filename)
|
||||
if os.path.isfile(fullpath):
|
||||
return fullpath
|
||||
os.makedirs(filesPath, exist_ok=True)
|
||||
tmp = fullpath + '.tmp'
|
||||
urllib.request.urlretrieve(url, tmp)
|
||||
os.rename(tmp, fullpath)
|
||||
return fullpath
|
||||
|
||||
def getClasses(self) -> list[str]:
|
||||
return list(self.labels.values())
|
||||
|
||||
@@ -250,13 +263,13 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
# print(detection_result)
|
||||
return detection_result
|
||||
|
||||
def run_detection_jpeg(self, detection_session: PredictSession, image_bytes: bytes, settings: Any) -> ObjectsDetected:
|
||||
async def run_detection_jpeg(self, detection_session: PredictSession, image_bytes: bytes, settings: Any) -> ObjectsDetected:
|
||||
stream = io.BytesIO(image_bytes)
|
||||
image = Image.open(stream)
|
||||
if image.mode == 'RGBA':
|
||||
image = image.convert('RGB')
|
||||
|
||||
detections, _ = self.run_detection_image(detection_session, image, settings, image.size)
|
||||
detections, _ = await self.run_detection_image(detection_session, image, settings, image.size)
|
||||
return detections
|
||||
|
||||
def get_detection_input_size(self, src_size):
|
||||
@@ -269,10 +282,11 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
pass
|
||||
|
||||
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
|
||||
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 and detection_session.settings
|
||||
src_size = videoFrame.width, videoFrame.height
|
||||
w, h = self.get_input_size()
|
||||
iw, ih = src_size
|
||||
@@ -288,7 +302,7 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
})
|
||||
image = Image.frombuffer('RGB', (w, h), data)
|
||||
try:
|
||||
ret = self.detect_once(image, settings, src_size, cvss)
|
||||
ret = await self.detect_once(image, settings, src_size, cvss)
|
||||
return ret
|
||||
finally:
|
||||
image.close()
|
||||
@@ -339,9 +353,9 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
def cvss2(point, normalize=False):
|
||||
return point[0] / s + ow, point[1] / s + oh, True
|
||||
|
||||
ret1 = self.detect_once(first, settings, src_size, cvss1)
|
||||
ret1 = await self.detect_once(first, settings, src_size, cvss1)
|
||||
first.close()
|
||||
ret2 = self.detect_once(second, settings, src_size, cvss2)
|
||||
ret2 = await self.detect_once(second, settings, src_size, cvss2)
|
||||
second.close()
|
||||
|
||||
two_intersect = intersect_rect(Rectangle(*first_crop), Rectangle(*second_crop))
|
||||
@@ -374,7 +388,7 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
ret['detections'] = dedupe_detections(ret1['detections'] + ret2['detections'], is_same_detection=is_same_detection_middle)
|
||||
return ret
|
||||
|
||||
def run_detection_image(self, detection_session: PredictSession, image: Image.Image, settings: Any, src_size, convert_to_src_size: Any = None, multipass_crop: Tuple[float, float, float, float] = None):
|
||||
async def run_detection_image(self, detection_session: PredictSession, image: Image.Image, settings: Any, src_size, convert_to_src_size: Any = None, multipass_crop: Tuple[float, float, float, float] = None):
|
||||
(w, h) = self.get_input_size() or image.size
|
||||
(iw, ih) = image.size
|
||||
|
||||
@@ -448,7 +462,7 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
converted = convert_to_src_size(unscaled, normalize) if convert_to_src_size else (unscaled[0], unscaled[1], True)
|
||||
return converted
|
||||
|
||||
ret = self.detect_once(input, settings, src_size, cvss)
|
||||
ret = await self.detect_once(input, settings, src_size, cvss)
|
||||
input.close()
|
||||
detection_session.processed = detection_session.processed + 1
|
||||
return ret, RawImage(image)
|
||||
@@ -461,7 +475,7 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
converted = convert_to_src_size(point, normalize) if convert_to_src_size else (point[0], point[1], True)
|
||||
return converted
|
||||
|
||||
ret = self.detect_once(image, settings, src_size, cvss)
|
||||
ret = await self.detect_once(image, settings, src_size, cvss)
|
||||
if detection_session:
|
||||
detection_session.processed = detection_session.processed + 1
|
||||
else:
|
||||
@@ -483,11 +497,11 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
converted = convert_to_src_size(unscaled, normalize) if convert_to_src_size else (unscaled[0], unscaled[1], True)
|
||||
return converted
|
||||
|
||||
ret1 = self.detect_once(first, settings, src_size, cvss1)
|
||||
ret1 = await self.detect_once(first, settings, src_size, cvss1)
|
||||
first.close()
|
||||
if detection_session:
|
||||
detection_session.processed = detection_session.processed + 1
|
||||
ret2 = self.detect_once(second, settings, src_size, cvss2)
|
||||
ret2 = await self.detect_once(second, settings, src_size, cvss2)
|
||||
if detection_session:
|
||||
detection_session.processed = detection_session.processed + 1
|
||||
second.close()
|
||||
@@ -576,11 +590,11 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
# print('untracked %s: %s' % (d['className'], d['score']))
|
||||
|
||||
|
||||
def run_detection_crop(self, detection_session: DetectionSession, sample: RawImage, settings: Any, src_size, convert_to_src_size, bounding_box: Tuple[float, float, float, float]) -> ObjectsDetected:
|
||||
(ret, _) = self.run_detection_image(detection_session, sample.image, settings, src_size, convert_to_src_size, bounding_box)
|
||||
async def run_detection_crop(self, detection_session: DetectionSession, sample: RawImage, settings: Any, src_size, convert_to_src_size, bounding_box: Tuple[float, float, float, float]) -> ObjectsDetected:
|
||||
(ret, _) = await self.run_detection_image(detection_session, sample.image, settings, src_size, convert_to_src_size, bounding_box)
|
||||
return ret
|
||||
|
||||
def run_detection_gstsample(self, detection_session: PredictSession, gstsample, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Image.Image]:
|
||||
async def run_detection_gstsample(self, detection_session: PredictSession, gstsample, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Image.Image]:
|
||||
caps = gstsample.get_caps()
|
||||
# can't trust the width value, compute the stride
|
||||
height = caps.get_structure(0).get_value('height')
|
||||
@@ -604,7 +618,7 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
gst_buffer.unmap(info)
|
||||
|
||||
try:
|
||||
return self.run_detection_image(detection_session, image, settings, src_size, convert_to_src_size)
|
||||
return await self.run_detection_image(detection_session, image, settings, src_size, convert_to_src_size)
|
||||
except:
|
||||
image.close()
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -3,7 +3,6 @@ import threading
|
||||
from .common import *
|
||||
from PIL import Image
|
||||
from pycoral.adapters import detect
|
||||
from pycoral.adapters.common import input_size
|
||||
loaded_py_coral = False
|
||||
try:
|
||||
from pycoral.utils.edgetpu import list_edge_tpus
|
||||
@@ -19,6 +18,9 @@ import scrypted_sdk
|
||||
from scrypted_sdk.types import Setting
|
||||
from typing import Any, Tuple
|
||||
from predict import PredictPlugin
|
||||
import concurrent.futures
|
||||
import queue
|
||||
import asyncio
|
||||
|
||||
def parse_label_contents(contents: str):
|
||||
lines = contents.splitlines()
|
||||
@@ -38,9 +40,15 @@ class TensorFlowLitePlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(MIME_TYPE, nativeId=nativeId)
|
||||
|
||||
labels_contents = scrypted_sdk.zip.open(
|
||||
'fs/coco_labels.txt').read().decode('utf8')
|
||||
tfliteFile = self.downloadFile('https://raw.githubusercontent.com/google-coral/test_data/master/ssd_mobilenet_v2_coco_quant_postprocess.tflite', 'ssd_mobilenet_v2_coco_quant_postprocess.tflite')
|
||||
edgetpuFile = self.downloadFile('https://raw.githubusercontent.com/google-coral/test_data/master/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite', 'ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite')
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/google-coral/test_data/master/coco_labels.txt', 'coco_labels.txt')
|
||||
|
||||
labels_contents = open(labelsFile, 'r').read()
|
||||
self.labels = parse_label_contents(labels_contents)
|
||||
self.interpreters = queue.Queue()
|
||||
self.interpreter_count = 0
|
||||
|
||||
try:
|
||||
edge_tpus = list_edge_tpus()
|
||||
print('edge tpus', edge_tpus)
|
||||
@@ -49,23 +57,39 @@ class TensorFlowLitePlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted
|
||||
self.edge_tpu_found = str(edge_tpus)
|
||||
# todo co-compile
|
||||
# https://coral.ai/docs/edgetpu/compiler/#co-compiling-multiple-models
|
||||
model = scrypted_sdk.zip.open(
|
||||
'fs/mobilenet_ssd_v2_coco_quant_postprocess_edgetpu.tflite').read()
|
||||
# face_model = scrypted_sdk.zip.open(
|
||||
# 'fs/mobilenet_ssd_v2_face_quant_postprocess.tflite').read()
|
||||
self.interpreter = make_interpreter(model)
|
||||
for idx, edge_tpu in enumerate(edge_tpus):
|
||||
try:
|
||||
interpreter = make_interpreter(edgetpuFile, ":%s" % idx)
|
||||
interpreter.allocate_tensors()
|
||||
_, height, width, channels = interpreter.get_input_details()[
|
||||
0]['shape']
|
||||
self.input_details = int(width), int(height), int(channels)
|
||||
self.interpreters.put(interpreter)
|
||||
self.interpreter_count = self.interpreter_count + 1
|
||||
print('added tpu %s' % (edge_tpu))
|
||||
except Exception as e:
|
||||
print('unable to use Coral Edge TPU', e)
|
||||
|
||||
if not self.interpreter_count:
|
||||
raise Exception('all tpus failed to load')
|
||||
# self.face_interpreter = make_interpreter(face_model)
|
||||
except Exception as e:
|
||||
print('unable to use Coral Edge TPU', e)
|
||||
self.edge_tpu_found = 'Edge TPU not found'
|
||||
model = scrypted_sdk.zip.open(
|
||||
'fs/mobilenet_ssd_v2_coco_quant_postprocess.tflite').read()
|
||||
# face_model = scrypted_sdk.zip.open(
|
||||
# 'fs/mobilenet_ssd_v2_face_quant_postprocess.tflite').read()
|
||||
self.interpreter = tflite.Interpreter(model_content=model)
|
||||
interpreter = tflite.Interpreter(model_path=tfliteFile)
|
||||
interpreter.allocate_tensors()
|
||||
_, height, width, channels = interpreter.get_input_details()[
|
||||
0]['shape']
|
||||
self.input_details = int(width), int(height), int(channels)
|
||||
self.interpreters.put(interpreter)
|
||||
self.interpreter_count = self.interpreter_count + 1
|
||||
# self.face_interpreter = make_interpreter(face_model)
|
||||
self.interpreter.allocate_tensors()
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.interpreter_count, thread_name_prefix="tflite", )
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
ret = await super().getSettings()
|
||||
@@ -83,30 +107,32 @@ class TensorFlowLitePlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted
|
||||
|
||||
# width, height, channels
|
||||
def get_input_details(self) -> Tuple[int, int, int]:
|
||||
with self.mutex:
|
||||
_, height, width, channels = self.interpreter.get_input_details()[
|
||||
0]['shape']
|
||||
return int(width), int(height), int(channels)
|
||||
return self.input_details
|
||||
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
w, h = input_size(self.interpreter)
|
||||
return int(w), int(h)
|
||||
return self.input_details[0:2]
|
||||
|
||||
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
try:
|
||||
with self.mutex:
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def predict():
|
||||
interpreter = self.interpreters.get()
|
||||
try:
|
||||
common.set_input(
|
||||
self.interpreter, input)
|
||||
interpreter, input)
|
||||
scale = (1, 1)
|
||||
# _, scale = common.set_resized_input(
|
||||
# self.interpreter, cropped.size, lambda size: cropped.resize(size, Image.ANTIALIAS))
|
||||
self.interpreter.invoke()
|
||||
interpreter.invoke()
|
||||
objs = detect.get_objects(
|
||||
self.interpreter, score_threshold=.2, image_scale=scale)
|
||||
except:
|
||||
print('tensorflow-lite encountered an error while detecting. requesting plugin restart.')
|
||||
self.requestRestart()
|
||||
raise e
|
||||
interpreter, score_threshold=.2, image_scale=scale)
|
||||
return objs
|
||||
except:
|
||||
print('tensorflow-lite encountered an error while detecting. requesting plugin restart.')
|
||||
self.requestRestart()
|
||||
raise e
|
||||
finally:
|
||||
self.interpreters.put(interpreter)
|
||||
|
||||
objs = await asyncio.get_event_loop().run_in_executor(self.executor, predict)
|
||||
|
||||
allowList = settings.get('allowList', None) if settings else None
|
||||
ret = self.create_detection_result(objs, src_size, allowList, cvss)
|
||||
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.36",
|
||||
"version": "0.1.37",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.36",
|
||||
"version": "0.1.37",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.1.36",
|
||||
"version": "0.1.37",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -115,7 +115,8 @@ class WebRTCMixin extends SettingsMixinDeviceBase<RTCSignalingClient & VideoCame
|
||||
const device = systemManager.getDeviceById<VideoCamera & Intercom>(this.id);
|
||||
const hasIntercom = this.mixinDeviceInterfaces.includes(ScryptedInterface.Intercom);
|
||||
|
||||
const mo = await sdk.mediaManager.createMediaObject(device, ScryptedMimeTypes.ScryptedDevice, {
|
||||
const requestMediaStream: RequestMediaStream = async options => device.getVideoStream(options);
|
||||
const mo = await mediaManager.createMediaObject(requestMediaStream, ScryptedMimeTypes.RequestMediaStream, {
|
||||
sourceId: device.id,
|
||||
});
|
||||
|
||||
@@ -126,7 +127,7 @@ class WebRTCMixin extends SettingsMixinDeviceBase<RTCSignalingClient & VideoCame
|
||||
mo,
|
||||
this.plugin.storageSettings.values.maximumCompatibilityMode,
|
||||
this.plugin.getRTCConfiguration(),
|
||||
await this.plugin.getWeriftConfiguration(),
|
||||
await this.plugin.getWeriftConfiguration(options?.disableTurn),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -409,7 +410,7 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
|
||||
};
|
||||
}
|
||||
|
||||
async getWeriftConfiguration(): Promise<Partial<PeerConfig>> {
|
||||
async getWeriftConfiguration(disableTurn?: boolean): Promise<Partial<PeerConfig>> {
|
||||
let ret: Partial<PeerConfig>;
|
||||
if (this.storageSettings.values.weriftConfiguration) {
|
||||
try {
|
||||
@@ -420,7 +421,7 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
|
||||
}
|
||||
}
|
||||
|
||||
const iceServers = this.storageSettings.values.useTurnServer
|
||||
const iceServers = this.storageSettings.values.useTurnServer && !disableTurn
|
||||
? [weriftStunServer, weriftTurnServer]
|
||||
: [weriftStunServer];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1942,7 +1943,14 @@ export interface RTCSignalingOptions {
|
||||
*/
|
||||
offer?: RTCSessionDescriptionInit;
|
||||
requiresOffer?: boolean;
|
||||
/**
|
||||
* Disables trickle ICE. All candidates must be sent in the initial offer/answer sdp.
|
||||
*/
|
||||
disableTrickle?: boolean;
|
||||
/**
|
||||
* Disables usage of TURN servers, if this client exposes public addresses or provides its own.
|
||||
*/
|
||||
disableTurn?: boolean;
|
||||
/**
|
||||
* Hint to proxy the feed, as the target client may be inflexible.
|
||||
*/
|
||||
|
||||
2
server/.vscode/launch.json
vendored
2
server/.vscode/launch.json
vendored
@@ -28,7 +28,7 @@
|
||||
"${workspaceFolder}/**/*.js"
|
||||
],
|
||||
"env": {
|
||||
"SCRYPTED_PYTHON_PATH": "python3.9",
|
||||
"SCRYPTED_PYTHON_PATH": "python3.10",
|
||||
// "SCRYPTED_SHARED_WORKER": "true",
|
||||
// "SCRYPTED_DISABLE_AUTHENTICATION": "true",
|
||||
// "DEBUG": "*",
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.8",
|
||||
"version": "0.7.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.8",
|
||||
"version": "0.7.12",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.9",
|
||||
"version": "0.7.13",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
|
||||
@@ -44,7 +44,7 @@ class StreamPipeReader:
|
||||
def readBlocking(self, n):
|
||||
b = bytes(0)
|
||||
while len(b) < n:
|
||||
self.conn.poll()
|
||||
self.conn.poll(None)
|
||||
add = os.read(self.conn.fileno(), n - len(b))
|
||||
if not len(add):
|
||||
raise Exception('unable to read requested bytes')
|
||||
@@ -488,6 +488,11 @@ class PluginRemote:
|
||||
reader = StreamPipeReader(parent_conn)
|
||||
forkPeer, readLoop = await rpc_reader.prepare_peer_readloop(self.loop, reader = reader, writeFd = parent_conn.fileno())
|
||||
forkPeer.peerName = 'thread'
|
||||
|
||||
async def updateStats(stats):
|
||||
allMemoryStats[forkPeer] = stats
|
||||
forkPeer.params['updateStats'] = updateStats
|
||||
|
||||
async def forkReadLoop():
|
||||
try:
|
||||
await readLoop()
|
||||
@@ -495,6 +500,7 @@ class PluginRemote:
|
||||
# traceback.print_exc()
|
||||
print('fork read loop exited')
|
||||
finally:
|
||||
allMemoryStats.pop(forkPeer)
|
||||
parent_conn.close()
|
||||
reader.executor.shutdown()
|
||||
asyncio.run_coroutine_threadsafe(forkReadLoop(), loop=self.loop)
|
||||
@@ -585,6 +591,9 @@ class PluginRemote:
|
||||
async def getServicePort(self, name):
|
||||
pass
|
||||
|
||||
|
||||
allMemoryStats = {}
|
||||
|
||||
async def plugin_async_main(loop: AbstractEventLoop, readFd: int = None, writeFd: int = None, reader: asyncio.StreamReader = None, writer: asyncio.StreamWriter = None):
|
||||
peer, readLoop = await rpc_reader.prepare_peer_readloop(loop, readFd=readFd, writeFd=writeFd, reader=reader, writer=writer)
|
||||
peer.params['print'] = print
|
||||
@@ -593,6 +602,7 @@ async def plugin_async_main(loop: AbstractEventLoop, readFd: int = None, writeFd
|
||||
async def get_update_stats():
|
||||
update_stats = await peer.getParam('updateStats')
|
||||
if not update_stats:
|
||||
print('host did not provide update_stats')
|
||||
return
|
||||
|
||||
def stats_runner():
|
||||
@@ -608,8 +618,12 @@ async def plugin_async_main(loop: AbstractEventLoop, readFd: int = None, writeFd
|
||||
resource.RUSAGE_SELF).ru_maxrss
|
||||
except:
|
||||
heapTotal = 0
|
||||
|
||||
for _, stats in allMemoryStats.items():
|
||||
ptime += stats['cpu']['user']
|
||||
heapTotal += stats['memoryUsage']['heapTotal']
|
||||
|
||||
stats = {
|
||||
'type': 'stats',
|
||||
'cpu': {
|
||||
'user': ptime,
|
||||
'system': 0,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -141,7 +141,8 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
||||
|
||||
for (const upsert of deviceManifest.devices) {
|
||||
upsert.providerNativeId = deviceManifest.providerNativeId;
|
||||
await this.pluginHost.upsertDevice(upsert);
|
||||
const id = await this.pluginHost.upsertDevice(upsert);
|
||||
this.scrypted.getDevice(id)?.probe().catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user