RFC 2833 (telephone events) works partially.

git-svn-id: http://yate.null.ro/svn/yate/trunk@344 acf43c95-373e-0410-b603-e72c3f656dc1
This commit is contained in:
paulc 2005-05-10 20:26:01 +00:00
parent 5ecca7cde4
commit e3e02f6432
5 changed files with 286 additions and 100 deletions

View File

@ -46,10 +46,10 @@ bool RTPBaseIO::eventPayload(int type)
return false;
}
bool RTPBaseIO::ciscoPayload(int type)
bool RTPBaseIO::silencePayload(int type)
{
if ((type >= -1) && (type <= 127)) {
m_ciscoType = type;
m_silenceType = type;
return true;
}
return false;
@ -115,12 +115,12 @@ void RTPReceiver::rtcpData(const void* data, int len)
bool RTPReceiver::rtpRecv(bool marker, int payload, unsigned int timestamp, const void* data, int len)
{
if ((payload != dataPayload()) && (payload != eventPayload()))
if ((payload != dataPayload()) && (payload != eventPayload()) && (payload != silencePayload()))
rtpNewPayload(payload,timestamp);
if (payload == eventPayload())
return decodeEvent(marker,timestamp,data,len);
if (payload == ciscoPayload())
return decodeCisco(marker,timestamp,data,len);
if (payload == silencePayload())
return decodeSilence(marker,timestamp,data,len);
finishEvent(timestamp);
if (payload == dataPayload())
return rtpRecvData(marker,timestamp,data,len);
@ -129,16 +129,18 @@ bool RTPReceiver::rtpRecv(bool marker, int payload, unsigned int timestamp, cons
bool RTPReceiver::rtpRecvData(bool marker, unsigned int timestamp, const void* data, int len)
{
return false;
return m_session && m_session->rtpRecvData(marker,timestamp,data,len);
}
bool RTPReceiver::rtpRecvEvent(int event, char key, int duration, int volume, unsigned int timestamp)
{
return false;
return m_session && m_session->rtpRecvEvent(event,key,duration,volume,timestamp);
}
void RTPReceiver::rtpNewPayload(int payload, unsigned int timestamp)
{
if (m_session)
m_session->rtpNewPayload(payload,timestamp);
}
bool RTPReceiver::decodeEvent(bool marker, unsigned int timestamp, const void* data, int len)
@ -160,7 +162,7 @@ bool RTPReceiver::decodeEvent(bool marker, unsigned int timestamp, const void* d
m_evVol = vol;
}
bool RTPReceiver::decodeCisco(bool marker, unsigned int timestamp, const void* data, int len)
bool RTPReceiver::decodeSilence(bool marker, unsigned int timestamp, const void* data, int len)
{
return false;
}
@ -227,18 +229,23 @@ bool RTPSender::rtpSendData(bool marker, unsigned int timestamp, const void* dat
{
if (dataPayload() < 0)
return false;
if (sendEventData(timestamp))
return true;
return rtpSend(marker,dataPayload(),timestamp,data,len);
}
bool RTPSender::rtpSendEvent(int event, int duration, int volume, unsigned int timestamp)
{
// send as RFC2833 if we have the payload type set
if (eventPayload() >= 0) {
}
// else try FRF.11 Annex A (Cisco's way) if it's set up
else if ((ciscoPayload() >= 0) && (event <= 16)) {
}
return false;
if (eventPayload() < 0)
return false;
if ((duration <= 50) || (duration > 10000))
duration = 4000;
m_evTs = timestamp;
m_evNum = event;
m_evVol = volume;
m_evTime = duration;
return sendEventData(timestamp);
}
bool RTPSender::rtpSendKey(char key, int duration, int volume, unsigned int timestamp)
@ -261,6 +268,29 @@ bool RTPSender::rtpSendKey(char key, int duration, int volume, unsigned int time
return rtpSendEvent(event,duration,volume,timestamp);
}
bool RTPSender::sendEventData(unsigned int timestamp)
{
if (m_evTs) {
if (eventPayload() < 0) {
m_evTs = 0;
return false;
}
int duration = timestamp - m_evTs;
char buf[4];
buf[0] = m_evNum;
buf[1] = m_evVol & 0x7f;
buf[2] = duration >> 8;
buf[3] = duration & 0xff;
timestamp = m_evTs;
if (duration >= m_evTime) {
buf[1] |= 0x80;
m_evTs = 0;
}
return rtpSend(!duration,eventPayload(),timestamp,buf,sizeof(buf));
}
return false;
}
void RTPSender::timerTick(const Time& when)
{
}
@ -310,6 +340,26 @@ void RTPSession::rtcpData(const void* data, int len)
m_recv->rtcpData(data,len);
}
bool RTPSession::rtpRecvData(bool marker, unsigned int timestamp, const void* data, int len)
{
XDebug(DebugAll,"RTPSession::rtpRecv(%s,%u,%p,%d) [%p]",
String::boolText(marker),timestamp,data,len,this);
return false;
}
bool RTPSession::rtpRecvEvent(int event, char key, int duration, int volume, unsigned int timestamp)
{
XDebug(DebugAll,"RTPSession::rtpRecvEvent(%d,%02x,%d,%d,%u) [%p]",
event,key,duration,volume,timestamp,this);
return false;
}
void RTPSession::rtpNewPayload(int payload, unsigned int timestamp)
{
XDebug(DebugAll,"RTPSession::rtpNewPayload(%d,%u) [%p]",
payload,timestamp,this);
}
RTPSender* RTPSession::createSender()
{
return new RTPSender(this);
@ -351,7 +401,7 @@ bool RTPSession::initTransport()
void RTPSession::transport(RTPTransport* trans)
{
XDebug(DebugInfo,"RTPSession::transport(%p) old=%p [%p]",trans,m_transport,this);
DDebug(DebugInfo,"RTPSession::transport(%p) old=%p [%p]",trans,m_transport,this);
if (trans == m_transport)
return;
if (m_transport)
@ -365,7 +415,7 @@ void RTPSession::transport(RTPTransport* trans)
void RTPSession::sender(RTPSender* send)
{
XDebug(DebugInfo,"RTPSession::sender(%p) old=%p [%p]",send,m_send,this);
DDebug(DebugInfo,"RTPSession::sender(%p) old=%p [%p]",send,m_send,this);
if (send == m_send)
return;
RTPSender* tmp = m_send;
@ -376,7 +426,7 @@ void RTPSession::sender(RTPSender* send)
void RTPSession::receiver(RTPReceiver* recv)
{
XDebug(DebugInfo,"RTPSession::receiver(%p) old=%p [%p]",recv,m_recv,this);
DDebug(DebugInfo,"RTPSession::receiver(%p) old=%p [%p]",recv,m_recv,this);
if (recv == m_recv)
return;
RTPReceiver* tmp = m_recv;
@ -399,4 +449,34 @@ bool RTPSession::direction(Direction dir)
return true;
}
bool RTPSession::dataPayload(int type)
{
if (m_recv || m_send) {
DDebug(DebugInfo,"RTPSession::dataPayload(%d) [%p]",type,this);
bool ok = (!m_recv) || m_recv->dataPayload(type);
return ((!m_send) || m_send->dataPayload(type)) && ok;
}
return false;
}
bool RTPSession::eventPayload(int type)
{
if (m_recv || m_send) {
DDebug(DebugInfo,"RTPSession::eventPayload(%d) [%p]",type,this);
bool ok = (!m_recv) || m_recv->eventPayload(type);
return ((!m_send) || m_send->eventPayload(type)) && ok;
}
return false;
}
bool RTPSession::silencePayload(int type)
{
if (m_recv || m_send) {
DDebug(DebugInfo,"RTPSession::silencePayload(%d) [%p]",type,this);
bool ok = (!m_recv) || m_recv->silencePayload(type);
return ((!m_send) || m_send->silencePayload(type)) && ok;
}
return false;
}
/* vi: set ts=8 sw=4 sts=4 noet: */

View File

@ -67,7 +67,7 @@ void RTPGroup::run()
}
}
unlock();
yield(true);
Thread::msleep(1,true);
}
XDebug(DebugInfo,"RTPGroup::run() ran out of processors [%p]",this);
}

View File

@ -264,7 +264,7 @@ public:
*/
inline RTPBaseIO(RTPSession* session = 0)
: m_session(session), m_ssrc(0), m_ts(0), m_seq(0),
m_dataType(-1), m_eventType(-1), m_ciscoType(-1)
m_dataType(-1), m_eventType(-1), m_silenceType(-1)
{ }
/**
@ -296,19 +296,19 @@ public:
bool eventPayload(int type);
/**
* Get the payload type for Cisco event packets
* Get the payload type for Silence event packets
* @return Payload type, -1 if not set
*/
inline int ciscoPayload() const
{ return m_ciscoType; }
inline int silencePayload() const
{ return m_silenceType; }
/**
* Set the payload type for Cisco event packets.
* Thanks, Cisco, for a new and incompatible way of sending events.
* Set the payload type for Silence event packets.
* Thanks, Silence, for a new and incompatible way of sending events.
* @param type Payload type, -1 to disable
* @return True if changed, false if invalid payload type
*/
bool ciscoPayload(int type);
bool silencePayload(int type);
/**
* Reset the SSRC requesting generation/grabbing of a new one
@ -334,7 +334,7 @@ protected:
private:
int m_dataType;
int m_eventType;
int m_ciscoType;
int m_silenceType;
};
/**
@ -408,7 +408,7 @@ private:
void rtpData(const void* data, int len);
void rtcpData(const void* data, int len);
bool decodeEvent(bool marker, unsigned int timestamp, const void* data, int len);
bool decodeCisco(bool marker, unsigned int timestamp, const void* data, int len);
bool decodeSilence(bool marker, unsigned int timestamp, const void* data, int len);
void finishEvent(unsigned int timestamp);
bool pushEvent(int event, int duration, int volume, unsigned int timestamp);
};
@ -424,7 +424,7 @@ public:
* Constructor
*/
inline RTPSender(RTPSession* session = 0)
: RTPBaseIO(session)
: RTPBaseIO(session), m_evTime(0)
{ }
/**
@ -477,6 +477,9 @@ protected:
*/
virtual void timerTick(const Time& when);
private:
int m_evTime;
bool sendEventData(unsigned int timestamp);
};
/**
@ -517,6 +520,38 @@ public:
*/
virtual void rtcpData(const void* data, int len);
/**
* Process one RTP data packet
* @param marker Set to true if the marker bit is set
* @param timestamp Sampling instant of the packet data
* @param data Pointer to data block to process
* @param len Length of the data block in bytes
* @return True if data was handled
*/
virtual bool rtpRecvData(bool marker, unsigned int timestamp,
const void* data, int len);
/**
* Process one RTP event
* @param event Received event code
* @param key Received key (for events 0-16) or zero
* @param duration Duration of the event as number of samples
* @param volume Attenuation of the tone, zero for don't care
* @param timestamp Sampling instant of the initial packet data
* @return True if data was handled
*/
virtual bool rtpRecvEvent(int event, char key, int duration,
int volume, unsigned int timestamp);
/**
* Method called for unknown payload types just before attempting
* to call rtpRecvData(). This is a good opportunity to change the
* payload type and continue.
* @param payload Payload number
* @param timestamp Sampling instant of the unexpected packet data
*/
virtual void rtpNewPayload(int payload, unsigned int timestamp);
/**
* Create a new RTP sender for this session.
* Override this method to create objects derived from RTPSender.
@ -651,6 +686,27 @@ public:
*/
bool direction(Direction dir);
/**
* Set the data payload type for both receiver and sender.
* @param type Payload type, -1 to disable
* @return True if changed, false if invalid payload type
*/
bool dataPayload(int type);
/**
* Set the event payload type for both receiver and sender.
* @param type Payload type, -1 to disable
* @return True if changed, false if invalid payload type
*/
bool eventPayload(int type);
/**
* Set the silence payload type for both receiver and sender.
* @param type Payload type, -1 to disable
* @return True if changed, false if invalid payload type
*/
bool silencePayload(int type);
/**
* Set the local network address of the RTP transport of this session
* @param addr New local RTP transport address

View File

@ -35,13 +35,16 @@ using namespace TelEngine;
static const char s_helpmsg[] =
"Available commands:\n"
" debug [level|on|off]\n"
" machine [on|off]\n"
" quit\n"
" help [command]\n"
" status [module]\n"
" machine [on|off]\n"
" auth password\n"
"Authenticated commands:\n"
" debug [level|on|off]\n"
" drop {chan|*|all}\n"
" call chan target\n"
" reload\n"
" quit\n"
" stop [exitcode]\n";
static Configuration s_cfg;
@ -77,6 +80,7 @@ public:
{ return m_address; }
static Connection *checkCreate(Socket* sock, const char* addr = 0);
private:
bool m_auth;
bool m_debug;
bool m_machine;
Socket* m_socket;
@ -145,7 +149,7 @@ Connection *Connection::checkCreate(Socket* sock, const char* addr)
Connection::Connection(Socket* sock, const char* addr)
: Thread("RManager Connection"),
m_debug(false), m_machine(false), m_socket(sock), m_address(addr)
m_auth(false), m_debug(false), m_machine(false), m_socket(sock), m_address(addr)
{
connectionlist.append(this);
}
@ -174,6 +178,7 @@ void Connection::run()
Debug("RManager",DebugWarn, "Failed to set tcp socket to TCP_NODELAY mode: %s\n", strerror(m_socket->error()));
Output("Remote connection from %s",m_address.c_str());
m_auth = !s_cfg.getValue("general","password");
const char *hdr = s_cfg.getValue("general","header","YATE (http://YATE.null.ro) ready.");
if (hdr) {
writeStr(hdr);
@ -255,13 +260,60 @@ bool Connection::processLine(const char *line)
str = "%%+status" + str + "\n";
str << m.retValue() << "%%-status\n";
writeStr(str);
return false;
}
else if (str.startSkip("machine"))
{
str >> m_machine;
str = "Machine mode: ";
str += (m_machine ? "on\n" : "off\n");
writeStr(str);
return false;
}
else if (str.startSkip("quit"))
{
writeStr(m_machine ? "%%=quit\n" : "Goodbye!\n");
return true;
}
else if (str.startSkip("drop"))
else if (str.startSkip("help") || str.startSkip("?"))
{
Message m("engine.help");
if (!str.null())
{
m.addParam("line",str);
if (Engine::dispatch(m))
writeStr(m.retValue());
else
writeStr("No help for '"+str+"'\n");
}
else
{
m.retValue() = s_helpmsg;
Engine::dispatch(m);
writeStr(m.retValue());
}
return false;
}
else if (str.startSkip("auth"))
{
if (m_auth) {
writeStr(m_machine ? "%%=auth:success\n" : "You are already authenticated!\n");
return false;
}
if (str == s_cfg.getValue("general","password")) {
Output("Authenticated connection %s",m_address.c_str());
m_auth = true;
writeStr(m_machine ? "%%=auth:success\n" : "Authenticated successfully!\n");
}
else
writeStr(m_machine ? "%%=auth:fail=badpass\n" : "Bad authentication password!\n");
return false;
}
if (!m_auth) {
writeStr(m_machine ? "%%=*:fail=noauth\n" : "Not authenticated!\n");
return false;
}
if (str.startSkip("drop"))
{
if (str.null()) {
writeStr(m_machine ? "%%=drop:fail=noarg\n" : "You must specify what connection to drop!\n");
@ -342,13 +394,6 @@ bool Connection::processLine(const char *line)
}
writeStr(str);
}
else if (str.startSkip("machine"))
{
str >> m_machine;
str = "Machine mode: ";
str += (m_machine ? "on\n" : "off\n");
writeStr(str);
}
else if (str.startSkip("reload"))
{
writeStr(m_machine ? "%%=reload\n" : "Reinitializing...\n");
@ -361,24 +406,6 @@ bool Connection::processLine(const char *line)
writeStr(m_machine ? "%%=shutdown\n" : "Engine shutting down - bye!\n");
Engine::halt(code);
}
else if (str.startSkip("help") || str.startSkip("?"))
{
Message m("engine.help");
if (!str.null())
{
m.addParam("line",str);
if (Engine::dispatch(m))
writeStr(m.retValue());
else
writeStr("No help for '"+str+"'\n");
}
else
{
m.retValue() = s_helpmsg;
Engine::dispatch(m);
writeStr(m.retValue());
}
}
else
{
Message m("engine.command");

View File

@ -59,14 +59,15 @@ class YRTPWrapper : public RefObject
{
friend class YRTPAudioSource;
friend class YRTPAudioConsumer;
friend class YRTPSession;
public:
YRTPWrapper(const char *localip, CallEndpoint* conn = 0, RTPSession::Direction direction = RTPSession::SendRecv);
~YRTPWrapper();
void setupRTP(const char* localip);
bool startRTP(const char* raddr, unsigned int rport, int payload, const char* format);
bool sendDTMF(char dtmf);
bool startRTP(const char* raddr, unsigned int rport, int payload, int evpayload, const char* format);
bool sendDTMF(char dtmf, int duration = 0);
void gotDTMF(char tone);
inline RTPSession* rtp() const
inline YRTPSession* rtp() const
{ return m_rtp; }
inline RTPSession::Direction dir() const
{ return m_dir; }
@ -84,7 +85,7 @@ public:
static YRTPWrapper* find(const String& id);
static void guessLocal(const char* remoteip, String& localip);
private:
RTPSession* m_rtp;
YRTPSession* m_rtp;
RTPSession::Direction m_dir;
CallEndpoint* m_conn;
YRTPAudioSource* m_source;
@ -95,6 +96,21 @@ private:
unsigned int m_port;
};
class YRTPSession : public RTPSession
{
public:
inline YRTPSession(YRTPWrapper* wrap)
: m_wrap(wrap)
{ }
virtual bool rtpRecvData(bool marker, unsigned int timestamp,
const void* data, int len);
virtual bool rtpRecvEvent(int event, char key, int duration,
int volume, unsigned int timestamp);
virtual void rtpNewPayload(int payload, unsigned int timestamp);
private:
YRTPWrapper* m_wrap;
};
class YRTPAudioSource : public DataSource
{
friend class YRTPWrapper;
@ -174,6 +190,12 @@ YRTPWrapper::~YRTPWrapper()
lookup(m_dir,dict_yrtp_dir),this);
s_mutex.lock();
s_calls.remove(this,false);
if (m_rtp) {
Debug(DebugAll,"Cleaning up RTP %p",m_rtp);
YRTPSession* tmp = m_rtp;
m_rtp = 0;
delete tmp;
}
if (m_source) {
Debug(DebugGoOn,"There is still a RTP source %p [%p]",m_source,this);
m_source->destruct();
@ -184,12 +206,6 @@ YRTPWrapper::~YRTPWrapper()
m_consumer->destruct();
m_consumer = 0;
}
if (m_rtp) {
Debug(DebugAll,"Cleaning up RTP %p",m_rtp);
RTPSession* tmp = m_rtp;
m_rtp = 0;
delete tmp;
}
s_mutex.unlock();
}
@ -220,7 +236,7 @@ YRTPWrapper* YRTPWrapper::find(const String& id)
void YRTPWrapper::setupRTP(const char* localip)
{
Debug(DebugAll,"YRTPWrapper::setupRTP(\"%s\") [%p]",localip,this);
m_rtp = new RTPSession;
m_rtp = new YRTPSession(this);
m_rtp->initTransport();
int minport = s_cfg.getIntValue("rtp","minport",16384);
int maxport = s_cfg.getIntValue("rtp","maxport",32768);
@ -251,7 +267,7 @@ void YRTPWrapper::setupRTP(const char* localip)
Debug(DebugWarn,"YRTPWrapper [%p] RTP bind failed in range %d-%d",this,minport,maxport);
}
bool YRTPWrapper::startRTP(const char* raddr, unsigned int rport, int payload, const char* format)
bool YRTPWrapper::startRTP(const char* raddr, unsigned int rport, int payload, int evpayload, const char* format)
{
Debug(DebugAll,"YRTPWrapper::startRTP(\"%s\",%u,%d) [%p]",raddr,rport,payload,this);
if (!m_rtp) {
@ -290,20 +306,6 @@ bool YRTPWrapper::startRTP(const char* raddr, unsigned int rport, int payload, c
Debug(DebugWarn,"RTP failed to set remote address %s:%d [%p]",raddr,rport,this);
return false;
}
#if 0
::rtp_session_set_scheduling_mode(m_rtp, s_cfg.getBoolValue("rtp","scheduled",true)); /* yes */
if (::rtp_session_set_remote_addr(m_rtp, (gchar *)raddr, rport)) {
Debug(DebugWarn,"RTP failed to set remote address %s:%d [%p]",raddr,rport,this);
return false;
}
if (::rtp_session_set_payload_type(m_rtp, payload)) {
Debug(DebugWarn,"RTP failed to set payload type %d [%p]",payload,this);
return false;
}
::rtp_session_signal_connect(m_rtp,"telephone-event",(RtpCallback)tel_event_cb,this);
::rtp_session_signal_connect(m_rtp,"telephone-event_packet",(RtpCallback)tel_packet_cb,this);
::rtp_session_set_jitter_compensation(m_rtp, s_cfg.getIntValue("rtp","jitter",50));
#endif
// Change format of source and/or consumer,
// reinstall them to rebuild codec chains
if (m_source) {
@ -316,9 +318,6 @@ bool YRTPWrapper::startRTP(const char* raddr, unsigned int rport, int payload, c
m_conn->setSource(m_source);
m_source->deref();
}
#if 0
m_source->start("YRTP Source");
#endif
}
if (m_consumer) {
if (m_conn) {
@ -333,21 +332,15 @@ bool YRTPWrapper::startRTP(const char* raddr, unsigned int rport, int payload, c
}
if (!(m_rtp->initGroup() && m_rtp->direction(m_dir)))
return false;
if (m_rtp->receiver())
m_rtp->receiver()->dataPayload(payload);
if (m_rtp->sender())
m_rtp->sender()->dataPayload(payload);
m_rtp->dataPayload(payload);
m_rtp->eventPayload(evpayload);
m_bufsize = s_cfg.getIntValue("rtp","buffer",160);
return true;
}
bool YRTPWrapper::sendDTMF(char dtmf)
bool YRTPWrapper::sendDTMF(char dtmf, int duration)
{
if (m_rtp && m_consumer) {
m_rtp->rtpSendKey(dtmf,0);
return true;
}
return false;
return m_rtp && m_rtp->rtpSendKey(dtmf,duration);
}
void YRTPWrapper::gotDTMF(char tone)
@ -382,6 +375,35 @@ void YRTPWrapper::guessLocal(const char* remoteip, String& localip)
Debug(DebugInfo,"Guessed local IP '%s' for remote '%s'",localip.c_str(),remoteip);
}
bool YRTPSession::rtpRecvData(bool marker, unsigned int timestamp, const void* data, int len)
{
YRTPAudioSource* source = m_wrap ? m_wrap->m_source : 0;
if (!source)
return false;
DataBlock block;
block.assign((void*)data, len, false);
source->Forward(block);
block.clear(false);
return true;
}
bool YRTPSession::rtpRecvEvent(int event, char key, int duration,
int volume, unsigned int timestamp)
{
if (!(m_wrap && key))
return false;
m_wrap->gotDTMF(key);
return true;
}
void YRTPSession::rtpNewPayload(int payload, unsigned int timestamp)
{
if (payload == 13) {
Debug(DebugInfo,"Activating RTP silence payload %d in wrapper %p",payload,m_wrap);
m_wrap->rtp()->silencePayload(payload);
}
}
YRTPAudioSource::YRTPAudioSource(YRTPWrapper* wrap)
: m_wrap(wrap)
{
@ -546,7 +568,7 @@ bool AttachHandler::received(Message &msg)
String p(msg.getValue("payload"));
if (p.null())
p = msg.getValue("format");
w->startRTP(rip,rport.toInteger(),p.toInteger(dict_payloads,-1),msg.getValue("format"));
w->startRTP(rip,rport.toInteger(),p.toInteger(dict_payloads,-1),msg.getIntValue("evpayload",101),msg.getValue("format"));
}
msg.setParam("localip",lip);
msg.setParam("localport",String(w->port()));
@ -632,7 +654,7 @@ bool RtpHandler::received(Message &msg)
String p(msg.getValue("payload"));
if (p.null())
p = msg.getValue("format");
w->startRTP(rip,rport.toInteger(),p.toInteger(dict_payloads,-1),msg.getValue("format"));
w->startRTP(rip,rport.toInteger(),p.toInteger(dict_payloads,-1),msg.getIntValue("evpayload",101),msg.getValue("format"));
}
msg.setParam("localport",String(w->port()));
msg.setParam("rtpid",w->id());
@ -651,8 +673,9 @@ bool DTMFHandler::received(Message &msg)
YRTPWrapper* wrap = YRTPWrapper::find(targetid);
if (wrap && wrap->rtp()) {
Debug(DebugInfo,"RTP DTMF '%s' targetid '%s'",text.c_str(),targetid.c_str());
int duration = msg.getIntValue("duration");
for (unsigned int i=0;i<text.length();i++)
wrap->sendDTMF(text.at(i));
wrap->sendDTMF(text.at(i),duration);
return true;
}
return false;