From 21410c238df235490b3958fec6ac51377dff3f3b Mon Sep 17 00:00:00 2001 From: Tristan Matthews <tristan.matthews@savoirfairelinux.com> Date: Fri, 14 Sep 2012 16:41:20 -0400 Subject: [PATCH] * #15528: sip/audiortp: support asymmetric audio codecs A call can send one codec and receive another. --- .../audiortp/audio_rtp_record_handler.cpp | 81 ++++++++++++---- .../audio/audiortp/audio_rtp_record_handler.h | 4 + .../src/audio/audiortp/audio_rtp_session.cpp | 3 +- daemon/src/iax/iaxvoiplink.cpp | 2 +- daemon/src/iax/iaxvoiplink.h | 2 +- daemon/src/managerimpl.cpp | 2 +- daemon/src/sip/sdp.cpp | 97 ++++++++++++------- daemon/src/sip/sdp.h | 6 +- daemon/src/sip/sipcall.cpp | 2 +- daemon/src/sip/sipvoiplink.cpp | 44 +++++---- daemon/src/sip/sipvoiplink.h | 2 +- daemon/src/voiplink.cpp | 1 + daemon/src/voiplink.h | 2 +- 13 files changed, 168 insertions(+), 80 deletions(-) diff --git a/daemon/src/audio/audiortp/audio_rtp_record_handler.cpp b/daemon/src/audio/audiortp/audio_rtp_record_handler.cpp index 4ab82a27c2..2735ccfb32 100644 --- a/daemon/src/audio/audiortp/audio_rtp_record_handler.cpp +++ b/daemon/src/audio/audiortp/audio_rtp_record_handler.cpp @@ -109,6 +109,7 @@ AudioRtpRecord::AudioRtpRecord() : #endif , dtmfPayloadType_(101) // same as Asterisk , dead_(false) + , currentCodecIndex_(0) {} // Call from processData* @@ -121,6 +122,16 @@ bool AudioRtpRecord::isDead() #endif } +sfl::AudioCodec * +AudioRtpRecord::getCurrentCodec() const +{ + if (audioCodecs_.empty() or currentCodecIndex_ >= audioCodecs_.size()) { + ERROR("No codec found"); + return 0; + } + return audioCodecs_[currentCodecIndex_]; +} + void AudioRtpRecord::deleteCodecs() { @@ -129,6 +140,23 @@ AudioRtpRecord::deleteCodecs() audioCodecs_.clear(); } +bool AudioRtpRecord::tryToSwitchPayloadTypes(int newPt) +{ + for (std::vector<AudioCodec *>::iterator i = audioCodecs_.begin(); i != audioCodecs_.end(); ++i) + if (*i and (*i)->getPayloadType() == newPt) { + codecPayloadType_ = (*i)->getPayloadType(); + codecSampleRate_ = (*i)->getClockRate(); + codecFrameSize_ = (*i)->getFrameSize(); + hasDynamicPayloadType_ = (*i)->hasDynamicPayload(); + currentCodecIndex_ = std::distance(audioCodecs_.begin(), i); + DEBUG("Switched payload type to %d", newPt); + return true; + } + + ERROR("Could not switch payload types"); + return false; +} + AudioRtpRecord::~AudioRtpRecord() { dead_ = true; @@ -174,6 +202,8 @@ void AudioRtpRecordHandler::setRtpMedia(const std::vector<AudioCodec*> &audioCod audioRtpRecord_.deleteCodecs(); // Set varios codec info to reduce indirection audioRtpRecord_.audioCodecs_ = audioCodecs; + + audioRtpRecord_.currentCodecIndex_ = 0; audioRtpRecord_.codecPayloadType_ = audioCodecs[0]->getPayloadType(); audioRtpRecord_.codecSampleRate_ = audioCodecs[0]->getClockRate(); audioRtpRecord_.codecFrameSize_ = audioCodecs[0]->getFrameSize(); @@ -273,13 +303,9 @@ int AudioRtpRecordHandler::processDataEncode() { ost::MutexLock lock(audioRtpRecord_.audioCodecMutex_); - if (audioRtpRecord_.audioCodecs_.empty()) { - ERROR("Audio codecs already destroyed"); - return 0; - } - RETURN_IF_NULL(audioRtpRecord_.audioCodecs_[0], 0, "Audio codec already destroyed"); + RETURN_IF_NULL(audioRtpRecord_.getCurrentCodec(), 0, "Audio codec already destroyed"); unsigned char *micDataEncoded = audioRtpRecord_.encodedData_.data(); - return audioRtpRecord_.audioCodecs_[0]->encode(micDataEncoded, out, getCodecFrameSize()); + return audioRtpRecord_.getCurrentCodec()->encode(micDataEncoded, out, getCodecFrameSize()); } } #undef RETURN_IF_NULL @@ -291,27 +317,24 @@ void AudioRtpRecordHandler::processDataDecode(unsigned char *spkrData, size_t si if (audioRtpRecord_.isDead()) return; if (audioRtpRecord_.codecPayloadType_ != payloadType) { - if (!warningInterval_) { - warningInterval_ = 250; - WARN("Invalid payload type %d, expected %d", payloadType, audioRtpRecord_.codecPayloadType_); - WARN("We have %u codecs total", audioRtpRecord_.audioCodecs_.size()); + const bool switched = audioRtpRecord_.tryToSwitchPayloadTypes(payloadType); + if (not switched) { + if (!warningInterval_) { + warningInterval_ = 250; + WARN("Invalid payload type %d, expected %d", payloadType, audioRtpRecord_.codecPayloadType_); + } + warningInterval_--; + return; } - warningInterval_--; - return; } int inSamples = 0; size = std::min(size, audioRtpRecord_.decData_.size()); SFLDataFormat *spkrDataDecoded = audioRtpRecord_.decData_.data(); { - ost::MutexLock lock(audioRtpRecord_.audioCodecMutex_); - if (audioRtpRecord_.audioCodecs_.empty()) { - ERROR("Audio codecs already destroyed"); - return; - } - RETURN_IF_NULL(audioRtpRecord_.audioCodecs_[0], "Audio codecs already destroyed"); + RETURN_IF_NULL(audioRtpRecord_.getCurrentCodec(), "Audio codecs already destroyed"); // Return the size of data in samples - inSamples = audioRtpRecord_.audioCodecs_[0]->decode(spkrDataDecoded, spkrData, size); + inSamples = audioRtpRecord_.getCurrentCodec()->decode(spkrDataDecoded, spkrData, size); } #if HAVE_SPEEXDSP @@ -354,6 +377,7 @@ void AudioRtpRecord::fadeInDecodedData(size_t size) if (fadeFactor_ >= 1.0 or size > decData_.size()) return; + // FIXME: this takes a lot more cycles than a plain old loop std::transform(decData_.begin(), decData_.begin() + size, decData_.begin(), std::bind1st(std::multiplies<double>(), fadeFactor_)); @@ -361,4 +385,23 @@ void AudioRtpRecord::fadeInDecodedData(size_t size) const double FADEIN_STEP_SIZE = 4.0; fadeFactor_ *= FADEIN_STEP_SIZE; } + +bool +AudioRtpRecordHandler::codecsDiffer(const std::vector<AudioCodec*> &codecs) const +{ + const std::vector<AudioCodec*> ¤t = audioRtpRecord_.audioCodecs_; + if (codecs.size() != current.size()) + return true; + for (std::vector<AudioCodec*>::const_iterator i = codecs.begin(); i != codecs.end(); ++i) { + if (*i) { + bool matched = false; + for (std::vector<AudioCodec*>::const_iterator j = current.begin(); !matched and j != current.end(); ++j) + matched = (*i)->getPayloadType() == (*j)->getPayloadType(); + if (not matched) + return true; + } + } + return false; +} + } diff --git a/daemon/src/audio/audiortp/audio_rtp_record_handler.h b/daemon/src/audio/audiortp/audio_rtp_record_handler.h index 2f35664bb2..98695bf922 100644 --- a/daemon/src/audio/audiortp/audio_rtp_record_handler.h +++ b/daemon/src/audio/audiortp/audio_rtp_record_handler.h @@ -73,6 +73,8 @@ class AudioRtpRecord { AudioRtpRecord(); ~AudioRtpRecord(); void deleteCodecs(); + bool tryToSwitchPayloadTypes(int newPt); + sfl::AudioCodec* getCurrentCodec() const; std::string callId_; int codecSampleRate_; std::list<DTMFEvent> dtmfQueue_; @@ -112,6 +114,7 @@ class AudioRtpRecord { #else ucommon::atomic::counter dead_; #endif + size_t currentCodecIndex_; }; @@ -180,6 +183,7 @@ class AudioRtpRecordHandler { void putDtmfEvent(char digit); protected: + bool codecsDiffer(const std::vector<AudioCodec*> &codecs) const; AudioRtpRecord audioRtpRecord_; private: diff --git a/daemon/src/audio/audiortp/audio_rtp_session.cpp b/daemon/src/audio/audiortp/audio_rtp_session.cpp index a33811f491..e7eb4fbbae 100644 --- a/daemon/src/audio/audiortp/audio_rtp_session.cpp +++ b/daemon/src/audio/audiortp/audio_rtp_session.cpp @@ -65,7 +65,8 @@ void AudioRtpSession::updateSessionMedia(const std::vector<AudioCodec*> &audioCo { int lastSamplingRate = audioRtpRecord_.codecSampleRate_; - setSessionMedia(audioCodecs); + if (codecsDiffer(audioCodecs)) + setSessionMedia(audioCodecs); Manager::instance().audioSamplingRateChanged(audioRtpRecord_.codecSampleRate_); diff --git a/daemon/src/iax/iaxvoiplink.cpp b/daemon/src/iax/iaxvoiplink.cpp index ac846e0980..aaf1333cf2 100644 --- a/daemon/src/iax/iaxvoiplink.cpp +++ b/daemon/src/iax/iaxvoiplink.cpp @@ -475,7 +475,7 @@ IAXVoIPLink::getCurrentVideoCodecName(Call * /*call*/) const } std::string -IAXVoIPLink::getCurrentAudioCodecName(Call *c) const +IAXVoIPLink::getCurrentAudioCodecNames(Call *c) const { IAXCall *call = static_cast<IAXCall*>(c); sfl::Codec *audioCodec = Manager::instance().audioCodecFactory.getCodec(call->getAudioCodec()); diff --git a/daemon/src/iax/iaxvoiplink.h b/daemon/src/iax/iaxvoiplink.h index 3c7bf7efb1..d7b53d9f9c 100644 --- a/daemon/src/iax/iaxvoiplink.h +++ b/daemon/src/iax/iaxvoiplink.h @@ -193,7 +193,7 @@ class IAXVoIPLink : public VoIPLink { * @param id The call identifier */ virtual std::string getCurrentVideoCodecName(Call *c) const; - virtual std::string getCurrentAudioCodecName(Call *c) const; + virtual std::string getCurrentAudioCodecNames(Call *c) const; private: NON_COPYABLE(IAXVoIPLink); diff --git a/daemon/src/managerimpl.cpp b/daemon/src/managerimpl.cpp index 8917a13c68..94a450581e 100644 --- a/daemon/src/managerimpl.cpp +++ b/daemon/src/managerimpl.cpp @@ -1874,7 +1874,7 @@ std::string ManagerImpl::getCurrentAudioCodecName(const std::string& id) Call::CallState state = call->getState(); if (state == Call::ACTIVE or state == Call::CONFERENCING) - codecName = link->getCurrentAudioCodecName(call); + codecName = link->getCurrentAudioCodecNames(call); } return codecName; diff --git a/daemon/src/sip/sdp.cpp b/daemon/src/sip/sdp.cpp index e868ea4dc6..737e93ce9a 100644 --- a/daemon/src/sip/sdp.cpp +++ b/daemon/src/sip/sdp.cpp @@ -71,15 +71,23 @@ Sdp::Sdp(pj_pool_t *pool) , telephoneEventPayload_(101) // same as asterisk {} +namespace { + bool hasPayload(const std::vector<sfl::AudioCodec*> &codecs, int pt) + { + for (std::vector<sfl::AudioCodec*>::const_iterator i = codecs.begin(); i != codecs.end(); ++i) + if (*i and (*i)->getPayloadType() == pt) + return true; + return false; + } +} + + void Sdp::setActiveLocalSdpSession(const pjmedia_sdp_session *sdp) { activeLocalSession_ = (pjmedia_sdp_session*) sdp; - if (activeLocalSession_->media_count < 1) - return; - - for (unsigned media = 0; media < activeLocalSession_->media_count; ++media) { - pjmedia_sdp_media *current = activeLocalSession_->media[media]; + for (unsigned i = 0; i < activeLocalSession_->media_count; ++i) { + pjmedia_sdp_media *current = activeLocalSession_->media[i]; for (unsigned fmt = 0; fmt < current->desc.fmt_count; ++fmt) { static const pj_str_t STR_RTPMAP = { (char*) "rtpmap", 6 }; @@ -93,24 +101,22 @@ void Sdp::setActiveLocalSdpSession(const pjmedia_sdp_session *sdp) pjmedia_sdp_rtpmap *rtpmap; pjmedia_sdp_attr_to_rtpmap(memPool_, attribute, &rtpmap); - string type(current->desc.media.ptr, current->desc.media.slen); - if (type == "audio") { + if (!pj_stricmp2(¤t->desc.media, "audio")) { const int pt = pj_strtoul(&rtpmap->pt); - sfl::Codec *codec = Manager::instance().audioCodecFactory.getCodec(pt); - if (codec) - sessionAudioMedia_.push_back(codec); - else { - DEBUG("Could not get codec for payload type %lu", pt); - break; + if (not hasPayload(sessionAudioMedia_, pt)) { + sfl::AudioCodec *codec = Manager::instance().audioCodecFactory.getCodec(pt); + if (codec) + sessionAudioMedia_.push_back(codec); + else + ERROR("Could not get codec for payload type %lu", pt); } - } else if (type == "video") + } else if (!pj_stricmp2(¤t->desc.media, "video")) sessionVideoMedia_.push_back(string(rtpmap->enc_name.ptr, rtpmap->enc_name.slen)); } } } - void Sdp::setActiveRemoteSdpSession(const pjmedia_sdp_session *sdp) { activeRemoteSession_ = (pjmedia_sdp_session*) sdp; @@ -121,22 +127,44 @@ void Sdp::setActiveRemoteSdpSession(const pjmedia_sdp_session *sdp) } for (unsigned i = 0; i < sdp->media_count; i++) { - if (pj_stricmp2(&sdp->media[i]->desc.media, "audio") == 0) { - pjmedia_sdp_media *r_media = sdp->media[i]; + pjmedia_sdp_media *r_media = sdp->media[i]; + if (!pj_stricmp2(&r_media->desc.media, "audio")) { static const pj_str_t STR_TELEPHONE_EVENT = { (char*) "telephone-event", 15}; - pjmedia_sdp_attr *attribute = pjmedia_sdp_attr_find(r_media->attr_count, r_media->attr, &STR_TELEPHONE_EVENT, NULL); + pjmedia_sdp_attr *telephoneEvent = pjmedia_sdp_attr_find(r_media->attr_count, r_media->attr, &STR_TELEPHONE_EVENT, NULL); - if (attribute != NULL) { + if (telephoneEvent != NULL) { pjmedia_sdp_rtpmap *rtpmap; - pjmedia_sdp_attr_to_rtpmap(memPool_, attribute, &rtpmap); + pjmedia_sdp_attr_to_rtpmap(memPool_, telephoneEvent, &rtpmap); telephoneEventPayload_ = pj_strtoul(&rtpmap->pt); } - return; + // add audio codecs from remote as needed + for (unsigned fmt = 0; fmt < r_media->desc.fmt_count; ++fmt) { + + static const pj_str_t STR_RTPMAP = { (char*) "rtpmap", 6 }; + pjmedia_sdp_attr *rtpMapAttr = pjmedia_sdp_media_find_attr(r_media, &STR_RTPMAP, NULL); + + if (!rtpMapAttr) { + ERROR("Could not find rtpmap attribute"); + continue; + } + + pjmedia_sdp_rtpmap *rtpmap; + pjmedia_sdp_attr_to_rtpmap(memPool_, rtpMapAttr, &rtpmap); + + const int pt = pj_strtoul(&rtpmap->pt); + if (not hasPayload(sessionAudioMedia_, pt)) { + sfl::AudioCodec *codec = Manager::instance().audioCodecFactory.getCodec(pt); + if (codec) { + DEBUG("Adding codec with new payload type %d", pt); + sessionAudioMedia_.push_back(codec); + } else + DEBUG("Could not get codec for payload type %lu", pt); + } + } } } - - ERROR("Could not found dtmf event from remote sdp"); + DEBUG("Using %u audio codecs total", sessionAudioMedia_.size()); } string Sdp::getSessionVideoCodec() const @@ -148,20 +176,23 @@ string Sdp::getSessionVideoCodec() const return sessionVideoMedia_[0]; } -string Sdp::getAudioCodecName() const +string Sdp::getAudioCodecNames() const { - sfl::AudioCodec *codec = getSessionAudioMedia(); - return codec ? codec->getMimeSubtype() : ""; + std::string result; + char sep = ' '; + for (std::vector<sfl::AudioCodec*>::const_iterator i = sessionAudioMedia_.begin(); + i != sessionAudioMedia_.end(); ++i) { + if (i == sessionAudioMedia_.end() - 1) + sep = '\0'; + if (*i) + result += (*i)->getMimeSubtype() + sep; + } + return result; } -sfl::AudioCodec* Sdp::getSessionAudioMedia() const +void Sdp::getSessionAudioMedia(std::vector<sfl::AudioCodec*> &codecs) const { - if (sessionAudioMedia_.empty()) { - ERROR("No codec description for this media"); - return 0; - } - - return static_cast<sfl::AudioCodec *>(sessionAudioMedia_[0]); + codecs = sessionAudioMedia_; } diff --git a/daemon/src/sip/sdp.h b/daemon/src/sip/sdp.h index 90eef19a4b..2fefc3f876 100644 --- a/daemon/src/sip/sdp.h +++ b/daemon/src/sip/sdp.h @@ -239,9 +239,9 @@ class Sdp { void setMediaTransportInfoFromRemoteSdp(); - std::string getAudioCodecName() const; + std::string getAudioCodecNames() const; std::string getSessionVideoCodec() const; - sfl::AudioCodec* getSessionAudioMedia() const; + void getSessionAudioMedia(std::vector<sfl::AudioCodec*> &) const; // Sets @param settings with appropriate values and returns true if // we are sending video, false otherwise bool getOutgoingVideoSettings(std::map<std::string, std::string> &settings) const; @@ -298,7 +298,7 @@ class Sdp { /** * The codecs that will be used by the session (after the SDP negotiation) */ - std::vector<sfl::Codec *> sessionAudioMedia_; + std::vector<sfl::AudioCodec *> sessionAudioMedia_; std::vector<std::string> sessionVideoMedia_; std::string localIpAddr_; diff --git a/daemon/src/sip/sipcall.cpp b/daemon/src/sip/sipcall.cpp index 1987fe677d..f91a00af42 100644 --- a/daemon/src/sip/sipcall.cpp +++ b/daemon/src/sip/sipcall.cpp @@ -89,7 +89,7 @@ std::map<std::string, std::string> SIPCall::createHistoryEntry() const { std::map<std::string, std::string> entry(Call::createHistoryEntry()); - entry[HistoryItem::AUDIO_CODEC_KEY] = local_sdp_->getAudioCodecName(); + entry[HistoryItem::AUDIO_CODEC_KEY] = local_sdp_->getAudioCodecNames(); #ifdef SFL_VIDEO entry[HistoryItem::VIDEO_CODEC_KEY] = local_sdp_->getSessionVideoCodec(); #endif diff --git a/daemon/src/sip/sipvoiplink.cpp b/daemon/src/sip/sipvoiplink.cpp index 72f9fc4aa0..2c1182b213 100644 --- a/daemon/src/sip/sipvoiplink.cpp +++ b/daemon/src/sip/sipvoiplink.cpp @@ -1030,19 +1030,23 @@ SIPVoIPLink::offhold(const std::string& id) throw VoipLinkException("Could not find sdp session"); try { - int pl = PAYLOAD_CODEC_ULAW; - sfl::AudioCodec *sessionMedia = sdpSession->getSessionAudioMedia(); - if (sessionMedia) - pl = sessionMedia->getPayloadType(); + std::vector<sfl::AudioCodec*> sessionMedia; + sdpSession->getSessionAudioMedia(sessionMedia); + std::vector<sfl::AudioCodec*> audioCodecs; + for (std::vector<sfl::AudioCodec*>::const_iterator i = sessionMedia.begin(); + i != sessionMedia.end(); ++i) { - // Create a new instance for this codec - sfl::AudioCodec* ac = Manager::instance().audioCodecFactory.instantiateCodec(pl); + if (!*i) + continue; - if (ac == NULL) - throw VoipLinkException("Could not instantiate codec"); + // Create a new instance for this codec + sfl::AudioCodec* ac = Manager::instance().audioCodecFactory.instantiateCodec((*i)->getPayloadType()); - std::vector<sfl::AudioCodec *> audioCodecs; - audioCodecs.push_back(ac); + if (ac == NULL) + throw VoipLinkException("Could not instantiate codec"); + + audioCodecs.push_back(ac); + } call->getAudioRtp().initConfig(); call->getAudioRtp().initSession(); @@ -1274,9 +1278,9 @@ SIPVoIPLink::getCurrentVideoCodecName(Call *call) const } std::string -SIPVoIPLink::getCurrentAudioCodecName(Call *call) const +SIPVoIPLink::getCurrentAudioCodecNames(Call *call) const { - return static_cast<SIPCall*>(call)->getLocalSDP()->getAudioCodecName(); + return static_cast<SIPCall*>(call)->getLocalSDP()->getAudioCodecNames(); } /* Only use this macro with string literals or character arrays, will not work @@ -1751,8 +1755,9 @@ void sdp_media_update_cb(pjsip_inv_session *inv, pj_status_t status) } #endif // HAVE_SDES - sfl::AudioCodec *sessionMedia = sdpSession->getSessionAudioMedia(); - if (!sessionMedia) + std::vector<sfl::AudioCodec*> sessionMedia; + sdpSession->getSessionAudioMedia(sessionMedia); + if (sessionMedia.empty()) return; try { @@ -1760,16 +1765,19 @@ void sdp_media_update_cb(pjsip_inv_session *inv, pj_status_t status) Manager::instance().getAudioDriver()->startStream(); Manager::instance().audioLayerMutexUnlock(); - int pl = sessionMedia->getPayloadType(); + std::vector<AudioCodec*> audioCodecs; + for (std::vector<sfl::AudioCodec*>::const_iterator i = sessionMedia.begin(); i != sessionMedia.end(); ++i) { + if (!*i) + continue; + const int pl = (*i)->getPayloadType(); - if (pl != call->getAudioRtp().getSessionMedia()) { sfl::AudioCodec *ac = Manager::instance().audioCodecFactory.instantiateCodec(pl); if (!ac) throw std::runtime_error("Could not instantiate codec"); - std::vector<AudioCodec*> audioCodecs; audioCodecs.push_back(ac); - call->getAudioRtp().updateSessionMedia(audioCodecs); } + if (not audioCodecs.empty()) + call->getAudioRtp().updateSessionMedia(audioCodecs); } catch (const SdpException &e) { ERROR("%s", e.what()); } catch (const std::exception &rtpException) { diff --git a/daemon/src/sip/sipvoiplink.h b/daemon/src/sip/sipvoiplink.h index 5dfad36f4c..d525a1c04c 100644 --- a/daemon/src/sip/sipvoiplink.h +++ b/daemon/src/sip/sipvoiplink.h @@ -252,7 +252,7 @@ class SIPVoIPLink : public VoIPLink { * @param c The call identifier */ std::string getCurrentVideoCodecName(Call *c) const; - std::string getCurrentAudioCodecName(Call *c) const; + std::string getCurrentAudioCodecNames(Call *c) const; /** * Retrive useragent name from account diff --git a/daemon/src/voiplink.cpp b/daemon/src/voiplink.cpp index 75f468ed1d..d6afe4954b 100644 --- a/daemon/src/voiplink.cpp +++ b/daemon/src/voiplink.cpp @@ -32,6 +32,7 @@ */ #include "voiplink.h" +#include "account.h" VoIPLink::VoIPLink() : handlingEvents_(false) {} diff --git a/daemon/src/voiplink.h b/daemon/src/voiplink.h index 4cc8590b45..841eb7679b 100644 --- a/daemon/src/voiplink.h +++ b/daemon/src/voiplink.h @@ -147,7 +147,7 @@ class VoIPLink { * @param call The call */ virtual std::string getCurrentVideoCodecName(Call *call) const = 0; - virtual std::string getCurrentAudioCodecName(Call *call) const = 0; + virtual std::string getCurrentAudioCodecNames(Call *call) const = 0; /** * Send a message to a call identified by its callid -- GitLab