homebridge#StreamRequestCallback TypeScript Examples

The following examples show how to use homebridge#StreamRequestCallback. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: new-streaming-delegate.ts    From homebridge-plugin-eufy-security with Apache License 2.0 6 votes vote down vote up
handleStreamRequest(
    request: StreamingRequest,
    callback: StreamRequestCallback,
  ): void {
    switch (request.type) {
      case StreamRequestTypes.START:
        this.startStream(request, callback);
        break;
      case StreamRequestTypes.RECONFIGURE:
        this.log.debug(
          'Received request to reconfigure: ' +
            request.video.width +
            ' x ' +
            request.video.height +
            ', ' +
            request.video.fps +
            ' fps, ' +
            request.video.max_bit_rate +
            ' kbps (Ignored)',
          this.cameraName,
          this.debug,
        );
        callback();
        break;
      case StreamRequestTypes.STOP:
        this.stopStream(request.sessionID);
        callback();
        break;
    }
  }
Example #2
Source File: streamingDelegate.ts    From homebridge-eufy-security with Apache License 2.0 6 votes vote down vote up
handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void {
    switch (request.type) {
      case StreamRequestTypes.START:
        this.startStream(request, callback);
        break;
      case StreamRequestTypes.RECONFIGURE:
        this.log.debug('Received request to reconfigure: ' + request.video.width + ' x ' + request.video.height + ', ' +
          request.video.fps + ' fps, ' + request.video.max_bit_rate + ' kbps (Ignored)', this.cameraName, this.videoConfig.debug);
        callback();
        break;
      case StreamRequestTypes.STOP:
        this.stopStream(request.sessionID);
        callback();
        break;
    }
  }
Example #3
Source File: ffmpeg.ts    From homebridge-plugin-eufy-security with Apache License 2.0 5 votes vote down vote up
constructor(
    cameraName: string,
    sessionId: string,
    videoProcessor: string,
    ffmpegArgs: string,
    log: Logger,
    debug: boolean,
    delegate: EufyCameraStreamingDelegate,
    callback?: StreamRequestCallback,
  ) {
    log.debug(
      'Stream command: ' + videoProcessor + ' ' + ffmpegArgs,
      cameraName,
      debug,
    );

    let started = false;
    this.process = spawn(videoProcessor, ffmpegArgs.split(/\s+/), {
      env: process.env,
    });

    if (this.process.stdin) {
      this.process.stdin.on('error', (error: Error) => {
        if (!error.message.includes('EPIPE')) {
          log.error(error.message, cameraName);
        }
      });
    }
    if (this.process.stderr) {
      this.process.stderr.on('data', (data) => {
        if (!started) {
          started = true;
          if (callback) {
            callback();
          }
        }

        if (debug) {
          data
            .toString()
            .split(/\n/)
            .forEach((line: string) => {
              if (line && line !== '') {
                log.debug(line);
              }
            });
        }
      });
    }
    this.process.on('error', (error: Error) => {
      log.error('Failed to start stream: ' + error.message, cameraName);
      if (callback) {
        callback(new Error('FFmpeg process creation failed'));
      }
      delegate.stopStream(sessionId);
    });
    this.process.on('exit', (code: number, signal: NodeJS.Signals) => {
      const message =
        'FFmpeg exited with code: ' + code + ' and signal: ' + signal;

      if (code === null || code === 255) {
        if (this.process.killed) {
          log.debug(message + ' (Expected)', cameraName, debug);
        } else {
          log.error(message + ' (Unexpected)', cameraName);
        }
      } else {
        log.error(message + ' (Error)', cameraName);
        delegate.stopStream(sessionId);
        if (!started && callback) {
          callback(new Error(message));
        } else {
          delegate.controller!.forceStopStreamingSession(sessionId);
        }
      }
    });
  }
