Skip to content

Commit 9a7a40a

Browse files
committed
New feature: ability to force bitrates when transcoding, ignoring HomeKit's requested bitrates.
1 parent 70a9e72 commit 9a7a40a

File tree

6 files changed

+51
-8
lines changed

6 files changed

+51
-8
lines changed

docs/FeatureOptions.md

+2
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ These option(s) apply to: Protect cameras.
138138
|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
139139
| `Video.Transcode.Hardware` | Use hardware-accelerated transcoding when available (Apple Macs, Intel Quick Sync Video-enabled CPUs, Raspberry Pi 4). **(default: disabled)**.
140140
| `Video.Transcode` | When streaming to local clients (e.g. at home), always transcode livestreams, instead of transmuxing them. **(default: disabled)**.
141+
| `Video.Transcode.Bitrate<I>.Value</I>` | Bitrate, in kilobits per second, to use when transcoding to local clients, ignoring the bitrate HomeKit requests. HomeKit typically requests lower video quality than you may desire in your environment. **(default: 2000)**.
141142
| `Video.Transcode.HighLatency` | When streaming to high-latency clients (e.g. cellular connections), transcode livestreams instead of transmuxing them. **(default: enabled)**.
143+
| `Video.Transcode.HighLatency.Bitrate<I>.Value</I>` | Bitrate, in kilobits per second, to use when transcoding to high-latency (e.g. cellular) clients, ignoring the bitrate HomeKit requests. HomeKit typically requests lower video quality than you may desire in your environment. **(default: 1000)**.
142144
| `Video.Stream.Only.High` | When viewing livestreams, force the use of the high quality video stream from the Protect controller. **(default: disabled)**.
143145
| `Video.Stream.Only.Medium` | When viewing livestreams, force the use of the medium quality video stream from the Protect controller. **(default: disabled)**.
144146
| `Video.Stream.Only.Low` | When viewing livestreams, force the use of the low quality video stream from the Protect controller. **(default: disabled)**.

src/devices/protect-camera.ts

+13
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,22 @@ export class ProtectCamera extends ProtectDevice {
7373
this.hints.smartDetect = this.ufp.featureFlags.hasSmartDetect && this.hasFeature("Motion.SmartDetect");
7474
this.hints.timeshift = this.hasFeature("Video.HKSV.TimeshiftBuffer");
7575
this.hints.transcode = this.hasFeature("Video.Transcode");
76+
this.hints.transcodeBitrate = this.getFeatureNumber("Video.Transcode.Bitrate") as number;
7677
this.hints.transcodeHighLatency = this.hasFeature("Video.Transcode.HighLatency");
78+
this.hints.transcodeHighLatencyBitrate = this.getFeatureNumber("Video.Transcode.HighLatency.Bitrate") as number;
7779
this.hints.twoWayAudio = this.ufp.featureFlags.hasSpeaker && this.hasFeature("Audio") && this.hasFeature("Audio.TwoWay");
7880

81+
// Sanity check our target transcoding bitrates, if defined.
82+
if((this.hints.transcodeBitrate === undefined) || (this.hints.transcodeBitrate <= 0)) {
83+
84+
this.hints.transcodeBitrate = -1;
85+
}
86+
87+
if((this.hints.transcodeHighLatencyBitrate === undefined) || (this.hints.transcodeHighLatencyBitrate <= 0)) {
88+
89+
this.hints.transcodeHighLatencyBitrate = -1;
90+
}
91+
7992
return true;
8093
}
8194

src/devices/protect-device.ts

+2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export interface ProtectHints {
4747
syncName: boolean,
4848
timeshift: boolean,
4949
transcode: boolean,
50+
transcodeBitrate: number,
5051
transcodeHighLatency: boolean,
52+
transcodeHighLatencyBitrate: number,
5153
twoWayAudio: boolean
5254
}
5355

