Skip to content

Commit

Permalink
pam-diff/videoanalysis: fix performance, remove sharp dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
koush committed Apr 18, 2023
1 parent 87be464 commit a05595e
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 218 deletions.
4 changes: 2 additions & 2 deletions plugins/objectdetector/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion plugins/objectdetector/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.130",
"version": "0.0.132",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",
Expand Down Expand Up @@ -35,6 +35,7 @@
"name": "Video Analysis Plugin",
"type": "API",
"interfaces": [
"DeviceProvider",
"Settings",
"MixinProvider"
],
Expand Down
169 changes: 169 additions & 0 deletions plugins/objectdetector/src/ffmpeg-videoframes-no-sharp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Deferred } from "@scrypted/common/src/deferred";
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
import { readLength, readLine } from "@scrypted/common/src/read-stream";
import sdk, { FFmpegInput, Image, ImageFormat, ImageOptions, MediaObject, ScryptedDeviceBase, ScryptedMimeTypes, VideoFrame, VideoFrameGenerator, VideoFrameGeneratorOptions } from "@scrypted/sdk";
import child_process from 'child_process';
import { Readable } from 'stream';


interface RawFrame {
width: number;
height: number;
data: Buffer;
}

async function createRawImageMediaObject(image: RawImage): Promise<VideoFrame & MediaObject> {
const ret = await sdk.mediaManager.createMediaObject(image, ScryptedMimeTypes.Image, {
format: null,
timestamp: 0,
width: image.width,
height: image.height,
queued: 0,
toBuffer: (options: ImageOptions) => image.toBuffer(options),
toImage: (options: ImageOptions) => image.toImage(options),
flush: async () => { },
});

return ret;
}

class RawImage implements Image, RawFrame {
constructor(public data: Buffer, public width: number, public height: number, public format: ImageFormat) {
}

checkOptions(options: ImageOptions) {
if (options?.resize || options?.crop)
throw new Error('resize and crop are not supported');
if (options?.format && options?.format !== this.format)
throw new Error('format not supported');
}

async toBuffer(options: ImageOptions) {
this.checkOptions(options);
return this.data;
}

async toImage(options: ImageOptions) {
this.checkOptions(options);
return createRawImageMediaObject(this);
}
}