Example #4
Source File: ffmpeg.ts    From homebridge-eufy-security with Apache License 2.0 5 votes vote down vote up
constructor(cameraName: string, sessionId: string, videoProcessor: string, ffmpegArgs: string, log: Logger,
    debug = false, delegate: StreamingDelegate, callback?: StreamRequestCallback) {
    log.debug('Stream command: ' + videoProcessor + ' ' + ffmpegArgs, cameraName, debug);

    let started = false;
    const startTime = Date.now();
    this.process = spawn(videoProcessor, ffmpegArgs.split(/\s+/), { env: process.env });
    this.stdin = this.process.stdin;

    this.process.stdout.on('data', (data) => {
      const progress = this.parseProgress(data);
      if (progress) {
        if (!started && progress.frame > 0) {
          started = true;
          const runtime = (Date.now() - startTime) / 1000;
          const message = 'Getting the first frames took ' + runtime + ' seconds.';
          if (runtime < 5) {
            log.debug(message, cameraName, debug);
          } else if (runtime < 22) {
            log.warn(message, cameraName);
          } else {
            log.error(message, cameraName);
          }
        }
      }
    });
    const stderr = readline.createInterface({
      input: this.process.stderr,
      terminal: false
    });
    stderr.on('line', (line: string) => {
      if (callback) {
        callback();
        callback = undefined;
      }
      if (debug && line.match(/\[(panic|fatal|error)\]/)) { // For now only write anything out when debug is set
        log.error(line, cameraName);
      } else if (debug) {
        log.debug(line, cameraName, true);
      }
    });
    this.process.on('error', (error: Error) => {
      log.error('FFmpeg process creation failed: ' + error.message, cameraName);
      if (callback) {
        callback(new Error('FFmpeg process creation failed'));
      }
      delegate.stopStream(sessionId);
    });
    this.process.on('exit', (code: number, signal: NodeJS.Signals) => {
      if (this.killTimeout) {
        clearTimeout(this.killTimeout);
      }

      const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;

      if (this.killTimeout && code === 0) {
        log.debug(message + ' (Expected)', cameraName, debug);
      } else if (code == null || code === 255) {
        if (this.process.killed) {
          log.debug(message + ' (Forced)', cameraName, debug);
        } else {
          log.error(message + ' (Unexpected)', cameraName);
        }
      } else {
        log.error(message + ' (Error)', cameraName);
        delegate.stopStream(sessionId);
        if (!started && callback) {
          callback(new Error(message));
        } else {
          delegate.controller.forceStopStreamingSession(sessionId);
        }
      }
    });
  }
