Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HomeKit Secure Video #920

Merged
merged 33 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
33d9388
HomeKit Secure Video
koush Aug 19, 2021
bccbfb8
Merge branch 'beta-0.10.0' into master
koush Oct 6, 2021
7eda2d5
Merge branch 'beta-0.10.0' into master
Supereg Oct 10, 2021
8c0ffff
HKSV: Send event trigger options.
koush Oct 11, 2021
d4daae5
Update src/lib/controller/CameraController.ts
koush Oct 11, 2021
371226a
Update src/lib/camera/RecordingManagement.ts
koush Oct 11, 2021
e68f042
WIP: characteristics should be persisted between reboots
koush Nov 8, 2021
799615d
Merge branch 'master' of github.com:koush/HAP-NodeJS
koush Nov 8, 2021
4b9f639
h265 stubs, albeit it does not seem to actually be supported.
koush Dec 6, 2021
a400094
Remove setting of characteristics, as that should be handled by imple…
koush Dec 12, 2021
9897d0b
Provide connection information to the stream request.
koush Dec 19, 2021
39f7590
Merge branch 'beta-0.10.0' into secure-video-pr
Supereg Dec 30, 2021
0ef294d
Implement Snapshot reason handling with basic test setup
Supereg Dec 30, 2021
0ba31fe
Automatically derive EventTriggerOptions. Added some docs.
Supereg Dec 30, 2021
f76518a
Implement HDS logic. Stream Handling. Updated Delegate API. Proper ch…
Supereg Dec 31, 2021
2b1a4a0
Implement persitent state handling
Supereg Jan 1, 2022
ea7bf49
Handling closing of a recording stream
Supereg Jan 2, 2022
d0e8b53
Define the Delegate interface
Supereg Jan 2, 2022
3304394
Some fixes to the HDS session handling.
Supereg Jan 3, 2022
6146c62
Implement example HSV camera. Remove generator.throw
Supereg Jan 3, 2022
8009168
Moving to a stable delegate API
Supereg Jan 3, 2022
c2362df
Handle Motion and occupancy sensors, some additional test vectors and…
Supereg Jan 3, 2022
a38f24a
Resolving some last issues
Supereg Jan 3, 2022
ad16223
Allow for external doorbell service, support doorbell service naming,…
Supereg Jan 3, 2022
95773f4
Update documentation
Supereg Jan 3, 2022
0aa0e11
New docs files
Supereg Jan 3, 2022
9f8af3a
Fix tests
Supereg Jan 3, 2022
fad303c
Merge branch 'beta-0.10.0' into secure-video-pr
Supereg Jan 3, 2022
288984f
Fix incorrect interpretation in log message of the DATA_SEND OPEN rea…
Supereg Jan 4, 2022
77fb030
Fixed naming confusion
Supereg Jan 4, 2022
17fc4dd
Purge controller data when removing HSV from camera.
Supereg Jan 4, 2022
33ea664
Merge branch 'beta-0.10.0' into secure-video-pr
Supereg Jan 5, 2022
24cc9fb
Make HDSProtocolSpecificErrorReason a const enum such that it can be …
Supereg Jan 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Handle Motion and occupancy sensors, some additional test vectors and…
… cleaned up the example camera.
  • Loading branch information
