855 lines
25 KiB
C++
855 lines
25 KiB
C++
/**
|
|
* conference.cpp
|
|
* This file is part of the YATE Project http://YATE.null.ro
|
|
*
|
|
* Conference room data mixer
|
|
*
|
|
* Yet Another Telephony Engine - a fully featured software PBX and IVR
|
|
* Copyright (C) 2004-2006 Null Team
|
|
*
|
|
* N-way mixing with self echo suppresion idea by Andrew McDonald
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include <yatephone.h>
|
|
|
|
using namespace TelEngine;
|
|
namespace { // anonymous
|
|
|
|
// size of the outgoing data blocks in bytes - divide by 2 to get samples
|
|
#define DATA_CHUNK 320
|
|
|
|
// minimum amount of buffered data when we start mixing
|
|
#define MIN_BUFFER 480
|
|
|
|
// maximum size we allow the buffer to grow
|
|
#define MAX_BUFFER 960
|
|
|
|
|
|
// Absolute maximum possible energy (square +-32767 wave) - do not change
|
|
#define ENERGY_MAX 1073676289
|
|
|
|
// Default / minimum noise threshold
|
|
#define ENERGY_MIN 256
|
|
|
|
// Attack / decay rates for computing average energy
|
|
#define DECAY_TOTAL 1000
|
|
#define DECAY_STORE 995
|
|
#define ATTACK_RATE (DECAY_TOTAL-DECAY_STORE)
|
|
|
|
// Shift for noise margin
|
|
#define SHIFT_LEVEL 5
|
|
// Shift for noise decay rate
|
|
#define SHIFT_RAISE 7
|
|
|
|
// some sanity checks
|
|
#if DECAY_TOTAL <= DECAY_STORE
|
|
#error DECAY_TOTAL must be higher than DECAY_STORE
|
|
#endif
|
|
|
|
#if SHIFT_LEVEL >= SHIFT_RAISE
|
|
#error SHIFT_RAISE must be higher than SHIFT_LEVEL
|
|
#endif
|
|
|
|
class ConfConsumer;
|
|
class ConfSource;
|
|
class ConfChan;
|
|
|
|
// The list of conference rooms
|
|
static ObjList s_rooms;
|
|
|
|
// Mutex that protects the source while accessed by the consumer
|
|
static Mutex s_srcMutex;
|
|
|
|
// Hold the number of the newest allocated dynamic room
|
|
static int s_roomAlloc = 0;
|
|
|
|
// The conference room holds a list of connected channels and does the mixing.
|
|
// It does also act as a data source for the sum of all channels
|
|
class ConfRoom : public DataSource
|
|
{
|
|
public:
|
|
virtual void destroyed();
|
|
static ConfRoom* get(const String& name, const NamedList* params = 0);
|
|
virtual const String& toString() const
|
|
{ return m_name; }
|
|
inline ObjList& channels()
|
|
{ return m_chans; }
|
|
inline int rate() const
|
|
{ return m_rate; }
|
|
inline int users() const
|
|
{ return m_users; }
|
|
inline bool full() const
|
|
{ return m_users >= m_maxusers; }
|
|
inline ConfChan* recorder() const
|
|
{ return m_record; }
|
|
void mix(ConfConsumer* cons = 0);
|
|
void addChannel(ConfChan* chan, bool player = false);
|
|
void delChannel(ConfChan* chan);
|
|
bool setRecording(const NamedList& params);
|
|
private:
|
|
ConfRoom(const String& name, const NamedList& params);
|
|
String m_name;
|
|
ObjList m_chans;
|
|
String m_notify;
|
|
String m_playerId;
|
|
bool m_lonely;
|
|
ConfChan* m_record;
|
|
int m_rate;
|
|
int m_users;
|
|
int m_maxusers;
|
|
};
|
|
|
|
// A conference channel is just a dumb holder of its data channels
|
|
class ConfChan : public Channel
|
|
{
|
|
YCLASS(ConfChan,Channel)
|
|
public:
|
|
ConfChan(const String& name, const NamedList& params);
|
|
ConfChan(ConfRoom* room, bool voice = false);
|
|
virtual ~ConfChan();
|
|
inline bool isUtility() const
|
|
{ return m_utility; }
|
|
inline bool isRecorder() const
|
|
{ return m_room && (m_room->recorder() == this); }
|
|
private:
|
|
RefPointer<ConfRoom> m_room;
|
|
bool m_utility;
|
|
bool m_billing;
|
|
};
|
|
|
|
// The data consumer computes energy and noise levels (if required) and
|
|
// triggers the mixing of data in the conference room
|
|
class ConfConsumer : public DataConsumer
|
|
{
|
|
friend class ConfRoom;
|
|
friend class ConfChan;
|
|
friend class ConfSource;
|
|
public:
|
|
ConfConsumer(ConfRoom* room, bool smart = false)
|
|
: m_room(room), m_src(0), m_muted(false), m_smart(smart),
|
|
m_energy2(ENERGY_MIN), m_noise2(ENERGY_MIN)
|
|
{ }
|
|
~ConfConsumer()
|
|
{ }
|
|
virtual void Consume(const DataBlock& data, unsigned long tStamp);
|
|
unsigned int energy() const;
|
|
unsigned int noise() const;
|
|
inline unsigned int energy2() const
|
|
{ return m_energy2; }
|
|
inline unsigned int noise2() const
|
|
{ return m_noise2; }
|
|
inline bool muted() const
|
|
{ return m_muted; }
|
|
inline bool hasSignal() const
|
|
{ return (!m_muted) && (m_buffer.length() > 1) && (m_energy2 >= m_noise2); }
|
|
private:
|
|
void consumed(const int* mixed, unsigned int samples);
|
|
void dataForward(const int* mixed, unsigned int samples);
|
|
ConfRoom* m_room;
|
|
ConfSource* m_src;
|
|
bool m_muted;
|
|
bool m_smart;
|
|
unsigned int m_energy2;
|
|
unsigned int m_noise2;
|
|
DataBlock m_buffer;
|
|
};
|
|
|
|
// Per channel data source with that channel's data removed from the mix
|
|
class ConfSource : public DataSource
|
|
{
|
|
friend class ConfChan;
|
|
public:
|
|
ConfSource(ConfConsumer* cons);
|
|
~ConfSource();
|
|
private:
|
|
RefPointer<ConfConsumer> m_cons;
|
|
};
|
|
|
|
// Handler for call.conference message to join both legs of a call in conference
|
|
class ConfHandler : public MessageHandler
|
|
{
|
|
public:
|
|
inline ConfHandler(unsigned int priority)
|
|
: MessageHandler("call.conference",priority)
|
|
{ }
|
|
virtual ~ConfHandler()
|
|
{ }
|
|
virtual bool received(Message& msg);
|
|
};
|
|
|
|
// The driver just holds all the channels (not conferences)
|
|
class ConferenceDriver : public Driver
|
|
{
|
|
public:
|
|
ConferenceDriver();
|
|
virtual ~ConferenceDriver();
|
|
bool checkRoom(String& room, bool existing);
|
|
protected:
|
|
virtual void initialize();
|
|
virtual bool msgExecute(Message& msg, String& dest);
|
|
virtual void statusParams(String& str);
|
|
private:
|
|
bool m_init;
|
|
};
|
|
|
|
INIT_PLUGIN(ConferenceDriver);
|
|
|
|
// Count the position of the most significant 1 bit - pretty close to logarithm
|
|
static unsigned int binLog(unsigned int x)
|
|
{
|
|
unsigned int v = 0;
|
|
while (x >>= 1)
|
|
v++;
|
|
return v;
|
|
}
|
|
|
|
|
|
// Get a pointer to a conference by name, optionally creates it with given parameters
|
|
// If a pointer is returned it must be dereferenced by the caller
|
|
// Thread safe
|
|
ConfRoom* ConfRoom::get(const String& name, const NamedList* params)
|
|
{
|
|
if (name.null())
|
|
return 0;
|
|
Lock lock(&__plugin);
|
|
ObjList* l = s_rooms.find(name);
|
|
ConfRoom* room = l ? static_cast<ConfRoom*>(l->get()) : 0;
|
|
if (room && !room->ref())
|
|
room = 0;
|
|
if (params && !room)
|
|
room = new ConfRoom(name,*params);
|
|
return room;
|
|
}
|
|
|
|
// Private constructor, always called from ConfRoom::get() with mutex hold
|
|
ConfRoom::ConfRoom(const String& name, const NamedList& params)
|
|
: m_name(name), m_lonely(false), m_record(0),
|
|
m_rate(8000), m_users(0), m_maxusers(10)
|
|
{
|
|
DDebug(&__plugin,DebugAll,"ConfRoom::ConfRoom('%s',%p) [%p]",
|
|
name.c_str(),¶ms,this);
|
|
m_rate = params.getIntValue("rate",m_rate);
|
|
m_maxusers = params.getIntValue("maxusers",m_maxusers);
|
|
m_notify = params.getValue("notify");
|
|
m_lonely = params.getBoolValue("lonely");
|
|
s_rooms.append(this);
|
|
// possibly create outgoing call to room record utility channel
|
|
setRecording(params);
|
|
// emit room creation notification
|
|
if (m_notify) {
|
|
Message* m = new Message("chan.notify");
|
|
m->userData(this);
|
|
m->addParam("targetid",m_notify);
|
|
m->addParam("event","created");
|
|
m->addParam("room",m_name);
|
|
m->addParam("caller",params.getValue("caller"));
|
|
m->addParam("called",params.getValue("called"));
|
|
m->addParam("billid",params.getValue("billid"));
|
|
m->addParam("username",params.getValue("username"));
|
|
Engine::enqueue(m);
|
|
}
|
|
}
|
|
|
|
void ConfRoom::destroyed()
|
|
{
|
|
DDebug(&__plugin,DebugAll,"ConfRoom::destroyed() '%s' [%p]",m_name.c_str(),this);
|
|
// plugin must be locked as the destructor is called when room is dereferenced
|
|
Lock lock(&__plugin);
|
|
s_rooms.remove(this,false);
|
|
m_chans.clear();
|
|
if (m_notify) {
|
|
Message* m = new Message("chan.notify");
|
|
m->addParam("targetid",m_notify);
|
|
m->addParam("event","destroyed");
|
|
m->addParam("room",m_name);
|
|
Engine::enqueue(m);
|
|
}
|
|
DataSource::destroyed();
|
|
}
|
|
|
|
// Add one channel to the room
|
|
void ConfRoom::addChannel(ConfChan* chan, bool player)
|
|
{
|
|
if (!chan)
|
|
return;
|
|
m_chans.append(chan);
|
|
if (player)
|
|
m_playerId = chan->id();
|
|
if (!chan->isUtility()) {
|
|
m_users++;
|
|
if (m_notify) {
|
|
String tmp(m_users);
|
|
Message* m = new Message("chan.notify");
|
|
m->addParam("id",chan->id());
|
|
m->addParam("targetid",m_notify);
|
|
m->addParam("event","joined");
|
|
m->addParam("room",m_name);
|
|
m->addParam("users",tmp);
|
|
if (m_playerId)
|
|
m->addParam("player",m_playerId);
|
|
Engine::enqueue(m);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove one channel from the room
|
|
void ConfRoom::delChannel(ConfChan* chan)
|
|
{
|
|
if (!chan)
|
|
return;
|
|
Lock lock(mutex());
|
|
if (chan == m_record)
|
|
m_record = 0;
|
|
if (m_playerId && (chan->id() == m_playerId))
|
|
m_playerId.clear();
|
|
if (m_chans.remove(chan,false) && !chan->isUtility()) {
|
|
m_users--;
|
|
bool alone = (m_users == 1);
|
|
lock.drop();
|
|
if (m_notify) {
|
|
String tmp(m_users);
|
|
Message* m = new Message("chan.notify");
|
|
m->addParam("id",chan->id());
|
|
m->addParam("targetid",m_notify);
|
|
m->addParam("event","left");
|
|
m->addParam("room",m_name);
|
|
m->addParam("users",tmp);
|
|
// easy to check parameter indicating one user will be left alone
|
|
if (m_lonely)
|
|
m->addParam("lonely",String::boolText(alone));
|
|
if (m_playerId)
|
|
m->addParam("player",m_playerId);
|
|
Engine::enqueue(m);
|
|
}
|
|
|
|
// cleanup if there are only 1 or 0 (if lonely==true) real users left
|
|
if (m_users <= (m_lonely ? 0 : 1)) {
|
|
// all channels left are utility or the lonely user - drop them
|
|
// make sure we continue to exist at least as long as the iterator
|
|
if (!ref())
|
|
return;
|
|
ListIterator iter(m_chans);
|
|
while (ConfChan* ch = static_cast<ConfChan*>(iter.get()))
|
|
ch->disconnect("hangup");
|
|
deref();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create or stop outgoing call to room record utility channel
|
|
bool ConfRoom::setRecording(const NamedList& params)
|
|
{
|
|
const String* record = params.getParam("record");
|
|
// only do something if the "record" parameter is present
|
|
if (!record)
|
|
return false;
|
|
// keep us safe - we may drop the recording of a lonely channel
|
|
if (!ref())
|
|
return false;
|
|
|
|
// stop any old recording channel
|
|
ConfChan* ch = m_record;
|
|
m_record = 0;
|
|
if (ch) {
|
|
DDebug(&__plugin,DebugCall,"Stopping record leg '%s'",ch->id().c_str());
|
|
ch->disconnect(params.getValue("reason","hangup"));
|
|
}
|
|
// create recorder if "record" is anything but "", "no", "false" or "disable"
|
|
if (*record && (*record != "-") && record->toBoolean(true)) {
|
|
String warn = params.getValue("recordwarn");
|
|
ch = new ConfChan(this,!warn.null());
|
|
DDebug(&__plugin,DebugCall,"Starting record leg '%s' to '%s'",
|
|
ch->id().c_str(),record->c_str());
|
|
Message* m = ch->message("call.execute");
|
|
m->userData(ch);
|
|
m->setParam("callto",*record);
|
|
m->setParam("cdrtrack",String::boolText(false));
|
|
m->setParam("caller",params.getValue("caller"));
|
|
m->setParam("called",params.getValue("called"));
|
|
m->setParam("billid",params.getValue("billid"));
|
|
m->setParam("username",params.getValue("username"));
|
|
m->addParam("room",m_name);
|
|
Engine::enqueue(m);
|
|
if (warn) {
|
|
// play record warning to the entire conference
|
|
m = ch->message("chan.attach",true,true);
|
|
m->addParam("override",warn);
|
|
m->addParam("single",String::boolText(true));
|
|
m->addParam("room",m_name);
|
|
Engine::enqueue(m);
|
|
}
|
|
else
|
|
Debug(&__plugin,DebugNote,"Recording '%s' without playing tone!",m_name.c_str());
|
|
if (m_notify) {
|
|
m = ch->message("chan.notify",true,true);
|
|
m->addParam("targetid",m_notify);
|
|
m->addParam("event","recording");
|
|
m->addParam("room",m_name);
|
|
m->addParam("record",*record);
|
|
Engine::enqueue(m);
|
|
}
|
|
m_record = ch;
|
|
ch->deref();
|
|
}
|
|
|
|
deref();
|
|
return true;
|
|
}
|
|
|
|
// Mix in buffered data from all channels, only if we have enough in buffer
|
|
void ConfRoom::mix(ConfConsumer* cons)
|
|
{
|
|
unsigned int len = MAX_BUFFER;
|
|
unsigned int mlen = 0;
|
|
Lock mylock(mutex());
|
|
// find out the minimum and maximum amount of data in buffers
|
|
ObjList* l = m_chans.skipNull();
|
|
for (; l; l = l->skipNext()) {
|
|
ConfChan* ch = static_cast<ConfChan*>(l->get());
|
|
ConfConsumer* co = static_cast<ConfConsumer*>(ch->getConsumer());
|
|
if (co) {
|
|
unsigned int buffered = co->m_buffer.length();
|
|
if (len > buffered)
|
|
len = buffered;
|
|
if (mlen < buffered)
|
|
mlen = buffered;
|
|
}
|
|
}
|
|
XDebug(DebugAll,"ConfRoom::mix() buffer %u - %u [%p]",len,mlen,this);
|
|
mlen += MIN_BUFFER;
|
|
// do we have at least minimum amount of data in buffer?
|
|
if (mlen <= MAX_BUFFER)
|
|
return;
|
|
mlen -= MAX_BUFFER;
|
|
// make sure we mix in enough data to prevent channels from overflowing
|
|
if (len < mlen)
|
|
len = mlen;
|
|
unsigned int chunks = len / DATA_CHUNK;
|
|
if (!chunks)
|
|
return;
|
|
len = chunks * DATA_CHUNK / sizeof(int16_t);
|
|
DataBlock mixbuf(0,len*sizeof(int));
|
|
int* buf = (int*)mixbuf.data();
|
|
for (l = m_chans.skipNull(); l; l = l->skipNext()) {
|
|
ConfChan* ch = static_cast<ConfChan*>(l->get());
|
|
ConfConsumer* co = static_cast<ConfConsumer*>(ch->getConsumer());
|
|
if (co) {
|
|
// avoid mixing in noise
|
|
if (co->hasSignal()) {
|
|
unsigned int n = co->m_buffer.length() / 2;
|
|
XDebug(ch,DebugAll,"Cons %p samp=%u |%s%s>",
|
|
co,n,String('#',co->noise()).safe(),
|
|
String('=',co->energy() - co->noise()).safe());
|
|
if (n > len)
|
|
n = len;
|
|
const int16_t* p = (const int16_t*)co->m_buffer.data();
|
|
for (unsigned int i=0; i < n; i++)
|
|
buf[i] += *p++;
|
|
}
|
|
}
|
|
}
|
|
// we finished mixing - notify consumers about it
|
|
for (l = m_chans.skipNull(); l; l = l->skipNext()) {
|
|
ConfChan* ch = static_cast<ConfChan*>(l->get());
|
|
ConfConsumer* co = static_cast<ConfConsumer*>(ch->getConsumer());
|
|
if (co)
|
|
co->consumed(buf,len);
|
|
}
|
|
DataBlock data(0,len*sizeof(int16_t));
|
|
int16_t* p = (int16_t*)data.data();
|
|
for (unsigned int i=0; i < len; i++) {
|
|
int val = buf[i];
|
|
// saturate symmetrically the result of addition
|
|
*p++ = (val < -32767) ? -32767 : ((val > 32767) ? 32767 : val);
|
|
}
|
|
mixbuf.clear();
|
|
mylock.drop();
|
|
Forward(data);
|
|
}
|
|
|
|
|
|
// Compute the energy level and noise threshold, store the data and call mixer
|
|
void ConfConsumer::Consume(const DataBlock& data, unsigned long tStamp)
|
|
{
|
|
if (m_muted || data.null() || !m_room)
|
|
return;
|
|
if (m_smart) {
|
|
// we need to compute the average energy and take decay into account
|
|
int64_t sum2 = m_energy2;
|
|
unsigned int min2 = ENERGY_MAX;
|
|
unsigned int n = data.length() / 2;
|
|
const int16_t* p = (const int16_t*)data.data();
|
|
for (unsigned int i=0; i < n; i++) {
|
|
int32_t samp = *p++;
|
|
// use square of the energy as extracting the square root is expensive
|
|
sum2 = (sum2 * DECAY_STORE + (int64_t)(samp*samp) * ATTACK_RATE) / DECAY_TOTAL;
|
|
if (min2 > sum2)
|
|
min2 = (unsigned int)sum2;
|
|
}
|
|
m_energy2 = (unsigned int)sum2;
|
|
// TODO: find a better algorithm to adjust the noise threshold
|
|
min2 += min2 >> SHIFT_LEVEL;
|
|
// try to keep noise threshold slightly above minimum energy
|
|
if (m_noise2 > min2)
|
|
m_noise2 = min2;
|
|
else
|
|
m_noise2 += 1 + (m_noise2 >> SHIFT_RAISE);
|
|
// but never below our arbitrary absolute minimum
|
|
if (m_noise2 < ENERGY_MIN)
|
|
m_noise2 = ENERGY_MIN;
|
|
}
|
|
// make sure looping back conferences is not fatal
|
|
if (!m_room->mutex()->lock(150000)) {
|
|
Debug(&__plugin,DebugWarn,"Failed to lock room '%s' - data loopback? [%p]",
|
|
m_room->toString().c_str(),this);
|
|
// mute the channel to avoid getting back here
|
|
m_muted = true;
|
|
return;
|
|
}
|
|
if (m_buffer.length()+data.length() <= MAX_BUFFER)
|
|
m_buffer += data;
|
|
m_room->mutex()->unlock();
|
|
if (m_buffer.length() >= MIN_BUFFER)
|
|
m_room->mix(this);
|
|
}
|
|
|
|
// Take out of the buffer the samples mixed in or skipped
|
|
// this method is called with the room locked
|
|
void ConfConsumer::consumed(const int* mixed, unsigned int samples)
|
|
{
|
|
if (!samples)
|
|
return;
|
|
dataForward(mixed,samples);
|
|
unsigned int n = m_buffer.length() / 2;
|
|
if (samples > n) {
|
|
// buffer underflowed
|
|
m_buffer.clear();
|
|
if (m_smart) {
|
|
// artificially decay for missing samples
|
|
n = samples - n;
|
|
int64_t sum2 = m_energy2;
|
|
while (n--)
|
|
sum2 = (sum2 * DECAY_STORE) / DECAY_TOTAL;
|
|
m_energy2 = (unsigned int)sum2;
|
|
}
|
|
return;
|
|
}
|
|
samples *= sizeof(int16_t);
|
|
m_buffer.cut(-(int)samples);
|
|
}
|
|
|
|
// Substract our own data from the mix and send it on the no-echo source
|
|
void ConfConsumer::dataForward(const int* mixed, unsigned int samples)
|
|
{
|
|
if (!(m_src && mixed))
|
|
return;
|
|
// static lock is used while we reference the source
|
|
s_srcMutex.lock();
|
|
RefPointer<ConfSource> src = m_src;
|
|
s_srcMutex.unlock();
|
|
if (!src)
|
|
return;
|
|
|
|
int16_t* d = (int16_t*)m_buffer.data();
|
|
unsigned int n = m_buffer.length() / 2;
|
|
DataBlock data(0,samples*sizeof(int16_t));
|
|
int16_t* p = (int16_t*)data.data();
|
|
for (unsigned int i=0; i < samples; i++) {
|
|
int val = *mixed++;
|
|
// substract our own data if we contributed - only as much as we have
|
|
if ((i < n) && hasSignal())
|
|
val -= d[i];
|
|
// saturate symmetrically the result of additions and substraction
|
|
*p++ = (val < -32767) ? -32767 : ((val > 32767) ? 32767 : val);
|
|
}
|
|
src->Forward(data);
|
|
}
|
|
|
|
unsigned int ConfConsumer::energy() const
|
|
{
|
|
return binLog(m_energy2);
|
|
}
|
|
|
|
unsigned int ConfConsumer::noise() const
|
|
{
|
|
return binLog(m_noise2);
|
|
}
|
|
|
|
|
|
ConfSource::ConfSource(ConfConsumer* cons)
|
|
: m_cons(cons)
|
|
{
|
|
if (m_cons)
|
|
m_cons->m_src = this;
|
|
}
|
|
|
|
ConfSource::~ConfSource()
|
|
{
|
|
if (m_cons) {
|
|
s_srcMutex.lock();
|
|
m_cons->m_src = 0;
|
|
s_srcMutex.unlock();
|
|
}
|
|
}
|
|
|
|
|
|
// Constructor of a new conference leg, creates or attaches to an existing
|
|
// conference room; noise and echo suppression are also set here
|
|
ConfChan::ConfChan(const String& name, const NamedList& params)
|
|
: Channel(__plugin,0,true), m_utility(true), m_billing(false)
|
|
{
|
|
DDebug(this,DebugAll,"ConfChan::ConfChan(%s,%p) %s [%p]",
|
|
name.c_str(),¶ms,id().c_str(),this);
|
|
// much of the defaults depend if this is an utility channel or not
|
|
m_utility = params.getBoolValue("utility",false);
|
|
m_billing = params.getBoolValue("billing",false);
|
|
bool smart = params.getBoolValue("smart",!m_utility);
|
|
bool echo = params.getBoolValue("echo",m_utility);
|
|
bool voice = params.getBoolValue("voice",true);
|
|
m_room = ConfRoom::get(name,¶ms);
|
|
if (m_room) {
|
|
m_address = name;
|
|
m_room->addChannel(this,params.getBoolValue("player",false));
|
|
RefPointer<ConfConsumer> cons;
|
|
if (voice) {
|
|
cons = new ConfConsumer(m_room,smart);
|
|
setConsumer(cons);
|
|
cons->deref();
|
|
}
|
|
if (echo || !cons)
|
|
setSource(m_room);
|
|
else {
|
|
ConfSource* src = new ConfSource(cons);
|
|
setSource(src);
|
|
src->deref();
|
|
}
|
|
// no need to keep it referenced - m_room wil do it automatically
|
|
m_room->deref();
|
|
}
|
|
if (m_billing) {
|
|
Message* s = message("chan.startup",params);
|
|
s->setParam("caller",params.getValue("caller"));
|
|
s->setParam("called",params.getValue("called"));
|
|
s->setParam("billid",params.getValue("billid"));
|
|
s->setParam("username",params.getValue("username"));
|
|
Engine::enqueue(s);
|
|
}
|
|
}
|
|
|
|
// Constructor of an utility conference leg (incoming call)
|
|
ConfChan::ConfChan(ConfRoom* room, bool voice)
|
|
: Channel(__plugin), m_utility(true), m_billing(false)
|
|
{
|
|
DDebug(this,DebugAll,"ConfChan::ConfChan(%p,%s) %s [%p]",
|
|
room,String::boolText(voice),id().c_str(),this);
|
|
m_room = room;
|
|
if (m_room) {
|
|
m_address = m_room->toString();
|
|
m_room->addChannel(this);
|
|
if (voice) {
|
|
ConfConsumer* cons = new ConfConsumer(m_room);
|
|
setConsumer(cons);
|
|
cons->deref();
|
|
}
|
|
setSource(m_room);
|
|
}
|
|
}
|
|
|
|
// Destructor - remove itself from room and optionally emit chan.hangup
|
|
ConfChan::~ConfChan()
|
|
{
|
|
DDebug(this,DebugAll,"ConfChan::~ConfChan() %s [%p]",id().c_str(),this);
|
|
Lock lock(&__plugin);
|
|
// keep the room referenced until we are done
|
|
RefPointer<ConfRoom> room = m_room;
|
|
// remove ourselves from the room's mixer
|
|
if (room)
|
|
room->delChannel(this);
|
|
// now we can safely remove the data streams
|
|
clearEndpoint();
|
|
if (m_billing)
|
|
Engine::enqueue(message("chan.hangup"));
|
|
}
|
|
|
|
|
|
bool ConfHandler::received(Message& msg)
|
|
{
|
|
String room = msg.getValue("room");
|
|
// if a room name is provided it must be like room/SOMETHING
|
|
if (room && (!room.startSkip(__plugin.prefix(),false) || room.null()))
|
|
return false;
|
|
// we don't need a RefPointer for this one as the message keeps it referenced
|
|
CallEndpoint* chan = YOBJECT(CallEndpoint,msg.userData());
|
|
if (!chan) {
|
|
bool ok = false;
|
|
ConfRoom* cr = ConfRoom::get(room);
|
|
if (cr) {
|
|
ok = cr->setRecording(msg);
|
|
cr->deref();
|
|
}
|
|
if (!ok)
|
|
Debug(&__plugin,DebugNote,"Conference request with no channel!");
|
|
return ok;
|
|
}
|
|
if (chan->getObject("ConfChan")) {
|
|
Debug(&__plugin,DebugWarn,"Conference request from a conference leg!");
|
|
return false;
|
|
}
|
|
|
|
if (!__plugin.checkRoom(room,msg.getBoolValue("existing")))
|
|
return false;
|
|
|
|
const char* reason = msg.getValue("reason","conference");
|
|
|
|
RefPointer<CallEndpoint> peer = chan->getPeer();
|
|
if (peer) {
|
|
ConfChan* conf = YOBJECT(ConfChan,peer);
|
|
if (conf) {
|
|
// caller's peer is already a conference - check if the same
|
|
if (conf->address() == room) {
|
|
Debug(&__plugin,DebugNote,"Do-nothing conference request to the same room");
|
|
return true;
|
|
}
|
|
// not same - we just drop old conference leg
|
|
peer = 0;
|
|
}
|
|
}
|
|
|
|
// create a conference leg or even a room for the caller
|
|
ConfChan *c = new ConfChan(room,msg);
|
|
if (chan->connect(c,reason,false)) {
|
|
msg.setParam("peerid",c->id());
|
|
c->deref();
|
|
msg.setParam("room",__plugin.prefix()+room);
|
|
if (peer) {
|
|
// create a conference leg for the old peer too
|
|
ConfChan *p = new ConfChan(room,msg);
|
|
peer->connect(p,reason,false);
|
|
p->deref();
|
|
}
|
|
return true;
|
|
}
|
|
c->destruct();
|
|
return false;
|
|
}
|
|
|
|
|
|
/* "call.execute" message
|
|
Input parameters - per room:
|
|
"callto" - must be of the form "conf/NAME" where NAME is the name of
|
|
the conference; an empty name will be replaced with a random one
|
|
"existing" - set to true to always attach to an existing conference;
|
|
the call will fail if a conference with that name doesn't exist
|
|
"maxusers" - maximum number of users allowed to connect to this
|
|
conference, not counting utility channels
|
|
"lonely" - set to true to allow lonely users to remain in conference
|
|
else they will be disconnected
|
|
"notify" - ID used for "chan.notify" room notifications, an empty
|
|
string (default) will disable notifications
|
|
"record" - route that will make an outgoing record-only call
|
|
Input parameters - per conference leg:
|
|
"utility" - true creates a channel that is used for housekeeping
|
|
tasks like recording or playing prompts to everybody
|
|
"voice" - set to false to have the conference leg just listen to the
|
|
voice mix without being able to talk
|
|
"smart" - set to false to disable energy and noise level calculation
|
|
"echo" - set to true to hear back the voice this channel has injected
|
|
in the conference
|
|
"billing" - set to true to generate "chan.startup" and "chan.hangup"
|
|
messages for billing purposes
|
|
Return parameters:
|
|
"room" - name of the conference room in the form "conf/NAME"
|
|
"peerid" - channel ID of the conference leg
|
|
*/
|
|
|
|
// Handle call.execute by creating or attaching to an existing conference
|
|
bool ConferenceDriver::msgExecute(Message& msg, String& dest)
|
|
{
|
|
if (!checkRoom(dest,msg.getBoolValue("existing")))
|
|
return false;
|
|
CallEndpoint* ch = static_cast<CallEndpoint*>(msg.userData());
|
|
if (ch) {
|
|
ConfChan *c = new ConfChan(dest,msg);
|
|
if (ch->connect(c)) {
|
|
msg.setParam("peerid",c->id());
|
|
c->deref();
|
|
msg.setParam("room",prefix()+dest);
|
|
return true;
|
|
}
|
|
else {
|
|
c->destruct();
|
|
return false;
|
|
}
|
|
}
|
|
// conference will never make outgoing calls
|
|
Debug(DebugWarn,"Conference call with no call endpoint!");
|
|
return false;
|
|
}
|
|
|
|
// Check if a room exists, allocates a new room name if not and asked so
|
|
bool ConferenceDriver::checkRoom(String& room, bool existing)
|
|
{
|
|
ConfRoom* conf = ConfRoom::get(room);
|
|
if (existing && !conf)
|
|
return false;
|
|
if (conf) {
|
|
bool ok = !conf->full();
|
|
conf->deref();
|
|
return ok;
|
|
}
|
|
if (room.null()) {
|
|
// allocate an atomically incremented room number
|
|
lock();
|
|
room << "x-" << ++s_roomAlloc;
|
|
unlock();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ConferenceDriver::ConferenceDriver()
|
|
: Driver("conf","misc"), m_init(false)
|
|
{
|
|
Output("Loaded module Conference");
|
|
}
|
|
|
|
ConferenceDriver::~ConferenceDriver()
|
|
{
|
|
Output("Unloading module Conference");
|
|
s_rooms.clear();
|
|
}
|
|
|
|
void ConferenceDriver::statusParams(String& str)
|
|
{
|
|
Driver::statusParams(str);
|
|
str.append("rooms=",",") << s_rooms.count();
|
|
}
|
|
|
|
|
|
void ConferenceDriver::initialize()
|
|
{
|
|
Output("Initializing module Conference");
|
|
setup();
|
|
if (m_init)
|
|
return;
|
|
m_init = true;
|
|
Engine::install(new ConfHandler(150));
|
|
}
|
|
|
|
}; // anonymous namespace
|
|
|
|
/* vi: set ts=8 sw=4 sts=4 noet: */
|