UNPKG

@koush/ring-client-api

Version:

Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting

128 lines (107 loc) 3.19 kB
import { parseLiveCallSession } from './rtp-utils' import { WebSocket } from 'ws' import { concatMap, firstValueFrom, fromEvent, Observable, ReplaySubject, Subject } from 'rxjs' import { logDebug, logError } from './util' import { RingCamera } from './ring-camera' import { Subscribed } from './subscribed' import { RTCSessionDescription } from '@koush/werift' export interface InitializationMessage { method: 'initialization' text: 'Done' } export interface OfferMessage { method: 'sdp' sdp: string type: 'offer' } export interface IceCandidateMessage { method: 'ice' ice: string mlineindex: number } export type LiveCallNegotiationMessage = InitializationMessage | OfferMessage | IceCandidateMessage; export class LiveCallNegotiation extends Subscribed { private readonly ws private readonly onWsOpen readonly onMessage: Observable<LiveCallNegotiationMessage> readonly onCallAnswered = new ReplaySubject<void>(1) readonly onCallEnded = new ReplaySubject<void>(1) constructor(private sessionId: string, public camera: RingCamera) { super(); const liveCallSession = parseLiveCallSession(sessionId) this.ws = new WebSocket( `wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, { headers: { API_VERSION: '3.1', API_TOKEN: sessionId, CLIENT_INFO: 'Ring/3.48.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00', }, } ) this.onMessage = fromEvent(this.ws, 'message') .pipe( concatMap(async (message) => JSON.parse((message as MessageEvent).data) as LiveCallNegotiationMessage)); this.onWsOpen = fromEvent(this.ws, 'open') const onError = fromEvent(this.ws, 'error'), onClose = fromEvent(this.ws, 'close') this.addSubscriptions( this.onWsOpen.subscribe(() => { logDebug(`WebSocket connected for ${this.camera.name}`) }), onError.subscribe((e) => { logError(e) this.callEnded() }), onClose.subscribe(() => { this.callEnded() }) ) } private sendMessage(message: Record<any, any>) { this.ws.send(JSON.stringify(message)) } sendAnswer(answer: RTCSessionDescriptionInit) { this.sendMessage({ method: 'sdp', ...answer, }) this.onCallAnswered.next() } activated = false async activate() { if (this.activated) { return } this.activated = true await firstValueFrom(this.onCallAnswered) this.sendMessage({ method: 'activate_session' }) this.sendMessage({ video_enabled: true, audio_enabled: true, method: 'stream_options', }) } async activateCameraSpeaker() { await firstValueFrom(this.onCallAnswered) this.sendMessage({ stealth_mode: false, method: 'camera_options', }) } protected callEnded() { try { this.sendMessage({ reason: { code: 0, text: '' }, method: 'close', }) this.ws.close() } catch (_) { // ignore any errors since we are stopping the call } this.unsubscribe() this.onCallEnded.next() } }