mirror of
https://github.com/koush/scrypted.git
synced 2026-02-06 23:42:19 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06d0a4a2f1 | ||
|
|
2fb6e0a368 | ||
|
|
c6ed0d8729 | ||
|
|
67c6f63dbe | ||
|
|
e62b4ad68b | ||
|
|
bfec5eb3f3 |
6
plugins/bticino/package-lock.json
generated
6
plugins/bticino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"dependencies": {
|
||||
"@slyoldfox/sip": "^0.0.6-1",
|
||||
"sdp": "^3.0.3",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.103",
|
||||
"version": "0.2.105",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
61
plugins/bticino/src/bticino-aswm-switch.ts
Normal file
61
plugins/bticino/src/bticino-aswm-switch.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ScryptedDeviceBase, HttpRequest, HttpResponse, HttpRequestHandler, OnOff } from "@scrypted/sdk";
|
||||
import { BticinoSipCamera } from "./bticino-camera";
|
||||
import { VoicemailHandler } from "./bticino-voicemailHandler";
|
||||
|
||||
export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
|
||||
private timeout : NodeJS.Timeout
|
||||
|
||||
constructor(private camera: BticinoSipCamera, private voicemailHandler : VoicemailHandler) {
|
||||
super( camera.nativeId + "-aswm-switch")
|
||||
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
|
||||
}
|
||||
|
||||
turnOff(): Promise<void> {
|
||||
this.on = false
|
||||
return this.camera.turnOffAswm()
|
||||
}
|
||||
|
||||
turnOn(): Promise<void> {
|
||||
this.on = true
|
||||
return this.camera.turnOnAswm()
|
||||
}
|
||||
|
||||
syncStatus() {
|
||||
this.on = this.voicemailHandler.isAswmEnabled()
|
||||
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
|
||||
}
|
||||
|
||||
cancelTimer() {
|
||||
if( this.timeout ) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
if (request.url.endsWith('/disabled')) {
|
||||
this.on = false
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else if( request.url.endsWith('/enabled') ) {
|
||||
this.on = true
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else if( request.url.endsWith('/enable') ) {
|
||||
this.turnOn()
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else if( request.url.endsWith('/disable') ) {
|
||||
this.turnOff()
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else {
|
||||
response.send('Unsupported operation', {
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import { SipRequest } from '../../sip/src/sip-manager';
|
||||
|
||||
import { get } from 'http'
|
||||
import { ControllerApi } from './c300x-controller-api';
|
||||
import { BticinoAswmSwitch } from './bticino-aswm-switch';
|
||||
import { BticinoMuteSwitch } from './bticino-mute-switch';
|
||||
|
||||
const STREAM_TIMEOUT = 65000;
|
||||
const { mediaManager } = sdk;
|
||||
@@ -63,10 +65,50 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
|
||||
get(`http://${c300x}:8080/reboot?now`, (res) => {
|
||||
console.log("Reboot API result: " + res.statusCode)
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
this.console.error(error)
|
||||
reject(error)
|
||||
} ).end();
|
||||
})
|
||||
}
|
||||
|
||||
muteRinger(mute : boolean): Promise<void> {
|
||||
return new Promise<void>( (resolve,reject ) => {
|
||||
let c300x = SipHelper.getIntercomIp(this)
|
||||
|
||||
get(`http://${c300x}:8080/mute?raw=true&enable=` + mute, (res) => {
|
||||
console.log("Mute API result: " + res.statusCode)
|
||||
}).on('error', (error) => {
|
||||
this.console.error(error)
|
||||
reject(error)
|
||||
} ).end();
|
||||
})
|
||||
}
|
||||
|
||||
muteStatus(): Promise<boolean> {
|
||||
return new Promise<boolean>( (resolve,reject ) => {
|
||||
let c300x = SipHelper.getIntercomIp(this)
|
||||
|
||||
get(`http://${c300x}:8080/mute?status=true&raw=true`, (res) => {
|
||||
let rawData = '';
|
||||
res.on('data', (chunk) => { rawData += chunk; })
|
||||
res.on('error', (error) => this.console.log(error))
|
||||
res.on('end', () => {
|
||||
try {
|
||||
return resolve(JSON.parse(rawData))
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
reject(e.message)
|
||||
|
||||
}
|
||||
})
|
||||
}).on('error', (error) => {
|
||||
this.console.error(error)
|
||||
reject(error)
|
||||
} ).end();
|
||||
})
|
||||
}
|
||||
|
||||
getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
|
||||
return new Promise<VideoClip[]>( (resolve,reject ) => {
|
||||
let c300x = SipHelper.getIntercomIp(this)
|
||||
@@ -95,7 +137,10 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
console.error(e.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
this.console.error(error)
|
||||
reject(error)
|
||||
} ).end(); ;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,6 +177,18 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
} )
|
||||
}
|
||||
|
||||
turnOnAswm() : Promise<void> {
|
||||
return this.persistentSipManager.enable().then( (sipCall) => {
|
||||
sipCall.message( "*8*91##" )
|
||||
} )
|
||||
}
|
||||
|
||||
turnOffAswm() : Promise<void> {
|
||||
return this.persistentSipManager.enable().then( (sipCall) => {
|
||||
sipCall.message( "*8*92##" )
|
||||
} )
|
||||
}
|
||||
|
||||
async takePicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
|
||||
}
|
||||
@@ -378,11 +435,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
]
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) : Promise<BticinoSipLock> {
|
||||
async getDevice(nativeId: string) : Promise<any> {
|
||||
if( nativeId && nativeId.endsWith('-aswm-switch')) {
|
||||
return new BticinoAswmSwitch(this, this.voicemailHandler)
|
||||
} else if( nativeId && nativeId.endsWith('-mute-switch') ) {
|
||||
return new BticinoMuteSwitch(this)
|
||||
}
|
||||
return new BticinoSipLock(this)
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
this.stopIntercom()
|
||||
this.voicemailHandler.cancelTimer()
|
||||
this.persistentSipManager.cancelTimer()
|
||||
this.controllerApi.cancelTimer()
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BticinoSipCamera } from "./bticino-camera"
|
||||
|
||||
export class VoicemailHandler extends SipRequestHandler {
|
||||
private timeout : NodeJS.Timeout
|
||||
private aswmIsEnabled: boolean
|
||||
|
||||
constructor( private sipCamera : BticinoSipCamera ) {
|
||||
super()
|
||||
@@ -15,14 +16,12 @@ export class VoicemailHandler extends SipRequestHandler {
|
||||
checkVoicemail() {
|
||||
if( !this.sipCamera )
|
||||
return
|
||||
if( this.isEnabled() ) {
|
||||
this.sipCamera.console.debug("Checking answering machine, cameraId: " + this.sipCamera.id )
|
||||
this.sipCamera.getAswmStatus().catch( e => this.sipCamera.console.error(e) )
|
||||
} else {
|
||||
this.sipCamera.console.debug("Answering machine check not enabled, cameraId: " + this.sipCamera.id )
|
||||
}
|
||||
//TODO: make interval customizable, now every 5 minutes
|
||||
this.timeout = setTimeout( () => this.checkVoicemail() , 5 * 60 * 1000 )
|
||||
|
||||
this.sipCamera.console.debug("Checking answering machine, cameraId: " + this.sipCamera.id )
|
||||
this.sipCamera.getAswmStatus().catch( e => this.sipCamera.console.error(e) )
|
||||
|
||||
//TODO: make interval customizable, now every minute
|
||||
this.timeout = setTimeout( () => this.checkVoicemail() , 1 * 60 * 1000 )
|
||||
}
|
||||
|
||||
cancelTimer() {
|
||||
@@ -32,10 +31,11 @@ export class VoicemailHandler extends SipRequestHandler {
|
||||
}
|
||||
|
||||
handle(request: SipRequest) {
|
||||
if( this.isEnabled() ) {
|
||||
const lastVoicemailMessageTimestamp : number = Number.parseInt( this.sipCamera.storage.getItem('lastVoicemailMessageTimestamp') ) || -1
|
||||
const message : string = request.content.toString()
|
||||
if( message.startsWith('*#8**40*0*0*1176*0*2##') ) {
|
||||
const lastVoicemailMessageTimestamp : number = Number.parseInt( this.sipCamera.storage.getItem('lastVoicemailMessageTimestamp') ) || -1
|
||||
const message : string = request.content.toString()
|
||||
if( message.startsWith('*#8**40*0*0*') || message.startsWith('*#8**40*1*0*') ) {
|
||||
this.aswmIsEnabled = message.startsWith('*#8**40*1*0*');
|
||||
if( this.isEnabled() ) {
|
||||
this.sipCamera.console.debug("Handling incoming answering machine reply")
|
||||
const messages : string[] = message.split(';')
|
||||
let lastMessageTimestamp : number = 0
|
||||
@@ -53,12 +53,12 @@ export class VoicemailHandler extends SipRequestHandler {
|
||||
}
|
||||
} )
|
||||
if( (lastVoicemailMessageTimestamp == null && lastMessageTimestamp > 0) ||
|
||||
( lastVoicemailMessageTimestamp != null && lastMessageTimestamp > lastVoicemailMessageTimestamp ) ) {
|
||||
( lastVoicemailMessageTimestamp != null && lastMessageTimestamp > lastVoicemailMessageTimestamp ) ) {
|
||||
this.sipCamera.log.a(`You have ${countNewMessages} new voicemail messages.`)
|
||||
this.sipCamera.storage.setItem('lastVoicemailMessageTimestamp', lastMessageTimestamp.toString())
|
||||
} else {
|
||||
} else {
|
||||
this.sipCamera.console.debug("No new messages since: " + lastVoicemailMessageTimestamp + " lastMessage: " + lastMessageTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,4 +66,8 @@ export class VoicemailHandler extends SipRequestHandler {
|
||||
isEnabled() : boolean {
|
||||
return this.sipCamera?.storage?.getItem('notifyVoicemail')?.toLocaleLowerCase() === 'true' || false
|
||||
}
|
||||
|
||||
isAswmEnabled() : boolean {
|
||||
return this.aswmIsEnabled
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export class ControllerApi {
|
||||
})
|
||||
}
|
||||
console.log("Endpoint registration status: " + res.statusCode)
|
||||
});
|
||||
}).on('error', (e) => this.sipCamera.console.error(e) );
|
||||
|
||||
// The default evict time on the c300x-controller is 5 minutes, so this will certainly be within bounds
|
||||
this.timeout = setTimeout( () => this.registerEndpoints( false ) , 2 * 60 * 1000 )
|
||||
@@ -114,7 +114,7 @@ export class ControllerApi {
|
||||
return new Promise( (resolve, reject) => get(`http://${ipAddress}:8080/register-endpoint?raw=true&updateStreamEndpoint=${sipFrom}`, (res) => {
|
||||
if( res.statusCode != 200 ) reject( "ERROR: Could not update streaming endpoint, call returned: " + res.statusCode )
|
||||
else resolve()
|
||||
} ) );
|
||||
} ).on('error', (error) => this.sipCamera.console.error(error) ).end() );
|
||||
}
|
||||
|
||||
public cancelTimer() {
|
||||
|
||||
@@ -36,7 +36,7 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
const name = settings.newCamera?.toString() === undefined ? "Doorbell" : settings.newCamera?.toString()
|
||||
await this.updateDevice(nativeId, name)
|
||||
|
||||
const device: Device = {
|
||||
const lockDevice: Device = {
|
||||
providerNativeId: nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
@@ -49,10 +49,38 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
type: ScryptedDeviceType.Lock,
|
||||
interfaces: [ScryptedInterface.Lock, ScryptedInterface.HttpRequestHandler],
|
||||
}
|
||||
|
||||
const aswmSwitchDevice: Device = {
|
||||
providerNativeId: nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
manufacturer: 'BticinoPlugin',
|
||||
//firmware: camera.data.firmware_version,
|
||||
//serialNumber: camera.data.device_id
|
||||
},
|
||||
nativeId: nativeId + '-aswm-switch',
|
||||
name: name + ' Voicemail',
|
||||
type: ScryptedDeviceType.Switch,
|
||||
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.HttpRequestHandler],
|
||||
}
|
||||
|
||||
const muteSwitchDevice: Device = {
|
||||
providerNativeId: nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
manufacturer: 'BticinoPlugin',
|
||||
//firmware: camera.data.firmware_version,
|
||||
//serialNumber: camera.data.device_id
|
||||
},
|
||||
nativeId: nativeId + '-mute-switch',
|
||||
name: name + ' Muted',
|
||||
type: ScryptedDeviceType.Switch,
|
||||
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.HttpRequestHandler],
|
||||
}
|
||||
|
||||
await deviceManager.onDevicesChanged({
|
||||
providerNativeId: nativeId,
|
||||
devices: [device],
|
||||
devices: [lockDevice, aswmSwitchDevice, muteSwitchDevice],
|
||||
})
|
||||
|
||||
let sipCamera : BticinoSipCamera = await this.getDevice(nativeId)
|
||||
|
||||
467
plugins/snapshot/package-lock.json
generated
467
plugins/snapshot/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.11",
|
||||
"description": "Snapshot Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
@@ -32,13 +32,11 @@
|
||||
"DeviceProvider"
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@koush/sharp": "^0.32.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@types/node": "^18.16.18",
|
||||
"axios": "^1.4.0",
|
||||
"sharp": "^0.32.6",
|
||||
"whatwg-mimetype": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import sdk, { BufferConverter, Image, ImageOptions, MediaObject, MediaObjectOptions, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import sharp from '@koush/sharp';
|
||||
import type sharp from 'sharp';
|
||||
|
||||
export let sharpInstance: typeof sharp;
|
||||
try {
|
||||
sharpInstance = require('sharp');
|
||||
console.log('sharp loaded');
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('sharp failed to load, scrypted server may be out of date', e);
|
||||
}
|
||||
|
||||
export const ImageReaderNativeId = 'imagereader';
|
||||
|
||||
async function createVipsMediaObject(image: VipsImage): Promise<Image & MediaObject> {
|
||||
const ret: Image & MediaObject = await sdk.mediaManager.createMediaObject(image, ScryptedMimeTypes.Image, {
|
||||
@@ -65,7 +76,7 @@ export class VipsImage implements Image {
|
||||
resolveWithObject: true,
|
||||
});
|
||||
|
||||
const newImage = sharp(data, {
|
||||
const newImage = sharpInstance(data, {
|
||||
raw: info,
|
||||
});
|
||||
|
||||
@@ -87,8 +98,8 @@ export class VipsImage implements Image {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadVipsImage(data: Buffer, sourceId: string) {
|
||||
const image = sharp(data, {
|
||||
export async function loadVipsImage(data: Buffer|string, sourceId: string) {
|
||||
const image = sharpInstance(data, {
|
||||
failOnError: false,
|
||||
});
|
||||
const metadata = await image.metadata();
|
||||
|
||||
@@ -2,15 +2,15 @@ import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-provider";
|
||||
import { createMapPromiseDebouncer, RefreshPromise, singletonPromise, TimeoutError } from "@scrypted/common/src/promise-utils";
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import sdk, { BufferConverter, Camera, DeviceProvider, FFmpegInput, Image, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
|
||||
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import https from 'https';
|
||||
import path from 'path';
|
||||
import MimeType from 'whatwg-mimetype';
|
||||
import { ffmpegFilterImage, ffmpegFilterImageBuffer } from './ffmpeg-image-filter';
|
||||
import { ImageReader, ImageReaderNativeId, loadVipsImage, sharpInstance } from './image-reader';
|
||||
import { ImageWriter, ImageWriterNativeId } from './image-writer';
|
||||
import { loadVipsImage, VipsImage } from './image-reader';
|
||||
|
||||
const { mediaManager, systemManager } = sdk;
|
||||
|
||||
@@ -303,6 +303,20 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}, async () => {
|
||||
this.debugConsole?.log("Resizing picture from camera", options?.picture);
|
||||
|
||||
if (sharpInstance) {
|
||||
const vips = await loadVipsImage(picture, this.id);
|
||||
try {
|
||||
const ret = await vips.toBuffer({
|
||||
resize: options?.picture,
|
||||
format: 'jpg',
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
finally {
|
||||
vips.close();
|
||||
}
|
||||
}
|
||||
|
||||
// try {
|
||||
// const mo = await mediaManager.createMediaObject(picture, 'image/jpeg', {
|
||||
// sourceId: this.id,
|
||||
@@ -326,24 +340,13 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
// throw e;
|
||||
// }
|
||||
|
||||
// return ffmpegFilterImageBuffer(picture, {
|
||||
// console: this.debugConsole,
|
||||
// ffmpegPath: await mediaManager.getFFmpegPath(),
|
||||
// resize: options?.picture,
|
||||
// timeout: 10000,
|
||||
// });
|
||||
return ffmpegFilterImageBuffer(picture, {
|
||||
console: this.debugConsole,
|
||||
ffmpegPath: await mediaManager.getFFmpegPath(),
|
||||
resize: options?.picture,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const vips = await loadVipsImage(picture, this.id);
|
||||
try {
|
||||
const ret = await vips.toBuffer({
|
||||
resize: options?.picture,
|
||||
format: 'jpg',
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
finally {
|
||||
vips.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
@@ -364,6 +367,25 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
const xmax = Math.max(...this.storageSettings.values.snapshotCropScale.map(([x, y]) => x)) / 100;
|
||||
const ymax = Math.max(...this.storageSettings.values.snapshotCropScale.map(([x, y]) => y)) / 100;
|
||||
|
||||
if (sharpInstance) {
|
||||
const vips = await loadVipsImage(picture, this.id);
|
||||
try {
|
||||
const ret = await vips.toBuffer({
|
||||
crop: {
|
||||
left: xmin * vips.width,
|
||||
top: ymin * vips.height,
|
||||
width: (xmax - xmin) * vips.width,
|
||||
height: (ymax - ymin) * vips.height,
|
||||
},
|
||||
format: 'jpg',
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
finally {
|
||||
vips.close();
|
||||
}
|
||||
}
|
||||
|
||||
// try {
|
||||
// const mo = await mediaManager.createMediaObject(picture, 'image/jpeg');
|
||||
// const image = await mediaManager.convertMediaObject<Image>(mo, ScryptedMimeTypes.Image);
|
||||
@@ -387,35 +409,18 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
// throw e;
|
||||
// }
|
||||
|
||||
// return ffmpegFilterImageBuffer(picture, {
|
||||
// console: this.debugConsole,
|
||||
// ffmpegPath: await mediaManager.getFFmpegPath(),
|
||||
// crop: {
|
||||
// fractional: true,
|
||||
// left: xmin,
|
||||
// top: ymin,
|
||||
// width: xmax - xmin,
|
||||
// height: ymax - ymin,
|
||||
// },
|
||||
// timeout: 10000,
|
||||
// });
|
||||
|
||||
const vips = await loadVipsImage(picture, this.id);
|
||||
try {
|
||||
const ret = await vips.toBuffer({
|
||||
crop: {
|
||||
left: xmin * vips.width,
|
||||
top: ymin * vips.height,
|
||||
width: (xmax - xmin) * vips.width,
|
||||
height: (ymax - ymin) * vips.height,
|
||||
},
|
||||
format: 'jpg',
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
finally {
|
||||
vips.close();
|
||||
}
|
||||
return ffmpegFilterImageBuffer(picture, {
|
||||
console: this.debugConsole,
|
||||
ffmpegPath: await mediaManager.getFFmpegPath(),
|
||||
crop: {
|
||||
fractional: true,
|
||||
left: xmin,
|
||||
top: ymin,
|
||||
width: xmax - xmin,
|
||||
height: ymax - ymin,
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
clearErrorImages() {
|
||||
@@ -546,7 +551,7 @@ export function parseDims<T extends string>(dict: DimDict<T>) {
|
||||
ret[t] = parseFloat(val?.substring(0, val?.length - 1)) / 100;
|
||||
}
|
||||
else {
|
||||
ret[t] = parseFloat(val);
|
||||
ret[t] = val ? parseFloat(val) : undefined;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
@@ -567,25 +572,42 @@ class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, B
|
||||
this.fromMimeType = ScryptedMimeTypes.FFmpegInput;
|
||||
this.toMimeType = 'image/jpeg';
|
||||
|
||||
const manifest: DeviceManifest = {
|
||||
devices: [
|
||||
{
|
||||
name: 'Image Writer',
|
||||
interfaces: [
|
||||
ScryptedInterface.BufferConverter,
|
||||
],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
nativeId: ImageWriterNativeId,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (sharpInstance) {
|
||||
manifest.devices.push(
|
||||
{
|
||||
name: 'Image Reader',
|
||||
interfaces: [
|
||||
ScryptedInterface.BufferConverter,
|
||||
],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
nativeId: ImageReaderNativeId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
process.nextTick(() => {
|
||||
sdk.deviceManager.onDevicesChanged({
|
||||
devices: [
|
||||
{
|
||||
name: 'Image Writer',
|
||||
interfaces: [
|
||||
ScryptedInterface.BufferConverter,
|
||||
],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
nativeId: ImageWriterNativeId,
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
sdk.deviceManager.onDevicesChanged(manifest)
|
||||
});
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
if (nativeId === ImageWriterNativeId)
|
||||
return new ImageWriter(ImageWriterNativeId);
|
||||
if (nativeId === ImageReaderNativeId)
|
||||
return new ImageReader(ImageReaderNativeId);
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
@@ -609,11 +631,6 @@ class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, B
|
||||
|
||||
const ffmpegInput = JSON.parse(data.toString()) as FFmpegInput;
|
||||
|
||||
const args = [
|
||||
...ffmpegInput.inputArguments,
|
||||
...(ffmpegInput.h264EncoderArguments || []),
|
||||
];
|
||||
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
@@ -636,17 +653,65 @@ class SnapshotPlugin extends AutoenableMixinProvider implements MixinProvider, B
|
||||
bottom: mime.parameters.get('bottom'),
|
||||
});
|
||||
|
||||
const filename = ffmpegInput.url?.startsWith('file:') && new URL(ffmpegInput.url).pathname;
|
||||
if (filename && sharpInstance) {
|
||||
const vips = await loadVipsImage(filename, options?.sourceId);
|
||||
|
||||
const resize = width && {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
if (fractional) {
|
||||
if (resize.width)
|
||||
resize.width *= vips.width;
|
||||
if (resize.height)
|
||||
resize.height *= vips.height;
|
||||
}
|
||||
|
||||
const crop = left && {
|
||||
left,
|
||||
top,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
};
|
||||
|
||||
if (cropFractional) {
|
||||
crop.left *= vips.width;
|
||||
crop.top *= vips.height;
|
||||
crop.width *= vips.width;
|
||||
crop.height *= vips.height;
|
||||
}
|
||||
|
||||
try {
|
||||
const ret = await vips.toBuffer({
|
||||
resize,
|
||||
crop,
|
||||
format: 'jpg',
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
finally {
|
||||
vips.close();
|
||||
}
|
||||
}
|
||||
|
||||
const args = [
|
||||
...ffmpegInput.inputArguments,
|
||||
...(ffmpegInput.h264EncoderArguments || []),
|
||||
];
|
||||
|
||||
return ffmpegFilterImage(args, {
|
||||
console: this.debugConsole,
|
||||
ffmpegPath: await mediaManager.getFFmpegPath(),
|
||||
resize: (isNaN(width) && isNaN(height))
|
||||
resize: width === undefined && height === undefined
|
||||
? undefined
|
||||
: {
|
||||
width,
|
||||
height,
|
||||
fractional,
|
||||
},
|
||||
crop: (isNaN(left) && isNaN(top) && isNaN(right) && isNaN(bottom))
|
||||
crop: left === undefined || right === undefined || top === undefined || bottom === undefined
|
||||
? undefined
|
||||
: {
|
||||
left,
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.66.0",
|
||||
"version": "0.67.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.66.0",
|
||||
"version": "0.67.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.67.0",
|
||||
"version": "0.68.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
|
||||
@@ -45,7 +45,7 @@ class ClusterObject(TypedDict):
|
||||
id: str
|
||||
port: int
|
||||
proxyId: str
|
||||
sourcePort: str
|
||||
sourcePort: int
|
||||
sha256: str
|
||||
|
||||
|
||||
@@ -403,7 +403,7 @@ class PluginRemote:
|
||||
m.update(bytes(f"{o['id']}{o['port']}{o.get('sourcePort', '')}{o['proxyId']}{clusterSecret}", 'utf8'))
|
||||
return base64.b64encode(m.digest()).decode('utf-8')
|
||||
|
||||
def onProxySerialization(value: Any, proxyId: str, source: int = None):
|
||||
def onProxySerialization(value: Any, proxyId: str, sourcePeerPort: int = None):
|
||||
properties: dict = rpc.RpcPeer.prepareProxyProperties(value) or {}
|
||||
clusterEntry = properties.get('__cluster', None)
|
||||
if not properties.get('__cluster', None):
|
||||
@@ -411,13 +411,11 @@ class PluginRemote:
|
||||
'id': clusterId,
|
||||
'proxyId': proxyId,
|
||||
'port': clusterPort,
|
||||
'source': source,
|
||||
'sourcePort': sourcePeerPort,
|
||||
}
|
||||
clusterEntry['sha256'] = computeClusterObjectHash(clusterEntry)
|
||||
properties['__cluster'] = clusterEntry
|
||||
|
||||
# clusterEntry['proxyId'] = proxyId
|
||||
# clusterEntry['source'] = source
|
||||
return properties
|
||||
|
||||
self.peer.onProxySerialization = onProxySerialization
|
||||
@@ -498,18 +496,21 @@ class PluginRemote:
|
||||
|
||||
port = clusterObject['port']
|
||||
proxyId = clusterObject['proxyId']
|
||||
source = clusterObject.get('source', None)
|
||||
sourcePort = clusterObject.get('sourcePort', None)
|
||||
if port == clusterPort:
|
||||
return await resolveObject(proxyId, source)
|
||||
return await resolveObject(proxyId, sourcePort)
|
||||
|
||||
clusterPeerPromise = ensureClusterPeer(port)
|
||||
|
||||
try:
|
||||
clusterPeer = await clusterPeerPromise
|
||||
if clusterPeer.tags.get('localPort') == source:
|
||||
if clusterPeer.tags.get('localPort') == sourcePort:
|
||||
return value
|
||||
c = await clusterPeer.getParam('connectRPCObject')
|
||||
newValue = await c(clusterObject)
|
||||
peerConnectRPCObject = clusterPeer.tags.get('connectRPCObject')
|
||||
if not peerConnectRPCObject:
|
||||
peerConnectRPCObject = await clusterPeer.getParam('connectRPCObject')
|
||||
clusterPeer.tags['connectRPCObject'] = peerConnectRPCObject
|
||||
newValue = await peerConnectRPCObject(clusterObject)
|
||||
if not newValue:
|
||||
raise Exception('ipc object not found?')
|
||||
return newValue
|
||||
|
||||
@@ -181,8 +181,12 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
// so return the existing proxy.
|
||||
if (clusterPeer.tags.localPort === sourcePort)
|
||||
return value;
|
||||
const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
|
||||
const newValue = await connectRPCObject(clusterObject);
|
||||
let peerConnectRPCObject: ConnectRPCObject = clusterPeer.tags['connectRPCObject'];
|
||||
if (!peerConnectRPCObject) {
|
||||
peerConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
|
||||
clusterPeer.tags['connectRPCObject'] = peerConnectRPCObject;
|
||||
}
|
||||
const newValue = await peerConnectRPCObject(clusterObject);
|
||||
if (!newValue)
|
||||
throw new Error('rpc object not found?');
|
||||
return newValue;
|
||||
|
||||
Reference in New Issue
Block a user