answer method
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');
}
}
}