package jpbx.plugins.core; import java.util.*; import javaforce.*; import javaforce.voip.*; import jpbx.core.*; /** Low-level plugin for handling INVITEs to IVR. */ public class IVR implements Plugin, DialChain, PBXEventHandler { public final static int pid = 15; //priority public static enum State { IVR_NONE, IVR_PLAY_MSG, IVR_GET_CHAR, IVR_GET_STRING, IVR_GOODBYE, IVR_TRANSFER, IVR_CONF } private PBXAPI api; //interface Plugin public void init(PBXAPI api) { this.api = api; api.hookDialChain(this); JFLog.log("IVR plugin init"); } public void uninit(PBXAPI api) { api.unhookDialChain(this); JFLog.log("IVR plugin uninit"); } public void install(PBXAPI api) { //nothing to do } public void uninstall(PBXAPI api) { //nothing to do } //interface DialChain public int getPriority() {return pid;} public int onInvite(CallDetailsPBX cd, SQL sql, boolean src) { //NOTE:There is no dst used in IVR (it's a one-sided call) if (!cd.authorized) { if (!cd.anon) return -1; } String ext = sql.select1value("SELECT ext FROM ivrs WHERE ext=" + sql.quote(cd.dialed)); if (ext == null) return -1; //an IVR is not being dialed if (cd.invited) { //reINVITE api.log(cd, "IVR : reINVITE"); SDP.Stream astream = cd.src.sdp.getFirstAudioStream(); cd.audioRelay.change_src(astream); cd.pbxsrc.sdp.getFirstAudioStream().codecs = astream.codecs; cd.sip.buildsdp(cd, cd.pbxsrc); api.reply(cd, 200, "OK", null, true, true); return pid; } String script = api.convertString(sql.select1value("SELECT script FROM ivrs WHERE ext=" + sql.quote(cd.dialed))); cd.ivrscript = script.replaceAll("\r", " ").replaceAll("\n", " ").replaceAll("\t", " ").replaceAll(">", " > ").replaceAll("<", " < ").replaceAll("=", " = ") .replaceAll("!", " ! ").replaceAll("[+]", " + ").split(" "); cd.ivrtag = 0; cd.pbxsrc.to = cd.src.to.clone(); cd.pbxsrc.from = cd.src.from.clone(); if (cd.audioRelay == null) { cd.audioRelay = new RTPRelay(); cd.audioRelay.init(); } cd.audioRelay.init(cd, this, api); cd.pbxsrc.host = cd.src.host; cd.pbxsrc.sdp = new SDP(); cd.pbxsrc.sdp.ip = api.getlocalhost(cd); SDP.Stream astream = cd.pbxsrc.sdp.addStream(SDP.Type.audio); astream.codecs = cd.src.sdp.getFirstAudioStream().codecs; astream.port = cd.audioRelay.getPort_src(); if (cd.src.sdp.hasVideo()) { if (cd.videoRelay == null) { cd.videoRelay = new RTPRelay(); cd.videoRelay.init(); } //do a test with video SDP.Stream vstream = cd.pbxsrc.sdp.addStream(SDP.Type.video); vstream.codecs = cd.src.sdp.getFirstVideoStream().codecs; vstream.port = cd.videoRelay.getPort_src(); vstream.mode = SDP.Mode.inactive; } cd.sip.buildsdp(cd, cd.pbxsrc); cd.invited = true; cd.connected = true; cd.pbxsrc.to = SIP.replacetag(cd.pbxsrc.to, SIP.generatetag()); //assign tag api.reply(cd, 200, "OK", null, true, true); start(cd, sql); return pid; } public void onRinging(CallDetailsPBX cd, SQL sql, boolean src) { } public void onSuccess(CallDetailsPBX cd, SQL sql, boolean src) { } public void onCancel(CallDetailsPBX cd, SQL sql, boolean src) { } public void onError(CallDetailsPBX cd, SQL sql, int code, boolean src) { } public void onTrying(CallDetailsPBX cd, SQL sql, boolean src) { } public void onBye(CallDetailsPBX cd, SQL sql, boolean src) { if (!src) return; api.reply(cd, 200, "OK", null, false, true); if (cd.audioRelay != null) { cd.audioRelay.uninit(); cd.audioRelay = null; } if (cd.ivrstate == State.IVR_CONF) { delMember(cd); } } public void onFeature(CallDetailsPBX cd, SQL sql, String cmd, String cmddata, boolean src) { } //interface PBXEventHandler public void event(CallDetailsPBX cd, int type, char digit, boolean interrupted) { //type = DTMF digit or end of playSound() JFLog.log("event:type=" + type + ":digit=" + digit + ":ivrstate=" + cd.ivrstate); switch (type) { case PBXEventHandler.DIGIT: cd.ivrstring.append(digit); switch (cd.ivrstate) { case IVR_PLAY_MSG: if (interrupted) { if (!cd.ivrint) break; } runScript(cd); break; case IVR_GET_CHAR: if (cd.ivrstring.length() > 0) { setvar(cd, cd.ivrvar, cd.ivrstring.substring(0, 1)); cd.ivrstring.delete(0,cd.ivrstring.length()); } else { setvar(cd, cd.ivrvar, " "); } runScript(cd); break; case IVR_GET_STRING: if ((digit == '#') || (digit == '*')) { int idx = cd.ivrstring.indexOf("#"); //assign it to $var setvar(cd, cd.ivrvar, cd.ivrstring.substring(0, idx)); flush(cd.ivrstring); runScript(cd); break; } break; } break; case PBXEventHandler.SOUND: switch (cd.ivrstate) { case IVR_PLAY_MSG: if (!interrupted) { runScript(cd); } break; case IVR_GET_CHAR: if (!interrupted) { setvar(cd, cd.ivrvar, " "); runScript(cd); } break; case IVR_GET_STRING: if (!interrupted) { setvar(cd, cd.ivrvar, cd.ivrstring.toString()); flush(cd.ivrstring); runScript(cd); } break; case IVR_CONF: switch (cd.confstate) { case CONF_DROP: hangup(cd); //end of 'conf-admin-left.wav' break; } } break; } } public void samples(CallDetailsPBX cd, short sam[]) { if (cd.ivrstate != State.IVR_CONF) { System.arraycopy(RTPRelay.silence, 0, sam, 0, 160); return; } Conference.Member member, myMember = cd.confmember; int actcnt = 0; int admincnt = 0; synchronized(Conference.lock) { Vector<Conference.Member> memberList = Conference.list.get(cd.dialed); for(int a=0;a<memberList.size();a++) { member = memberList.get(a); if (!member.dropped) { actcnt++; if (member.admin) admincnt++; } } //record samples for mixing System.arraycopy(sam, 0, myMember.buf[inc(myMember.idx)], 0, 160); myMember.idx = inc(myMember.idx); switch (cd.confstate) { case CONF_WAIT: System.arraycopy(RTPRelay.silence, 0, sam, 0, 160); //check if admin is present if (admincnt > 0) { cd.confstate = Conference.State.CONF_TALK; //reset all jitter idxs for(int a=0;a<memberList.size();a++) { member = memberList.get(a); myMember.idxs[a] = member.idx; } //add video if enabled if (cd.confVideo) { reInviteVideo(cd, myMember, memberList); for(int a=0;a<memberList.size();a++) { member = memberList.get(a); if (!member.cd.confVideo) continue; if (member.dropped) continue; reInviteVideo(member.cd, member, memberList); } } break; } if (cd.conftimer <= 0) { cd.audioRelay.playSound("conf-no-admin"); cd.conftimer = 60 * 8000 * 160; //1min of silence } else { cd.conftimer -= 160; } break; case CONF_TALK: if (admincnt == 0) { cd.confstate = Conference.State.CONF_DROP; } //mix samples System.arraycopy(RTPRelay.silence, 0, sam, 0, 160); for(int a=0;a<memberList.size();a++) { member = memberList.get(a); if (member == myMember) continue; //don't want to hear yourself if (myMember.idxs[a] == inc(member.idx)) { myMember.idxs[a] = dec(member.idx); //reset to head-1 } mix(sam, member.buf[myMember.idxs[a]]); myMember.idxs[a] = inc(myMember.idxs[a]); } break; case CONF_DROP: if (!myMember.dropped) { myMember.dropped = true; cd.audioRelay.playSound("conf-admin-left"); } break; } } } /* Increment buffer index */ private int inc(int in) { in++; if (in == Conference.bufs) { in = 0; } return in; } /* Decrement buffer index */ private int dec(int in) { in--; if (in == -1) { in = Conference.bufs-1; } return in; } private void mix(short out[], short in[]) { //mix 'in' into 'out' for(int a=0;a<160;a++) { out[a] += in[a]; } } //private code protected void start(CallDetailsPBX cd, SQL sql) { String lang = "en"; //TODO : query for ext cd.ivrstate = State.IVR_NONE; cd.ivrstring = new StringBuffer(); cd.ivrvars = new HashMap<String,String>(); cd.audioRelay.setLang(lang); cd.audioRelay.setRawMode(false); SDP.Stream stream = cd.src.sdp.getFirstAudioStream(); if (api.getExtension(cd.fromnumber) != null) { //NAT src stream.port = -1; } cd.audioRelay.start_src(stream); while (runScript(cd, sql)); } private String gettag(CallDetailsPBX cd) { int len = cd.ivrscript.length; if (cd.ivrtag == -1) return null; String tag; do { if (cd.ivrtag == len) return null; tag = cd.ivrscript[cd.ivrtag++]; } while (tag.length() == 0); return tag; } private String peektag(CallDetailsPBX cd) { String tag = gettag(cd); cd.ivrtag--; return tag; } private void setvar(CallDetailsPBX cd, String key, String value) { api.log(cd, "setvar:" + key + "=" + value); cd.ivrvars.put(key, value); } private String getvar(CallDetailsPBX cd, String key) { return cd.ivrvars.get(key); } private boolean runScript(CallDetailsPBX cd, SQL sql) { String tag = gettag(cd); if (tag.length() == 0) { api.log(cd, "IVR error : tag = zero length"); //should never happen return true; } if (tag == null) { api.log(cd, "IVR reached end of script"); hangup(cd); return false; } api.log(cd, "IVR:tag=" + tag); if (tag.charAt(0) == '$') { //assignment : $var = value String op = gettag(cd); if (op.equals("=")) { String value = gettag(cd); if (value.charAt(0) == '$') value = getvar(cd, value); String op2 = peektag(cd); if (op2.equals("+")) { op2 = gettag(cd); String value2 = gettag(cd); if (value2.charAt(0) == '$') value2 = getvar(cd, value2); value += value2; } setvar(cd, tag, value); } else { api.log(cd, "IVR:Unknown operation:"+tag+" "+op); hangup(cd); return false; } return true; } if (tag.equalsIgnoreCase("playmsg")) { cd.ivrstate = State.IVR_PLAY_MSG; cd.ivrint = true; String msg = gettag(cd); if (msg.charAt(0) == '$') msg = getvar(cd, msg); playSound(cd, msg); return false; } if (tag.equalsIgnoreCase("playmsgnoint")) { cd.ivrstate = State.IVR_PLAY_MSG; cd.ivrint = false; String msg = gettag(cd); if (msg.charAt(0) == '$') msg = getvar(cd, msg); playSound(cd, msg); return false; } if (tag.equalsIgnoreCase("getchar")) { cd.ivrvar = gettag(cd); if (cd.ivrstring.length() > 0) { //remove 1 char and assign it $var setvar(cd, cd.ivrvar, cd.ivrstring.substring(0, 1)); cd.ivrstring.delete(0,cd.ivrstring.length()); return true; } cd.ivrint = true; cd.ivrstate = State.IVR_GET_CHAR; playSound(cd, "vm-pause"); return false; } if (tag.equalsIgnoreCase("getstring")) { cd.ivrvar = gettag(cd); int idx = cd.ivrstring.indexOf("#"); if (idx != -1) { //assign it to $var setvar(cd, cd.ivrvar, cd.ivrstring.substring(0, idx)); flush(cd.ivrstring); return true; } cd.ivrint = true; cd.ivrstate = State.IVR_GET_STRING; playSound(cd, "vm-pause"); return false; } if (tag.equalsIgnoreCase("hangup")) { hangup(cd); return false; } if (tag.equalsIgnoreCase("goto")) { String target = gettag(cd); if (target.charAt(0) == '$') target = getvar(cd, target); cd.ivrtag = 0; do { tag = gettag(cd); if (tag.equals("label")) { tag = gettag(cd); if (tag.equalsIgnoreCase(target)) break; } } while (true); return true; } if (tag.equalsIgnoreCase("label")) { gettag(cd); //ignore next tag return true; } if (tag.equalsIgnoreCase("if")) { String v1 = gettag(cd); if (v1.charAt(0) == '$') v1 = getvar(cd, v1); String op = gettag(cd); String v2 = gettag(cd); if (v2.equals("=")) { op += v2; v2 = gettag(cd); } if (v2.charAt(0) == '$') v2 = getvar(cd, v2); boolean res = false; int i1, i2; if ((op.equals("==")) || (op.equals("="))) { res = v1.equalsIgnoreCase(v2); } else if (op.equals("!=")) { res = !v1.equalsIgnoreCase(v2); } else if (op.equals("<")) { i1 = Integer.valueOf(v1); i2 = Integer.valueOf(v2); res = i1 < i2; } else if (op.equals(">")) { i1 = Integer.valueOf(v1); i2 = Integer.valueOf(v2); res = i1 > i2; } else if (op.equals("<=")) { i1 = Integer.valueOf(v1); i2 = Integer.valueOf(v2); res = i1 <= i2; } else if (op.equals(">=")) { i1 = Integer.valueOf(v1); i2 = Integer.valueOf(v2); res = i1 >= i2; } else { api.log(cd, "IVR:Unknown operation:"+op); hangup(cd); return false; } if (!res) { //look for ENDIF while (!(gettag(cd).equalsIgnoreCase("endif"))); } return true; } if (tag.equalsIgnoreCase("endif")) { return true; } if (tag.equalsIgnoreCase("transfer")) { String target = gettag(cd); if (target.charAt(0) == '$') target = getvar(cd, target); cd.ivrstate = State.IVR_TRANSFER; api.transfer_src(cd, target); return false; } if (tag.equalsIgnoreCase("conf")) { //user enters conference mode tag = gettag(cd); if (tag.equals("admin")) { addMember(cd, true); } else if (tag.equals("user")) { addMember(cd, false); } else { api.log(cd, "IVR:conf command bad user type:"+tag); hangup(cd); return false; } return false; } if (tag.equalsIgnoreCase("enable")) { tag = gettag(cd); if (tag.equals("video")) { if (cd.src.sdp.getFirstVideoStream() != null) { cd.confVideo = true; } } return true; } if (tag.equalsIgnoreCase("disable")) { tag = gettag(cd); if (tag.equals("video")) cd.confVideo = false; return true; } api.log(cd, "IVR:Unknown command:"+tag); hangup(cd); return false; } private void runScript(CallDetailsPBX cd) { SQL sql = new SQL(); if (!sql.connect(Service.jdbc)) return; while (runScript(cd, sql)) {} sql.close(); } private void hangup(CallDetailsPBX cd) { try { cd.ivrtag = -1; cd.cmd = "BYE"; cd.pbxsrc.cseq++; JFLog.log("IVR:issuing BYE to:" + cd.user); api.issue(cd, null, false, true); JFLog.log("IVR:hangup:" + cd.user + ":state=" + cd.ivrstate); if (cd.ivrstate == State.IVR_CONF) { delMember(cd); } if (cd.audioRelay != null) { cd.audioRelay.uninit(); cd.audioRelay = null; } } catch (Exception e) { JFLog.log(e); } } private void flush(StringBuffer sb) { sb.delete(0,sb.length()); } private void playSound(CallDetailsPBX cd, String name) { cd.audioRelay.cleanup(); //stop cd.audioRelay.playSound(name); } /* Conference Code */ private void addMember(CallDetailsPBX cd, boolean admin) { JFLog.log("Conf:" + cd.dialed + ":add member:" + cd.user + ":admin:" + admin); Conference.Member member = new Conference.Member(); cd.confmember = member; member.cd = cd; member.admin = admin; int siz; synchronized(Conference.lock) { //check conf list for cd.dialed Vector<Conference.Member> memberList = Conference.list.get(cd.dialed); if (memberList == null) { //create new list memberList = new Vector<Conference.Member>(); Conference.list.put(cd.dialed, memberList); } member.memberList = memberList; memberList.add(member); //add new member to all members jitter idxs buffers siz = memberList.size(); for(int a=0;a<siz;a++) { member = memberList.get(a); if (member.idxs == null) { member.idxs = new int[siz]; } else { member.idxs = Arrays.copyOf(member.idxs, siz); } } } cd.confstate = Conference.State.CONF_WAIT; cd.ivrstate = State.IVR_CONF; JFLog.log("Conf:" + cd.dialed + ":add member:" + cd.user + ":admin:" + admin + ":size=" + siz); } private void delMember(CallDetailsPBX cd) { JFLog.log("Conf:" + cd.dialed + ":del member:" + cd.user + ":admin:" + cd.confmember.admin); if (cd.confmember.dropped) { JFLog.log("Conf:" + cd.dialed + ":del member:" + cd.user + ":admin:" + cd.confmember.admin + ":already done"); return; //already done } cd.confmember.dropped = true; boolean admindrop = cd.confmember.admin; Conference.Member member; int actcnt = 0; int admincnt = 0; synchronized(Conference.lock) { Vector<Conference.Member> memberList = Conference.list.get(cd.dialed); int idx = memberList.indexOf(cd.confmember); if (idx == -1) { JFLog.log("Conf:del member:failed:idx==-1"); return; } memberList.remove(cd.confmember); int size = memberList.size(); for(int a=0;a<size;a++) { member = memberList.get(a); if (!member.dropped) { actcnt++; if (member.admin) admincnt++; if (member.cd.confVideo) { reInviteVideo(member.cd, member, memberList); } } if (member.idxs == null) continue; member.idxs = JF.copyOfExcluding(member.idxs, idx); } if (actcnt == 0) { //last member dropped Conference.list.remove(cd.dialed); JFLog.log("Conf:" + cd.dialed + ":del member:last member dropped"); return; } if (admindrop && admincnt == 0) { //last admin dropped - drop all other users (CONF_DROP) JFLog.log("Conf:" + cd.dialed + ":del member:last admin dropped:dropping other users"); for(int a=0;a<memberList.size();a++) { member = memberList.get(a); member.cd.confstate = Conference.State.CONF_DROP; } } } JFLog.log("Conf:" + cd.dialed + ":del member:" + cd.user + ":admin:" + cd.confmember.admin + ":actcnt=" + actcnt); } private void reInviteVideo(CallDetailsPBX cd, Conference.Member myMember, Vector<Conference.Member> memberList) { //NOTE:already have Conference.lock //need to send a reINVITE to user to add video streams with other members SDP sdp = new SDP(); sdp.streams = new SDP.Stream[1]; sdp.streams[0] = cd.pbxsrc.sdp.getFirstAudioStream(); //keep audio stream sdp.ip = cd.pbxsrc.sdp.ip; cd.pbxsrc.sdp = sdp; //now add video streams SDP.Stream stream; int size = memberList.size(); for(int a=0;a<size;a++) { Conference.Member member = memberList.get(a); if (member != myMember && (member.cd.src.sdp == null || !member.cd.src.sdp.hasVideo())) continue; stream = sdp.addStream(SDP.Type.video); if (member == myMember) { stream.mode = SDP.Mode.inactive; //so there is at least one stream to start video camera stream.ip = "0.0.0.0"; stream.port = 0; } else { stream.mode = SDP.Mode.sendrecv; stream.ip = member.cd.src.sdp.getFirstVideoStream().getIP(); stream.port = member.cd.src.sdp.getFirstVideoStream().getPort(); } stream.codecs = getVideoCodecs(); stream.content = member.cd.user; } JFLog.log("IVR:Video Conference:Reinvite " + cd.user + " with " + (sdp.streams.length - 1) + " video streams"); cd.cmd = "INVITE"; cd.sip.buildsdp(cd, cd.pbxsrc); api.issue(cd, null, true, true); } private Codec[] getVideoCodecs() { String cfg = api.getCfg("videoCodecs"); if (cfg == null || cfg.length() == 0) cfg = "H264,VP8"; String cs[] = cfg.split(","); Codec codecs[] = new Codec[cs.length]; for(int a=0;a<cs.length;a++) { if (cs[a].equals("JPEG")) {codecs[a] = RTP.CODEC_JPEG; continue;} if (cs[a].equals("H263")) {codecs[a] = RTP.CODEC_H263; continue;} if (cs[a].equals("H263-1998")) {codecs[a] = RTP.CODEC_H263_1998; continue;} if (cs[a].equals("H263-2000")) {codecs[a] = RTP.CODEC_H263_2000; continue;} if (cs[a].equals("H264")) {codecs[a] = RTP.CODEC_H264; continue;} if (cs[a].equals("VP8")) {codecs[a] = RTP.CODEC_VP8; continue;} codecs[a] = RTP.CODEC_UNKNOWN; } return codecs; } }