answer method

Future<void> answer(
  1. String callId, {
  2. VideoEncoder? videoEncoder,
  3. VideoDecoder? videoDecoder,
})

Implementation

Future<void> answer(
  String callId, {
  VideoEncoder? videoEncoder,
  VideoDecoder? videoDecoder,
}) async {
  final ctx = _calls[callId];
  if (ctx == null || ctx.call.state != CallState.incomingRinging) return;
  final acc = _account;
  if (acc == null) return;
  if (isWeb) {
    _log(
      'answer: rejected — RTP media (UDP) is not supported on web. '
      'Answering audio/video calls requires a native build.',
    );
    return;
  }

  // Bind RTP socket and parse the peer's offer so we know where to send.
  final media = MediaSession(
    sink: _audioSinkFactory?.call(),
    packetTap: _rtpPacketTap,
  );
  final rtpPort = await media.bindLocalPort();
  ctx.media = media;
  final offer = parseSdp(ctx.lastInvite.body);
  final remoteAudio = offer.audio;
  final remoteVideo = offer.video;
  if (remoteAudio != null) {
    ctx.negotiatedAudioCodec = remoteAudio.codec;
    ctx.negotiatedDtmfPt = remoteAudio.telephoneEventPt;
    ctx.negotiatedDtmfRange = remoteAudio.telephoneEventRange;
  }

  VideoSession? video;
  int? videoPort;
  if (remoteVideo != null) {
    video = VideoSession(encoder: videoEncoder, decoder: videoDecoder);
    videoPort = await video.bindLocalPort();
    ctx.video = video;
  }

  final answerSdp = video == null
      ? (remoteAudio == null
            ? buildG711Offer(
                username: acc.username,
                localHost: _mediaLocalHost(),
                localPort: rtpPort,
                rtcpPort: media.localRtcpPort,
                sessionId: ctx.sdpSessionId,
                sessionVersion: ctx.bumpSdpVersion(),
              )
            : buildG711Answer(
                username: acc.username,
                localHost: _mediaLocalHost(),
                localPort: rtpPort,
                remoteOffer: remoteAudio,
                rtcpPort: media.localRtcpPort,
                sessionId: ctx.sdpSessionId,
                sessionVersion: ctx.bumpSdpVersion(),
              ))
      : buildAvOffer(
          username: acc.username,
          localHost: _mediaLocalHost(),
          audioPort: rtpPort,
          videoPort: videoPort,
          audioRtcpPort: media.localRtcpPort,
          audioPreferred: remoteAudio?.codec ?? G711Variant.pcmu,
          audioSecond: null,
          telephoneEventPt: remoteAudio?.telephoneEventPt,
          telephoneEventRange: remoteAudio?.telephoneEventRange ?? '0-15',
          videoPayloadType: remoteVideo!.payloadType,
          videoCodec: remoteVideo.codec,
          sessionId: ctx.sdpSessionId,
          sessionVersion: ctx.bumpSdpVersion(),
        );

  // RFC 4028: if the peer asked for timers, accept and choose refresher.
  final extra = <String, String>{};
  if (ctx.proposedSE != null) {
    // If peer didn't pin a refresher, take the role ourselves (uas).
    ctx.refresher ??= 'uas';
    extra['Require'] = 'timer';
    extra['Session-Expires'] = '${ctx.proposedSE};refresher=${ctx.refresher}';
  }
  _respondToInvite(
    ctx,
    code: 200,
    reason: 'OK',
    sdp: answerSdp,
    extra: extra,
  );
  ctx.call.state = CallState.active;
  _emitCall(ctx.call);
  _armSessionTimers(ctx);
  if (remoteAudio != null) {
    try {
      await media.start(remoteAudio.toEndpoint());
    } catch (e) {
      _log('media: failed to start: $e');
    }
  }
  if (video != null && remoteVideo != null) {
    try {
      await video.start(VideoEndpoint.fromSdp(remoteVideo));
    } catch (e) {
      _log('video: failed to start: $e');
    }
  }
}