Example #5
Source File: streaming-delegate.ts    From homebridge-nest-cam with GNU General Public License v3.0 4 votes vote down vote up
handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void {
    const sessionId = request.sessionID;

    switch (request.type) {
      case StreamRequestTypes.START:
        const sessionInfo = this.pendingSessions[sessionId];
        const video: VideoInfo = request.video;
        const audio: AudioInfo = request.audio;

        const address = sessionInfo.address;
        const audioSRTP = sessionInfo.audioSRTP.toString('base64');
        const twoWayAudioPort = sessionInfo.twoWayAudioPort;

        if (!this.ffmpegInstalled) {
          this.log.error('FFMPEG is not installed. Please install it and restart homebridge.');
          callback(new Error('FFmpeg not installed'));
          break;
        }

        const videoffmpegCommand = this.getVideoCommand(video, sessionId);
        const ffmpegVideo = new FfmpegProcess(
          'VIDEO',
          videoffmpegCommand,
          this.log,
          this,
          sessionId,
          false,
          this.customFfmpeg,
          (error) => {
            callback(error);
          },
        );

        let ffmpegAudio: FfmpegProcess | undefined;
        let ffmpegReturnAudio: FfmpegProcess | undefined;
        if (this.camera.info.properties['audio.enabled'] && this.camera.info.properties['streaming.enabled']) {
          if (this.ffmpegSupportsLibfdk_acc) {
            const audioffmpegCommand = this.getAudioCommand(audio, sessionId);
            if (audioffmpegCommand) {
              ffmpegAudio = new FfmpegProcess(
                'AUDIO',
                audioffmpegCommand,
                this.log,
                this,
                sessionId,
                false,
                this.customFfmpeg,
              );
            }

            if (this.ffmpegSupportsLibspeex) {
              const returnAudioffmpegCommand = this.getReturnAudioCommand(audio, sessionId);
              if (returnAudioffmpegCommand) {
                ffmpegReturnAudio = new FfmpegProcess(
                  'RETURN AUDIO',
                  returnAudioffmpegCommand,
                  this.log,
                  this,
                  sessionId,
                  false,
                  this.customFfmpeg,
                );
                const sdpReturnAudio = [
                  'v=0',
                  'o=- 0 0 IN IP4 127.0.0.1',
                  's=Talk',
                  `c=IN IP4 ${address}`,
                  't=0 0',
                  'a=tool:libavformat 58.38.100',
                  `m=audio ${twoWayAudioPort} RTP/AVP 110`,
                  'b=AS:24',
                  'a=rtpmap:110 MPEG4-GENERIC/16000/1',
                  'a=fmtp:110 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=F8F0212C00BC00',
                  `a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${audioSRTP}`,
                ].join('\n');
                ffmpegReturnAudio.getStdin()?.write(sdpReturnAudio);
                ffmpegReturnAudio.getStdin()?.end();
              }
            } else {
              this.log.error(
                "This version of FFMPEG does not support the audio codec 'libspeex'. You may need to recompile FFMPEG using '--enable-libspeex' and restart homebridge.",
              );
            }
          } else {
            this.log.error(
              "This version of FFMPEG does not support the audio codec 'libfdk_aac'. You may need to recompile FFMPEG using '--enable-libfdk_aac' and restart homebridge.",
            );
          }
        }

        if (this.camera.info.properties['streaming.enabled'] && this.pendingSessions[sessionId]) {
          const streamer = new NexusStreamer(
            this.camera.info,
            this.config.access_token,
            this.config.options?.streamQuality || 3,
            ffmpegVideo,
            ffmpegAudio,
            ffmpegReturnAudio,
            this.log,
            this.config.nest_token !== undefined,
          );
          streamer.startPlayback();
          this.ongoingStreams[sessionId] = streamer;
        }

        // Used to switch offline/online stream on-the-fly
        // this.camera.on(NestCamEvents.CAMERA_STATE_CHANGED, (state) => {
        //   ffmpegVideo.stop();
        //   ffmpegAudio?.stop();
        //   ffmpegReturnAudio?.stop();
        //   const newVideoffmpegCommand = this.getVideoCommand(video, sessionId);
        //   const newFfmpegVideo = new FfmpegProcess(
        //     'VIDEO',
        //     newVideoffmpegCommand,
        //     this.log,
        //     undefined,
        //     this,
        //     sessionId,
        //     true,
        //     this.customFfmpeg,
        //   );
        //   this.ongoingSessions[sessionId] = [newFfmpegVideo, ffmpegAudio, ffmpegReturnAudio];

        //   if (state) {
        //     const streamer = new NexusStreamer(
        //       this.camera.info,
        //       this.config.access_token,
        //       this.log,
        //       this.config,
        //       newFfmpegVideo,
        //       ffmpegAudio,
        //       ffmpegReturnAudio,
        //     );
        //     streamer.startPlayback();
        //     this.ongoingStreams[sessionId] = streamer;
        //   } else {
        //     const streamer = this.ongoingStreams[sessionId];
        //     streamer.stopPlayback();
        //   }
        // });

        this.ongoingSessions[sessionId] = [ffmpegVideo, ffmpegAudio, ffmpegReturnAudio];
        break;
      case StreamRequestTypes.RECONFIGURE:
        // not implemented
        this.log.debug('(Not implemented) Received request to reconfigure to: ' + JSON.stringify(request.video));
        callback();
        break;
      case StreamRequestTypes.STOP:
        this.stopStream(sessionId);
        callback();
        break;
    }
  }
