Files
scrypted/plugins/tapo/src/main.ts
2023-03-08 07:36:57 -08:00

141 lines
5.1 KiB
TypeScript

import { SettingsMixinDeviceBase } from '@scrypted/common/src/settings-mixin';
import sdk, { DeviceProvider, DeviceState, FFmpegInput, Intercom, MediaObject, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { MpegTSWriter, StreamTypePCMATapo } from './mpegts-writer';
import { TapoAPI } from './tapo-api';
class TapoIntercomMixin extends SettingsMixinDeviceBase<VideoCamera & Settings> implements Intercom {
storageSettings = new StorageSettings(this, {
cloudPassword: {
title: 'Cloud Password',
description: 'The Tapo Cloud account password. This is not the same as the ONVIF/RTSP local password.',
type: 'password',
}
});
client: Promise<TapoAPI>;
async startIntercom(media: MediaObject): Promise<void> {
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(media, ScryptedMimeTypes.FFmpegInput);
const settings = await this.mixinDevice.getSettings();
const ip = settings.find(s => s.key === 'ip')?.value?.toString();
await this.stopIntercom();
if (!this.storageSettings.values.cloudPassword) {
const error = 'Two Way Audio failed. Tapo Cloud password is unconfigured on ' + this.name;
sdk.log.a(error);
throw error;
}
this.client = TapoAPI.connect({
address: `${ip}:8800`,
cloudPassword: this.storageSettings.values.cloudPassword,
});
const client = await this.client;
client.processMessages();
const mpegts = await client.startMpegTsBackchannel();
const w = new MpegTSWriter();
w.AddPES(68, StreamTypePCMATapo)
w.WritePAT()
w.WritePMT()
const forwarder = await startRtpForwarderProcess(this.console, ffmpegInput, {
audio: {
codecCopy: 'pcm_alaw',
encoderArguments: [
'-vn', '-sn', '-dn',
'-acodec', 'pcm_alaw',
'-ar', '8000',
'-ac', '1',
],
onRtp: rtp => {
const payload = rtp.subarray(12);
w.WritePES(68, 192, payload)
const bytes = w.ResetBytes();
mpegts.write(bytes);
}
}
});
// cp.stdio[3].pipe(mpegts);
forwarder.killPromise.finally(() => client.stream.destroy());
client.stream.on('close', () => forwarder.kill());
}
async stopIntercom(): Promise<void> {
const c = this.client;
this.client = undefined;
const client = await c;
client?.stream.destroy();
}
getMixinSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putMixinSetting(key: string, value: SettingValue): Promise<boolean | void> {
return this.storageSettings.putSetting(key, value);
}
}
class TapoIntercom extends ScryptedDeviceBase implements MixinProvider {
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (type !== ScryptedDeviceType.Doorbell && type !== ScryptedDeviceType.Camera)
return;
if (!interfaces.includes(ScryptedInterface.VideoCamera) || !interfaces.includes(ScryptedInterface.Settings)
// this is currently a necessary hack to make sure the intercom gets advertised
// before the homekit and webrtc plugins mixin it.
|| !interfaces.includes(ScryptedInterface.Intercom)) {
return;
}
return [
ScryptedInterface.Intercom,
ScryptedInterface.Settings,
]
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState): Promise<any> {
return new TapoIntercomMixin({
mixinProviderNativeId: this.nativeId,
group: 'Tapo Two Way Audio',
groupKey: 'tapo',
mixinDevice,
mixinDeviceInterfaces,
mixinDeviceState,
});
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
}
}
class TapoPlugin extends ScryptedDeviceBase implements DeviceProvider {
constructor(nativeId?: string) {
super(nativeId);
process.nextTick(() => {
sdk.deviceManager.onDeviceDiscovered({
nativeId: 'intercom',
type: ScryptedDeviceType.Builtin,
interfaces: [
ScryptedInterface.MixinProvider,
],
name: 'Tapo Two Way Audio',
});
})
}
async getDevice(nativeId: string): Promise<any> {
if (nativeId === 'intercom')
return new TapoIntercom('intercom');
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
}
export default TapoPlugin;