bool Application::init(int argc, const char* argv[]) { // set main thread name Util::SetCurrentThreadName("Main"); initApplicationPaths(argv[0]); // configurations string name("configs"); getString("application.baseName", name); string configPath(_path.parent()); configPath.append(name); configPath.append(".ini"); if (loadConfigurations(configPath)) { setString("application.configPath", configPath); vector<string> values; FileSystem::Unpack(configPath, values); if (!values.empty()) values.resize(values.size() - 1); FileSystem::Pack(values, configPath); setString("application.configDir", configPath); } // logs Logs::SetLogger(*this); string logDir(_path.parent()); logDir.append("logs"); string logFileName("log"); if (loadLogFiles(logDir, logFileName, _logSizeByFile, _logRotation)) { FileSystem::CreateDirectory(logDir); _logPath = logDir + "/" + logFileName; if (_logRotation > 0) { _logPath.append("."); _logStream.open(_logPath + "0", ios::out | ios::binary | ios::app); } else _logStream.open(_logPath, ios::out | ios::binary | ios::app); } // options Exception ex; defineOptions(ex, _options); if (ex) FATAL_ERROR(ex.error()); if (!_options.process(ex, argc, argv, [this](const string& name, const string& value) { setString("arguments." + name, value); })) FATAL_ERROR("Arguments, ",ex.error()," use 'help'") else if (ex) WARN("Arguments, ",ex.error()," use 'help'") if (hasArgument("help")) { displayHelp(); return false; } return true; }
void FlashStream::messageHandler(const string& name,AMFReader& message,FlashWriter& writer) { if(name=="play") { disengage(&writer); string publication; message.readString(publication); // TODO implements completly NetStream.play method, with possible NetStream.play.failed too! Exception ex; _pListener = invoker.subscribe(ex,peer,publication,writer); if (ex) { writer.writeAMFStatus("NetStream.Play.Failed",ex.error()); return; } if(message.available()) _pListener->setNumber("unbuffered",message.readNumber()==-3000); if(_bufferTime>0) { // To do working the buffertime on receiver side BinaryWriter& raw = writer.writeRaw(); raw.write16(0); raw.write32(id); _pListener->setNumber("bufferTime",_bufferTime); } writer.writeAMFStatus("NetStream.Play.Reset","Playing and resetting " + publication); // for entiere playlist writer.writeAMFStatus("NetStream.Play.Start","Started playing " + publication); // for item } else if(name == "closeStream") { disengage(&writer); } else if(name=="publish") { disengage(&writer); string type,publication; message.readString(publication); size_t query = publication.find('?'); if (query != string::npos) publication = publication.substr(0, query); // TODO use query in Util::UnpackQuery for publication options? if(message.available()) message.readString(type); // TODO support "append" and "appendWithGap" Exception ex; _pPublication = invoker.publish(ex, peer, publication, type == "record" ? Publication::RECORD : Publication::LIVE); if (ex) writer.writeAMFStatus("NetStream.Publish.BadName",ex.error()); else writer.writeAMFStatus("NetStream.Publish.Start",publication +" is now published"); } else if(_pListener && name=="receiveAudio") { _pListener->receiveAudio = message.readBoolean(); } else if(_pListener && name=="receiveVideo") { _pListener->receiveVideo = message.readBoolean(); } else ERROR("RTMFPMessage '",name,"' unknown on stream ",id); }
bool Application::init(int argc, const char* argv[]) { // set main thread name Util::SetCurrentThreadName("Main"); initApplicationPaths(argv[0]); // configurations string configPath; if (loadConfigurations(String::Format(configPath,_file.parent(),_file.baseName(),".ini"))) { setString("application.configPath", configPath); setString("application.configDir", FileSystem::GetParent(configPath)); } // logs Logs::SetLogger(*this); string logDir(_file.parent()); logDir.append("logs"); string logFileName("log"); Exception ex; if (loadLogFiles(logDir, logFileName, _logSizeByFile, _logRotation)) { bool success; EXCEPTION_TO_LOG(success=FileSystem::CreateDirectory(ex, logDir),file().baseName()," log system") if (success) { _logPath.assign(FileSystem::MakeFolder(logDir)).append(logFileName); if (_logRotation > 0) { _logPath += '.'; _logStream.open(_logPath += '0', ios::out | ios::binary | ios::app); } else _logStream.open(_logPath, ios::out | ios::binary | ios::app); } } // options defineOptions(ex, _options); if (ex) FATAL_ERROR(ex.error()); if (!_options.process(ex, argc, argv, [this](const string& name, const string& value) { setString("arguments." + name, value); })) FATAL_ERROR("Arguments, ",ex.error()," use 'help'") else if (ex) WARN("Arguments, ",ex.error()," use 'help'") if (hasArgument("help")) { displayHelp(); return false; } return true; }
void Peer::onConnection(Exception& ex, Writer& writer,DataReader& parameters,DataWriter& response) { if(!connected) { _pWriter = &writer; // reset default protocol parameters _parameters.clear(); Parameters::ForEach forEach([this](const string& key,const string& value) { _parameters.setString(key,value); }); string buffer; _handler.iterate(String::Format(buffer,protocol,"."), forEach); ParameterWriter parameterWriter(_parameters); SplitWriter parameterAndResponse(parameterWriter,response); _handler.onConnection(ex, *this,parameters,parameterAndResponse); if (!ex) { (bool&)connected = ((Entities<Client>&)_handler.clients).add(*this); if (!connected) { ex.set(Exception::PROTOCOL, "Client ", Util::FormatHex(id, ID_SIZE, buffer), " exists already"); ERROR(ex.error()); _handler.onDisconnection(*this); } } if (!connected) { writer.abort(); _pWriter = NULL; } else { OnInitParameters::raise(_parameters); DEBUG("Client ",address.toString()," connection") } writer.open(); // open even if "ex" to send error messages! } else
bool Application::loadConfigurations(string& path) { Exception ex; if (Util::ReadIniFile(ex, path, *this)) return true; DEBUG("Impossible to load configuration file (", ex.error(), ")"); return false; }
UInt8 QueryReader::valueType() { if (String::ICompare(_value, "null") == 0) return NIL; if (String::ICompare(_value, "false") == 0) { _number = 0; return BOOLEAN; } if (String::ICompare(_value, "true") == 0) { _number = 1; return BOOLEAN; } Exception ex; if (_date.update(ex, _value)) { if (ex) WARN("QueryReader date, ", ex.error()); return DATE; } if (String::ToNumber(_value, _number)) return NUMBER; return STRING; }
bool Parse(const char * stDate, Int32 year, UInt8 month, UInt8 day,UInt8 weekDay, UInt8 hour, UInt8 minute, UInt8 second, UInt16 millisecond,Int32 offset,const char* format=NULL) { bool bIsParseOk = _Date.update(Ex,stDate,format); if (!bIsParseOk) { DEBUG("Error during parsing of ", stDate, ", ",Ex.error()); return false; } return Check(year, month, day, weekDay, hour, minute, second, millisecond, offset); }
void RTMFPWriter::manage(Exception& ex, Invoker& invoker) { if(!consumed() && !_band.failed()) { if(_trigger.raise(ex)) raiseMessage(); if (ex) { fail("RTMFPWriter can't deliver its data, ",ex.error()); return; } } if(critical && state()==CLOSED) { ex.set(Exception::NETWORK, "Main flow writer closed, session is closing"); return; } flush(false); }
void RTMFPSession::manage() { if(died) return; Session::manage(); if (_failed) { failSignal(); return; } // After 6 mn we considerate than the session has failed if(_recvTimestamp.isElapsed(360000000)) { fail("Timeout no client message"); return; } // To accelerate the deletion of peer ghost (mainly for netgroup efficient), starts a keepalive server after 2 mn if(_recvTimestamp.isElapsed(120000000) && !keepAlive()) // TODO check it! return; // Raise RTMFPWriter auto it=_flowWriters.begin(); while (it != _flowWriters.end()) { Exception ex; it->second->manage(ex, invoker); if (ex) { if (it->second->critical) { fail(ex.error()); break; } continue; } if (it->second->consumed()) { _flowWriters.erase(it++); continue; } ++it; } flush(); }
void RTMFPSession::flush(UInt8 marker,bool echoTime,RTMFPEngine::Type type) { _pLastWriter=NULL; if(!_pSender) return; if (!died && _pSender->available()) { PacketWriter& packet(_pSender->packet); // After 30 sec, send packet without echo time if(_recvTimestamp.isElapsed(30000000)) echoTime = false; if(echoTime) marker+=4; else packet.clip(2); BinaryWriter writer(packet, 6); writer.write8(marker).write16(RTMFP::TimeNow()); if(echoTime) writer.write16(_timeSent+RTMFP::Time(_recvTimestamp.elapsed())); _pSender->farId = farId; _pSender->encoder.type = type; _pSender->address.set(peer.address); if (packet.size() > RTMFP_MAX_PACKET_SIZE) ERROR("Message exceeds max RTMFP packet size"); dumpResponse(packet.data() + 6, packet.size() - 6); Exception ex; _pThread = _socket.send<RTMFPSender>(ex, _pSender,_pThread); if (ex) ERROR("RTMFP flush, ", ex.error()); } _pSender.reset(); }
void RTMFPSession::flush(bool echoTime,UInt8 marker) { _pLastWriter=NULL; if(!_pSender) return; if (!died && _pSender->available()) { BinaryWriter& packet(_pSender->packet); // After 30 sec, send packet without echo time if(peer.lastReceptionTime.isElapsed(30000)) echoTime = false; if(echoTime) marker+=4; else packet.clip(2); BinaryWriter writer(packet.data()+6, 5); writer.write8(marker).write16(RTMFP::TimeNow()); if (echoTime) writer.write16(_timeSent+RTMFP::Time(peer.lastReceptionTime.elapsed())); _pSender->farId = farId; _pSender->address.set(peer.address); if (packet.size() > RTMFP_MAX_PACKET_SIZE) ERROR("Message exceeds max RTMFP packet size on session ",name()," (",packet.size(),">",RTMFP_MAX_PACKET_SIZE,")"); dumpResponse(packet.data() + 6, packet.size() - 6); Exception ex; _pThread = Session::send<RTMFProtocol,RTMFPSender>(ex, _pSender,_pThread); if (ex) ERROR("RTMFP flush, ", ex.error()); } _pSender.reset(); }
void FlashStream::messageHandler(const string& name, AMFReader& message, FlashWriter& writer) { if (name == "play") { disengage(&writer); string publication; message.readString(publication); // TODO implements completly NetStream.play method, with possible NetStream.play.failed too! Exception ex; _pListener = invoker.subscribe(ex, peer, publication, writer); // ex already log displayed if (!_pListener) { writer.writeAMFStatus("NetStream.Play.Failed", ex.error()); return; } OnStart::raise(id, writer); // stream begin writer.writeAMFStatus("NetStream.Play.Reset", "Playing and resetting " + publication); // for entiere playlist writer.writeAMFStatus("NetStream.Play.Start", "Started playing "+publication); // for item AMFWriter& amf(writer.writeInfos("|RtmpSampleAccess")); amf.writeBoolean(true); // audioSampleAccess amf.writeBoolean(true); // videoSampleAccess if (_bufferTime > 0) _pListener->setNumber("bufferTime", _bufferTime); } else if (name == "closeStream") { disengage(&writer); } else if (name == "publish") { disengage(&writer); string type, publication; message.readString(publication); size_t query = publication.find('?'); if (query != string::npos) publication = publication.substr(0, query); // TODO use query in Util::UnpackQuery for publication options? if (message.available()) message.readString(type); // TODO support "append" and "appendWithGap" Exception ex; _pPublication = invoker.publish(ex, peer, publication, type == "record" ? Publication::RECORD : Publication::LIVE); if (ex) { writer.writeAMFStatus("NetStream.Publish.BadName", ex.error()); _pPublication = NULL; } else writer.writeAMFStatus("NetStream.Publish.Start", publication + " is now published"); } else if (_pListener && name == "receiveAudio") { message.readBoolean(_pListener->receiveAudio); } else if (_pListener && name == "receiveVideo") { message.readBoolean(_pListener->receiveVideo); } else if (_pListener && name == "pause") { bool paused(true); message.readBoolean(paused); // TODO support pause for VOD if (paused) { // useless, client knows it when it calls NetStream::pause method // writer.writeAMFStatus("NetStream.Pause.Notify", _pListener->publication.name() + " paused"); } else { double position; if (message.readNumber(position)) _pListener->seek((UInt32)position); OnStart::raise(id, writer); // stream begin // useless, client knows it when it calls NetStream::resume method // writer.writeAMFStatus("NetStream.Unpause.Notify", _pListener->publication.name() + " resumed"); } } else if (_pListener && name == "seek") { double position; if (message.readNumber(position)) { _pListener->seek((UInt32)position); // TODO support seek for VOD OnStart::raise(id, writer); // stream begin // useless, client knows it when it calls NetStream::seek method, and wait "NetStream.Seek.Complete" rather (raised by client side) // writer.writeAMFStatus("NetStream.Seek.Notify", _pListener->publication.name() + " seek operation"); } else writer.writeAMFStatus("NetStream.Seek.InvalidTime", _pListener->publication.name() + " seek operation must pass in argument a milliseconds position time"); } else if (_pPublication && name == "@setDataFrame") { // metadata _pPublication->writeProperties(message); } else if (_pPublication && name == "@clearDataFrame") { _pPublication->clearProperties(); } else ERROR("Message '",name,"' unknown on stream ",id); }
void WSSession::packetHandler(PacketReader& packet) { UInt8 type = 0; Exception ex; if(peer.connected) { type = packet.read8(); switch(type) { case WS::TYPE_BINARY: { RawReader reader(packet); peer.onMessage(ex, "onMessage",reader,WS::TYPE_BINARY); break; } case WS::TYPE_TEXT: { if(!JSONReader::IsValid(packet)) { RawReader reader(packet); peer.onMessage(ex, "onMessage",reader); break; } JSONReader reader(packet); if(reader.followingType()!=JSONReader::STRING) { peer.onMessage(ex, "onMessage",reader); break; } string name; reader.readString(name); if(name=="__publish") { if(reader.followingType()!=JSONReader::STRING) { ex.set(Exception::PROTOCOL, "__publish method takes a stream name in first parameter",WS::CODE_MALFORMED_PAYLOAD); break; } reader.readString(name); if(_pPublication) invoker.unpublish(peer,_pPublication->name()); _pPublication = invoker.publish(ex, peer,name); } else if(name=="__play") { if(reader.followingType()!=JSONReader::STRING) { ex.set(Exception::PROTOCOL, "__play method takes a stream name in first parameter",WS::CODE_MALFORMED_PAYLOAD); break; } reader.readString(name); closeSusbcription(); } else if(name=="__closePublish") { closePublication(); } else if(name=="__closePlay") { closeSusbcription(); } else if (name == "__close") { closePublication(); closeSusbcription(); } else if(_pPublication) { reader.reset(); _pPublication->pushData(reader); } else peer.onMessage(ex, name,reader); break; } case WS::TYPE_CLOSE: _writer.close(packet.available() ? packet.read16() : 0); break; case WS::TYPE_PING: _writer.writePong(packet.current(),packet.available()); break; case WS::TYPE_PONG: peer.setPing(_writer.ping = (UInt16)(_time.elapsed()/1000)); break; default: ex.set(Exception::PROTOCOL, Format<UInt8>("Type %#x unknown", type), WS::CODE_MALFORMED_PAYLOAD); break; } if (ex) { ERROR(ex.error()); _writer.close((ex.code()==Exception::APPLICATION || ex.code() == Exception::SOFTWARE) ? WS::CODE_PROTOCOL_ERROR : ex.code()); } } if(!peer.connected || type==WS::TYPE_CLOSE) kill(); else _writer.flush(); }
UInt8 RTMFPHandshake::handshakeHandler(UInt8 id,const SocketAddress& address, BinaryReader& request,BinaryWriter& response) { switch(id){ case 0x30: { request.read7BitValue(); // = epdLen + 2 (useless) UInt16 epdLen = request.read7BitValue()-1; UInt8 type = request.read8(); string epd; request.read(epdLen,epd); string tag; request.read(16,tag); response.write7BitValue(tag.size()).write(tag); if(type == 0x0f) { const UInt8* peerId((const UInt8*)epd.c_str()); RTMFPSession* pSessionWanted = _sessions.findByPeer<RTMFPSession>(peerId); if(pSessionWanted) { if(pSessionWanted->failed()) return 0x00; // TODO no way in RTMFP to tell "died!" /// Udp hole punching UInt32 times = attempt(tag); RTMFPSession* pSession(NULL); if(times > 0 || address.host() == pSessionWanted->peer.address.host()) // try in first just with public address (excepting if the both peer are on the same machine) pSession = _sessions.findByAddress<RTMFPSession>(address,Socket::DATAGRAM); bool hasAnExteriorPeer(pSessionWanted->p2pHandshake(tag,address,times,pSession)); // public address RTMFP::WriteAddress(response,pSessionWanted->peer.address, RTMFP::ADDRESS_PUBLIC); DEBUG("P2P address initiator exchange, ",pSessionWanted->peer.address.toString()); if (hasAnExteriorPeer && pSession->peer.serverAddress.host()!=pSessionWanted->peer.address.host()) { // the both peer see the server in a different way (and serverAddress.host()!= public address host written above), // Means an exterior peer, but we can't know which one is the exterior peer // so add an interiorAddress build with how see eachone the server on the both side SocketAddress interiorAddress(pSession->peer.serverAddress.host(), pSessionWanted->peer.address.port()); RTMFP::WriteAddress(response,interiorAddress, RTMFP::ADDRESS_PUBLIC); DEBUG("P2P address initiator exchange, ",interiorAddress.toString()); } // local address for(const SocketAddress& address : pSessionWanted->peer.localAddresses) { RTMFP::WriteAddress(response,address, RTMFP::ADDRESS_LOCAL); DEBUG("P2P address initiator exchange, ",address.toString()); } // add the turn address (RelayServer) if possible and required if (pSession && times>0) { UInt8 timesBeforeTurn(0); if(pSession->peer.parameters().getNumber("timesBeforeTurn",timesBeforeTurn) && timesBeforeTurn>=times) { UInt16 port = invoker.relayer.relay(pSession->peer.address,pSessionWanted->peer.address,20); // 20 sec de timeout is enough for RTMFP! if (port > 0) { SocketAddress address(pSession->peer.serverAddress.host(), port); RTMFP::WriteAddress(response, address, RTMFP::ADDRESS_REDIRECTION); } // else ERROR already display by RelayServer class } } return 0x71; } DEBUG("UDP Hole punching, session ", Util::FormatHex(peerId, ID_SIZE, LOG_BUFFER), " wanted not found") set<SocketAddress> addresses; peer.onRendezVousUnknown(peerId,addresses); set<SocketAddress>::const_iterator it; for(it=addresses.begin();it!=addresses.end();++it) { if(it->host().isWildcard()) continue; if(address == *it) WARN("A client tries to connect to himself (same ", address.toString()," address)"); RTMFP::WriteAddress(response,*it,RTMFP::ADDRESS_REDIRECTION); DEBUG("P2P address initiator exchange, ",it->toString()); } return addresses.empty() ? 0 : 0x71; } if(type == 0x0a){ /// RTMFPHandshake HelloAttempt& attempt = AttemptCounter::attempt<HelloAttempt>(tag); Peer& peer(*_pPeer); // Fill peer infos peer.properties().clear(); string serverAddress; Util::UnpackUrl(epd, serverAddress, (string&)peer.path,(string&)peer.query); peer.setServerAddress(serverAddress); Util::UnpackQuery(peer.query, peer.properties()); Exception ex; set<SocketAddress> addresses; peer.onHandshake(attempt.count+1,addresses); if(!addresses.empty()) { set<SocketAddress>::iterator it; for(it=addresses.begin();it!=addresses.end();++it) { if (it->host().isWildcard()) RTMFP::WriteAddress(response, peer.serverAddress, RTMFP::ADDRESS_REDIRECTION); else RTMFP::WriteAddress(response, *it, RTMFP::ADDRESS_REDIRECTION); } return 0x71; } // New RTMFPCookie RTMFPCookie* pCookie = attempt.pCookie; if (!pCookie) { pCookie = new RTMFPCookie(*this, invoker, tag, _pPeer); if (!pCookie->run(ex)) { delete pCookie; ERROR("RTMFPCookie creation, ", ex.error()) return 0; } _pPeer.reset(new Peer((Handler&)invoker)); // reset peer _cookies.emplace(pCookie->value(), pCookie); attempt.pCookie = pCookie; } // response response.write8(COOKIE_SIZE); response.write(pCookie->value(),COOKIE_SIZE); // instance id (certificat in the middle) response.write(_certificat,sizeof(_certificat)); return 0x70; } else
bool SDP::build(Exception& ex, const string& text) { SDPMedia* pMedia = NULL; vector<string> lines; String::Split(text, "\r\n", lines, String::SPLIT_IGNORE_EMPTY | String::SPLIT_TRIM); for(string& line : lines) { if(line.empty() || line[1] != '=') { ex.set(Exception::FORMATTING, "SDP line ",line," malformed, second byte isn't an equal sign"); return false; } UInt8 type = line[0]; line.erase(0,2); String::Trim(line, String::TRIM_LEFT); // RFC 4566 switch(type) { case 'v': // v= (protocol version) version = String::ToNumber<UInt32>(ex, line); break; case 'o': { // o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address> vector<string> fields; String::Split(line, " ", fields, String::SPLIT_IGNORE_EMPTY | String::SPLIT_TRIM); if (fields.size()!=6) { ex.set(Exception::PROTOCOL, "fields.size()!=6"); break; } user = fields[0]; sessionId = String::ToNumber<UInt32>(ex, fields[1]); if (ex) break; sessionVersion = String::ToNumber<UInt32>(ex, fields[2]); if (ex) break; unicastAddress.set(ex,fields[5],fields[4]=="IP6" ? IPAddress::IPv6 : IPAddress::IPv4); break; } case 's': // s= (session name) sessionName = line; break; /// Optional lines case 'i': // i=* (session information) sessionInfos = line; break; case 'u': // u=* (URI of description) uri = line; break; case 'e': // e=* (email address) email = line; break; case 'p': // p=* (phone number) phone = line; break; case 'c': { // c=* (connection information -- not required if included in all media) vector<string> fields; String::Split(line, " ", fields, String::SPLIT_IGNORE_EMPTY | String::SPLIT_TRIM); if(fields.size()!=3) { ex.set(Exception::PROTOCOL, "fields.size()!=3"); break; } IPAddress defaultAddress; // TODO defaultAddress is useless for what? defaultAddress.set(ex,fields[2], fields[1] == "IP6" ? IPAddress::IPv6 : IPAddress::IPv4); break; } case 'b': // b=* (zero or more bandwidth information lines) // TODO useless? break; // One or more time descriptions ("t=" and "r=" lines; see below) // t= (time the session is active) // r=* (zero or more repeat times) case 't': // TODO useless? break; case 'r': // TODO useless? break; case 'z': // z=* (time zone adjustments) // TODO useless? break; case 'k': // k=* (encryption key) encryptKey = line; break; case 'm': { // m=<name> <port> <proto> <fmt> vector<string> values; String::Split(line, " ", values, String::SPLIT_IGNORE_EMPTY | String::SPLIT_TRIM); if (values.size()<4) { ex.set(Exception::PROTOCOL , "values.size()<4"); break; } UInt16 val = String::ToNumber<UInt16>(ex, values[1]); if (ex) break; pMedia = addMedia(values[0], val, values[2]); for(const string& st : values) { pMedia->formats.emplace_back(String::ToNumber<UInt8>(ex, st)); if (ex) break; } break; } case 'a': { // a=* (zero or more session attribute lines) vector<string>* pFields = NULL; bool isMsId = false; string fingerHash; // TODO SDPSource* pSource = NULL; // TODO list<UInt32>* pSourceGroupe = NULL; vector<string> fields; String::Split(line, " ", fields, String::SPLIT_IGNORE_EMPTY | String::SPLIT_TRIM); for(const string& st : fields) { size_t pos = st.find(':'); string key,value; if(pos!=string::npos) { key = st.substr(0,pos); value = st.substr(pos+1); } else key = st; // RFC 5576 if(pMedia) { /* TODO if(pSourceGroupe) pSourceGroupe->push_back(NumberParser::parseUnsigned(field)); else if(pSource) { if(isMsId) pSource->msData = field; else if(key=="cname") // cname:<value> pSource->cname = value; else if(key=="msid") {// draft-alvestrand-mmusic-msid-00 isMsId = true; pSource->msId = value; } else if(key=="mslabel") // draft-alvestrand-rtcweb-mid-01 pSource->msLabel = value; else if(key=="label") // draft-alvestrand-rtcweb-mid-01 pSource->label = value; else WARN("Media source attribute ",key," unknown"); } else if(key=="ssrc") // a=ssrc:<ssrc-id> pSource = &sources[NumberParser::parseUnsigned(key)];// a=ssrc:<ssrc-id> <attribute> else if(key=="rtcp-mux") pMedia->rtcpMux = true; else if(key=="mid") // RFC 3388, a=mid:<token> pMedia->mid = value; else if(key=="ssrc-group") // a=ssrc-group:<semantics> pSourceGroupe = &sourceGroupes[value]; else if(key="crypto") { // a=crypto:<tag> <crypto-suite> <key-params> [<session-params>] StringTokenizer values(value," ",StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); poco_assert(values.size()>2) pMedia->cryptoTag = } else */ WARN("Media attribute ",key," unknown"); } else if(pFields) pFields->emplace_back(st); else if(!fingerHash.empty()) int i=0; // TODO finger = talk_base::SSLFingerprint::CreateFromRfc4572(fingerHash, field); else if(key=="group") // RFC 5888 and draft-holmberg-mmusic-sdp-bundle-negotiation-00 pFields = &groups[value]; else if(key=="ice-ufrag") iceUFrag = value; else if(key=="ice-pwd") icePwd = value; else if(key=="ice-options") { pFields = &iceOptions; iceOptions.emplace_back(value); } else if(key=="fingerprint") // fingerprint:<hash> <algo> fingerHash = value; // fingerprint:<hash> else if(key=="msid-semantic") supportMsId = value=="VMS"; else if(key=="extmap") // RFC 5285 a=extmap:<value>["/"<direction>] <URI> <extensionattributes> pFields = &extensions[value]; else WARN("SDP attribute ",key," unknown"); } } // case 'a': break; } // switch(type) if (ex) { ex.set(Exception::PROTOCOL, "SDP '",type,"' value ",line," malformed, ",ex.error()); return false; } } // for(string& line : lines) return true; }