Supereg committed Jan 3, 2022
commit c2362dfd65a42d83028192e3fc74d061b2b06c0e
33 changes: 15 additions & 18 deletions 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"semver": "^7.3.5",
"simple-plist": "^1.1.1",
"ts-jest": "^27.0.5",
"ts-node": "^10.2.1",
"ts-node": "^10.4.0",
"typedoc": "0.22.8",
"typescript": "~4.4.3"
}
Expand Down
100 changes: 82 additions & 18 deletions src/accessories/Camera_accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
MediaContainerType,
PrepareStreamCallback,
PrepareStreamRequest,
PrepareStreamResponse, RecordingPacket,
PrepareStreamResponse,
RecordingPacket,
Service,
SnapshotRequest,
SnapshotRequestCallback,
Expand Down Expand Up @@ -58,7 +59,7 @@ type SessionInfo = {
videoSRTP: Buffer, // key and salt concatenated
videoSSRC: number, // rtp synchronisation source

/* Won't be save as audio is not supported by this example
/* Won't be saved as audio is not supported by this example
audioPort: number,
audioCryptoSuite: SRTPCryptoSuites,
audioSRTP: Buffer,
Expand Down Expand Up @@ -102,6 +103,11 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate
pendingSessions: Record<string, SessionInfo> = {};
ongoingSessions: Record<string, OngoingSession> = {};

// minimal secure video properties.
configuration?: CameraRecordingConfiguration;
handlingStreamingRequest: boolean = false;
server?: MP4StreamingServer;

handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void {
const ffmpegCommand = `-f lavfi -i testsrc=s=${request.width}x${request.height} -vframes 1 -f mjpeg -`;
const ffmpeg = spawn("ffmpeg", ffmpegCommand.split(" "), {env: process.env});
Expand Down Expand Up @@ -293,11 +299,6 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate
}
}

// TODO move
configuration?: CameraRecordingConfiguration;
handlingStreamingRequest: boolean = false;
server?: MP4StreamingServer;

updateRecordingActive(active: boolean): void {
// we haven't implemented a prebuffer
console.log("Recording active set to " + active);
Expand All @@ -308,11 +309,31 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate
console.log(configuration);
}

/**
* This is a very minimal, very experimental example on how to implement fmp4 streaming with a
* CameraController supporting HomeKit Secure Video.
*
* An ideal implementation would diverge from this in the following ways:
* * It would implement a prebuffer and respect the recording `active` characteristic for that.
* * It would start to immediately record after a trigger event occurred and not just
* when the HomeKit Controller requests it (see the documentation of `CameraRecordingDelegate`).
*/
async *handleRecordingStreamRequest(streamId: number): AsyncGenerator<RecordingPacket> {
assert(!!this.configuration);

/**
* With this flag you can control how the generator reacts to a reset to the motion trigger.
* If set to true, the generator will send a proper endOfStream if the motion stops.
* If set to false, the generator will run till the HomeKit Controller closes the stream.
*
* Note: In a real implementation you would most likely introduce a bit of a delay.
*/
const STOP_AFTER_MOTION_STOP = false;

this.handlingStreamingRequest = true;

assert(this.configuration.videoCodec.type === VideoCodecType.H264);

const profile = this.configuration.videoCodec.parameters.profile === H264Profile.HIGH ? 'high'
: this.configuration.videoCodec.parameters.profile === H264Profile.MAIN ? 'main' : 'baseline';

Expand All @@ -333,13 +354,49 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate
'-b:v', `${this.configuration.videoCodec.parameters.bitRate}k`,
'-force_key_frames', `expr:eq(t,n_forced*${this.configuration.videoCodec.parameters.iFrameInterval / 1000})`,
'-r', this.configuration.videoCodec.resolution[2].toString()
]
];

let samplerate: string;
switch (this.configuration.audioCodec.samplerate) {
case AudioRecordingSamplerate.KHZ_8:
samplerate = "8";
break;
case AudioRecordingSamplerate.KHZ_16:
samplerate = "16";
break;
case AudioRecordingSamplerate.KHZ_24:
samplerate = "24";
break;
case AudioRecordingSamplerate.KHZ_32:
samplerate = "32";
break;
case AudioRecordingSamplerate.KHZ_44_1:
samplerate = "44.1";
break;
case AudioRecordingSamplerate.KHZ_48:
samplerate = "48";
break;
default:
throw new Error("Unsupported audio samplerate: " + this.configuration.audioCodec.samplerate);
}

const audioArgs: Array<string> = this.controller?.recordingManagement?.recordingManagementService.getCharacteristic(Characteristic.RecordingAudioActive)
? [
'-acodec', 'libfdk_aac',
...(this.configuration.audioCodec.type === AudioRecordingCodecType.AAC_LC ?
['-profile:a', 'aac_low'] :
['-profile:a', 'aac_eld']),
'-ar', `${samplerate}k`,
'-b:a', `${this.configuration.audioCodec.bitrate}k`,
'-ac', `${this.configuration.audioCodec.audioChannels}`
]
: [];