src/protect-options.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* protect-options.ts: Feature option and type definitions for UniFi Protect.
44
*/
55
import { PROTECT_DEVICE_REMOVAL_DELAY_INTERVAL, PROTECT_DOORBELL_CHIME_DURATION_DIGITAL, PROTECT_FFMPEG_AUDIO_FILTER_FFTNR, PROTECT_FFMPEG_AUDIO_FILTER_HIGHPASS,
6-
PROTECT_FFMPEG_AUDIO_FILTER_LOWPASS, PROTECT_M3U_PLAYLIST_PORT, PROTECT_MOTION_DURATION, PROTECT_OCCUPANCY_DURATION } from "./settings.js";
6+
PROTECT_FFMPEG_AUDIO_FILTER_LOWPASS, PROTECT_M3U_PLAYLIST_PORT, PROTECT_MOTION_DURATION, PROTECT_OCCUPANCY_DURATION, PROTECT_TRANSCODE_BITRATE,
7+
PROTECT_TRANSCODE_HIGH_LATENCY_BITRATE } from "./settings.js";
78
import { ProtectDeviceConfigTypes } from "./protect-types.js";
89
import { ProtectNvrConfig } from "unifi-protect";
910

@@ -133,7 +134,9 @@ export const featureOptions: { [index: string]: FeatureOption[] } = {
133134

134135
{ default: false, description: "Use hardware-accelerated transcoding when available (Apple Macs, Intel Quick Sync Video-enabled CPUs, Raspberry Pi 4).", name: "Transcode.Hardware" },
135136
{ default: false, description: "When streaming to local clients (e.g. at home), always transcode livestreams, instead of transmuxing them.", name: "Transcode" },
137+
{ default: false, defaultValue: PROTECT_TRANSCODE_BITRATE, description: "Bitrate, in kilobits per second, to use when transcoding to local clients, ignoring the bitrate HomeKit requests. HomeKit typically requests lower video quality than you may desire in your environment.", group: "Transcode", name: "Transcode.Bitrate" },
136138
{ default: true, description: "When streaming to high-latency clients (e.g. cellular connections), transcode livestreams instead of transmuxing them.", name: "Transcode.HighLatency" },
139+
{ default: false, defaultValue: PROTECT_TRANSCODE_HIGH_LATENCY_BITRATE, description: "Bitrate, in kilobits per second, to use when transcoding to high-latency (e.g. cellular) clients, ignoring the bitrate HomeKit requests. HomeKit typically requests lower video quality than you may desire in your environment.", group: "Transcode.HighLatency", name: "Transcode.HighLatency.Bitrate" },
137140
{ default: false, description: "When viewing livestreams, force the use of the high quality video stream from the Protect controller.", name: "Stream.Only.High" },
138141
{ default: false, description: "When viewing livestreams, force the use of the medium quality video stream from the Protect controller.", name: "Stream.Only.Medium" },
139142
{ default: false, description: "When viewing livestreams, force the use of the low quality video stream from the Protect controller.", name: "Stream.Only.Low" },

src/protect-stream.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,11 @@ export class ProtectStreamingDelegate implements CameraStreamingDelegate {
394394
// How do we determine if we're a high latency connection? We look at the RTP packet time of the audio packet time for a hint. HomeKit uses values of 20, 30, 40,
395395
// and 60ms. We make an assumption, validated by lots of real-world testing, that when we see 60ms used by HomeKit, it's a high latency connection and act
396396
// accordingly.
397-
const isTranscoding = this.protectCamera.hints.transcode || this.protectCamera.hints.crop ||
398-
((request.audio.packet_time >= 60) && this.protectCamera.hints.transcodeHighLatency);
397+
const isHighLatency = request.audio.packet_time >= 60;
398+
const isTranscoding = this.protectCamera.hints.transcode || this.protectCamera.hints.crop || (isHighLatency && this.protectCamera.hints.transcodeHighLatency);
399+
400+
// Set the initial bitrate we should use for this request based on what HomeKit is requesting.
401+
let targetBitrate = request.video.max_bit_rate;
399402

400403
// Find the best RTSP stream based on what we're looking for.
401404
if(isTranscoding) {
@@ -405,6 +408,21 @@ export class ProtectStreamingDelegate implements CameraStreamingDelegate {
405408
(this.protectCamera.hints.hardwareTranscoding) ? 2160 : request.video.height,
406409
{ biasHigher: true, maxPixels: this.ffmpegOptions.hostSystemMaxPixels }
407410
);
411+
412+
// If we have specified the bitrates we want to use when transcoding, let's honor those here.
413+
if(isHighLatency && (this.protectCamera.hints.transcodeHighLatencyBitrate > 0)) {
414+
415+
targetBitrate = this.protectCamera.hints.transcodeHighLatencyBitrate;
416+
} else if(!isHighLatency && (this.protectCamera.hints.transcodeBitrate > 0)) {
417+
418+
targetBitrate = this.protectCamera.hints.transcodeBitrate;
419+
}
420+
421+
// If we're targeting a bitrate that's beyond the capabilities of our input channel, match the bitrate of the input channel.
422+
if(this.rtspEntry && (targetBitrate > (this.rtspEntry.channel.bitrate / 1000))) {
423+
424+
targetBitrate = this.rtspEntry.channel.bitrate / 1000;
425+
}
408426
} else {
409427

410428
this.rtspEntry = this.protectCamera.findRtsp(request.video.width, request.video.height);
@@ -421,8 +439,7 @@ export class ProtectStreamingDelegate implements CameraStreamingDelegate {
421439
return;
422440
}
423441

424-
// Save our current bitrate before we modify it, but only if we're the first stream - we don't want to do this for
425-
// concurrent streaming clients for this camera.
442+
// Save our current bitrate before we modify it, but only if we're the first stream - we don't want to do this for concurrent streaming clients for this camera.
426443
if(!this.savedBitrate) {
427444

428445
this.savedBitrate = this.protectCamera.getBitrate(this.rtspEntry.channel.id);
@@ -435,7 +452,7 @@ export class ProtectStreamingDelegate implements CameraStreamingDelegate {
435452

436453
// Set the desired bitrate in Protect. We don't need to for this to return, because Protect
437454
// will adapt the stream once it processes the configuration change.
438-
await this.protectCamera.setBitrate(this.rtspEntry.channel.id, request.video.max_bit_rate * 1000);
455+
await this.protectCamera.setBitrate(this.rtspEntry.channel.id, targetBitrate * 1000);
439456

440457
// Set our packet size to be 564. Why? MPEG transport stream (TS) packets are 188 bytes in size each.
441458
// These packets transmit the video data that you ultimately see on your screen and are transmitted using
@@ -494,7 +511,7 @@ export class ProtectStreamingDelegate implements CameraStreamingDelegate {
494511
// Inform the user.
495512
this.log.info("Streaming request from %s%s: %sx%s@%sfps, %s kbps. %s %s, %s kbps.",
496513
sessionInfo.address, (request.audio.packet_time === 60) ? " (high latency connection)" : "",
497-
request.video.width, request.video.height, request.video.fps, request.video.max_bit_rate.toLocaleString("en-US"),
514+
request.video.width, request.video.height, request.video.fps, targetBitrate.toLocaleString("en-US"),
498515
isTranscoding ? (this.protectCamera.hints.hardwareTranscoding ? "Hardware accelerated transcoding" : "Transcoding") : "Using",
499516
this.rtspEntry.name, (this.rtspEntry.channel.bitrate / 1000).toLocaleString("en-US"));
500517

@@ -504,7 +521,7 @@ export class ProtectStreamingDelegate implements CameraStreamingDelegate {
504521
// Configure our video parameters for transcoding.
505522
ffmpegArgs.push(...this.ffmpegOptions.streamEncoder({
506523

507-
bitrate: request.video.max_bit_rate,
524+
bitrate: targetBitrate,
508525
fps: this.rtspEntry.channel.fps,
509526
height: request.video.height,
510527
idrInterval: PROTECT_HOMEKIT_IDR_INTERVAL,

src/settings.ts

+6
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,11 @@ export const PROTECT_RPI_GPU_MINIMUM = 128;
7777
// Maximum age of a snapshot in seconds.
7878
export const PROTECT_SNAPSHOT_CACHE_MAXAGE = 90;
7979

80+
// Bitrate, in kilobits per second, to use when transcoding to local clients.
81+
export const PROTECT_TRANSCODE_BITRATE = 2000;
82+
83+
// Bitrate, in kilobits per second, to use when transcoding to high-latency clients.
84+
export const PROTECT_TRANSCODE_HIGH_LATENCY_BITRATE = 1000;
85+
8086
// How often, in seconds, should we heartbeat FFmpeg in two-way audio sessions. This should be less than 5 seconds, which is FFmpeg's input timeout interval.
8187
export const PROTECT_TWOWAY_HEARTBEAT_INTERVAL = 3;

0 commit comments

Comments
 (0)