package jpbx.core;
import java.io.*;
import java.util.*;
import javaforce.*;
import javaforce.voip.*;
/** Relays RTP packets between calling parties or from one party to local service (VM, IVR). */
public class RTPRelay implements RTPInterface {
private volatile boolean active_src, active_dst;
private RTP rtp_src, rtp_dst;
private CallDetailsPBX cd;
private short samples[];
private byte samples8[];
private RandomAccessFile recording;
private int recordinglen;
private FileInputStream playing;
private PBXEventHandler eh;
private String lang = "en";
private int maxrecordinglen;
private PBXAPI api;
private Vector<String> playList = new Vector<String>();
private boolean recv_src = true, recv_dst = true; //can recv from
private boolean send_src = true, send_dst = true; //can send to
private MusicOnHold moh_src = new MusicOnHold();
private MusicOnHold moh_dst = new MusicOnHold();
private boolean moh = false; //force moh mode
private String content; //name of stream
public final static short silence[] = new short[160];
/** Init RTPRelay. Ports may be -1 for NATing. */
public boolean init() {
rtp_src = new RTP();
rtp_src.init(this);
rtp_dst = new RTP();
rtp_dst.init(this);
return true;
}
public boolean init(CallDetailsPBX cd, PBXEventHandler eh, PBXAPI api) {
this.cd = cd;
this.eh = eh;
this.api = api;
samples = new short[160];
samples8 = new byte[160*2];
return true;
}
public int getPort_src() {return rtp_src.getlocalrtpport();}
public int getPort_dst() {return rtp_dst.getlocalrtpport();}
public void setLang(String lang) {
this.lang = lang;
}
public String getContent() {return content;}
public void setContent(String in) {content = in;}
/* Raw mode controls if RTPInterface.rtpPacket (if true) or rtpSamples (if false) is called. */
public void setRawMode(boolean state) {
rtp_src.setRawMode(state);
rtp_dst.setRawMode(state);
}
public boolean start_src(SDP.Stream stream) {
if (active_src) return true;
active_src = true;
rtp_src.start();
if (rtp_src.createChannel(stream) == null) return false;
/*
if (stream.isSecure()) {
SRTPChannel channel = (SRTPChannel)rtp_src.getDefaultChannel();
if (stream.keyExchange == SDP.KeyExchange.DTLS) {
channel.setDTLS(true, RTP.genIceufrag(), RTP.genIcepwd());
} else if (stream.keyExchange == SDP.KeyExchange.SDP) {
//TODO!!!
JFLog.log("RTPRelay:TODO:SDP Key Exchange");
} else {
JFLog.log("RTPRelay:Error:RTP Channel is secure but key exchange is undefined.");
}
}
*/
rtp_src.getDefaultChannel().start();
return true;
}
public boolean start_dst(SDP.Stream stream) {
if (active_dst) return true;
active_dst = true;
rtp_dst.start();
if (rtp_dst.createChannel(stream) == null) return false;
/*
if (stream.isSecure()) {
SRTPChannel channel = (SRTPChannel)rtp_dst.getDefaultChannel();
if (stream.keyExchange == SDP.KeyExchange.DTLS) {
channel.setDTLS(false, RTP.genIceufrag(), RTP.genIcepwd());
} else if (stream.keyExchange == SDP.KeyExchange.SDP) {
//TODO!!!
JFLog.log("RTPRelay:TODO:SDP Key Exchange");
} else {
JFLog.log("RTPRelay:Error:RTP Channel is secure but key exchange is undefined.");
}
}
*/
rtp_dst.getDefaultChannel().start();
return true;
}
public boolean start(SDP.Stream src, SDP.Stream dst) {
//NOTE:No codecs are needed since packets are only relayed as is
// JFLog.log("RTPRelay:start:src.ice=" + src.sdp.hashCode() + ":dst.ice=" + dst.sdp.hashCode());
if (active_src)
change_src(src);
else
start_src(src);
if (active_dst)
change_dst(dst);
else
start_dst(dst);
return true;
}
/** Swap src and dst. */
public void swap() {
RTP rtp;
rtp = rtp_src;
rtp_src = rtp_dst;
rtp_dst = rtp;
boolean sendrecv;
sendrecv = recv_dst;
recv_dst = recv_src;
recv_src = sendrecv;
sendrecv = send_dst;
send_dst = send_src;
send_src = sendrecv;
MusicOnHold moh;
moh = moh_src;
moh_src = moh_dst;
moh_dst = moh;
}
/** Swaps the dst RTP in two relays. */
public static void swap(RTPRelay r1, RTPRelay r2) {
RTP rtp;
rtp = r1.rtp_dst;
r1.rtp_dst = r2.rtp_dst;
r2.rtp_dst = rtp;
//reset interfaces
r1.rtp_dst.setInterface(r1);
r2.rtp_dst.setInterface(r2);
}
public void uninit() {
uninit_src();
uninit_dst();
}
public void uninit_src() {
if (active_src) {
active_src = false;
rtp_src.uninit();
}
cleanup();
}
public void uninit_dst() {
if (active_dst) {
active_dst = false;
rtp_dst.uninit();
}
cleanup();
}
public void cleanup() {
if (recording != null) {
closeRecording(true);
}
if (playing != null) {
closePlaying(true);
}
}
public void change_src(SDP.Stream stream) {
rtp_src.getDefaultChannel().change(stream);
recv_src = stream.canRecv();
send_src = stream.canSend();
}
public void change_dst(SDP.Stream stream) {
rtp_dst.getDefaultChannel().change(stream);
recv_dst = stream.canRecv();
send_dst = stream.canSend();
}
public boolean playSound(String fn) {
fn = Paths.sounds + lang + "/" + fn + ".wav";
return playSoundFull(fn);
}
public boolean playSoundFull(String fn) {
if (playing != null) return addSoundFull(fn);
api.log(cd, "playing:" + fn);
FileInputStream wav = null;
try {
byte data[] = new byte[30];
wav = new FileInputStream(fn);
//read RIFF header (20 bytes);
wav.read(data, 0, 20);
if (!LE.getString(data, 0, 4).equals("RIFF")) throw new Exception(fn + " is not a valid WAV file (RIFF)");
if (!LE.getString(data, 8, 4).equals("WAVE")) throw new Exception(fn + " is not a valid WAV file (WAVE)");
if (!LE.getString(data, 12, 4).equals("fmt ")) throw new Exception(fn + " is not a valid WAV file (fmt )");
int fmtsiz = LE.getuint32(data, 16);
if ((fmtsiz < 16) || (fmtsiz > 30)) throw new Exception(fn + " is not a valid WAV file (fmtsiz)");
wav.read(data, 0, fmtsiz);
if (LE.getuint16(data, 0) != 1) throw new Exception(fn + " is not PCM");
if (LE.getuint16(data, 2) != 1) throw new Exception(fn + " is not mono");
if (LE.getuint32(data, 4) != 8000) throw new Exception(fn + " is not 8000Hz");
if (LE.getuint16(data, 12) != 2) throw new Exception(fn + " is not 16bits");
wav.read(data, 0, 8);
if (!LE.getString(data, 0, 4).equals("data")) {
//ignore block
int len = LE.getuint32(data, 4);
byte junk[] = new byte[len];
wav.read(junk);
wav.read(data, 0, 8);
}
if (!LE.getString(data, 0, 4).equals("data")) throw new Exception(fn + " is not a valid WAV file (data)");
playing = wav; //let it rip
} catch (Exception e) {
api.log(cd, e);
try { if (wav != null) wav.close(); } catch (Exception e2) {}
return false;
}
return true;
}
public boolean addSound(String fn) {
playList.add(Paths.sounds + lang + "/" + fn + ".wav");
return true;
}
public boolean addSoundFull(String fn) {
playList.add(fn);
return true;
}
public boolean addNumber(int num) {
String numstr = Integer.toString(num);
int sl = numstr.length();
for(int i=0;i<sl;i++) {
addSound("vm-" + numstr.charAt(i));
}
return true;
}
/** Records to a full filename (extension .wav added). */
public boolean recordSoundFull(String fn, int maxSeconds) {
byte data[] = new byte[44];
maxrecordinglen = maxSeconds * 8000 * 2; //*2 for 16bit
fn += ".wav";
try {
File file = new File(fn);
if (file.exists()) file.delete(); //delete if exists
RandomAccessFile wav = new RandomAccessFile(fn, "rw");
LE.setString(data, 0, 4, "RIFF");
// setuint32(data, 4, filelength-8); //pending time travel code branch prediction in next release, or just patch when closing file
LE.setString(data, 8, 4, "WAVE");
LE.setString(data, 12, 4, "fmt ");
LE.setuint32(data, 16, 16); //fmt header size
LE.setuint16(data, 20, 1); //PCM
LE.setuint16(data, 22, 1); //mono
LE.setuint32(data, 24, 8000); //hz
LE.setuint32(data, 28, 0x3e80); //bytes/sec
LE.setuint16(data, 32, 2); //bytes/sam
LE.setuint16(data, 34, 0x10); //???
LE.setString(data, 36, 4, "data");
// setuint32(data, 40, filelength-44); //same deal
wav.write(data);
recordinglen = 0;
recording = wav; //let it rip
} catch (Exception e) { return false; }
return true;
}
/** Returns length of last recording in seconds. */
public int getRecordingLength() {
return recordinglen / (8000 * 2);
}
/* Convert samples to samples8. BE -> LE.*/
private void short2byte() {
for (int a = 0; a < 160; a++) {
samples8[a * 2 + 1] = (byte) (samples[a] >>> 8);
samples8[a * 2] = (byte) (samples[a] & 0xff);
}
}
/* Convert samples8 to samples. LE -> BE. */
private void byte2short() {
for (int a = 0; a < 160; a++) {
samples[a] = (short) ((((short) (samples8[a * 2 + 1])) << 8) + (((short) (samples8[a * 2])) & 0xff));
}
}
public void rtpSamples(RTPChannel channel) {
if (recording != null) {
if (channel.getSamples(samples)) {
short2byte();
try {
recording.write(samples8);
recordinglen += 160*2;
} catch (Exception e) {
recordinglen = maxrecordinglen;
}
if (recordinglen >= maxrecordinglen) {
closeRecording(false);
}
}
System.arraycopy(silence, 0, samples, 0, 160);
} else if (playing != null) {
try {
if (playing.read(samples8, 0, 160*2) != 160*2) {
System.arraycopy(silence, 0, samples, 0, 160);
closePlaying(false);
} else {
byte2short();
}
} catch (Exception e) {
closePlaying(false);
System.arraycopy(silence, 0, samples, 0, 160);
}
} else {
channel.getSamples(samples);
if (moh) {
moh_src.getSamples(samples);
}
if (eh != null) {
eh.samples(cd, samples);
} else {
System.arraycopy(silence, 0, samples, 0, 160);
}
}
try {
byte packet[] = channel.coder.encode(samples);
channel.writeRTP(packet, 0, packet.length);
// api.log(cd, "VM : Sent samples, length=" + packet.length);
} catch (Exception e) {
api.log(cd, e);
}
}
public void rtpDigit(RTPChannel channel, char digit) {
if (recording != null) {
closeRecording(true);
} else if (playing != null) {
closePlaying(true);
}
eh.event(cd, eh.DIGIT, digit, true);
}
public void rtcpPacket(RTPChannel channel, byte data[], int off, int len) {
if (channel.rtp == rtp_src) {
rtp_dst.getDefaultChannel().writeRTCP(data, off, len);
} else if (channel.rtp == rtp_dst) {
rtp_src.getDefaultChannel().writeRTCP(data, off, len);
}
}
public void rtpPacket(RTPChannel channel, byte data[], int off, int len) {
// JFLog.log("rtpPacket:" + channel + ",len=" + len);
try {
if (channel.rtp == rtp_src) {
if (!recv_dst) {
moh_src.getSamples(samples);
byte packet[] = rtp_src.getDefaultChannel().coder.encode(samples);
rtp_src.getDefaultChannel().writeRTP(packet, 0, packet.length);
}
if (send_dst) {
// JFLog.log("sending to:" + rtp_dst.getDefaultChannel());
rtp_dst.getDefaultChannel().writeRTP(data, off, len);
}
} else if (channel.rtp == rtp_dst) {
if (!recv_src) {
moh_dst.getSamples(samples);
byte packet[] = rtp_dst.getDefaultChannel().coder.encode(samples);
rtp_dst.getDefaultChannel().writeRTP(packet, 0, packet.length);
}
if (send_src) {
// JFLog.log("sending to:" + rtp_src.getDefaultChannel());
rtp_src.getDefaultChannel().writeRTP(data, off, len);
}
} else {
JFLog.log("Error: Unknown RTP Packet:" + channel.rtp);
}
} catch (Exception e) {
JFLog.log(e);
}
}
public void rtpH263(RTPChannel rtp, byte data[],int off,int len) {}
public void rtpH263_1998(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpH263_2000(RTPChannel rtp, byte[] data, int off, int len) {}
public void rtpH264(RTPChannel rtp, byte data[],int off,int len) {}
public void rtpJPEG(RTPChannel rtp, byte data[],int off,int len) {}
public void rtpVP8(RTPChannel rtp, byte data[],int off,int len) {}
public void rtpInactive(RTPChannel rtp) {}
private void closeRecording(boolean interrupted) {
try {
byte data[] = new byte[4];
recording.seek(4);
LE.setuint32(data, 0, recordinglen+44-8);
recording.write(data);
recording.seek(40);
LE.setuint32(data, 0, recordinglen);
recording.write(data);
recording.close();
} catch (Exception e) {}
recording = null;
eh.event(cd, eh.SOUND, ' ', interrupted);
}
private void closePlaying(boolean interrupted) {
try {playing.close(); } catch (Exception e) {}
playing = null;
if ((!interrupted) && (playList.size() > 0)) {
playSoundFull(playList.get(0));
playList.remove(0);
} else {
playList.clear();
eh.event(cd, eh.SOUND, ' ', interrupted);
}
}
/** Forces playing MOH to both parties. */
public void setMOH(boolean state) {
moh = state;
}
public String toString() {
return "RTPRelay:{src=" + rtp_src + ",dst=" + rtp_dst + "}";
}
}