diff --git a/conf.d/yjinglechan.conf.sample b/conf.d/yjinglechan.conf.sample index 0d307338..1f8969ce 100644 --- a/conf.d/yjinglechan.conf.sample +++ b/conf.d/yjinglechan.conf.sample @@ -130,6 +130,9 @@ alaw=true ; gsm: bool: European GSM 06.10 (GSM/8000) gsm=default +; ilbc: bool: Internet Low Bandwidth Codec (iLBC/8000) +;ilbc=default + ; lpc10: bool: Linear Prediction Codec (LPC/8000) lpc10=default @@ -147,3 +150,16 @@ g728=default ; g729: bool: ITU G.729 all variations (G729/8000) g729=default + + +[hacks] +; This section holds the dirty stuff required to work with some broken +; or old implementations + +; ilbc_forced: string: Format to offer as iLBC, can be: ilbc20 or ilbc30 +; Defaults to ilbc30 +;ilbc_forced=ilbc30 + +; ilbc_default: string: Format to use for iLBC when packetization is unknown +; Defaults to ilbc30 +;ilbc_default=ilbc30 diff --git a/modules/yjinglechan.cpp b/modules/yjinglechan.cpp index 2cde62ca..6a4b7dd3 100644 --- a/modules/yjinglechan.cpp +++ b/modules/yjinglechan.cpp @@ -287,6 +287,8 @@ private: void overrideJingleVersion(const NamedList& list, bool caps); // Copy chan/session parameters to a destination list void copySessionParams(NamedList& list, bool redirect = true); + // Check media for a received content + bool checkMedia(const JGEvent& event, JGSessionContent& c); Mutex m_mutex; // Lock transport and session State m_state; // Connection state @@ -298,8 +300,8 @@ private: JabberID m_remote; // Remote user's JID ObjList m_audioContents; // The list of negotiated audio contents JGSessionContent* m_audioContent; // The current audio content + JGRtpMediaList m_audioFormats; // Audio formats used by this channel String m_callerPrompt; // Text to be sent to called before calling it - String m_formats; // Formats received in call.execute String m_subject; // Connection subject String m_line; // Connection line String m_localip; // Local address @@ -526,6 +528,7 @@ static String s_capsNode = "http://yate.null.ro/yate/jingle/caps"; // node for e static bool s_serverMode = true; // Server/client mode static YJGEngine* s_jingle = 0; static YJGDriver plugin; // The driver +static bool s_ilbcDefault30 = true; // Default ilbc format when ptime is unknown (30 or 20) // Message handlers installed by the module static const TokenDict s_msgHandler[] = { @@ -608,20 +611,6 @@ static inline void jingleAddParam(NamedList& list, const char* param, const char copy->append(param,","); } -// Add formats to a list of jingle payloads -static void setMedia(JGRtpMediaList& dest, const String& formats, - const JGRtpMediaList& src) -{ - ObjList* f = formats.split(','); - for (ObjList* o = f->skipNull(); o; o = o->skipNext()) { - String* format = static_cast(o->get()); - JGRtpMedia* a = src.findSynonym(*format); - if (a) - dest.append(new JGRtpMedia(*a)); - } - TelEngine::destruct(f); -} - /* * YJGEngine @@ -738,6 +727,7 @@ YJGConnection::YJGConnection(Message& msg, const char* caller, const char* calle m_state(Pending), m_session(0), m_rtpStarted(false), m_acceptRelay(s_acceptRelay), m_sessVersion(s_sessVersion), m_local(caller), m_remote(called), m_audioContent(0), + m_audioFormats(JGRtpMediaList::Audio), m_callerPrompt(msg.getValue("callerprompt")), m_localip(localip), m_offerRawTransport(true), m_offerIceTransport(true), @@ -771,9 +761,12 @@ YJGConnection::YJGConnection(Message& msg, const char* caller, const char* calle } // Get formats. Check if this is a file transfer session if (null(file)) { - m_formats = msg.getValue("formats"); - if (!m_formats) - s_usedCodecs.createList(m_formats,true); + String audio = msg["formats"]; + plugin.lock(); + if (!(audio || s_usedCodecs.createList(audio,true))) + audio = "alaw,mulaw"; + m_audioFormats.setMedia(s_usedCodecs,audio); + plugin.unlock(); } else { m_ftStatus = FTIdle; @@ -835,6 +828,7 @@ YJGConnection::YJGConnection(JGEvent* event) m_sessVersion(event->session()->version()), m_local(event->session()->local()), m_remote(event->session()->remote()), m_audioContent(0), + m_audioFormats(JGRtpMediaList::Audio), m_offerRawTransport(true), m_offerIceTransport(true), m_redirectCount(0), m_dtmfMeth(s_dtmfMeth), m_secure(s_useCrypto), m_secureRequired(s_cryptoMandatory), @@ -843,6 +837,9 @@ YJGConnection::YJGConnection(JGEvent* event) m_connSocksServer(false) { m_line = m_session->line(); + plugin.lock(); + m_audioFormats.setMedia(s_usedCodecs); + plugin.unlock(); // Update local ip in non server mode if (!s_serverMode && m_line) { Message* m = plugin.checkAccount(m_line); @@ -1001,6 +998,11 @@ bool YJGConnection::route() m->addParam("file_time",String(time)); } } + else { + String formats; + m_audioFormats.createList(formats,true); + m->addParam("formats",formats,false); + } m_mutex.unlock(); return startRouter(m); } @@ -2078,15 +2080,6 @@ void YJGConnection::addContent(bool local, JGSessionContent* c) else c->m_rtpLocalCandidates.generateOldIceAuth(); } - // Fill synonym for received media - if (!local) { - for (ObjList* o = c->m_rtpMedia.skipNull(); o; o = o->skipNext()) { - JGRtpMedia* m = static_cast(o->get()); - JGRtpMedia* tmp = s_knownCodecs.findMedia(m->toString()); - if (tmp) - m->m_synonym = tmp->m_synonym; - } - } Debug(this,DebugAll,"Added content='%s' type=%s initiator=%s [%p]", c->toString().c_str(),c->m_rtpLocalCandidates.typeName(), String::boolText(c->creator() == JGSessionContent::CreatorInitiator),this); @@ -2131,12 +2124,8 @@ void YJGConnection::removeCurrentAudioContent(bool removeReq) if (m_audioContent->isEarlyMedia()) c->setEarlyMedia(); // Copy media - c->m_rtpMedia.m_media = m_audioContent->m_rtpMedia.m_media; c->m_rtpMedia.m_cryptoRequired = m_audioContent->m_rtpMedia.m_cryptoRequired; - for (ObjList* o = m_audioContent->m_rtpMedia.skipNull(); o; o = o->skipNext()) { - JGRtpMedia* m = static_cast(o->get()); - c->m_rtpMedia.append(new JGRtpMedia(*m)); - } + c->m_rtpMedia.setMedia(m_audioContent->m_rtpMedia); // Append addContent(true,c); if (m_session) @@ -2234,8 +2223,11 @@ bool YJGConnection::startRtp() m.addParam("media","audio"); m.addParam("getsession","true"); ObjList* obj = m_audioContent->m_rtpMedia.skipNull(); - if (obj) - m.addParam("format",(static_cast(obj->get()))->m_synonym); + if (obj) { + JGRtpMedia* media = static_cast(obj->get()); + m.addParam("payload",media->m_id); + m.addParam("format",media->m_synonym); + } m.addParam("localip",rtpLocal->m_address); m.addParam("localport",rtpLocal->m_port); m.addParam("remoteip",rtpRemote->m_address); @@ -2411,34 +2403,7 @@ bool YJGConnection::processContentAdd(const JGEvent& event, ObjList& ok, ObjList } // Check media - // Fill a string with our capabilities for debug purposes - String remoteCaps; - if (debugAt(DebugInfo)) - c->m_rtpMedia.createList(remoteCaps,false); - // Check received media against the used codecs list - // Compare 'id' and 'name' - ListIterator iter(c->m_rtpMedia); - for (GenObject* go; (go = iter.get());) { - JGRtpMedia* recv = static_cast(go); - ObjList* used = s_usedCodecs.skipNull(); - for (; used; used = used->skipNext()) { - JGRtpMedia* local = static_cast(used->get()); - if (local->m_id == recv->m_id && local->m_name == recv->m_name) - break; - } - if (!used) - c->m_rtpMedia.remove(recv,true); - } - // Check if both parties have common media - if (!c->m_rtpMedia.skipNull()) { - if (debugAt(DebugInfo)) { - String localCaps; - s_usedCodecs.createList(localCaps,false); - Debug(this,DebugNote, - "Event(%s) no common media for content='%s' local='%s' remote='%s' [%p]", - event.actionName(),c->toString().c_str(),localCaps.c_str(), - remoteCaps.c_str(),this); - } + if (!checkMedia(event,*c)) { remove.append(c)->setDelete(false); continue; } @@ -2514,7 +2479,7 @@ JGSessionContent* YJGConnection::buildAudioContent(JGRtpCandidates::Type type, if (m_secure && m_secureRequired) c->m_rtpMedia.m_cryptoRequired = true; if (useFormats) - setMedia(c->m_rtpMedia,m_formats,s_usedCodecs); + c->m_rtpMedia.setMedia(m_audioFormats); c->m_rtpLocalCandidates.m_type = c->m_rtpRemoteCandidates.m_type = type; @@ -2685,7 +2650,9 @@ void YJGConnection::setEarlyMediaOut(Message& msg) if (!c) { c = buildAudioContent(JGRtpCandidates::RtpRawUdp, JGSessionContent::SendResponder,false,false); - setMedia(c->m_rtpMedia,formats,s_usedCodecs); + plugin.lock(); + c->m_rtpMedia.setMedia(s_usedCodecs,formats); + plugin.unlock(); c->setEarlyMedia(); addContent(true,c); } @@ -3037,8 +3004,11 @@ void YJGConnection::copySessionParams(NamedList& list, bool redirect) jingleAddParam(list,"redirectcount",String(m_redirectCount),copy); list.addParam("diverter",m_remote,false); } - if (m_ftStatus == FTNone) - jingleAddParam(list,"formats",m_formats,copy,false); + if (m_ftStatus == FTNone) { + String formats; + m_audioFormats.createList(formats,true); + jingleAddParam(list,"formats",formats,copy,false); + } else jingleAddParam(list,"format","data",copy); // Jingle session params @@ -3075,6 +3045,125 @@ void YJGConnection::copySessionParams(NamedList& list, bool redirect) jingleAddParam(list,"file_time",String(t),copy); } +// Check media in a received content +bool YJGConnection::checkMedia(const JGEvent& event, JGSessionContent& c) +{ + JGRtpMediaList& codecs = m_audioFormats; + // Fill a string with our capabilities for debug purposes + String remoteCaps; + if (debugAt(DebugInfo)) + c.m_rtpMedia.createList(remoteCaps,false); + ListIterator iter(c.m_rtpMedia); + for (GenObject* go = 0; (go = iter.get());) { + JGRtpMedia* recv = static_cast(go); + XDebug(this,DebugAll,"Checking received media %s/%s/%s/%s/%s/%s/%s [%p]", + recv->m_id.c_str(),recv->m_name.c_str(),recv->m_clockrate.c_str(), + recv->m_channels.c_str(),recv->m_pTime.c_str(), + recv->m_maxPTime.c_str(),recv->m_bitRate.c_str(),this); + const char* reason = 0; + int level = DebugNote; + // Use a while() to break to the end + while (true) { + // RTP payload id must be [0..127] + int payloadId = recv->m_id.toInteger(-1); + if (payloadId < 0 || payloadId > 127) { + reason = "Invalid id"; + break; + } + // XEP 0167: Channels is an unsigned byte, defaults to 1 + // We support only 1 channel for now + if (recv->m_channels.toInteger(1) != 1) { + reason = "Invalid number of channels"; + break; + } + JGRtpMedia* found = 0; + // 0..95: static payloads: match by id + // > 95: dynamic payloads: match by name + if (payloadId < 96) + found = codecs.findMedia(recv->m_id); + else if (recv->m_name) { + // Remove tel event from offer + if ((recv->m_name &= "telephone-event") || + (recv->m_name &= "tone") || + (recv->m_name &= "audio/telephone-event")) { + XDebug(this,DebugAll,"Removing tel event '%s' [%p]", + recv->m_name.c_str(),this); + c.m_rtpMedia.remove(recv); + break; + } + for (ObjList* o = codecs.skipNull(); o; o = o->skipNext(), found = 0) { + found = static_cast(o->get()); + if (found->m_name |= recv->m_name) + continue; + if (recv->m_clockrate && recv->m_clockrate != found->m_clockrate) + continue; + // Fix ilbc + if (recv->m_name &= "ilbc") { + // RFC 3952 specifies + // 30ms ptime = 13.33 kbit/s: check 13000 + // 20ms ptime = 15.2 kbit/s: check 15000 + if (!recv->m_pTime && recv->m_bitRate) { + int val = recv->m_bitRate.toInteger() / 1000; + if (val == 13) + recv->m_pTime = "30"; + else if (val == 15) + recv->m_pTime = "20"; + } + if (!recv->m_pTime) + recv->m_pTime = (s_ilbcDefault30 ? "30" : "20"); + if (recv->m_pTime != found->m_pTime) + continue; + } + break; + } + } + else { + // XEP 0167: name is mandatory for dynamic payloads + reason = "Missing name for dynamic payload"; + break; + } + if (found) { + XDebug(this,DebugAll,"Setting synonym=%s to received %s from %s/%s [%p]", + found->m_synonym.c_str(),recv->m_name.c_str(), + found->m_id.c_str(),found->m_name.c_str(),this); + recv->m_synonym = found->m_synonym; + } + else { + reason = "Codec disabled/unknown"; + level = DebugAll; + } + break; + } + if (!reason) + continue; + Debug(this,level, + "Event(%s) removing payload id=%s %s/%s/%s/%s from content='%s': %s [%p]", + event.actionName(),recv->m_id.c_str(),recv->m_name.c_str(), + recv->m_clockrate.c_str(),recv->m_channels.c_str(),recv->m_pTime.c_str(), + c.toString().c_str(),reason,this); + c.m_rtpMedia.remove(recv); + } + // Check if both parties have common media + if (c.m_rtpMedia.skipNull()) { +#ifdef DEBUG + String formats; + c.m_rtpMedia.createList(formats,true); + Debug(this,DebugAll,"Set formats '%s' in content '%s' [%p]", + formats.c_str(),c.toString().c_str(),this); +#endif + return true; + } + if (debugAt(DebugInfo)) { + String localCaps; + codecs.createList(localCaps,false); + Debug(this,DebugNote, + "Event(%s) no common media for content='%s' local='%s' remote='%s' [%p]", + event.actionName(),c.toString().c_str(),localCaps.c_str(), + remoteCaps.c_str(),this); + } + return false; +} + /* * Transfer thread (route and execute) @@ -3261,8 +3350,11 @@ void YJGDriver::initialize() s_knownCodecs.add("32", "MPV", "90000", "mpv"); s_knownCodecs.add("34", "H263", "90000", "h263"); s_knownCodecs.add("98", "iLBC", "8000", "ilbc"); - s_knownCodecs.add("98", "iLBC", "8000", "ilbc20"); - s_knownCodecs.add("98", "iLBC", "8000", "ilbc30"); + s_knownCodecs.add("98", "iLBC", "8000", "ilbc20", 0, "20"); + s_knownCodecs.add("98", "iLBC", "8000", "ilbc30", 0, "30"); + s_knownCodecs.add("102","speex", "8000", "speex"); + s_knownCodecs.add("103","speex", "16000", "speex/16000"); + s_knownCodecs.add("104","speex", "32000", "speex/32000"); s_jingle = new YJGEngine; s_jingle->debugChain(this); @@ -3369,10 +3461,24 @@ void YJGDriver::initialize() bool defcodecs = s_cfg.getBoolValue("codecs","default",true); for (ObjList* o = s_knownCodecs.skipNull(); o; o = o->skipNext()) { JGRtpMedia* crt = static_cast(o->get()); + if (crt->m_name &= "ilbc") + continue; bool enable = defcodecs && DataTranslator::canConvert(crt->m_synonym); if (s_cfg.getBoolValue("codecs",crt->m_synonym,enable)) s_usedCodecs.append(new JGRtpMedia(*crt)); } + // Special care for ilbc + bool ilbc = s_cfg.getBoolValue("codecs","ilbc",defcodecs); + if (ilbc) { + String tmp = s_cfg.getValue("hacks","ilbc_forced","ilbc30"); + if (tmp != "ilbc20" && tmp != "ilbc30") + tmp = "ilbc30"; + JGRtpMedia* s = s_knownCodecs.findSynonym(tmp); + if (s && DataTranslator::canConvert(s->m_synonym)) + s_usedCodecs.append(new JGRtpMedia(*s)); + tmp = s_cfg.getValue("hacks","ilbc_default","ilbc30"); + s_ilbcDefault30 = (tmp != "ilbc20"); + } TelEngine::destruct(m_ftProxy); const char* ftJid = sect->getValue("socks_proxy_jid"); @@ -3388,14 +3494,14 @@ void YJGDriver::initialize() } int dbg = DebugInfo; - if (!m_localAddress) + if (!m_localAddress && s_serverMode) dbg = DebugNote; if (!s_usedCodecs.count()) dbg = DebugWarn; if (debugAt(dbg)) { String s; - s << " localip=" << m_localAddress ? m_localAddress.c_str() : "MISSING"; + s << " localip=" << (m_localAddress ? m_localAddress.c_str() : "MISSING"); s << " jingle_version=" << JGSession::lookupVersion(s_sessVersion); s << " singletone=" << String::boolText(s_singleTone); s << " pending_timeout=" << s_pendingTimeout;