homebridge-ring 10.0.0-beta.5 → 10.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/api/ring-camera.d.ts +5 -4
- package/lib/api/ring-camera.js +12 -5
- package/lib/api/{peer-connection.d.ts → streaming/peer-connection.d.ts} +21 -4
- package/lib/api/{peer-connection.js → streaming/peer-connection.js} +4 -4
- package/lib/api/streaming/ring-edge-connection.d.ts +65 -0
- package/lib/api/streaming/ring-edge-connection.js +155 -0
- package/lib/api/streaming/streaming-connection-base.d.ts +30 -0
- package/lib/api/streaming/streaming-connection-base.js +111 -0
- package/lib/api/{live-call.d.ts → streaming/streaming-session.d.ts} +15 -18
- package/lib/api/{live-call.js → streaming/streaming-session.js} +32 -95
- package/lib/api/streaming/webrtc-connection.d.ts +23 -0
- package/lib/api/streaming/webrtc-connection.js +60 -0
- package/lib/homebridge/camera-source.js +2 -2
- package/package.json +23 -23
- package/lib/api/live-call-ring-edge.d.ts +0 -41
- package/lib/api/live-call-ring-edge.js +0 -294
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [10.0.0-beta.6](https://github.com/dgreif/ring/compare/v10.0.0-beta.5...v10.0.0-beta.6) (2022-05-06)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **api:** allow custom peer connections for streaming ([3e9e784](https://github.com/dgreif/ring/commit/3e9e78458afe2849a8c414a6b50eec8718bf77cc))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* update dependencies ([4b25595](https://github.com/dgreif/ring/commit/4b255950bcfbf94486192d55cfc90d966cd79d24))
|
|
16
|
+
|
|
5
17
|
## [10.0.0-beta.5](https://github.com/dgreif/ring/compare/v10.0.0-beta.4...v10.0.0-beta.5) (2022-04-10)
|
|
6
18
|
|
|
7
19
|
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { RingRestClient } from './rest-client';
|
|
|
4
4
|
import { BehaviorSubject, Subject } from 'rxjs';
|
|
5
5
|
import { DeepPartial } from './util';
|
|
6
6
|
import { Subscribed } from './subscribed';
|
|
7
|
-
import { FfmpegOptions,
|
|
8
|
-
import {
|
|
7
|
+
import { FfmpegOptions, StreamingSession } from './streaming/streaming-session';
|
|
8
|
+
import { StreamingConnectionOptions } from './streaming/streaming-connection-base';
|
|
9
9
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'> & {
|
|
10
10
|
health?: Partial<CameraData['health']>;
|
|
11
11
|
}): number | null;
|
|
@@ -55,7 +55,8 @@ export declare class RingCamera extends Subscribed {
|
|
|
55
55
|
getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
|
|
56
56
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
57
57
|
getHealth(): Promise<CameraHealth>;
|
|
58
|
-
|
|
58
|
+
private createStreamingConnection;
|
|
59
|
+
startLiveCall(options?: StreamingConnectionOptions): Promise<StreamingSession>;
|
|
59
60
|
private removeDingById;
|
|
60
61
|
processPushNotification(notification: PushNotification): void;
|
|
61
62
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
@@ -93,7 +94,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
93
94
|
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
94
95
|
getSnapshotByUuid(uuid: string): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
95
96
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
96
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<
|
|
97
|
+
streamVideo(ffmpegOptions: FfmpegOptions): Promise<StreamingSession>;
|
|
97
98
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
98
99
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
99
100
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -16,8 +16,9 @@ const rxjs_1 = require("rxjs");
|
|
|
16
16
|
const operators_1 = require("rxjs/operators");
|
|
17
17
|
const util_1 = require("./util");
|
|
18
18
|
const subscribed_1 = require("./subscribed");
|
|
19
|
-
const
|
|
20
|
-
const
|
|
19
|
+
const webrtc_connection_1 = require("./streaming/webrtc-connection");
|
|
20
|
+
const ring_edge_connection_1 = require("./streaming/ring-edge-connection");
|
|
21
|
+
const streaming_session_1 = require("./streaming/streaming-session");
|
|
21
22
|
const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
|
|
22
23
|
function parseBatteryLife(batteryLife) {
|
|
23
24
|
if (batteryLife === null || batteryLife === undefined) {
|
|
@@ -229,11 +230,11 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
229
230
|
return response.device_health;
|
|
230
231
|
});
|
|
231
232
|
}
|
|
232
|
-
|
|
233
|
+
createStreamingConnection(options) {
|
|
233
234
|
return __awaiter(this, void 0, void 0, function* () {
|
|
234
235
|
if (this.isRingEdgeEnabled) {
|
|
235
236
|
const auth = yield this.restClient.getCurrentAuth();
|
|
236
|
-
return new
|
|
237
|
+
return new ring_edge_connection_1.RingEdgeConnection(auth.access_token, this, options);
|
|
237
238
|
}
|
|
238
239
|
const liveCall = yield this.restClient
|
|
239
240
|
.request({
|
|
@@ -249,7 +250,13 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
249
250
|
}
|
|
250
251
|
throw e;
|
|
251
252
|
});
|
|
252
|
-
return new
|
|
253
|
+
return new webrtc_connection_1.WebrtcConnection(liveCall.data.session_id, this, options);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
startLiveCall(options = {}) {
|
|
257
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
258
|
+
const connection = yield this.createStreamingConnection(options);
|
|
259
|
+
return new streaming_session_1.StreamingSession(this, connection);
|
|
253
260
|
});
|
|
254
261
|
}
|
|
255
262
|
removeDingById(idToRemove) {
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import { MediaStreamTrack, RTCIceCandidate,
|
|
2
|
-
import { ReplaySubject, Subject } from 'rxjs';
|
|
3
|
-
export
|
|
4
|
-
|
|
1
|
+
import { ConnectionState, MediaStreamTrack, RTCIceCandidate, RtcpPacket, RtpPacket } from '@koush/werift';
|
|
2
|
+
import { Observable, ReplaySubject, Subject } from 'rxjs';
|
|
3
|
+
export interface BasicPeerConnection {
|
|
4
|
+
createOffer(): Promise<{
|
|
5
|
+
sdp: string;
|
|
6
|
+
}>;
|
|
7
|
+
createAnswer(offer: {
|
|
8
|
+
type: 'offer';
|
|
9
|
+
sdp: string;
|
|
10
|
+
}): Promise<RTCSessionDescriptionInit>;
|
|
11
|
+
acceptAnswer(answer: {
|
|
12
|
+
type: 'answer';
|
|
13
|
+
sdp: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
16
|
+
onIceCandidate: Observable<RTCIceCandidate>;
|
|
17
|
+
onConnectionState: Observable<ConnectionState>;
|
|
18
|
+
close(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class WeriftPeerConnection implements BasicPeerConnection {
|
|
21
|
+
private pc;
|
|
5
22
|
onAudioRtp: Subject<RtpPacket>;
|
|
6
23
|
onAudioRtcp: Subject<RtcpPacket>;
|
|
7
24
|
onVideoRtp: Subject<RtpPacket>;
|
|
@@ -9,10 +9,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
12
|
+
exports.WeriftPeerConnection = void 0;
|
|
13
13
|
const werift_1 = require("@koush/werift");
|
|
14
14
|
const rxjs_1 = require("rxjs");
|
|
15
|
-
const util_1 = require("
|
|
15
|
+
const util_1 = require("../util");
|
|
16
16
|
const debug = false, ringIceServers = [
|
|
17
17
|
'stun:stun.kinesisvideo.us-east-1.amazonaws.com:443',
|
|
18
18
|
'stun:stun.kinesisvideo.us-east-2.amazonaws.com:443',
|
|
@@ -23,7 +23,7 @@ const debug = false, ringIceServers = [
|
|
|
23
23
|
'stun:stun3.l.google.com:19302',
|
|
24
24
|
'stun:stun4.l.google.com:19302',
|
|
25
25
|
];
|
|
26
|
-
class
|
|
26
|
+
class WeriftPeerConnection {
|
|
27
27
|
constructor() {
|
|
28
28
|
this.onAudioRtp = new rxjs_1.Subject();
|
|
29
29
|
this.onAudioRtcp = new rxjs_1.Subject();
|
|
@@ -131,4 +131,4 @@ class PeerConnection {
|
|
|
131
131
|
this.pc.close().catch(util_1.logError);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
exports.
|
|
134
|
+
exports.WeriftPeerConnection = WeriftPeerConnection;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RingCamera } from '../ring-camera';
|
|
2
|
+
import { StreamingConnectionBase, StreamingConnectionOptions } from './streaming-connection-base';
|
|
3
|
+
interface SessionBody {
|
|
4
|
+
doorbot_id: number;
|
|
5
|
+
session_id: string;
|
|
6
|
+
}
|
|
7
|
+
interface AnswerMessage {
|
|
8
|
+
method: 'sdp';
|
|
9
|
+
body: {
|
|
10
|
+
sdp: string;
|
|
11
|
+
type: 'answer';
|
|
12
|
+
} & SessionBody;
|
|
13
|
+
}
|
|
14
|
+
interface IceCandidateMessage {
|
|
15
|
+
method: 'ice';
|
|
16
|
+
body: {
|
|
17
|
+
ice: string;
|
|
18
|
+
mlineindex: number;
|
|
19
|
+
} & SessionBody;
|
|
20
|
+
}
|
|
21
|
+
interface SessionCreatedMessage {
|
|
22
|
+
method: 'session_created';
|
|
23
|
+
body: SessionBody;
|
|
24
|
+
}
|
|
25
|
+
interface SessionStartedMessage {
|
|
26
|
+
method: 'session_started';
|
|
27
|
+
body: SessionBody;
|
|
28
|
+
}
|
|
29
|
+
interface PongMessage {
|
|
30
|
+
method: 'pong';
|
|
31
|
+
body: SessionBody;
|
|
32
|
+
}
|
|
33
|
+
interface NotificationMessage {
|
|
34
|
+
method: 'notification';
|
|
35
|
+
body: {
|
|
36
|
+
is_ok: boolean;
|
|
37
|
+
text: string;
|
|
38
|
+
} & SessionBody;
|
|
39
|
+
}
|
|
40
|
+
declare enum CloseReasonCode {
|
|
41
|
+
NormalClose = 0,
|
|
42
|
+
AuthenticationFailed = 5,
|
|
43
|
+
Timeout = 6
|
|
44
|
+
}
|
|
45
|
+
interface CloseMessage {
|
|
46
|
+
method: 'close';
|
|
47
|
+
body: {
|
|
48
|
+
reason: {
|
|
49
|
+
code: CloseReasonCode;
|
|
50
|
+
text: string;
|
|
51
|
+
};
|
|
52
|
+
} & SessionBody;
|
|
53
|
+
}
|
|
54
|
+
declare type IncomingMessage = AnswerMessage | IceCandidateMessage | SessionCreatedMessage | SessionStartedMessage | PongMessage | CloseMessage | NotificationMessage;
|
|
55
|
+
export declare class RingEdgeConnection extends StreamingConnectionBase {
|
|
56
|
+
private camera;
|
|
57
|
+
private readonly onSessionId;
|
|
58
|
+
private readonly onOfferSent;
|
|
59
|
+
constructor(authToken: string, camera: RingCamera, options: StreamingConnectionOptions);
|
|
60
|
+
private initiateCall;
|
|
61
|
+
private sessionId;
|
|
62
|
+
protected handleMessage(message: IncomingMessage): Promise<void>;
|
|
63
|
+
protected sendSessionMessage(method: string, body?: Record<any, any>): void;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RingEdgeConnection = void 0;
|
|
13
|
+
const ws_1 = require("ws");
|
|
14
|
+
const rxjs_1 = require("rxjs");
|
|
15
|
+
const util_1 = require("../util");
|
|
16
|
+
const operators_1 = require("rxjs/operators");
|
|
17
|
+
const streaming_connection_base_1 = require("./streaming-connection-base");
|
|
18
|
+
// eslint-disable-next-line no-shadow
|
|
19
|
+
var CloseReasonCode;
|
|
20
|
+
(function (CloseReasonCode) {
|
|
21
|
+
CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
|
|
22
|
+
// reason: { code: 5, text: '[rsl-apps/webrtc-liveview-server/Session.cpp:429] [Auth] [0xd540]: [rsl-apps/session-manager/Manager.cpp:227] [AppAuth] Unauthorized: invalid or expired token' }
|
|
23
|
+
// reason: { code: 5, text: 'Authentication failed: -1' }
|
|
24
|
+
// reason: { code: 5, text: 'Sessions with the provided ID not found' }
|
|
25
|
+
CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
|
|
26
|
+
// reason: { code: 6, text: 'Timeout waiting for ping' }
|
|
27
|
+
CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
|
|
28
|
+
})(CloseReasonCode || (CloseReasonCode = {}));
|
|
29
|
+
class RingEdgeConnection extends streaming_connection_base_1.StreamingConnectionBase {
|
|
30
|
+
constructor(authToken, camera, options) {
|
|
31
|
+
super(new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
|
|
32
|
+
headers: {
|
|
33
|
+
Authorization: `Bearer ${authToken}`,
|
|
34
|
+
'X-Sig-API-Version': '4.0',
|
|
35
|
+
'X-Sig-Client-ID': 'ring_android-aabb123',
|
|
36
|
+
'X-Sig-Client-Info': 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
37
|
+
'X-Sig-Auth-Type': 'ring_oauth',
|
|
38
|
+
},
|
|
39
|
+
}), options);
|
|
40
|
+
this.camera = camera;
|
|
41
|
+
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
42
|
+
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
43
|
+
this.sessionId = null;
|
|
44
|
+
this.addSubscriptions(this.onWsOpen.subscribe(() => {
|
|
45
|
+
(0, util_1.logDebug)(`WebSocket connected for ${camera.name} (Ring Edge)`);
|
|
46
|
+
this.initiateCall().catch((e) => {
|
|
47
|
+
(0, util_1.logError)(e);
|
|
48
|
+
this.callEnded();
|
|
49
|
+
});
|
|
50
|
+
}),
|
|
51
|
+
// The ring-edge session needs a ping every 5 seconds to keep the connection alive
|
|
52
|
+
this.onCallAnswered
|
|
53
|
+
.pipe((0, operators_1.switchMap)(() => (0, rxjs_1.interval)(5000)))
|
|
54
|
+
.subscribe(() => {
|
|
55
|
+
if (!this.sessionId) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.sendSessionMessage('ping');
|
|
59
|
+
}), this.pc.onIceCandidate.subscribe((iceCandidate) => __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
yield (0, rxjs_1.firstValueFrom)(this.onOfferSent);
|
|
61
|
+
this.sendMessage({
|
|
62
|
+
method: 'ice',
|
|
63
|
+
body: {
|
|
64
|
+
doorbot_id: camera.id,
|
|
65
|
+
ice: iceCandidate.candidate,
|
|
66
|
+
mlineindex: 0,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
// HACK: send ice candidate with both mline indexes to convince ring edge to connect both audio and video
|
|
70
|
+
// Without this, only audio will connect unless you connect from the network of the Ring Edge router
|
|
71
|
+
this.sendMessage({
|
|
72
|
+
method: 'ice',
|
|
73
|
+
body: {
|
|
74
|
+
doorbot_id: camera.id,
|
|
75
|
+
ice: iceCandidate.candidate,
|
|
76
|
+
mlineindex: 1,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
})));
|
|
80
|
+
}
|
|
81
|
+
initiateCall() {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
const { sdp } = yield this.pc.createOffer();
|
|
84
|
+
this.sendMessage({
|
|
85
|
+
method: 'live_view',
|
|
86
|
+
body: {
|
|
87
|
+
doorbot_id: this.camera.id,
|
|
88
|
+
stream_options: { audio_enabled: true, video_enabled: true },
|
|
89
|
+
sdp: sdp.replace('\na=group:BUNDLE 0 1', ''),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
this.onOfferSent.next();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
handleMessage(message) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
if (message.body.doorbot_id !== this.camera.id) {
|
|
98
|
+
// ignore messages for other cameras
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (['session_created', 'session_started'].includes(message.method) &&
|
|
102
|
+
'session_id' in message.body &&
|
|
103
|
+
!this.sessionId) {
|
|
104
|
+
this.sessionId = message.body.session_id;
|
|
105
|
+
this.onSessionId.next(this.sessionId);
|
|
106
|
+
}
|
|
107
|
+
if (message.body.session_id && message.body.session_id !== this.sessionId) {
|
|
108
|
+
// ignore messages for other sessions
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
switch (message.method) {
|
|
112
|
+
case 'session_created':
|
|
113
|
+
case 'session_started':
|
|
114
|
+
// session already stored above
|
|
115
|
+
return;
|
|
116
|
+
case 'sdp':
|
|
117
|
+
yield this.pc.acceptAnswer(message.body);
|
|
118
|
+
this.onCallAnswered.next(message.body.sdp);
|
|
119
|
+
return;
|
|
120
|
+
case 'ice':
|
|
121
|
+
yield this.pc.addIceCandidate({
|
|
122
|
+
candidate: message.body.ice,
|
|
123
|
+
sdpMLineIndex: message.body.mlineindex,
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
case 'pong':
|
|
127
|
+
return;
|
|
128
|
+
case 'close':
|
|
129
|
+
(0, util_1.logError)('Video stream closed');
|
|
130
|
+
(0, util_1.logError)(message.body);
|
|
131
|
+
this.callEnded();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
(0, util_1.logError)('UNKNOWN MESSAGE');
|
|
135
|
+
(0, util_1.logError)(message);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
sendSessionMessage(method, body = {}) {
|
|
139
|
+
const message = {
|
|
140
|
+
method,
|
|
141
|
+
body: Object.assign(Object.assign({}, body), { doorbot_id: this.camera.id, session_id: this.sessionId }),
|
|
142
|
+
};
|
|
143
|
+
if (this.sessionId) {
|
|
144
|
+
// Send immediately if we already have a session id
|
|
145
|
+
// This is needed to send `close` before closing the websocket
|
|
146
|
+
this.sendMessage(message);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
(0, rxjs_1.firstValueFrom)(this.onSessionId)
|
|
150
|
+
.then(() => this.sendMessage(message))
|
|
151
|
+
.catch((e) => (0, util_1.logError)(e));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.RingEdgeConnection = RingEdgeConnection;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BasicPeerConnection } from './peer-connection';
|
|
2
|
+
import { Subscribed } from '../subscribed';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
import { RtpPacket } from '@koush/werift';
|
|
5
|
+
import { ReplaySubject, Subject } from 'rxjs';
|
|
6
|
+
export interface StreamingConnectionOptions {
|
|
7
|
+
createPeerConnection?: () => BasicPeerConnection;
|
|
8
|
+
}
|
|
9
|
+
export declare abstract class StreamingConnectionBase extends Subscribed {
|
|
10
|
+
protected ws: WebSocket;
|
|
11
|
+
protected options: StreamingConnectionOptions;
|
|
12
|
+
readonly onCallAnswered: ReplaySubject<string>;
|
|
13
|
+
readonly onCallEnded: ReplaySubject<void>;
|
|
14
|
+
readonly onWsOpen: import("rxjs").Observable<unknown>;
|
|
15
|
+
protected readonly pc: BasicPeerConnection;
|
|
16
|
+
readonly onAudioRtp: Subject<RtpPacket>;
|
|
17
|
+
readonly onVideoRtp: Subject<RtpPacket>;
|
|
18
|
+
constructor(ws: WebSocket, options?: StreamingConnectionOptions);
|
|
19
|
+
protected abstract handleMessage(message: {
|
|
20
|
+
method: string;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
protected abstract sendSessionMessage(method: string, body?: Record<any, any>): void;
|
|
23
|
+
private activated;
|
|
24
|
+
activate(): Promise<void>;
|
|
25
|
+
activateCameraSpeaker(): Promise<void>;
|
|
26
|
+
protected sendMessage(message: Record<any, any>): void;
|
|
27
|
+
sendAudioPacket(rtp: RtpPacket): void;
|
|
28
|
+
protected callEnded(): void;
|
|
29
|
+
stop(): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.StreamingConnectionBase = void 0;
|
|
13
|
+
const peer_connection_1 = require("./peer-connection");
|
|
14
|
+
const subscribed_1 = require("../subscribed");
|
|
15
|
+
const rxjs_1 = require("rxjs");
|
|
16
|
+
const operators_1 = require("rxjs/operators");
|
|
17
|
+
const util_1 = require("../util");
|
|
18
|
+
class StreamingConnectionBase extends subscribed_1.Subscribed {
|
|
19
|
+
constructor(ws, options = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.ws = ws;
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
24
|
+
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
25
|
+
this.activated = false;
|
|
26
|
+
if (options.createPeerConnection) {
|
|
27
|
+
// we were passed a custom peer connection factory
|
|
28
|
+
this.pc = options.createPeerConnection();
|
|
29
|
+
// passing rtp packets is not supported for custom peer connections
|
|
30
|
+
this.onAudioRtp = new rxjs_1.Subject();
|
|
31
|
+
this.onVideoRtp = new rxjs_1.Subject();
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// no custom peer connection factory, use the werift and pass along rtp packets
|
|
35
|
+
const pc = new peer_connection_1.WeriftPeerConnection();
|
|
36
|
+
this.pc = pc;
|
|
37
|
+
this.onAudioRtp = pc.onAudioRtp;
|
|
38
|
+
this.onVideoRtp = pc.onVideoRtp;
|
|
39
|
+
}
|
|
40
|
+
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
41
|
+
const onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message'), onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
42
|
+
this.addSubscriptions(onMessage
|
|
43
|
+
.pipe((0, operators_1.concatMap)((message) => {
|
|
44
|
+
return this.handleMessage(JSON.parse(message.data));
|
|
45
|
+
}))
|
|
46
|
+
.subscribe(), onError.subscribe((e) => {
|
|
47
|
+
(0, util_1.logError)(e);
|
|
48
|
+
this.callEnded();
|
|
49
|
+
}), onClose.subscribe(() => {
|
|
50
|
+
this.callEnded();
|
|
51
|
+
}), this.pc.onConnectionState.subscribe((state) => {
|
|
52
|
+
if (state === 'failed') {
|
|
53
|
+
(0, util_1.logError)('Stream connection failed');
|
|
54
|
+
this.callEnded();
|
|
55
|
+
}
|
|
56
|
+
if (state === 'closed') {
|
|
57
|
+
(0, util_1.logDebug)('Stream connection closed');
|
|
58
|
+
this.callEnded();
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
activate() {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
if (this.activated) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.activated = true;
|
|
68
|
+
yield (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
|
|
69
|
+
(0, util_1.logInfo)('Activating Session');
|
|
70
|
+
this.sendSessionMessage('activate_session');
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
activateCameraSpeaker() {
|
|
74
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
76
|
+
this.sendSessionMessage('camera_options', {
|
|
77
|
+
stealth_mode: false,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
sendMessage(message) {
|
|
82
|
+
this.ws.send(JSON.stringify(message));
|
|
83
|
+
}
|
|
84
|
+
sendAudioPacket(rtp) {
|
|
85
|
+
if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
|
|
86
|
+
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new Error('Cannot send audio packets to a custom peer connection implementation');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
callEnded() {
|
|
93
|
+
try {
|
|
94
|
+
this.sendMessage({
|
|
95
|
+
reason: { code: 0, text: '' },
|
|
96
|
+
method: 'close',
|
|
97
|
+
});
|
|
98
|
+
this.ws.close();
|
|
99
|
+
}
|
|
100
|
+
catch (_) {
|
|
101
|
+
// ignore any errors since we are stopping the call
|
|
102
|
+
}
|
|
103
|
+
this.unsubscribe();
|
|
104
|
+
this.onCallEnded.next();
|
|
105
|
+
this.pc.close();
|
|
106
|
+
}
|
|
107
|
+
stop() {
|
|
108
|
+
this.callEnded();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.StreamingConnectionBase = StreamingConnectionBase;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ReplaySubject } from 'rxjs';
|
|
2
|
-
import { RingCamera } from './ring-camera';
|
|
3
|
-
import { Subscribed } from './subscribed';
|
|
4
1
|
import { RtpPacket } from '@koush/werift';
|
|
2
|
+
import { ReplaySubject, Subject } from 'rxjs';
|
|
3
|
+
import { RingEdgeConnection } from './ring-edge-connection';
|
|
4
|
+
import { WebrtcConnection } from './webrtc-connection';
|
|
5
|
+
import { RingCamera } from '../ring-camera';
|
|
6
|
+
import { Subscribed } from '../subscribed';
|
|
5
7
|
declare type SpawnInput = string | number;
|
|
6
8
|
export interface FfmpegOptions {
|
|
7
9
|
input?: SpawnInput[];
|
|
@@ -9,27 +11,22 @@ export interface FfmpegOptions {
|
|
|
9
11
|
audio?: SpawnInput[];
|
|
10
12
|
output: SpawnInput[];
|
|
11
13
|
}
|
|
12
|
-
export declare class
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private readonly ws;
|
|
16
|
-
private readonly onWsOpen;
|
|
17
|
-
private readonly onMessage;
|
|
18
|
-
private readonly pc;
|
|
19
|
-
readonly onCallAnswered: ReplaySubject<string>;
|
|
14
|
+
export declare class StreamingSession extends Subscribed {
|
|
15
|
+
private readonly camera;
|
|
16
|
+
private connection;
|
|
20
17
|
readonly onCallEnded: ReplaySubject<void>;
|
|
18
|
+
readonly onVideoRtp: Subject<RtpPacket>;
|
|
19
|
+
readonly onAudioRtp: Subject<RtpPacket>;
|
|
21
20
|
private readonly audioSplitter;
|
|
22
21
|
private readonly videoSplitter;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
constructor(sessionId: string, camera: RingCamera);
|
|
26
|
-
private handleMessage;
|
|
27
|
-
reservePort(bufferPorts?: number): Promise<number>;
|
|
28
|
-
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
22
|
+
constructor(camera: RingCamera, connection: RingEdgeConnection | WebrtcConnection);
|
|
23
|
+
private bindToConnection;
|
|
29
24
|
activated: boolean;
|
|
30
25
|
activate(): Promise<void>;
|
|
26
|
+
cameraSpeakerActivated: boolean;
|
|
31
27
|
activateCameraSpeaker(): Promise<void>;
|
|
32
|
-
|
|
28
|
+
reservePort(bufferPorts?: number): Promise<number>;
|
|
29
|
+
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
33
30
|
private callEnded;
|
|
34
31
|
stop(): void;
|
|
35
32
|
sendAudioPacket(rtp: RtpPacket): void;
|
|
@@ -9,15 +9,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
13
|
-
const
|
|
12
|
+
exports.StreamingSession = void 0;
|
|
13
|
+
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
14
14
|
const rxjs_1 = require("rxjs");
|
|
15
|
-
const
|
|
16
|
-
const util_1 = require("
|
|
15
|
+
const ffmpeg_1 = require("../ffmpeg");
|
|
16
|
+
const util_1 = require("../util");
|
|
17
17
|
const operators_1 = require("rxjs/operators");
|
|
18
|
-
const subscribed_1 = require("
|
|
19
|
-
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
20
|
-
const ffmpeg_1 = require("./ffmpeg");
|
|
18
|
+
const subscribed_1 = require("../subscribed");
|
|
21
19
|
function getCleanSdp(sdp, includeVideo) {
|
|
22
20
|
return sdp
|
|
23
21
|
.split('\nm=')
|
|
@@ -26,62 +24,39 @@ function getCleanSdp(sdp, includeVideo) {
|
|
|
26
24
|
.filter((section) => includeVideo || !section.startsWith('m=video'))
|
|
27
25
|
.join('\n');
|
|
28
26
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return JSON.parse(text);
|
|
32
|
-
}
|
|
33
|
-
class LiveCall extends subscribed_1.Subscribed {
|
|
34
|
-
constructor(sessionId, camera) {
|
|
27
|
+
class StreamingSession extends subscribed_1.Subscribed {
|
|
28
|
+
constructor(camera, connection) {
|
|
35
29
|
super();
|
|
36
|
-
this.sessionId = sessionId;
|
|
37
30
|
this.camera = camera;
|
|
38
|
-
this.
|
|
31
|
+
this.connection = connection;
|
|
39
32
|
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
33
|
+
this.onVideoRtp = new rxjs_1.Subject();
|
|
34
|
+
this.onAudioRtp = new rxjs_1.Subject();
|
|
40
35
|
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
41
36
|
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
42
37
|
this.activated = false;
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
this.cameraSpeakerActivated = false;
|
|
39
|
+
this.bindToConnection(connection);
|
|
40
|
+
}
|
|
41
|
+
bindToConnection(connection) {
|
|
42
|
+
this.addSubscriptions(connection.onAudioRtp.subscribe(this.onAudioRtp), connection.onVideoRtp.subscribe(this.onVideoRtp));
|
|
43
|
+
}
|
|
44
|
+
activate() {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
if (this.activated) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.activated = true;
|
|
50
|
+
yield this.connection.activate();
|
|
51
51
|
});
|
|
52
|
-
this.onAudioRtp = this.pc.onAudioRtp;
|
|
53
|
-
this.onVideoRtp = this.pc.onVideoRtp;
|
|
54
|
-
this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
|
|
55
|
-
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
56
|
-
const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
57
|
-
this.addSubscriptions(this.onMessage
|
|
58
|
-
.pipe((0, operators_1.concatMap)((message) => {
|
|
59
|
-
return this.handleMessage(JSON.parse(message.data));
|
|
60
|
-
}))
|
|
61
|
-
.subscribe(), this.onWsOpen.subscribe(() => {
|
|
62
|
-
(0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
|
|
63
|
-
}), onError.subscribe((e) => {
|
|
64
|
-
(0, util_1.logError)(e);
|
|
65
|
-
this.callEnded();
|
|
66
|
-
}), onClose.subscribe(() => {
|
|
67
|
-
this.callEnded();
|
|
68
|
-
}));
|
|
69
52
|
}
|
|
70
|
-
|
|
53
|
+
activateCameraSpeaker() {
|
|
71
54
|
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const answer = yield this.pc.createAnswer(message);
|
|
75
|
-
this.sendMessage(Object.assign({ method: 'sdp' }, answer));
|
|
76
|
-
this.onCallAnswered.next(message.sdp);
|
|
77
|
-
return;
|
|
78
|
-
case 'ice':
|
|
79
|
-
yield this.pc.addIceCandidate({
|
|
80
|
-
candidate: message.ice,
|
|
81
|
-
sdpMLineIndex: message.mlineindex,
|
|
82
|
-
});
|
|
83
|
-
return;
|
|
55
|
+
if (this.cameraSpeakerActivated) {
|
|
56
|
+
return;
|
|
84
57
|
}
|
|
58
|
+
this.cameraSpeakerActivated = true;
|
|
59
|
+
yield this.connection.activateCameraSpeaker();
|
|
85
60
|
});
|
|
86
61
|
}
|
|
87
62
|
reservePort(bufferPorts = 0) {
|
|
@@ -103,7 +78,7 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
103
78
|
...(ffmpegOptions.input || []),
|
|
104
79
|
'-i',
|
|
105
80
|
'pipe:',
|
|
106
|
-
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
81
|
+
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.connection.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
107
82
|
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
108
83
|
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
109
84
|
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
@@ -139,48 +114,10 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
139
114
|
yield this.activate();
|
|
140
115
|
});
|
|
141
116
|
}
|
|
142
|
-
activate() {
|
|
143
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
144
|
-
if (this.activated) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
this.activated = true;
|
|
148
|
-
yield (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
|
|
149
|
-
(0, util_1.logInfo)('Activating Session');
|
|
150
|
-
this.sendMessage({ method: 'activate_session' });
|
|
151
|
-
this.sendMessage({
|
|
152
|
-
video_enabled: true,
|
|
153
|
-
audio_enabled: true,
|
|
154
|
-
method: 'stream_options',
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
activateCameraSpeaker() {
|
|
159
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
160
|
-
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
161
|
-
this.sendMessage({
|
|
162
|
-
stealth_mode: false,
|
|
163
|
-
method: 'camera_options',
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
sendMessage(message) {
|
|
168
|
-
this.ws.send(JSON.stringify(message));
|
|
169
|
-
}
|
|
170
117
|
callEnded() {
|
|
171
|
-
try {
|
|
172
|
-
this.sendMessage({
|
|
173
|
-
reason: { code: 0, text: '' },
|
|
174
|
-
method: 'close',
|
|
175
|
-
});
|
|
176
|
-
this.ws.close();
|
|
177
|
-
}
|
|
178
|
-
catch (_) {
|
|
179
|
-
// ignore any errors since we are stopping the call
|
|
180
|
-
}
|
|
181
118
|
this.unsubscribe();
|
|
182
119
|
this.onCallEnded.next();
|
|
183
|
-
this.
|
|
120
|
+
this.connection.stop();
|
|
184
121
|
this.audioSplitter.close();
|
|
185
122
|
this.videoSplitter.close();
|
|
186
123
|
}
|
|
@@ -188,7 +125,7 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
188
125
|
this.callEnded();
|
|
189
126
|
}
|
|
190
127
|
sendAudioPacket(rtp) {
|
|
191
|
-
this.
|
|
128
|
+
this.connection.sendAudioPacket(rtp);
|
|
192
129
|
}
|
|
193
130
|
}
|
|
194
|
-
exports.
|
|
131
|
+
exports.StreamingSession = StreamingSession;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RingCamera } from '../ring-camera';
|
|
2
|
+
import { StreamingConnectionBase, StreamingConnectionOptions } from './streaming-connection-base';
|
|
3
|
+
interface InitializationMessage {
|
|
4
|
+
method: 'initialization';
|
|
5
|
+
text: 'Done';
|
|
6
|
+
}
|
|
7
|
+
interface OfferMessage {
|
|
8
|
+
method: 'sdp';
|
|
9
|
+
sdp: string;
|
|
10
|
+
type: 'offer';
|
|
11
|
+
}
|
|
12
|
+
interface IceCandidateMessage {
|
|
13
|
+
method: 'ice';
|
|
14
|
+
ice: string;
|
|
15
|
+
mlineindex: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class WebrtcConnection extends StreamingConnectionBase {
|
|
18
|
+
private sessionId;
|
|
19
|
+
constructor(sessionId: string, camera: RingCamera, options: StreamingConnectionOptions);
|
|
20
|
+
protected handleMessage(message: InitializationMessage | OfferMessage | IceCandidateMessage): Promise<void>;
|
|
21
|
+
protected sendSessionMessage(method: string, body?: Record<any, any>): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.WebrtcConnection = void 0;
|
|
13
|
+
const ws_1 = require("ws");
|
|
14
|
+
const streaming_connection_base_1 = require("./streaming-connection-base");
|
|
15
|
+
const util_1 = require("../util");
|
|
16
|
+
function parseLiveCallSession(sessionId) {
|
|
17
|
+
const encodedSession = sessionId.split('.')[1], buff = Buffer.from(encodedSession, 'base64'), text = buff.toString('ascii');
|
|
18
|
+
return JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
class WebrtcConnection extends streaming_connection_base_1.StreamingConnectionBase {
|
|
21
|
+
constructor(sessionId, camera, options) {
|
|
22
|
+
const liveCallSession = parseLiveCallSession(sessionId);
|
|
23
|
+
super(new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
|
|
24
|
+
headers: {
|
|
25
|
+
API_VERSION: '3.1',
|
|
26
|
+
API_TOKEN: sessionId,
|
|
27
|
+
CLIENT_INFO: 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
28
|
+
},
|
|
29
|
+
}), options);
|
|
30
|
+
this.sessionId = sessionId;
|
|
31
|
+
this.addSubscriptions(this.onWsOpen.subscribe(() => {
|
|
32
|
+
(0, util_1.logDebug)(`WebSocket connected for ${camera.name}`);
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
handleMessage(message) {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
switch (message.method) {
|
|
38
|
+
case 'sdp':
|
|
39
|
+
const answer = yield this.pc.createAnswer(message);
|
|
40
|
+
this.sendSessionMessage('sdp', answer);
|
|
41
|
+
this.onCallAnswered.next(message.sdp);
|
|
42
|
+
this.sendSessionMessage('stream_options', {
|
|
43
|
+
audio_enabled: true,
|
|
44
|
+
video_enabled: true,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
case 'ice':
|
|
48
|
+
yield this.pc.addIceCandidate({
|
|
49
|
+
candidate: message.ice,
|
|
50
|
+
sdpMLineIndex: message.mlineindex,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
sendSessionMessage(method, body = {}) {
|
|
57
|
+
this.sendMessage(Object.assign(Object.assign({}, body), { method }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.WebrtcConnection = WebrtcConnection;
|
|
@@ -34,7 +34,7 @@ function getSessionConfig(srtpOptions) {
|
|
|
34
34
|
profile: 1,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
class
|
|
37
|
+
class StreamingSessionWrapper {
|
|
38
38
|
constructor(liveCall, prepareStreamRequest, ringCamera, start) {
|
|
39
39
|
this.liveCall = liveCall;
|
|
40
40
|
this.prepareStreamRequest = prepareStreamRequest;
|
|
@@ -351,7 +351,7 @@ class CameraSource {
|
|
|
351
351
|
const start = Date.now();
|
|
352
352
|
(0, util_1.logInfo)(`Preparing Live Stream for ${this.ringCamera.name}`);
|
|
353
353
|
try {
|
|
354
|
-
const liveCall = yield this.ringCamera.startLiveCall(), session = new
|
|
354
|
+
const liveCall = yield this.ringCamera.startLiveCall(), session = new StreamingSessionWrapper(liveCall, request, this.ringCamera, start);
|
|
355
355
|
this.sessions[request.sessionID] = session;
|
|
356
356
|
(0, util_1.logInfo)(`Stream Prepared for ${this.ringCamera.name} (${getDurationSeconds(start)}s)`);
|
|
357
357
|
callback(undefined, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-ring",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.6",
|
|
4
4
|
"description": "Homebridge plugin for Ring doorbells, cameras, security alarm system and smart lighting",
|
|
5
5
|
"main": "lib/homebridge/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,44 +40,44 @@
|
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@eneris/push-receiver": "^3.0.1",
|
|
43
|
-
"@homebridge/camera-utils": "2.1.0",
|
|
44
|
-
"@homebridge/plugin-ui-utils": "0.0.19",
|
|
43
|
+
"@homebridge/camera-utils": "^2.1.0",
|
|
44
|
+
"@homebridge/plugin-ui-utils": "^0.0.19",
|
|
45
45
|
"@koush/werift": "0.14.5-beta9",
|
|
46
46
|
"@types/socket.io-client": "1.4.36",
|
|
47
|
-
"colors": "1.4.0",
|
|
48
|
-
"debug": "4.3.4",
|
|
49
|
-
"got": "11.8.3",
|
|
50
|
-
"rxjs": "7.5.5",
|
|
51
|
-
"socket.io-client": "2.4.0",
|
|
52
|
-
"systeminformation": "5.11.
|
|
53
|
-
"uuid": "8.3.2",
|
|
54
|
-
"ws": "8.
|
|
47
|
+
"colors": "^1.4.0",
|
|
48
|
+
"debug": "^4.3.4",
|
|
49
|
+
"got": "^11.8.3",
|
|
50
|
+
"rxjs": "^7.5.5",
|
|
51
|
+
"socket.io-client": "^2.4.0",
|
|
52
|
+
"systeminformation": "^5.11.14",
|
|
53
|
+
"uuid": "^8.3.2",
|
|
54
|
+
"ws": "^8.6.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/debug": "4.1.7",
|
|
58
|
-
"@types/jest": "27.
|
|
59
|
-
"@types/node": "17.0.
|
|
58
|
+
"@types/jest": "27.5.0",
|
|
59
|
+
"@types/node": "17.0.31",
|
|
60
60
|
"@types/uuid": "8.3.4",
|
|
61
61
|
"@types/ws": "^8.5.3",
|
|
62
|
-
"@typescript-eslint/eslint-plugin": "5.
|
|
63
|
-
"@typescript-eslint/parser": "5.
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "5.22.0",
|
|
63
|
+
"@typescript-eslint/parser": "5.22.0",
|
|
64
64
|
"concurrently": "^7.1.0",
|
|
65
65
|
"conventional-github-releaser": "3.1.5",
|
|
66
66
|
"dotenv": "16.0.0",
|
|
67
|
-
"esbuild": "^0.14.
|
|
68
|
-
"eslint": "8.
|
|
67
|
+
"esbuild": "^0.14.38",
|
|
68
|
+
"eslint": "8.14.0",
|
|
69
69
|
"eslint-config-prettier": "8.5.0",
|
|
70
|
-
"eslint-plugin-jest": "26.1.
|
|
70
|
+
"eslint-plugin-jest": "26.1.5",
|
|
71
71
|
"eslint-plugin-prettier": "4.0.0",
|
|
72
|
-
"express": "4.
|
|
73
|
-
"homebridge": "1.4.
|
|
74
|
-
"jest": "
|
|
72
|
+
"express": "4.18.1",
|
|
73
|
+
"homebridge": "1.4.1",
|
|
74
|
+
"jest": "28.1.0",
|
|
75
75
|
"prettier": "2.6.2",
|
|
76
76
|
"reflect-metadata": "0.1.13",
|
|
77
77
|
"rimraf": "3.0.2",
|
|
78
78
|
"standard-version": "9.3.2",
|
|
79
|
-
"ts-jest": "
|
|
80
|
-
"typescript": "4.6.
|
|
79
|
+
"ts-jest": "28.0.1",
|
|
80
|
+
"typescript": "4.6.4"
|
|
81
81
|
},
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=8.3.0",
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { ReplaySubject } from 'rxjs';
|
|
2
|
-
import { RingCamera } from './ring-camera';
|
|
3
|
-
import { Subscribed } from './subscribed';
|
|
4
|
-
import { RtpPacket } from '@koush/werift';
|
|
5
|
-
declare type SpawnInput = string | number;
|
|
6
|
-
export interface FfmpegOptions {
|
|
7
|
-
input?: SpawnInput[];
|
|
8
|
-
video?: SpawnInput[] | false;
|
|
9
|
-
audio?: SpawnInput[];
|
|
10
|
-
output: SpawnInput[];
|
|
11
|
-
}
|
|
12
|
-
export declare class LiveCallRingEdge extends Subscribed {
|
|
13
|
-
private camera;
|
|
14
|
-
private readonly ws;
|
|
15
|
-
private readonly onWsOpen;
|
|
16
|
-
private readonly onMessage;
|
|
17
|
-
private readonly pc;
|
|
18
|
-
readonly onSessionId: ReplaySubject<string>;
|
|
19
|
-
readonly onCallAnswered: ReplaySubject<string>;
|
|
20
|
-
readonly onCallEnded: ReplaySubject<void>;
|
|
21
|
-
private readonly audioSplitter;
|
|
22
|
-
private readonly videoSplitter;
|
|
23
|
-
readonly onVideoRtp: import("rxjs").Subject<RtpPacket>;
|
|
24
|
-
readonly onAudioRtp: import("rxjs").Subject<RtpPacket>;
|
|
25
|
-
private readonly onOfferSent;
|
|
26
|
-
constructor(authToken: string, camera: RingCamera);
|
|
27
|
-
private initiateCall;
|
|
28
|
-
private sessionId;
|
|
29
|
-
private handleMessage;
|
|
30
|
-
reservePort(bufferPorts?: number): Promise<number>;
|
|
31
|
-
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
32
|
-
activated: boolean;
|
|
33
|
-
activate(): Promise<void>;
|
|
34
|
-
activateCameraSpeaker(): Promise<void>;
|
|
35
|
-
private sendSessionMessage;
|
|
36
|
-
private sendMessage;
|
|
37
|
-
private callEnded;
|
|
38
|
-
stop(): void;
|
|
39
|
-
sendAudioPacket(rtp: RtpPacket): void;
|
|
40
|
-
}
|
|
41
|
-
export {};
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.LiveCallRingEdge = void 0;
|
|
13
|
-
const ws_1 = require("ws");
|
|
14
|
-
const rxjs_1 = require("rxjs");
|
|
15
|
-
const peer_connection_1 = require("./peer-connection");
|
|
16
|
-
const util_1 = require("./util");
|
|
17
|
-
const operators_1 = require("rxjs/operators");
|
|
18
|
-
const subscribed_1 = require("./subscribed");
|
|
19
|
-
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
20
|
-
const ffmpeg_1 = require("./ffmpeg");
|
|
21
|
-
// eslint-disable-next-line no-shadow
|
|
22
|
-
var CloseReasonCode;
|
|
23
|
-
(function (CloseReasonCode) {
|
|
24
|
-
CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
|
|
25
|
-
// reason: { code: 5, text: '[rsl-apps/webrtc-liveview-server/Session.cpp:429] [Auth] [0xd540]: [rsl-apps/session-manager/Manager.cpp:227] [AppAuth] Unauthorized: invalid or expired token' }
|
|
26
|
-
// reason: { code: 5, text: 'Authentication failed: -1' }
|
|
27
|
-
// reason: { code: 5, text: 'Sessions with the provided ID not found' }
|
|
28
|
-
CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
|
|
29
|
-
// reason: { code: 6, text: 'Timeout waiting for ping' }
|
|
30
|
-
CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
|
|
31
|
-
})(CloseReasonCode || (CloseReasonCode = {}));
|
|
32
|
-
function getCleanSdp(sdp, includeVideo) {
|
|
33
|
-
return sdp
|
|
34
|
-
.split('\nm=')
|
|
35
|
-
.slice(1)
|
|
36
|
-
.map((section) => 'm=' + section)
|
|
37
|
-
.filter((section) => includeVideo || !section.startsWith('m=video'))
|
|
38
|
-
.join('\n');
|
|
39
|
-
}
|
|
40
|
-
class LiveCallRingEdge extends subscribed_1.Subscribed {
|
|
41
|
-
constructor(authToken, camera) {
|
|
42
|
-
super();
|
|
43
|
-
this.camera = camera;
|
|
44
|
-
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
45
|
-
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
46
|
-
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
47
|
-
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
48
|
-
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
49
|
-
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
50
|
-
this.sessionId = null;
|
|
51
|
-
this.activated = false;
|
|
52
|
-
this.pc = new peer_connection_1.PeerConnection();
|
|
53
|
-
this.ws = new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
|
|
54
|
-
headers: {
|
|
55
|
-
Authorization: `Bearer ${authToken}`,
|
|
56
|
-
'X-Sig-API-Version': '4.0',
|
|
57
|
-
'X-Sig-Client-ID': 'ring_android-aabb123',
|
|
58
|
-
'X-Sig-Client-Info': 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
59
|
-
'X-Sig-Auth-Type': 'ring_oauth',
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
this.onAudioRtp = this.pc.onAudioRtp;
|
|
63
|
-
this.onVideoRtp = this.pc.onVideoRtp;
|
|
64
|
-
this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
|
|
65
|
-
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
66
|
-
const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
67
|
-
this.addSubscriptions(this.onMessage
|
|
68
|
-
.pipe((0, operators_1.concatMap)((message) => {
|
|
69
|
-
return this.handleMessage(JSON.parse(message.data));
|
|
70
|
-
}))
|
|
71
|
-
.subscribe(), this.onWsOpen.subscribe(() => {
|
|
72
|
-
(0, util_1.logDebug)(`WebSocket connected for ${this.camera.name} (Ring Edge)`);
|
|
73
|
-
this.initiateCall().catch((e) => {
|
|
74
|
-
(0, util_1.logError)(e);
|
|
75
|
-
this.callEnded();
|
|
76
|
-
});
|
|
77
|
-
}), onError.subscribe((e) => {
|
|
78
|
-
(0, util_1.logError)(e);
|
|
79
|
-
this.callEnded();
|
|
80
|
-
}), onClose.subscribe(() => {
|
|
81
|
-
this.callEnded();
|
|
82
|
-
}), this.onCallAnswered
|
|
83
|
-
.pipe((0, operators_1.switchMap)(() => (0, rxjs_1.interval)(5000)))
|
|
84
|
-
.subscribe(() => {
|
|
85
|
-
if (!this.sessionId) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
this.sendSessionMessage('ping');
|
|
89
|
-
}), this.pc.onIceCandidate.subscribe((iceCandidate) => __awaiter(this, void 0, void 0, function* () {
|
|
90
|
-
yield (0, rxjs_1.firstValueFrom)(this.onOfferSent);
|
|
91
|
-
this.sendMessage({
|
|
92
|
-
method: 'ice',
|
|
93
|
-
body: {
|
|
94
|
-
doorbot_id: this.camera.id,
|
|
95
|
-
ice: iceCandidate.candidate,
|
|
96
|
-
mlineindex: 0,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
// HACK: send ice candidate with both mline indexes to convince ring edge to connect both audio and video
|
|
100
|
-
// Without this, only audio will connect unless you connect from the network of the Ring Edge router
|
|
101
|
-
this.sendMessage({
|
|
102
|
-
method: 'ice',
|
|
103
|
-
body: {
|
|
104
|
-
doorbot_id: this.camera.id,
|
|
105
|
-
ice: iceCandidate.candidate,
|
|
106
|
-
mlineindex: 1,
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
})), this.pc.onConnectionState.subscribe((state) => {
|
|
110
|
-
if (state === 'failed') {
|
|
111
|
-
(0, util_1.logError)('Stream connection failed');
|
|
112
|
-
this.callEnded();
|
|
113
|
-
}
|
|
114
|
-
if (state === 'closed') {
|
|
115
|
-
(0, util_1.logDebug)('Stream connection closed');
|
|
116
|
-
this.callEnded();
|
|
117
|
-
}
|
|
118
|
-
}));
|
|
119
|
-
}
|
|
120
|
-
initiateCall() {
|
|
121
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
-
const offer = yield this.pc.createOffer();
|
|
123
|
-
offer.sdp = offer.sdp.replace('\na=group:BUNDLE 0 1', '');
|
|
124
|
-
this.sendMessage({
|
|
125
|
-
method: 'live_view',
|
|
126
|
-
body: {
|
|
127
|
-
doorbot_id: this.camera.id,
|
|
128
|
-
stream_options: { audio_enabled: true, video_enabled: true },
|
|
129
|
-
sdp: offer.sdp,
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
this.onOfferSent.next();
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
handleMessage(message) {
|
|
136
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
137
|
-
if (message.body.doorbot_id !== this.camera.id) {
|
|
138
|
-
// ignore messages for other cameras
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (['session_created', 'session_started'].includes(message.method) &&
|
|
142
|
-
'session_id' in message.body &&
|
|
143
|
-
!this.sessionId) {
|
|
144
|
-
this.sessionId = message.body.session_id;
|
|
145
|
-
this.onSessionId.next(this.sessionId);
|
|
146
|
-
}
|
|
147
|
-
if (message.body.session_id && message.body.session_id !== this.sessionId) {
|
|
148
|
-
// ignore messages for other sessions
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
switch (message.method) {
|
|
152
|
-
case 'session_created':
|
|
153
|
-
case 'session_started':
|
|
154
|
-
// session already stored above
|
|
155
|
-
return;
|
|
156
|
-
case 'sdp':
|
|
157
|
-
yield this.pc.acceptAnswer(message.body);
|
|
158
|
-
this.onCallAnswered.next(message.body.sdp);
|
|
159
|
-
return;
|
|
160
|
-
case 'ice':
|
|
161
|
-
yield this.pc.addIceCandidate({
|
|
162
|
-
candidate: message.body.ice,
|
|
163
|
-
sdpMLineIndex: message.body.mlineindex,
|
|
164
|
-
});
|
|
165
|
-
return;
|
|
166
|
-
case 'pong':
|
|
167
|
-
return;
|
|
168
|
-
case 'close':
|
|
169
|
-
(0, util_1.logError)('Video stream closed');
|
|
170
|
-
(0, util_1.logError)(message.body);
|
|
171
|
-
this.callEnded();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
(0, util_1.logError)('UNKNOWN MESSAGE');
|
|
175
|
-
(0, util_1.logError)(message);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
reservePort(bufferPorts = 0) {
|
|
179
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
-
const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
|
|
181
|
-
return ports[0];
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
startTranscoding(ffmpegOptions) {
|
|
185
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
186
|
-
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ffmpegInputArguments = [
|
|
187
|
-
'-hide_banner',
|
|
188
|
-
'-protocol_whitelist',
|
|
189
|
-
'pipe,udp,rtp,file,crypto',
|
|
190
|
-
'-acodec',
|
|
191
|
-
'libopus',
|
|
192
|
-
'-f',
|
|
193
|
-
'sdp',
|
|
194
|
-
...(ffmpegOptions.input || []),
|
|
195
|
-
'-i',
|
|
196
|
-
'pipe:',
|
|
197
|
-
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
198
|
-
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
199
|
-
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
200
|
-
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
201
|
-
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
202
|
-
: []), ...(ffmpegOptions.output || [])),
|
|
203
|
-
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
204
|
-
exitCallback: () => this.callEnded(),
|
|
205
|
-
logLabel: `From Ring (${this.camera.name})`,
|
|
206
|
-
logger: {
|
|
207
|
-
error: util_1.logError,
|
|
208
|
-
info: util_1.logDebug,
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
this.addSubscriptions(this.onAudioRtp
|
|
212
|
-
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
213
|
-
return this.audioSplitter.send(rtp.serialize(), {
|
|
214
|
-
port: audioPort,
|
|
215
|
-
});
|
|
216
|
-
}))
|
|
217
|
-
.subscribe());
|
|
218
|
-
if (transcodeVideoStream) {
|
|
219
|
-
this.addSubscriptions(this.onVideoRtp
|
|
220
|
-
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
221
|
-
return this.videoSplitter.send(rtp.serialize(), {
|
|
222
|
-
port: videoPort,
|
|
223
|
-
});
|
|
224
|
-
}))
|
|
225
|
-
.subscribe());
|
|
226
|
-
}
|
|
227
|
-
this.onCallEnded.subscribe(() => ff.stop());
|
|
228
|
-
ff.writeStdin(inputSdp);
|
|
229
|
-
// Activate the stream now that ffmpeg is ready to receive
|
|
230
|
-
yield this.activate();
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
activate() {
|
|
234
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
-
if (this.activated) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
this.activated = true;
|
|
239
|
-
yield (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
|
|
240
|
-
(0, util_1.logInfo)('Activating Session');
|
|
241
|
-
this.sendSessionMessage('activate_session');
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
activateCameraSpeaker() {
|
|
245
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
246
|
-
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
247
|
-
this.sendSessionMessage('camera_options', {
|
|
248
|
-
stealth_mode: false,
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
sendSessionMessage(method, body = {}) {
|
|
253
|
-
const message = {
|
|
254
|
-
method,
|
|
255
|
-
body: Object.assign(Object.assign({}, body), { doorbot_id: this.camera.id, session_id: this.sessionId }),
|
|
256
|
-
};
|
|
257
|
-
if (this.sessionId) {
|
|
258
|
-
// Send immediately if we already have a session id
|
|
259
|
-
// This is needed to send `close` before closing the websocket
|
|
260
|
-
this.sendMessage(message);
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
(0, rxjs_1.firstValueFrom)(this.onSessionId)
|
|
264
|
-
.then(() => this.sendMessage(message))
|
|
265
|
-
.catch((e) => (0, util_1.logError)(e));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
sendMessage(message) {
|
|
269
|
-
this.ws.send(JSON.stringify(message));
|
|
270
|
-
}
|
|
271
|
-
callEnded() {
|
|
272
|
-
try {
|
|
273
|
-
this.sendSessionMessage('close', {
|
|
274
|
-
reason: { code: CloseReasonCode.NormalClose, text: '' },
|
|
275
|
-
});
|
|
276
|
-
this.ws.close();
|
|
277
|
-
}
|
|
278
|
-
catch (_) {
|
|
279
|
-
// ignore any errors since we are stopping the call
|
|
280
|
-
}
|
|
281
|
-
this.unsubscribe();
|
|
282
|
-
this.onCallEnded.next();
|
|
283
|
-
this.pc.close();
|
|
284
|
-
this.audioSplitter.close();
|
|
285
|
-
this.videoSplitter.close();
|
|
286
|
-
}
|
|
287
|
-
stop() {
|
|
288
|
-
this.callEnded();
|
|
289
|
-
}
|
|
290
|
-
sendAudioPacket(rtp) {
|
|
291
|
-
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
exports.LiveCallRingEdge = LiveCallRingEdge;
|