mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08cf9f7774 | ||
|
|
9f2fabf9c0 | ||
|
|
e2e1c7be44 | ||
|
|
ba030ba197 | ||
|
|
a4f37bdc16 | ||
|
|
f6c7b00562 | ||
|
|
b951614f7c | ||
|
|
f1dfdb3494 | ||
|
|
ffbd25b13b | ||
|
|
4f03fe2420 | ||
|
|
ffdb386afa | ||
|
|
9eeeaa79d0 | ||
|
|
4163142d1e | ||
|
|
71cddc67e0 | ||
|
|
2cbc4eb54f | ||
|
|
fc94fb4221 | ||
|
|
85ed41c590 | ||
|
|
59f889a200 |
@@ -33,7 +33,8 @@ RUN apt-get -y install \
|
||||
gcc \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
pkg-config \
|
||||
libvips
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
@@ -61,6 +62,9 @@ RUN apt-get -y install \
|
||||
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -40,13 +40,14 @@ echo "Installing Scrypted dependencies..."
|
||||
RUN_IGNORE xcode-select --install
|
||||
RUN brew update
|
||||
RUN_IGNORE brew install node@18
|
||||
# needed by scrypted-ffmpeg
|
||||
RUN_IGNORE brew install sdl2
|
||||
# snapshot plugin and others
|
||||
RUN brew install libvips
|
||||
# gstreamer plugins
|
||||
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly
|
||||
# gst python bindings
|
||||
RUN_IGNORE brew install gst-python
|
||||
# python image library
|
||||
# todo: consider removing this
|
||||
RUN_IGNORE brew install pillow
|
||||
|
||||
### HACK WORKAROUND
|
||||
|
||||
@@ -30,7 +30,8 @@ RUN apt-get -y install \
|
||||
gcc \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
pkg-config \
|
||||
libvips
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
@@ -58,6 +59,9 @@ RUN apt-get -y install \
|
||||
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -17,6 +17,12 @@ 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
|
||||
screen: {
|
||||
devicePixelRatio: 1,
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.102",
|
||||
"version": "0.1.103",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.102",
|
||||
"version": "0.1.103",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.102",
|
||||
"version": "0.1.103",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
4
plugins/coreml/package-lock.json
generated
4
plugins/coreml/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.26",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.26",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.24"
|
||||
"version": "0.0.26"
|
||||
}
|
||||
|
||||
@@ -3,3 +3,8 @@ Pillow>=5.4.1
|
||||
PyGObject>=3.30.4
|
||||
coremltools~=6.1
|
||||
av>=10.0.0; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
|
||||
|
||||
# sort_oh
|
||||
scipy
|
||||
filterpy
|
||||
numpy
|
||||
|
||||
3
plugins/eufy/package-lock.json
generated
3
plugins/eufy/package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
}
|
||||
},
|
||||
"../../packages/h264-repacketizer": {
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.6",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
@@ -43,7 +44,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.84",
|
||||
"version": "0.2.85",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
|
||||
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
|
||||
import sdk, { Battery, Camera, Device, DeviceProvider, FFmpegInput, MediaObject, MotionSensor, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import eufy, { CaptchaOptions, EufySecurity } from 'eufy-security-client';
|
||||
import eufy, { CaptchaOptions, EufySecurity, P2PClientProtocol, P2PConnectionType } from 'eufy-security-client';
|
||||
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
|
||||
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
@@ -11,17 +11,14 @@ import { LocalLivestreamManager } from './stream';
|
||||
|
||||
const { deviceManager, mediaManager, systemManager } = sdk;
|
||||
|
||||
class EufyCamera extends ScryptedDeviceBase implements Camera, VideoCamera, Battery, MotionSensor {
|
||||
class EufyCamera extends ScryptedDeviceBase implements VideoCamera, MotionSensor {
|
||||
client: EufySecurity;
|
||||
device: eufy.Camera;
|
||||
livestreamManager: LocalLivestreamManager
|
||||
|
||||
constructor(nativeId: string, client: EufySecurity, device: eufy.Camera) {
|
||||
super(nativeId);
|
||||
this.client = client;
|
||||
this.device = device;
|
||||
this.livestreamManager = new LocalLivestreamManager(this.client, this.device, this.console);
|
||||
this.batteryLevel = this.device.getBatteryValue() as number;
|
||||
this.setupMotionDetection();
|
||||
}
|
||||
|
||||
@@ -37,31 +34,6 @@ class EufyCamera extends ScryptedDeviceBase implements Camera, VideoCamera, Batt
|
||||
this.device.on('radar motion detected', handle);
|
||||
}
|
||||
|
||||
async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
||||
// if this stream is prebuffered, its safe to use the prebuffer to generate an image
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
try {
|
||||
const msos = await realDevice.getVideoStreamOptions();
|
||||
const prebuffered: RequestMediaStreamOptions = msos.find(mso => mso.prebuffer);
|
||||
if (prebuffered) {
|
||||
prebuffered.refresh = false;
|
||||
return realDevice.getVideoStream(prebuffered);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// try to fetch the cloud image if one exists
|
||||
const url = this.device.getLastCameraImageURL();
|
||||
if (url) {
|
||||
return mediaManager.createMediaObjectFromUrl(url.toString());
|
||||
}
|
||||
|
||||
throw new Error("snapshot unavailable");
|
||||
}
|
||||
|
||||
getPictureOptions(): Promise<ResponsePictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
|
||||
getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
|
||||
return this.createVideoStream(options);
|
||||
}
|
||||
@@ -80,15 +52,32 @@ class EufyCamera extends ScryptedDeviceBase implements Camera, VideoCamera, Batt
|
||||
},
|
||||
tool: 'scrypted',
|
||||
userConfigurable: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
container: 'rtsp',
|
||||
id: 'p2p-low',
|
||||
name: 'P2P (Low Resolution)',
|
||||
video: {
|
||||
codec: 'h264',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
audio: {
|
||||
codec: 'aac',
|
||||
},
|
||||
tool: 'scrypted',
|
||||
userConfigurable: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async createVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
|
||||
const livestreamManager = new LocalLivestreamManager(options.id, this.client, this.device, this.console);
|
||||
|
||||
const kill = new Deferred<void>();
|
||||
kill.promise.finally(() => {
|
||||
this.console.log('video stream exited');
|
||||
this.livestreamManager.stopLocalLiveStream();
|
||||
livestreamManager.stopLocalLiveStream();
|
||||
});
|
||||
|
||||
const rtspServer = await listenSingleRtspClient();
|
||||
@@ -138,7 +127,7 @@ class EufyCamera extends ScryptedDeviceBase implements Camera, VideoCamera, Batt
|
||||
await rtsp.handlePlayback();
|
||||
});
|
||||
|
||||
const proxyStream = await this.livestreamManager.getLocalLivestream();
|
||||
const proxyStream = await livestreamManager.getLocalLivestream();
|
||||
proxyStream.videostream.pipe(process.cp.stdio[4] as Writable);
|
||||
proxyStream.audiostream.pipe((process.cp.stdio as any)[5] as Writable);
|
||||
}
|
||||
@@ -240,14 +229,13 @@ class EufyPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
|
||||
password: this.storageSettings.values.password,
|
||||
country: this.storageSettings.values.country,
|
||||
language: 'en',
|
||||
p2pConnectionSetup: 2,
|
||||
p2pConnectionSetup: P2PConnectionType.QUICKEST,
|
||||
pollingIntervalMinutes: 10,
|
||||
eventDurationSeconds: 10
|
||||
}
|
||||
this.client = await EufySecurity.initialize(config);
|
||||
this.client.on('device added', this.deviceAdded.bind(this));
|
||||
this.client.on('station added', this.stationAdded.bind(this));
|
||||
|
||||
this.client.on('tfa request', () => {
|
||||
this.log.a('Login failed: 2FA is enabled, check your email or texts for your code, then enter it into the Two Factor Code setting to conplete login.');
|
||||
});
|
||||
@@ -277,7 +265,6 @@ class EufyPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
|
||||
const nativeId = eufyDevice.getSerial();
|
||||
|
||||
const interfaces = [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.VideoCamera
|
||||
];
|
||||
if (eufyDevice.hasBattery())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Based off of https://github.com/homebridge-eufy-security/plugin/blob/master/src/plugin/controller/LocalLivestreamManager.ts
|
||||
|
||||
import { Camera, CommandData, CommandName, CommandType, Device, DeviceType, EufySecurity, isGreaterEqualMinVersion, P2PClientProtocol, ParamType, Station, StreamMetadata, VideoCodec } from 'eufy-security-client';
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import { Station, Device, StreamMetadata, Camera, EufySecurity } from 'eufy-security-client';
|
||||
|
||||
type StationStream = {
|
||||
station: Station;
|
||||
@@ -19,28 +19,39 @@ export class LocalLivestreamManager extends EventEmitter {
|
||||
private livestreamStartedAt: number | null;
|
||||
private livestreamIsStarting = false;
|
||||
|
||||
private readonly id: string;
|
||||
private readonly client: EufySecurity;
|
||||
private readonly device: Camera;
|
||||
|
||||
constructor(client: EufySecurity, device: Camera, console: Console) {
|
||||
private station: Station;
|
||||
private p2pSession: P2PClientProtocol;
|
||||
|
||||
constructor(id: string, client: EufySecurity, device: Camera, console: Console) {
|
||||
super();
|
||||
|
||||
this.id = id;
|
||||
this.console = console;
|
||||
this.client = client;
|
||||
this.device = device;
|
||||
|
||||
this.client.getStation(this.device.getStationSerial()).then( (station) => {
|
||||
this.station = station;
|
||||
this.p2pSession = new P2PClientProtocol(station.getRawStation(), this.client.getApi(), station.getIPAddress());
|
||||
this.p2pSession.on("livestream started", (channel: number, metadata: StreamMetadata, videostream: Readable, audiostream: Readable) => {
|
||||
this.onStationLivestreamStart(station, device, metadata, videostream, audiostream);
|
||||
});
|
||||
this.p2pSession.on("livestream stopped", (channel: number) => {
|
||||
this.onStationLivestreamStop(station, device);
|
||||
});
|
||||
this.p2pSession.on("livestream error", (channel: number, error: Error) => {
|
||||
this.stopLivestream();
|
||||
});
|
||||
});
|
||||
|
||||
this.stationStream = null;
|
||||
this.livestreamStartedAt = null;
|
||||
|
||||
this.initialize();
|
||||
|
||||
this.client.on('station livestream stop', (station: Station, device: Device) => {
|
||||
this.onStationLivestreamStop(station, device);
|
||||
});
|
||||
this.client.on('station livestream start',
|
||||
(station: Station, device: Device, metadata: StreamMetadata, videostream: Readable, audiostream: Readable) => {
|
||||
this.onStationLivestreamStart(station, device, metadata, videostream, audiostream);
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
@@ -55,10 +66,10 @@ export class LocalLivestreamManager extends EventEmitter {
|
||||
}
|
||||
|
||||
public async getLocalLivestream(): Promise<StationStream> {
|
||||
this.console.debug(this.device.getName(), 'New instance requests livestream.');
|
||||
this.console.debug(this.device.getName(), this.id, 'New instance requests livestream.');
|
||||
if (this.stationStream) {
|
||||
const runtime = (Date.now() - this.livestreamStartedAt!) / 1000;
|
||||
this.console.debug(this.device.getName(), 'Using livestream that was started ' + runtime + ' seconds ago.');
|
||||
this.console.debug(this.device.getName(), this.id, 'Using livestream that was started ' + runtime + ' seconds ago.');
|
||||
return this.stationStream;
|
||||
} else {
|
||||
return await this.startAndGetLocalLiveStream();
|
||||
@@ -67,17 +78,17 @@ export class LocalLivestreamManager extends EventEmitter {
|
||||
|
||||
private async startAndGetLocalLiveStream(): Promise<StationStream> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.console.debug(this.device.getName(), 'Start new station livestream (P2P Session)...');
|
||||
this.console.debug(this.device.getName(), this.id, 'Start new station livestream...');
|
||||
if (!this.livestreamIsStarting) { // prevent multiple stream starts from eufy station
|
||||
this.livestreamIsStarting = true;
|
||||
this.client.startStationLivestream(this.device.getSerial());
|
||||
this.startStationLivestream();
|
||||
} else {
|
||||
this.console.debug(this.device.getName(), 'stream is already starting. waiting...');
|
||||
this.console.debug(this.device.getName(), this.id, 'stream is already starting. waiting...');
|
||||
}
|
||||
|
||||
this.once('livestream start', async () => {
|
||||
if (this.stationStream !== null) {
|
||||
this.console.debug(this.device.getName(), 'New livestream started.');
|
||||
this.console.debug(this.device.getName(), this.id, 'New livestream started.');
|
||||
this.livestreamIsStarting = false;
|
||||
resolve(this.stationStream);
|
||||
} else {
|
||||
@@ -87,15 +98,102 @@ export class LocalLivestreamManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
private async startStationLivestream(videoCodec: VideoCodec = VideoCodec.H264): Promise<void> {
|
||||
const commandData: CommandData = {
|
||||
name: CommandName.DeviceStartLivestream,
|
||||
value: videoCodec
|
||||
};
|
||||
this.console.debug(this.device.getName(), this.id, `Sending start livestream command to station ${this.station.getSerial()}`);
|
||||
const rsa_key = this.p2pSession.getRSAPrivateKey();
|
||||
|
||||
if (this.device.isSoloCameras() || this.device.getDeviceType() === DeviceType.FLOODLIGHT_CAMERA_8423 || this.device.isWiredDoorbellT8200X()) {
|
||||
this.console.debug(this.device.getName(), this.id, `Using CMD_DOORBELL_SET_PAYLOAD (1) for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
|
||||
await this.p2pSession.sendCommandWithStringPayload({
|
||||
commandType: CommandType.CMD_DOORBELL_SET_PAYLOAD,
|
||||
value: JSON.stringify({
|
||||
"commandType": ParamType.COMMAND_START_LIVESTREAM,
|
||||
"data": {
|
||||
"accountId": this.station.getRawStation().member.admin_user_id,
|
||||
"encryptkey": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
|
||||
"streamtype": videoCodec
|
||||
}
|
||||
}),
|
||||
channel: this.device.getChannel()
|
||||
}, {
|
||||
command: commandData
|
||||
});
|
||||
} else if (this.device.isWiredDoorbell() || (this.device.isFloodLight() && this.device.getDeviceType() !== DeviceType.FLOODLIGHT) || this.device.isIndoorCamera() || (this.device.getSerial().startsWith("T8420") && isGreaterEqualMinVersion("2.0.4.8", this.station.getSoftwareVersion()))) {
|
||||
this.console.debug(this.device.getName(), this.id, `Using CMD_DOORBELL_SET_PAYLOAD (2) for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
|
||||
await this.p2pSession.sendCommandWithStringPayload({
|
||||
commandType: CommandType.CMD_DOORBELL_SET_PAYLOAD,
|
||||
value: JSON.stringify({
|
||||
"commandType": ParamType.COMMAND_START_LIVESTREAM,
|
||||
"data": {
|
||||
"account_id": this.station.getRawStation().member.admin_user_id,
|
||||
"encryptkey": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
|
||||
"streamtype": videoCodec
|
||||
}
|
||||
}),
|
||||
channel: this.device.getChannel()
|
||||
}, {
|
||||
command: commandData
|
||||
});
|
||||
} else {
|
||||
if ((Device.isIntegratedDeviceBySn(this.station.getSerial()) || !isGreaterEqualMinVersion("2.0.9.7", this.station.getSoftwareVersion())) && (!this.station.getSerial().startsWith("T8420") || !isGreaterEqualMinVersion("1.0.0.25", this.station.getSoftwareVersion()))) {
|
||||
this.console.debug(this.device.getName(), this.id, `Using CMD_START_REALTIME_MEDIA for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
|
||||
await this.p2pSession.sendCommandWithInt({
|
||||
commandType: CommandType.CMD_START_REALTIME_MEDIA,
|
||||
value: this.device.getChannel(),
|
||||
strValue: rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
|
||||
channel: this.device.getChannel()
|
||||
}, {
|
||||
command: commandData
|
||||
});
|
||||
} else {
|
||||
this.console.debug(this.device.getName(), this.id, `Using CMD_SET_PAYLOAD for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
|
||||
await this.p2pSession.sendCommandWithStringPayload({
|
||||
commandType: CommandType.CMD_SET_PAYLOAD,
|
||||
value: JSON.stringify({
|
||||
"account_id": this.station.getRawStation().member.admin_user_id,
|
||||
"cmd": CommandType.CMD_START_REALTIME_MEDIA,
|
||||
"mValue3": CommandType.CMD_START_REALTIME_MEDIA,
|
||||
"payload": {
|
||||
"ClientOS": "Android",
|
||||
"key": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
|
||||
"streamtype": videoCodec === VideoCodec.H264 ? 1 : 2,
|
||||
}
|
||||
}),
|
||||
channel: this.device.getChannel()
|
||||
}, {
|
||||
command: commandData
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stopLocalLiveStream(): void {
|
||||
this.console.debug(this.device.getName(), 'Stopping station livestream.');
|
||||
this.client.stopStationLivestream(this.device.getSerial());
|
||||
this.console.debug(this.device.getName(), this.id, 'Stopping station livestream.');
|
||||
this.stopLivestream();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private async stopLivestream(): Promise<void> {
|
||||
const commandData: CommandData = {
|
||||
name: CommandName.DeviceStopLivestream
|
||||
};
|
||||
this.console.debug(this.device.getName(), this.id, `Sending stop livestream command to station ${this.station.getSerial()}`);
|
||||
await this.p2pSession.sendCommandWithInt({
|
||||
commandType: CommandType.CMD_STOP_REALTIME_MEDIA,
|
||||
value: this.device.getChannel(),
|
||||
channel: this.device.getChannel()
|
||||
}, {
|
||||
command: commandData
|
||||
});
|
||||
}
|
||||
|
||||
private onStationLivestreamStop(station: Station, device: Device) {
|
||||
if (device.getSerial() === this.device.getSerial()) {
|
||||
this.console.info(station.getName() + ' station livestream for ' + device.getName() + ' has stopped.');
|
||||
this.console.info(this.id + ' - ' + station.getName() + ' station livestream for ' + device.getName() + ' has stopped.');
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
@@ -111,17 +209,17 @@ export class LocalLivestreamManager extends EventEmitter {
|
||||
if (this.stationStream) {
|
||||
const diff = (Date.now() - this.stationStream.createdAt) / 1000;
|
||||
if (diff < 5) {
|
||||
this.console.warn(this.device.getName(), 'Second livestream was started from station. Ignore.');
|
||||
this.console.warn(this.device.getName(), this.id, 'Second livestream was started from station. Ignore.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.initialize(); // important to prevent unwanted behaviour when the eufy station emits the 'livestream start' event multiple times
|
||||
|
||||
this.console.info(station.getName() + ' station livestream (P2P session) for ' + device.getName() + ' has started.');
|
||||
this.console.info(this.id + ' - ' + station.getName() + ' station livestream (P2P session) for ' + device.getName() + ' has started.');
|
||||
this.livestreamStartedAt = Date.now();
|
||||
const createdAt = Date.now();
|
||||
this.stationStream = {station, device, metadata, videostream, audiostream, createdAt};
|
||||
this.console.debug(this.device.getName(), 'Stream metadata: ' + JSON.stringify(this.stationStream.metadata));
|
||||
this.console.debug(this.device.getName(), this.id, 'Stream metadata: ' + JSON.stringify(this.stationStream.metadata));
|
||||
|
||||
this.emit('livestream start');
|
||||
}
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.102",
|
||||
"version": "0.0.103",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.102",
|
||||
"version": "0.0.103",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.102",
|
||||
"version": "0.0.103",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
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.0.23",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.0.23",
|
||||
"version": "0.1.2",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"description": "Scrypted Python Codecs",
|
||||
"version": "0.1.2",
|
||||
"description": "Python Codecs for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
@@ -29,6 +30,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.23"
|
||||
}
|
||||
}
|
||||
|
||||
4
plugins/snapshot/package-lock.json
generated
4
plugins/snapshot/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.48",
|
||||
"version": "0.0.49",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.48",
|
||||
"version": "0.0.49",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@types/node": "^16.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.0.48",
|
||||
"version": "0.0.49",
|
||||
"description": "Snapshot Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
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.110",
|
||||
"version": "0.0.112",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.0.110",
|
||||
"version": "0.0.112",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.110"
|
||||
"version": "0.0.112"
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
# this is useful for high quality thumbnails.
|
||||
return (None, None)
|
||||
|
||||
def get_input_size(self) -> Tuple[float, float]:
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
pass
|
||||
|
||||
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
|
||||
|
||||
@@ -88,8 +88,9 @@ class TensorFlowLitePlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted
|
||||
0]['shape']
|
||||
return int(width), int(height), int(channels)
|
||||
|
||||
def get_input_size(self) -> Tuple[float, float]:
|
||||
return input_size(self.interpreter)
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
w, h = input_size(self.interpreter)
|
||||
return int(w), int(h)
|
||||
|
||||
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
try:
|
||||
|
||||
4
plugins/tensorflow/package-lock.json
generated
4
plugins/tensorflow/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.3"
|
||||
"version": "0.1.4"
|
||||
}
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.5",
|
||||
"version": "0.7.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.5",
|
||||
"version": "0.7.7",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
|
||||
@@ -8,6 +8,7 @@ import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import time
|
||||
import traceback
|
||||
import zipfile
|
||||
@@ -35,6 +36,21 @@ class SystemDeviceState(TypedDict):
|
||||
value: any
|
||||
|
||||
|
||||
class StreamPipeReader:
|
||||
def __init__(self, conn: multiprocessing.connection.Connection) -> None:
|
||||
self.conn = conn
|
||||
self.executor = concurrent.futures.ThreadPoolExecutor()
|
||||
|
||||
def readBlocking(self, n):
|
||||
b = bytes(0)
|
||||
while len(b) < n:
|
||||
self.conn.poll()
|
||||
b += os.read(self.conn.fileno(), n - len(b))
|
||||
return b
|
||||
|
||||
async def read(self, n):
|
||||
return await asyncio.get_event_loop().run_in_executor(self.executor, lambda: self.readBlocking(n))
|
||||
|
||||
class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
|
||||
def __init__(self, api: Any, systemState: Mapping[str, Mapping[str, SystemDeviceState]]) -> None:
|
||||
super().__init__()
|
||||
@@ -467,14 +483,17 @@ class PluginRemote:
|
||||
|
||||
async def getFork():
|
||||
fd = os.dup(parent_conn.fileno())
|
||||
forkPeer, readLoop = await rpc_reader.prepare_peer_readloop(self.loop, fd, fd)
|
||||
reader = StreamPipeReader(parent_conn)
|
||||
forkPeer, readLoop = await rpc_reader.prepare_peer_readloop(self.loop, reader = reader, writeFd = fd)
|
||||
forkPeer.peerName = 'thread'
|
||||
async def forkReadLoop():
|
||||
try:
|
||||
await readLoop()
|
||||
except:
|
||||
# traceback.print_exc()
|
||||
print('fork read loop exited')
|
||||
pass
|
||||
finally:
|
||||
reader.executor.shutdown()
|
||||
asyncio.run_coroutine_threadsafe(forkReadLoop(), loop=self.loop)
|
||||
getRemote = await forkPeer.getParam('getRemote')
|
||||
remote: PluginRemote = await getRemote(self.api, self.pluginId, self.hostInfo)
|
||||
@@ -563,13 +582,15 @@ class PluginRemote:
|
||||
async def getServicePort(self, name):
|
||||
pass
|
||||
|
||||
async def plugin_async_main(loop: AbstractEventLoop, readFd: int, writeFd: int):
|
||||
peer, readLoop = await rpc_reader.prepare_peer_readloop(loop, readFd, writeFd)
|
||||
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
|
||||
peer.params['getRemote'] = lambda api, pluginId, hostInfo: PluginRemote(peer, api, pluginId, hostInfo, loop)
|
||||
|
||||
async def get_update_stats():
|
||||
update_stats = await peer.getParam('updateStats')
|
||||
if not update_stats:
|
||||
return
|
||||
|
||||
def stats_runner():
|
||||
ptime = round(time.process_time() * 1000000)
|
||||
@@ -601,10 +622,14 @@ async def plugin_async_main(loop: AbstractEventLoop, readFd: int, writeFd: int):
|
||||
|
||||
asyncio.run_coroutine_threadsafe(get_update_stats(), loop)
|
||||
|
||||
await readLoop()
|
||||
try:
|
||||
await readLoop()
|
||||
finally:
|
||||
if reader and hasattr(reader, 'executor'):
|
||||
r: StreamPipeReader = reader
|
||||
r.executor.shutdown()
|
||||
|
||||
|
||||
def main(readFd: int, writeFd: int):
|
||||
def main(readFd: int = None, writeFd: int = None, reader: asyncio.StreamReader = None, writer: asyncio.StreamWriter = None):
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
def gc_runner():
|
||||
@@ -612,10 +637,10 @@ def main(readFd: int, writeFd: int):
|
||||
loop.call_later(10, gc_runner)
|
||||
gc_runner()
|
||||
|
||||
loop.run_until_complete(plugin_async_main(loop, readFd, writeFd))
|
||||
loop.run_until_complete(plugin_async_main(loop, readFd=readFd, writeFd=writeFd, reader=reader, writer=writer))
|
||||
loop.close()
|
||||
|
||||
def plugin_main(readFd: int, writeFd: int):
|
||||
def plugin_main(readFd: int = None, writeFd: int = None, reader: asyncio.StreamReader = None, writer: asyncio.StreamWriter = None):
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
@@ -624,17 +649,18 @@ def plugin_main(readFd: int, writeFd: int):
|
||||
|
||||
loop = GLib.MainLoop()
|
||||
|
||||
worker = threading.Thread(target=main, args=(readFd, writeFd), name="asyncio-main")
|
||||
worker = threading.Thread(target=main, args=(readFd, writeFd, reader, writer), name="asyncio-main")
|
||||
worker.start()
|
||||
|
||||
loop.run()
|
||||
except:
|
||||
main(readFd, writeFd)
|
||||
main(readFd=readFd, writeFd=writeFd, reader=reader, writer=writer)
|
||||
|
||||
|
||||
def plugin_fork(conn: multiprocessing.connection.Connection):
|
||||
fd = os.dup(conn.fileno())
|
||||
plugin_main(fd, fd)
|
||||
reader = StreamPipeReader(conn)
|
||||
plugin_main(reader=reader, writeFd=fd)
|
||||
|
||||
if __name__ == "__main__":
|
||||
plugin_main(3, 4)
|
||||
|
||||
@@ -385,7 +385,7 @@ export abstract class MediaManagerBase implements MediaManager {
|
||||
node[candidateId] = inputWeight + outputWeight;
|
||||
}
|
||||
catch (e) {
|
||||
console.warn(converter.name, 'skipping converter due to error', e)
|
||||
console.warn(candidate.name, 'skipping converter due to error', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user