Files
scrypted/plugins/objectdetector/src/main.ts
2022-12-09 11:14:37 -08:00

847 lines
28 KiB
TypeScript

import sdk, { Camera, DeviceState, EventListenerRegister, MediaObject, MediaStreamOptions, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, VideoCamera } from '@scrypted/sdk';
import crypto from 'crypto';
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
import { DenoisedDetectionEntry, DenoisedDetectionState, denoiseDetections } from './denoise';
import { safeParseJson } from './util';
const polygonOverlap = require('polygon-overlap');
const insidePolygon = require('point-inside-polygon');
const { mediaManager, systemManager, log } = sdk;
const defaultDetectionDuration = 60;
const defaultDetectionInterval = 60;
const defaultDetectionTimeout = 60;
const defaultMotionDuration = 10;
const DETECT_PERIODIC_SNAPSHOTS = "Periodic Snapshots";
const DETECT_MOTION_SNAPSHOTS = "Motion Snapshots";
const DETECT_VIDEO_MOTION = "Video Motion";
type ClipPath = [number, number][];
type Zones = { [zone: string]: ClipPath };
type TrackedDetection = ObjectDetectionResult & { bestScore?: number };
class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera & MotionSensor & ObjectDetector> implements ObjectDetector, Settings, ObjectDetectionCallbacks {
released = false;
motionListener: EventListenerRegister;
detectionListener: EventListenerRegister;
detectorListener: EventListenerRegister;
detections = new Map<string, MediaObject>();
cameraDevice: ScryptedDevice & Camera & VideoCamera & MotionSensor;
detectSnapshotsOnly = this.storage.getItem('detectionMode');
detectionModes = this.getDetectionModes();
detectionTimeout = parseInt(this.storage.getItem('detectionTimeout')) || defaultDetectionTimeout;
detectionDuration = parseInt(this.storage.getItem('detectionDuration')) || defaultDetectionDuration;
motionDuration = parseInt(this.storage.getItem('motionDuration')) || defaultMotionDuration;
motionAsObjects = this.storage.getItem('motionAsObjects') === 'true';
motionTimeout: NodeJS.Timeout;
detectionInterval = parseInt(this.storage.getItem('detectionInterval')) || defaultDetectionInterval;
zones = this.getZones();
exclusionZones = this.getExclusionZones();
detectionIntervalTimeout: NodeJS.Timeout;
detectionState: DenoisedDetectionState<TrackedDetection> = {};
detectionId: string;
running = false;
hasMotionType: boolean;
settings: Setting[];
analyzeStarted = 0;
constructor(mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public internal: boolean) {
super({
mixinDevice, mixinDeviceState,
mixinProviderNativeId: providerNativeId,
mixinDeviceInterfaces,
group,
groupKey: "objectdetectionplugin:" + objectDetection.id,
mixinStorageSuffix: objectDetection.id,
});
this.cameraDevice = systemManager.getDeviceById<Camera & VideoCamera & MotionSensor>(this.id);
this.detectionId = internal ? modelName : modelName + '-' + this.cameraDevice.id;
this.bindObjectDetection();
this.register();
this.resetDetectionTimeout();
}
getDetectionModes(): string[] {
try {
return JSON.parse(this.storage.getItem('detectionModes'));
}
catch (e) {
return [
DETECT_VIDEO_MOTION,
];
}
}
clearDetectionTimeout() {
clearTimeout(this.detectionIntervalTimeout);
this.detectionIntervalTimeout = undefined;
}
resetDetectionTimeout() {
this.clearDetectionTimeout();
this.detectionIntervalTimeout = setInterval(() => {
if ((!this.running && this.detectionModes.includes(DETECT_PERIODIC_SNAPSHOTS)) || this.hasMotionType) {
this.snapshotDetection();
}
}, this.detectionInterval * 1000);
}
clearMotionTimeout() {
clearTimeout(this.motionTimeout);
this.motionTimeout = undefined;
}
resetMotionTimeout() {
this.clearMotionTimeout();
this.motionTimeout = setTimeout(() => {
this.motionDetected = false;
}, this.motionDuration * 1000);
}
async ensureSettings(): Promise<Setting[]> {
if (this.hasMotionType !== undefined)
return;
this.hasMotionType = false;
const model = await this.objectDetection.getDetectionModel();
this.hasMotionType = model.classes.includes('motion');
this.settings = model.settings;
this.motionDetected = false;
}
async getCurrentSettings() {
await this.ensureSettings();
if (!this.settings)
return;
const ret: any = {};
for (const setting of this.settings) {
ret[setting.key] = (setting.multiple ? safeParseJson(this.storage.getItem(setting.key)) : this.storage.getItem(setting.key))
|| setting.value;
}
return ret;
}
async maybeStartMotionDetection() {
await this.ensureSettings();
if (this.hasMotionType)
await this.startVideoDetection();
}
async snapshotDetection() {
await this.ensureSettings();
if (this.hasMotionType) {
await this.startVideoDetection();
return;
}
const picture = await this.cameraDevice.takePicture();
const detections = await this.objectDetection.detectObjects(picture, {
detectionId: this.detectionId,
settings: await this.getCurrentSettings(),
});
this.objectsDetected(detections, true);
this.reportObjectDetections(detections);
}
bindObjectDetection() {
this.running = false;
this.detectionListener?.removeListener();
this.detectionListener = undefined;
this.detectorListener?.removeListener();
this.detectorListener = undefined;
this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
});
this.detectionListener = this.objectDetection.listen({
event: ScryptedInterface.ObjectDetection,
watch: false,
}, (eventSource, eventDetails, eventData: ObjectsDetected) => {
if (eventData?.detectionId !== this.detectionId)
return;
this.objectsDetected(eventData);
this.reportObjectDetections(eventData);
this.running = eventData.running;
});
this.maybeStartMotionDetection();
if (this.detectionModes.includes(DETECT_PERIODIC_SNAPSHOTS))
this.snapshotDetection();
if (this.detectionModes.includes(DETECT_MOTION_SNAPSHOTS)) {
this.detectorListener = this.cameraDevice.listen(ScryptedInterface.ObjectDetector, async (eventSource, eventDetails, eventData: ObjectsDetected) => {
if (!eventData?.detections?.find(d => d.className === 'motion'))
return;
if (!eventData?.eventId)
return;
const od = eventSource as any as ObjectDetector;
const mo = await od.getDetectionInput(eventData.detectionId, eventData.eventId);
const detections = await this.objectDetection.detectObjects(mo, {
detectionId: this.detectionId,
settings: await this.getCurrentSettings(),
});
this.objectsDetected(detections, true);
this.setDetection(detections, mo);
this.reportObjectDetections(detections);
});
}
}
async register() {
this.motionListener = this.cameraDevice.listen(ScryptedInterface.MotionSensor, async () => {
// ignore any motion events if this is a motion detector.
if (this.hasMotionType)
return;
if (!this.cameraDevice.motionDetected) {
if (this.running) {
// allow anaysis due to user request.
if (this.analyzeStarted + 60000 > Date.now())
return;
this.console.log('motion stopped, cancelling ongoing detection')
this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
});
}
return;
}
if (this.detectionModes.includes(DETECT_VIDEO_MOTION))
await this.startVideoDetection();
});
}
handleDetectionEvent(detection: ObjectsDetected, mediaObject?: MediaObject) {
this.running = detection.running;
const newOrBetterDetection = this.objectsDetected(detection);
if (newOrBetterDetection)
this.setDetection(detection, mediaObject);
this.reportObjectDetections(detection);
// if (newOrBetterDetection) {
// mediaManager.convertMediaObjectToBuffer(mediaObject, 'image/jpeg')
// .then(jpeg => {
// fs.writeFileSync(`/Volumes/External/test/${Date.now()}.jpeg`, jpeg);
// this.console.log('jepg!');
// })
// this.console.log('retaining media');
// }
return newOrBetterDetection;
}
async onDetection(detection: ObjectsDetected, mediaObject?: MediaObject): Promise<boolean> {
return this.handleDetectionEvent(detection, mediaObject);
}
async onDetectionEnded(detection: ObjectsDetected): Promise<void> {
this.handleDetectionEvent(detection);
}
async startVideoDetection() {
try {
const settings = await this.getCurrentSettings();
// prevent stream retrieval noise until notified that the detection is no longer running.
if (this.running) {
const session = await this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
settings,
}, this);
this.running = session.running;
if (this.running)
return;
}
// dummy up the last detection time to prevent the idle timers from purging everything.
this.detectionState.lastDetection = Date.now();
this.running = true;
let model: ObjectDetectionModel;
try {
model = await this.objectDetection.getDetectionModel(settings);
}
catch (e) {
}
let stream: MediaObject;
// internal streams must implicitly be available.
if (!this.internal) {
stream = await this.cameraDevice.getVideoStream({
destination: model?.inputStream || 'low-resolution',
// ask rebroadcast to mute audio, not needed.
audio: null,
});
}
else {
stream = await mediaManager.createMediaObject(Buffer.alloc(0), 'x-scrypted/x-internal-media-object');
}
const session = await this.objectDetection?.detectObjects(stream, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
settings,
}, this);
this.running = session.running;
}
catch (e) {
this.console.log('failure retrieving stream', e);
this.running = false;
}
}
getDetectionDuration() {
// when motion type, the detection interval is a keepalive reset.
// the duration needs to simply be an arbitrarily longer time.
return this.hasMotionType ? this.detectionInterval * 1000 * 5 : this.detectionDuration * 1000;
}
reportObjectDetections(detection: ObjectsDetected) {
// determine zones of the objects, if configured.
if (detection.detections && (Object.keys(this.zones).length || Object.keys(this.exclusionZones).length)) {
let copy = detection.detections.slice();
for (const o of detection.detections) {
if (!o.boundingBox)
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];
let excluded = false;
for (const [zone, zoneValue] of Object.entries(this.exclusionZones)) {
if (insidePolygon([x, y], zoneValue) &&
insidePolygon([x, y2], zoneValue) &&
insidePolygon([x2, y], zoneValue) &&
insidePolygon([x2, y2], zoneValue)) {
excluded = true;
copy = copy.filter(c => c !== o);
break;
}
}
if (excluded)
continue;
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
for (const [zone, zoneValue] of Object.entries(this.zones)) {
if (polygonOverlap(box, zoneValue)) {
// this.console.log(o.className, 'inside', zone);
o.zones.push(zone);
}
}
}
detection.detections = copy;
}
// if this detector supports bounding boxes, and there are zones configured,
// filter the detections to the zones.
if (Object.keys(this.zones).length)
detection.detections = detection.detections.filter(o => !o.boundingBox || o?.zones?.length);
if (this.hasMotionType) {
const found = detection.detections?.find(d => d.className === 'motion');
if (found) {
if (!this.motionDetected)
this.motionDetected = true;
this.resetMotionTimeout();
const areas = detection.detections.filter(d => d.className === 'motion' && d.score !== 1).map(d => d.score)
if (areas.length)
this.console.log('detection areas', areas);
}
}
if (!this.hasMotionType || this.motionAsObjects)
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
}
async extendedObjectDetect(force?: boolean) {
try {
if (!force && !this.motionDetected)
return;
await this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
}, this);
}
catch (e) {
// ignore any
}
}
objectsDetected(detectionResult: ObjectsDetected, showAll?: boolean) {
// do not denoise
if (this.hasMotionType) {
return;
}
if (!detectionResult?.detections) {
// detection session ended.
return;
}
const { detections } = detectionResult;
let newOrBetterDetection = false;
const found: DenoisedDetectionEntry<TrackedDetection>[] = [];
denoiseDetections<TrackedDetection>(this.detectionState, detections.map(detection => ({
get id() {
return detection.id;
},
set id(id) {
detection.id = id;
},
name: detection.className,
score: detection.score,
detection,
get firstSeen() {
return detection.history?.firstSeen
},
set firstSeen(value) {
detection.history = detection.history || {
firstSeen: value,
lastSeen: value,
};
detection.history.firstSeen = value;
},
get lastSeen() {
return detection.history?.lastSeen
},
set lastSeen(value) {
detection.history = detection.history || {
firstSeen: value,
lastSeen: value,
};
detection.history.lastSeen = value;
},
boundingBox: detection.boundingBox,
})), {
timeout: this.detectionTimeout * 1000,
added: d => {
found.push(d);
newOrBetterDetection = true;
d.detection.bestScore = d.detection.score;
},
removed: d => {
this.console.log('expired detection:', `${d.detection.className} (${d.detection.score})`);
if (detectionResult.running)
this.extendedObjectDetect();
},
retained: (d, o) => {
if (d.detection.score > o.detection.bestScore) {
newOrBetterDetection = true;
d.detection.bestScore = d.detection.score;
}
else {
d.detection.bestScore = o.detection.bestScore;
}
},
expiring: (d) => {
},
});
if (found.length) {
this.console.log('new detection:', found.map(d => `${d.detection.className} (${d.detection.score})`).join(', '));
if (detectionResult.running)
this.extendedObjectDetect();
}
if (found.length || showAll) {
this.console.log('current detections:', this.detectionState.previousDetections.map(d => `${d.detection.className} (${d.detection.score}, ${d.detection.boundingBox?.join(', ')})`).join(', '));
}
// removes items that is not tracked yet (may require more present frames)
detectionResult.detections = detectionResult.detections.filter(d => d.id);
return newOrBetterDetection;
}
setDetection(detection: ObjectsDetected, detectionInput: MediaObject) {
if (!detection.detectionId)
detection.detectionId = crypto.randomBytes(4).toString('hex');
const { detectionId } = detection;
this.detections.set(detectionId, detectionInput);
setTimeout(() => {
this.detections.delete(detectionId);
}, 2000);
}
async getNativeObjectTypes(): Promise<ObjectDetectionTypes> {
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.ObjectDetector))
return this.mixinDevice.getObjectTypes();
return {};
}
async getObjectTypes(): Promise<ObjectDetectionTypes> {
const ret = await this.getNativeObjectTypes();
if (!ret.classes)
ret.classes = [];
ret.classes.push(...(await this.objectDetection.getDetectionModel()).classes);
return ret;
}
async getDetectionInput(detectionId: any): Promise<MediaObject> {
const detection = this.detections.get(detectionId);
if (detection)
return detection;
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.ObjectDetector))
return this.mixinDevice.getDetectionInput(detectionId);
throw new Error('Detection not found. It may have expired.');
}
async getMixinSettings(): Promise<Setting[]> {
const settings: Setting[] = [];
if (this.hasMotionType && this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor)) {
settings.push({
title: 'Existing Motion Sensor',
description: 'This camera has a built in motion sensor. Using OpenCV Motion Sensing may be unnecessary and will use additional CPU.',
readonly: true,
value: 'WARNING',
key: 'existingMotionSensor',
})
}
let msos: MediaStreamOptions[] = [];
try {
msos = await this.cameraDevice.getVideoStreamOptions();
}
catch (e) {
}
if (!this.hasMotionType) {
settings.push({
title: 'Detection Modes',
description: 'Configure when to analyze the video stream. Video Motion can be CPU intensive.',
key: 'detectionModes',
type: 'string',
multiple: true,
choices: [
DETECT_PERIODIC_SNAPSHOTS,
DETECT_VIDEO_MOTION,
DETECT_MOTION_SNAPSHOTS,
],
value: this.detectionModes,
});
if (this.detectionModes.includes(DETECT_VIDEO_MOTION)) {
settings.push(
{
title: 'Detection Duration',
description: 'The duration in seconds to analyze video when motion occurs.',
key: 'detectionDuration',
type: 'number',
value: this.detectionDuration.toString(),
}
);
}
if (this.detectionModes.includes(DETECT_PERIODIC_SNAPSHOTS)) {
settings.push(
{
title: 'Idle Detection Interval',
description: 'The interval in seconds to analyze snapshots when there is no motion.',
key: 'detectionInterval',
type: 'number',
value: this.detectionInterval.toString(),
}
);
}
settings.push(
{
title: 'Detection Timeout',
description: 'Timeout in seconds before removing an object that is no longer detected.',
key: 'detectionTimeout',
type: 'number',
value: this.detectionTimeout.toString(),
},
);
}
else {
settings.push({
title: 'Motion Duration',
description: 'The duration in seconds to wait to reset the motion sensor.',
key: 'motionDuration',
type: 'number',
value: this.motionDuration.toString(),
},
{
title: 'Motion Detection Objects',
description: 'Report motion detections as objects (useful for debugging).',
key: 'motionAsObjects',
type: 'boolean',
value: this.motionAsObjects,
}
);
}
await this.ensureSettings();
if (this.settings) {
settings.push(...this.settings.map(setting =>
Object.assign({}, setting, {
placeholder: setting.placeholder?.toString(),
value: (setting.multiple ? safeParseJson(this.storage.getItem(setting.key)) : this.storage.getItem(setting.key))
|| setting.value,
} as Setting))
);
}
if (!this.hasMotionType) {
settings.push(
{
title: 'Analyze',
description: 'Analyzes the video stream for 1 minute. Results will be shown in the Console.',
key: 'analyzeButton',
type: 'button',
}
);
}
settings.push({
key: 'zones',
title: 'Inclusion Zones',
type: 'string',
description: 'Inclusion zones limit detections to those within the included areas. Enter the name of a new zone or delete an existing zone.',
multiple: true,
value: Object.keys(this.zones),
choices: Object.keys(this.zones),
combobox: true,
});
settings.push({
key: 'exclusionZones',
title: 'Exclusion Zones',
description: 'Exclusion zones prevent detections that are fully contained within excluded areas. Enter the name of a new zone or delete an existing zone.',
type: 'string',
multiple: true,
value: Object.keys(this.exclusionZones),
choices: Object.keys(this.exclusionZones),
combobox: true,
});
for (const [name, value] of Object.entries(this.zones)) {
settings.push({
key: `zone-${name}`,
title: `Edit Zone: ${name}`,
type: 'clippath',
value: JSON.stringify(value),
});
}
for (const [name, value] of Object.entries(this.exclusionZones)) {
settings.push({
key: `zone-${name}`,
title: `Edit Zone: ${name}`,
type: 'clippath',
value: JSON.stringify(value),
});
}
return settings;
}
getZones(): Zones {
try {
return JSON.parse(this.storage.getItem('zones'));
}
catch (e) {
return {};
}
}
getExclusionZones(): Zones {
try {
return JSON.parse(this.storage.getItem('exclusionZones'));
}
catch (e) {
return {};
}
}
async putMixinSetting(key: string, value: string | number | boolean | string[] | number[]): Promise<void> {
let vs = value?.toString();
if (key === 'zones') {
const newZones: Zones = {};
for (const name of value as string[]) {
newZones[name] = this.zones[name] || [];
}
this.zones = newZones;
this.storage.setItem('zones', JSON.stringify(newZones));
return;
}
if (key === 'exclusionZones') {
const newZones: Zones = {};
for (const name of value as string[]) {
newZones[name] = this.exclusionZones[name] || [];
}
this.exclusionZones = newZones;
this.storage.setItem('exclusionZones', JSON.stringify(newZones));
return;
}
if (key.startsWith('zone-')) {
const zoneName = key.substring(5);
if (this.zones[zoneName]) {
this.zones[zoneName] = JSON.parse(vs);
this.storage.setItem('zones', JSON.stringify(this.zones));
}
if (this.exclusionZones[zoneName]) {
this.exclusionZones[zoneName] = JSON.parse(vs);
this.storage.setItem('exclusionZones', JSON.stringify(this.exclusionZones));
}
return;
}
if (value && this.settings?.find(s => s.key === key)?.multiple) {
vs = JSON.stringify(value);
}
this.storage.setItem(key, vs);
if (key === 'detectionDuration') {
this.detectionDuration = parseInt(vs) || defaultDetectionDuration;
}
else if (key === 'detectionInterval') {
this.detectionInterval = parseInt(vs) || defaultDetectionInterval;
this.resetDetectionTimeout();
}
else if (key === 'detectionTimeout') {
this.detectionTimeout = parseInt(vs) || defaultDetectionTimeout;
}
else if (key === 'motionDuration') {
this.motionDuration = parseInt(vs) || defaultMotionDuration;
}
else if (key === 'motionAsObjects') {
this.motionAsObjects = vs === 'true';
}
else if (key === 'analyzeButton') {
this.analyzeStarted = Date.now();
// await this.snapshotDetection();
await this.startVideoDetection();
await this.extendedObjectDetect(true);
}
else if (key === 'detectionModes') {
this.storage.setItem(key, JSON.stringify(value));
this.detectionModes = this.getDetectionModes();
this.bindObjectDetection();
}
else {
const settings = await this.getCurrentSettings();
if (settings && settings[key]) {
settings[key] = value;
}
this.bindObjectDetection();
}
}
async release() {
super.release();
this.released = true;
this.clearDetectionTimeout();
this.clearMotionTimeout();
this.motionListener?.removeListener();
this.detectionListener?.removeListener();
this.detectorListener?.removeListener();
this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
});
}
}
class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements MixinProvider {
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, mixinProviderNativeId: ScryptedNativeId, public modelName: string, public internal?: boolean) {
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId });
// trigger mixin creation. todo: fix this to not be stupid hack.
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById<VideoCamera & Settings>(id);
if (!device.mixins?.includes(this.id))
continue;
device.probe();
}
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
// filter out
for (const iface of interfaces) {
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
const deviceMatch = this.mixinDeviceInterfaces.find(miface => miface.startsWith(iface));
if (deviceMatch)
continue;
return null;
}
}
if ((type === ScryptedDeviceType.Camera || type === ScryptedDeviceType.Doorbell) && (interfaces.includes(ScryptedInterface.VideoCamera) || interfaces.includes(ScryptedInterface.Camera))) {
return [ScryptedInterface.ObjectDetector, ScryptedInterface.MotionSensor, ScryptedInterface.Settings];
}
return null;
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
let objectDetection = systemManager.getDeviceById<ObjectDetection>(this.id);
const group = objectDetection.name;
return new ObjectDetectionMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.modelName, group, this.internal);
}
async releaseMixin(id: string, mixinDevice: any) {
this.console.log('releasing ObjectDetection mixin', id);
mixinDevice.release();
}
}
class ObjectDetectionPlugin extends AutoenableMixinProvider {
constructor(nativeId?: ScryptedNativeId) {
super(nativeId);
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (!interfaces.includes(ScryptedInterface.ObjectDetection))
return;
return [ScryptedInterface.MixinProvider];
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
for (const iface of mixinDeviceInterfaces) {
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
const model = await mixinDevice.getDetectionModel();
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model.name, true);
}
}
const model = await mixinDevice.getDetectionModel();
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model.name);
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
// what does this mean to make a mixin provider no longer available?
// just ignore it until reboot?
}
}
export default ObjectDetectionPlugin;