Files
scrypted/plugins/ffmpeg-camera/src/main.ts
2022-01-26 11:20:35 -08:00

130 lines
4.7 KiB
TypeScript

import sdk, { FFMpegInput, Intercom, MediaObject, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue } from "@scrypted/sdk";
import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "./common";
import { StorageSettings } from "../../../common/src/settings";
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "../../../common/src/media-helpers";
import child_process, { ChildProcess } from "child_process";
const { log, deviceManager, mediaManager } = sdk;
class FFmpegCamera extends CameraBase<UrlMediaStreamOptions> implements Intercom {
storageSettings = new StorageSettings(this, {
ffmpegInputs: {
title: 'FFmpeg Input Stream Arguments',
description: 'FFmpeg input arguments passed to the command line ffmpeg tool. A camera may have multiple streams with different bitrates.',
placeholder: '-i rtmp://[user:password@]192.168.1.100[:1935]/channel/101',
multiple: true,
},
ffmpegOutput: {
title: 'FFmpeg Output Stream Arguments',
description: 'Optional (two way audio): FFmpeg output arguments passed to the command line ffmpeg tool to play back an audio stream.',
placeholder: '-vn -acodec copy -f adts udp://192.168.1.101:1234',
onPut: (_, newValue) => {
let interfaces = this.providedInterfaces;
if (!newValue)
interfaces = interfaces.filter(iface => iface !== ScryptedInterface.Intercom);
else
interfaces.push(ScryptedInterface.Intercom);
this.provider.updateDevice(this.nativeId, this.providedName, interfaces);
},
},
})
twoway: ChildProcess;
async startIntercom(media: MediaObject): Promise<void> {
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
const ffmpegInput: FFMpegInput = JSON.parse(buffer.toString());
const args = ffmpegInput.inputArguments.slice();
args.push(...this.storageSettings.values.ffmpegOutput.split(' '));
this.console.log('starting intercom', safePrintFFmpegArguments(this.console, args));
this.stopIntercom();
this.twoway = child_process.spawn(await mediaManager.getFFmpegPath(), args);
ffmpegLogInitialOutput(this.console, this.twoway);
}
async stopIntercom(): Promise<void> {
this.twoway?.kill('SIGKILL');
this.twoway = undefined;
}
createFFmpegMediaStreamOptions(ffmpegInput: string, index: number) {
// this might be usable as a url so check that.
let url: string;
try {
const parsedUrl = new URL(ffmpegInput);
}
catch (e) {
}
return {
id: `channel${index}`,
name: `Stream ${index + 1}`,
url,
video: {
},
audio: this.isAudioDisabled() ? null : {},
};
}
getRawVideoStreamOptions(): UrlMediaStreamOptions[] {
const ffmpegInputs = this.storageSettings.values.ffmpegInputs;
// filter out empty strings.
const ret = ffmpegInputs
.filter(ffmpegInput => !!ffmpegInput)
.map((ffmpegInput, index) => this.createFFmpegMediaStreamOptions(ffmpegInput, index));
if (!ret.length)
return;
return ret;
}
async getFFmpegInputSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
async putSettingBase(key: string, value: SettingValue) {
if (this.storageSettings.settings[key]) {
this.storageSettings.putSetting(key, value);
}
else {
super.putSettingBase(key, value);
}
}
async getUrlSettings(): Promise<Setting[]> {
return [
...await this.getSnapshotUrlSettings(),
...await this.getFFmpegInputSettings(),
];
}
async createVideoStream(options?: UrlMediaStreamOptions): Promise<MediaObject> {
const index = this.getRawVideoStreamOptions()?.findIndex(vso => vso.id === options.id);
const ffmpegInputs = this.storageSettings.values.ffmpegInputs;
const ffmpegInput = ffmpegInputs[index];
if (!ffmpegInput)
throw new Error('video streams not set up or no longer exists.');
const ret: FFMpegInput = {
url: options.url,
inputArguments: ffmpegInput.split(' '),
mediaStreamOptions: options,
};
return mediaManager.createFFmpegMediaObject(ret);
}
}
class FFmpegProvider extends CameraProviderBase<UrlMediaStreamOptions> {
createCamera(nativeId: string): FFmpegCamera {
return new FFmpegCamera(nativeId, this);
}
}
export default new FFmpegProvider();