export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements VideoFrameGenerator {
async *generateVideoFramesInternal(mediaObject: MediaObject, options?: VideoFrameGeneratorOptions, filter?: (videoFrame: VideoFrame & MediaObject) => Promise<boolean>): AsyncGenerator<VideoFrame & MediaObject, any, unknown> {
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(mediaObject, ScryptedMimeTypes.FFmpegInput);
const gray = options?.format === 'gray';
const channels = gray ? 1 : 3;
const format: ImageFormat = gray ? 'gray' : 'rgb';
const vf: string[] = [];
if (options?.fps)
vf.push(`fps=${options.fps}`);
if (options.resize)
vf.push(`scale=${options.resize.width}:${options.resize.height}`);
const args = [
'-hide_banner',
//'-hwaccel', 'auto',
...ffmpegInput.inputArguments,
'-vcodec', 'pam',
'-pix_fmt', gray ? 'gray' : 'rgb24',
...vf.length ? [
'-vf',
vf.join(','),
] : [],
'-f', 'image2pipe',
'pipe:3',
];

// this seems to reduce latency.
// addVideoFilterArguments(args, 'fps=10', 'fps');

const cp = child_process.spawn(await sdk.mediaManager.getFFmpegPath(), args, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
const console = mediaObject?.sourceId ? sdk.deviceManager.getMixinConsole(mediaObject.sourceId) : this.console;
safePrintFFmpegArguments(console, args);
ffmpegLogInitialOutput(console, cp);

let finished = false;
let frameDeferred: Deferred<RawFrame>;

const reader = async () => {
try {

const readable = cp.stdio[3] as Readable;
const headers = new Map<string, string>();
while (!finished) {
const line = await readLine(readable);
if (line !== 'ENDHDR') {
const [key, value] = line.split(' ');
headers[key] = value;
continue;
}


if (headers['TUPLTYPE'] !== 'RGB' && headers['TUPLTYPE'] !== 'GRAYSCALE')
throw new Error(`Unexpected TUPLTYPE in PAM stream: ${headers['TUPLTYPE']}`);

const width = parseInt(headers['WIDTH']);
const height = parseInt(headers['HEIGHT']);
if (!width || !height)
throw new Error('Invalid dimensions in PAM stream');

const length = width * height * channels;
headers.clear();
const data = await readLength(readable, length);

if (frameDeferred) {
const f = frameDeferred;
frameDeferred = undefined;
f.resolve({
width,
height,
data,
});
}
else {
// this.console.warn('skipped frame');
}
}
}
catch (e) {
}
finally {
console.log('finished reader');
finished = true;
frameDeferred?.reject(new Error('frame generator finished'));
}
}

try {
reader();
while (!finished) {
frameDeferred = new Deferred();
const raw = await frameDeferred.promise;
const { width, height, data } = raw;

const rawImage = new RawImage(data, width, height, format);
try {
const mo = await createRawImageMediaObject(rawImage);
yield mo;
}
finally {
rawImage.data = undefined;
}
}
}
catch (e) {
}
finally {
console.log('finished generator');
finished = true;
safeKillFFmpeg(cp);
}
}


async generateVideoFrames(mediaObject: MediaObject, options?: VideoFrameGeneratorOptions, filter?: (videoFrame: VideoFrame & MediaObject) => Promise<boolean>): Promise<AsyncGenerator<VideoFrame & MediaObject, any, unknown>> {
return this.generateVideoFramesInternal(mediaObject, options, filter);
}
}
4 changes: 3 additions & 1 deletion plugins/objectdetector/src/ffmpeg-videoframes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ async function createVipsMediaObject(image: VipsImage): Promise<VideoFrame & Med
timestamp: 0,
width: image.width,
height: image.height,
queued: 0,
toBuffer: (options: ImageOptions) => image.toBuffer(options),
toImage: async (options: ImageOptions) => {
const newImage = await image.toVipsImage(options);
return createVipsMediaObject(newImage);
}
},
flush: async () => {},
});

return ret;
Expand Down
35 changes: 18 additions & 17 deletions plugins/objectdetector/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
// import { FFmpegVideoFrameGenerator, sharpLib } from './ffmpeg-videoframes';
import { serverSupportsMixinEventMasking } from './server-version';
import { getAllDevices, safeParseJson } from './util';
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes-no-sharp';

const polygonOverlap = require('polygon-overlap');
const insidePolygon = require('point-inside-polygon');
Expand Down Expand Up @@ -363,6 +363,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
});

frameGenerator = await videoFrameGenerator.generateVideoFrames(stream, {
queue: 0,
resize: this.model?.inputSize ? {
width: this.model.inputSize[0],
height: this.model.inputSize[1],
Expand Down Expand Up @@ -955,25 +956,25 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
constructor(nativeId?: ScryptedNativeId) {
super(nativeId);

// process.nextTick(() => {
// sdk.deviceManager.onDevicesChanged({
// devices: [
// {
// name: 'FFmpeg Frame Generator',
// type: ScryptedDeviceType.Builtin,
// interfaces: sharpLib ? [
// ScryptedInterface.VideoFrameGenerator,
// ] : [],
// nativeId: 'ffmpeg',
// }
// ]
// })
// })
process.nextTick(() => {
sdk.deviceManager.onDevicesChanged({
devices: [
{
name: 'FFmpeg Frame Generator',
type: ScryptedDeviceType.Builtin,
interfaces: [
ScryptedInterface.VideoFrameGenerator,
],
nativeId: 'ffmpeg',
}
]
})
})
}

async getDevice(nativeId: string): Promise<any> {
// if (nativeId === 'ffmpeg')
// return new FFmpegVideoFrameGenerator('ffmpeg');
if (nativeId === 'ffmpeg')
return new FFmpegVideoFrameGenerator('ffmpeg');
}

async releaseDevice(id: string, nativeId: string): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions plugins/pam-diff/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/pam-diff/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.20"
"version": "0.0.21"
}
Loading

0 comments on commit a05595e

Please sign in to comment.