UNPKG

@koush/ring-client-api

Version:

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

200 lines (175 loc) 5.1 kB
import { PeerConnection } from './peer-connection' import { logDebug, logError } from './util' import { RingCamera } from './ring-camera' import { concatMap } from 'rxjs/operators' import { FfmpegProcess, reservePorts, RtpSplitter, } from '@homebridge/camera-utils' import { getFfmpegPath } from './ffmpeg' import { RtpPacket } from '@koush/werift' import { IceCandidateMessage, InitializationMessage, LiveCallNegotiation, OfferMessage } from './live-call-negotiation' type SpawnInput = string | number export interface FfmpegOptions { input?: SpawnInput[] video?: SpawnInput[] | false audio?: SpawnInput[] output: SpawnInput[] } export class LiveCall extends LiveCallNegotiation { private readonly pc private readonly audioSplitter = new RtpSplitter() private readonly videoSplitter = new RtpSplitter() readonly onVideoRtp readonly onAudioRtp constructor(sessionId: string, camera: RingCamera) { super(sessionId, camera) this.pc = new PeerConnection() this.onAudioRtp = this.pc.onAudioRtp this.onVideoRtp = this.pc.onVideoRtp this.addSubscriptions( this.onMessage .pipe( concatMap((message) => { return this.handleMessage(message) }) ) .subscribe(), ) } private async handleMessage( message: InitializationMessage | OfferMessage | IceCandidateMessage ) { switch (message.method) { case 'sdp': const answer = await this.pc.createAnswer(message) this.sendAnswer(answer); return case 'ice': await this.pc.addIceCandidate({ candidate: message.ice, sdpMLineIndex: message.mlineindex, }) return } } async reservePort(bufferPorts = 0) { const ports = await reservePorts({ count: bufferPorts + 1 }) return ports[0] } public prepareTranscoder( transcodeVideoStream: boolean, ffmpegInputOptions: SpawnInput[] | undefined, audioPort: number, videoPort: number, sdpInput: string ) { const ffmpegInputArguments = [ '-hide_banner', '-protocol_whitelist', 'pipe,udp,rtp,file,crypto', '-acodec', 'libopus', '-f', 'sdp', ...(ffmpegInputOptions || []), '-i', sdpInput, ], inputSdpLines = [ 'v=0', 'o=105202070 3747 461 IN IP4 127.0.0.1', 's=Talk', 'c=IN IP4 127.0.0.1', 'b=AS:380', 't=0 0', 'a=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics', `m=audio ${audioPort} RTP/SAVP 101`, 'a=rtpmap:101 OPUS/48000/2', 'a=rtcp-fb:101 nack pli', 'a=fmtp:101 useinbandfec=1;sprop-stereo=0', 'a=rtcp-mux', ] if (transcodeVideoStream) { inputSdpLines.push( `m=video ${videoPort} RTP/SAVP 96`, 'a=rtpmap:96 H264/90000', 'a=rtcp-fb:96 nack', 'a=rtcp-fb:96 nack pli', 'a=fmtp:96 packetization-mode=1;profile-level-id=640029;level-asymmetry-allowed=1', 'a=rtcp-mux' ) this.addSubscriptions( this.onVideoRtp .pipe( concatMap((rtp) => { return this.videoSplitter.send(rtp.serialize(), { port: videoPort, }) }) ) .subscribe() ) } this.addSubscriptions( this.onAudioRtp .pipe( concatMap((rtp) => { return this.audioSplitter.send(rtp.serialize(), { port: audioPort, }) }) ) .subscribe() ) return { ffmpegInputArguments, inputSdpLines, } } async startTranscoding(ffmpegOptions: FfmpegOptions) { const videoPort = await this.reservePort(1), audioPort = await this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, { ffmpegInputArguments, inputSdpLines } = this.prepareTranscoder( transcodeVideoStream, ffmpegOptions.input, audioPort, videoPort, 'pipe:' ), ff = new FfmpegProcess({ ffmpegArgs: ffmpegInputArguments.concat( ...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream ? ffmpegOptions.video || ['-vcodec', 'copy'] : []), ...(ffmpegOptions.output || []) ), ffmpegPath: getFfmpegPath(), exitCallback: () => this.callEnded(), logLabel: `From Ring (${this.camera.name})`, logger: { error: logError, info: logDebug, }, }) this.onCallEnded.subscribe(() => ff.stop()) ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n')) // Activate the stream now that ffmpeg is ready to receive await this.activate() } protected callEnded() { super.callEnded(); this.pc.close() this.audioSplitter.close() this.videoSplitter.close() } stop() { this.callEnded() } sendAudioPacket(rtp: RtpPacket) { this.pc.returnAudioTrack.writeRtp(rtp) } }