Example #6
Source File: new-streaming-delegate.ts    From homebridge-plugin-eufy-security with Apache License 2.0 4 votes vote down vote up
private startStream(
    request: StartStreamRequest,
    callback: StreamRequestCallback,
  ): void {
    this.platform.httpService
      .startStream({
        device_sn: this.device.device_sn,
        station_sn: this.device.station_sn,
        proto: 2,
      })
      .then(async ({ url }) => {
        await new Promise((r) => setTimeout(r, 500));
        return url;
      })
      .then((url) => {
        const sessionInfo = this.pendingSessions[request.sessionID];
        const vcodec = 'libx264';
        const mtu = 1316; // request.video.mtu is not used
        const encoderOptions = '-preset ultrafast';

        const resolution = this.determineResolution(request.video, false);

        const fps = request.video.fps;
        const videoBitrate = request.video.max_bit_rate;
        //   let fps = (this.videoConfig.forceMax && this.videoConfig.maxFPS) ||
        //     (request.video.fps > this.videoConfig.maxFPS) ?
        //     this.videoConfig.maxFPS : request.video.fps;

        //   let videoBitrate = (this.videoConfig.forceMax && this.videoConfig.maxBitrate) ||
        //     (request.video.max_bit_rate > this.videoConfig.maxBitrate) ?
        //     this.videoConfig.maxBitrate : request.video.max_bit_rate;

        //   if (vcodec === 'copy') {
        //     resolution.width = 0;
        //     resolution.height = 0;
        //     resolution.videoFilter = '';
        //     fps = 0;
        //     videoBitrate = 0;
        //   }

        this.log.debug(
          'Video stream requested: ' +
            request.video.width +
            ' x ' +
            request.video.height +
            ', ' +
            request.video.fps +
            ' fps, ' +
            request.video.max_bit_rate +
            ' kbps',
          this.cameraName,
          this.debug,
        );
        this.log.info(
          'Starting video stream: ' +
            (resolution.width > 0 ? resolution.width : 'native') +
            ' x ' +
            (resolution.height > 0 ? resolution.height : 'native') +
            ', ' +
            (fps > 0 ? fps : 'native') +
            ' fps, ' +
            (videoBitrate > 0 ? videoBitrate : '???') +
            ' kbps',
          this.cameraName,
        );

        //   let ffmpegArgs = this.videoConfig.source;
        let ffmpegArgs = `-i ${url}`;

        ffmpegArgs += // Video
          // (this.videoConfig.mapvideo ? ' -map ' + this.videoConfig.mapvideo : ' -an -sn -dn') +
          ' -an -sn -dn' +
          ' -codec:v ' +
          vcodec +
          ' -pix_fmt yuv420p' +
          ' -color_range mpeg' +
          // (fps > 0 ? ' -r ' + fps : '') +
          ' -f rawvideo' +
          (encoderOptions ? ' ' + encoderOptions : '') +
          (resolution.videoFilter.length > 0
            ? ' -filter:v ' + resolution.videoFilter
            : '') +
          (videoBitrate > 0 ? ' -b:v ' + videoBitrate + 'k' : '') +
          ' -payload_type ' +
          request.video.pt;

        ffmpegArgs += // Video Stream
          ' -ssrc ' +
          sessionInfo.videoSSRC +
          ' -f rtp' +
          ' -srtp_out_suite AES_CM_128_HMAC_SHA1_80' +
          ' -srtp_out_params ' +
          sessionInfo.videoSRTP.toString('base64') +
          ' srtp://' +
          sessionInfo.address +
          ':' +
          sessionInfo.videoPort +
          '?rtcpport=' +
          sessionInfo.videoPort +
          '&pkt_size=' +
          mtu;

        if (this.audio) {
          ffmpegArgs += // Audio
            //   (this.videoConfig.mapaudio ? ' -map ' + this.videoConfig.mapaudio : ' -vn -sn -dn') +
            ' -vn -sn -dn';
          ' -codec:a libfdk_aac' +
            ' -profile:a aac_eld' +
            ' -flags +global_header' +
            ' -f null' +
            ' -ar ' +
            request.audio.sample_rate +
            'k' +
            ' -b:a ' +
            request.audio.max_bit_rate +
            'k' +
            ' -ac ' +
            request.audio.channel +
            ' -payload_type ' +
            request.audio.pt;

          ffmpegArgs += // Audio Stream
            ' -ssrc ' +
            sessionInfo.audioSSRC +
            ' -f rtp' +
            ' -srtp_out_suite AES_CM_128_HMAC_SHA1_80' +
            ' -srtp_out_params ' +
            sessionInfo.audioSRTP.toString('base64') +
            ' srtp://' +
            sessionInfo.address +
            ':' +
            sessionInfo.audioPort +
            '?rtcpport=' +
            sessionInfo.audioPort +
            '&pkt_size=188';
        }

        if (this.debug) {
          ffmpegArgs += ' -loglevel level+verbose';
        }

        const activeSession: ActiveSession = {};

        activeSession.socket = createSocket(sessionInfo.ipv6 ? 'udp6' : 'udp4');
        activeSession.socket.on('error', (err: Error) => {
          this.log.error('Socket error: ' + err.name, this.cameraName);
          this.stopStream(request.sessionID);
        });
        activeSession.socket.on('message', () => {
          if (activeSession.timeout) {
            clearTimeout(activeSession.timeout);
          }
          activeSession.timeout = setTimeout(() => {
            this.log.info(
              'Device appears to be inactive. Stopping stream.',
              this.cameraName,
            );
            this.controller.forceStopStreamingSession(request.sessionID);
            this.stopStream(request.sessionID);
          }, request.video.rtcp_interval * 2 * 1000);
        });
        activeSession.socket.bind(
          sessionInfo.videoReturnPort,
          sessionInfo.localAddress,
        );

        activeSession.mainProcess = new FfmpegProcess(
          this.cameraName,
          request.sessionID,
          this.videoProcessor,
          ffmpegArgs,
          this.log,
          this.debug,
          this,
          callback,
        );

        //   if (this.videoConfig.returnAudioTarget) {
        //     let ffmpegReturnArgs =
        //       '-hide_banner' +
        //       ' -protocol_whitelist pipe,udp,rtp,file,crypto' +
        //       ' -f sdp' +
        //       ' -c:a libfdk_aac' +
        //       ' -i pipe:' +
        //       ' ' + this.videoConfig.returnAudioTarget;

        //     if (this.videoConfig.debugReturn) {
        //       ffmpegReturnArgs += ' -loglevel level+verbose';
        //     }

        //     const ipVer = sessionInfo.ipv6 ? 'IP6' : 'IP4';

        //     const sdpReturnAudio =
        //       'v=0\r\n' +
        //       'o=- 0 0 IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
        //       's=Talk\r\n' +
        //       'c=IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
        //       't=0 0\r\n' +
        //       'm=audio ' + sessionInfo.audioReturnPort + ' RTP/AVP 110\r\n' +
        //       'b=AS:24\r\n' +
        //       'a=rtpmap:110 MPEG4-GENERIC/16000/1\r\n' +
        //       'a=rtcp-mux\r\n' + // FFmpeg ignores this, but might as well
        //       'a=fmtp:110 ' +
        //         'profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; ' +
        //         'config=F8F0212C00BC00\r\n' +
        //       'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + sessionInfo.audioSRTP.toString('base64') + '\r\n';
        //     activeSession.returnProcess = new FfmpegProcess(this.cameraName + '] [Two-way', request.sessionID,
        //       this.videoProcessor, ffmpegReturnArgs, this.log, this.videoConfig.debugReturn, this);
        //     activeSession.returnProcess.getStdin()?.end(sdpReturnAudio);
        //   }

        this.ongoingSessions[request.sessionID] = activeSession;
        delete this.pendingSessions[request.sessionID];
      });
  }
