diff --git a/plugins/bticino/package-lock.json b/plugins/bticino/package-lock.json index 00467863d0..537758683e 100644 --- a/plugins/bticino/package-lock.json +++ b/plugins/bticino/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/bticino", - "version": "0.0.13", + "version": "0.0.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/bticino", - "version": "0.0.13", + "version": "0.0.15", "dependencies": { "@slyoldfox/sip": "^0.0.6-1", "sdp": "^3.0.3", @@ -30,23 +30,23 @@ "dependencies": { "@scrypted/sdk": "file:../sdk", "@scrypted/server": "file:../server", - "http-auth-utils": "^3.0.2", - "node-fetch-commonjs": "^3.1.1", - "typescript": "^4.4.3" + "http-auth-utils": "^5.0.1", + "typescript": "^5.3.3" }, "devDependencies": { - "@types/node": "^16.9.0" + "@types/node": "^20.11.0", + "ts-node": "^10.9.2" } }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.3.2", + "version": "0.3.14", "dev": true, "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.18.6", "adm-zip": "^0.4.13", - "axios": "^0.21.4", + "axios": "^1.6.5", "babel-loader": "^9.1.0", "babel-plugin-const-enum": "^1.1.0", "esbuild": "^0.15.9", @@ -1219,10 +1219,10 @@ "requires": { "@scrypted/sdk": "file:../sdk", "@scrypted/server": "file:../server", - "@types/node": "^16.9.0", - "http-auth-utils": "^3.0.2", - "node-fetch-commonjs": "^3.1.1", - "typescript": "^4.4.3" + "@types/node": "^20.11.0", + "http-auth-utils": "^5.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" } }, "@scrypted/sdk": { @@ -1232,7 +1232,7 @@ "@types/node": "^18.11.18", "@types/stringify-object": "^4.0.0", "adm-zip": "^0.4.13", - "axios": "^0.21.4", + "axios": "^1.6.5", "babel-loader": "^9.1.0", "babel-plugin-const-enum": "^1.1.0", "esbuild": "^0.15.9", diff --git a/plugins/bticino/package.json b/plugins/bticino/package.json index 846dde8ae5..013a6080cf 100644 --- a/plugins/bticino/package.json +++ b/plugins/bticino/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/bticino", - "version": "0.0.13", + "version": "0.0.15", "scripts": { "scrypted-setup-project": "scrypted-setup-project", "prescrypted-setup-project": "scrypted-package-json", diff --git a/plugins/bticino/src/bticino-camera.ts b/plugins/bticino/src/bticino-camera.ts index 716a0a0fe0..0e792debde 100644 --- a/plugins/bticino/src/bticino-camera.ts +++ b/plugins/bticino/src/bticino-camera.ts @@ -17,6 +17,12 @@ import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from import { PersistentSipManager } from './persistent-sip-manager'; import { InviteHandler } from './bticino-inviteHandler'; import { SipOptions, SipRequest } from '../../sip/src/sip-manager'; +import fs from "fs" +import url from "url" +import path from 'path'; +import { default as stream } from 'node:stream' +import type { ReadableStream } from 'node:stream/web' +import { finished } from "stream/promises"; import { get } from 'http' import { ControllerApi } from './c300x-controller-api'; @@ -25,6 +31,7 @@ import { BticinoMuteSwitch } from './bticino-mute-switch'; const STREAM_TIMEOUT = 65000; const { mediaManager } = sdk; +const BTICINO_CLIPS = path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'bticino-clips'); export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor, DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot { @@ -147,11 +154,87 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor }); } - getVideoClip(videoId: string): Promise { + async getVideoClip(videoId: string): Promise { + const outputfile = await this.fetchAndConvertVoicemailMessage(videoId); + + const fileURLToPath: string = url.pathToFileURL(outputfile).toString() + this.console.log(`Creating mediaObject for url: ${fileURLToPath}`) + return await mediaManager.createMediaObjectFromUrl(fileURLToPath); + } + + private async fetchAndConvertVoicemailMessage(videoId: string) { let c300x = SipHelper.getIntercomIp(this) - const url = `http://${c300x}:8080/voicemail?msg=${videoId}/aswm.avi&raw=true`; - return mediaManager.createMediaObjectFromUrl(url); + + const response = await fetch(`http://${c300x}:8080/voicemail?msg=${videoId}/aswm.avi&raw=true`); + + const contentLength: number = Number(response.headers.get("Content-Length")); + const lastModified: Date = new Date(response.headers.get("Last-Modified-Time")); + + const avifile = `${BTICINO_CLIPS}/${videoId}.avi`; + const outputfile = `${BTICINO_CLIPS}/${videoId}.mp4`; + + if (!fs.existsSync(BTICINO_CLIPS)) { + this.console.log(`Creating clips dir at: ${BTICINO_CLIPS}`) + fs.mkdirSync(BTICINO_CLIPS); + } + + if (fs.existsSync(avifile)) { + const stat = fs.statSync(avifile); + if (stat.size != contentLength || stat.mtime.getTime() != lastModified.getTime()) { + this.console.log(`Size ${stat.size} != ${contentLength} or time ${stat.mtime.getTime} != ${lastModified.getTime}`) + try { + fs.rmSync(avifile); + } catch (e) { } + try { + fs.rmSync(outputfile); + } catch (e) { } + } else { + this.console.log(`Keeping the cached video at ${avifile}`) + } + } + + if (!fs.existsSync(avifile)) { + this.console.log("Starting download.") + await finished(stream.Readable.from(response.body as ReadableStream).pipe(fs.createWriteStream(avifile))); + this.console.log("Download finished.") + try { + this.console.log(`Setting mtime to ${lastModified}`) + fs.utimesSync(avifile, lastModified, lastModified); + } catch (e) { } + } + + const ffmpegPath = await mediaManager.getFFmpegPath(); + const ffmpegArgs = [ + '-hide_banner', + '-nostats', + '-y', + '-i', avifile, + outputfile + ]; + + safePrintFFmpegArguments(console, ffmpegArgs); + const cp = child_process.spawn(ffmpegPath, ffmpegArgs, { + stdio: ['pipe', 'pipe', 'pipe', 'pipe'], + }); + + const p = new Promise((resolveFunc) => { + cp.stdout.on("data", (x) => { + this.console.log(x.toString()); + }); + cp.stderr.on("data", (x) => { + this.console.error(x.toString()); + }); + cp.on("exit", (code) => { + resolveFunc(code); + }); + }); + + let returnCode = await p; + + this.console.log(`Converted file returned code: ${returnCode}`); + return outputfile; } + getVideoClipThumbnail(thumbnailId: string): Promise { let c300x = SipHelper.getIntercomIp(this) const url = `http://${c300x}:8080/voicemail?msg=${thumbnailId}/aswm.jpg&raw=true`;