mixin improvements, homekit settings

This commit is contained in:
Koushik Dutta
2021-09-01 14:47:47 -07:00
parent 4e4610b08c
commit 580c6a82ac
17 changed files with 143 additions and 71 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.16",
"version": "0.0.17",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.16",
"version": "0.0.17",
"license": "Apache",
"dependencies": {
"@mhoc/axios-digest-auth": "^0.7.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.16",
"version": "0.0.17",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,7 +1,10 @@
<template>
<div>
<v-card
v-for="[key, settingsGroup] of Object.entries(settingsGroups)"
v-for="([key, settingsGroup], index) of Object.entries(settingsGroups)"
:class="
index === Object.entries(settingsGroups).length - 1 ? undefined : 'mb-6'
"
:key="key"
>
<v-card-title
@@ -9,18 +12,14 @@
>{{ key }}</v-card-title
>
<v-flex xs12>
<v-layout>
<v-flex xs12>
<v-form>
<Setting
:device="device"
v-for="setting in settingsGroup"
:key="setting.key"
v-model="setting.value"
></Setting>
</v-form>
</v-flex>
</v-layout>
<v-form>
<Setting
:device="device"
v-for="setting in settingsGroup"
:key="setting.key"
v-model="setting.value"
></Setting>
</v-form>
</v-flex>
</v-card>
</div>
@@ -51,22 +50,22 @@ export default {
settingsGroups() {
const ret = {};
for (const setting of this.settings) {
const group = setting.value.group || 'Settings';
const group = setting.value.group || "Settings";
if (!ret[group]) {
ret[group] = [];
}
ret[group].push(setting);
}
return ret;
}
},
},
methods: {
async refresh() {
const blub = this.rpc().getSettings();
var settings = await blub;
this.settings = this.settings = settings.map(setting => ({
this.settings = this.settings = settings.map((setting) => ({
key: setting.key,
value: setting
value: setting,
}));
},
},

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/google-device-access",
"version": "0.0.9",
"version": "0.0.10",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/google-device-access",
"version": "0.0.9",
"version": "0.0.10",
"dependencies": {
"@googleapis/smartdevicemanagement": "^0.2.0",
"axios": "^0.21.1",

View File

@@ -36,5 +36,5 @@
"@types/node": "^14.17.11",
"@types/url-parse": "^1.4.3"
},
"version": "0.0.9"
"version": "0.0.10"
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/homekit",
"version": "0.0.14",
"version": "0.0.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/homekit",
"version": "0.0.14",
"version": "0.0.15",
"dependencies": {
"axios": "^0.21.1",
"hap-nodejs": "file:../HAP-NodeJS",

View File

@@ -36,5 +36,5 @@
"@types/node": "^14.17.9",
"@types/url-parse": "^1.4.3"
},
"version": "0.0.14"
"version": "0.0.15"
}

View File

@@ -0,0 +1,36 @@
import { MixinDeviceBase, VideoCamera, Settings, Setting, ScryptedInterface } from "@scrypted/sdk";
export class CameraMixin extends MixinDeviceBase<VideoCamera & Settings> implements Settings {
constructor(mixinDevice: VideoCamera & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string) {
super(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, providerNativeId);
}
async getSettings(): Promise<Setting[]> {
const settings = this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings) ?
await this.mixinDevice.getSettings() : [];
settings.push({
group: 'HomeKit Settings',
title: 'Transcode Streaming',
type: 'boolean',
key: 'transcodeStreaming',
value: (this.storage.getItem('transcodeStreaming') === 'true').toString(),
description: 'Use FFMpeg to transcode streaming to a format supported by HomeKit.',
});
if (this.interfaces.includes(ScryptedInterface.MotionSensor)) {
settings.push({
group: 'HomeKit Settings',
title: 'Transcode Recording',
key: 'transcodeRecording',
type: 'boolean',
value: (this.storage.getItem('transcodeRecording') === 'true').toString(),
description: 'Use FFMpeg to transcode recording to a format supported by HomeKit Secure Video.',
});
}
return settings;
}
async putSetting(key: string, value: string | number | boolean) {
this.storage.setItem(key, value.toString());
}
}

View File

@@ -1,8 +1,9 @@
import sdk, { Settings, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, Setting } from '@scrypted/sdk';
import sdk, { Settings, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, Setting, ScryptedInterface } from '@scrypted/sdk';
import { Bridge, Categories, HAPStorage } from './hap';
import os from 'os';
import { supportedTypes } from './common';
import './types'
import { CameraMixin } from './camera-mixin';
const { systemManager, mediaManager } = sdk;
@@ -70,7 +71,6 @@ class HomeKit extends ScryptedDeviceBase implements MixinProvider, Settings {
}
async start() {
let defaultIncluded: any;
try {
defaultIncluded = JSON.parse(this.storage.getItem('defaultIncluded'));
@@ -137,10 +137,18 @@ class HomeKit extends ScryptedDeviceBase implements MixinProvider, Settings {
})) {
return null;
}
if (type === ScryptedDeviceType.Camera) {
return [ScryptedInterface.Settings];
}
return [];
}
getMixin(device: ScryptedDevice, deviceState: any) {
return device;
getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
if ((mixinDeviceState.type === ScryptedDeviceType.Camera || mixinDeviceState.type === ScryptedDeviceType.Doorbell)
&& mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
return new CameraMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId);
}
return mixinDevice;
}
}