Example #7
Source File: streamingDelegate.ts    From homebridge-eufy-security with Apache License 2.0 4 votes vote down vote up
private async startStream(request: StartStreamRequest, callback: StreamRequestCallback): Promise<void> {

    this.videoConfig.source = '-i ' + await this.device.startStream();

    const sessionInfo = this.pendingSessions.get(request.sessionID);
    if (sessionInfo) {
      const vcodec = this.videoConfig.vcodec || 'libx264';
      const mtu = this.videoConfig.packetSize || 1316; // request.video.mtu is not used
      let encoderOptions = this.videoConfig.encoderOptions;
      if (!encoderOptions && vcodec === 'libx264') {
        encoderOptions = '-preset ultrafast -tune zerolatency';
      }

      const resolution = this.determineResolution(request.video, false);

      let fps = (this.videoConfig.maxFPS !== undefined &&
        (this.videoConfig.forceMax || request.video.fps > this.videoConfig.maxFPS)) ?
        this.videoConfig.maxFPS : request.video.fps;
      let videoBitrate = (this.videoConfig.maxBitrate !== undefined &&
        (this.videoConfig.forceMax || request.video.max_bit_rate > this.videoConfig.maxBitrate)) ?
        this.videoConfig.maxBitrate : request.video.max_bit_rate;

      if (vcodec === 'copy') {
        resolution.width = 0;
        resolution.height = 0;
        resolution.videoFilter = undefined;
        fps = 0;
        videoBitrate = 0;
      }

      this.log.debug('Video stream requested: ' + request.video.width + ' x ' + request.video.height + ', ' +
        request.video.fps + ' fps, ' + request.video.max_bit_rate + ' kbps', this.cameraName, this.videoConfig.debug);
      this.log.info('Starting video stream: ' + (resolution.width > 0 ? resolution.width : 'native') + ' x ' +
        (resolution.height > 0 ? resolution.height : 'native') + ', ' + (fps > 0 ? fps : 'native') +
        ' fps, ' + (videoBitrate > 0 ? videoBitrate : '???') + ' kbps' +
        (this.videoConfig.audio ? (' (' + request.audio.codec + ')') : ''), this.cameraName);

      let ffmpegArgs = this.videoConfig.source!;

      ffmpegArgs += // Video
        (this.videoConfig.mapvideo ? ' -map ' + this.videoConfig.mapvideo : ' -an -sn -dn') +
        ' -codec:v ' + vcodec +
        ' -pix_fmt yuv420p' +
        ' -color_range mpeg' +
        (fps > 0 ? ' -r ' + fps : '') +
        ' -f rawvideo' +
        (encoderOptions ? ' ' + encoderOptions : '') +
        (resolution.videoFilter ? ' -filter:v ' + resolution.videoFilter : '') +
        (videoBitrate > 0 ? ' -b:v ' + videoBitrate + 'k' : '') +
        ' -payload_type ' + request.video.pt;

      ffmpegArgs += // Video Stream
        ' -ssrc ' + sessionInfo.videoSSRC +
        ' -f rtp' +
        ' -srtp_out_suite AES_CM_128_HMAC_SHA1_80' +
        ' -srtp_out_params ' + sessionInfo.videoSRTP.toString('base64') +
        ' srtp://' + sessionInfo.address + ':' + sessionInfo.videoPort +
        '?rtcpport=' + sessionInfo.videoPort + '&pkt_size=' + mtu;

      if (this.videoConfig.audio) {
        if (request.audio.codec === AudioStreamingCodecType.OPUS || request.audio.codec === AudioStreamingCodecType.AAC_ELD) {
          ffmpegArgs += // Audio
            (this.videoConfig.mapaudio ? ' -map ' + this.videoConfig.mapaudio : ' -vn -sn -dn') +
            (request.audio.codec === AudioStreamingCodecType.OPUS ?
              ' -codec:a libopus' +
              ' -application lowdelay' :
              ' -codec:a aac' +
              ' -profile:a aac_eld') +
            ' -flags +global_header' +
            ' -f null' +
            ' -ar ' + request.audio.sample_rate + 'k' +
            ' -b:a ' + request.audio.max_bit_rate + 'k' +
            ' -ac ' + request.audio.channel +
            ' -payload_type ' + request.audio.pt;

          ffmpegArgs += // Audio Stream
            ' -ssrc ' + sessionInfo.audioSSRC +
            ' -f rtp' +
            ' -srtp_out_suite AES_CM_128_HMAC_SHA1_80' +
            ' -srtp_out_params ' + sessionInfo.audioSRTP.toString('base64') +
            ' srtp://' + sessionInfo.address + ':' + sessionInfo.audioPort +
            '?rtcpport=' + sessionInfo.audioPort + '&pkt_size=188';
        } else {
          this.log.error('Unsupported audio codec requested: ' + request.audio.codec, this.cameraName);
        }
      }

      ffmpegArgs += ' -loglevel level' + (this.videoConfig.debug ? '+verbose' : '') +
        ' -progress pipe:1';

      const activeSession: ActiveSession = {};

      activeSession.socket = createSocket(sessionInfo.ipv6 ? 'udp6' : 'udp4');
      activeSession.socket.on('error', (err: Error) => {
        this.log.error('Socket error: ' + err.message, this.cameraName);
        this.stopStream(request.sessionID);
      });
      activeSession.socket.on('message', () => {
        if (activeSession.timeout) {
          clearTimeout(activeSession.timeout);
        }
        activeSession.timeout = setTimeout(() => {
          this.log.info('Device appears to be inactive. Stopping stream.', this.cameraName);
          this.controller.forceStopStreamingSession(request.sessionID);
          this.stopStream(request.sessionID);
        }, request.video.rtcp_interval * 5 * 1000);
      });
      activeSession.socket.bind(sessionInfo.videoReturnPort);

      activeSession.mainProcess = new FfmpegProcess(this.cameraName, request.sessionID, this.videoProcessor,
        ffmpegArgs, this.log, this.videoConfig.debug, this, callback);

      if (this.videoConfig.returnAudioTarget) {
        const ffmpegReturnArgs =
          '-hide_banner' +
          ' -protocol_whitelist pipe,udp,rtp,file,crypto' +
          ' -f sdp' +
          ' -c:a aac' +
          ' -i pipe:' +
          ' ' + this.videoConfig.returnAudioTarget +
          ' -loglevel level' + (this.videoConfig.debugReturn ? '+verbose' : '');

        const ipVer = sessionInfo.ipv6 ? 'IP6' : 'IP4';

        const sdpReturnAudio =
          'v=0\r\n' +
          'o=- 0 0 IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
          's=Talk\r\n' +
          'c=IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
          't=0 0\r\n' +
          'm=audio ' + sessionInfo.audioReturnPort + ' RTP/AVP 110\r\n' +
          'b=AS:24\r\n' +
          'a=rtpmap:110 MPEG4-GENERIC/16000/1\r\n' +
          'a=rtcp-mux\r\n' + // FFmpeg ignores this, but might as well
          'a=fmtp:110 ' +
          'profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; ' +
          'config=F8F0212C00BC00\r\n' +
          'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + sessionInfo.audioSRTP.toString('base64') + '\r\n';
        activeSession.returnProcess = new FfmpegProcess(this.cameraName + '] [Two-way', request.sessionID,
          this.videoProcessor, ffmpegReturnArgs, this.log, this.videoConfig.debugReturn, this);
        activeSession.returnProcess.stdin.end(sdpReturnAudio);
      }

      this.ongoingSessions.set(request.sessionID, activeSession);
      this.pendingSessions.delete(request.sessionID);
    } else {
      this.log.error('Error finding session information.', this.cameraName);
      callback(new Error('Error finding session information'));
    }
  }