Skip to content

Commit

Permalink
hikvision: wip autoconfigure
Browse files Browse the repository at this point in the history
  • Loading branch information
koush committed Aug 10, 2024
1 parent c2756a3 commit 6379aa8
Show file tree
Hide file tree
Showing 16 changed files with 1,404 additions and 248 deletions.
163 changes: 163 additions & 0 deletions common/src/autoconfigure-codecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, Setting } from "@scrypted/sdk";

export const automaticallyConfigureSettings: Setting = {
key: 'autoconfigure',
title: 'Automatically Configure Settings',
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
type: 'boolean',
value: true,
};

const MEGABIT = 1024 * 1000;

function getBitrateForResolution(resolution: number) {
if (resolution >= 3840 * 2160)
return 8 * MEGABIT;
if (resolution >= 2688 * 1520)
return 3 * MEGABIT;
if (resolution >= 1920 * 1080)
return 2 * MEGABIT;
if (resolution >= 1280 * 720)
return MEGABIT;
if (resolution >= 640 * 480)
return MEGABIT / 2;
return MEGABIT / 4;
}

export async function autoconfigureCodecs(getCodecs: () => Promise<MediaStreamOptions[]>, configureCodecs: (options: MediaStreamOptions) => Promise<MediaStreamConfiguration>) {
const codecs = await getCodecs();
const configurable: MediaStreamConfiguration[] = [];
for (const codec of codecs) {
const config = await configureCodecs( {
id: codec.id,
});
configurable.push(config);
}

const used: MediaStreamConfiguration[] = [];

for (const _ of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
// find stream with the highest configurable resolution.
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
for (const codec of configurable) {
if (used.includes(codec))
continue;
for (const resolution of codec.video.resolutions) {
if (resolution[0] * resolution[1] > highest[1]) {
highest = [codec, resolution[0] * resolution[1]];
}
}
}

const config = highest[0];
if (!config)
break;

used.push(config);
}

const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
let diff = 999999999;
let ret: [number, number];

for (const res of config.video.resolutions) {
const d = Math.abs(res[0] - width) + Math.abs(res[1] - height);
if (d < diff) {
diff = d;
ret = res;
}
}

return ret;
}

// find the highest resolution
const l = used[0];
const resolution = findResolutionTarget(l, 8192, 8192);

// get the fps of 20 or highest available
let fps = Math.min(20, Math.max(...l.video.fpsRange));

await configureCodecs({
id: l.id,
video: {
width: resolution[0],
height: resolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
});

if (used.length === 3) {
// find remote and low
const r = used[1];
const l = used[2];

const rResolution = findResolutionTarget(r, 1280, 720);
const lResolution = findResolutionTarget(l, 640, 360);

fps = Math.min(20, Math.max(...r.video.fpsRange));
await configureCodecs({
id: r.id,
video: {
width: rResolution[0],
height: rResolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: 1 * MEGABIT,
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
});

fps = Math.min(20, Math.max(...l.video.fpsRange));
await configureCodecs( {
id: l.id,
video: {
width: lResolution[0],
height: lResolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: MEGABIT / 2,
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
});
}
else if (used.length == 2) {
let target: [number, number];
if (resolution[0] * resolution[1] > 1920 * 1080)
target = [1280, 720];
else
target = [640, 360];

const rResolution = findResolutionTarget(used[1], target[0], target[1]);
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
await configureCodecs({
id: used[1].id,
video: {
width: rResolution[0],
height: rResolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
});
}
else if (used.length === 1) {
// no nop
}
}
24 changes: 18 additions & 6 deletions plugins/amcrest/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
this.info = deviceInfo;
}

async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
async setVideoStreamOptions(options: MediaStreamOptions) {
if (!options.id?.startsWith('channel'))
throw new Error('invalid id');
const channel = parseInt(this.getRtspChannel()) || 1;
const formatNumber = parseInt(options.id?.substring('channel'.length)) - 1;
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
const encode = `Encode[${channel - 1}].${format}[${formatNumber}]`;
const params = new URLSearchParams();
Expand All @@ -128,6 +128,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (options.video?.codec === 'h264') {
params.set(`${encode}.Video.Compression`, 'H.264');
}
if (options.video?.profile) {
let profile = 'Main';
if (options.video.profile === 'high')
profile = 'High';
else if (options.video.profile === 'baseline')
profile = 'Baseline';
params.set(`${encode}.Video.Profile`, profile);

}
if (options.video?.codec === 'h265') {
params.set(`${encode}.Video.Compression`, 'H.265');
}
Expand All @@ -136,12 +145,12 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
if (options.video?.fps) {
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
if (options.video?.idrIntervalMillis) {
params.set(`${encode}.Video.GOP`, (options.video.fps * options.video?.idrIntervalMillis / 1000).toString());
}
}
if (options.video?.keyframeInterval) {
params.set(`${encode}.Video.GOP`, options.video?.keyframeInterval.toString());
}
if (options.video?.bitrateControl) {
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'variable' ? 'VBR' : 'CBR');
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'constant' ? 'CBR' : 'VBR');
}

if (![...params.keys()].length)
Expand All @@ -152,6 +161,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
responseType: 'text',
});
this.console.log('reconfigure result', response.body);
return undefined;
}

getClient() {
Expand Down Expand Up @@ -284,6 +294,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const ret = await super.getOtherSettings();
ret.push(
{
subgroup: 'Advanced',
title: 'Doorbell Type',
choices: [
'Not a Doorbell',
Expand Down Expand Up @@ -354,6 +365,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,

ret.push(
{
subgroup: 'Advanced',
title: 'Two Way Audio',
value: twoWayAudio,
key: 'twoWayAudio',
Expand Down
2 changes: 1 addition & 1 deletion plugins/amcrest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",
Expand Down
1 change: 1 addition & 0 deletions plugins/ffmpeg-camera/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e

getInterfaces() {
return [
ScryptedInterface.VideoCameraConfiguration,
ScryptedInterface.VideoCamera,
ScryptedInterface.Settings,
...this.getAdditionalInterfaces()
Expand Down
15 changes: 15 additions & 0 deletions plugins/ffmpeg-camera/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ class FFmpegCamera extends CameraBase<UrlMediaStreamOptions> {
return mediaManager.createFFmpegMediaObject(ret);
}

isAudioDisabled() {
return this.storage.getItem('noAudio') === 'true';
}

async getOtherSettings(): Promise<Setting[]> {
return [
{
key: 'noAudio',
title: 'No Audio',
description: 'Enable this setting if the camera does not have audio or to mute audio.',
type: 'boolean',
value: (this.isAudioDisabled()).toString(),
},
]
}
}

class FFmpegProvider extends CameraProviderBase<UrlMediaStreamOptions> {
Expand Down
2 changes: 1 addition & 1 deletion plugins/ffmpeg-camera/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",
Expand Down
Loading

0 comments on commit 6379aa8

Please sign in to comment.