View File

@@ -15,7 +15,7 @@ import { CameraRecordingDelegate, CharacteristicEventTypes, CharacteristicValue,
import { AudioRecordingCodec, AudioRecordingCodecType, AudioRecordingSamplerate, AudioRecordingSamplerateValues, CameraRecordingConfiguration, CameraRecordingOptions } from '../../HAP-NodeJS/src/lib/camera/RecordingManagement';
import { startFFMPegFragmetedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session';
const { log, mediaManager } = sdk;
const { log, mediaManager, deviceManager } = sdk;
async function getPort(): Promise<{ socket: dgram.Socket, port: number }> {
const socket = dgram.createSocket('udp4');
@@ -53,14 +53,26 @@ async function* handleFragmentsRequests(device: ScryptedDevice & VideoCamera & M
const level = configuration.videoCodec.level === H264Level.LEVEL4_0 ? '4.0'
: configuration.videoCodec.level === H264Level.LEVEL3_2 ? '3.2' : '3.1';
const videoArgs: string[] = [
'-profile:v', profile,
'-level:v', level,
'-b:v', `${configuration.videoCodec.bitrate}k`,
'-force_key_frames', `expr:gte(t,n_forced*${iframeIntervalSeconds})`,
'-r', configuration.videoCodec.resolution[2].toString(),
'-vf', `scale=w=${configuration.videoCodec.resolution[0]}:h=${configuration.videoCodec.resolution[1]}:force_original_aspect_ratio=1,pad=${configuration.videoCodec.resolution[0]}:${configuration.videoCodec.resolution[1]}:(ow-iw)/2:(oh-ih)/2`,
];
const storage = deviceManager.getMixinStorage(device.id);
const transcodeRecording = storage.getItem('transcodeRecording') === 'true';
let videoArgs: string[];
if (transcodeRecording) {
videoArgs = [
'-profile:v', profile,
'-level:v', level,
'-b:v', `${configuration.videoCodec.bitrate}k`,
'-force_key_frames', `expr:gte(t,n_forced*${iframeIntervalSeconds})`,
'-r', configuration.videoCodec.resolution[2].toString(),
'-vf', `scale=w=${configuration.videoCodec.resolution[0]}:h=${configuration.videoCodec.resolution[1]}:force_original_aspect_ratio=1,pad=${configuration.videoCodec.resolution[0]}:${configuration.videoCodec.resolution[1]}:(ow-iw)/2:(oh-ih)/2`,
];
}
else {
videoArgs = [
'-vcodec', 'copy',
];
}
log.i(`${device.name} motion recording starting`);
const session = await startFFMPegFragmetedMP4Session(ffmpegInput, audioArgs, videoArgs);
@@ -230,20 +242,31 @@ addSupportedType({
args.push(...ffmpegInput.inputArguments);
args.push(
"-an", '-sn', '-dn',
);
"-vcodec", "copy",
// "-vcodec", "libx264",
// '-pix_fmt', 'yuvj420p',
// "-profile:v", "high",
// '-color_range', 'mpeg',
// "-bf", "0",
// "-b:v", request.video.max_bit_rate.toString() + "k",
// "-bufsize", (2 * request.video.max_bit_rate).toString() + "k",
// "-maxrate", request.video.max_bit_rate.toString() + "k",
// "-filter:v", "fps=fps=" + request.video.fps.toString(),
const storage = deviceManager.getMixinStorage(device.id);
const transcodeStreaming = storage.getItem('transcodeStreaming') === 'true';
if (transcodeStreaming) {
args.push(
"-vcodec", "libx264",
'-pix_fmt', 'yuvj420p',
"-profile:v", "high",
'-color_range', 'mpeg',
"-bf", "0",
"-b:v", request.video.max_bit_rate.toString() + "k",
"-bufsize", (2 * request.video.max_bit_rate).toString() + "k",
"-maxrate", request.video.max_bit_rate.toString() + "k",
"-filter:v", "fps=fps=" + request.video.fps.toString(),
)
}
else {
args.push(
"-vcodec", "copy",
);
}
args.push(
"-payload_type", (request as StartStreamRequest).video.pt.toString(),
"-ssrc", session.videossrc.toString(),
"-f", "rtp",

View File

@@ -17,8 +17,8 @@ async function audioFetch(body: string): Promise<string> {
var memoizedAudioFetch = memoizeOne(audioFetch);
class NotifierMixin extends MixinDeviceBase<MediaPlayer> implements Notifier {
constructor(mixinDevice: ScryptedDevice & MediaPlayer, deviceState: any) {
super(mixinDevice, deviceState);
constructor(mixinDevice: MediaPlayer, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string) {
super(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, providerNativeId);
}
async sendNotification(title: string, body: string, media: string | MediaObject, mimeType: string): Promise<void> {
@@ -53,8 +53,8 @@ class NotifierProvider extends ScryptedDeviceBase implements MixinProvider {
return [ScryptedInterface.Notifier];
}
getMixin(device: ScryptedDevice, deviceState: any) {
return new NotifierMixin(device as any, deviceState);
getMixin(mixinDevice: MediaPlayer, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
return new NotifierMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId);
}
}

View File

@@ -16,7 +16,7 @@ interface Prebuffer {
time: number;
}
class PrebufferMixin extends MixinDeviceBase<VideoCamera> implements VideoCamera, Settings {
class PrebufferMixin extends MixinDeviceBase<VideoCamera & Settings> implements VideoCamera, Settings {
prebufferSession: Promise<FFMpegFragmentedMP4Session>;
prebuffer: Prebuffer[] = [];
ftyp: MP4Atom;
@@ -24,8 +24,8 @@ class PrebufferMixin extends MixinDeviceBase<VideoCamera> implements VideoCamera
events = new EventEmitter();
released = false;
constructor(mixinDevice: ScryptedDevice & VideoCamera, deviceState: any, nativeId: string|undefined) {
super(mixinDevice, deviceState, nativeId);
constructor(mixinDevice: VideoCamera & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string) {
super(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, providerNativeId);
// to prevent noisy startup/reload/shutdown, delay the prebuffer starting.
log.i(`${this.name} prebuffer session starting in 10 seconds`);
@@ -33,7 +33,10 @@ class PrebufferMixin extends MixinDeviceBase<VideoCamera> implements VideoCamera
}
async getSettings(): Promise<Setting[]> {
return [
const settings = this.interfaces.includes(ScryptedInterface.Settings) ?
await this.mixinDevice.getSettings() : [];
settings.push(
{
title: 'Prebuffer Duration',
description: 'Duration of the prebuffer in milliseconds.',
@@ -41,7 +44,8 @@ class PrebufferMixin extends MixinDeviceBase<VideoCamera> implements VideoCamera
key: PREBUFFER_DURATION_MS,
value: this.storage.getItem(PREBUFFER_DURATION_MS) || defaultPrebufferDuration.toString(),
}
];
);
return settings;
}
async putSetting(key: string, value: string | number | boolean): Promise<void> {
this.storage.setItem(key, value.toString());
@@ -198,8 +202,8 @@ class PrebufferProvider extends ScryptedDeviceBase implements MixinProvider {
return [ScryptedInterface.VideoCamera, ScryptedInterface.Settings];
}
getMixin(device: ScryptedDevice, deviceState: any) {
return new PrebufferMixin(device as ScryptedDevice & VideoCamera, deviceState, this.nativeId);
getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
return new PrebufferMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId);
}
}

5
sdk/index.d.ts vendored
View File

@@ -1,5 +1,5 @@
export * from './types'
import { ScryptedStatic, ScryptedDeviceType, Logger, ColorRgb, ColorHsv, DeviceState, TemperatureUnit, LockState, ThermostatMode, Position, ScryptedDevice } from './types';
import { ScryptedInterface, ScryptedStatic, ScryptedDeviceType, Logger, ColorRgb, ColorHsv, DeviceState, TemperatureUnit, LockState, ThermostatMode, Position, ScryptedDevice } from './types';
export class ScryptedDeviceBase implements DeviceState {
constructor(nativeId?: string);
@@ -64,8 +64,9 @@ export class ScryptedDeviceBase implements DeviceState {
export class MixinDeviceBase<T> implements DeviceState {
constructor(mixinDevice: T, deviceState: any, providerNativeId: string);
constructor(mixinDevice: T, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string);
mixinDevice: ScryptedDevice & T;
mixinDeviceInterfaces: ScryptedInterface[];
console: Console;
storage: Storage;
id?: string;

View File

@@ -40,15 +40,17 @@ class ScryptedDeviceBase {
}
class MixinDeviceBase {
constructor(mixinDevice, deviceState, providerNativeId) {
constructor(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, providerNativeId) {
this.mixinDevice = mixinDevice;
this._deviceState = deviceState;
this.mixinDevice = mixinDevice;
this.mixinDeviceInterfaces = mixinDeviceInterfaces;
this._deviceState = mixinDeviceState;
this.providerNativeId = providerNativeId;
}
get storage() {
if (!this._storage) {
this._storage = deviceManager.getMixinStorage(this.providerNativeId);
this._storage = deviceManager.getMixinStorage(this.id, this.providerNativeId);
}
return this._storage;
}

2
sdk/types.d.ts vendored
View File

@@ -849,7 +849,7 @@ export interface MixinProvider {
/**
* Create a mixin that can be applied to the supplied device.
*/
getMixin(device: ScryptedDevice, deviceState: any): any;
getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): any;
}
/**

View File

@@ -86,7 +86,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
const host = this.scrypted.getPluginHostForDeviceId(mixinId);
const deviceState = await host.remote.createDeviceState(this.id,
async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value));
const mixinProxy = await mixin.getMixin(wrappedProxy, deviceState);
const mixinProxy = await mixin.getMixin(wrappedProxy, allInterfaces, deviceState);
if (!mixinProxy)
throw new Error(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
allInterfaces.push(...interfaces);

View File

@@ -424,7 +424,7 @@ export function startPluginClusterWorker() {
const stderr = new PassThrough();
stdout.on('data', data => events.emit('stdout', data, nativeId));
stderr.on('data', data => this.events.emit('stderr', data, nativeId));
stderr.on('data', data => events.emit('stderr', data, nativeId));
const ret = new Console(stdout, stderr);
@@ -520,6 +520,5 @@ class LazyRemote implements PluginRemote {
async getServicePort(name: string): Promise<number> {
return (await this.init).getServicePort(name);
}
}
}