this.server = new MP4StreamingServer(
"ffmpeg",
`-f lavfi -i testsrc=s=${this.configuration.videoCodec.resolution[0]}x${this.configuration.videoCodec.resolution[1]}:r=${this.configuration.videoCodec.resolution[2]}`
.split(/ /g),
[], // TODO audio parameters!
audioArgs,
videoArgs,
);

Expand All @@ -354,22 +411,21 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate
for await (const box of this.server.generator()) {
pending.push(box.header, box.data);

const stop = camera.getService(Service.MotionSensor)?.getCharacteristic(Characteristic.MotionDetected).value === false;
const motionDetected = camera.getService(Service.MotionSensor)?.getCharacteristic(Characteristic.MotionDetected).value === false;

console.log("mp4 box type " + box.type + " and length " + box.length);
if (box.type === "moov" || box.type == "mdat") {
const fragment = Buffer.concat(pending);
pending.splice(0, pending.length);
if (stop) {
console.log("Yielding with stop="+ stop);
}

const isLast = STOP_AFTER_MOTION_STOP && !motionDetected;

yield {
data: fragment,
isLast: false,
isLast: isLast,
};

if (false) {
if (isLast) {
console.log("Ending session due to motion stopped!");
break;
}
Expand Down Expand Up @@ -398,7 +454,10 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate
class MP4StreamingServer {
readonly server: Server;

readonly debugMode: boolean = false; // TODO configurable
/**
* This can be configured to output ffmpeg debug output!
*/
readonly debugMode: boolean = false;

readonly ffmpegPath: string;
readonly args: string[];
Expand Down Expand Up @@ -589,7 +648,6 @@ const cameraController = new CameraController({
type: MediaContainerType.FRAGMENTED_MP4,
fragmentLength: 4000,
},
motionService: true,
video: {
type: VideoCodecType.H264,
parameters: {
Expand Down Expand Up @@ -621,13 +679,19 @@ const cameraController = new CameraController({
},

delegate: streamDelegate,
},

sensors: {
motion: true,
occupancy: true,
}
});
streamDelegate.controller = cameraController;

camera.configureController(cameraController);

camera.addService(Service.Switch)
// a service to trigger the motion sensor!
camera.addService(Service.Switch, "MOTION TRIGGER")
.getCharacteristic(Characteristic.On)
.onSet(value => {
camera.getService(Service.MotionSensor)
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ export class Accessory extends EventEmitter {
}

// all other services get added. We can't really control possibly linking to any of those ignored services
// so this is really only half baked stuff.
// so this is really only half-baked stuff.
this.addService(service);
});

Expand Down
15 changes: 13 additions & 2 deletions src/lib/camera/RTPStreamManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,14 @@ export class RTPStreamManagement {
audioProxy?: RTPProxy;
videoProxy?: RTPProxy;

constructor(id: number, options: CameraStreamingOptions, delegate: CameraStreamingDelegate, service?: CameraRTPStreamManagement) {
/**
* A RTPStreamManagement is considered disabled if `HomeKitCameraActive` is set to false.
* We use a closure based approach to retrieve the value of this characteristic.
* The characteristic is managed by the RecordingManagement.
*/
private readonly disabledThroughOperatingMode?: () => boolean;

constructor(id: number, options: CameraStreamingOptions, delegate: CameraStreamingDelegate, service?: CameraRTPStreamManagement, disabledThroughOperatingMode?: () => boolean) {
this.id = id;
this.delegate = delegate;

Expand Down Expand Up @@ -550,6 +557,8 @@ export class RTPStreamManagement {

this.resetSetupEndpointsResponse();
this.resetSelectedStreamConfiguration();

this.disabledThroughOperatingMode = disabledThroughOperatingMode;
}

public forceStop() {
Expand Down Expand Up @@ -664,7 +673,9 @@ export class RTPStreamManagement {
throw new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE);
}

// TODO check for HomeKitCameraActive as well!
if (this.disabledThroughOperatingMode?.()) {
throw new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE);
}
}

private _handleSelectedStreamConfigurationWrite(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
Expand Down
Loading