commit f62fbb5d7aab16aea07ee05ec119e39464981d71 Author: rdkartono Date: Wed Dec 4 09:19:38 2024 +0700 first commit diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..95c7b9f --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + 4.0.0 + AxisAudio + AxisAudio + 0.0.1-SNAPSHOT + + src + + + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + com.google.code.gson + gson + 2.10.1 + + + + + net.java.dev.jna + jna + 5.6.0 + + + + + org.apache.httpcomponents + httpmime + 4.5.14 + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/bass/BassAACLibrary.java b/src/bass/BassAACLibrary.java new file mode 100644 index 0000000..9296111 --- /dev/null +++ b/src/bass/BassAACLibrary.java @@ -0,0 +1,44 @@ +package bass; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import bass.BassLibrary.BASS_FILEPROCS; +import bass.BassLibrary.DOWNLOADPROC; + +/** + * BASS AAC Library + * @author rdkartono + * + */ +public interface BassAACLibrary extends Library { + + BassAACLibrary BASS_AAC = (BassAACLibrary) Native.load("bass_aac",BassAACLibrary.class); + public interface Constant{ + // additional error codes returned by BASS_ErrorGetCode + public static final int BASS_ERROR_MP4_NOSTREAM = 6000; // non-streamable due to MP4 atom order ("mdat" before "moov") + + // Additional BASS_SetConfig options + public static final int BASS_CONFIG_MP4_VIDEO = 0x10700; // play the audio from MP4 videos + public static final int BASS_CONFIG_AAC_MP4 = 0x10701; // support MP4 in BASS_AAC_StreamCreateXXX functions (no need for BASS_MP4_StreamCreateXXX) + public static final int BASS_CONFIG_AAC_PRESCAN = 0x10702; // pre-scan ADTS AAC files for seek points and accurate length + + // Additional BASS_AAC_StreamCreateFile/etc flags + public static final int BASS_AAC_FRAME960 = 0x1000; // 960 samples per frame + public static final int BASS_AAC_STEREO = 0x400000; // downmatrix to stereo + + // BASS_CHANNELINFO type + public static final int BASS_CTYPE_STREAM_AAC = 0x10b00; // AAC + public static final int BASS_CTYPE_STREAM_MP4 = 0x10b01; // AAC in MP4 + } + + + public int BASS_AAC_StreamCreateFile(String file, long offset, long length, int flags); + public int BASS_AAC_StreamCreateFile(Pointer file, long offset, long length, int flags); + public int BASS_AAC_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); + public int BASS_AAC_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); + public int BASS_MP4_StreamCreateFile(String file, long offset, long length, int flags); + public int BASS_MP4_StreamCreateFile(Pointer file, long offset, long length, int flags); + public int BASS_MP4_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); +} diff --git a/src/bass/BassEncAACLibrary.java b/src/bass/BassEncAACLibrary.java new file mode 100644 index 0000000..fb90d49 --- /dev/null +++ b/src/bass/BassEncAACLibrary.java @@ -0,0 +1,21 @@ +package bass; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import bass.BassEncLibrary.ENCODEPROCEX; + +/** + * Bass Encoder AAC Plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +public interface BassEncAACLibrary extends Library { + + BassEncAACLibrary BASSENC_AAC = (BassEncAACLibrary) Native.load("bassenc_aac",BassEncAACLibrary.class); + public int BASS_Encode_AAC_GetVersion(); + + public int BASS_Encode_AAC_Start(int handle, String options, int flags, ENCODEPROCEX proc, Pointer user); + public int BASS_Encode_AAC_StartFile(int handle, String options, int flags, String filename); +} diff --git a/src/bass/BassEncLibrary.java b/src/bass/BassEncLibrary.java new file mode 100644 index 0000000..93c38eb --- /dev/null +++ b/src/bass/BassEncLibrary.java @@ -0,0 +1,172 @@ +package bass; +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +/** + * BASS Encoder Library + * @author rdkartono + * + */ +public interface BassEncLibrary extends Library { + BassEncLibrary BASSENC = (BassEncLibrary) Native.load("bassenc", BassEncLibrary.class); + + public interface ENCODEPROC extends Callback + { + void ENCODEPROC(int handle, int channel, Pointer buffer, int length, Pointer user); + /* Encoding callback function. + handle : The encoder + channel: The channel handle + buffer : Buffer containing the encoded data + length : Number of bytes + user : The 'user' parameter value given when starting the encoder */ + } + + public interface ENCODEPROCEX extends Callback + { + void ENCODEPROCEX(int handle, int channel, Pointer buffer, int length, long offset, Pointer user); + /* Encoding callback function. + handle : The encoder + channel: The channel handle + buffer : Buffer containing the encoded data + length : Number of bytes + offset : File offset of the data + user : The 'user' parameter value given when starting the encoder */ + } + + public interface ENCODERPROC extends Callback + { + int ENCODERPROC(int handle, int channel, Pointer buffer, int length, int maxout, Pointer user); + /* Encoder callback function. + handle : The encoder + channel: The channel handle + buffer : Buffer containing the PCM data (input) and receiving the encoded data (output) + length : Number of bytes in (-1=closing) + maxout : Maximum number of bytes out + user : The 'user' parameter value given when calling BASS_Encode_StartUser + RETURN : The amount of encoded data (-1=stop) */ + } + + public interface ENCODECLIENTPROC extends Callback + { + boolean ENCODECLIENTPROC(int handle, boolean connect, String client, StringBuffer headers, Pointer user); + /* Client connection notification callback function. + handle : The encoder + connect: true/false=client is connecting/disconnecting + client : The client's address (xxx.xxx.xxx.xxx:port) + headers: Request headers (optionally response headers on return) + user : The 'user' parameter value given when calling BASS_Encode_ServerInit + RETURN : true/false=accept/reject connection (ignored if connect=false) */ + } + + public interface ENCODENOTIFYPROC extends Callback + { + void ENCODENOTIFYPROC(int handle, int status, Pointer user); + /* Encoder death notification callback function. + handle : The encoder + status : Notification (BASS_ENCODE_NOTIFY_xxx) + user : The 'user' parameter value given when calling BASS_Encode_SetNotify */ + } + + + public int BASS_Encode_GetVersion(); + + public int BASS_Encode_Start(int handle, String cmdline, int flags, ENCODEPROC proc, Pointer user); + public int BASS_Encode_StartLimit(int handle, String cmdline, int flags, ENCODEPROC proc, Pointer user, int limit); + public int BASS_Encode_StartUser(int handle, String file, int flags, ENCODERPROC proc, Pointer user); + public boolean BASS_Encode_AddChunk(int handle, String id, Pointer buffer, int length); + public boolean BASS_Encode_Write(int handle, Pointer buffer, int length); + public boolean BASS_Encode_Stop(int handle); + public boolean BASS_Encode_StopEx(int handle, boolean queue); + public boolean BASS_Encode_SetPaused(int handle, boolean paused); + public int BASS_Encode_IsActive(int handle); + public boolean BASS_Encode_SetNotify(int handle, ENCODENOTIFYPROC proc, Pointer user); + public long BASS_Encode_GetCount(int handle, int count); + public boolean BASS_Encode_SetChannel(int handle, int channel); + public int BASS_Encode_GetChannel(int handle); + public boolean BASS_Encode_UserOutput(int handle, long offset, Pointer buffer, int length); + + public boolean BASS_Encode_CastInit(int handle, String server, String pass, String content, String name, String url, String genre, String desc, String headers, int bitrate, int flags); + public boolean BASS_Encode_CastSetTitle(int handle, String title, String url); + public boolean BASS_Encode_CastSendMeta(int handle, int type, Pointer data, int length); + public String BASS_Encode_CastGetStats(int handle, int type, String pass); + + public int BASS_Encode_ServerInit(int handle, String port, int buffer, int burst, int flags, ENCODECLIENTPROC proc, Pointer user); + public boolean BASS_Encode_ServerKick(int handle, String client); + + public interface Constant{ + // Additional error codes returned by BASS_ErrorGetCode + public static final int BASS_ERROR_CAST_DENIED = 2100; // access denied (invalid password) + public static final int BASS_ERROR_SERVER_CERT = 2101; // missing/invalid certificate + + // Additional BASS_SetConfig options + public static final int BASS_CONFIG_ENCODE_PRIORITY = 0x10300; + public static final int BASS_CONFIG_ENCODE_QUEUE = 0x10301; + public static final int BASS_CONFIG_ENCODE_CAST_TIMEOUT = 0x10310; + + // Additional BASS_SetConfigPtr options + public static final int BASS_CONFIG_ENCODE_CAST_PROXY = 0x10311; + public static final int BASS_CONFIG_ENCODE_CAST_BIND = 0x10312; + public static final int BASS_CONFIG_ENCODE_SERVER_CERT = 0x10320; + public static final int BASS_CONFIG_ENCODE_SERVER_KEY = 0x10321; + + // BASS_Encode_Start flags + public static final int BASS_ENCODE_NOHEAD = 1; // don't send a WAV header to the encoder + public static final int BASS_ENCODE_FP_8BIT = 2; // convert floating-point sample data to 8-bit integer + public static final int BASS_ENCODE_FP_16BIT = 4; // convert floating-point sample data to 16-bit integer + public static final int BASS_ENCODE_FP_24BIT = 6; // convert floating-point sample data to 24-bit integer + public static final int BASS_ENCODE_FP_32BIT = 8; // convert floating-point sample data to 32-bit integer + public static final int BASS_ENCODE_FP_AUTO = 14; // convert floating-point sample data back to channel's format + public static final int BASS_ENCODE_BIGEND = 16; // big-endian sample data + public static final int BASS_ENCODE_PAUSE = 32; // start encording paused + public static final int BASS_ENCODE_PCM = 64; // write PCM sample data (no encoder) + public static final int BASS_ENCODE_RF64 = 128; // send an RF64 header + public static final int BASS_ENCODE_QUEUE = 0x200; // queue data to feed encoder asynchronously + public static final int BASS_ENCODE_WFEXT = 0x400; // WAVEFORMATEXTENSIBLE "fmt" chunk + public static final int BASS_ENCODE_CAST_NOLIMIT = 0x1000; // don't limit casting data rate + public static final int BASS_ENCODE_LIMIT = 0x2000; // limit data rate to real-time + public static final int BASS_ENCODE_AIFF = 0x4000; // send an AIFF header rather than WAV + public static final int BASS_ENCODE_DITHER = 0x8000; // apply dither when converting floating-point sample data to integer + public static final int BASS_ENCODE_AUTOFREE = 0x40000; // free the encoder when the channel is freed + + // BASS_Encode_GetCount counts + public static final int BASS_ENCODE_COUNT_IN = 0; // sent to encoder + public static final int BASS_ENCODE_COUNT_OUT = 1; // received from encoder + public static final int BASS_ENCODE_COUNT_CAST = 2; // sent to cast server + public static final int BASS_ENCODE_COUNT_QUEUE = 3; // queued + public static final int BASS_ENCODE_COUNT_QUEUE_LIMIT = 4; // queue limit + public static final int BASS_ENCODE_COUNT_QUEUE_FAIL = 5; // failed to queue + public static final int BASS_ENCODE_COUNT_IN_FP = 6; // sent to encoder before floating-point conversion + + // BASS_Encode_CastInit content MIME types + public static final String BASS_ENCODE_TYPE_MP3 = "audio/mpeg"; + public static final String BASS_ENCODE_TYPE_OGG = "audio/ogg"; + public static final String BASS_ENCODE_TYPE_AAC = "audio/aacp"; + + // BASS_Encode_CastInit flags + public static final int BASS_ENCODE_CAST_PUBLIC = 1; // add to public directory + public static final int BASS_ENCODE_CAST_PUT = 2; // use PUT method + public static final int BASS_ENCODE_CAST_SSL = 4; // use SSL/TLS encryption + + // BASS_Encode_CastGetStats types + public static final int BASS_ENCODE_STATS_SHOUT = 0; // Shoutcast stats + public static final int BASS_ENCODE_STATS_ICE = 1; // Icecast mount-point stats + public static final int BASS_ENCODE_STATS_ICESERV = 2; // Icecast server stats + + // BASS_Encode_ServerInit flags + public static final int BASS_ENCODE_SERVER_NOHTTP = 1; // no HTTP headers + public static final int BASS_ENCODE_SERVER_META = 2; // Shoutcast metadata + public static final int BASS_ENCODE_SERVER_SSL = 4; // support SSL/TLS encryption + public static final int BASS_ENCODE_SERVER_SSLONLY = 8; // require SSL/TLS encryption + + // Encoder notifications + public static final int BASS_ENCODE_NOTIFY_ENCODER = 1; // encoder died + public static final int BASS_ENCODE_NOTIFY_CAST = 2; // cast server connection died + public static final int BASS_ENCODE_NOTIFY_SERVER = 3; // server died + public static final int BASS_ENCODE_NOTIFY_CAST_TIMEOUT = 0x10000; // cast timeout + public static final int BASS_ENCODE_NOTIFY_QUEUE_FULL = 0x10001; // queue is out of space + public static final int BASS_ENCODE_NOTIFY_FREE = 0x10002; // encoder has been freed + + } +} diff --git a/src/bass/BassEncMP3Library.java b/src/bass/BassEncMP3Library.java new file mode 100644 index 0000000..e0fcd25 --- /dev/null +++ b/src/bass/BassEncMP3Library.java @@ -0,0 +1,22 @@ +package bass; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import bass.BassEncLibrary.ENCODEPROCEX; + +/** + * BASS Encoder MP3 plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +public interface BassEncMP3Library extends Library{ + BassEncMP3Library BASSENC_MP3 = (BassEncMP3Library) Native.load("bassenc_mp3",BassEncMP3Library.class); + + public int BASS_Encode_MP3_GetVersion(); + + public int BASS_Encode_MP3_Start(int handle, String options, int flags, ENCODEPROCEX proc, Pointer user); + public int BASS_Encode_MP3_StartFile(int handle, String options, int flags, String filename); +} diff --git a/src/bass/BassEncOGGLibrary.java b/src/bass/BassEncOGGLibrary.java new file mode 100644 index 0000000..2bc7dec --- /dev/null +++ b/src/bass/BassEncOGGLibrary.java @@ -0,0 +1,27 @@ +package bass; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import bass.BassEncLibrary.ENCODEPROC; + +/** + * Bass Encoder OGG Plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +public interface BassEncOGGLibrary extends Library { + BassEncOGGLibrary BASSENC_OGG = (BassEncOGGLibrary) Native.load("bassenc_ogg",BassEncOGGLibrary.class); + public int BASS_Encode_OGG_GetVersion(); + + public int BASS_Encode_OGG_Start(int handle, String options, int flags, ENCODEPROC proc, Pointer user); + public int BASS_Encode_OGG_StartFile(int handle, String options, int flags, String filename); + public boolean BASS_Encode_OGG_NewStream(int handle, String options, int flags); + + public interface Constant{ + // BASS_Encode_OGG_NewStream flags + public static final int BASS_ENCODE_OGG_RESET = 0x1000000; + } +} diff --git a/src/bass/BassEncOPUSLibrary.java b/src/bass/BassEncOPUSLibrary.java new file mode 100644 index 0000000..77cdf35 --- /dev/null +++ b/src/bass/BassEncOPUSLibrary.java @@ -0,0 +1,27 @@ +package bass; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import bass.BassEncLibrary.ENCODEPROC; + +/** + * Bass Encoder OPUS Plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +public interface BassEncOPUSLibrary extends Library{ + BassEncOPUSLibrary BASSENC_OPUS = (BassEncOPUSLibrary) Native.load("bassenc_opus",BassEncOPUSLibrary.class); + public interface Constant{ + // BASS_Encode_OPUS_NewStream flags + public static final int BASS_ENCODE_OPUS_RESET = 0x1000000; + public static final int BASS_ENCODE_OPUS_CTLONLY = 0x2000000; + } + public int BASS_Encode_OPUS_GetVersion(); + + public int BASS_Encode_OPUS_Start(int handle, String options, int flags, ENCODEPROC proc, Pointer user); + public int BASS_Encode_OPUS_StartFile(int handle, String options, int flags, String filename); + public boolean BASS_Encode_OPUS_NewStream(int handle, String options, int flags); +} diff --git a/src/bass/BassLibrary.java b/src/bass/BassLibrary.java new file mode 100644 index 0000000..30ad69a --- /dev/null +++ b/src/bass/BassLibrary.java @@ -0,0 +1,1363 @@ +package bass; +import java.text.MessageFormat; + +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import anywheresoftware.b4a.BA; +import anywheresoftware.b4a.BA.Hide; +import anywheresoftware.b4a.BA.ShortName; + +/** + * Bass Library + * @author rdkartono + * + */ +public interface BassLibrary extends Library { + + BassLibrary BASS = (BassLibrary)Native.load("bass", BassLibrary.class); + + public interface Constant{ + public static final int BASSVERSION = 0x204; // API version + public static final String BASSVERSIONTEXT = "2.4"; + // Error codes returned by BASS_ErrorGetCode + public static final int BASS_OK = 0; // all is OK + public static final int BASS_ERROR_MEM = 1; // memory error + public static final int BASS_ERROR_FILEOPEN = 2; // can't open the file + public static final int BASS_ERROR_DRIVER = 3; // can't find a free/valid driver + public static final int BASS_ERROR_BUFLOST = 4; // the sample buffer was lost + public static final int BASS_ERROR_HANDLE = 5; // invalid handle + public static final int BASS_ERROR_FORMAT = 6; // unsupported sample format + public static final int BASS_ERROR_POSITION = 7; // invalid position + public static final int BASS_ERROR_INIT = 8; // BASS_Init has not been successfully called + public static final int BASS_ERROR_START = 9; // BASS_Start has not been successfully called + public static final int BASS_ERROR_SSL = 10; // SSL/HTTPS support isn't available + public static final int BASS_ERROR_REINIT = 11; // device needs to be reinitialized + public static final int BASS_ERROR_ALREADY = 14; // already initialized/paused/whatever + public static final int BASS_ERROR_NOTAUDIO = 17; // file does not contain audio + public static final int BASS_ERROR_NOCHAN = 18; // can't get a free channel + public static final int BASS_ERROR_ILLTYPE = 19; // an illegal type was specified + public static final int BASS_ERROR_ILLPARAM = 20; // an illegal parameter was specified + public static final int BASS_ERROR_NO3D = 21; // no 3D support + public static final int BASS_ERROR_NOEAX = 22; // no EAX support + public static final int BASS_ERROR_DEVICE = 23; // illegal device number + public static final int BASS_ERROR_NOPLAY = 24; // not playing + public static final int BASS_ERROR_FREQ = 25; // illegal sample rate + public static final int BASS_ERROR_NOTFILE = 27; // the stream is not a file stream + public static final int BASS_ERROR_NOHW = 29; // no hardware voices available + public static final int BASS_ERROR_EMPTY = 31; // the file has no sample data + public static final int BASS_ERROR_NONET = 32; // no internet connection could be opened + public static final int BASS_ERROR_CREATE = 33; // couldn't create the file + public static final int BASS_ERROR_NOFX = 34; // effects are not available + public static final int BASS_ERROR_NOTAVAIL = 37; // requested data/action is not available + public static final int BASS_ERROR_DECODE = 38; // the channel is a "decoding channel" + public static final int BASS_ERROR_DX = 39; // a sufficient DirectX version is not installed + public static final int BASS_ERROR_TIMEOUT = 40; // connection timedout + public static final int BASS_ERROR_FILEFORM = 41; // unsupported file format + public static final int BASS_ERROR_SPEAKER = 42; // unavailable speaker + public static final int BASS_ERROR_VERSION = 43; // invalid BASS version (used by add-ons) + public static final int BASS_ERROR_CODEC = 44; // codec is not available/supported + public static final int BASS_ERROR_ENDED = 45; // the channel/file has ended + public static final int BASS_ERROR_BUSY = 46; // the device is busy + public static final int BASS_ERROR_UNSTREAMABLE = 47; // unstreamable file + public static final int BASS_ERROR_PROTOCOL = 48; // unsupported protocol + public static final int BASS_ERROR_DENIED = 49; // access denied + public static final int BASS_ERROR_UNKNOWN = -1; // some other mystery problem + + public static final int BASS_ERROR_JAVA_CLASS = 500; // object class problem + + // BASS_RecordSetInput flags + public static final int BASS_INPUT_OFF=0x10000; + public static final int BASS_INPUT_ON=0x20000; + + public static final int BASS_INPUT_TYPE_MASK=0xff000000; + public static final int BASS_INPUT_TYPE_UNDEF=0x00000000; + public static final int BASS_INPUT_TYPE_DIGITAL =0x01000000; + public static final int BASS_INPUT_TYPE_LINE=0x02000000; + public static final int BASS_INPUT_TYPE_MIC =0x03000000; + public static final int BASS_INPUT_TYPE_SYNTH=0x04000000; + public static final int BASS_INPUT_TYPE_CD=0x05000000; + public static final int BASS_INPUT_TYPE_PHONE=0x06000000; + public static final int BASS_INPUT_TYPE_SPEAKER=0x07000000; + public static final int BASS_INPUT_TYPE_WAVE=0x08000000; + public static final int BASS_INPUT_TYPE_AUX=0x09000000; + public static final int BASS_INPUT_TYPE_ANALOG=0x0a000000; + + // BASS_SetConfig options + public static final int BASS_CONFIG_BUFFER = 0; + public static final int BASS_CONFIG_UPDATEPERIOD = 1; + public static final int BASS_CONFIG_GVOL_SAMPLE = 4; + public static final int BASS_CONFIG_GVOL_STREAM = 5; + public static final int BASS_CONFIG_GVOL_MUSIC = 6; + public static final int BASS_CONFIG_CURVE_VOL = 7; + public static final int BASS_CONFIG_CURVE_PAN = 8; + public static final int BASS_CONFIG_FLOATDSP = 9; + public static final int BASS_CONFIG_3DALGORITHM = 10; + public static final int BASS_CONFIG_NET_TIMEOUT = 11; + public static final int BASS_CONFIG_NET_BUFFER = 12; + public static final int BASS_CONFIG_PAUSE_NOPLAY = 13; + public static final int BASS_CONFIG_NET_PREBUF = 15; + public static final int BASS_CONFIG_NET_PASSIVE = 18; + public static final int BASS_CONFIG_REC_BUFFER = 19; + public static final int BASS_CONFIG_NET_PLAYLIST = 21; + public static final int BASS_CONFIG_MUSIC_VIRTUAL = 22; + public static final int BASS_CONFIG_VERIFY = 23; + public static final int BASS_CONFIG_UPDATETHREADS = 24; + public static final int BASS_CONFIG_DEV_BUFFER = 27; + public static final int BASS_CONFIG_DEV_DEFAULT = 36; + public static final int BASS_CONFIG_NET_READTIMEOUT = 37; + public static final int BASS_CONFIG_HANDLES = 41; + public static final int BASS_CONFIG_SRC = 43; + public static final int BASS_CONFIG_SRC_SAMPLE = 44; + public static final int BASS_CONFIG_ASYNCFILE_BUFFER = 45; + public static final int BASS_CONFIG_OGG_PRESCAN = 47; + public static final int BASS_CONFIG_DEV_NONSTOP = 50; + public static final int BASS_CONFIG_VERIFY_NET = 52; + public static final int BASS_CONFIG_DEV_PERIOD = 53; + public static final int BASS_CONFIG_FLOAT = 54; + public static final int BASS_CONFIG_NET_SEEK = 56; + public static final int BASS_CONFIG_AM_DISABLE = 58; + public static final int BASS_CONFIG_NET_PLAYLIST_DEPTH = 59; + public static final int BASS_CONFIG_NET_PREBUF_WAIT = 60; + public static final int BASS_CONFIG_ANDROID_SESSIONID = 62; + public static final int BASS_CONFIG_ANDROID_AAUDIO = 67; + public static final int BASS_CONFIG_SAMPLE_ONEHANDLE = 69; + public static final int BASS_CONFIG_DEV_TIMEOUT = 70; + public static final int BASS_CONFIG_NET_META = 71; + public static final int BASS_CONFIG_NET_RESTRATE = 72; + public static final int BASS_CONFIG_REC_DEFAULT = 73; + public static final int BASS_CONFIG_NORAMP = 74; + + // BASS_SetConfigPtr options + public static final int BASS_CONFIG_NET_AGENT = 16; + public static final int BASS_CONFIG_NET_PROXY = 17; + public static final int BASS_CONFIG_LIBSSL = 64; + public static final int BASS_CONFIG_FILENAME = 75; + + public static final int BASS_CONFIG_THREAD = 0x40000000; // flag: thread-specific setting + + // BASS_Init flags + public static final int BASS_DEVICE_8BITS = 1; // unused + public static final int BASS_DEVICE_MONO = 2; // mono + public static final int BASS_DEVICE_3D = 4; // unused + public static final int BASS_DEVICE_16BITS = 8; // limit output to 16-bit + public static final int BASS_DEVICE_REINIT = 128; // reinitialize + public static final int BASS_DEVICE_LATENCY = 0x100; // unused + public static final int BASS_DEVICE_SPEAKERS = 0x800; // force enabling of speaker assignment + public static final int BASS_DEVICE_NOSPEAKER = 0x1000; // ignore speaker arrangement + public static final int BASS_DEVICE_FREQ = 0x4000; // set device sample rate + public static final int BASS_DEVICE_AUDIOTRACK = 0x20000; // use AudioTrack output + public static final int BASS_DEVICE_SOFTWARE = 0x80000; // disable hardware/fastpath output + + // BASS_DEVICEINFO flags + public static final int BASS_DEVICE_ENABLED = 1; + public static final int BASS_DEVICE_DEFAULT = 2; + public static final int BASS_DEVICE_INIT = 4; + + public static final int BASS_SAMPLE_8BITS = 1; // 8 bit + public static final int BASS_SAMPLE_FLOAT = 256; // 32-bit floating-point + public static final int BASS_SAMPLE_MONO = 2; // mono + public static final int BASS_SAMPLE_LOOP = 4; // looped + public static final int BASS_SAMPLE_3D = 8; // 3D functionality + public static final int BASS_SAMPLE_SOFTWARE = 16; // unused + public static final int BASS_SAMPLE_MUTEMAX = 32; // mute at max distance (3D only) + public static final int BASS_SAMPLE_VAM = 64; // unused + public static final int BASS_SAMPLE_FX = 128; // unused + public static final int BASS_SAMPLE_OVER_VOL = 0x10000; // override lowest volume + public static final int BASS_SAMPLE_OVER_POS = 0x20000; // override longest playing + public static final int BASS_SAMPLE_OVER_DIST = 0x30000; // override furthest from listener (3D only) + + public static final int BASS_STREAM_PRESCAN = 0x20000; // scan file for accurate seeking and length + public static final int BASS_STREAM_AUTOFREE = 0x40000; // automatically free the stream when it stops/ends + public static final int BASS_STREAM_RESTRATE = 0x80000; // restrict the download rate of internet file streams + public static final int BASS_STREAM_BLOCK = 0x100000; // download/play internet file stream in small blocks + public static final int BASS_STREAM_DECODE = 0x200000; // don't play the stream, only decode (BASS_ChannelGetData) + public static final int BASS_STREAM_STATUS = 0x800000; // give server status info (HTTP/ICY tags) in DOWNLOADPROC + + public static final int BASS_MP3_IGNOREDELAY = 0x200; // ignore LAME/Xing/VBRI/iTunes delay & padding info + public static final int BASS_MP3_SETPOS = BASS_STREAM_PRESCAN; + + public static final int BASS_MUSIC_FLOAT = BASS_SAMPLE_FLOAT; + public static final int BASS_MUSIC_MONO = BASS_SAMPLE_MONO; + public static final int BASS_MUSIC_LOOP = BASS_SAMPLE_LOOP; + public static final int BASS_MUSIC_3D = BASS_SAMPLE_3D; + public static final int BASS_MUSIC_FX = BASS_SAMPLE_FX; + public static final int BASS_MUSIC_AUTOFREE = BASS_STREAM_AUTOFREE; + public static final int BASS_MUSIC_DECODE = BASS_STREAM_DECODE; + public static final int BASS_MUSIC_PRESCAN = BASS_STREAM_PRESCAN; // calculate playback length + public static final int BASS_MUSIC_CALCLEN = BASS_MUSIC_PRESCAN; + public static final int BASS_MUSIC_RAMP = 0x200; // normal ramping + public static final int BASS_MUSIC_RAMPS = 0x400; // sensitive ramping + public static final int BASS_MUSIC_SURROUND = 0x800; // surround sound + public static final int BASS_MUSIC_SURROUND2 = 0x1000; // surround sound (mode 2) + public static final int BASS_MUSIC_FT2PAN = 0x2000; // apply FastTracker 2 panning to XM files + public static final int BASS_MUSIC_FT2MOD = 0x2000; // play .MOD as FastTracker 2 does + public static final int BASS_MUSIC_PT1MOD = 0x4000; // play .MOD as ProTracker 1 does + public static final int BASS_MUSIC_NONINTER = 0x10000; // non-interpolated sample mixing + public static final int BASS_MUSIC_SINCINTER = 0x800000; // sinc interpolated sample mixing + public static final int BASS_MUSIC_POSRESET = 0x8000; // stop all notes when moving position + public static final int BASS_MUSIC_POSRESETEX = 0x400000; // stop all notes and reset bmp/etc when moving position + public static final int BASS_MUSIC_STOPBACK = 0x80000; // stop the music on a backwards jump effect + public static final int BASS_MUSIC_NOSAMPLE = 0x100000; // don't load the samples + + // Speaker assignment flags + public static final int BASS_SPEAKER_FRONT = 0x1000000; // front speakers + public static final int BASS_SPEAKER_REAR = 0x2000000; // rear speakers + public static final int BASS_SPEAKER_CENLFE = 0x3000000; // center & LFE speakers (5.1) + public static final int BASS_SPEAKER_SIDE = 0x4000000; // side speakers (7.1) + public static int BASS_SPEAKER_N(int n) { return n<<24; } // n'th pair of speakers (max 15) + public static final int BASS_SPEAKER_LEFT = 0x10000000; // modifier: left + public static final int BASS_SPEAKER_RIGHT = 0x20000000; // modifier: right + public static final int BASS_SPEAKER_FRONTLEFT = BASS_SPEAKER_FRONT | BASS_SPEAKER_LEFT; + public static final int BASS_SPEAKER_FRONTRIGHT = BASS_SPEAKER_FRONT | BASS_SPEAKER_RIGHT; + public static final int BASS_SPEAKER_REARLEFT = BASS_SPEAKER_REAR | BASS_SPEAKER_LEFT; + public static final int BASS_SPEAKER_REARRIGHT = BASS_SPEAKER_REAR | BASS_SPEAKER_RIGHT; + public static final int BASS_SPEAKER_CENTER = BASS_SPEAKER_CENLFE | BASS_SPEAKER_LEFT; + public static final int BASS_SPEAKER_LFE = BASS_SPEAKER_CENLFE | BASS_SPEAKER_RIGHT; + public static final int BASS_SPEAKER_SIDELEFT = BASS_SPEAKER_SIDE | BASS_SPEAKER_LEFT; + public static final int BASS_SPEAKER_SIDERIGHT = BASS_SPEAKER_SIDE | BASS_SPEAKER_RIGHT; + public static final int BASS_SPEAKER_REAR2 = BASS_SPEAKER_SIDE; + public static final int BASS_SPEAKER_REAR2LEFT = BASS_SPEAKER_SIDELEFT; + public static final int BASS_SPEAKER_REAR2RIGHT = BASS_SPEAKER_SIDERIGHT; + + public static final int BASS_ASYNCFILE = 0x40000000; // read file asynchronously + + public static final int BASS_RECORD_PAUSE = 0x8000; // start recording paused + + // BASS_CHANNELINFO types + public static final int BASS_CTYPE_SAMPLE = 1; + public static final int BASS_CTYPE_RECORD = 2; + public static final int BASS_CTYPE_STREAM = 0x10000; + public static final int BASS_CTYPE_STREAM_VORBIS = 0x10002; + public static final int BASS_CTYPE_STREAM_OGG = 0x10002; + public static final int BASS_CTYPE_STREAM_MP1 = 0x10003; + public static final int BASS_CTYPE_STREAM_MP2 = 0x10004; + public static final int BASS_CTYPE_STREAM_MP3 = 0x10005; + public static final int BASS_CTYPE_STREAM_AIFF = 0x10006; + public static final int BASS_CTYPE_STREAM_CA = 0x10007; + public static final int BASS_CTYPE_STREAM_MF = 0x10008; + public static final int BASS_CTYPE_STREAM_AM = 0x10009; + public static final int BASS_CTYPE_STREAM_SAMPLE = 0x1000a; + public static final int BASS_CTYPE_STREAM_DUMMY = 0x18000; + public static final int BASS_CTYPE_STREAM_DEVICE = 0x18001; + public static final int BASS_CTYPE_STREAM_WAV = 0x40000; // WAVE flag (LOWORD=codec) + public static final int BASS_CTYPE_STREAM_WAV_PCM = 0x50001; + public static final int BASS_CTYPE_STREAM_WAV_FLOAT = 0x50003; + public static final int BASS_CTYPE_MUSIC_MOD = 0x20000; + public static final int BASS_CTYPE_MUSIC_MTM = 0x20001; + public static final int BASS_CTYPE_MUSIC_S3M = 0x20002; + public static final int BASS_CTYPE_MUSIC_XM = 0x20003; + public static final int BASS_CTYPE_MUSIC_IT = 0x20004; + public static final int BASS_CTYPE_MUSIC_MO3 = 0x00100; // MO3 flag + + public static final int BASS_ORIGRES_FLOAT = 0x10000; + + // 3D channel modes + public static final int BASS_3DMODE_NORMAL = 0; // normal 3D processing + public static final int BASS_3DMODE_RELATIVE = 1; // position is relative to the listener + public static final int BASS_3DMODE_OFF = 2; // no 3D processing + + // software 3D mixing algorithms (used with BASS_CONFIG_3DALGORITHM) + public static final int BASS_3DALG_DEFAULT = 0; + public static final int BASS_3DALG_OFF = 1; + public static final int BASS_3DALG_FULL = 2; + public static final int BASS_3DALG_LIGHT = 3; + + // BASS_SampleGetChannel flags + public static final int BASS_SAMCHAN_NEW = 1; // get a new playback channel + public static final int BASS_SAMCHAN_STREAM = 2; // create a stream + + // Special STREAMPROCs + public static final int STREAMPROC_DUMMY = 0; // "dummy" stream + public static final int STREAMPROC_PUSH = -1; // push stream + public static final int STREAMPROC_DEVICE = -2; // device mix stream + public static final int STREAMPROC_DEVICE_3D = -3; // device 3D mix stream + + // BASS_StreamCreateFileUser file systems + public static final int STREAMFILE_NOBUFFER = 0; + public static final int STREAMFILE_BUFFER = 1; + public static final int STREAMFILE_BUFFERPUSH = 2; + + public static final int BASS_STREAMPROC_END = 0x80000000; // end of user stream flag + public static final int BASS_STREAMPROC_DEVICE = -2; // device mix stream + public static final int BASS_STREAMPROC_DUMMY = 0; // "dummy" stream + public static final int BASS_STREAMPROC_PUSH = -1; // push stream + public static final int BASS_STREAMPROC_DEVICE_3D = -3; // device 3D mix stream + + // BASS_StreamPutFileData options + public static final int BASS_FILEDATA_END = 0; // end & close the file + + // BASS_StreamGetFilePosition modes + public static final int BASS_FILEPOS_CURRENT = 0; + public static final int BASS_FILEPOS_DECODE = BASS_FILEPOS_CURRENT; + public static final int BASS_FILEPOS_DOWNLOAD = 1; + public static final int BASS_FILEPOS_END = 2; + public static final int BASS_FILEPOS_START = 3; + public static final int BASS_FILEPOS_CONNECTED = 4; + public static final int BASS_FILEPOS_BUFFER = 5; + public static final int BASS_FILEPOS_SOCKET = 6; + public static final int BASS_FILEPOS_ASYNCBUF = 7; + public static final int BASS_FILEPOS_SIZE = 8; + public static final int BASS_FILEPOS_BUFFERING = 9; + public static final int BASS_FILEPOS_AVAILABLE = 10; + + // BASS_ChannelSetSync types + public static final int BASS_SYNC_POS = 0; + public static final int BASS_SYNC_END = 2; + public static final int BASS_SYNC_META = 4; + public static final int BASS_SYNC_SLIDE = 5; + public static final int BASS_SYNC_STALL = 6; + public static final int BASS_SYNC_DOWNLOAD = 7; + public static final int BASS_SYNC_FREE = 8; + public static final int BASS_SYNC_SETPOS = 11; + public static final int BASS_SYNC_MUSICPOS = 10; + public static final int BASS_SYNC_MUSICINST = 1; + public static final int BASS_SYNC_MUSICFX = 3; + public static final int BASS_SYNC_OGG_CHANGE = 12; + public static final int BASS_SYNC_DEV_FAIL = 14; + public static final int BASS_SYNC_DEV_FORMAT = 15; + public static final int BASS_SYNC_THREAD = 0x20000000; // flag: call sync in other thread + public static final int BASS_SYNC_MIXTIME = 0x40000000; // flag: sync at mixtime, else at playtime + public static final int BASS_SYNC_ONETIME = 0x80000000; // flag: sync only once, else continuously + + // BASS_ChannelIsActive return values + public static final int BASS_ACTIVE_STOPPED = 0; + public static final int BASS_ACTIVE_PLAYING =1; + public static final int BASS_ACTIVE_STALLED = 2; + public static final int BASS_ACTIVE_PAUSED = 3; + public static final int BASS_ACTIVE_PAUSED_DEVICE = 4; + + // Channel attributes + public static final int BASS_ATTRIB_FREQ = 1; + public static final int BASS_ATTRIB_VOL = 2; + public static final int BASS_ATTRIB_PAN = 3; + public static final int BASS_ATTRIB_EAXMIX = 4; + public static final int BASS_ATTRIB_NOBUFFER = 5; + public static final int BASS_ATTRIB_VBR = 6; + public static final int BASS_ATTRIB_CPU = 7; + public static final int BASS_ATTRIB_SRC = 8; + public static final int BASS_ATTRIB_NET_RESUME = 9; + public static final int BASS_ATTRIB_SCANINFO = 10; + public static final int BASS_ATTRIB_NORAMP = 11; + public static final int BASS_ATTRIB_BITRATE = 12; + public static final int BASS_ATTRIB_BUFFER = 13; + public static final int BASS_ATTRIB_GRANULE = 14; + public static final int BASS_ATTRIB_USER = 15; + public static final int BASS_ATTRIB_TAIL = 16; + public static final int BASS_ATTRIB_PUSH_LIMIT = 17; + public static final int BASS_ATTRIB_DOWNLOADPROC = 18; + public static final int BASS_ATTRIB_VOLDSP = 19; + public static final int BASS_ATTRIB_VOLDSP_PRIORITY = 20; + public static final int BASS_ATTRIB_MUSIC_AMPLIFY = 0x100; + public static final int BASS_ATTRIB_MUSIC_PANSEP = 0x101; + public static final int BASS_ATTRIB_MUSIC_PSCALER = 0x102; + public static final int BASS_ATTRIB_MUSIC_BPM = 0x103; + public static final int BASS_ATTRIB_MUSIC_SPEED = 0x104; + public static final int BASS_ATTRIB_MUSIC_VOL_GLOBAL = 0x105; + public static final int BASS_ATTRIB_MUSIC_VOL_CHAN = 0x200; // + channel # + public static final int BASS_ATTRIB_MUSIC_VOL_INST = 0x300; // + instrument # + + // BASS_ChannelSlideAttribute flags + public static final int BASS_SLIDE_LOG = 0x1000000; + + // BASS_ChannelGetData flags + public static final int BASS_DATA_AVAILABLE = 0; // query how much data is buffered + public static final int BASS_DATA_NOREMOVE = 0x10000000; // flag: don't remove data from recording buffer + public static final int BASS_DATA_FIXED = 0x20000000; // unused + public static final int BASS_DATA_FLOAT = 0x40000000; // flag: return floating-point sample data + public static final int BASS_DATA_FFT256 = 0x80000000; // 256 sample FFT + public static final int BASS_DATA_FFT512 = 0x80000001; // 512 FFT + public static final int BASS_DATA_FFT1024 = 0x80000002; // 1024 FFT + public static final int BASS_DATA_FFT2048 = 0x80000003; // 2048 FFT + public static final int BASS_DATA_FFT4096 = 0x80000004; // 4096 FFT + public static final int BASS_DATA_FFT8192 = 0x80000005; // 8192 FFT + public static final int BASS_DATA_FFT16384 = 0x80000006; // 16384 FFT + public static final int BASS_DATA_FFT32768 = 0x80000007; // 32768 FFT + public static final int BASS_DATA_FFT_INDIVIDUAL = 0x10; // FFT flag: FFT for each channel, else all combined + public static final int BASS_DATA_FFT_NOWINDOW = 0x20; // FFT flag: no Hanning window + public static final int BASS_DATA_FFT_REMOVEDC = 0x40; // FFT flag: pre-remove DC bias + public static final int BASS_DATA_FFT_COMPLEX = 0x80; // FFT flag: return complex data + public static final int BASS_DATA_FFT_NYQUIST = 0x100; // FFT flag: return extra Nyquist value + + // BASS_ChannelGetLevelEx flags + public static final int BASS_LEVEL_MONO = 1; // get mono level + public static final int BASS_LEVEL_STEREO = 2; // get stereo level + public static final int BASS_LEVEL_RMS = 4; // get RMS levels + public static final int BASS_LEVEL_VOLPAN = 8; // apply VOL/PAN attributes to the levels + public static final int BASS_LEVEL_NOREMOVE = 16; // don't remove data from recording buffer + + // BASS_ChannelGetTags types : what's returned + public static final int BASS_TAG_ID3 = 0; // ID3v1 tags : TAG_ID3 + public static final int BASS_TAG_ID3V2 = 1; // ID3v2 tags : ByteBuffer + public static final int BASS_TAG_OGG = 2; // OGG comments : String array + public static final int BASS_TAG_HTTP = 3; // HTTP headers : String array + public static final int BASS_TAG_ICY = 4; // ICY headers : String array + public static final int BASS_TAG_META = 5; // ICY metadata : String + public static final int BASS_TAG_APE = 6; // APE tags : String array + public static final int BASS_TAG_MP4 = 7; // MP4/iTunes metadata : String array + public static final int BASS_TAG_VENDOR = 9; // OGG encoder : String + public static final int BASS_TAG_LYRICS3 = 10; // Lyric3v2 tag : String + public static final int BASS_TAG_WAVEFORMAT = 14; // WAVE format : ByteBuffer containing WAVEFORMATEEX structure + public static final int BASS_TAG_AM_NAME = 16; // Android Media codec name : String + public static final int BASS_TAG_ID3V2_2 = 17; // ID3v2 tags (2nd block) : ByteBuffer + public static final int BASS_TAG_AM_MIME = 18; // Android Media MIME type : String + public static final int BASS_TAG_LOCATION = 19; // redirected URL : String + public static final int BASS_TAG_RIFF_INFO = 0x100; // RIFF "INFO" tags : String array + public static final int BASS_TAG_RIFF_BEXT = 0x101; // RIFF/BWF "bext" tags : TAG_BEXT + public static final int BASS_TAG_RIFF_CART = 0x102; // RIFF/BWF "cart" tags : TAG_CART + public static final int BASS_TAG_RIFF_DISP = 0x103; // RIFF "DISP" text tag : String + public static final int BASS_TAG_RIFF_CUE = 0x104; // RIFF "cue " chunk : TAG_CUE structure + public static final int BASS_TAG_RIFF_SMPL = 0x105; // RIFF "smpl" chunk : TAG_SMPL structure + public static final int BASS_TAG_APE_BINARY = 0x1000; // + index #, binary APE tag : TAG_APE_BINARY + public static final int BASS_TAG_MUSIC_NAME = 0x10000; // MOD music name : String + public static final int BASS_TAG_MUSIC_MESSAGE = 0x10001; // MOD message : String + public static final int BASS_TAG_MUSIC_ORDERS = 0x10002; // MOD order list : ByteBuffer + public static final int BASS_TAG_MUSIC_AUTH = 0x10003; // MOD author : UTF-8 string + public static final int BASS_TAG_MUSIC_INST = 0x10100; // + instrument #, MOD instrument name : String + public static final int BASS_TAG_MUSIC_CHAN = 0x10200; // + channel #, MOD channel name : String + public static final int BASS_TAG_MUSIC_SAMPLE = 0x10300; // + sample #, MOD sample name : String + public static final int BASS_TAG_BYTEBUFFER = 0x10000000; // flag: return a ByteBuffer instead of a String or TAG_ID3 + + // BASS_ChannelGetLength/GetPosition/SetPosition modes + public static final int BASS_POS_BYTE = 0; // byte position + public static final int BASS_POS_MUSIC_ORDER = 1; // order.row position, MAKELONG(order,row) + public static final int BASS_POS_OGG = 3; // OGG bitstream number + public static final int BASS_POS_END = 0x10; // trimmed end position + public static final int BASS_POS_LOOP = 0x11; // loop start positiom + public static final int BASS_POS_FLUSH = 0x1000000; // flag: flush decoder/FX buffers + public static final int BASS_POS_RESET = 0x2000000; // flag: reset user file buffers + public static final int BASS_POS_RELATIVE = 0x4000000; // flag: seek relative to the current position + public static final int BASS_POS_INEXACT = 0x8000000; // flag: allow seeking to inexact position + public static final int BASS_POS_DECODE = 0x10000000; // flag: get the decoding (not playing) position + public static final int BASS_POS_DECODETO = 0x20000000; // flag: decode to the position instead of seeking + public static final int BASS_POS_SCAN = 0x40000000; // flag: scan to the position + + // BASS_ChannelSetDevice/GetDevice option + public static final int BASS_NODEVICE = 0x20000; + + // DX8 effect types, use with BASS_ChannelSetFX + public static final int BASS_FX_DX8_CHORUS = 0; + public static final int BASS_FX_DX8_COMPRESSOR = 1; + public static final int BASS_FX_DX8_DISTORTION = 2; + public static final int BASS_FX_DX8_ECHO = 3; + public static final int BASS_FX_DX8_FLANGER = 4; + public static final int BASS_FX_DX8_GARGLE = 5; + public static final int BASS_FX_DX8_I3DL2REVERB = 6; + public static final int BASS_FX_DX8_PARAMEQ = 7; + public static final int BASS_FX_DX8_REVERB = 8; + public static final int BASS_FX_VOLUME = 9; + + public static final int BASS_DX8_PHASE_NEG_180 = 0; + public static final int BASS_DX8_PHASE_NEG_90 = 1; + public static final int BASS_DX8_PHASE_ZERO = 2; + public static final int BASS_DX8_PHASE_90 = 3; + public static final int BASS_DX8_PHASE_180 = 4; + } + + + // Device info structure + @ShortName("BASS_DEVICEINFO") + @Structure.FieldOrder({"name","driver","flags"}) + public static class BASS_DEVICEINFO extends Structure{ + public String name; // description + public String driver; // driver + public int flags; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + + @Override + public String toString() { + return MessageFormat.format("Name={0}, Driver={1}, Flags={2}", name, driver, Integer.toHexString(flags)); + } + + /** + * Check if Device is enabled + * @return true if enabled + */ + public boolean getIsEnabled() { + return (flags & Constant.BASS_DEVICE_ENABLED)>0; + } + + /** + * Check if Device is Initialized + * @return true if inited + */ + public boolean getIsInitialized() { + return (flags & Constant.BASS_DEVICE_INIT)>0; + } + + /** + * Check if Device is System Default + * @return true if system default + */ + public boolean getIsDefault() { + return (flags & Constant.BASS_DEVICE_DEFAULT)>0; + } + + + } + + + @BA.ShortName("BASS_INFO") + @Structure.FieldOrder({"flags","hwsize","hwfree","freesam","free3d","minrate","maxrate","eax","minbuf","dsver","latency","initflags","speakers","freq"}) + public static class BASS_INFO extends Structure { + public int flags; // device capabilities (DSCAPS_xxx flags) + public int hwsize; // unused + public int hwfree; // unused + public int freesam; // unused + public int free3d; // unused + public int minrate; // unused + public int maxrate; // unused + public int eax; // unused + public int minbuf; // recommended minimum buffer length in ms + public int dsver; // DirectSound version + public int latency; // average delay (in ms) before start of playback + public int initflags; // BASS_Init "flags" parameter + public int speakers; // number of speakers available + public int freq; // current output rate + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + // Recording device info structure + @BA.ShortName("BASS_RECORDINFO") + @Structure.FieldOrder({"flags","formats","inputs","singlein","freq"}) + public static class BASS_RECORDINFO extends Structure { + public int flags; // device capabilities (DSCCAPS_xxx flags) + public int formats; // supported standard formats (WAVE_FORMAT_xxx flags) + public int inputs; // number of inputs + public boolean singlein; // TRUE = only 1 input can be set at a time + public int freq; // current input rate + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + // Sample info structure + @Structure.FieldOrder({"freq","volume","pan","flags","length","max","origres","chans" + ,"mingap","mode3d","mindist","maxdist","iangle","oangle","outvol","vam","priority"}) + public static class BASS_SAMPLE extends Structure{ + public int freq; // default playback rate + public float volume; // default volume (0-1) + public float pan; // default pan (-1=left, 0=middle, 1=right) + public int flags; // BASS_SAMPLE_xxx flags + public int length; // length (in bytes) + public int max; // maximum simultaneous playbacks + public int origres; // original resolution bits + public int chans; // number of channels + public int mingap; // minimum gap (ms) between creating channels + public int mode3d; // BASS_3DMODE_xxx mode + public float mindist; // minimum distance + public float maxdist; // maximum distance + public int iangle; // angle of inside projection cone + public int oangle; // angle of outside projection cone + public float outvol; // delta-volume outside the projection cone + public int vam; // unused + public int priority; // unused + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + // Channel info structure + @BA.ShortName("BASS_CHANNELINFO") + @Structure.FieldOrder({"freq","chans","flags","ctype","origres","plugin","sample","filename"}) + public static class BASS_CHANNELINFO extends Structure{ + public int freq; // default playback rate + public int chans; // channels + public int flags; + public int ctype; // type of channel + public int origres; // original resolution + public int plugin; + public int sample; + public String filename; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + + + @Structure.FieldOrder({"ctype","name","exts"}) + public static class BASS_PLUGINFORM extends Structure{ + public int ctype; // channel type + public String name; // format description + public String exts; // file extension filter (*.ext1;*.ext2;etc...) + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"version","formatc","formats"}) + public static class BASS_PLUGININFO extends Structure{ + public int version; // version (same form as BASS_GetVersion) + public int formatc; // number of formats + public BASS_PLUGINFORM[] formats = new BASS_PLUGINFORM[formatc]; // the array of formats + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() { + super.autoRead(); + } + } + + // 3D vector (for 3D positions/velocities/orientations) + @Structure.FieldOrder({"x","y","z"}) + public static class BASS_3DVECTOR extends Structure{ + public BASS_3DVECTOR() {} + public BASS_3DVECTOR(float _x, float _y, float _z) { x=_x; y=_y; z=_z; } + public float x; // +=right, -=left + public float y; // +=up, -=down + public float z; // +=front, -=behind + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + + public interface STREAMPROC extends Callback + { + int STREAMPROC(int handle, Pointer buffer, int length, Pointer user); + /* User stream callback function. + handle : The stream that needs writing + buffer : Buffer to write the samples in + length : Number of bytes to write + user : The 'user' parameter value given when calling BASS_StreamCreate + RETURN : Number of bytes written. Set the BASS_STREAMPROC_END flag to end + the stream. */ + } + + public interface FILECLOSEPROC extends Callback{ + void FILECLOSEPROC(Pointer user); + } + public interface FILELENPROC extends Callback{ + long FILELENPROC(Pointer user); + } + public interface FILEREADPROC extends Callback{ + int FILEREADPROC(Pointer buffer, int length, Pointer user); + } + public interface FILESEEKPROC extends Callback{ + boolean FILESEEKPROC(long offset, Pointer user); + } + + @Structure.FieldOrder({"FILECLOSEPROC","FILELENPROC","FILEREADPROC","FILESEEKPROC"}) + public static class BASS_FILEPROCS extends Structure + { + public FILECLOSEPROC FILECLOSEPROC; + public FILELENPROC FILELENPROC; + public FILEREADPROC FILEREADPROC; + public FILESEEKPROC FILESEEKPROC; + public BASS_FILEPROCS() { + this.FILECLOSEPROC = null; + this.FILELENPROC = null; + this.FILEREADPROC = null; + this.FILESEEKPROC = null; + } + public BASS_FILEPROCS(FILECLOSEPROC fc, FILELENPROC fl, FILEREADPROC fr, FILESEEKPROC fs) { + this.FILECLOSEPROC = fc; + this.FILELENPROC = fl; + this.FILEREADPROC = fr; + this.FILESEEKPROC = fs; + } + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + + public interface DOWNLOADPROC extends Callback + { + void DOWNLOADPROC(Pointer buffer, int length, Pointer user); + /* Internet stream download callback function. + buffer : Buffer containing the downloaded data... NULL=end of download + length : Number of bytes in the buffer + user : The 'user' parameter value given when calling BASS_StreamCreateURL */ + } + + + public interface SYNCPROC extends Callback + { + void SYNCPROC(int handle, int channel, int data, Pointer user); + /* Sync callback function. + handle : The sync that has occured + channel: Channel that the sync occured in + data : Additional data associated with the sync's occurance + user : The 'user' parameter given when calling BASS_ChannelSetSync */ + } + + public interface DSPPROC extends Callback + { + void DSPPROC(int handle, int channel, Pointer buffer, int length, Pointer user); + /* DSP callback function. + handle : The DSP handle + channel: Channel that the DSP is being applied to + buffer : Buffer to apply the DSP to + length : Number of bytes in the buffer + user : The 'user' parameter given when calling BASS_ChannelSetDSP */ + } + + public interface RECORDPROC extends Callback + { + boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user); + /* Recording callback function. + handle : The recording handle + buffer : Buffer containing the recorded sample data + length : Number of bytes + user : The 'user' parameter value given when calling BASS_RecordStart + RETURN : true = continue recording, false = stop */ + } + + + // ID3v1 tag structure + @Structure.FieldOrder({"id","title","artist","album","year","comment","genre","track"}) + public static class TAG_ID3 extends Structure { + public String id; + public String title; + public String artist; + public String album; + public String year; + public String comment; + public byte genre; + public byte track; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + // Binary APE tag structure + @Structure.FieldOrder({"key","data","length"}) + public static class TAG_APE_BINARY extends Structure{ + public String key; + public Pointer data; + public int length; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + + @Structure.FieldOrder({"fWetDryMix","fDepth","fFeedback","fFrequency","lWaveform","fDelay","lPhase"}) + public static class BASS_DX8_CHORUS extends Structure { + public float fWetDryMix; + public float fDepth; + public float fFeedback; + public float fFrequency; + public int lWaveform; // 0=triangle, 1=sine + public float fDelay; + public int lPhase; // BASS_DX8_PHASE_xxx + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"fGain","fEdge","fPostEQCenterFrequency","fPostEQBandwidth","fPreLowpassCutoff"}) + public static class BASS_DX8_DISTORTION extends Structure { + public float fGain; + public float fEdge; + public float fPostEQCenterFrequency; + public float fPostEQBandwidth; + public float fPreLowpassCutoff; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"fWetDryMix","fFeedback","fLeftDelay","fRightDelay","lPanDelay"}) + public static class BASS_DX8_ECHO extends Structure { + public float fWetDryMix; + public float fFeedback; + public float fLeftDelay; + public float fRightDelay; + public boolean lPanDelay; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"fWetDryMix","fDepth","fFeedback","fFrequency","lWaveform","fDelay","lPhase"}) + public static class BASS_DX8_FLANGER extends Structure { + public float fWetDryMix; + public float fDepth; + public float fFeedback; + public float fFrequency; + public int lWaveform; // 0=triangle, 1=sine + public float fDelay; + public int lPhase; // BASS_DX8_PHASE_xxx + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"fCenter","fBandwidth","fGain"}) + public static class BASS_DX8_PARAMEQ extends Structure { + public float fCenter; + public float fBandwidth; + public float fGain; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"fInGain","fReverbMix","fReverbTime","fHighFreqRTRatio"}) + public static class BASS_DX8_REVERB extends Structure { + public float fInGain; + public float fReverbMix; + public float fReverbTime; + public float fHighFreqRTRatio; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + @Structure.FieldOrder({"fTarget","fCurrent","fTime","lCurve"}) + public static class BASS_FX_VOLUME_PARAM extends Structure { + public float fTarget; + public float fCurrent; + public float fTime; + public int lCurve; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + @Structure.FieldOrder({"value"}) + public static class FloatValue { + public float value; + } + + public boolean BASS_SetConfig(int option, int value); + public int BASS_GetConfig(int option); + public boolean BASS_SetConfigPtr(int option, Object value); + public Object BASS_GetConfigPtr(int option); + public int BASS_GetVersion(); + public int BASS_ErrorGetCode(); + public boolean BASS_GetDeviceInfo(int device, BASS_DEVICEINFO info); + public boolean BASS_Init(int device, int freq, int flags); + public boolean BASS_Free(); + public boolean BASS_SetDevice(int device); + public int BASS_GetDevice(); + public boolean BASS_GetInfo(BASS_INFO info); + public boolean BASS_Start(); + public boolean BASS_Stop(); + public boolean BASS_Pause(); + public int BASS_IsStarted(); + public boolean BASS_Update(int length); + public float BASS_GetCPU(); + public boolean BASS_SetVolume(float volume); + public float BASS_GetVolume(); + + public boolean BASS_Set3DFactors(float distf, float rollf, float doppf); + public boolean BASS_Get3DFactors(FloatValue distf, FloatValue rollf, FloatValue doppf); + public boolean BASS_Set3DPosition(BASS_3DVECTOR pos, BASS_3DVECTOR vel, BASS_3DVECTOR front, BASS_3DVECTOR top); + public boolean BASS_Get3DPosition(BASS_3DVECTOR pos, BASS_3DVECTOR vel, BASS_3DVECTOR front, BASS_3DVECTOR top); + public void BASS_Apply3D(); + + public int BASS_PluginLoad(String file, int flags); + public boolean BASS_PluginFree(int handle); + public boolean BASS_PluginEnable(int handle, boolean enable); + public BASS_PLUGININFO BASS_PluginGetInfo(int handle); + + public int BASS_SampleLoad(String file, long offset, int length, int max, int flags); + public int BASS_SampleLoad(Pointer file, long offset, int length, int max, int flags); + public int BASS_SampleCreate(int length, int freq, int chans, int max, int flags); + public boolean BASS_SampleFree(int handle); + public boolean BASS_SampleSetData(int handle, Pointer buffer); + public boolean BASS_SampleGetData(int handle, Pointer buffer); + public boolean BASS_SampleGetInfo(int handle, BASS_SAMPLE info); + public boolean BASS_SampleSetInfo(int handle, BASS_SAMPLE info); + public int BASS_SampleGetChannel(int handle, boolean onlynew); + public int BASS_SampleGetChannels(int handle, int[] channels); + public boolean BASS_SampleStop(int handle); + public int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user); + public int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags); + public int BASS_StreamCreateFile(String file, long offset, long length, int flags); + public int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags); + public int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); + public int BASS_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); + public boolean BASS_StreamFree(int handle); + public long BASS_StreamGetFilePosition(int handle, int mode); + public int BASS_StreamPutData(int handle, Pointer buffer, int length); + public int BASS_StreamPutFileData(int handle, Pointer buffer, int length); + + public int BASS_MusicLoad(String file, long offset, int length, int flags, int freq); + public int BASS_MusicLoad(Pointer file, long offset, int length, int flags, int freq); + public boolean BASS_MusicFree(int handle); + + public boolean BASS_RecordGetDeviceInfo(int device, BASS_DEVICEINFO info); + public boolean BASS_RecordInit(int device); + public boolean BASS_RecordFree(); + public boolean BASS_RecordSetDevice(int device); + public int BASS_RecordGetDevice(); + public boolean BASS_RecordGetInfo(BASS_RECORDINFO info); + public String BASS_RecordGetInputName(int input); + public boolean BASS_RecordSetInput(int input, int flags, float volume); + public int BASS_RecordGetInput(int input, FloatValue volume); + public int BASS_RecordStart(int freq, int chans, int flags, RECORDPROC proc, Pointer user); + + public double BASS_ChannelBytes2Seconds(int handle, long pos); + public long BASS_ChannelSeconds2Bytes(int handle, double pos); + public int BASS_ChannelGetDevice(int handle); + public boolean BASS_ChannelSetDevice(int handle, int device); + public int BASS_ChannelIsActive(int handle); + public boolean BASS_ChannelGetInfo(int handle, BASS_CHANNELINFO info); + public Object BASS_ChannelGetTags(int handle, int tags); + public long BASS_ChannelFlags(int handle, int flags, int mask); + public boolean BASS_ChannelLock(int handle, boolean lock); + public boolean BASS_ChannelFree(int handle); + public boolean BASS_ChannelPlay(int handle, boolean restart); + public boolean BASS_ChannelStart(int handle); + public boolean BASS_ChannelStop(int handle); + public boolean BASS_ChannelPause(int handle); + public boolean BASS_ChannelUpdate(int handle, int length); + public boolean BASS_ChannelSetAttribute(int handle, int attrib, float value); + public boolean BASS_ChannelGetAttribute(int handle, int attrib, FloatValue value); + public boolean BASS_ChannelSlideAttribute(int handle, int attrib, float value, int time); + public boolean BASS_ChannelIsSliding(int handle, int attrib); + public boolean BASS_ChannelSetAttributeEx(int handle, int attrib, Pointer value, int size); + public boolean BASS_ChannelSetAttributeDOWNLOADPROC(int handle, DOWNLOADPROC proc, Pointer user); + public int BASS_ChannelGetAttributeEx(int handle, int attrib, Pointer value, int size); + public boolean BASS_ChannelSet3DAttributes(int handle, int mode, float min, float max, int iangle, int oangle, float outvol); + public boolean BASS_ChannelGet3DAttributes(int handle, Integer mode, FloatValue min, FloatValue max, Integer iangle, Integer oangle, FloatValue outvol); + public boolean BASS_ChannelSet3DPosition(int handle, BASS_3DVECTOR pos, BASS_3DVECTOR orient, BASS_3DVECTOR vel); + public boolean BASS_ChannelGet3DPosition(int handle, BASS_3DVECTOR pos, BASS_3DVECTOR orient, BASS_3DVECTOR vel); + public long BASS_ChannelGetLength(int handle, int mode); + public boolean BASS_ChannelSetPosition(int handle, long pos, int mode); + public long BASS_ChannelGetPosition(int handle, int mode); + public int BASS_ChannelGetLevel(int handle); + public boolean BASS_ChannelGetLevelEx(int handle, float[] levels, float length, int flags); + public int BASS_ChannelGetData(int handle, Pointer buffer, int length); + public int BASS_ChannelSetSync(int handle, int type, long param, SYNCPROC proc, Pointer user); + public boolean BASS_ChannelRemoveSync(int handle, int sync); + public boolean BASS_ChannelSetLink(int handle, int chan); + public boolean BASS_ChannelRemoveLink(int handle, int chan); + public int BASS_ChannelSetDSP(int handle, DSPPROC proc, Pointer user, int priority); + public boolean BASS_ChannelRemoveDSP(int handle, int dsp); + public int BASS_ChannelSetFX(int handle, int type, int priority); + public boolean BASS_ChannelRemoveFX(int handle, int fx); + + public boolean BASS_FXSetParameters(int handle, Object params); + public boolean BASS_FXGetParameters(int handle, Object params); + public boolean BASS_FXSetPriority(int handle, int priority); + public boolean BASS_FXReset(int handle); + + //public int BASS_StreamCreateConst(int freq, int chans, int flags, int proc, Pointer user); + +// public default int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user) { +// return BASS_StreamCreateConst(freq, chans, flags, proc, user); +// } + + public int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user); + + public static class Utils { + public static int LOBYTE(int n) { return n&0xff; } + public static int HIBYTE(int n) { return (n>>8)&0xff; } + public static int LOWORD(int n) { return n&0xffff; } + public static int HIWORD(int n) { return (n>>16)&0xffff; } + public static int MAKEWORD(int a, int b) { return (a&0xff)|((b&0xff)<<8); } + public static int MAKELONG(int a, int b) { return (a&0xffff)|(b<<16); } + } + + /** + * Get Bass Error in String + * @param function function name + * @param err error code from BASS_ErrorGetCode + * @return reason of error in string + */ + public default String GetBassError(String function, int err) { + if (err==0) + return function+": SUCCESS"; + else if (err==Constant.BASS_ERROR_DEVICE) + return function+": Device Is Invalid"; + else if (err==Constant.BASS_ERROR_INIT) + return function+": BASS_Init/BASS_RecordInit has not been called"; + else if (err==Constant.BASS_ERROR_MEM) + return function+": Insufficient Memory"; + else if (err==Constant.BASS_ERROR_HANDLE) + return function+": Invalid Handle"; + else if (err==Constant.BASS_ERROR_NO3D) + return function+": Couldnt initialize 3D support"; + else if (err==Constant.BASS_ERROR_SPEAKER) + return function+": Specified SPEAKERS flags are invalid"; + else if (err==Constant.BASS_ERROR_UNKNOWN) { + return function+": Unknown Error "; + } + else { + StringBuilder str = new StringBuilder(); + str.append(function).append(": "); + String funct = function.trim().toLowerCase(); + switch(funct) { + case "bass_free": + switch(err) { + case Constant.BASS_ERROR_BUSY: + return str.append("Device curently being reinitialized").toString(); + default : + return str.append("Unknown error code="+err).toString(); + } + case "bass_init": + case "bass_recordinit": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL : + str.append("BASS_DEVICE_REINIT cannot be used when Device is -1. Use Real Device Number instead"); + return str.toString(); + case Constant.BASS_ERROR_ALREADY : + str.append("Device already Initialized"); + return str.toString(); + case Constant.BASS_ERROR_DRIVER : + str.append("Device Driver not available"); + return str.toString(); + case Constant.BASS_ERROR_BUSY : + str.append("Device has exclusive use from other"); + return str.toString(); + case Constant.BASS_ERROR_FORMAT : + str.append("Format invalid, Try changing Frequency parameter"); + return str.toString(); + default : + str.append("Unknown error code="+err); + return str.toString(); + + } + case "bass_streamcreate": + case "bass_streamcreateconst": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL : + str.append("BASS_STREAM_FREE cannot be combined with BASS_STREAM_DECODE"); + return str.toString(); + case Constant.BASS_ERROR_FORMAT : + str.append("Sample format is not supported"); + return str.toString(); + case Constant.BASS_ERROR_SPEAKER: + str.append("Specified Speaker Flag are invalid"); + return str.toString(); + default : + str.append("Unknown error code="+err); + return str.toString(); + } + case "bass_streamcreatefile" : + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL : + str.append("BASS_STREAM_AUTOFREE cannot be combined with BASS_STREAM_DECODE"); + return str.toString(); + case Constant.BASS_ERROR_ILLPARAM : + str.append("Length must be specified when streaming from memory"); + return str.toString(); + case Constant.BASS_ERROR_FILEOPEN: + str.append("File Cant be opened"); + return str.toString(); + case Constant.BASS_ERROR_FILEFORM: + str.append("File Format not recognized"); + return str.toString(); + case Constant.BASS_ERROR_NOTAUDIO: + str.append("File doesnt contain audio, or contain video but video is disabled"); + return str.toString(); + case Constant.BASS_ERROR_CODEC: + str.append("File use a codec that is not supported"); + return str.toString(); + case Constant.BASS_ERROR_FORMAT: + str.append("Sample Format is not supported"); + return str.toString(); + case Constant.BASS_ERROR_SPEAKER: + str.append("Specified SPEAKER flag is invalid"); + return str.toString(); + default: + str.append("Unknown error code="+err); + return str.toString(); + } + case "bass_channelgetdata": + switch(err) { + case Constant.BASS_ERROR_ENDED : + return str.append("Channel has reached the end").toString(); + + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("BASS_DATA_AVAILABLE cant be use with decoding channel, or Not Possible to get Data from final output mix stream using STREAMPROC_DEVICE").toString(); + + case Constant.BASS_ERROR_ILLPARAM: + return str.append("invalid flags were used").toString(); + + default: + return str.append("Unknown error code="+err).toString(); + + } + case "bass_mixer_streamcreate": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("BASS_STREAM_AUTOFREE cannot be combined with BASS_STREAM_DECODE").toString(); + + case Constant.BASS_ERROR_FORMAT: + return str.append("Sample format is not supported").toString(); + + default: + return str.append("Unknown error code="+err).toString(); + + } + case "bass_mixer_streamaddchannel": + switch(err) { + case Constant.BASS_ERROR_DECODE: + return str.append("Source is not decoding channel").toString(); + + case Constant.BASS_ERROR_ALREADY: + return str.append("Source already added to a mixer").toString(); + + default: + return str.append("Unknown error code="+err).toString(); + + } + case "bass_streamfree": + case "bass_channelfree": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("Device Streams STREAMPROC_DEVICE cannot be freed").toString(); + + default: + return str.append("Unknown error code="+err).toString(); + + + } + case "bass_channelstart": + case "bass_channelplay": + switch(err) { + case Constant.BASS_ERROR_DECODE: + return str.append("Handle is Decoding Channel, cant be played").toString(); + + case Constant.BASS_ERROR_START: + return str.append("Output is paused/stopped").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_channelsetdsp": + switch(err) { + case Constant.BASS_ERROR_ILLPARAM: + return str.append("DSPPROC cant NULL").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_channelsetattribute": + switch(err) { + case Constant.BASS_ERROR_ILLTYPE: + return str.append("Attribute Not Valid").toString(); + case Constant.BASS_ERROR_ILLPARAM : + return str.append("Valud not valid").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_channelgetattribute": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("Attribute Not Available").toString(); + case Constant.BASS_ERROR_ILLTYPE: + return str.append("Attribute Not Valid").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_setvolume": + case "bass_getvolume": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("No Volume Control for No-Sound device").toString(); + case Constant.BASS_ERROR_ILLPARAM: + return str.append("Volume is invalid").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_recordgetdeviceinfo": + switch(err) { + case Constant.BASS_ERROR_DX: + return str.append("DirectX not installed").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_recordgetinputname": + switch(err) { + case Constant.BASS_ERROR_ILLPARAM: + return str.append("Input is Invalid").toString(); + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("Master Input is not available").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_recordstart": + switch(err) { + case Constant.BASS_ERROR_BUSY: + return str.append("Device is Busy. Another Recording need to be stopped before starting another one").toString(); + case Constant.BASS_ERROR_DENIED: + return str.append("Recording permission denied").toString(); + case Constant.BASS_ERROR_FORMAT: + return str.append("Sample format is not supported").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_recordsetinput": + case "bass_recordgetinput": + switch(err) { + case Constant.BASS_ERROR_ILLPARAM : + return str.append("Input or Volume is invalid").toString(); + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("Input doesnt have volume control").toString(); + default: + return str.append("Unknown error code="+err).toString(); + } + case "bass_pluginload": + switch(err) { + case Constant.BASS_ERROR_FILEOPEN: + return str.append("Plugin could not be opened").toString(); + case Constant.BASS_ERROR_FILEFORM: + return str.append("File is not plugin").toString(); + case Constant.BASS_ERROR_VERSION: + return str.append("Plugin version mismatch").toString(); + case Constant.BASS_ERROR_ALREADY: + return str.append("Plugin already loaded").toString(); + } + case "bass_encode_aac_start": + case "bass_encode_mp3_start": + case "bass_encode_ogg_start": + case "bass_encode_flac_start": + case "bass_encode_aac_startfile": + case "bass_encode_mp3_startfile": + case "bass_encode_ogg_startfile": + case "bass_encode_flac_startfile": + switch(err) { + case Constant.BASS_ERROR_FORMAT: + return str.append("Format not supported").toString(); + case Constant.BASS_ERROR_ILLPARAM: + return str.append("options contains invalid setting").toString(); + case Constant.BASS_ERROR_FILEOPEN: + return str.append("File could not be opened").toString(); + case Constant.BASS_ERROR_FILEFORM: + return str.append("File Format invalid").toString(); + case Constant.BASS_ERROR_NOTAVAIL: + return str.append("Function not available without FPU").toString(); + case Constant.BASS_ERROR_CREATE: + return str.append("Target file could not be created").toString(); + default : + return str.append("Unknown error code="+err).toString(); + } + case "bass_streamputdata": + switch(err) { + case Constant.BASS_ERROR_NOTAVAIL : + str.append("Stream is not using push system"); + return str.toString(); + case Constant.BASS_ERROR_ILLPARAM: + str.append("Length is not valid"); + return str.toString(); + case Constant.BASS_ERROR_ENDED: + str.append("Stream has ended"); + return str.toString(); + default: + str.append("Unknown error code = "+err); + return str.toString(); + } + default : + return "GetBassError not implemented at "+function; + } + } + } + + /** + * Get Bass error in String + * @param function function name + * @return reason of error in string + */ + public default String GetBassError(String function) { + return GetBassError(function, BASS.BASS_ErrorGetCode()); + + } +} diff --git a/src/bass/BassMixLibrary.java b/src/bass/BassMixLibrary.java new file mode 100644 index 0000000..be81986 --- /dev/null +++ b/src/bass/BassMixLibrary.java @@ -0,0 +1,141 @@ +package bass; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import anywheresoftware.b4a.BA.Hide; +import bass.BassLibrary.SYNCPROC; + +/** + * Bass Mixer Library + * @author rdkartono + * + */ +public interface BassMixLibrary extends Library { + BassMixLibrary BASSMIX = (BassMixLibrary) Native.load("bassmix",BassMixLibrary.class); + + public interface Constant{ + // Additional BASS_SetConfig options + public static final int BASS_CONFIG_MIXER_BUFFER = 0x10601; + public static final int BASS_CONFIG_MIXER_POSEX = 0x10602; + public static final int BASS_CONFIG_SPLIT_BUFFER = 0x10610; + + // BASS_Mixer_StreamCreate flags + public static final int BASS_MIXER_RESUME = 0x1000; // resume stalled immediately upon new/unpaused source + public static final int BASS_MIXER_POSEX = 0x2000; // enable BASS_Mixer_ChannelGetPositionEx support + public static final int BASS_MIXER_NOSPEAKER = 0x4000; // ignore speaker arrangement + public static final int BASS_MIXER_QUEUE = 0x8000; // queue sources + public static final int BASS_MIXER_END = 0x10000; // end the stream when there are no sources + public static final int BASS_MIXER_NONSTOP = 0x20000; // don't stall when there are no sources + + // BASS_Mixer_StreamAddChannel/Ex flags + public static final int BASS_MIXER_CHAN_ABSOLUTE = 0x1000; // start is an absolute position + public static final int BASS_MIXER_CHAN_BUFFER = 0x2000; // buffer data for BASS_Mixer_ChannelGetData/Level + public static final int BASS_MIXER_CHAN_LIMIT = 0x4000; // limit mixer processing to the amount available from this source + public static final int BASS_MIXER_CHAN_MATRIX = 0x10000; // matrix mixing + public static final int BASS_MIXER_CHAN_PAUSE = 0x20000; // don't process the source + public static final int BASS_MIXER_CHAN_DOWNMIX = 0x400000; // downmix to stereo/mono + public static final int BASS_MIXER_CHAN_NORAMPIN = 0x800000; // don't ramp-in the start + public static final int BASS_MIXER_BUFFER = BASS_MIXER_CHAN_BUFFER; + public static final int BASS_MIXER_LIMIT = BASS_MIXER_CHAN_LIMIT; + public static final int BASS_MIXER_MATRIX = BASS_MIXER_CHAN_MATRIX; + public static final int BASS_MIXER_PAUSE = BASS_MIXER_CHAN_PAUSE; + public static final int BASS_MIXER_DOWNMIX = BASS_MIXER_CHAN_DOWNMIX; + public static final int BASS_MIXER_NORAMPIN = BASS_MIXER_CHAN_NORAMPIN; + + // Mixer attributes + public static final int BASS_ATTRIB_MIXER_LATENCY = 0x15000; + public static final int BASS_ATTRIB_MIXER_THREADS = 0x15001; + public static final int BASS_ATTRIB_MIXER_VOL = 0x15002; + + // Additional BASS_Mixer_ChannelIsActive return values + public static final int BASS_ACTIVE_WAITING = 5; + public static final int BASS_ACTIVE_QUEUED = 6; + + // BASS_Split_StreamCreate flags + public static final int BASS_SPLIT_SLAVE = 0x1000; // only read buffered data + public static final int BASS_SPLIT_POS = 0x2000; + + // Splitter attributes + public static final int BASS_ATTRIB_SPLIT_ASYNCBUFFER = 0x15010; + public static final int BASS_ATTRIB_SPLIT_ASYNCPERIOD = 0x15011; + + // Envelope types + public static final int BASS_MIXER_ENV_FREQ = 1; + public static final int BASS_MIXER_ENV_VOL = 2; + public static final int BASS_MIXER_ENV_PAN = 3; + public static final int BASS_MIXER_ENV_LOOP = 0x10000; // flag: loop + public static final int BASS_MIXER_ENV_REMOVE = 0x20000; // flag: remove at end + + // Additional sync types + public static final int BASS_SYNC_MIXER_ENVELOPE = 0x10200; + public static final int BASS_SYNC_MIXER_ENVELOPE_NODE = 0x10201; + public static final int BASS_SYNC_MIXER_QUEUE = 0x10202; + + // Additional BASS_Mixer_ChannelSetPosition flag + public static final int BASS_POS_MIXER_RESET = 0x10000; // flag: clear mixer's playback buffer + + // Additional BASS_Mixer_ChannelGetPosition mode + public static final int BASS_POS_MIXER_DELAY = 5; + + // BASS_CHANNELINFO types + public static final int BASS_CTYPE_STREAM_MIXER = 0x10800; + public static final int BASS_CTYPE_STREAM_SPLIT = 0x10801; + } + + + // Envelope node + @Structure.FieldOrder({"pos, value"}) + public static class BASS_MIXER_NODE extends Structure { + public BASS_MIXER_NODE() {} + public BASS_MIXER_NODE(long _pos, float _value) { pos=_pos; value=_value; } + public long pos; + public float value; + + @Override + @Hide + public void autoWrite() {super.autoWrite();} + + @Override + @Hide + public void autoRead() {super.autoRead();} + } + + + + public int BASS_Mixer_GetVersion(); + + public int BASS_Mixer_StreamCreate(int freq, int chans, int flags); + public boolean BASS_Mixer_StreamAddChannel(int handle, int channel, int flags); + public boolean BASS_Mixer_StreamAddChannelEx(int handle, int channel, int flags, long start, long length); + public int BASS_Mixer_StreamGetChannels(int handle, int[] channels, int count); + + public int BASS_Mixer_ChannelGetMixer(int handle); + public int BASS_Mixer_ChannelIsActive(int handle); + public int BASS_Mixer_ChannelFlags(int handle, int flags, int mask); + public boolean BASS_Mixer_ChannelRemove(int handle); + public boolean BASS_Mixer_ChannelSetPosition(int handle, long pos, int mode); + public long BASS_Mixer_ChannelGetPosition(int handle, int mode); + public long BASS_Mixer_ChannelGetPositionEx(int channel, int mode, int delay); + public int BASS_Mixer_ChannelGetLevel(int handle); + public boolean BASS_Mixer_ChannelGetLevelEx(int handle, float[] levels, float length, int flags); + public int BASS_Mixer_ChannelGetData(int handle, Pointer buffer, int length); + public int BASS_Mixer_ChannelSetSync(int handle, int type, long param, SYNCPROC proc, Pointer user); + public boolean BASS_Mixer_ChannelRemoveSync(int channel, int sync); + public boolean BASS_Mixer_ChannelSetMatrix(int handle, float[][] matrix); + public boolean BASS_Mixer_ChannelSetMatrixEx(int handle, float[][] matrix, float time); + public boolean BASS_Mixer_ChannelGetMatrix(int handle, float[][] matrix); + public boolean BASS_Mixer_ChannelSetEnvelope(int handle, int type, BASS_MIXER_NODE[] nodes, int count); + public boolean BASS_Mixer_ChannelSetEnvelopePos(int handle, int type, long pos); + public long BASS_Mixer_ChannelGetEnvelopePos(int handle, int type, Float value); + + public int BASS_Split_StreamCreate(int channel, int flags, int[] chanmap); + public int BASS_Split_StreamGetSource(int handle); + public int BASS_Split_StreamGetSplits(int handle, int[] splits, int count); + public boolean BASS_Split_StreamReset(int handle); + public boolean BASS_Split_StreamResetEx(int handle, int offset); + public int BASS_Split_StreamGetAvailable(int handle); + +} diff --git a/src/bass/BassOPUSLibrary.java b/src/bass/BassOPUSLibrary.java new file mode 100644 index 0000000..33ac2fc --- /dev/null +++ b/src/bass/BassOPUSLibrary.java @@ -0,0 +1,23 @@ +package bass; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import bass.BassLibrary.BASS_FILEPROCS; +import bass.BassLibrary.DOWNLOADPROC; + +public interface BassOPUSLibrary extends Library { + BassOPUSLibrary BASS_OPUS = (BassOPUSLibrary) Native.load("bassopus",BassOPUSLibrary.class); + public interface Constant{ + // BASS_CHANNELINFO type + public static final int BASS_CTYPE_STREAM_OPUS = 0x11200; + + // Additional attributes + public static final int BASS_ATTRIB_OPUS_ORIGFREQ = 0x13000; + } + public int BASS_OPUS_StreamCreateFile(String file, long offset, long length, int flags); + public int BASS_OPUS_StreamCreateFile(Pointer file, long offset, long length, int flags); + public int BASS_OPUS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); + public int BASS_OPUS_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); +} diff --git a/src/bass/CompressInputStream.java b/src/bass/CompressInputStream.java new file mode 100644 index 0000000..fb172a1 --- /dev/null +++ b/src/bass/CompressInputStream.java @@ -0,0 +1,159 @@ +package bass; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Converts a PCM byte stream into an A-Law or u-Law byte stream + * Source : http://thorntonzone.com/manuals/Compression/Fax,%20IBM%20MMR/MMSC/mmsc/uk/co/mmscomputing/sound/ + * The PCM-Format is : + * PCM 8000.0 Hz, 16 bit, mono, SIGNED, little-endian + * static AudioFormat pcmformat = new AudioFormat(8000,16,1,true,false); + * @author rdkartono + * + */ +public class CompressInputStream extends FilterInputStream{ + + /* + Convert mono PCM byte stream into A-Law u-Law byte stream + + static AudioFormat alawformat= new AudioFormat(AudioFormat.Encoding.ALAW,8000,8,1,1,8000,false); + static AudioFormat ulawformat= new AudioFormat(AudioFormat.Encoding.ULAW,8000,8,1,1,8000,false); + + PCM 8000.0 Hz, 16 bit, mono, SIGNED, little-endian + static AudioFormat pcmformat = new AudioFormat(8000,16,1,true,false); + + */ + + static private Compressor alawcompressor=new ALawCompressor(); + static private Compressor ulawcompressor=new uLawCompressor(); + + private Compressor compressor=null; + + public CompressInputStream(InputStream in, boolean useALaw)throws IOException{ + super(in); + compressor=(useALaw)?alawcompressor:ulawcompressor; + } + + public int read()throws IOException{ + throw new IOException(getClass().getName()+".read() :\n\tDo not support simple read()."); + } + + public int read(byte[] b)throws IOException{ + return read(b,0,b.length); + } + + public int read(byte[] b, int off, int len)throws IOException{ + int i,sample; + byte[] inb; + + inb=new byte[len<<1]; // get 16bit PCM data + len=in.read(inb); + if(len==-1){return -1;}; + + i=0; + while(i>1; + } + } + + abstract class Compressor{ + protected abstract int compress(short sample); + } + + /* + Mathematical Tools in Signal Processing with C++ and Java Simulations + by Willi-Hans Steeb + International School for Scientific Computing + */ + + class ALawCompressor extends Compressor{ + + static final int cClip = 32635; + + static final int[] ALawCompressTable ={ + 1,1,2,2,3,3,3,3, + 4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7 + }; + + protected int compress(short sample){ + int sign; + int exponent; + int mantissa; + int compressedByte; + + sign = ((~sample) >> 8) & 0x80; + if(sign==0){ sample *= -1;} + if(sample > cClip){ sample = cClip; } + if(sample >= 256){ + exponent = ALawCompressTable[(sample >> 8) & 0x007F]; + mantissa = (sample >> (exponent + 3) ) & 0x0F; + compressedByte = 0x007F & ((exponent << 4) | mantissa); + }else{ + compressedByte = 0x007F & (sample >> 4); + } + compressedByte ^= (sign ^ 0x55); + return compressedByte; + } + } + + class uLawCompressor extends Compressor{ + + static final int cClip = 32635; + static final int cBias = 0x84; + + int[] uLawCompressTable ={ + 0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3, + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 + }; + + protected int compress(short sample){ + int sign; + int exponent; + int mantissa; + int compressedByte; + + sign = (sample >> 8) & 0x80; + if(sign!=0){ sample *= -1;} + if(sample > cClip){ sample = cClip; } + sample += cBias; + + exponent = uLawCompressTable[(sample >> 7) & 0x00FF]; + mantissa = (sample >> (exponent + 3)) & 0x0F; + compressedByte = ~(sign | (exponent << 4) | mantissa); + return compressedByte&0x000000FF; + } + } diff --git a/src/bass/ConvertInputStream.java b/src/bass/ConvertInputStream.java new file mode 100644 index 0000000..c14391d --- /dev/null +++ b/src/bass/ConvertInputStream.java @@ -0,0 +1,115 @@ +package bass; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Converts an A-Law into u-Law byte stream or vice versa + * Source : http://thorntonzone.com/manuals/Compression/Fax,%20IBM%20MMR/MMSC/mmsc/uk/co/mmscomputing/sound/ + * @author rdkartono + * + */ +public class ConvertInputStream extends FilterInputStream{ + + /* + Convert A-Law or u-Law byte stream into u-Law or A-Law byte stream + + static AudioFormat alawformat= new AudioFormat(AudioFormat.Encoding.ALAW,8000,8,1,1,8000,false); + static AudioFormat ulawformat= new AudioFormat(AudioFormat.Encoding.ULAW,8000,8,1,1,8000,false); + + */ + + static private byte[] alaw2ulawtable={ + 41, 42, 39, 40, 45, 46, 43, 44, + 33, 34, 31, 32, 37, 38, 35, 36, + 57, 58, 55, 56, 61, 62, 59, 60, + 49, 50, 47, 48, 53, 54, 51, 52, + 10, 11, 8, 9, 14, 15, 12, 13, + 2, 3, 0, 1, 6, 7, 4, 5, + 26, 27, 24, 25, 30, 31, 28, 29, + 18, 19, 16, 17, 22, 23, 20, 21, + 98, 99, 96, 97, 102, 103, 100, 101, + 93, 93, 92, 92, 95, 95, 94, 94, + 116, 118, 112, 114, 124, 126, 120, 122, + 106, 107, 104, 105, 110, 111, 108, 109, + 72, 73, 70, 71, 76, 77, 74, 75, + 64, 65, 63, 63, 68, 69, 66, 67, + 86, 87, 84, 85, 90, 91, 88, 89, + 79, 79, 78, 78, 82, 83, 80, 81, + -87, -86, -89, -88, -83, -82, -85, -84, + -95, -94, -97, -96, -91, -90, -93, -92, + -71, -70, -73, -72, -67, -66, -69, -68, + -79, -78, -81, -80, -75, -74, -77, -76, + -118, -117, -120, -119, -114, -113, -116, -115, + -126, -125, -128, -127, -122, -121, -124, -123, + -102, -101, -104, -103, -98, -97, -100, -99, + -110, -109, -112, -111, -106, -105, -108, -107, + -30, -29, -32, -31, -26, -25, -28, -27, + -35, -35, -36, -36, -33, -33, -34, -34, + -12, -10, -16, -14, -4, -2, -8, -6, + -22, -21, -24, -23, -18, -17, -20, -19, + -56, -55, -58, -57, -52, -51, -54, -53, + -64, -63, -65, -65, -60, -59, -62, -61, + -42, -41, -44, -43, -38, -37, -40, -39, + -49, -49, -50, -50, -46, -45, -48, -47, + }; + + static private byte[] ulaw2alawtable={ + 42, 43, 40, 41, 46, 47, 44, 45, + 34, 35, 32, 33, 38, 39, 36, 37, + 58, 59, 56, 57, 62, 63, 60, 61, + 50, 51, 48, 49, 54, 55, 52, 53, + 11, 8, 9, 14, 15, 12, 13, 2, + 3, 0, 1, 6, 7, 4, 5, 26, + 27, 24, 25, 30, 31, 28, 29, 18, + 19, 16, 17, 22, 23, 20, 21, 107, + 104, 105, 110, 111, 108, 109, 98, 99, + 96, 97, 102, 103, 100, 101, 123, 121, + 126, 127, 124, 125, 114, 115, 112, 113, + 118, 119, 116, 117, 75, 73, 79, 77, + 66, 67, 64, 65, 70, 71, 68, 69, + 90, 91, 88, 89, 94, 95, 92, 93, + 82, 82, 83, 83, 80, 80, 81, 81, + 86, 86, 87, 87, 84, 84, 85, -43, + -86, -85, -88, -87, -82, -81, -84, -83, + -94, -93, -96, -95, -90, -89, -92, -91, + -70, -69, -72, -71, -66, -65, -68, -67, + -78, -77, -80, -79, -74, -73, -76, -75, + -117, -120, -119, -114, -113, -116, -115, -126, + -125, -128, -127, -122, -121, -124, -123, -102, + -101, -104, -103, -98, -97, -100, -99, -110, + -109, -112, -111, -106, -105, -108, -107, -21, + -24, -23, -18, -17, -20, -19, -30, -29, + -32, -31, -26, -25, -28, -27, -5, -7, + -2, -1, -4, -3, -14, -13, -16, -15, + -10, -9, -12, -11, -53, -55, -49, -51, + -62, -61, -64, -63, -58, -57, -60, -59, + -38, -37, -40, -39, -34, -33, -36, -35, + -46, -46, -45, -45, -48, -48, -47, -47, + -42, -42, -41, -41, -44, -44, -43, -43, + }; + + private byte[] table=null; + + public ConvertInputStream(InputStream in, boolean useALaw2uLaw)throws IOException{ + super(in); + table=(useALaw2uLaw)?alaw2ulawtable:ulaw2alawtable; + } + + public int read()throws IOException{ + int b=in.read(); + if(b==-1){return -1;}; + return table[b]; + } + + public int read(byte[] b, int off, int len)throws IOException{ + byte[] inb=new byte[len]; + len=in.read(inb); + if(len==-1){return -1;}; + for(int i=0;i>1]; // get A-Law or u-Law bytes + len=in.read(inb); + if(len==-1){return -1;}; + + for(int i=0;i>8)&0x00FF); // little-endian + b[off++]=(byte)(value&0x00FF); + } + return len<<1; + } +} diff --git a/src/bass/G711.java b/src/bass/G711.java new file mode 100644 index 0000000..46732c3 --- /dev/null +++ b/src/bass/G711.java @@ -0,0 +1,231 @@ +package bass; + +/** G.711 codec. + * This class provides methods for u-law, A-law and linear PCM conversions. + */ +public class G711 { + static final int SIGN_BIT=0x80; // Sign bit for a A-law byte. + static final int QUANT_MASK=0xf; // Quantization field mask. + static final int NSEGS=8; // Number of A-law segments. + static final int SEG_SHIFT=4; // Left shift for segment number. + static final int SEG_MASK=0x70; // Segment field mask. + + static final int[] seg_end={ 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF }; + + // copy from CCITT G.711 specifications + + /** u- to A-law conversions */ + static final int[] _u2a={ + 1, 1, 2, 2, 3, 3, 4, 4, + 5, 5, 6, 6, 7, 7, 8, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 27, 29, 31, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, + 46, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, + 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, + 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, + 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, + 113, 114, 115, 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, 126, 127, 128 }; + + /** A- to u-law conversions */ + static final int[] _a2u={ + 1, 3, 5, 7, 9, 11, 13, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 32, 32, 33, 33, 34, 34, 35, 35, + 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 48, 49, 49, + 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 64, + 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79, 79, + 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127 }; + + + static int search(int val, int[] table) + { for (int i=0; i=0) + { mask=0xD5; // sign (7th) bit = 1 + } + else + { mask=0x55; // sign bit = 0 + pcm_val=-pcm_val-8; + } + + // Convert the scaled magnitude to segment number. + seg=search(pcm_val,seg_end); + + // Combine the sign, segment, and quantization bits. + if (seg>=8) // out of range, return maximum value. + return (0x7F^mask); + else + { aval=seg<>4)&QUANT_MASK; + else aval|=(pcm_val>>(seg+3))&QUANT_MASK; + return (aval^mask); + } + } + + /** Converts an A-law value to 16-bit linear PCM + */ + //public static int alaw2linear(unsigned char a_val) + public static int alaw2linear(int a_val) + { int t; + int seg; + a_val^=0x55; + t=(a_val&QUANT_MASK)<<4; + //seg=((unsigned)a_val&SEG_MASK)>>SEG_SHIFT; + seg=(a_val&SEG_MASK)>>SEG_SHIFT; + switch (seg) + { case 0: + t+=8; + break; + case 1: + t+=0x108; + break; + default: + t+=0x108; + t<<=seg-1; + } + return ((a_val&SIGN_BIT)!=0)? t : -t; + } + + + /** Bias for linear code. */ + public static final int BIAS=0x84; + + + /** Converts a linear PCM value to u-law + * + * In order to simplify the encoding process, the original linear magnitude + * is biased by adding 33 which shifts the encoding range from (0 - 8158) to + * (33 - 8191). The result can be seen in the following encoding table: + * + * Biased Linear Input Code Compressed Code + * ------------------------ --------------- + * 00000001wxyza 000wxyz + * 0000001wxyzab 001wxyz + * 000001wxyzabc 010wxyz + * 00001wxyzabcd 011wxyz + * 0001wxyzabcde 100wxyz + * 001wxyzabcdef 101wxyz + * 01wxyzabcdefg 110wxyz + * 1wxyzabcdefgh 111wxyz + * + * Each biased linear code has a leading 1 which identifies the segment + * number. The value of the segment number is equal to 7 minus the number + * of leading 0's. The quantization interval is directly available as the + * four bits wxyz. The trailing bits (a - h) are ignored. + * + * Ordinarily the complement of the resulting code word is used for + * transmission, and so the code word is complemented before it is returned. + * + * For further information see John C. Bellamy's Digital Telephony, 1982, + * John Wiley & Sons, pps 98-111 and 472-476. + */ + public static int linear2ulaw(int pcm_val) // 2's complement (16-bit range) + { int mask; + int seg; + //unsigned char uval; + int uval; + + // Get the sign and the magnitude of the value. + if (pcm_val<0) + { pcm_val=BIAS-pcm_val; + mask=0x7F; + } + else + { pcm_val+=BIAS; + mask=0xFF; + } + // Convert the scaled magnitude to segment number. + seg=search(pcm_val,seg_end); + + // Combine the sign, segment, quantization bits; and complement the code word. + + if (seg>=8) return (0x7F^mask); // out of range, return maximum value. + else + { uval=(seg<<4) | ((pcm_val>>(seg+3)) & 0xF); + return (uval^mask); + } + } + + /** ConvertS a u-law value to 16-bit linear PCM. + * + * First, a biased linear code is derived from the code word. An unbiased + * output can then be obtained by subtracting 33 from the biased code. + * + * Note that this function expects to be passed the complement of the + * original code word. This is in keeping with ISDN conventions. + */ + //public static int ulaw2linear(unsigned char u_val) + public static int ulaw2linear(int u_val) + { int t; + // Complement to obtain normal u-law value. + u_val=~u_val; + // Extract and bias the quantization bits. Then shift up by the segment number and subtract out the bias. + t=((u_val&QUANT_MASK)<<3) + BIAS; + //t<<=((unsigned)u_val&SEG_MASK)>>SEG_SHIFT; + t<<=(u_val&SEG_MASK)>>SEG_SHIFT; + + return ((u_val&SIGN_BIT)!=0)? (BIAS-t) : (t-BIAS); + } + + + /** A-law to u-law conversion. + */ + //public static int alaw2ulaw(unsigned char aval) + public static int alaw2ulaw(int aval) + { aval&=0xff; + return ((aval & 0x80)!=0)? (0xFF^_a2u[aval^0xD5]) : (0x7F^_a2u[aval^0x55]); + } + + + /** u-law to A-law conversion. + */ + //public static int ulaw2alaw(unsigned char uval) + public static int ulaw2alaw(int uval) + { uval&=0xff; + return ((uval&0x80)!=0)? (0xD5^(_u2a[0xFF^uval]-1)) : (0x55^(_u2a[0x7F^uval]-1)); + } +} + diff --git a/src/bass/JSIPAudio.java b/src/bass/JSIPAudio.java new file mode 100644 index 0000000..f035a24 --- /dev/null +++ b/src/bass/JSIPAudio.java @@ -0,0 +1,970 @@ +package bass; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import anywheresoftware.b4a.BA; +import anywheresoftware.b4a.keywords.Bit; +import anywheresoftware.b4a.keywords.DateTime; +import bass.BassLibrary.BASS_DEVICEINFO; +import bass.BassLibrary.FloatValue; +import bass.BassLibrary.RECORDPROC; +import code.common; + + + +/** + * SIP Audio Converter + * @author rdkartono + * + */ +@BA.ShortName("JSIPAudio") +@BA.Events(values= { + "log(msg as string)", + "fileconvertfinish(sourcefile as string, targetfile as string)", + "filestreaming(filename as string, handle as int, data() as byte)", + "inputstreaming(inputname as string, handle as int, data() as byte)" + +}) +public class JSIPAudio { + + public JSIPAudio() { + Me = this; + currentfolder = new File("").getAbsolutePath(); + + inited_playback = new HashSet(); + inited_recorder = new HashSet(); + loaded_plugin = new HashSet(); + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + System.out.println("JSIPAudio ShutdownHook called"); + inited_recorder.forEach(recdev->{ + if (BassLibrary.BASS.BASS_RecordSetDevice(recdev)) { + if (BassLibrary.BASS.BASS_RecordFree()) { + System.out.println("Recorder Device="+recdev+" freed"); + } else System.out.println(BassLibrary.BASS.GetBassError("BASS_RecordFree")); + } else System.out.println(BassLibrary.BASS.GetBassError("BASS_RecordSetDevice")); + + }); + inited_recorder.clear(); + + inited_playback.forEach(playdev ->{ + if (BassLibrary.BASS.BASS_SetDevice(playdev)) { + if (BassLibrary.BASS.BASS_Free()) { + System.out.println("Playback Device="+playdev+" freed"); + } else System.out.println(BassLibrary.BASS.GetBassError("BASS_Free")); + } else System.out.println(BassLibrary.BASS.GetBassError("BASS_SetDevice")); + + }); + inited_playback.clear(); + + loaded_plugin.forEach(ph ->{ + if (BassLibrary.BASS.BASS_PluginFree(ph)) { + System.out.println("Plugin freed"); + } else System.out.println(BassLibrary.BASS.GetBassError("BASS_PluginFree")); + }); + loaded_plugin.clear(); + } + }); + } + private final String currentfolder ; + + private BA ba; + private String event; + private final Object Me; + private boolean need_log_event = false; + + private boolean inited = false; + + private final ExecutorService exec = Executors.newCachedThreadPool(); + private final Set inited_playback ; + private final Set inited_recorder ; + private final Set loaded_plugin; + + public ULaw ULaw; + public PCM PCM16; + public PCM LPCM24; + public AAC AAC; + + + /** + * Initialize JSIPAudio + * @param eventname Event Name + */ + public void Initialize(BA ba, String eventname) { + this.ba = ba; + this.event = eventname; + check_b4x_events(); + extract_libraries(); + + ULaw = new ULaw(16,1); + PCM16 = new PCM(16,1); + AAC = new AAC(16,1); + LPCM24 = new PCM(24,1); + + inited = true; + + } + + /** + * Check for B4X events requirement + */ + private void check_b4x_events() { + if (this.ba!=null) { + if (common.ValidString(this.event)) { + need_log_event = this.ba.subExists(event+"_log"); + + } + } + } + + private void extract_libraries() { + raise_log(common.extract_library(currentfolder, "bass")); + raise_log(common.extract_library(currentfolder, "bassmix")); + raise_log(common.extract_library(currentfolder, "bass_aac")); + raise_log(common.extract_library(currentfolder, "bassenc")); + raise_log(common.extract_library(currentfolder, "bassenc_aac")); + + raise_log("Bass Version = "+Bit.ToHexString(BassLibrary.BASS.BASS_GetVersion())); + raise_log("BassMix Version = "+Bit.ToHexString(BassMixLibrary.BASSMIX.BASS_Mixer_GetVersion())); + raise_log("BassEnc Version = "+Bit.ToHexString(BassEncLibrary.BASSENC.BASS_Encode_GetVersion())); + raise_log("BassEnc_AAC Version = "+Bit.ToHexString(BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_GetVersion())); + + int pluginhandle = BassLibrary.BASS.BASS_PluginLoad("bass_aac", 0); + if (pluginhandle!=0) { + loaded_plugin.add(pluginhandle); + raise_log("Plugin AAC Loaded"); + } else raise_log(BassLibrary.BASS.GetBassError("BASS_PluginLoad")); + } + + + + /** + * Set Channel Volume + * @param handle handle value + * @param value 0 - 100 + */ + public void SetChannelVolume(int handle, int value) { + if (value<0) value = 0; + if (value>100) value = 100; + if (handle!=0) { + if (!BassLibrary.BASS.BASS_ChannelSetAttribute(handle, BassLibrary.Constant.BASS_ATTRIB_VOL, value/100.0f)) { + raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelSetAttribute")); + } + + } + } + + /** + * Get Channel Volume + * @param handle handle value + * @return -1 if failed, 0 - 100 if success + */ + public int GetChannelVolume(int handle) { + if (handle!=0) { + FloatValue val = new FloatValue(); + if (BassLibrary.BASS.BASS_ChannelGetAttribute(handle, BassLibrary.Constant.BASS_ATTRIB_VOL, val)) { + int intval = (int)(val.value*100); + if (intval<0) intval = 0; + if (intval>100) intval = 100; + return intval; + } + } + return -1; + } + + /** + * Set Master Volume for specific Playback Device + * @param devid start from 1. No-sound cant be controlled + * @param value 0 - 100 + * @return true if SetVolume success + */ + public boolean SetMasterVolume(int devid, int value) { + if (value<0) value = 0; + if (value>100) value = 100; + if (devid>0) { + if (BassLibrary.BASS.BASS_SetDevice(devid)) { + if (BassLibrary.BASS.BASS_SetVolume(value/100.0f)) { + return true; + } else raise_log(BassLibrary.BASS.GetBassError("BASS_SetVolume")); + } else raise_log(BassLibrary.BASS.GetBassError("BASS_SetDevice")); + } + return false; + } + + /** + * Get Master Volume for specific Playback Device + * @param devid start from 1 + * @return -1 if GetVolume failed + */ + public int GetMasterVolume(int devid) { + if (devid>0) { + if (BassLibrary.BASS.BASS_SetDevice(devid)) { + float val = BassLibrary.BASS.BASS_GetVolume(); + if (val>=0) { + int result = (int)(val * 100); + if (result>100) result = 100; + return result; + } else raise_log(BassLibrary.BASS.GetBassError("BASS_GetVolume")); + } else raise_log(BassLibrary.BASS.GetBassError("BASS_SetDevice")); + } + return -1; + } + + /** + * Set Record Volume + * @param devid start from 0 + * @param value 0 - 100 + * @return true if success + */ + public boolean SetRecordVolume(int devid, int value) { + // value negative = leave current + // value = 0 = silent + // value = 100 = max + if (value<-1) value = -1; + if (value>100) value = 100; + if (BassLibrary.BASS.BASS_RecordSetInput(devid, BassLibrary.Constant.BASS_INPUT_ON, value/100.0f)) { + return true; + } else { + raise_log(BassLibrary.BASS.GetBassError("BASS_RecordSetInput")); + return false; + } + } + + /** + * Get Record Volume + * @param devid start from 0 + * @return -1 if failed, 0 - 100 if success + */ + public int GetRecordVolume(int devid) { + FloatValue val = new FloatValue(); + int result = BassLibrary.BASS.BASS_RecordGetInput(devid, val); + if (result==-1) { + // error + raise_log(BassLibrary.BASS.GetBassError("BASS_RecordGetInput")); + return -1; + } else { + int intval = (int)(val.value * 100); + return intval; + } + } + + /** + * Check if already initialized + * @return true if initialized + */ + public boolean IsInitialized() { + return inited; + } + + /** + * Get all Playback Devices + * @return array of BASS_DEVICEINFO + */ + public BASS_DEVICEINFO[] GetPlaybackDevices() { + int ii = 0; + boolean success = true; + List result = new ArrayList(); + do { + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + success = BassLibrary.BASS.BASS_GetDeviceInfo(ii, dev); + if (success) { + ii++; + result.add(dev); + } + } while(success); + return result.toArray(new BASS_DEVICEINFO[0]); + } + + /** + * Get All Recorder Devices + * @return array of BASS_DEVICEINFO + */ + public BASS_DEVICEINFO[] GetRecorderDevices() { + int ii = 0; + boolean success = true; + List result = new ArrayList(); + do { + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + success = BassLibrary.BASS.BASS_RecordGetDeviceInfo(ii, dev); + if (success) { + ii++; + result.add(dev); + } + + } while(success); + return result.toArray(new BASS_DEVICEINFO[0]); + } + + /** + * Get Playback Device Info + * @param devid 0 = no sound, 1 = first device + * @return BASS_DEVICEINFO object, or null if failed + */ + public BASS_DEVICEINFO GetPlaybackDeviceInformation(int devid) { + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + if (BassLibrary.BASS.BASS_GetDeviceInfo(devid, dev)) { + return dev; + } else raise_log(BassLibrary.BASS.GetBassError("BASS_GetDeviceInfo")); + return null; + } + + /** + * Get Recorder Device Info + * @param devid 0 = first device + * @return BASS_DEVICEINFO object, or null if failed + */ + public BASS_DEVICEINFO GetRecorderDeviceInformation(int devid) { + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + if (BassLibrary.BASS.BASS_RecordGetDeviceInfo(devid, dev)) { + return dev; + } else raise_log(BassLibrary.BASS.GetBassError("BASS_RecordGetDeviceInfo")); + return null; + } + + private void raise_log(String msg) { + if (need_log_event) { + ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg}); + } else { + System.out.println(msg); + } + } + + public class ULaw extends basicfunctions{ + public ULaw(int bits, int channel) { + super(48000, BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_FREQ); + this.channel = channel; + this.bits = bits; + this.need_fileconvert_event = (ba!=null) ? ba.subExists(fileconvert_event) : false; + this.need_filestreaming_event = (ba!=null) ? ba.subExists(filestreaming_event) : false; + this.need_inputstreaming_event = (ba!=null) ? ba.subExists(inputstreaming_event) : false; + } + public final int[] samplingrate = {8000}; + public final int channel;; + public final int bits; + public final String MIME = "audio/basic"; + + private final boolean need_fileconvert_event; + private final boolean need_filestreaming_event; + private final boolean need_inputstreaming_event; + + private final String fileconvert_event = "ulaw_fileconvertfinish"; + private final String filestreaming_event = "ulaw_filestreaming"; + private final String inputstreaming_event = "ulaw_inputstreaming"; + + private void raise_fileconvert(String sourcefile, String targetfile) { + if (need_fileconvert_event) ba.raiseEventFromDifferentThread(Me, null, 0, fileconvert_event, false, new Object[] {sourcefile, targetfile}); + } + + private void raise_filestreaming(String filename, int handle, byte[] data) { + if (need_filestreaming_event) ba.raiseEventFromDifferentThread(Me,null,0,filestreaming_event, false, new Object[] {filename, handle, data}); + } + + private void raise_inputstreaming(String inputname, int handle, byte[] data) { + if (need_inputstreaming_event) ba.raiseEventFromDifferentThread(Me, null, 0, inputstreaming_event, false, new Object[] {inputname, handle, data}); + } + + /** + * Convert PCM 16bit Bytes to ULaw + * @param pcm PCM Data + * @return ULaw data + */ + private byte[] ConvertPCMToULaw(byte[] pcm) { + //TODO check di sini, harusnya readcount = pcm.length / 2 + // karena PCM 16 bit, ULaw = 8 bit + // tapi gak tau juga, belum cek + int readcount = pcm.length; + byte[] cc = new byte[readcount]; + + ShortBuffer sb = ByteBuffer.wrap(pcm).asShortBuffer(); + ShortBuffer sc = ByteBuffer.wrap(cc).asShortBuffer(); + + // sampe sini dapat PCM nya , convert ke ULaw + + // convert dari PCM to ULaw + while(sb.hasRemaining()) { + sc.put((short)G711.linear2ulaw(sb.get())); + } + return cc; + } + + /** + * Convert file from source to target, in G711 U-Law format + * will raise event ulaw_fileconvertfinish(sourcefile , targetfile) when finished + * @param sourcefilename Source File + * @param targetsamplingrate target sampling rate, only 8000 is supported + * @param targetfilename Target File + * @return true if convesion can be started + */ + @Override + public boolean FileConvert(final String sourcefilename, int targetsamplingrate, final String targetfilename) { + targetsamplingrate = samplingrate[0]; + OpenFile op = new OpenFile(sourcefilename, 0, targetsamplingrate, true); + + if (op.isSuccess()) { + // sampe sini sudah plugged ke Mixer + inited_playback.add(0); + + try(FileOutputStream fos = new FileOutputStream(targetfilename)){ + exec.submit(()->{ + long timestamp = DateTime.getNow(); + int totalread = 0; + int readcount = 0; + + try { + do { + Pointer ptr = new Memory(1024); + readcount = BassLibrary.BASS.BASS_ChannelGetData(op.getMixerhandle(), ptr, 1024); + if (readcount>0) { + byte[] bb = ptr.getByteArray(0, readcount); + byte[] cc = ConvertPCMToULaw(bb); + + totalread+=readcount; + fos.write(cc); + + } + } while(readcount>0); + + fos.close(); + } catch(IOException e) { + raise_log(MessageFormat.format("FileOutputStream {0} IOException={1}", targetfilename, e.getMessage())); + } + + // sampe sini selesai + long delta = DateTime.getNow() - timestamp; + raise_log(MessageFormat.format("ULaw FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta)); + raise_fileconvert(sourcefilename, targetfilename); + }); + return true; + } catch(IOException e) { + raise_log("FileOutputStream create exception="+e.getMessage()); + return false; + } finally { + String[] closeerror = op.CloseHandles(); + Stream.of(closeerror).forEach(err -> raise_log(err)); + } + + + } else { + Stream.of(op.getErrorlist()).forEach(err -> raise_log(err)); + Stream.of(op.CloseHandles()).forEach(err-> raise_log(err)); + return false; + } + + + + + } + + /** + * Open File for streaming + * will raise event ulaw_filestreaming + * @param playdevice 0 = no sound, 1 = first audio + * @param sourcefilename Source File to stream + * @param targetsamplingrate target sampling rate, only 8000 is supported + * @return 0 if failed, or other value if success + */ + @Override + public int OpenFileForStreaming(final int playdevice, final String sourcefilename, int targetsamplingrate) { + targetsamplingrate = samplingrate[0]; + + OpenFile op = new OpenFile(sourcefilename, playdevice, targetsamplingrate); + if (op.isSuccess()) { + inited_playback.add(playdevice); + // sampe sini , channel sudah di add + if (op.StartPlayback()) { + exec.submit(()->{ + + int dsphandle = BassLibrary.BASS.BASS_ChannelSetDSP(op.getMixerhandle(), (dsp_handle, channel_handle, buffer, length, user)->{ + // di sini dapat PCM dari Mixer + if (length>0) { + byte[] data = buffer.getByteArray(0, length); + byte[] ulaw = ConvertPCMToULaw(data); + raise_filestreaming(sourcefilename, op.getFilehandle(), ulaw); + } + + }, null, 0); + + if (dsphandle!=0) { + // sampe sini , DSP sudah created + long timestamp = DateTime.getNow(); + raise_log("Mixer Playback for "+sourcefilename+" started"); + // tinggal loop aja sampe selesai + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + raise_log("Interrupt Exception="+e.getMessage()); + } + } while(BassLibrary.BASS.BASS_ChannelIsActive(op.getFilehandle())==BassLibrary.Constant.BASS_ACTIVE_PLAYING); + + // finish playback !! + long delta = DateTime.getNow() - timestamp; + raise_log("Mixer Playback for "+sourcefilename+" finished, duration="+delta+" ms"); + } else raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelSetDSP")); + + // sampe sini selesai semua + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + }); + + return op.getFilehandle(); + } else { + raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + Stream.of(op.CloseHandles()).forEach(err-> raise_log(err)); + return 0; + } + + } else { + Stream.of(op.getErrorlist()).forEach(err-> raise_log(err)); + Stream.of(op.CloseHandles()).forEach(err-> raise_log(err)); + return 0; + } + + + + + + } + + /** + * Open Input for streaming + * will raise event ulaw_inputstreaming + * @param recdevice 0 = first recording device + * @param targetsamplingrate target sampling rate, only 8000 is supported + * @return 0 if failed, other value if success + */ + @Override + public int OpenInputForStreaming(final int recdevice, int targetsamplingrate) { + // default ke 8000 + targetsamplingrate = samplingrate[0]; + + OpenInput oi = new OpenInput(recdevice, targetsamplingrate, new RECORDPROC() { + + @Override + public boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user) { + + byte[] data = buffer.getByteArray(0,length); + byte[] ulaw = ConvertPCMToULaw(data); + // inputname ada di Pointer user + raise_inputstreaming(user==null ? "": user.getString(0), handle, ulaw); + + return true; // terusin RecordProc + } + + }); + + if (oi.isSuccess()) { + inited_recorder.add(oi.getRecorderdev()); + String[] errorstart = oi.StartRecord(); + if (errorstart.length>0) { + // ada error + Stream.of(oi.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(oi.StopRecord()).forEach(err->raise_log(err)); + return 0; + } else return oi.getRecordhandle(); // aman + } else { + Stream.of(oi.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(oi.StopRecord()).forEach(err->raise_log(err)); + return 0; + } + + + + } + + } + + public class PCM extends basicfunctions{ + public PCM(int bits, int channel) { + super(48000, BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_FREQ); + + this.channel = channel; + this.bits = bits; + this.MIME = bits==24 ? "audio/L24" : "audio/L16"; + this.fileconvert_event = (bits==24 ? "pcm24" : "pcm16")+"_fileconvertfinish"; + this.filestreaming_event = (bits==24 ? "pcm24" : "pcm16")+"_filestreaming"; + this.inputstreaming_event = (bits==24 ? "pcm24" : "pcm16")+"_inputstreaming"; + this.need_fileconvert_event = (ba!=null) ? ba.subExists(fileconvert_event) : false; + this.need_filestreaming_event = (ba!=null) ? ba.subExists(filestreaming_event) : false; + this.need_inputstreaming_event = (ba!=null) ? ba.subExists(inputstreaming_event) : false; + + + } + + public final int[] samplingrate = {16000, 32000, 44100, 48000}; + public final int channel; + public final int bits; + public final String MIME; + + private final boolean need_fileconvert_event; + private final boolean need_filestreaming_event; + private final boolean need_inputstreaming_event; + + private final String fileconvert_event; + private final String filestreaming_event; + private final String inputstreaming_event; + + private void raise_fileconvert(String sourcefile, String targetfile) { + if (need_fileconvert_event) ba.raiseEventFromDifferentThread(Me, null, 0, fileconvert_event, false, new Object[] {sourcefile, targetfile}); + } + + private void raise_filestreaming(String filename, int handle, byte[] data) { + if (need_filestreaming_event) ba.raiseEventFromDifferentThread(Me,null,0,filestreaming_event, false, new Object[] {filename, handle, data}); + } + + private void raise_inputstreaming(String inputname, int handle, byte[] data) { + if (need_inputstreaming_event) ba.raiseEventFromDifferentThread(Me, null, 0, inputstreaming_event, false, new Object[] {inputname, handle, data}); + } + + private byte[] Convert_PCM16_to_PCM24(byte[] data) { + // TODO belum pernah dicek + int length16 = data.length; // length data dalam 16 bit + int length = length16 / 2; // jumlah samples nya, yaitu 16bit dibagi 2 + int length24 = length * 3; // jumlah data dengan samples yang sama, tapi 24 bit + + byte[] result = new byte[length24]; + int jj = 0; + for (int ii=0;ii xx==targetsamplingrate).findAny().orElse(samplingrate[0]); + + OpenFile op = new OpenFile(sourcefilename, 0, samplingfreq, true); + if (op.isSuccess()) { + inited_playback.add(0); + // sampe sini sudah plugged ke Mixer + + try(FileOutputStream fos = new FileOutputStream(targetfilename)){ + + exec.submit(()->{ + long timestamp = DateTime.getNow(); + int totalread = 0; + int readcount = 0; + do { + Pointer ptr = new Memory(1024); + readcount = BassLibrary.BASS.BASS_ChannelGetData(op.getMixerhandle(), ptr, 1024); + if (readcount>0) { + // ada data + byte[] bb = ptr.getByteArray(0, readcount); + + // tulis ke FileOutputStream + try { + if (bits==24) { + byte[] cc = Convert_PCM16_to_PCM24(bb); + fos.write(cc); + totalread+=cc.length; + } else { + fos.write(bb); + totalread+=bb.length; + } + + } catch (IOException e) { + raise_log("FileOutputStream write exception="+e.getMessage()); + break; + } + } + } while(readcount>0); + + + // close FileOutputStream + try { + fos.close(); + } catch (IOException e) { + raise_log("FileOutputStream close exception="+e.getMessage()); + } + + Stream.of(op.CloseHandles()).forEach(err-> raise_log(err)); + + long delta = DateTime.getNow() - timestamp; + raise_log(MessageFormat.format("FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta)); + raise_fileconvert(sourcefilename, targetfilename); + }); + return true; + + + } catch(IOException e) { + raise_log(MessageFormat.format("FileOutputStream {0} Exception={1}", targetfilename, e.getMessage())); + return false; + } finally { + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + } + } else { + Stream.of(op.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return false; + } + + } + + @Override + public int OpenFileForStreaming(int playdevice, final String sourcefilename, int targetsamplingrate) { + final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]); + + OpenFile op = new OpenFile(sourcefilename, playdevice, samplingfreq); + if (op.isSuccess()) { + inited_playback.add(playdevice); + if (op.StartPlayback()) { + final int dsphandle = BassLibrary.BASS.BASS_ChannelSetDSP(op.getMixerhandle(), (dsp_h, ch_h, buffer,length, user)->{ + // dsp nya disini + byte[] data = buffer.getByteArray(0, length); + byte[] cc = Convert_PCM16_to_PCM24(data); + raise_filestreaming(sourcefilename, op.getFilehandle(),cc); + }, null, 0); + + if (dsphandle==0) { + raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelSetDSP")); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return 0; + } + + // sampe sini berhasil + return op.getFilehandle(); + } + else { + raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return 0; + } + + } else { + Stream.of(op.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return 0; + } + } + + @Override + public int OpenInputForStreaming(final int recdevice, final int targetsamplingrate) { + final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]); + + OpenInput oi = new OpenInput(recdevice, samplingfreq, new RECORDPROC() { + + @Override + public boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user) { + byte[] data = buffer.getByteArray(0, length); + String recdevname = user==null ? "" : user.getString(0); + if (bits==24) { + raise_inputstreaming(recdevname, handle, Convert_PCM16_to_PCM24(data)); + } else { + raise_inputstreaming(recdevname, handle, data); + } + return true; + } + + }); + + if (oi.isSuccess()) { + inited_recorder.add(oi.getRecorderdev()); + return oi.getRecordhandle(); + } else { + Stream.of(oi.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(oi.StopRecord()).forEach(err->raise_log(err)); + return 0; + } + + } + + } + + public class AAC extends basicfunctions{ + public AAC(int bits, int channel) { + super(48000, BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_FREQ); + + this.bits = bits; + this.channel = channel; + this.need_fileconvert_event = (ba!=null) ? ba.subExists(fileconvert_event) : false; + this.need_filestreaming_event = (ba!=null) ? ba.subExists(filestreaming_event) : false; + this.need_inputstreaming_event = (ba!=null) ? ba.subExists(inputstreaming_event) : false; + + } + public final int[] samplingrate = {8000, 16000, 32000, 44100, 48000}; + public final int bits; + public final int channel; + public final String MIME = "audio/mpeg4-generic"; + + private final boolean need_fileconvert_event; + private final boolean need_filestreaming_event; + private final boolean need_inputstreaming_event; + + private final String fileconvert_event = "aac_fileconvertfinish"; + private final String filestreaming_event = "aac_filestreaming"; + private final String inputstreaming_event = "aac_inputstreaming"; + + private void raise_fileconvert(String sourcefile, String targetfile) { + if (need_fileconvert_event) ba.raiseEventFromDifferentThread(Me, null, 0, fileconvert_event, false, new Object[] {sourcefile, targetfile}); + } + + private void raise_filestreaming(String filename, int handle, byte[] data) { + if (need_filestreaming_event) ba.raiseEventFromDifferentThread(Me,null,0,filestreaming_event, false, new Object[] {filename, handle, data}); + } + + private void raise_inputstreaming(String inputname, int handle, byte[] data) { + if (need_inputstreaming_event) ba.raiseEventFromDifferentThread(Me, null, 0, inputstreaming_event, false, new Object[] {inputname, handle, data}); + } + + @Override + public boolean FileConvert(String sourcefilename, final int targetsamplingrate, String targetfilename) { + final int samplingfreq = Arrays.stream(samplingrate).filter(xx->xx==targetsamplingrate).findAny().orElse(samplingrate[0]); + OpenFile op = new OpenFile(sourcefilename, 0, samplingfreq); + if (op.isSuccess()) { + inited_playback.add(0); + // sampe sini sudah plugged ke Mixer + + // create encoder handle dari mixer handle + final int encodehandle = BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_StartFile(op.getMixerhandle(), null, 0, targetfilename); + if (encodehandle==0) { + // gak bisa create encoder + raise_log(BassLibrary.BASS.GetBassError("BASS_Encode_AAC_StartFile")); + // free handle + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return false; + } + + // sampe sini encoder jalan + exec.submit(()->{ + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + raise_log("InterruptException"); + } + } while (BassLibrary.BASS.BASS_ChannelIsActive(op.getMixerhandle())==BassLibrary.Constant.BASS_ACTIVE_PLAYING); + + // free encoder + BassEncLibrary.BASSENC.BASS_Encode_Stop(encodehandle); + // free handle + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + + + + raise_fileconvert(sourcefilename, targetfilename); + }); + return true; + } else { + Stream.of(op.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return false; + } + + } + + + + @Override + public int OpenFileForStreaming(int playdevice, final String sourcefilename, int targetsamplingrate) { + // cari sampling rate yang supported. kalau gak ketemu, pilih default + final int samplingfreq = Arrays.stream(samplingrate) + .filter(xx -> xx == targetsamplingrate) + .findFirst() + .orElse(samplingrate[0]); + + OpenFile op = new OpenFile(sourcefilename, playdevice, samplingfreq); + if (op.isSuccess()) { + inited_playback.add(playdevice); + // sampe sini sudah plugged ke Mixer + if (op.StartPlayback()) { + + final int encodehandle = BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_Start(op.getMixerhandle(), null, 0, + (encode_handle, source_handle, buffer, length, offset, user)->{ + raise_filestreaming(sourcefilename, op.getFilehandle(), buffer.getByteArray(0,length)); + } + , null); + + if (encodehandle==0) { + raise_log(BassLibrary.BASS.GetBassError("BASS_Encode_AAC_Start")); + + // free handle + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return 0; + } + + exec.submit(()->{ + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + raise_log("InterruptException"); + } + } while(BassLibrary.BASS.BASS_ChannelIsActive(op.getMixerhandle())==BassLibrary.Constant.BASS_ACTIVE_PLAYING); + + BassEncLibrary.BASSENC.BASS_Encode_Stop(encodehandle); + + // free handle + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + + }); + + return op.getFilehandle(); + } else { + raise_log(BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return 0; + } + + + } else { + Stream.of(op.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(op.CloseHandles()).forEach(err->raise_log(err)); + return 0; + } + + + } + + @Override + public int OpenInputForStreaming(int recdevice, final int targetsamplingrate) { + final int samplingfreq = Arrays.stream(samplingrate).filter(xx -> xx==targetsamplingrate).findAny().orElse(samplingrate[0]); + + OpenInput oi = new OpenInput(recdevice, samplingfreq,null); + if (oi.isSuccess()) { + inited_recorder.add(oi.getRecorderdev()); + // sampe sini bisa recording + final int encodehandle = BassEncAACLibrary.BASSENC_AAC.BASS_Encode_AAC_Start(oi.getRecordhandle(), null, 0, (encode_handle, source_handle, buffer, length, offset, user)->{ + byte[] data = buffer.getByteArray(0, length); + raise_inputstreaming(oi.getDeviceName(), source_handle, data); + }, null); + + if (encodehandle!=0) { + return oi.getRecordhandle(); + } else { + raise_log(BassLibrary.BASS.GetBassError("BASS_Encode_AAC_Start")); + Stream.of(oi.StopRecord()).forEach(err->raise_log(err)); + } + + } else { + Stream.of(oi.getErrorlist()).forEach(err->raise_log(err)); + Stream.of(oi.StopRecord()).forEach(err->raise_log(err)); + + } + + return 0; + + + } + + } +} diff --git a/src/bass/basicfunctions.java b/src/bass/basicfunctions.java new file mode 100644 index 0000000..6d84dd3 --- /dev/null +++ b/src/bass/basicfunctions.java @@ -0,0 +1,350 @@ +package bass; + +import java.util.ArrayList; +import java.util.List; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import bass.BassLibrary.BASS_DEVICEINFO; +import bass.BassLibrary.RECORDPROC; +import lombok.Getter; + + +public abstract class basicfunctions { + + abstract public boolean FileConvert(String sourcefilename, int targetsamplingrate, String targetfilename); + abstract public int OpenFileForStreaming(int playdevice, String sourcefilename, int targetsamplingrate); + abstract public int OpenInputForStreaming(int recdevice, int targetsamplingrate); + //used in BASS_Init + protected final int devinit_freq; + protected final int devinit_flag; + + public basicfunctions(int init_freq, int init_flag) { + this.devinit_freq = init_freq; + this.devinit_flag = init_flag; + } + + /** + * Stop/Close Input For Streaming + * @param rechandle + * @return true if Input can be stopped + */ + public boolean CloseInputForStreaming(int rechandle) { + if (BassLibrary.BASS.BASS_ChannelFree(rechandle)) { + return true; + } else { + return false; + } + } + + /** + * BASS_Init dengan devinit_freq dan devinit_flag + * @param device playback device start from 0=no sound, 1=first real device + * @return true if playback device inited or already inited + */ + private boolean BassInit(int device) { + if (!BassLibrary.BASS.BASS_Init(device, devinit_freq, devinit_flag)) { + // ada gagal init, cek dulu + int err = BassLibrary.BASS.BASS_ErrorGetCode(); + if (err!=BassLibrary.Constant.BASS_ERROR_ALREADY) { + // gak boleh gagal + //raise_log(BassLibrary.BASS.GetBassError("BASS_Init", err)); + return false; + } + } + // sampe sini sukses + return true; + } + + + /** + * BASS_RecordInit + * @param device recorder device, start from 0=first real device + * @return true if recording device inited or already inited + */ + private boolean RecordInit(int device) { + if (!BassLibrary.BASS.BASS_RecordInit(device)) { + // ada gagal init, cek dulu + int err = BassLibrary.BASS.BASS_ErrorGetCode(); + if (err!=BassLibrary.Constant.BASS_ERROR_ALREADY) { + // gak boleh gagal + //raise_log(BassLibrary.BASS.GetBassError("BASS_RecordInit", err)); + return false; + } + } + return true; + } + + /** + * OpenInput helper + * @author rdkartono + * + */ + protected class OpenInput{ + private final int recorderdev; + private final @Getter int samplingrate; + private boolean success = false; + private String[] errorlist; + private int recordhandle=0; + private final RECORDPROC recproc; + private @Getter BASS_DEVICEINFO devinfo; + + public String[] getErrorlist() { + return errorlist; + } + + public int getRecorderdev() { + return recorderdev; + } + + public int getRecordhandle() { + return recordhandle; + } + + public boolean isSuccess() { + return success; + } + + /** + * Open Input + * @param dev 0 = real device + * @param samplingrate sampling rate + * @param proc RECORDPROC to receive PCM data + */ + public OpenInput(int dev, int samplingrate, RECORDPROC proc) { + this.recorderdev = dev; + this.samplingrate = samplingrate; + this.recproc = proc; + errorlist = OpenSequence(); + + } + + private String[] OpenSequence() { + + List error = new ArrayList(); + if (RecordInit(recorderdev)) { + devinfo = new BASS_DEVICEINFO(); + if (!BassLibrary.BASS.BASS_RecordGetDeviceInfo(recorderdev, devinfo)) devinfo = null; + Pointer user = null; + if (devinfo!=null) { + user = new Memory(devinfo.name.length()+1); + user.setString(0, devinfo.name); + } + + recordhandle = BassLibrary.BASS.BASS_RecordStart(samplingrate, 1, BassLibrary.Constant.BASS_RECORD_PAUSE, recproc, user); + if (recordhandle!=0) { + success = true; + } else error.add(BassLibrary.BASS.GetBassError("BASS_RecordStart")); + } else error.add(BassLibrary.BASS.GetBassError("BASS_RecordInit")); + + return error.toArray(new String[0]); + } + + public Pointer PointerToDeviceName() { + if (devinfo==null) + return null; + else { + Pointer pp = new Memory(devinfo.name.length()+1); + pp.setString(0, devinfo.name); + return pp; + } + } + + public String getDeviceName() { + return devinfo==null ? "" : devinfo.name; + } + + /** + * Start Recording + * @return error messages if available + */ + public String[] StartRecord() { + List error = new ArrayList(); + if (recordhandle!=0) { + if (BassLibrary.BASS.BASS_ChannelStart(recordhandle)) { + // nothing... + } else error.add(BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + } else error.add("RecordHandle is zero"); + + return error.toArray(new String[0]); + } + + /** + * Stop Recording + * @return error messages if available + */ + public String[] StopRecord() { + List error = new ArrayList(); + if (recordhandle!=0) { + if (BassLibrary.BASS.BASS_ChannelFree(recordhandle)) { + // nothing... + } else error.add(BassLibrary.BASS.GetBassError("BASS_ChannelStop")); + recordhandle = 0; + } else error.add("RecordHandle is zero"); + + return error.toArray(new String[0]); + } + } + + /** + * OpenFile helper + * @author rdkartono + * + */ + protected class OpenFile{ + private final @Getter int playbackdev; + private final @Getter int samplingrate; + private final @Getter String filename; + private final @Getter boolean decodingonly; + private boolean success = false; + private int filehandle; + private int mixerhandle; + private int mixerflag; + private String[] errorlist; + private @Getter long ByteSize; + private @Getter double Duration; + private @Getter BASS_DEVICEINFO devinfo; + + public String[] getErrorlist() { + return errorlist; + } + + public int getFilehandle() { + return filehandle; + } + + public int getMixerhandle() { + return mixerhandle; + } + + public boolean isSuccess() { + return success; + } + + + + /** + * Create OpenFile object + * @param filename source file name + * @param playbackdev 0 =no sound, 1 = real device + * @param samplingrate sampling rate + * @param decodingonly if true, mixer will have Decode flag + */ + public OpenFile(String filename, int playbackdev, int samplingrate, boolean decodingonly) { + this.playbackdev = playbackdev; + this.samplingrate = samplingrate; + this.filename = filename; + this.decodingonly = decodingonly; + this.errorlist = OpenSequence(); + if (success) { + ByteSize = BassLibrary.BASS.BASS_ChannelGetLength(filehandle, BassLibrary.Constant.BASS_POS_BYTE); + Duration = BassLibrary.BASS.BASS_ChannelBytes2Seconds(filehandle, ByteSize); + + } + devinfo = new BASS_DEVICEINFO(); + if (!BassLibrary.BASS.BASS_GetDeviceInfo(playbackdev, devinfo)) { + devinfo = null; + } + + } + + public Pointer PointerToDeviceName() { + if (devinfo==null) + return null; + else { + Pointer pp = new Memory(devinfo.name.length()+1); + pp.setString(0, devinfo.name); + return pp; + } + } + + public String getDeviceName() { + return devinfo==null ? "" : devinfo.name; + } + + /** + * Create OpenFile Object + * @param filename source file anme + * @param playbackdev 0 = no sound, 1 = real device + * @param samplingrate samplingrate + */ + public OpenFile(String filename, int playbackdev, int samplingrate) { + this(filename, playbackdev, samplingrate, false); + } + + private String[] OpenSequence() { + + List error = new ArrayList(); + + if (BassInit(playbackdev)) { + + filehandle = BassLibrary.BASS.BASS_StreamCreateFile(filename, 0, 0, BassLibrary.Constant.BASS_STREAM_DECODE); + if (filehandle!=0) { + mixerflag = decodingonly ? BassLibrary.Constant.BASS_STREAM_DECODE : 0; + + mixerhandle = BassMixLibrary.BASSMIX.BASS_Mixer_StreamCreate(samplingrate, 1, mixerflag); + if (mixerhandle!=0) { + if (BassMixLibrary.BASSMIX.BASS_Mixer_StreamAddChannel(mixerhandle, filehandle, BassLibrary.Constant.BASS_STREAM_AUTOFREE)) { + success = true; + } else error.add(BassLibrary.BASS.GetBassError("BASS_Mixer_StreamAddChannel")); + } else error.add(BassLibrary.BASS.GetBassError("BASS_Mixer_StreamCreate")); + } else error.add(BassLibrary.BASS.GetBassError("BASS_StreamCreateFile")); + } else error.add(BassLibrary.BASS.GetBassError("Bass_Init")); + + return error.toArray(new String[0]); + } + + /** + * Start Playback + * @return true if can be started + */ + public boolean StartPlayback() { + if (filehandle!=0) { + // file sudah kebuka + if (mixerhandle!=0) { + // mixer sudah kebuka + if (success) { + // sudah plugged in + if (!decodingonly) { + // bukan channel decoding + return BassLibrary.BASS.BASS_ChannelStart(filehandle); + } + } + + } + } + return false; + } + + /** + * Close Opened handles + * @return array of string containing error messages + */ + public String[] CloseHandles() { + List error = new ArrayList(); + + if (filehandle!=0) { + if (success) { + // sudah plug ke mixer + if (!BassMixLibrary.BASSMIX.BASS_Mixer_ChannelRemove(filehandle)) { + error.add(BassLibrary.BASS.GetBassError("BASS_Mixer_ChannelRemove")); + } + } + if (!BassLibrary.BASS.BASS_ChannelFree(filehandle)) { + error.add(BassLibrary.BASS.GetBassError("BASS_ChannelFree")); + } + + filehandle = 0; + } + if (mixerhandle!=0) { + if (!BassLibrary.BASS.BASS_ChannelFree(mixerhandle)) { + error.add(BassLibrary.BASS.GetBassError("BASS_ChannelFree")); + } + mixerhandle = 0; + } + + return error.toArray(new String[0]); + } + } +} diff --git a/src/code/BassFileReader.java b/src/code/BassFileReader.java new file mode 100644 index 0000000..c625981 --- /dev/null +++ b/src/code/BassFileReader.java @@ -0,0 +1,146 @@ +package code; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import bass.BassLibrary; +import bass.BassMixLibrary; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.media.SoundSource; + +/** + * Pengganti /AxisAudio/src/net/sourceforge/peers/media/FileReader.java + * karena terbatas codec nya + * @author rdkartono + * + */ +public class BassFileReader implements SoundSource { + private Logger logger; + // Device = 0 --> no sound + private final int bassdev = 0; + // Device BassInit normal 48khz + private final int initfreq = 48000; + // Device BassInit set ke mono, 16 bit + private final int initflag = BassLibrary.Constant.BASS_DEVICE_MONO | BassLibrary.Constant.BASS_DEVICE_16BITS; + // flag ketika open file, BASS_STREAM_DECODE karena akan plug ke mixer + private final int openflag = BassLibrary.Constant.BASS_STREAM_DECODE; + // Mixer frequency untuk convert ke 8khz + private final int mixerfreq = 8000; + // Mixer channel untuk convert ke Mono + private final int mixerchannel = 1; + // Mixer create flag BASS_STREAM_DECODE karena untuk ambil data aja, bukan playback + private final int mixerflag = BassLibrary.Constant.BASS_STREAM_DECODE; + // Mixer_AddChannel flag BAS_STREAM_AUTOFREE , supaya kalau channel habis & selesai, channel free sendiri + //private final int addchannelflag = BassLibrary.Constant.BASS_STREAM_AUTOFREE; + private final int addchannelflag = 0; + + // sekali baca data, 256 bytes + private final int BUFFER_SIZE = 256; + + private final String filename; + private int filehandle = 0; + private int mixerhandle = 0; + private long filesize = 0; + private double duration = 0; + // indicator if file opened, mixer created, and file plugged to mixer + private boolean channeladded = false; + + private BassFileReaderListener listener; // belum kepake + + public BassFileReader(final String filename, BassFileReaderListener listener) { + this.filename = filename; + this.listener = listener; + if (BassLibrary.BASS.BASS_GetVersion()!=0) { + if (BassMixLibrary.BASSMIX.BASS_Mixer_GetVersion()!=0) { + boolean initresult = BassLibrary.BASS.BASS_Init(bassdev, initfreq, initflag); + if (initresult) { + System.out.println("BassFileReader Bass_Init device = "+bassdev+" success"); + } else System.out.println("BassFileReader BASS_Init error = "+BassLibrary.BASS.GetBassError("BASS_Init")); + + filehandle = BassLibrary.BASS.BASS_StreamCreateFile(false, filename, 0, 0, openflag); + if (filehandle!=0){ + filesize = BassLibrary.BASS.BASS_ChannelGetLength(filehandle, BassLibrary.Constant.BASS_POS_BYTE); + duration = BassLibrary.BASS.BASS_ChannelBytes2Seconds(filehandle, filesize); + if (listener!=null) listener.fileopened(filename, filesize, duration); + + mixerhandle = BassMixLibrary.BASSMIX.BASS_Mixer_StreamCreate(mixerfreq, mixerchannel, mixerflag); + if (mixerhandle!=0) { + if (BassMixLibrary.BASSMIX.BASS_Mixer_StreamAddChannel(mixerhandle, filehandle, addchannelflag)) { + // sequence pertama sukses + if (listener!=null) listener.filereadstart(filename); + channeladded = true; + } else { + logger_error("BassFileReader BASS_Mixer_StreamAddChannel error = "+BassLibrary.BASS.GetBassError("BASS_Mixer_StreamAddChannel")); + close(); + } + } else { + logger_error("BassFileReader BASS_Mixer_StreamCreate error = "+BassLibrary.BASS.GetBassError("BASS_Mixer_StreamCreate")); + close(); + } + } else logger_error("BassFileReader BASS_StreamCreateFile error = "+BassLibrary.BASS.GetBassError("BASS_StreamCreateFile")); + } else logger_error("BassFileReader BASS_Mixer_GetVersion invalid"); + } else logger_error("BassFileReader BASS_GetVersion invalid"); + } + + public synchronized void close() { + if (mixerhandle!=0) { + if (BassLibrary.BASS.BASS_StreamFree(mixerhandle)) { + logger_info("mixer closed in BassFileReader"); + } else logger_error("BassFileReader mixerhandle BASS_StreamFree error = "+BassLibrary.BASS.GetBassError("BASS_StreamFree")); + mixerhandle = 0; + } + if (filehandle!=0) { + if (BassLibrary.BASS.BASS_StreamFree(filehandle)) { + logger_info(filename+" closed in BassFileReader"); + } else logger_error("BassFileReader filehandle BASS_StreamFree error = "+BassLibrary.BASS.GetBassError("BASS_StreamFree")); + filehandle = 0; + } + + if (listener!=null) listener.fileclosed(filename); + channeladded = false; + } + + @Override + public synchronized byte[] readData() { + if (channeladded) { + Pointer pbuf = new Memory(BUFFER_SIZE); + int readsize = BassLibrary.BASS.BASS_ChannelGetData(mixerhandle, pbuf, BUFFER_SIZE); + if (readsize>0) { + byte[] rr = pbuf.getByteArray(0, readsize); + if (listener!=null) listener.filereadresult(filename, readsize, rr); + try { + Thread.sleep(15); + return rr; + } catch (InterruptedException e) { + logger_error("BassFileReader interrupted"); + } + } else if (readsize==-1) { + // ada error + logger_error("BassFileReader BASS_ChannelGetData error = "+BassLibrary.BASS.GetBassError("BASS_ChannelGetData")); + } else { + // selesai + if (listener!=null) listener.filereadfinish(filename); + logger_info("BassFileReader BASS_ChannelGetData readsize = "+readsize+", closing it"); + close(); + } + } + return null; + } + + public void AddListener(BassFileReaderListener listener) { + this.listener = listener; + } + + private void logger_error(String msg) { + if (logger!=null) + logger.error(msg); + else System.out.println(msg); + } + + private void logger_info(String msg) { + if (logger!=null) + logger.info(msg); + else System.out.println(msg); + } + +} diff --git a/src/code/BassFileReaderListener.java b/src/code/BassFileReaderListener.java new file mode 100644 index 0000000..6eaea76 --- /dev/null +++ b/src/code/BassFileReaderListener.java @@ -0,0 +1,14 @@ +package code; + +/** + * Java Event untuk BassFileReader + * @author rdkartono + * + */ +public interface BassFileReaderListener { + public void fileopened(String filename, long filesize, double duration); + public void fileclosed(String filename); + public void filereadresult(String filename, int readlength, byte[] pcmdata); + public void filereadstart(String filename); + public void filereadfinish(String filename); +} diff --git a/src/code/BassSoundManager.java b/src/code/BassSoundManager.java new file mode 100644 index 0000000..4091757 --- /dev/null +++ b/src/code/BassSoundManager.java @@ -0,0 +1,271 @@ +package code; + + + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import bass.BassLibrary; +import bass.BassLibrary.FloatValue; +import bass.BassLibrary.SYNCPROC; +import net.sourceforge.peers.media.AbstractSoundManager; + +/** + * Pengganti /AxisAudio/src/net/sourceforge/peers/javaxsound/JavaxSoundManager.java + * karena terbatas codec nya + * @author rdkartono + * + */ +public class BassSoundManager extends AbstractSoundManager { + + private BassSoundManagerListener bsml; + private final int record_device, play_device; + private final int init_freq = 48000; + private final int samplingrate = 8000; + private final int play_initflag = BassLibrary.Constant.BASS_DEVICE_16BITS | BassLibrary.Constant.BASS_DEVICE_MONO; + private final int streamcreate_flag = 0; + private final int recordstart_flag = 0; + private final int read_size = 320; + + private int streamhandle, recordhandle; + + + public BassSoundManager(int outputdevice , int inputdevice, BassSoundManagerListener listener) { + this.bsml = listener; + this.record_device = inputdevice; + this.play_device = outputdevice; + } + + public void AddListener(BassSoundManagerListener listener) { + this.bsml = listener; + } + + public boolean IsInitialized() { + return (streamhandle!=0) && (recordhandle!=0); + } + + public void setInputChannelVolume(int value) { + if (value<0) value = 0; + if (value>100) value = 100; + + if (!BassLibrary.BASS.BASS_RecordSetInput(record_device, BassLibrary.Constant.BASS_INPUT_ON, value/100.0f)) { + // gagal + logger_error("BASS_RecordSetInput error = "+BassLibrary.BASS.GetBassError("BASS_RecordSetInput")); + } + } + + public int getInputChannelVolume() { + FloatValue vol = new FloatValue(); + int code = BassLibrary.BASS.BASS_RecordGetInput(record_device, vol); + if (code==-1) { + logger_error("BASS_RecordGetInput error = "+BassLibrary.BASS.GetBassError("BASS_RecordGetInput")); + return 0; + } else { + int value = (int)(vol.value * 100); + if (value<0) value = 0; + if (value>100) value = 100; + return value; + } + } + + public void setOutputChannelVolume(int value) { + if (value<0) value = 0; + if (value>100) value = 100; + if (!BassLibrary.BASS.BASS_SetVolume(value/100.0f)) { + logger_error("BASS_SetVolume error = "+BassLibrary.BASS.GetBassError("BASS_SetVolume")); + } + } + + public int getOutputChannelVolume() { + float fl = BassLibrary.BASS.BASS_GetVolume() ; + if (fl==-1) { + logger_error("BASS_GetVolume error = "+BassLibrary.BASS.GetBassError("BASS_GetVolume")); + return 0; + } else { + int value = (int)(fl * 100); + if (value<0) value = 0; + if (value>100) value = 100; + return value; + } + + } + + @Override + public byte[] readData() { + if (recordhandle!=0) { + int rs = 0; + Pointer buf = new Memory(read_size); + while(true) { + if (BassLibrary.BASS.BASS_ChannelIsActive(recordhandle)!=BassLibrary.Constant.BASS_ACTIVE_PLAYING) break; + rs = BassLibrary.BASS.BASS_ChannelGetData(recordhandle, buf, read_size); + if (rs<0) { + // ada error + if (bsml!=null) bsml.log_error("readData BASS_ChannelGetData error = "+BassLibrary.BASS.GetBassError("BASS_ChannelGetData")); + return null; + } else if (rs>0) { + // ada hasil + byte[] result = buf.getByteArray(0, rs); + if (bsml!=null) bsml.ReadToRTP(rs, result); + return result; + } else { + // nol + try { + Thread.sleep(2); + continue; + } catch (InterruptedException e) { + return null; + } + + } + } + + } + return null; + + } + + + + + + @Override + public void init() { + if (streamhandle!=0 || recordhandle!=0) close(); + + if (BassLibrary.BASS.BASS_GetVersion()!=0) { + BassLibrary.BASS.BASS_Init(play_device, init_freq, play_initflag); + BassLibrary.BASS.BASS_RecordInit(record_device); + + recordhandle = BassLibrary.BASS.BASS_RecordStart(samplingrate, 1, recordstart_flag,null, null); + if (recordhandle!=0) { + BassLibrary.BASS.BASS_ChannelSetSync(recordhandle, BassLibrary.Constant.BASS_SYNC_DEV_FAIL, 0, devfail, null); + BassLibrary.BASS.BASS_ChannelSetSync(recordhandle, BassLibrary.Constant.BASS_SYNC_STALL, 0, bufferinginformation, null); + + if (BassLibrary.BASS.BASS_ChannelStart(recordhandle)) { + logger_info("InputChannel Started"); + } else logger_error("BASS_ChannelStart recordhandle error = "+BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + } else logger_error("BASS_RecordStart error = "+BassLibrary.BASS.GetBassError("BASS_RecordStart")); + + + streamhandle = BassLibrary.BASS.BASS_StreamCreate(samplingrate, 1, streamcreate_flag, BassLibrary.Constant.STREAMPROC_PUSH, null); + if (streamhandle!=0) { + BassLibrary.BASS.BASS_ChannelSetSync(streamhandle, BassLibrary.Constant.BASS_SYNC_DEV_FAIL, 0, devfail, null); + BassLibrary.BASS.BASS_ChannelSetSync(streamhandle, BassLibrary.Constant.BASS_SYNC_STALL, 0, bufferinginformation, null); + + if (BassLibrary.BASS.BASS_ChannelStart(streamhandle)) { + logger_info("OutputChannel Started"); + } else logger_error("BASS_ChannelStart streamhandle error = "+BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + } else logger_error("BASS_StreamCreate error = "+BassLibrary.BASS.GetBassError("BASS_StreamCreate")); + + if (IsInitialized()) { + logger_info("init complete"); + + new Thread() { + public void run() { + int inputvalue = 0; + int outputvalue = 0; + + while(IsInitialized()) { + inputvalue = BassLibrary.BASS.BASS_ChannelGetLevel(recordhandle); + outputvalue = BassLibrary.BASS.BASS_ChannelGetLevel(streamhandle); + if (inputvalue>=0) { + inputvalue = (int) ((BassLibrary.Utils.LOWORD(inputvalue) * 1.0f)/32768); + if (bsml!=null) bsml.ChannelLevel(true, inputvalue); + } + if (outputvalue>=0) { + outputvalue = (int) ((BassLibrary.Utils.LOWORD(outputvalue) * 1.0f)/32768); + if (bsml!=null) bsml.ChannelLevel(false, outputvalue); + } + try { + Thread.sleep(500); + } catch (InterruptedException e) { + logger_info("ChannelLevel thread interrupted"); + } + } + } + }.start(); + + if (bsml!=null) bsml.Opened(); + return; + } + + logger_info("init failed"); + close(); + + } + + } + + @Override + public void close() { + boolean somethingclosed = false; + if (streamhandle!=0) { + BassLibrary.BASS.BASS_StreamFree(streamhandle); + streamhandle = 0; + somethingclosed = true; + } + if (recordhandle!=0) { + BassLibrary.BASS.BASS_ChannelStop(recordhandle); + recordhandle = 0; + somethingclosed = true; + } + + if (somethingclosed) { + if (bsml!=null) bsml.Closed(); + } + + } + + @Override + public int writeData(byte[] buffer, int offset, int length) { + if (streamhandle!=0) { + Pointer buf = new Memory(length); + buf.write(0, buffer, 0, length); + int rs = BassLibrary.BASS.BASS_StreamPutData(streamhandle, buf, length); + if (rs==-1) { + if (bsml!=null) bsml.log_error("writeData BASS_StreamPutData error = "+BassLibrary.BASS.GetBassError("BASS_StreamPutData")); + } else { + + if (bsml!=null) bsml.WriteFromRTP(length, buffer); + return length; + } + + } + return 0; + } + + private void logger_error(String msg) { + if (bsml!=null) bsml.log_error(msg); + + } + + private void logger_info(String msg) { + if (bsml!=null) bsml.log_info(msg); + + } + + SYNCPROC devfail = new SYNCPROC() { + + @Override + public void SYNCPROC(int handle, int channel, int data, Pointer user) { + if (channel==streamhandle) { + if (bsml!=null) bsml.DeviceFailure("OutputChannel stops unexpectedly"); + } else if (channel==recordhandle) { + if (bsml!=null) bsml.DeviceFailure("InputChannel stops unexpectedly"); + } + } + }; + + SYNCPROC bufferinginformation = new SYNCPROC() { + + @Override + public void SYNCPROC(int handle, int channel, int data, Pointer user) { + if (channel==streamhandle) { + if (bsml!=null) bsml.BufferingInformation("OutputChannel "+ (data==0?"Stalled":"Resumed")); + } else if (channel==recordhandle) { + if (bsml!=null) bsml.BufferingInformation("InputChannel "+ (data==0?"Stalled":"Resumed")); + } + } + + }; +} diff --git a/src/code/BassSoundManagerListener.java b/src/code/BassSoundManagerListener.java new file mode 100644 index 0000000..1f3808a --- /dev/null +++ b/src/code/BassSoundManagerListener.java @@ -0,0 +1,18 @@ +package code; + +/** + * Java Event untuk BassSoundManager + * @author rdkartono + * + */ +public interface BassSoundManagerListener { + public void Opened(); + public void Closed(); + public void WriteFromRTP(int length, byte[] pcmdata); + public void ReadToRTP(int length, byte[] pcmdata); + public void log_info(String msg); + public void log_error(String msg); + public void DeviceFailure(String msg); + public void BufferingInformation(String msg); + public void ChannelLevel(boolean is_input, int value); +} diff --git a/src/code/CgiClient.java b/src/code/CgiClient.java new file mode 100644 index 0000000..6f66d1e --- /dev/null +++ b/src/code/CgiClient.java @@ -0,0 +1,510 @@ +package code; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CookieStore; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ContentProducer; +import org.apache.http.entity.EntityTemplate; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.auth.DigestScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * CGI Client to VAPIX Device + * @author rdkartono + * + */ +public class CgiClient { + protected final HttpHost target; + private final UsernamePasswordCredentials userpasscred; + private final CredentialsProvider credsProvider; + private final CookieStore cookieStore; + protected final CloseableHttpClient httpclient; + protected final DigestScheme digestAuth; + protected final AuthCache authCache; + protected RequestConfig reqconfig; + protected InetAddress localip = null; + + /** + * Initialize CGI Client + * @param hostname host name or IP address, default to 192.168.1.1 if null + * @param port port number, default to 80 if invalid number + * @param username specified username , default to "root" if null + * @param password specified password, default to "pass" if null + */ + public CgiClient(String hostname, int port, String username, String password) { + if (!common.ValidString(hostname)) hostname="192.168.1.1"; + if (!common.ValidPortNumber(port)) port = 80; + if (!common.ValidString(username)) username="root"; + if (!common.ValidString(password)) password = "pass"; + + setUsingLocalIP(null); + target = new HttpHost(hostname, port, "http"); + userpasscred = new UsernamePasswordCredentials(username, password); + credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(hostname, port), + userpasscred); + cookieStore = new BasicCookieStore(); + httpclient = HttpClients.custom() + .setDefaultCookieStore(cookieStore) + .setDefaultCredentialsProvider(credsProvider) + .build(); + + + digestAuth = new DigestScheme(); + digestAuth.overrideParamter("qop", "auth"); + digestAuth.overrideParamter("nc", "0"); + digestAuth.overrideParamter("cnonce", DigestScheme.createCnonce()); + authCache = new BasicAuthCache(); + authCache.put(target, digestAuth); + + + } + + public void setUsingLocalIP(String ip) { + try { + localip = common.ValidString(ip) ? InetAddress.getByName(ip) : InetAddress.getLocalHost(); + return; + } catch (UnknownHostException e) { + System.out.println("setUsingLocalIP exception on ip="+ip+", Message = "+e.getMessage()); + } + localip = null; + } + + protected RequestConfig getRequestConfig() { + + RequestConfig result = RequestConfig.custom() + .setLocalAddress(localip) + .build(); + return result; + } + + /** + * Close HTTP Client used to this object + * @return true if can be closed + */ + public boolean CloseHttpClient() { + + if (httpclient!=null) { + try { + httpclient.close(); + return true; + } catch (IOException e) { + raise_log("CloseHttpClient exception="+e.getMessage()); + } + } + + return false; + } + +// public CloseableHttpResponse HttpPost_ByteArray(String urlpath, byte[] value) { +// if (!common.ValidString(urlpath)) urlpath = "/"; // to default +// raise_log("About to HttpPost from "+urlpath); +// HttpPost httppost = new HttpPost(urlpath); +// httppost.setConfig(getRequestConfig()); +// HttpClientContext localContext = HttpClientContext.create(); +// localContext.setAuthCache(authCache); +// +// CloseableHttpResponse response = null; +// +// +// } + + public CloseableHttpResponse HttpPost_Filename(String filepath, String urlpath, String... values) { + if (!common.ValidString(urlpath)) urlpath = "/"; // to default + if (common.ValidArrayOfString(values)) { + urlpath+=Stream.of(values).reduce("?", (partialstring,nextelement)->partialstring+"&"+nextelement); + } + raise_log("About to HttpPost from "+urlpath); + HttpPost httppost = new HttpPost(urlpath); + httppost.setConfig(getRequestConfig()); + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + + CloseableHttpResponse response = null; + + File fis = new File(filepath); + if (fis!=null && fis.exists() && fis.isFile()) { + try { + + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("file_upload", fis) + .build(); + + httppost.setEntity(entity); + httppost.setHeader("Content-Type","multipart/form-data"); + + response = httpclient.execute(target, httppost, localContext); + //int code = GetStatusCode(response); + //String message = GetStatusMessage(response); + //String content = common.ConcatListOfString(GetContent(response)); + //raise_log("HttpPost_Filename Response Code="+code+", Message="+message+", Content="+content); + + } catch (IOException e) { + raise_log("HttpPost_Filename exception="+e.getMessage()); + } + + } else raise_log("HttpPost_Filename failed, invalid filepath="+filepath); + + + + return response; + } + + public CloseableHttpResponse HttpPost(String ContentType, ContentProducer cp, String urlpath, String... parameters) { + if (!common.ValidString(urlpath)) urlpath = "/"; // to default + raise_log("About to HttpPost from "+urlpath); + HttpPost httppost = new HttpPost(urlpath); + httppost.setConfig(getRequestConfig()); + + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + + CloseableHttpResponse response = null; + + HttpEntity entity = new EntityTemplate(cp); + + httppost.setEntity(entity); + httppost.setHeader("Content-type", ContentType); + + try { + response = httpclient.execute(target, httppost, localContext); + return response; + } catch (IOException e) { + raise_log("HttpPost exception = "+e.getMessage()); + } + + return null; + } + + + /** + * HTTP Post + * @param urlpath target URL + * @param value Post values that will be written in Body + * @return null if failed; + */ + public CloseableHttpResponse HttpPost(String urlpath, JsonObject value) { + if (!common.ValidString(urlpath)) urlpath = "/"; // to default + //raise_log("About to HttpPost from "+urlpath); + HttpPost httppost = new HttpPost(urlpath); + httppost.setConfig(getRequestConfig()); + + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + + CloseableHttpResponse response = null; + + + try { + StringEntity poststring = new StringEntity(value.toString()); + + httppost.setEntity(poststring); + httppost.setHeader("Content-type", "application/json"); + + response = httpclient.execute(target,httppost, localContext); + //int code = GetStatusCode(response); + //String message = GetStatusMessage(response); + //raise_log("HttpPost Response Code="+code+", Message="+message); + + } catch (IOException e) { + raise_log("HttpPost exception="+e.getMessage()); + } + return response; + } + + /** + * Doing HTTP Get to specified urlpath + * @param urlpath url path, default to "/" if null + * @return CloseableHttpResponse object or null if failed + */ + public CloseableHttpResponse HttpGet(String urlpath, String... parameters){ + if (!common.ValidString(urlpath)) urlpath = "/"; // to default + if (common.ValidArrayOfString(parameters)) { + urlpath+=Stream.of(parameters).reduce("?", (partialstring,nextelement)->partialstring+"&"+nextelement); + } + //raise_log("About to HttpGet from "+urlpath); + HttpGet httpget = new HttpGet(urlpath); + httpget.setConfig(getRequestConfig()); + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + CloseableHttpResponse response = null; + + try { + response = httpclient.execute(target, httpget, localContext); + int code = GetStatusCode(response); + //String message = GetStatusMessage(response); + //raise_log("HttpGet Response Code="+code+", Message="+message); + if (code==401) { + // rejected karena authentication + Map wwwauth = GetWWWAuthentication(response); + if (wwwauth!=null && wwwauth.size()>0) { + raise_log("HttpGet rejected, Need to update WWW authentication"); + String opaque = wwwauth.get("opaque"); + if (common.ValidString(opaque)) { + replace_digestauthvalue("opaque", opaque); + } + String nonce = wwwauth.get("nonce"); + if (common.ValidString(nonce)) { + replace_digestauthvalue("nonce",nonce); + + } + String realm = wwwauth.get("Digest realm"); + if (common.ValidString(realm)) { + replace_digestauthvalue("realm",realm); + } + } + + } + } catch (IOException e) { + raise_log("HttpGet exception="+e.getMessage()); + } + + return response; + } + + private void replace_digestauthvalue(String key, String newvalue) { + if (digestAuth!=null) { + if (common.ValidString(key)) { + if (common.ValidString(newvalue)) { + String oldvalue = digestAuth.getParameter(key); + if (oldvalue==null) { + raise_log("Set key="+key+" value="+newvalue); + digestAuth.overrideParamter(key, newvalue); + } else if (newvalue.equals(oldvalue)==false) { + raise_log("Replacing key="+key+", old value="+oldvalue+" with new value="+newvalue); + digestAuth.overrideParamter(key, newvalue); + } else raise_log("key="+key+", already same value"); + } + } + } + } + + /** + * Get HTTP Status Code from CloseableHttpResponse object + * @param response CloseableHttpResponse object + * @return 0 if failed + */ + public int GetStatusCode(CloseableHttpResponse response) { + if (response!=null) { + if (response.getStatusLine()!=null) { + return response.getStatusLine().getStatusCode(); + } + } + return 0; + } + + /** + * Get HTTP Status Message from CloseableHttpResponse object + * @param response CloseableHttpResponse object + * @return null if failed + */ + public String GetStatusMessage(CloseableHttpResponse response) { + if (response!=null) { + if (response.getStatusLine()!=null) { + return response.getStatusLine().getReasonPhrase(); + } + } + return null; + } + + /** + * Get HTTP Content Type from CloseableHttpResponse object + * @param response CloseableHttpResponse object + * @return null if failed + */ + public String GetContentType(CloseableHttpResponse response) { + HttpEntity ent = GetResponseEntity(response); + return ent!=null ? ent.getContentType().getValue() : null; + } + + /** + * Get Content Length + * @param response CloseableHttpResponse object + * @return 0 if failed, or -1 if unknown from HTTP Response + */ + public long GetContentLength(CloseableHttpResponse response) { + HttpEntity ent = GetResponseEntity(response); + return ent!=null ? ent.getContentLength() : 0; + + } + + /** + * Get FileName from CloseableHttpResponse + * @param response CloseableHttpResponse object + * @return null if not available + */ + public String GetFileName(CloseableHttpResponse response) { + if (response!=null) { + String vv = response.getFirstHeader("Content-Disposition").getValue(); + if (common.ValidString(vv)) { + Pattern pp = Pattern.compile("filename=\"(\\S+)\""); + Matcher mm = pp.matcher(vv); + if (mm.find()) { + return mm.group(1); + } + } + + } + return null; + } + + /** + * Get HTTP Entity from a CloseableHttpResponse + * @param response CloseableHttpResponse object + * @return null if failed + */ + public HttpEntity GetResponseEntity(CloseableHttpResponse response) { + if (response!=null) { + return response.getEntity(); + } + return null; + } + + /** + * Close CloseableHttpResponse Object + * @param response CloseableHttpResponse object + */ + public void Close(CloseableHttpResponse response) { + if (response!=null) { + try { + response.close(); + } catch (IOException e) { + raise_log("Close exception="+e.getMessage()); + } + response = null; + } + } + + /** + * Get WWW-Authenticate headers from CloseableHttpResponse + * @param response CloseableHttpResponse object + * @return Empty Map if failed + */ + private Map GetWWWAuthentication(CloseableHttpResponse response){ + if (response!=null) { + Map wwwAuth = Arrays + .stream(response.getHeaders("WWW-Authenticate")[0] + .getElements()) + .collect(Collectors.toMap(HeaderElement::getName, + HeaderElement::getValue)); + + return wwwAuth; + } else return new HashMap(); + + } + + /** + * Get Content from CloseableHttpResponse object + * @param response CloseableHttpResponse object + * @return List of String + */ + public List GetContent(CloseableHttpResponse response){ + List result = new ArrayList(); + if (response!=null) { + HttpEntity entity = response.getEntity(); + if (entity!=null) { + try(InputStream is = entity.getContent()) { + BufferedReader buf = new BufferedReader(new InputStreamReader(is)); + buf.lines().forEach(ss->result.add(ss)); + } catch (UnsupportedOperationException | IOException e) { + raise_log("GetContent exception="+e.getMessage()); + } + + } + } + return result; + } + + /** + * Get Content from body, expect to return Bytes array + * @param response CloseableHttpResponse + * @return null if failed + */ + public byte[] GetContent_BytesArray(CloseableHttpResponse response) { + if (response!=null) { + HttpEntity entity = response.getEntity(); + if (entity!=null) { + try (InputStream is = entity.getContent()) { + ByteBuffer result = ByteBuffer.allocate(0); + + int len = 0; + while(true) { + byte[] xx = new byte[1024]; + len = is.read(xx); + if (len>0) { + ByteBuffer temp = ByteBuffer.allocate(result.capacity()+len); + temp.put(result); + temp.put(xx,0,len); + + result = temp; + } else break; + } + + return result.array(); + } catch (UnsupportedOperationException | IOException e) { + raise_log("GetContent_BytesArray failed, exception="+e.getMessage()); + } + + } + } + return null; + } + + /** + * Get content as JsonObject + * @param response CloseableHttpResponse + * @return null if failed + */ + public JsonObject GetContentAsJson(CloseableHttpResponse response) { + if (response!=null) { + return JsonParser.parseString(common.ConcatListOfString(GetContent(response))).getAsJsonObject(); + } + return null; + } + + private void raise_log(String msg) { + System.out.println(msg); + } + + + +} diff --git a/src/code/JVAPIX.java b/src/code/JVAPIX.java new file mode 100644 index 0000000..a29d72d --- /dev/null +++ b/src/code/JVAPIX.java @@ -0,0 +1,123 @@ +package code; +import java.util.HashMap; +import java.util.Map; + +import anywheresoftware.b4a.BA; + + +@BA.Version(0.06f) +@BA.ShortName("jVAPIX") +@BA.Events(values= { + "log(msg as string)" +}) +public class JVAPIX { + private BA ba = null; + private String event = null; + private Object Me = this; + //private ExecutorService exec = Executors.newCachedThreadPool(); + + private boolean inited = false; + private boolean need_log_event = false; + private Map devices = new HashMap(); + + + /** + * Initialize jAxisAudio + * @param event eventname + */ + public void Initialize(BA ba, String event) { + this.ba = ba; + this.event = event; + if (this.ba!=null) { + if (this.event!=null) { + if (this.event.length()>0) { + inited = true; + need_log_event = this.ba.subExists(event+"_log"); + } + } + } + } + + /** + * Connect to a Vapix Device + * Connected VapixDevice is stored in a Map + * @param hostname hostname or IP + * @param port port number + * @param username username + * @param password password + * @return true if can be connected and a valid Vapix Device + */ + public boolean Connect(String hostname, int port, String username, String password, String localip) { + VapixDevice dev = new VapixDevice(hostname, port, username, password, localip); + + boolean success = dev.GetParametersFromDevice(); + if (!success) success = dev.GetParametersFromDevice(); // ambil kedua kali + + if (success) { + + if (dev.getHTTPVersion().equals("3") && dev.IsAudioDevice()) { + raise_log("Device at "+hostname+":"+port+" is VapixAudio"); + VapixAudio va = new VapixAudio(dev); + VapixDevice prev = devices.put(hostname, va); + if (prev!=null) { + raise_log("Connect to "+hostname+":"+port+" has replaced a VapixDevice with SerialNumber="+prev.getSerialNumber()+", MAC="+prev.getMACAddress()); + } + } + + return true; + } + return success; //kalau gagal , berarti bukan Vapix Device + } + + /** + * Get all Connected Vapix Devices + * @return array of IP Address + */ + public String[] ConnectedDevices() { + if (devices.size()>0) { + return devices.keySet().stream().toArray(String[]::new); + } else return new String[0]; + } + + /** + * Disconnect a Vapix Device + * @param ip IP Address to disconnect + * @return true if can be disconnect + */ + public boolean DisconnectDevice(String ip) { + if (devices.size()>0) { + VapixDevice dev = devices.get(ip); + if (dev!=null) { + dev.Close(); + devices.remove(ip); + dev = null; + } + } + return false; + } + + /** + * Get Connected Vapix Device with specific ip address + * Object returned must be check if instanceof valid type, such as VapixAudio + * @param ipaddress ip address to search + * @return null if not exists + */ + public Object GetConnectedDevice(String ipaddress) { + return devices.get(ipaddress); + } + + /** + * Check if this Object is initialized + * @return true if inited + */ + public boolean IsInitialized() { + return inited; + } + + private void raise_log(String msg) { + if (ba!=null) { + if (need_log_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg}); + } else System.out.println(msg); + + } +} diff --git a/src/code/VapixAudio.java b/src/code/VapixAudio.java new file mode 100644 index 0000000..a82642d --- /dev/null +++ b/src/code/VapixAudio.java @@ -0,0 +1,1940 @@ +package code; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.ContentProducer; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; + +import anywheresoftware.b4a.BA; +import lombok.Getter; +import lombok.ToString; + + +@BA.Events(values= { + "receivebuffer(fromip as string, contenttype as string, buf() as byte)" +}) + +/** + * Vapix Audio Device + * @author rdkartono + * + */ +@BA.ShortName("VapixAudio") +public class VapixAudio extends VapixDevice{ + private Gson gs; + public AudioApi AudioApi; + public MediaApi MediaApi; + public AudioRelayServiceApi AudioRelayServiceApi; + public AudioControlServiceApi AudioControlServiceApi; + public CallServiceApi CallServiceApi; + + + private boolean need_receivebuffer_event = false; + + + public VapixAudio(VapixDevice dev) { + super(dev); + + if (ba_inited) { + need_receivebuffer_event = ba.subExists(event+"_receivebuffer"); + } + + Me = this; + gs = new Gson(); + AudioApi = new AudioApi(); + MediaApi = new MediaApi(); + AudioRelayServiceApi = new AudioRelayServiceApi(); + AudioControlServiceApi = new AudioControlServiceApi(); + CallServiceApi = new CallServiceApi(); + } + + + private void raise_receivebuffer(String fromip, String contenttype, byte[] buf) { + if (need_receivebuffer_event) { + ba.raiseEventFromDifferentThread(Me, null, 0, event+"_receivebuffer", false, new Object[] {fromip, contenttype, buf}); + } + } + + /** + * Class to group Methods for Audio API + * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10036015/display + * @author rdkartono + * + */ + public class AudioApi{ + + // Buffer used for Start_Receive thread + private ByteBuffer rxbuf = ByteBuffer.allocate(0); + private @Getter Boolean ReceiveRunning = false; + + // Buffer used for Start_Transmit thread + private ByteBuffer txbuf = ByteBuffer.allocate(0); + private @Getter Boolean TransmitRunning = false; + + public boolean IsSupported() { + String ver = GetParameterValue("Properties.API.HTTP.Version"); + String yes1 = GetParameterValue("Properties.Audio.Audio"); + if ("3".equals(ver)) { + if ("yes".equals(yes1)) { + return true; + } + } + return false; + } + + /** + * Check if have audio input on channel + * @param channel start from 0 + * @return true if have + */ + public boolean HasAudioInput(int channel) { + return "yes".equals(GetParameterValue("Properties.Audio.Source.A"+channel+".Input")); + +// String result = GetParameterValue("Properties.Audio.Source.A"+channel+".Input"); +// if (common.ValidString(result)) { +// return result.equals("yes"); +// } +// return false; + } + + /** + * Check if have audio output on channel + * @param channel start from 0 + * @return true if have + */ + public boolean HasAudioOutput(int channel) { + return "yes".equals(GetParameterValue("Properties.Audio.Source.A"+channel+".Input")); + +// String result = c; +// if (common.ValidString(result)) { +// return result.equals("yes"); +// } +// return false; + } + + /** + * Check Supported Encoding Format + * @return empty String array if failed + */ + public String[] EncodingFormat() { + String result = GetParameterValue("Properties.Audio.Format"); + if (common.ValidString(result)) { + return result.split(","); + } else return new String[0]; + } + + /** + * Check Supported Decoding Format + * @return empty String array if failed + */ + public String[] DecodingFormat() { + String result = GetParameterValue("Properties.Audio.Decoder.Format"); + if (common.ValidString(result)) { + return result.split(","); + } else return new String[0]; + } + + /** + * Check Supported Input Level Type + * typical result = mic, line + * @return empty String array if failed + */ + public String[] InputType() { + String result = GetParameterValue("Properties.Audio.InputType"); + if (common.ValidString(result)) { + return result.split(","); + } else return new String[0]; + } + + /** + * Check supported Duplex Mode + * Typical result = full, half, post, get + * @return empty String array if failed + */ + public String[] DuplexMode() { + String result = GetParameterValue("Properties.Audio.DuplexMode"); + if (common.ValidString(result)) { + return result.split(","); + } else return new String[0]; + } + + /** + * Check if Audio Support is enabled on audio channel + * @param ID start from 0 + * @return true if audio function is supported + */ + public boolean GetAudioSupport(int ID) { + return "yes".equals(GetParameterValue("AudioSource.A"+ID+".AudioSupport")); + +// String val = GetParameterValue("AudioSource.A"+ID+".AudioSupport"); +// if (common.ValidString(val)) { +// return val.equals("yes"); +// } +// return false; + } + + /** + * Change Audio Support status on audio channel + * @param ID start from 0 + * @param yes if true, enable audio support on ID + * @return true if can be set + */ + public boolean ChangeAudioSupport(int ID, boolean yes) { + int code = HTTPGet_NoResult("/axis-cgi/param.cgi","action=update",MessageFormat.format("AudioSource.A{0}.AudioSupport={1}", ID, (yes ? "yes":"no"))); + return code==200; + } + + /** + * Get Audio Enabled on audio channel + * @param ID start from 0 + * @return true if enabled + */ + public boolean GetAudioEnabled(int ID) { + return "yes".equals(GetParameterValue("Audio.A"+ID+".Enabled")); + +// String val = GetParameterValue("Audio.A"+ID+".Enabled"); +// if (common.ValidString(val)) { +// return val.equals("yes"); +// } +// return false; + } + + /** + * Change Audio Enabled on audio channel + * @param ID start from 0 + * @param yes if true, enable audio + * @return true if can be set + */ + public boolean ChangeAudioEnabled(int ID, boolean yes) { + int code = HTTPGet_NoResult("/axis-cgi/param.cgi","action=update",MessageFormat.format("Audio.A{0}.Enabled={1}", ID, (yes ? "yes":"no"))); + return code==200; + } + + /** + * Check available bytes to read from ReceiveBuffer + * @return 0 if empty + */ + public int AvailableReceivedBytes() { + if (common.ByteBufferReady(rxbuf)) { + if (common.IsReadMode(rxbuf)) { + return rxbuf.remaining(); + } else { + return rxbuf.position(); + } + } + return 0; + } + + /** + * Get All available bytes from Receive Buffer + * @return null if failed or not available + */ +// public byte[] GetReceivedBuffers() { +// if (common.ByteBufferReady(rxbuf)) { +// synchronized(rxbuf) { +// // berubah jadi read mode kalau perlu +// if (common.ChangeToReadMode(rxbuf)) { +// int len = common.AvailableToRead(rxbuf); +// if (len>0) { +// byte[] buf = new byte[len]; +// rxbuf.get(buf); +// common.ChangeToWriteMode(rxbuf); +// return buf; +// } +// } +// } +// } +// return null; +// } + + /** + * Push some bytes to Transmit Buffer + * @param buf buffer to push + * @return true if can be pushed + */ + public boolean PutTransmitBuffer(byte[] buf) { + if (buf!=null && buf.length>0) { + int len = buf.length + txbuf.capacity(); + ByteBuffer temp = ByteBuffer.allocate(len); + temp.put(txbuf); + temp.put(buf); + + txbuf = temp; + return true; + } + return false; + } + + /** + * Get available bytes from Receive Buffer, by some limit + * @param maxsize how many bytes to read, give 0 to read all + * @return null if failed or not available + */ + public byte[] GetReceivedBuffers_bySize(final int maxsize) { + + if (common.ByteBufferReady(rxbuf)) { + synchronized(rxbuf) { + if (rxbuf.position()>0) { + // sudah ada yang ketulis + rxbuf.flip(); // pindah mode baca + int len = rxbuf.remaining(); + if (maxsize>0) { + if (len>maxsize) len = maxsize; + } + byte[] buf = new byte[len]; + rxbuf.get(buf); + rxbuf.compact(); // pindah mode tulis + return buf; + } + } + } + return null; + } + + /** + * Start Receive + * Receive : tell VapixAudio to start giving streaming bytes to Java app + * Java App use GetReceiveBuffers function to get data from VapixAudio + * @return true if can be started + */ + public boolean Start_Receive() { + //TODO belum ditest + Stop_Receive(); + CloseableHttpResponse resp = client.HttpGet("/axis-cgi/audio/receive.cgi", "httptype=singlepart"); + if (resp!=null) { + int code = client.GetStatusCode(resp); + String msg = client.GetStatusMessage(resp); + if (code==200) { + Thread thread = new Thread(()->{ + String contenttype = resp.getEntity().getContentType().getValue(); + long contentlength = resp.getEntity().getContentLength(); + try (InputStream inp = resp.getEntity().getContent()) { + + ReceiveRunning = true; + raise_log(MessageFormat.format("Start_Receive started, Content-Type={0}, Content-Length={1}", contenttype, contentlength)); + while(ReceiveRunning) { + byte[] xx = new byte[1024]; + int len; + + try { + + len = inp.read(xx); + } catch (IOException e) { + raise_log("ReceiverRunner IOException = "+e.getMessage()); + Stop_Receive(); + len = 0; + } + if (len>0) { + + if (len!=xx.length) { + byte[] yy = Arrays.copyOf(xx, len); + xx = yy; + } + //raise_log("ReceiveRunner got "+len+" bytes"); + raise_receivebuffer(client.target.getHostName(),contenttype,xx); + + ByteBuffer temp = ByteBuffer.allocate(rxbuf.capacity()+len); + temp.put(rxbuf); + temp.put(xx); + rxbuf = temp; + + //raise_log("ReceiveRunner got "+len+" bytes, rxbuf capacity = "+rxbuf.capacity()); + } + } + raise_log("Start_Receive stopped"); + resp.close(); + } catch (UnsupportedOperationException | IOException e) { + raise_log("Start_Receive exception = "+e.getMessage()); + } + + }) ; + thread.start(); + return true; + } else raise_log("Start_Receive code = "+code+", Message = "+msg); + } else raise_log("Start_Receive HttpGet returning null"); + return false; + } + + /** + * Stop Receive + */ + public void Stop_Receive() { + ReceiveRunning = false; + rxbuf = ByteBuffer.allocate(0); + } + + /** + * Start Transmit + * Transmit : tell VapixAudio to start receiving bytes from Java app + * Java app use PutTransmitBuffer function to push data to VapixAudio + * @return true if can be started + */ + public boolean Start_Transmit() { + //TODO belum dibuat + Stop_Transmit(); + + CloseableHttpResponse resp = client.HttpPost("audio/basic", new ContentProducer() { + + @Override + public void writeTo(OutputStream outStream) { + if (TransmitRunning) { + if (txbuf!=null) { + // masih lanjut + synchronized(txbuf) { + if (common.IsWriteMode(txbuf)) common.ChangeToReadMode(txbuf); + int len = common.AvailableToRead(txbuf); + if (len>0) { + byte[] xx = new byte[len]; + txbuf.get(xx); + try { + outStream.write(xx); + raise_log("Wrote "+len+" bytes to OutputStream ContentProducer"); + } catch (IOException e) { + raise_log("Write OutputStream ContentProducer IOException = "+e.getMessage()); + } + } + common.ChangeToWriteMode(txbuf); + } + } + } + + // finish + try { + outStream.close(); + raise_log("Closed OutputStream ContentProducer"); + } catch (IOException e) { + raise_log("Closing OutputStream ContentProducer IOException = "+e.getMessage()); + } + + } + + }, "/axis-cgi/audio/transmit.cgi", ""); + + if (resp!=null) { + int code = client.GetStatusCode(resp); + String msg = client.GetStatusMessage(resp); + raise_log(MessageFormat.format("Start_Transmit code={0}, Message={1}", code, msg)); + + if (code==200) { + Thread thread = new Thread(()->{ + raise_log("Start_Transmit started"); + TransmitRunning = true; + while(TransmitRunning) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + break; + } + } + raise_log("Start_Transmit stopped"); + }); + thread.start(); + + return true; + } + } + return false; + } + + /** + * Stop Transmit + */ + public void Stop_Transmit() { + TransmitRunning = false; + txbuf = ByteBuffer.allocate(0); + } + + + } + + /** + * Class to group Methods for Media Clip API + * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10036045/display + * @author rdkartono + * + */ + public class MediaApi{ + private final String url_param = "/axis-cgi/param.cgi"; + private final String url_mediaclip = "/axis-cgi/mediaclip.cgi"; + private final String url_playclip = "/axis-cgi/playclip.cgi"; + private final String maxgroups_header = "root.MediaClip.MaxGroups="; + private final String maxuploadsize_header = "root.MediaClip.MaxUploadSize="; + + /** + * Available Media Clips on the device + * Update this variable using GetAvailableMediaClips + */ + public MediaClip[] AvailableMediaClips; + + /** + * Maximum recognized Clip ID + */ + public int MediaClip_MaxGroups = 0; + + /** + * Upload Size in MB + */ + public int MediaClip_MaxUploadSize = 0; + + /** + * Check if Media Clip Playback is supported using specific Audio Ouput + * @param deviceid start from 0 + * @return true if supported + */ + public boolean IsSupported(int deviceid) { + String ver = GetParameterValue("Properties.API.HTTP.Version"); + String yes1 = GetParameterValue("Properties.Audio.Audio"); + String yes2 = GetParameterValue("Properties.Audio.Source.A0.Output"); + if ("3".equals(ver)) { + if ("yes".equals(yes1)) { + if ("yes".equals(yes2)) { + return true; + } + } + } + return false; + } + + /** + * Get Avaliable Media Clips installed at Device + * Result will be on variable AvailableMediaClips + * @return true if success + */ + public boolean GetAvailableMediaClips() { + AvailableMediaClips = new MediaClip[0]; + List getresult = HTTPGet(url_param, "action=list","group=MediaClip"); + if (common.ValidListOfString(getresult)) { + Pattern pp = Pattern.compile("root.MediaClip.M(\\d+).(\\S+)=(\\S+( \\w+)*)"); + Map map = new HashMap(); + getresult.forEach(ss ->{ + //raise_log(ss); + Matcher mm = pp.matcher(ss); + if (mm.find()) { + try { + Integer ID = Integer.valueOf(mm.group(1)); + String parameter = mm.group(2); + String value = mm.group(3); + //raise_log(MessageFormat.format("ID={0}, Parameter={1}, Value={2}", ID, parameter, value)); + MediaClip xx = map.get(ID); + if (xx==null) { + xx = new MediaClip(); + xx.ID = ID; + map.put(ID, xx); + } + // pake Reflection + MediaClip.class.getField(parameter).set(xx, value); + + } catch(IllegalArgumentException | IndexOutOfBoundsException | IllegalStateException | SecurityException | IllegalAccessException | NoSuchFieldException ex) { + raise_log("GetAvailableMediaClips failed for value="+ss+", Exception="+ex.getMessage()); + } + + } else { + if (ss.startsWith(maxgroups_header)) { + MediaClip_MaxGroups = Integer.parseInt(ss.substring(maxgroups_header.length())); + raise_log("MediaClip MaxGroups = "+MediaClip_MaxGroups); + } else if (ss.startsWith(maxuploadsize_header)) { + MediaClip_MaxUploadSize = Integer.parseInt(ss.substring(maxuploadsize_header.length())); + raise_log("MediaClip MaxUploadSize = "+MediaClip_MaxUploadSize); + } + } + }); + if (map.size()>0) { + AvailableMediaClips = map.values().toArray(new MediaClip[0]); + return true; + } + + } + return false; + } + + /** + * Play Media Clip + * @param AudioID Audio Output ID, start from 0 + * @param ClipID start from 0, get from MediaClip Object + * @return true if can be played + */ + public boolean PlayClip(int AudioID, int ClipID) { + String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=play","clip="+ClipID,"audiooutput="+(AudioID+1)); + if (common.ValidString(result)) { + raise_log(result); + if (result.contains("OKplaying=")) { + return true; + } + } + return false; + } + + /** + * Play Media Clip + * @param AudioID Audio Output ID, start from 1 + * @param ClipID start from 0, from MediaClip Object + * @param Repeat -1 = forever, 0 = play once, N = repeat again N times + * @param Volume 0 - 1000, 0 = mute, default at 100 + * @return String result + */ + public String PlayClip2(int AudioID, int ClipID, int Repeat, int Volume) { + if (AudioID<1) AudioID=1; + if (ClipID<0) ClipID=0; + if (Repeat<-1) Repeat = 0; + if (Volume<0) Volume = 0; + if (Volume>1000) Volume = 1000; + + return HTTPGet_GetString("text/plain", url_playclip,"clip="+ClipID,"audiooutput="+AudioID, "repeat="+Repeat, "volume="+Volume); + + } + + /** + * Remove Media Clip + * @param ClipID start from 0, get from MediaClip object + * @return true if can be removed + */ + public boolean RemoveClip(int ClipID) { + String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=remove","clip="+ClipID); + if (common.ValidString(result)) { + if (result.contains("removed=")) { + return true; + } + } + return false; + } + + /** + * Stop current playback + * @return true if can be stopped + */ + public boolean StopClip() { + String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=stop"); + if (common.ValidString(result)) { + if (result.contains("OKstopping")) { + return true; + } + } + return false; + } + + /** + * Download Media Clip + * result is bytes of array, content of file format .au (G711 u-law) + * sampling rate 8 Khz, mono, 8 bit u-law + * @param ClipID start from 0 + * @return array of bytes or null if failed + */ + public byte[] DownloadClip_ToByteArray(int ClipID) { + if (ClipID<0) ClipID = 0; + return HTTPGet_BytesResult("audio/basic",url_mediaclip,"action=download","clip="+ClipID ); + } + + /** + * Download Media Clip + * @param ClipID start from 0 + * @param savelocation Save location + * @return DownloadResult object if success, or null if failed + */ + public DownloadResult DownloadClip_ToFile(int ClipID, String savelocation) { + if (ClipID<0) ClipID = 0; + + DownloadResult dr = HTTPGet_DownloadFile(savelocation, url_mediaclip, "action=download","clip="+ClipID ); + return dr; + } + + /** + * Upload a Media Clip + * Not implemented yet + * @param clipname Name to remember for this Clip + * @param FileName File path to upload + * @return clip ID if success, or -1 if failed + */ + public int UploadClip(String clipname, String FileName) { + return HTTPPost_UploadFile(FileName, url_mediaclip,"action=upload","media=audio","name="+clipname); + + } + + /** + * Update a Media Clip at ClipID slot + * Not implemented yet + * @param ClipID start from 0 + * @param ClipName Clip Description + * @return true if success + */ + public boolean RenameClip(int ClipID, String ClipName) { + String result = HTTPGet_GetString("text/plain", url_mediaclip,"action=update","clip="+ClipID,"name="+ClipName); + //System.out.println(result); + if (common.ValidString(result)) { + if (result.contains("updated=")) { + return true; + } + } + return false; + + } + + /** + * Check Supported AudioClip file format + * @return empty String array if failed + */ + public String[] AudioClipFormat() { + String result = GetParameterValue("Properties.AudioClip.Format"); + if (common.ValidString(result)) { + return result.split(","); + } else return new String[0]; + } + + /** + * Dipakai untuk hasil GetAvailableMediaClips + * @author rdkartono + * + */ + @BA.ShortName("MediaClip") + public class MediaClip{ + public Integer ID; + public String Name; + public String Location; + public String Type; + } + } + + + /** + * Class to group Methods for Audio Relay Service API + * Source : https://www.axis.com/vapix-library/subjects/t10100065/section/t10095155/display + * @author rdkartono + * + */ + public class AudioRelayServiceApi{ + private final String url = "/vapix/audiorelay"; + + /** + * Check if Audio Relay Service is supported + * @return true if supported + */ + public boolean IsSupported() { + String ver = GetParameterValue("Properties.API.AudioRelay.Version"); + //raise_log("Properties.API.AudioControl.Version = "+ver); + if (common.ValidString(ver) && ver.contains("1.")) return true; else return false; + } + + + /** + * Create Broadcast Pattern + * @param senderip IP address of sender / leader + * @param receiverip array of IP address for receivers / followers + * @param use_multicast if true, broadcast using multicast. if false, using unicast + * @return array of Audio Peer Id + */ + public String[] CreateBroadcastPattern(String senderip, String[] receiverip, boolean use_multicast) { + //TODO belum tested + if (common.ValidString(senderip)) { + if (common.ValidArrayOfString(receiverip)) { + // buat configuration + JsonObject val = new SetAudioPeerConfigurations(senderip, receiverip, use_multicast).getValue(); + raise_log(gs.toJson(val)); + JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:SetAudioPeerConfigurations", val)); + if (result!=null && result.has("AudioPeerId")) { + try { + String[] api = gs.fromJson(result.get("AudioPeerId").getAsJsonArray(), String[].class); + raise_log(gs.toJson(api)); + return api; + } catch(IllegalStateException | NullPointerException | JsonSyntaxException e) { + raise_log("SetAudioPeerConfigurations result not contain AudioPeerId array"); + } + + } + } + } + + + return null; + } + + /** + * Add receiver / follower to existing broadcast + * @param ipaddress IP Address of follower + * @param name follower name / description if necessary + * @return AudioPeerId string, or null if failed + */ + public String AddReceiver(String ipaddress, String name) { + //TODO belum tested + SetAudioPeerConfiguration sap = new SetAudioPeerConfiguration(ipaddress, name); + JsonObject jo = new JsonObject(); + jo.add("axar:SetAudioNetworkConfiguration", sap.getValue()); + raise_log(gs.toJson(jo)); + + JsonObject result = HTTPPost(url, jo); + if (result!=null && result.has("AudioPeerId")) { + return result.get("AudioPeerId").getAsString(); + } + return null; + } + + /** + * Remove Audio Peer + * @param AudioPeerId Audio Peer Id + * @return true if can be removed + */ + public boolean RemoveAudioPeer(String AudioPeerId) { + //TODO belum tested + if (common.ValidString(AudioPeerId)) { + return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axar:RemoveAudioPeer", "AudioPeerId", AudioPeerId)); + } + return false; + } + + /** + * Get Audio Peers + * @return array of AudioPeerConfiguration or null if failed + */ + public AudioPeer[] GetAudioPeers() { + JsonObject result = HTTPPost(url, common.Create_1Level_Command("axar:GetAudioPeers", new JsonObject())); + if (result!=null && result.has("Peer")) { + try { + JsonArray ja = result.get("Peer").getAsJsonArray(); + int length = ja.size(); + + + if (length>0) { + AudioPeer[] apc = new AudioPeer[length]; + for(int ii=0;ii new SIPAccount(jo.getAsJsonObject())).toArray(SIPAccount[]::new); + + } + return new SIPAccount[0]; + } + + /** + * Get SIP Account Status + * @param SIPAccountId Account ID + * @return null if failed + */ + public SIPAccountStatus GetSIPAccountStatus(String SIPAccountId) { + if (common.ValidString(SIPAccountId)) { + JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:GetSIPAccountStatus", "SIPAccountId", SIPAccountId)); + if (result!=null && result.has("SIPAccountStatus")) { + //raise_log(result.toString()); + return new SIPAccountStatus(common.GetJsonObjectValue_JsonObject(result, "SIPAccountStatus")); + } + } + return null; + } + + /** + * Add a SIP User + * @param UserId Extension Number or user ID + * @param Password Password + * @param Registrar SIP Server + * @param PublicDomain Domain where this account + * @return String SIPAccountId or null if failed + */ + public String SetSIPAccount(String UserId, String Password, String Registrar, String PublicDomain) { + //TODO belum ditest + JsonObject jc = new JsonObject(); + if (common.ValidString(UserId)) jc.addProperty("UserId", UserId); + if (common.ValidString(Password)) jc.addProperty("Password", Password); + if (common.ValidString(Registrar)) jc.addProperty("Registrar", Registrar); + if (common.ValidString(PublicDomain)) jc.addProperty("PublicDomain", PublicDomain); + + JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:SetSIPAccount", "SIPAccount", jc)); + if (result!=null && result.has("SIPAccountId")) return common.GetJsonObjectValue_String(result, "SIPAccountId"); + return null; + } + + /** + * Add a SIP user + * @param UserId extension number or user iD + * @param Password password + * @param Registrar SIP Server + * @param PublicDomain Domain where this SIP Account + * @param proxies Proxy Server if required + * @return SIP Account ID + */ + public String SetSIPAccount(String UserId, String Password, String Registrar, String PublicDomain, SIPProxyServer[] proxies) { + //TODO belum di test + JsonObject jc = new JsonObject(); + if (common.ValidString(UserId)) jc.addProperty("UserId", UserId); + if (common.ValidString(Password)) jc.addProperty("Password", Password); + if (common.ValidString(Registrar)) jc.addProperty("Registrar", Registrar); + if (common.ValidString(PublicDomain)) jc.addProperty("PublicDomain", PublicDomain); + if (proxies!=null && proxies.length>0) { + JsonArray jd = new JsonArray(); + for(SIPProxyServer prox : proxies) jd.add(prox.getJsonObject()); + jc.add("SIPAccount", jd); + } + JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:SetSIPAccount", "SIPAccount", jc)); + if (result!=null && result.has("SIPAccountId")) return common.GetJsonObjectValue_String(result, "SIPAccountId"); + return null; + } + + /** + * Try to call another device + * Use SIP URI in format sip:extension@publicdomain + * example sip:6002@example.axis.com + * @param SIPURI SIP URI. Get it from Registered_SIP_Accounts + * @return CallID if success or null if failed + */ + public String Call(String SIPURI) { + if (common.ValidString(SIPURI)) { + JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:Call", "To", SIPURI)); + if (result!=null && result.has("CallId")) { + return common.GetJsonObjectValue_String(result, "CallId"); + } + } return null; + } + + /** + * Get Call Status + * @param CallId CallId , result of Call function + * @return SIPCallStatus object, or null if failed + */ + public SIPCallStatus GetCallStatus(String CallId) { + if (common.ValidString(CallId)) { + JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:GetCallStatus", "CallId", CallId)); + if (result!=null && result.has("CallStatus")) { + SIPCallStatus scs = gs.fromJson(result.get("CallStatus"), SIPCallStatus.class); + System.out.println(scs.toString()); + return scs; + } + } + return null; + } + + /** + * Get All Call Statuses + * @return SIPCallStatus array object, or null if failed + */ + public SIPCallStatus[] GetCallStatuses() { + JsonObject result = HTTPPost(url, common.Create_1Level_Command("axcall:GetCallStatuses", new JsonObject())); + if (result!=null && result.has("CallStatus")) { + try { + JsonArray ja = result.get("CallStatus").getAsJsonArray(); + SIPCallStatus[] vv = ja.asList() + .stream() + .map(je -> gs.fromJson(je, SIPCallStatus.class)) + .toArray(SIPCallStatus[]::new); + if (vv!=null && vv.length>0) { + for(SIPCallStatus vx : vv) { + System.out.println(vx); + } + + return vv; + } + } catch(IllegalStateException e) { + raise_log("GetCallStatuses exception="+e.getMessage()); + } + + } + return null; + } + + /** + * Terminate a call + * @param CallId CallId, result of Call function + * @return true if success + */ + public boolean TerminateCall(String CallId) { + if (common.ValidString(CallId)) { + return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axcall:TerminateCall", "CallId", CallId)); + } + return false; + } + + /** + * Create a DTMF Event + * @param Name Name of event + * @param Enabled true or false + * @return DTMFEventID string if success, or null if failed + */ + public String SetDTMFEvent(String Name, boolean Enabled) { + //TODO belum ditest + if (common.ValidString(Name)) { + JsonObject ja = new JsonObject(); + ja.addProperty("Name", Name); + ja.addProperty("Enabled", Enabled); + + JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:SetDTMFEvent", "DTMFEvent", ja)); + if (result!=null && result.has("DTMFEventId")) { + return common.GetJsonObjectValue_String(result, "DTMFEventId"); + } + } + return null; + } + + /** + * Remove a DTMF Event + * @param DTMFEventId value from SetDTMFEvent + * @return true if can be removed + */ + public boolean RemoveDTMFEvent(String DTMFEventId) { + //TODO belum ditest + if (common.ValidString(DTMFEventId)) { + return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axcall:RemoveDTMFEvent", "DTMFEventId", DTMFEventId)); + } + return false; + } + + /** + * Create DTMF Trigger + * @param DTMFSequence DTMF Code that trigger a DTMF Event + * @param DTMFEventId DTMF Event Id from SetDTMFEvent + * @return DTMFTriggerId if success, or null if failed + */ + public String SetDTMFTrigger(String DTMFSequence, String DTMFEventId) { + //TODO belum ditest + if (common.ValidString(DTMFSequence)) { + if (common.ValidString(DTMFEventId)) { + JsonObject ja = new JsonObject(); + ja.addProperty("DTMFSequence", DTMFSequence); + ja.addProperty("DTMFEventId", DTMFEventId); + JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall.SetDTMFTrigger", "DTMFTrigger", ja)); + if (result!=null && result.has("DTMFTriggerId")) { + return common.GetJsonObjectValue_String(result, "DTMFTriggerId"); + } + + } + } + return null; + } + + /** + * Get DTMFTrigger configuration from DTMFTriggerId + * @param DTMFTriggerId DTMF Trigger Id + * @return null if failed + */ + public DTMFTrigger GetDTMFTrigger(String DTMFTriggerId) { + //TODO belum ditest + if (common.ValidString(DTMFTriggerId)) { + JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:GetDTMFTrigger", "DTMFTriggerId", DTMFTriggerId)); + if (result!=null && result.has("DTMFTrigger")) { + return new DTMFTrigger(common.GetJsonObjectValue_JsonObject(result, "DTMFTrigger")); + } + } + return null; + } + + /** + * Create DTMF Configuration + * @param Name some name + * @param DTMFTriggerId DTMF Trigger Id + * @return DTMFConfigurationId if success, or null if failed + */ + public String SetDTMFConfiguration(String Name, String DTMFTriggerId) { + //TODO belum ditest + if (common.ValidString(Name)) { + if (common.ValidString(DTMFTriggerId)) { + JsonObject jo = new JsonObject(); + jo.addProperty("Name", Name); + jo.addProperty("DTMFTriggerId", DTMFTriggerId); + + JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:SetDTMFConfiguration", "DTMFConfiguration", jo)); + if (result!=null && result.has("DTMFConfigurationId")) { + return common.GetJsonObjectValue_String(result, "DTMFConfigurationId"); + } + } + } + return null; + } + + /** + * Remove DTMF Configuration + * @param DTMFConfigurationId DTMF Configuration Id + * @return true if can be removed + */ + public boolean RemoveDTMFConfiguration(String DTMFConfigurationId) { + //TODO belum ditest + if (common.ValidString(DTMFConfigurationId)) { + return HTTPPost_BooleanResult(url, common.Create_2Level_Command("axcall:RemoveDTMFConfiguration", "DTMFConfigurationId", DTMFConfigurationId)); + } + return false; + } + + /** + * Get DTMF Configuration + * @param DTMFConfigurationId DTMF Configuration Id + * @return DTMFConfiguration if success, or null if failed + */ + public DTMFConfiguration GetDTMFConfiguration(String DTMFConfigurationId) { + //TODO belum ditest + if (common.ValidString(DTMFConfigurationId)) { + JsonObject result = HTTPPost(url, common.Create_2Level_Command("axcall:GetDTMFConfiguration", "DTMFConfigurationId", DTMFConfigurationId)); + if (result!=null && result.has("DTMFConfiguration")) { + return new DTMFConfiguration(common.GetJsonObjectValue_JsonObject(result, "DTMFConfiguration")); + } + } + return null; + } + + @BA.ShortName("DTMFConfiguration") + public class DTMFConfiguration{ + public String Name; + public String DTMFTriggerId; + public String Id; + public boolean RFC2833; + public boolean RFC2976; + public DTMFConfiguration(JsonObject jo) { + if (jo!=null) { + Name = common.GetJsonObjectValue_String(jo, "Name"); + DTMFTriggerId = common.GetJsonObjectValue_String(jo, "DTMFTriggerId"); + Id = common.GetJsonObjectValue_String(jo, "Id"); + RFC2833 = common.GetJsonObjectValue_Boolean(jo, "RFC2833"); + RFC2976 = common.GetJsonObjectValue_Boolean(jo, "RFC2976"); + } + } + } + + @BA.ShortName("DTMFTrigger") + public class DTMFTrigger{ + public String Id; + public String DTMFSequence; + public String DTMFEventId; + public DTMFTrigger(JsonObject jo) { + if (jo!=null) { + Id = common.GetJsonObjectValue_String(jo, "Id"); + DTMFSequence = common.GetJsonObjectValue_String(jo, "DTMFSequence"); + DTMFEventId = common.GetJsonObjectValue_String(jo, "DTMFEventId"); + } + } + } + + @BA.ShortName("SIPCallStatus") + @ToString + public class SIPCallStatus{ + + /** + * Call Identifier + */ + public String CallId; + + /** + * Source dari call.. tapi belum konfirm bener + */ + public String Source; + + /** + * Active / Terminated + */ + public String CallState; + + + public String CallStateReason; + + /** + * Kapan incoming call terjadi + */ + public String StartTime; + + /** + * Kapan incoming call selesai + * Kalau belum selesai, null + */ + public String StopTime; + + /** + * Pada receiver, Direction = Incoming + */ public String Direction; + + /** + * SIP Account diri sendiri + */ + public String DeviceURI; + + /** + * SIP Account yang nge-call device ini + */ + public String RemoteURI; + + /** + * Codec = PCMU/8000 + */ + public String AudioCodec; + + /** + * SIP Account ID diri sendiri + */ + public String SIPAccountId; + + /** + * value = SIP + */ + public String CallType; + + /** + * Kalau gak ada, null + */ + public String VideoCodec; + + /** + * CallState = Active atau Terminated ? + * @return true kalau masih calling + */ + public boolean CallingIsActive() { + return "Active".equals(CallState); + } + + + } + + @BA.ShortName("SIPProxyServer") + public class SIPProxyServer{ + private JsonObject jo; + public SIPProxyServer() { + jo = new JsonObject(); + } + + public SIPProxyServer(JsonObject jo) { + if (jo!=null) { + this.jo = jo; + } + } + + public String getServer() { + return common.GetJsonObjectValue_String(jo, "Server"); + } + + public void setServer(String value) { + common.SetJsonObjectValue(jo, "Server", value); + } + + public String getUsername() { + return common.GetJsonObjectValue_String(jo, "Username"); + } + + public void setUsername(String value) { + common.SetJsonObjectValue(jo, "Username", value); + } + + public String getPassword() { + return common.GetJsonObjectValue_String(jo, "Password"); + } + + public void setPassword(String value) { + common.SetJsonObjectValue(jo, "Password", value); + } + + public JsonObject getJsonObject() { + return jo; + } + } + + @BA.ShortName("SIPAccountStatus") + public class SIPAccountStatus{ + public String SIPAccountId; + public String SIPURI; + public boolean IsDefault; + public String RegURI; + public int Status; + public String StatusText; + + public SIPAccountStatus(JsonObject jo) { + if (jo!=null) { + this.SIPAccountId = common.GetJsonObjectValue_String(jo, "SIPAccountId"); + this.SIPURI = common.GetJsonObjectValue_String(jo, "SIPURI"); + this.IsDefault = common.GetJsonObjectValue_Boolean(jo, "IsDefault"); + this.RegURI = common.GetJsonObjectValue_String(jo, "RegURI"); + this.Status = common.GetJsonObjectValue_Integer(jo, "Status"); + this.StatusText = common.GetJsonObjectValue_String(jo, "StatusText"); + } + } + + @Override + public String toString() { + return MessageFormat.format("Id:{0}, SIPURI:{1}, REGURI:{2}, Status:{3}, StatusText:{4}", SIPAccountId, SIPURI, RegURI, Status, StatusText); + } + } + + @BA.ShortName("SIPAccount") + public class SIPAccount{ + public String Id; + public Boolean Enabled; + public String Name; + public String CallerId; + public String UserId; + public String Password; + public String Registrar; + public String PublicDomain; + public Boolean IsDefault; + public Boolean AutoAnswerEnabled; + public String Transport; + public String StreamParameters; + public String DTMFConfigurationId; + public String MediaEncryption; + + public SIPAccount(JsonObject jo) { + if (jo!=null) { + Id = common.GetJsonObjectValue_String(jo, "Id"); + Enabled = common.GetJsonObjectValue_Boolean(jo, "Enabled"); + Name = common.GetJsonObjectValue_String(jo, "Name"); + CallerId = common.GetJsonObjectValue_String(jo, "CallerId"); + UserId = common.GetJsonObjectValue_String(jo, "UserId"); + Password = common.GetJsonObjectValue_String(jo, "Password"); + Registrar = common.GetJsonObjectValue_String(jo, "Registrar"); + PublicDomain = common.GetJsonObjectValue_String(jo, "PublicDomain"); + IsDefault = common.GetJsonObjectValue_Boolean(jo, "IsDefault"); + AutoAnswerEnabled = common.GetJsonObjectValue_Boolean(jo, "AutoAnswerEnabled"); + Transport = common.GetJsonObjectValue_String(jo, "Transport"); + StreamParameters = common.GetJsonObjectValue_String(jo, "StreamParameters"); + DTMFConfigurationId = common.GetJsonObjectValue_String(jo, "DTMFConfigurationId"); + MediaEncryption = common.GetJsonObjectValue_String(jo, "MediaEncryption"); + } + } + + @Override + public String toString() { + return MessageFormat.format("Id:{0}, UserId:{1}, Password:{2}, PublicDomain:{3}, Enabled:{4}", Id, UserId, Password, PublicDomain, Enabled); + } + + } + } + + public class SIPConfiguration{ + private JsonObject SIPConfiguration; + public SIPConfiguration(){ + SIPConfiguration = new JsonObject(); + } + + /** + * Re-Get SIP Configuration from Device + * @return true if can be get + */ + public boolean Get() { + JsonObject jo = new JsonObject(); + + jo.add("axcall:GetSIPConfiguration", new JsonObject()); + //raise_log("jo = "+jo.toString()); + JsonObject result = HTTPPost(url,jo); + if (result!=null) { + try { + SIPConfiguration = result.get("SIPConfiguration").getAsJsonObject(); + //raise_log("SIP Configuration = "+SIPConfiguration.toString()); + return true; + } catch(IllegalStateException | NullPointerException e) { + raise_log("axcall:GetSIPConfiguration not returning JsonObject"); + } + } + return false; + } + + + + + /** + * Set SIP Configuration to Device + * @return true if can be set + */ + public boolean Set() { + JsonObject result = HTTPPost(url,common.Create_2Level_Command("axcall:SetSIPConfiguration", "SIPConfiguration", SIPConfiguration)); + raise_log("SetSIPConfiguration result = "+result.toString()); + return true; + } + + public boolean getSIPEnabled() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "SIPEnabled"); + } + + public void setSIPEnabled(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "SIPEnabled", value); + } + + public boolean getICEEnabled() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "ICEEnabled"); + } + + public String[] getTURNServers() { + return common.GetJsonObjectValue_Strings(SIPConfiguration, "TURNServers"); + } + + public void setTURNServers(String[] server) { + common.SetJsonObjectValue(SIPConfiguration,"TURNServers" , server); + } + + public String[] getSTUNServers() { + return common.GetJsonObjectValue_Strings(SIPConfiguration, "STUNServers"); + } + + public void setSTUNServers(String[] server) { + common.SetJsonObjectValue(SIPConfiguration,"STUNServers" , server); + } + + public String[] getAllowedUsers() { + return common.GetJsonObjectValue_Strings(SIPConfiguration, "AllowedUsers"); + } + + public void setAllowedUsers(String[] users) { + common.SetJsonObjectValue(SIPConfiguration,"AllowedUsers" , users); + } + + public String[] getAllowedURIs() { + return common.GetJsonObjectValue_Strings(SIPConfiguration, "AllowedURIs"); + } + + public void setAllowedURIs(String[] uri) { + common.SetJsonObjectValue(SIPConfiguration,"AllowedURIs" , uri); + } + + public void setICEEnabled(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "ICEEnabled", value); + } + + public boolean getAllowIncomingCalls() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "AllowIncomingCalls"); + } + + public void setAllowIncomingCalls(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "AllowIncomingCalls", value); + } + + public boolean getTURNEnabled() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "TURNEnabled"); + } + + public void setTURNEnabled(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "TURNEnabled", value); + } + + public boolean getSTUNEnabled() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "STUNEnabled"); + } + + public void setSTUNEnabled(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "STUNEnabled", value); + } + + public boolean getApplyUserAuthentication() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "ApplyUserAuthentication"); + } + + public void setApplyUserAuthentication(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "ApplyUserAuthentication", value); + } + + public boolean getApplyAllowedURIs() { + return common.GetJsonObjectValue_Boolean(SIPConfiguration, "ApplyAllowedURIs"); + } + + public void setApplyAllowedURIs(boolean value) { + common.SetJsonObjectValue(SIPConfiguration, "ApplyAllowedURIs", value); + } + + public int getSIPPort() { + return common.GetJsonObjectValue_Integer(SIPConfiguration, "SIPPort"); + } + + public void setSIPPort(int value) { + common.SetJsonObjectValue(SIPConfiguration, "SIPPort", value); + } + + public int getSIPTLSPort() { + return common.GetJsonObjectValue_Integer(SIPConfiguration, "SIPTLSPort"); + } + + public void setSIPTLSPort(int value) { + common.SetJsonObjectValue(SIPConfiguration, "SIPTLSPort", value); + } + + } + + + + } +} diff --git a/src/code/VapixDevice.java b/src/code/VapixDevice.java new file mode 100644 index 0000000..77fd0d6 --- /dev/null +++ b/src/code/VapixDevice.java @@ -0,0 +1,510 @@ +package code; + + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; + +import com.google.gson.JsonObject; + +import anywheresoftware.b4a.BA; + + + +@BA.Events(values= { + "log(msg as string)" +}) + +/** + * General VAPIX Device + * @author rdkartono + * + */ +public class VapixDevice { + protected final CgiClient client; + private final Map parameters; + + protected BA ba; + protected String event; + protected boolean need_log_event = false; + protected Object Me = this; + protected boolean ba_inited = false; + + public void InitializeEvent(BA ba, String event) { + this.ba = ba; + this.event = event; + if (this.ba!=null) { + if (common.ValidString(this.event)) { + ba_inited = true; + need_log_event = ba.subExists(this.event+"_log"); + } + } + } + + /** + * Create Generic Vapix Device that point to another VapixDevice + * @param other Other Vapix Device + */ + protected VapixDevice(VapixDevice other) { + this.client = (other!=null ? other.client : null); + this.parameters = (other!=null ? other.parameters : new HashMap()); + } + + /** + * Create Generic Vapix Device + * @param hostname host name or IP address + * @param port port number + * @param username username to login + * @param password password to login + */ + protected VapixDevice(String hostname, int port, String username, String password) { + client = new CgiClient(hostname, port, username, password); + parameters = new HashMap(); + } + + /** + * Create Generic Vapix Device + * @param hostname host name or IP Address + * @param port port number + * @param username username to login + * @param password password to login + * @param localip local ip address / ethernet to use for connecting the device + */ + protected VapixDevice(String hostname, int port, String username, String password, String localip) { + this(hostname, port, username, password); + client.setUsingLocalIP(localip); + + } + + + + public void Close() { + if (client!=null) { + client.CloseHttpClient(); + } + } + + /** + * Get Serial Number + * @return null if failed + */ + public String getSerialNumber() { + return GetParameterValue("Properties.System.SerialNumber"); + } + + /** + * Get Product Name + * @return null if failed + */ + public String getProductName() { + return GetParameterValue("Brand.ProdShortName"); + } + + /** + * Get Product Type + * @return null if failed + */ + public String getProductType() { + return GetParameterValue("Brand.ProdType"); + } + + /** + * Get IP Address + * @return null if failed + */ + public String getIPAddress() { + return GetParameterValue("Network.eth0.IPAddress"); + } + + /** + * Get Subnet Mask + * @return null if failed + */ + public String getSubnetMask() { + return GetParameterValue("Network.eth0.SubnetMask"); + } + + /** + * Get MAC Address + * @return null if failed + */ + public String getMACAddress() { + return GetParameterValue("Network.eth0.MACAddress"); + } + + /** + * Get Network Hostname recognized in DHCP Router + * @return null if failed + */ + public String getHostName() { + return GetParameterValue("Network.HostName"); + } + + /** + * Get Network Gateway + * @return null if failed + */ + public String getGateway() { + return GetParameterValue("Network.Routing.DefaultRouter"); + } + + /** + * Get Primary DNS (DNS1) + * @return null if failed + */ + public String getDNS1() { + return GetParameterValue("Network.Resolver.NameServer1"); + } + + /** + * Get Secondary DNS (DNS2) + * @return null if faild + */ + public String getDNS2() { + return GetParameterValue("Network.Resolver.NameServer2"); + } + + /** + * Check if this is an Vapix Audio Device + * @return true if correct + */ + public boolean IsAudioDevice() { + return ("yes".equals(GetParameterValue("Properties.Audio.Audio"))); + } + + /** + * Get HTTP Version + * @return string value + */ + public String getHTTPVersion() { + return GetParameterValue("Properties.API.HTTP.Version"); + } + /** + * Get saved parameters with specific key + * Call GetParametersFromDevice to save parameters locally + * @param key parameter title + * @return null if failed + */ + public String GetParameterValue(String key) { + String result = parameters.entrySet().stream() + .filter(e->e.getKey().contains(key)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + return result; + } + + /** + * Replace Parameter Value + * @param key Specific key to replace the value + * @param value new value + * @return previous value replaced, or null if not available + */ + protected String SetParameterValue(String key, String value) { + for(Map.Entry entry : parameters.entrySet()) { + if (entry.getKey().contains(key)) { + return entry.setValue(value); + + } + } + return null; + } + + /** + * Get Array of Parameter Keys + * @return parameter keys + */ + public String[] GetParameterKeys() { + return parameters.keySet().toArray(new String[0]); + } + + /** + * Re-Get parameters from Vapix Device + * @return true if success + */ + public boolean GetParametersFromDevice(){ + List vv = HTTPGet("/axis-cgi/param.cgi","action=list"); + if (common.ValidListOfString(vv)) { + raise_log("GetParameters success"); + vv.forEach(ss -> { + int index = ss.indexOf('='); + if (index!=-1) { + // ada '=' + String key = ss.substring(0, index); + String value = ss.substring(index+1); + String prev_value = parameters.put(key, value); + if (common.ValidString(prev_value)) { + raise_log("key="+key+", old_value="+prev_value+", new_value="+value); + } + } + }); + return true; + } else raise_log("GetParameters failed"); + return false; + } + + /** + * General Purpose HTTP Get + * @param url url path + * @param parameters parameters that follow HTTP Get + * @return List of String if success, or null if failed + */ + protected List HTTPGet(String url, String... parameters){ + List result = null; + CloseableHttpResponse response = client.HttpGet(url,parameters); + if (client.GetStatusCode(response)==200) { + result = client.GetContent(response); + } + client.Close(response); + return result; + } + + /** + * General Purpose HTTP Get + * return String as content, or null if falied + * @param expected_contenttype expected content type, put null if not expecting + * @param url URL + * @param parameters Parameters + * @return null if failed + */ + protected String HTTPGet_GetString(String expected_contenttype, String url, String... parameters) { + CloseableHttpResponse response = client.HttpGet(url, parameters); + int code = client.GetStatusCode(response); + //String message = client.GetStatusMessage(response); + String contenttype = client.GetContentType(response); + String content = common.ConcatListOfString(client.GetContent(response)); + //raise_log(MessageFormat.format("HTTPGet_GetString to {0}, Code={1}, Message={2}, Content Type={3}, Content={4}", url, code, message, contenttype, content)); + client.Close(response); + + + if (code==200) { + if (common.Strings_Valid_and_Match(expected_contenttype, contenttype)){ + return content; + } + } + + return null; + } + + /** + * General Purpose HTTP Get, but specifically want to find a string + * @param findme String to find + * @param url URL path + * @param parameters parameters that follow HTTP Get + * @return null if findme not found + */ + protected String HTTPGet_FindString(String findme, String url, String... parameters) { + List xx = HTTPGet(url, parameters); + if (xx!=null && xx.size()>0) { + return xx.stream() + .filter(ss -> ss.contains(findme)) + .findAny() + .orElse(null); + } + return null; + } + + /** + * General Purpose HTTP Get, but specifically want to find a string + * @param findme String to find + * @param url URL Path + * @param parameters parameters that follow HTTP Get + * @return array of string, contains any string that contains findme + */ + protected String[] HTTPGet_FindStrings(String findme, String url, String... parameters) { + List xx = HTTPGet(url,parameters); + if (xx!=null && xx.size()>0) { + List result = xx.stream() + .filter(ss -> ss.contains(findme)) + .collect(Collectors.toList()); + return result.toArray(new String[0]); + } + return new String[0]; + } + + /** + * General Purpuse HTTP Get, but dont need the result in Body + * Just need status code + * @param url URL Path + * @param parameters parameters that follow HTTP Get + * @return HTTP Get status code + */ + protected int HTTPGet_NoResult(String url, String... parameters) { + CloseableHttpResponse response = client.HttpGet(url,parameters); + int code = client.GetStatusCode(response); + String message = client.GetStatusMessage(response); + client.Close(response); + raise_log("HTTPGet_NoResult to "+url+", Code="+code+", Message="+message); + + return code; + } + + /** + * General Purpose HTTP Get, return bytes array from ContentInputStream + * @param expected_ContentType expected content type + * @param url URL to download + * @param parameters some parameters + * @return byte array if success, or null if failed + */ + protected byte[] HTTPGet_BytesResult(String expected_ContentType, String url, String... parameters) { + CloseableHttpResponse response = client.HttpGet(url, parameters); + int code = client.GetStatusCode(response); + + byte[] result = client.GetContent_BytesArray(response); + client.Close(response); + if (code==200) { + return result; + + } + + return null; + } + + /** + * Download file + * @param savelocation location path + * @param url URL to download + * @param parameters some parameters needed + * @return DownloadResult object, or null if invalid + */ + protected DownloadResult HTTPGet_DownloadFile(String savelocation, String url, String... parameters) { + CloseableHttpResponse response = client.HttpGet(url, parameters); + int code = client.GetStatusCode(response); + if (code==200) { + HttpEntity entity = client.GetResponseEntity(response); + if (entity!=null) { + String filename = client.GetFileName(response); + if (common.ValidString(filename)) { + File ff = new File(savelocation, filename); + try (FileOutputStream fos = new FileOutputStream(ff)) { + entity.writeTo(fos); + return new DownloadResult(savelocation, filename, ff.length()); + } catch (IOException e) { + raise_log("HTTPGet_DownloadFile exception = "+e.getMessage()); + } + } else raise_log("HTTPGet_DownloadFile filename not available"); + } else raise_log("HTTPGet_DownloadFile HttpEntity not available"); + } else raise_log("HTTPGet_DownloadFile code not 200"); + return null; + } + + + public class DownloadResult{ + public String path; + public String filename; + public long size; + + public DownloadResult(String path, String filename, long size) { + this.path = path; + this.filename = filename; + this.size = size; + } + } + + /** + * HTTP Get to specific url and expect some Content Type to be considered success + * @param expected_ContentType content type message to expect + * @param url URL + * @param parameters parameters + * @return true if Code=200 and contain expected_ContentType + */ + protected boolean HTTPGet_BooleanResult(String expected_ContentType, String url, String... parameters) { + CloseableHttpResponse response = client.HttpGet(url, parameters); + int code=client.GetStatusCode(response); + String message = client.GetStatusMessage(response); + String contentype = client.GetContentType(response); + raise_log(MessageFormat.format("HTTPGet_BooleanResult to {0}, Code={1}, Message={2}, ContentType={3}", url, code, message, contentype)); + client.Close(response); + if (code==200) { + if (contentype.contains(expected_ContentType)) { + return true; + } + } + return false; + } + + /** + * HTTP Post a Json Object + * @param url URL to post + * @param data Json Object to post + * @return Json Object result + */ + protected JsonObject HTTPPost(String url, JsonObject data) { + CloseableHttpResponse response = client.HttpPost(url, data); + int code = client.GetStatusCode(response); + JsonObject result = client.GetContentAsJson(response); + client.Close(response); + if (code==200) { + return result; + } + return null; + } + + /** + * HTTP Post a Json Object + * @param url URL to post + * @param data Json Object to post + * @return true if success (code 200) + */ + protected boolean HTTPPost_BooleanResult(String url, JsonObject data) { + CloseableHttpResponse response = client.HttpPost(url, data); + int code = client.GetStatusCode(response); + client.Close(response); + return (code==200) ; + + } + + /** + * Upload file using HTTP Post + * @param filename file to upload + * @param url URL to post + * @return clip ID, or -1 if failed + */ + protected int HTTPPost_UploadFile(String filename, String url, String... parameters) { + CloseableHttpResponse response = client.HttpPost_Filename(filename, url, parameters); + int code = client.GetStatusCode(response); + List content = client.GetContent(response); + client.Close(response); + if (code==200) { + // bisa dua macam status + // p1 kalau file sudah ada, tapi di upload ulang + Pattern p1 = Pattern.compile("updated=MediaClip.M(\\d+)"); + // p2 kalau file belum ada, di upload baru + Pattern p2 = Pattern.compile("uploaded=(\\d+)"); + + for(String ss : content) { + //System.out.println(ss); + Matcher m1 = p1.matcher(ss); + Matcher m2 = p2.matcher(ss); + if (m1.find()) { + //System.out.println("Found clip "+m1.group(1)+" using updated"); + return Integer.valueOf(m1.group(1)); + } else if (m2.find()) { + //System.out.println("Found clip "+m2.group(1)+" using uploaded"); + return Integer.valueOf(m2.group(1)); + } + + } + + } + return -1; + } + + protected void raise_log(String msg) { + if (need_log_event) { + ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg}); + } else System.out.println(msg); + } + + +} diff --git a/src/code/WaveOutputFile.java b/src/code/WaveOutputFile.java new file mode 100644 index 0000000..ec12a81 --- /dev/null +++ b/src/code/WaveOutputFile.java @@ -0,0 +1,151 @@ +package code; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +import javax.sound.sampled.AudioFormat; + +import anywheresoftware.b4a.BA.Hide; + +/** + * Writes sound data into a wav file + * Source : http://thorntonzone.com/manuals/Compression/Fax,%20IBM%20MMR/MMSC/mmsc/uk/co/mmscomputing/sound/ + * @author rdkartono + * + */ +@Hide +public class WaveOutputFile extends RandomAccessFile{ + + byte[] h=new byte[58]; + + public WaveOutputFile(File file,AudioFormat format)throws FileNotFoundException,IOException{ + super(file,"rw"); + write(getHeader(format)); + } + + public WaveOutputFile(String file,AudioFormat format)throws FileNotFoundException,IOException{ + super(file,"rw"); + write(getHeader(format)); + } + + private byte[] getHeader(AudioFormat format){ + /* + riff chunk = [format chunk, fact chunk, data chunk] + We don't know the length of the data chunk yet. + + Need fact chunk for compressed data. + */ + + // start riff chunk + + int i=0; + h[i++]='R'; h[i++]='I'; h[i++]='F'; h[i++]='F'; // 0 : 'RIFF' + h[i++]= 50; h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; // 4 : length of riff chunk [8..(data size+58)] + h[i++]='W'; h[i++]='A'; h[i++]='V'; h[i++]='E'; // 8 : 'WAVE' + + // start format chunk + + h[i++]='f'; h[i++]='m'; h[i++]='t'; h[i++]=' '; // 12: 'fmt ' + h[i++]= 18; h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; // 16 : length of format chunk = 0x12 = 18 [20..38] + + AudioFormat.Encoding encoding=format.getEncoding(); + if(encoding.equals(AudioFormat.Encoding.PCM_SIGNED)){ + h[i++]= 1 ; h[i++]= 0 ; // 20 : PCM = 1 + }else if(encoding.equals(AudioFormat.Encoding.PCM_UNSIGNED)){ + throw new IllegalArgumentException(getClass().getName() + +".getHeader(AudioFormat format)\n\tDo not support PCM unsigned data" + ); + }else if(encoding.equals(AudioFormat.Encoding.ALAW)){ + h[i++]= 6 ; h[i++]= 0 ; // 20 : A-Law = 6 + }else if(encoding.equals(AudioFormat.Encoding.ULAW)){ + h[i++]= 7 ; h[i++]= 0 ; // 20 : u-Law = 7 + }else{ + throw new IllegalArgumentException(getClass().getName() + +".getHeader(AudioFormat format)\n\tDo not support encoding ["+encoding+"]" + ); + } + int channels=format.getChannels(); + if(channels==1){ + h[i++]= 1 ; h[i++]= 0 ; // 22 : mono = 1 + }else if(channels==2){ + h[i++]= 2 ; h[i++]= 0 ; // 22 : stereo = 2 + }else{ + throw new IllegalArgumentException(getClass().getName() + +".getHeader(AudioFormat format)\n\tDo not support "+channels+" channels" + ); + } + int sr = (int)format.getFrameRate(); + h[i++]=(byte)( sr & 0x000000FF); // 24 : sample rate + h[i++]=(byte)((sr>>8) & 0x000000FF); + h[i++]=(byte)((sr>>16) & 0x000000FF); + h[i++]=(byte)((sr>>24) & 0x000000FF); + + int ss = format.getFrameSize(); + int bs = sr * ss; + h[i++]=(byte)( bs & 0x000000FF); // 28 : bytes per second + h[i++]=(byte)((bs>>8) & 0x000000FF); + h[i++]=(byte)((bs>>16) & 0x000000FF); + h[i++]=(byte)((bs>>24) & 0x000000FF); + + h[i++]=(byte)( ss & 0x000000FF); // 32 : bytes per sample + h[i++]=(byte)((ss>>8) & 0x000000FF); + + int ssib = format.getSampleSizeInBits(); + h[i++]=(byte)( ssib & 0x000000FF); // 34 : bits per sample + h[i++]=(byte)((ssib>>8)& 0x000000FF); + + h[i++]= 0 ; h[i++]= 0 ; // 36 : number of bytes of additional compressor-specific information + + // end format chunk // 38 + // start fact chunk // 38 + + h[i++]='f'; h[i++]='a'; h[i++]='c'; h[i++]='t'; // 38: 'fact' + h[i++]= 4 ; h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; // 42 : length of fact chunk = 0x04 = 4 [46..50] + h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; // 46 : data size + + // end fact chunk // 50 + // start data chunk // 50 + + h[i++]='d'; h[i++]='a'; h[i++]='t'; h[i++]='a'; // 50: 'data' + h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; h[i++]= 0 ; // 54 : length of data chunk [58..(data size+58)] + + // data size bytes + + // end data chunk // data size + 58 + // end riff chunk // data size + 58 + return h; + } + + public void close()throws IOException{ + long size = length(); + if(size>=0x7FFFFFFFl){ + throw new IOException(getClass().getName()+".close()\n\tData size ["+size+"] is too big for WAV file."); + } + int i=4; + int len=(int)size-8; + h[i++]=(byte)( len & 0x000000FF); // 4 : length of riff chunk [8..(data size+58)] + h[i++]=(byte)((len>>8) & 0x000000FF); + h[i++]=(byte)((len>>16) & 0x000000FF); + h[i++]=(byte)((len>>24) & 0x000000FF); + + i=46; + len=(int)size-58; // data size + h[i++]=(byte)( len & 0x000000FF); // 46 : 'fact' data size + h[i++]=(byte)((len>>8) & 0x000000FF); + h[i++]=(byte)((len>>16) & 0x000000FF); + h[i++]=(byte)((len>>24) & 0x000000FF); + + i=54; + h[i++]=(byte)( len & 0x000000FF); // 54 : length of data chunk [58..(data size+58)] + h[i++]=(byte)((len>>8) & 0x000000FF); + h[i++]=(byte)((len>>16) & 0x000000FF); + h[i++]=(byte)((len>>24) & 0x000000FF); + + seek(0); + write(h); // write header at beginning of file + + super.close(); + } +} \ No newline at end of file diff --git a/src/code/common.java b/src/code/common.java new file mode 100644 index 0000000..5d8b904 --- /dev/null +++ b/src/code/common.java @@ -0,0 +1,560 @@ +package code; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.text.MessageFormat; +import java.util.List; +import java.util.stream.Stream; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.sun.jna.Native; + +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; + +public class common { + + /** + * Check if value is a valid String + * Not Null, and length at least 1 character + * @param vv String value + * @return true if valid + */ + public static boolean ValidString(String vv) { + if (vv != null) { + if (vv.length()>0) { + return true; + } + } + return false; + } + + /** + * Check if value is a valid Port number + * @param vv between 1 ~ 65535 + * @return true if valid + */ + public static boolean ValidPortNumber(int vv) { + if (vv>0) { + if (vv<65536) { + return true; + } + } + return false; + } + + /** + * Check if valus is a valid array of string + * array is not null, array length at least 1 + * and all members of array is valid string + * @param vv array of string + * @return true if valid + */ + public static boolean ValidArrayOfString(String[] vv) { + if (vv!=null) { + if (vv.length>0) { + return Stream.of(vv).allMatch(ss-> ValidString(ss)); + } + } + return false; + } + + public static boolean ValidArrayOfBytes(byte[] vv) { + if (vv!=null) { + if (vv.length>0) { + return true; + } + } + return false; + } + + public static boolean ValidListOfString(List vv) { + if (vv!=null) { + if (vv.size()>0) { + return true; + } + } + return false; + } + + public static String getClassValues(Class cc, Object obj) { + Field[] fields = cc.getFields(); + + if (fields!=null && fields.length>0) { + int length = fields.length; + String[] fs = new String[length]; + for(int i=0;i. + + Copyright 2012 Yohann Martineau +*/ + +package net.sourceforge.peers; + +import java.net.InetAddress; + +import net.sourceforge.peers.media.MediaMode; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; + +public class JavaConfig implements Config { + + private InetAddress localInetAddress; + private InetAddress publicInetAddress; + private String userPart; + private String domain; + private String password; + private SipURI outboundProxy; + private int sipPort; + private MediaMode mediaMode; + private boolean mediaDebug; + private String mediaFile; + private int rtpPort; + + @Override + public void save() { + throw new RuntimeException("not implemented"); + } + + @Override + public InetAddress getLocalInetAddress() { + return localInetAddress; + } + + @Override + public InetAddress getPublicInetAddress() { + return publicInetAddress; + } + + @Override + public String getUserPart() { + return userPart; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public SipURI getOutboundProxy() { + return outboundProxy; + } + + @Override + public int getSipPort() { + return sipPort; + } + + @Override + public MediaMode getMediaMode() { + return mediaMode; + } + + @Override + public boolean isMediaDebug() { + return mediaDebug; + } + + @Override + public int getRtpPort() { + return rtpPort; + } + + @Override + public void setLocalInetAddress(InetAddress inetAddress) { + localInetAddress = inetAddress; + } + + @Override + public void setPublicInetAddress(InetAddress inetAddress) { + publicInetAddress = inetAddress; + } + + @Override + public void setUserPart(String userPart) { + this.userPart = userPart; + } + + @Override + public void setDomain(String domain) { + this.domain = domain; + } + + @Override + public void setPassword(String password) { + this.password = password; + } + + @Override + public void setOutboundProxy(SipURI outboundProxy) { + this.outboundProxy = outboundProxy; + } + + @Override + public void setSipPort(int sipPort) { + this.sipPort = sipPort; + } + + @Override + public void setMediaMode(MediaMode mediaMode) { + this.mediaMode = mediaMode; + } + + @Override + public void setMediaDebug(boolean mediaDebug) { + this.mediaDebug = mediaDebug; + } + + @Override + public void setRtpPort(int rtpPort) { + this.rtpPort = rtpPort; + } + + @Override + public String getMediaFile() { + return mediaFile; + } + + @Override + public void setMediaFile(String mediaFile) { + this.mediaFile = mediaFile; + } + +} diff --git a/src/net/sourceforge/peers/Logger.java b/src/net/sourceforge/peers/Logger.java new file mode 100644 index 0000000..6e864ac --- /dev/null +++ b/src/net/sourceforge/peers/Logger.java @@ -0,0 +1,30 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2013 Yohann Martineau +*/ + +package net.sourceforge.peers; + +public interface Logger { + + public void debug(String message); + public void info(String message); + public void error(String message); + public void error(String message, Exception exception); + public void traceNetwork(String message, String direction); + +} \ No newline at end of file diff --git a/src/net/sourceforge/peers/XmlConfig.java b/src/net/sourceforge/peers/XmlConfig.java new file mode 100644 index 0000000..c724907 --- /dev/null +++ b/src/net/sourceforge/peers/XmlConfig.java @@ -0,0 +1,361 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010-2013 Yohann Martineau +*/ + +package net.sourceforge.peers; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import net.sourceforge.peers.media.MediaMode; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + + +public class XmlConfig implements Config { + + public final static int RTP_DEFAULT_PORT = 8000; + + private Logger logger; + + private File file; + private Document document; + + // persistent variables + + private InetAddress localInetAddress; + private String userPart; + private String domain; + private String password; + private SipURI outboundProxy; + private int sipPort; + private MediaMode mediaMode; + private boolean mediaDebug; + private String mediaFile; + private int rtpPort; + + // corresponding DOM nodes + + private Node ipAddressNode; + private Node userPartNode; + private Node domainNode; + private Node passwordNode; + private Node outboundProxyNode; + private Node sipPortNode; + private Node mediaModeNode; + private Node mediaDebugNode; + private Node mediaFileNode; + private Node rtpPortNode; + + // non-persistent variables + + private InetAddress publicInetAddress; + + //private InetAddress + public XmlConfig(String fileName, Logger logger) { + file = new File(fileName); + this.logger = logger; + if (!file.exists()) { + if (logger!=null) logger.debug("config file " + fileName + " not found"); + return; + } + DocumentBuilderFactory documentBuilderFactory = + DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + if (logger!=null) logger.error("parser configuration exception", e); + return; + } + try { + document = documentBuilder.parse(file); + } catch (SAXException e) { + if (logger!=null) logger.error("cannot parse " + fileName,e ); + return; + } catch (IOException e) { + if (logger!=null) logger.error("IOException", e); + return; + } + Element documentElement = document.getDocumentElement(); + ipAddressNode = getFirstChild(documentElement, "ipAddress"); + String address = ipAddressNode.getTextContent(); + try { + if (isNullOrEmpty(ipAddressNode)) { + localInetAddress = InetAddress.getLocalHost(); + } else { + localInetAddress = InetAddress.getByName(address); + } + } catch (UnknownHostException e) { + if (logger!=null) logger.error("unknown host: " + address, e); + } + userPartNode = getFirstChild(documentElement, "userPart"); + if (isNullOrEmpty(userPartNode)) { + if (logger!=null) logger.error("userpart not found in configuration file"); + } else { + userPart = userPartNode.getTextContent(); + } + domainNode = getFirstChild(documentElement, "domain"); + if (isNullOrEmpty(domainNode)) { + if (logger!=null) logger.error("domain not found in configuration file"); + } else { + domain = domainNode.getTextContent(); + } + passwordNode = getFirstChild(documentElement, "password"); + if (!isNullOrEmpty(passwordNode)) { + password = passwordNode.getTextContent(); + } + outboundProxyNode = getFirstChild(documentElement, "outboundProxy"); + if (!isNullOrEmpty(outboundProxyNode)) { + String uri = outboundProxyNode.getTextContent(); + try { + outboundProxy = new SipURI(uri); + } catch (SipUriSyntaxException e) { + if (logger!=null) logger.error("sip uri syntax exception: " + uri, e); + } + } + sipPortNode = getFirstChild(documentElement, "sipPort"); + if (isNullOrEmpty(sipPortNode)) { + sipPort = RFC3261.TRANSPORT_DEFAULT_PORT; + } else { + sipPort = Integer.parseInt(sipPortNode.getTextContent()); + } + mediaModeNode = getFirstChild(documentElement, "mediaMode"); + if (isNullOrEmpty(mediaModeNode)) { + mediaMode = MediaMode.captureAndPlayback; + } else { + mediaMode = MediaMode.valueOf(mediaModeNode.getTextContent()); + } + mediaDebugNode = getFirstChild(documentElement, "mediaDebug"); + if (isNullOrEmpty(mediaDebugNode)) { + mediaDebug = false; + } else { + mediaDebug = Boolean.parseBoolean(mediaDebugNode.getTextContent()); + } + mediaFileNode = getFirstChild(documentElement, "mediaFile"); + if (!isNullOrEmpty(mediaFileNode)) { + mediaFile = mediaFileNode.getTextContent(); + } + if (mediaMode == MediaMode.file) { + if (mediaFile == null || "".equals(mediaFile.trim())) { + if (logger!=null) logger.error("streaming from file but no file provided"); + } + } + rtpPortNode = getFirstChild(documentElement, "rtpPort"); + if (isNullOrEmpty(rtpPortNode)) { + rtpPort = RTP_DEFAULT_PORT; + } else { + rtpPort = Integer.parseInt(rtpPortNode.getTextContent()); + if (rtpPort % 2 != 0) { + if (logger!=null) logger.error("rtp port provided is " + rtpPort + + " rtp port must be even"); + } + } + } + + private boolean isNullOrEmpty(Node node) { + return node == null || "".equals(node.getTextContent().trim()); + } + + private Node getFirstChild(Node parent, String childName) { + if (parent == null || childName == null) { + return null; + } + NodeList nodeList = parent.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); ++i) { + Node node = nodeList.item(i); + if (childName.equals(node.getNodeName())) { + return node; + } + } + return null; + } + + @Override + public void save() { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer; + try { + transformer = transformerFactory.newTransformer(); + } catch (TransformerConfigurationException e) { + if (logger!=null) logger.error("cannot create transformer", e); + return; + } + FileWriter fileWriter; + try { + fileWriter = new FileWriter(file); + } catch (IOException e) { + if (logger!=null) logger.error("cannot create file writer", e); + return; + } + StreamResult streamResult = new StreamResult(fileWriter); + DOMSource domSource = new DOMSource(document); + try { + transformer.transform(domSource, streamResult); + } catch (TransformerException e) { + if (logger!=null) logger.error("cannot save config file", e); + return; + } + if (logger!=null) logger.debug("config file saved"); + } + + @Override + public InetAddress getLocalInetAddress() { + return localInetAddress; + } + + @Override + public InetAddress getPublicInetAddress() { + return publicInetAddress; + } + + @Override + public String getUserPart() { + return userPart; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public SipURI getOutboundProxy() { + return outboundProxy; + } + + @Override + public int getSipPort() { + return sipPort; + } + + @Override + public MediaMode getMediaMode() { + return mediaMode; + } + + @Override + public boolean isMediaDebug() { + return mediaDebug; + } + + @Override + public int getRtpPort() { + return rtpPort; + } + + @Override + public void setLocalInetAddress(InetAddress inetAddress) { + this.localInetAddress = inetAddress; + ipAddressNode.setTextContent(inetAddress.getHostAddress()); + } + + @Override + public void setPublicInetAddress(InetAddress inetAddress) { + this.publicInetAddress = inetAddress; + } + + @Override + public void setUserPart(String userPart) { + this.userPart = userPart; + userPartNode.setTextContent(userPart); + } + + @Override + public void setDomain(String domain) { + this.domain = domain; + domainNode.setTextContent(domain); + } + + @Override + public void setPassword(String password) { + this.password = password; + passwordNode.setTextContent(password); + } + + @Override + public void setOutboundProxy(SipURI outboundProxy) { + this.outboundProxy = outboundProxy; + if (outboundProxy == null) { + outboundProxyNode.setTextContent(""); + } else { + outboundProxyNode.setTextContent(outboundProxy.toString()); + } + + } + + @Override + public void setSipPort(int sipPort) { + this.sipPort = sipPort; + sipPortNode.setTextContent(Integer.toString(sipPort)); + } + + @Override + public void setMediaMode(MediaMode mediaMode) { + this.mediaMode = mediaMode; + mediaModeNode.setTextContent(mediaMode.toString()); + } + + @Override + public void setMediaDebug(boolean mediaDebug) { + this.mediaDebug = mediaDebug; + mediaDebugNode.setTextContent(Boolean.toString(mediaDebug)); + } + + @Override + public void setRtpPort(int rtpPort) { + this.rtpPort = rtpPort; + rtpPortNode.setTextContent(Integer.toString(rtpPort)); + } + + @Override + public String getMediaFile() { + return mediaFile; + } + + @Override + public void setMediaFile(String mediaFile) { + this.mediaFile = mediaFile; + } + +} diff --git a/src/net/sourceforge/peers/javaxsound/JavaxSoundManager.java b/src/net/sourceforge/peers/javaxsound/JavaxSoundManager.java new file mode 100644 index 0000000..0f8670f --- /dev/null +++ b/src/net/sourceforge/peers/javaxsound/JavaxSoundManager.java @@ -0,0 +1,214 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010, 2011, 2012 Yohann Martineau +*/ + +package net.sourceforge.peers.javaxsound; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.TargetDataLine; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.media.AbstractSoundManager; +import net.sourceforge.peers.sip.Utils; + +public class JavaxSoundManager extends AbstractSoundManager { + + private AudioFormat audioFormat; + private TargetDataLine targetDataLine; + private SourceDataLine sourceDataLine; + private DataLine.Info targetInfo; + private DataLine.Info sourceInfo; + private FileOutputStream microphoneOutput; + private FileOutputStream speakerInput; + private boolean mediaDebug; + private Logger logger; + private String peersHome; + + public JavaxSoundManager(boolean mediaDebug, Logger logger, String peersHome) { + this.mediaDebug = mediaDebug; + this.logger = logger; + this.peersHome = peersHome; + if (peersHome == null) { + this.peersHome = Utils.DEFAULT_PEERS_HOME; + } + // linear PCM 8kHz, 16 bits signed, mono-channel, little endian + audioFormat = new AudioFormat(8000, 16, 1, true, false); + targetInfo = new DataLine.Info(TargetDataLine.class, audioFormat); + sourceInfo = new DataLine.Info(SourceDataLine.class, audioFormat); + + } + + @Override + public void init() { + if (logger!=null) logger.debug("openAndStartLines"); + if (mediaDebug) { + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + String date = simpleDateFormat.format(new Date()); + StringBuffer buf = new StringBuffer(); + buf.append(peersHome).append(File.separator); + buf.append(MEDIA_DIR).append(File.separator); + buf.append(date).append("_"); + buf.append(audioFormat.getEncoding()).append("_"); + buf.append(audioFormat.getSampleRate()).append("_"); + buf.append(audioFormat.getSampleSizeInBits()).append("_"); + buf.append(audioFormat.getChannels()).append("_"); + buf.append(audioFormat.isBigEndian() ? "be" : "le"); + try { + microphoneOutput = new FileOutputStream(buf.toString() + + "_microphone.output"); + speakerInput = new FileOutputStream(buf.toString() + + "_speaker.input"); + } catch (FileNotFoundException e) { + if (logger!=null) if (logger!=null) logger.error("cannot create file", e); + return; + } + } + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public Void run() { + try { + targetDataLine = (TargetDataLine) AudioSystem.getLine(targetInfo); + targetDataLine.open(audioFormat); + + } catch (LineUnavailableException e) { + if (logger!=null) if (logger!=null) logger.error("target line unavailable", e); + return null; + } catch (SecurityException e) { + if (logger!=null) if (logger!=null) logger.error("security exception", e); + return null; + } catch (Throwable t) { + if (logger!=null) if (logger!=null) logger.error("throwable " + t.getMessage()); + return null; + } + targetDataLine.start(); + try { + sourceDataLine = (SourceDataLine) AudioSystem.getLine(sourceInfo); + sourceDataLine.open(audioFormat); + + } catch (LineUnavailableException e) { + if (logger!=null) if (logger!=null) logger.error("source line unavailable", e); + return null; + } + sourceDataLine.start(); + return null; + } + }); + + } + + @Override + public synchronized void close() { + if (logger!=null) logger.debug("closeLines"); + if (microphoneOutput != null) { + try { + microphoneOutput.close(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot close file", e); + } + microphoneOutput = null; + } + if (speakerInput != null) { + try { + speakerInput.close(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot close file", e); + } + speakerInput = null; + } + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged(new PrivilegedAction() { + + @Override + public Void run() { + if (targetDataLine != null) { + targetDataLine.close(); + targetDataLine = null; + } + if (sourceDataLine != null) { + sourceDataLine.drain(); + sourceDataLine.stop(); + sourceDataLine.close(); + sourceDataLine = null; + } + return null; + } + }); + } + + @Override + public synchronized byte[] readData() { + if (targetDataLine == null) { + return null; + } + int ready = targetDataLine.available(); + while (ready == 0) { + try { + Thread.sleep(2); + ready = targetDataLine.available(); + } catch (InterruptedException e) { + return null; + } + } + if (ready <= 0) { + return null; + } + byte[] buffer = new byte[ready]; + targetDataLine.read(buffer, 0, buffer.length); + if (mediaDebug) { + try { + microphoneOutput.write(buffer, 0, buffer.length); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot write to file", e); + return null; + } + } + return buffer; + } + + @Override + public int writeData(byte[] buffer, int offset, int length) { + int numberOfBytesWritten = sourceDataLine.write(buffer, offset, length); + if (mediaDebug) { + try { + speakerInput.write(buffer, offset, numberOfBytesWritten); + } catch (IOException e) { + if (logger!=null) logger.error("cannot write to file", e); + return -1; + } + } + return numberOfBytesWritten; + } + +} diff --git a/src/net/sourceforge/peers/media/AbstractSoundManager.java b/src/net/sourceforge/peers/media/AbstractSoundManager.java new file mode 100644 index 0000000..25a0932 --- /dev/null +++ b/src/net/sourceforge/peers/media/AbstractSoundManager.java @@ -0,0 +1,28 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2013 Yohann Martineau +*/ +package net.sourceforge.peers.media; + +public abstract class AbstractSoundManager implements SoundSource { + + public final static String MEDIA_DIR = "media"; + + public abstract void init(); + public abstract void close(); + public abstract int writeData(byte[] buffer, int offset, int length); +} diff --git a/src/net/sourceforge/peers/media/Capture.java b/src/net/sourceforge/peers/media/Capture.java new file mode 100644 index 0000000..be5ce62 --- /dev/null +++ b/src/net/sourceforge/peers/media/Capture.java @@ -0,0 +1,79 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.IOException; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + +import net.sourceforge.peers.Logger; + + +public class Capture implements Runnable { + + public static final int SAMPLE_SIZE = 16; + public static final int BUFFER_SIZE = SAMPLE_SIZE * 20; + + private PipedOutputStream rawData; + private boolean isStopped; + private SoundSource soundSource; + private Logger logger; + private CountDownLatch latch; + + public Capture(PipedOutputStream rawData, SoundSource soundSource, + Logger logger, CountDownLatch latch) { + this.rawData = rawData; + this.soundSource = soundSource; + this.logger = logger; + this.latch = latch; + isStopped = false; + } + + public void run() { + byte[] buffer; + + while (!isStopped) { + buffer = soundSource.readData(); + try { + if (buffer == null) { + break; + } + rawData.write(buffer); + rawData.flush(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + } + latch.countDown(); + if (latch.getCount() != 0) { + try { + latch.await(); + } catch (InterruptedException e) { + if (logger!=null) if (logger!=null) logger.error("interrupt exception", e); + } + } + } + + public synchronized void setStopped(boolean isStopped) { + this.isStopped = isStopped; + } + +} diff --git a/src/net/sourceforge/peers/media/CaptureRtpSender.java b/src/net/sourceforge/peers/media/CaptureRtpSender.java new file mode 100644 index 0000000..af9bcb6 --- /dev/null +++ b/src/net/sourceforge/peers/media/CaptureRtpSender.java @@ -0,0 +1,129 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.rtp.RFC3551; +import net.sourceforge.peers.rtp.RtpSession; +import net.sourceforge.peers.sdp.Codec; + + + +public class CaptureRtpSender { + + public static final int PIPE_SIZE = 4096; + + private RtpSession rtpSession; + private Capture capture; + private Encoder encoder; + private RtpSender rtpSender; + + public CaptureRtpSender(RtpSession rtpSession, SoundSource soundSource, + boolean mediaDebug, Codec codec, Logger logger, String peersHome) + throws IOException { + super(); + this.rtpSession = rtpSession; + // the use of PipedInputStream and PipedOutputStream in Capture, + // Encoder and RtpSender imposes a synchronization point at the + // end of life of those threads to a void read end dead exceptions + CountDownLatch latch = new CountDownLatch(3); + PipedOutputStream rawDataOutput = new PipedOutputStream(); + PipedInputStream rawDataInput; + try { + rawDataInput = new PipedInputStream(rawDataOutput, PIPE_SIZE); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + + PipedOutputStream encodedDataOutput = new PipedOutputStream(); + PipedInputStream encodedDataInput; + try { + encodedDataInput = new PipedInputStream(encodedDataOutput, + PIPE_SIZE); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error"); + rawDataInput.close(); + return; + } + capture = new Capture(rawDataOutput, soundSource, logger, latch); + switch (codec.getPayloadType()) { + case RFC3551.PAYLOAD_TYPE_PCMU: + encoder = new PcmuEncoder(rawDataInput, encodedDataOutput, + mediaDebug, logger, peersHome, latch); + break; + case RFC3551.PAYLOAD_TYPE_PCMA: + encoder = new PcmaEncoder(rawDataInput, encodedDataOutput, + mediaDebug, logger, peersHome, latch); + break; + default: + encodedDataInput.close(); + rawDataInput.close(); + throw new RuntimeException("unknown payload type"); + } + rtpSender = new RtpSender(encodedDataInput, rtpSession, mediaDebug, + codec, logger, peersHome, latch); + } + + public void start() throws IOException { + + capture.setStopped(false); + encoder.setStopped(false); + rtpSender.setStopped(false); + + Thread captureThread = new Thread(capture, + Capture.class.getSimpleName()); + Thread encoderThread = new Thread(encoder, + Encoder.class.getSimpleName()); + Thread rtpSenderThread = new Thread(rtpSender, + RtpSender.class.getSimpleName()); + + captureThread.start(); + encoderThread.start(); + rtpSenderThread.start(); + + } + + public void stop() { + if (capture != null) { + capture.setStopped(true); + } + if (encoder != null) { + encoder.setStopped(true); + } + if (rtpSender != null) { + rtpSender.setStopped(true); + } + } + + public synchronized RtpSession getRtpSession() { + return rtpSession; + } + + public RtpSender getRtpSender() { + return rtpSender; + } + +} diff --git a/src/net/sourceforge/peers/media/Decoder.java b/src/net/sourceforge/peers/media/Decoder.java new file mode 100644 index 0000000..7792f0e --- /dev/null +++ b/src/net/sourceforge/peers/media/Decoder.java @@ -0,0 +1,26 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +public abstract class Decoder { + + public abstract byte[] process(byte[] media); + +} diff --git a/src/net/sourceforge/peers/media/DtmfFactory.java b/src/net/sourceforge/peers/media/DtmfFactory.java new file mode 100644 index 0000000..394d781 --- /dev/null +++ b/src/net/sourceforge/peers/media/DtmfFactory.java @@ -0,0 +1,95 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.util.ArrayList; +import java.util.List; + +import net.sourceforge.peers.rtp.RFC4733; +import net.sourceforge.peers.rtp.RtpPacket; + +public class DtmfFactory { + + public List createDtmfPackets(char digit) { + List packets = new ArrayList(); + byte[] data = new byte[4]; + // RFC4733 + if (digit == '*') { + data[0] = 10; + } else if (digit == '#') { + data[0] = 11; + } else if (digit >= 'A' && digit <= 'D') { + data[0] = (byte) (digit - 53); + } else { + data[0] = (byte) (digit - 48); + } + data[1] = 10; // volume 10 + // Set Duration to 160 + // duration 8 bits + data[2] = 0; + // duration 8 bits + data[3] = -96; + + RtpPacket rtpPacket = new RtpPacket(); + rtpPacket.setData(data); + rtpPacket.setPayloadType(RFC4733.PAYLOAD_TYPE_TELEPHONE_EVENT); + rtpPacket.setMarker(true); + packets.add(rtpPacket); + + // two classical packets + + rtpPacket = new RtpPacket(); + // set duration to 320 + data = data.clone(); + data[2] = 1; + data[3] = 64; + rtpPacket.setData(data); + rtpPacket.setMarker(false); + rtpPacket.setPayloadType(RFC4733.PAYLOAD_TYPE_TELEPHONE_EVENT); + packets.add(rtpPacket); + + rtpPacket = new RtpPacket(); + // set duration to 320 + data = data.clone(); + data[2] = 1; + data[3] = -32; + rtpPacket.setData(data); + rtpPacket.setMarker(false); + rtpPacket.setPayloadType(RFC4733.PAYLOAD_TYPE_TELEPHONE_EVENT); + packets.add(rtpPacket); + + data = data.clone(); + // create three end event packets + data[1] = -0x76; // end event flag + volume set to 10 + // set Duration to 640 + data[2] = 2; // duration 8 bits + data[3] = -128; // duration 8 bits + for (int r = 0; r < 3; r++) { + rtpPacket = new RtpPacket(); + rtpPacket.setData(data); + rtpPacket.setMarker(false); + rtpPacket.setPayloadType(RFC4733.PAYLOAD_TYPE_TELEPHONE_EVENT); + packets.add(rtpPacket); + } + + return packets; + } + +} diff --git a/src/net/sourceforge/peers/media/Echo.java b/src/net/sourceforge/peers/media/Echo.java new file mode 100644 index 0000000..de11596 --- /dev/null +++ b/src/net/sourceforge/peers/media/Echo.java @@ -0,0 +1,79 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2009, 2010, 2012 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +import net.sourceforge.peers.Logger; + +public class Echo implements Runnable { + + public static final int BUFFER_SIZE = 2048; + + private DatagramSocket datagramSocket; + private InetAddress remoteAddress; + private int remotePort; + private boolean isRunning; + private Logger logger; + + public Echo(DatagramSocket datagramSocket, + String remoteAddress, int remotePort, Logger logger) + throws UnknownHostException { + this.datagramSocket = datagramSocket; + this.remoteAddress = InetAddress.getByName(remoteAddress); + this.remotePort = remotePort; + this.logger = logger; + isRunning = true; + } + + @Override + public void run() { + try { + while (isRunning) { + byte[] buf = new byte[BUFFER_SIZE]; + DatagramPacket datagramPacket = new DatagramPacket(buf, + buf.length); + try { + datagramSocket.receive(datagramPacket); + } catch (SocketTimeoutException e) { + if (logger!=null) logger.debug("echo socket timeout"); + continue; + } + datagramPacket = new DatagramPacket(buf, + datagramPacket.getLength(), remoteAddress, remotePort); + datagramSocket.send(datagramPacket); + } + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + } finally { + datagramSocket.close(); + } + + } + + public synchronized void stop() { + isRunning = false; + } +} diff --git a/src/net/sourceforge/peers/media/Encoder.java b/src/net/sourceforge/peers/media/Encoder.java new file mode 100644 index 0000000..9c437c0 --- /dev/null +++ b/src/net/sourceforge/peers/media/Encoder.java @@ -0,0 +1,148 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.CountDownLatch; + +import net.sourceforge.peers.Logger; + + +public abstract class Encoder implements Runnable { + + private PipedInputStream rawData; + private PipedOutputStream encodedData; + private boolean isStopped; + private FileOutputStream encoderOutput; + private FileOutputStream encoderInput; + private boolean mediaDebug; + private Logger logger; + private String peersHome; + private CountDownLatch latch; + + public Encoder(PipedInputStream rawData, PipedOutputStream encodedData, + boolean mediaDebug, Logger logger, String peersHome, + CountDownLatch latch) { + this.rawData = rawData; + this.encodedData = encodedData; + this.mediaDebug = mediaDebug; + this.logger = logger; + this.peersHome = peersHome; + this.latch = latch; + isStopped = false; + } + + public void run() { + byte[] buffer; + if (mediaDebug) { + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + String date = simpleDateFormat.format(new Date()); + String dir = peersHome + File.separator + + AbstractSoundManager.MEDIA_DIR + File.separator; + String fileName = dir + date + "_g711_encoder.output"; + try { + encoderOutput = new FileOutputStream(fileName); + fileName = dir + date + "_g711_encoder.input"; + encoderInput = new FileOutputStream(fileName); + } catch (FileNotFoundException e) { + if (logger!=null) if (logger!=null) logger.error("cannot create file", e); + return; + } + } + int ready; + while (!isStopped) { + try { + ready = rawData.available(); + while (ready == 0 && !isStopped) { + try { + Thread.sleep(2); + ready = rawData.available(); + } catch (InterruptedException e) { + if (logger!=null) if (logger!=null) logger.error("interrupt exception", e); + } + } + if (isStopped) { + break; + } + buffer = new byte[ready]; + rawData.read(buffer); + if (mediaDebug) { + try { + encoderInput.write(buffer); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot write to file", e); + } + } + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + + byte[] ulawData = process(buffer); + if (mediaDebug) { + try { + encoderOutput.write(ulawData); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot write to file", e); + break; + } + } + try { + encodedData.write(ulawData); + encodedData.flush(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + } + if (mediaDebug) { + try { + encoderOutput.close(); + encoderInput.close(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot close file", e); + return; + } + } + latch.countDown(); + if (latch.getCount() != 0) { + try { + latch.await(); + } catch (InterruptedException e) { + if (logger!=null) if (logger!=null) logger.error("interrupt exception", e); + } + } + } + + public synchronized void setStopped(boolean isStopped) { + this.isStopped = isStopped; + } + + public abstract byte[] process(byte[] media); + +} diff --git a/src/net/sourceforge/peers/media/FileReader.java b/src/net/sourceforge/peers/media/FileReader.java new file mode 100644 index 0000000..acc18a7 --- /dev/null +++ b/src/net/sourceforge/peers/media/FileReader.java @@ -0,0 +1,96 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2012 Yohann Martineau +*/ +package net.sourceforge.peers.media; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import net.sourceforge.peers.Logger; + +// To create an audio file for peers, you can use audacity: +// +// Edit > Preferences +// +// - Peripherals +// - Channels: 1 (Mono) +// - Quality +// - Sampling frequency of default: 8000 Hz +// - Default Sample Format: 16 bits +// +// Validate +// +// Record audio +// +// File > Export +// +// - File name: test.raw +// +// Validate + +public class FileReader implements SoundSource { + + public final static int BUFFER_SIZE = 256; + + private FileInputStream fileInputStream; + private Logger logger; + + public FileReader(String fileName, Logger logger) { + this.logger = logger; + try { + fileInputStream = new FileInputStream(fileName); + } catch (FileNotFoundException e) { + if (logger!=null) if (logger!=null) logger.error("file not found: " + fileName, e); + } + } + + public synchronized void close() { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("io exception", e); + } + fileInputStream = null; + } + } + + @Override + public synchronized byte[] readData() { + if (fileInputStream == null) { + return null; + } + byte buffer[] = new byte[BUFFER_SIZE]; + try { + if (fileInputStream.read(buffer) >= 0) { + Thread.sleep(15); + return buffer; + } else { + fileInputStream.close(); + fileInputStream = null; + } + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("io exception", e); + } catch (InterruptedException e) { + if (logger!=null) logger.debug("file reader interrupted"); + } + return null; + } + +} diff --git a/src/net/sourceforge/peers/media/IncomingRtpReader.java b/src/net/sourceforge/peers/media/IncomingRtpReader.java new file mode 100644 index 0000000..f14ad76 --- /dev/null +++ b/src/net/sourceforge/peers/media/IncomingRtpReader.java @@ -0,0 +1,68 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.IOException; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.rtp.RFC3551; +import net.sourceforge.peers.rtp.RtpListener; +import net.sourceforge.peers.rtp.RtpPacket; +import net.sourceforge.peers.rtp.RtpSession; +import net.sourceforge.peers.sdp.Codec; + +public class IncomingRtpReader implements RtpListener { + + private RtpSession rtpSession; + private AbstractSoundManager soundManager; + private Decoder decoder; + + public IncomingRtpReader(RtpSession rtpSession, + AbstractSoundManager soundManager, Codec codec, Logger logger) + throws IOException { + if (logger!=null) logger.debug("playback codec:" + codec.toString().trim()); + this.rtpSession = rtpSession; + this.soundManager = soundManager; + switch (codec.getPayloadType()) { + case RFC3551.PAYLOAD_TYPE_PCMU: + decoder = new PcmuDecoder(); + break; + case RFC3551.PAYLOAD_TYPE_PCMA: + decoder = new PcmaDecoder(); + break; + default: + throw new RuntimeException("unsupported payload type"); + } + rtpSession.addRtpListener(this); + } + + public void start() { + rtpSession.start(); + } + + @Override + public void receivedRtpPacket(RtpPacket rtpPacket) { + byte[] rawBuf = decoder.process(rtpPacket.getData()); + if (soundManager != null) { + soundManager.writeData(rawBuf, 0, rawBuf.length); + } + } + +} diff --git a/src/net/sourceforge/peers/media/MediaManager.java b/src/net/sourceforge/peers/media/MediaManager.java new file mode 100644 index 0000000..5ab5f44 --- /dev/null +++ b/src/net/sourceforge/peers/media/MediaManager.java @@ -0,0 +1,340 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +import code.BassFileReader; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.rtp.RtpPacket; +import net.sourceforge.peers.rtp.RtpSession; +import net.sourceforge.peers.sdp.Codec; +import net.sourceforge.peers.sip.core.useragent.UserAgent; + +public class MediaManager { + + public static final int DEFAULT_CLOCK = 8000; // Hz + + private UserAgent userAgent; + private CaptureRtpSender captureRtpSender; + private IncomingRtpReader incomingRtpReader; + private RtpSession rtpSession; + private DtmfFactory dtmfFactory; + private Logger logger; + private DatagramSocket datagramSocket; + //private FileReader fileReader; + private BassFileReader fileReader; + + public MediaManager(UserAgent userAgent, Logger logger) { + this.userAgent = userAgent; + this.logger = logger; + dtmfFactory = new DtmfFactory(); + } + + private void startRtpSessionOnSuccessResponse(String localAddress, + String remoteAddress, int remotePort, Codec codec, + SoundSource soundSource) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(localAddress); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host: " + localAddress, e); + return; + } + + rtpSession = new RtpSession(inetAddress, datagramSocket, + userAgent.isMediaDebug(), logger, userAgent.getPeersHome()); + + try { + inetAddress = InetAddress.getByName(remoteAddress); + rtpSession.setRemoteAddress(inetAddress); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host: " + remoteAddress, e); + } + rtpSession.setRemotePort(remotePort); + + + try { + captureRtpSender = new CaptureRtpSender(rtpSession, + soundSource, userAgent.isMediaDebug(), codec, logger, + userAgent.getPeersHome()); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + + try { + captureRtpSender.start(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + } + } + + public void successResponseReceived(String localAddress, + String remoteAddress, int remotePort, Codec codec) { + switch (userAgent.getMediaMode()) { + case captureAndPlayback: + AbstractSoundManager soundManager = userAgent.getSoundManager(); + soundManager.init(); + startRtpSessionOnSuccessResponse(localAddress, remoteAddress, + remotePort, codec, soundManager); + + try { + incomingRtpReader = new IncomingRtpReader( + captureRtpSender.getRtpSession(), soundManager, codec, + logger); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + + incomingRtpReader.start(); + break; + + case echo: + Echo echo; + try { + echo = new Echo(datagramSocket, remoteAddress, remotePort, + logger); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host amongst " + + localAddress + " or " + remoteAddress); + return; + } + userAgent.setEcho(echo); + Thread echoThread = new Thread(echo, Echo.class.getSimpleName()); + echoThread.start(); + break; + case file: + String fileName = userAgent.getConfig().getMediaFile(); + //fileReader = new FileReader(fileName, logger); + fileReader = new BassFileReader(fileName,null); + + startRtpSessionOnSuccessResponse(localAddress, remoteAddress, + remotePort, codec, fileReader); + try { + incomingRtpReader = new IncomingRtpReader( + captureRtpSender.getRtpSession(), null, codec, + logger); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + + incomingRtpReader.start(); + break; + case none: + default: + break; + } + } + + private void startRtpSession(String destAddress, int destPort, + Codec codec, SoundSource soundSource) { + rtpSession = new RtpSession(userAgent.getConfig() + .getLocalInetAddress(), datagramSocket, + userAgent.isMediaDebug(), logger, userAgent.getPeersHome()); + + try { + InetAddress inetAddress = InetAddress.getByName(destAddress); + rtpSession.setRemoteAddress(inetAddress); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host: " + destAddress, e); + } + rtpSession.setRemotePort(destPort); + + try { + captureRtpSender = new CaptureRtpSender(rtpSession, + soundSource, userAgent.isMediaDebug(), codec, logger, + userAgent.getPeersHome()); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + try { + captureRtpSender.start(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + } + + } + + public void handleAck(String destAddress, int destPort, Codec codec) { + switch (userAgent.getMediaMode()) { + case captureAndPlayback: + + AbstractSoundManager soundManager = userAgent.getSoundManager(); + soundManager.init(); + + startRtpSession(destAddress, destPort, codec, soundManager); + + try { + //FIXME RTP sessions can be different ! + incomingRtpReader = new IncomingRtpReader(rtpSession, + soundManager, codec, logger); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + + incomingRtpReader.start(); + + break; + case echo: + Echo echo; + try { + echo = new Echo(datagramSocket, destAddress, destPort, logger); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host amongst " + + userAgent.getConfig().getLocalInetAddress() + .getHostAddress() + " or " + destAddress); + return; + } + userAgent.setEcho(echo); + Thread echoThread = new Thread(echo, Echo.class.getSimpleName()); + echoThread.start(); + break; + case file: + if (fileReader != null) { + fileReader.close(); + } + String fileName = userAgent.getConfig().getMediaFile(); + //fileReader = new FileReader(fileName, logger); + fileReader = new BassFileReader(fileName, null); + + startRtpSession(destAddress, destPort, codec, fileReader); + try { + incomingRtpReader = new IncomingRtpReader(rtpSession, + null, codec, logger); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + incomingRtpReader.start(); + break; + case none: + default: + break; + } + } + + public void updateRemote(String destAddress, int destPort, Codec codec) { + switch (userAgent.getMediaMode()) { + case captureAndPlayback: + try { + InetAddress inetAddress = InetAddress.getByName(destAddress); + rtpSession.setRemoteAddress(inetAddress); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host: " + destAddress, e); + } + rtpSession.setRemotePort(destPort); + break; + case echo: + //TODO update echo socket + break; + case file: + try { + InetAddress inetAddress = InetAddress.getByName(destAddress); + rtpSession.setRemoteAddress(inetAddress); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host: " + destAddress, e); + } + rtpSession.setRemotePort(destPort); + break; + + default: + break; + } + + } + + public void sendDtmf(char digit) { + if (captureRtpSender != null) { + List rtpPackets = dtmfFactory.createDtmfPackets(digit); + RtpSender rtpSender = captureRtpSender.getRtpSender(); + rtpSender.pushPackets(rtpPackets); + } + } + + public void stopSession() { + if (rtpSession != null) { + rtpSession.stop(); + while (!rtpSession.isSocketClosed()) { + try { + Thread.sleep(15); + } catch (InterruptedException e) { + if (logger!=null) logger.debug("sleep interrupted"); + } + } + rtpSession = null; + } + if (incomingRtpReader != null) { + incomingRtpReader = null; + } + if (captureRtpSender != null) { + captureRtpSender.stop(); + captureRtpSender = null; + } + if (datagramSocket != null) { + datagramSocket = null; + } + + switch (userAgent.getMediaMode()) { + case captureAndPlayback: + AbstractSoundManager soundManager = userAgent.getSoundManager(); + if (soundManager != null) { + soundManager.close(); + } + break; + case echo: + Echo echo = userAgent.getEcho(); + if (echo != null) { + echo.stop(); + userAgent.setEcho(null); + } + break; + case file: + fileReader.close(); + break; + default: + break; + } + } + + public void setDatagramSocket(DatagramSocket datagramSocket) { + this.datagramSocket = datagramSocket; + } + + public DatagramSocket getDatagramSocket() { + return datagramSocket; + } + +// public FileReader getFileReader() { +// return fileReader; +// } + + public BassFileReader getFileReader() { + return fileReader; + } +} diff --git a/src/net/sourceforge/peers/media/MediaMode.java b/src/net/sourceforge/peers/media/MediaMode.java new file mode 100644 index 0000000..5718e07 --- /dev/null +++ b/src/net/sourceforge/peers/media/MediaMode.java @@ -0,0 +1,24 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +public enum MediaMode { + none, captureAndPlayback, echo, file +} diff --git a/src/net/sourceforge/peers/media/PcmaDecoder.java b/src/net/sourceforge/peers/media/PcmaDecoder.java new file mode 100644 index 0000000..1f9cc7e --- /dev/null +++ b/src/net/sourceforge/peers/media/PcmaDecoder.java @@ -0,0 +1,71 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Oleg Kulikov, Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +public class PcmaDecoder extends Decoder { + + private static short aLawDecompressTable[] = new short[]{ + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944, + -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136, + -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, + -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848 + }; + + @Override + public byte[] process(byte[] media) { + byte[] res = new byte[media.length * 2]; + int j = 0; + for (int i = 0; i < media.length; i++) { + short s = aLawDecompressTable[media[i] & 0xff]; + res[j++] = (byte) s; + res[j++] = (byte) (s >> 8); + } + return res; + } + +} diff --git a/src/net/sourceforge/peers/media/PcmaEncoder.java b/src/net/sourceforge/peers/media/PcmaEncoder.java new file mode 100644 index 0000000..b896ba2 --- /dev/null +++ b/src/net/sourceforge/peers/media/PcmaEncoder.java @@ -0,0 +1,98 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010, 2011 Oleg Kulikov, Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + +import net.sourceforge.peers.Logger; + +public class PcmaEncoder extends Encoder { + + private final static int cClip = 32635; + private static byte aLawCompressTable[] = new byte[]{ + 1, 1, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7 + }; + + public PcmaEncoder(PipedInputStream rawData, PipedOutputStream encodedData, + boolean mediaDebug, Logger logger, String peersHome, + CountDownLatch latch) { + super(rawData, encodedData, mediaDebug, logger, peersHome, latch); + } + + @Override + public byte[] process(byte[] media) { + byte[] compressed = new byte[media.length / 2]; + + int j = 0; + for (int i = 0; i < compressed.length; i++) { + short sample = (short) (((media[j++] & 0xff) | (media[j++]) << 8)); + compressed[i] = linearToALawSample(sample); + } + return compressed; + } + + /** + * Compress 16bit value to 8bit value + * + * @param sample 16-bit sample + * @return compressed 8-bit value. + */ + private byte linearToALawSample(short sample) { + int sign; + int exponent; + int mantissa; + int s; + + sign = ((~sample) >> 8) & 0x80; + if (!(sign == 0x80)) { + sample = (short) -sample; + } + if (sample > cClip) { + sample = cClip; + } + if (sample >= 256) { + exponent = (int) aLawCompressTable[(sample >> 8) & 0x7F]; + mantissa = (sample >> (exponent + 3)) & 0x0F; + s = (exponent << 4) | mantissa; + } else { + s = sample >> 4; + } + s ^= (sign ^ 0x55); + return (byte) s; + } + +} diff --git a/src/net/sourceforge/peers/media/PcmuDecoder.java b/src/net/sourceforge/peers/media/PcmuDecoder.java new file mode 100644 index 0000000..346aa24 --- /dev/null +++ b/src/net/sourceforge/peers/media/PcmuDecoder.java @@ -0,0 +1,103 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Oleg Kulikov, Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +public class PcmuDecoder extends Decoder { + +// private final static int cBias = 0x84; +// private int QUANT_MASK = 0xf; +// private final static int SEG_SHIFT = 4; +// private final static int SEG_MASK = 0x70; +// private final static int SIGN_BIT = 0x80; + private static short muLawDecompressTable[] = new short[]{ + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, + -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, + -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, + -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, 0, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0 + }; + + @Override + public byte[] process(byte[] media) { + byte[] res = new byte[media.length * 2]; + int j = 0; + for (int i = 0; i < media.length; i++) { + short s = muLawDecompressTable[media[i] & 0xff]; + res[j++] = (byte) s; + res[j++] = (byte) (s >> 8); + } + return res; + } + + //TODO compare what's the fastest: table lookup or real conversion + /* + * ulaw2linear() - Convert a u-law value to 16-bit linear PCM + * + * First, a biased linear code is derived from the code word. An unbiased + * output can then be obtained by subtracting 33 from the biased code. + * + * Note that this function expects to be passed the complement of the + * original code word. This is in keeping with ISDN conventions. + */ +// private short ulaw2linear(byte u_val) { +// int t; +// +// /* Complement to obtain normal u-law value. */ +// u_val = (byte) ~u_val; +// +// /* +// * Extract and bias the quantization bits. Then +// * shift up by the segment number and subtract out the bias. +// */ +// t = ((u_val & QUANT_MASK) << 3) + cBias; +// t <<= (u_val & SEG_MASK) >> SEG_SHIFT; +// +// boolean s = (u_val & SIGN_BIT) == SIGN_BIT; +// return (short) (s ? (cBias - t) : (t - cBias)); +// } + +} diff --git a/src/net/sourceforge/peers/media/PcmuEncoder.java b/src/net/sourceforge/peers/media/PcmuEncoder.java new file mode 100644 index 0000000..e25e888 --- /dev/null +++ b/src/net/sourceforge/peers/media/PcmuEncoder.java @@ -0,0 +1,130 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010, 2011 Oleg Kulikov, Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + +import net.sourceforge.peers.Logger; + +public class PcmuEncoder extends Encoder { + + private final static int cBias = 0x84; + private final static short seg_end[] = new short[]{0xFF, 0x1FF, 0x3FF, 0x7FF, + 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF + }; + + /* + * linear2ulaw() - Convert a linear PCM value to u-law + * + * In order to simplify the encoding process, the original linear magnitude + * is biased by adding 33 which shifts the encoding range from (0 - 8158) to + * (33 - 8191). The result can be seen in the following encoding table: + * + * Biased Linear Input Code Compressed Code + * ------------------------ --------------- + * 00000001wxyza 000wxyz + * 0000001wxyzab 001wxyz + * 000001wxyzabc 010wxyz + * 00001wxyzabcd 011wxyz + * 0001wxyzabcde 100wxyz + * 001wxyzabcdef 101wxyz + * 01wxyzabcdefg 110wxyz + * 1wxyzabcdefgh 111wxyz + * + * Each biased linear code has a leading 1 which identifies the segment + * number. The value of the segment number is equal to 7 minus the number + * of leading 0's. The quantization interval is directly available as the + * four bits wxyz. * The trailing bits (a - h) are ignored. + * + * Ordinarily the complement of the resulting code word is used for + * transmission, and so the code word is complemented before it is returned. + * + * For further information see John C. Bellamy's Digital Telephony, 1982, + * John Wiley & Sons, pps 98-111 and 472-476. + */ + private static byte linear2ulaw(short pcm_val) { + int mask; + int seg; + byte uval; + + /* Get the sign and the magnitude of the value. */ + if (pcm_val < 0) { + pcm_val = (short) (cBias - pcm_val); + mask = 0x7F; + } else { + pcm_val += cBias; + mask = 0xFF; + } + + /* Convert the scaled magnitude to segment number. */ + seg = search(pcm_val, seg_end, 8); + + /* + * Combine the sign, segment, quantization bits; + * and complement the code word. + */ + if (seg >= 8) /* out of range, return maximum value. */ { + return (byte)(0x7F ^ mask); + } else { + uval = (byte)((seg << 4) | ((pcm_val >> (seg + 3)) & 0xF)); + return (byte)(uval ^ mask); + } + + } + + private static int search(int val, short[] table, int size) { + int i; + + for (i = 0; i < size; i++) { + if (val <= table[i]) { + return (i); + } + } + return (size); + } + + public PcmuEncoder(PipedInputStream rawData, PipedOutputStream encodedData, + boolean mediaDebug, Logger logger, String peersHome, + CountDownLatch latch) { + super(rawData, encodedData, mediaDebug, logger, peersHome, latch); + } + + /** + * Perform compression using U-law. Retrieved from Mobicents media server + * code. + * + * @param media the input uncompressed media + * @return the output compressed media. + */ + public byte[] process(byte[] media) { + byte[] compressed = new byte[media.length / 2]; + + int j = 0; + for (int i = 0; i < compressed.length; i++) { + short sample = (short) ((media[j++] & 0xff) | ((media[j++]) << 8)); + compressed[i] = linear2ulaw(sample); + } + return compressed; + } + + +} diff --git a/src/net/sourceforge/peers/media/RtpSender.java b/src/net/sourceforge/peers/media/RtpSender.java new file mode 100644 index 0000000..b4028fe --- /dev/null +++ b/src/net/sourceforge/peers/media/RtpSender.java @@ -0,0 +1,200 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.rtp.RtpPacket; +import net.sourceforge.peers.rtp.RtpSession; +import net.sourceforge.peers.sdp.Codec; + +public class RtpSender implements Runnable { + + private PipedInputStream encodedData; + private RtpSession rtpSession; + private boolean isStopped; + private FileOutputStream rtpSenderInput; + private boolean mediaDebug; + private Codec codec; + private List pushedPackets; + private Logger logger; + private String peersHome; + private CountDownLatch latch; + + public RtpSender(PipedInputStream encodedData, RtpSession rtpSession, + boolean mediaDebug, Codec codec, Logger logger, String peersHome, + CountDownLatch latch) { + this.encodedData = encodedData; + this.rtpSession = rtpSession; + this.mediaDebug = mediaDebug; + this.codec = codec; + this.peersHome = peersHome; + this.latch = latch; + isStopped = false; + pushedPackets = Collections.synchronizedList( + new ArrayList()); + } + + public void run() { + if (mediaDebug) { + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + String date = simpleDateFormat.format(new Date()); + String fileName = peersHome + File.separator + + AbstractSoundManager.MEDIA_DIR + File.separator + date + + "_rtp_sender.input"; + try { + rtpSenderInput = new FileOutputStream(fileName); + } catch (FileNotFoundException e) { + if (logger!=null) if (logger!=null) logger.error("cannot create file", e); + return; + } + } + RtpPacket rtpPacket = new RtpPacket(); + rtpPacket.setVersion(2); + rtpPacket.setPadding(false); + rtpPacket.setExtension(false); + rtpPacket.setCsrcCount(0); + rtpPacket.setMarker(false); + rtpPacket.setPayloadType(codec.getPayloadType()); + Random random = new Random(); + int sequenceNumber = random.nextInt(); + rtpPacket.setSequenceNumber(sequenceNumber); + rtpPacket.setSsrc(random.nextInt()); + int buf_size = Capture.BUFFER_SIZE / 2; + byte[] buffer = new byte[buf_size]; + int timestamp = 0; + int numBytesRead; + int tempBytesRead; + long sleepTime = 0; + long offset = 0; + long lastSentTime = System.nanoTime(); + // indicate if its the first time that we send a packet (dont wait) + boolean firstTime = true; + + while (!isStopped) { + numBytesRead = 0; + try { + while (!isStopped && numBytesRead < buf_size) { + // expect that the buffer is full + tempBytesRead = encodedData.read(buffer, numBytesRead, + buf_size - numBytesRead); + numBytesRead += tempBytesRead; + } + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + return; + } + byte[] trimmedBuffer; + if (numBytesRead < buffer.length) { + trimmedBuffer = new byte[numBytesRead]; + System.arraycopy(buffer, 0, trimmedBuffer, 0, numBytesRead); + } else { + trimmedBuffer = buffer; + } + if (mediaDebug) { + try { + rtpSenderInput.write(trimmedBuffer); // TODO use classpath + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot write to file", e); + break; + } + } + if (pushedPackets.size() > 0) { + RtpPacket pushedPacket = pushedPackets.remove(0); + rtpPacket.setMarker(pushedPacket.isMarker()); + rtpPacket.setPayloadType(pushedPacket.getPayloadType()); + byte[] data = pushedPacket.getData(); + rtpPacket.setData(data); + } else { + if (rtpPacket.getPayloadType() != codec.getPayloadType()) { + rtpPacket.setPayloadType(codec.getPayloadType()); + rtpPacket.setMarker(false); + } + rtpPacket.setData(trimmedBuffer); + } + + rtpPacket.setSequenceNumber(sequenceNumber++); + timestamp += buf_size; + rtpPacket.setTimestamp(timestamp); + if (firstTime) { + rtpSession.send(rtpPacket); + lastSentTime = System.nanoTime(); + firstTime = false; + continue; + } + sleepTime = 19500000 - (System.nanoTime() - lastSentTime) + offset; + if (sleepTime > 0) { + try { + Thread.sleep(Math.round(sleepTime / 1000000f)); + } catch (InterruptedException e) { + if (logger!=null) if (logger!=null) logger.error("Thread interrupted", e); + return; + } + rtpSession.send(rtpPacket); + lastSentTime = System.nanoTime(); + offset = 0; + } else { + rtpSession.send(rtpPacket); + lastSentTime = System.nanoTime(); + if (sleepTime < -20000000) { + offset = sleepTime + 20000000; + } + } + } + if (mediaDebug) { + try { + rtpSenderInput.close(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot close file", e); + return; + } + } + latch.countDown(); + if (latch.getCount() != 0) { + try { + latch.await(); + } catch (InterruptedException e) { + if (logger!=null) if (logger!=null) logger.error("interrupt exception", e); + } + } + } + + public synchronized void setStopped(boolean isStopped) { + this.isStopped = isStopped; + } + + public void pushPackets(List rtpPackets) { + this.pushedPackets.addAll(rtpPackets); + } + +} diff --git a/src/net/sourceforge/peers/media/SoundSource.java b/src/net/sourceforge/peers/media/SoundSource.java new file mode 100644 index 0000000..5cd57a0 --- /dev/null +++ b/src/net/sourceforge/peers/media/SoundSource.java @@ -0,0 +1,30 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2012 Yohann Martineau +*/ + +package net.sourceforge.peers.media; + +public interface SoundSource { + + /** + * read raw data linear PCM 8kHz, 16 bits signed, mono-channel, little endian + * @return + */ + public byte[] readData(); + +} diff --git a/src/net/sourceforge/peers/nat/Client.java b/src/net/sourceforge/peers/nat/Client.java new file mode 100644 index 0000000..d711807 --- /dev/null +++ b/src/net/sourceforge/peers/nat/Client.java @@ -0,0 +1,64 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat; + +import java.io.IOException; +import java.net.InetAddress; + +import org.w3c.dom.Document; + +public class Client { + + private Server server; + //private String email; + private PeerManager peerManager; + + public Client(String email, String localInetAddress, int localPort) { + //this.email = email; + // TODO automatic global access interface discovery + try { + InetAddress localAddress = InetAddress.getByName(localInetAddress); + server = new Server(localAddress, localPort); + peerManager = new PeerManager(localAddress, localPort); + } catch (IOException e) { + e.printStackTrace(); + return; + } + server.update(email); + Document document = server.getPeers(email); + peerManager.setDocument(document); + peerManager.start(); + } + + /** + * @param args + */ + public static void main(String[] args) { + if (args.length != 3) { + System.err.println("usage: java ... " + + " "); + System.exit(1); + } + + new Client(args[0], args[1], Integer.parseInt(args[2])); + + } + +} diff --git a/src/net/sourceforge/peers/nat/PeerManager.java b/src/net/sourceforge/peers/nat/PeerManager.java new file mode 100644 index 0000000..5a8157f --- /dev/null +++ b/src/net/sourceforge/peers/nat/PeerManager.java @@ -0,0 +1,117 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class PeerManager extends Thread { + + private InetAddress localAddress; + private int localPort; + private Document document; + + public PeerManager(InetAddress localAddress, int localPort) { + this.localAddress = localAddress; + this.localPort = localPort; + } + + public void setDocument(Document document) { + this.document = document; + } + + public void run() { + DatagramSocket datagramSocket; + try { + datagramSocket = new DatagramSocket(localPort, localAddress); + } catch (SocketException e) { + e.printStackTrace(); + return; + } +// UDPReceiver udpReceiver = new UDPReceiver(datagramSocket); +// udpReceiver.start(); + while (true) { + Element root = document.getDocumentElement(); + NodeList peers = root.getChildNodes(); + for (int i = 0; i < peers.getLength(); ++i) { + Node node = peers.item(i); + if (node.getNodeName().equals("peer")) { + createConnection(node, datagramSocket); + } + } + try { + Thread.sleep(30000); + } catch (InterruptedException e) { + e.printStackTrace(); + return; + } + } + } + + private void createConnection(Node peer, DatagramSocket datagramSocket) { + NodeList childNodes = peer.getChildNodes(); + String ipAddress = null; + String port = null; + for (int i = 0; i < childNodes.getLength(); ++i) { + Node node = childNodes.item(i); + String nodeName = node.getNodeName(); + if (nodeName.equals("ipaddress")) { + ipAddress = node.getTextContent(); + } else if (nodeName.equals("port")) { + port = node.getTextContent(); + } + } + if (ipAddress == null || port == null) { + return; + } + int remotePort = Integer.parseInt(port); + try { + InetAddress remoteAddress = InetAddress.getByName(ipAddress); + // DatagramSocket datagramSocket = new DatagramSocket(localPort, localAddress); + for (int i = 0; i < 5; ++i) { + String message = "hello world " + System.currentTimeMillis(); + byte[] buf = message.getBytes(); + DatagramPacket datagramPacket = + new DatagramPacket(buf, buf.length, remoteAddress, remotePort); + datagramSocket.send(datagramPacket); + System.out.println("> sent:\n" + message); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + return; + } + } + + //datagramSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + return; + } + } +} diff --git a/src/net/sourceforge/peers/nat/Server.java b/src/net/sourceforge/peers/nat/Server.java new file mode 100644 index 0000000..e94ee56 --- /dev/null +++ b/src/net/sourceforge/peers/nat/Server.java @@ -0,0 +1,175 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +public class Server { + + public static final String SERVER_HOST = "peers.sourceforge.net"; + public static final String PREFIX = "/peers"; + //public static final int SOCKET_TIMEOUT = 30000;//millis + + //private InetAddress localAddress; + //private int localPort; + private InetAddress remoteAddress; + private int remotePort; + + private Socket socket; + + //TODO constructor without parameters + public Server(InetAddress localAddress, int localPort) throws IOException { + super(); + //this.localAddress = localAddress; + //this.localPort = localPort; + this.remoteAddress = InetAddress.getByName(SERVER_HOST); + this.remotePort = 80; + socket = new Socket(remoteAddress, remotePort, localAddress, localPort); + //socket.setSoTimeout(SOCKET_TIMEOUT); + } + + /** + * This method will update public address on the web server. + * @param email user identifier + */ + public void update(String email) { + String encodedEmail; + try { + encodedEmail = URLEncoder.encode(email, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return; + } + StringBuffer urlEnd = new StringBuffer(); + urlEnd.append("update2.php?email="); + urlEnd.append(encodedEmail); + get(urlEnd.toString()); + close(); + } + + public Document getPeers(String email) { + String encodedEmail; + try { + encodedEmail = URLEncoder.encode(email, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return null; + } + StringBuffer urlBuf = new StringBuffer(); + urlBuf.append("http://"); + urlBuf.append(SERVER_HOST); + urlBuf.append(PREFIX); + urlBuf.append("/getassocasxml.php?email="); + urlBuf.append(encodedEmail); + URL url; + try { + url = new URL(urlBuf.toString()); + } catch (MalformedURLException e) { + e.printStackTrace(); + return null; + } + System.out.println("retrieved peers"); + DocumentBuilderFactory documentBuilderFactory + = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + return null; + } + try { + URLConnection urlConnection = url.openConnection(); + InputStream inputStream = urlConnection.getInputStream(); + return documentBuilder.parse(inputStream); + } catch (IOException e) { + e.printStackTrace(); + } catch (SAXException e) { + e.printStackTrace(); + } + return null; + } + + private String get(String urlEnd) { + StringBuffer get = new StringBuffer(); + get.append("GET "); + get.append(PREFIX); + get.append('/'); + get.append(urlEnd); + get.append(" HTTP/1.1\r\n"); + get.append("Host: "); + get.append(SERVER_HOST); + get.append("\r\n"); + get.append("\r\n"); + + try { + socket.getOutputStream().write(get.toString().getBytes()); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + System.out.println("> sent:\n" + get.toString()); + + StringBuffer result = new StringBuffer(); + try { + byte[] buf = new byte[256]; + int read = 0; + while ((read = socket.getInputStream().read(buf)) > -1) { + byte[] exactBuf = new byte[read]; + System.arraycopy(buf, 0, exactBuf, 0, read); + result.append(new String(exactBuf)); + } + } catch (SocketTimeoutException e) { + System.out.println("socket timeout"); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + System.out.println("< received:\n" + result.toString()); + return result.toString(); + } + + public void close() { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/net/sourceforge/peers/nat/UDPReceiver.java b/src/net/sourceforge/peers/nat/UDPReceiver.java new file mode 100644 index 0000000..3025f72 --- /dev/null +++ b/src/net/sourceforge/peers/nat/UDPReceiver.java @@ -0,0 +1,49 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +public class UDPReceiver extends Thread { + + private DatagramSocket datagramSocket; + + public UDPReceiver(DatagramSocket datagramSocket) { + super(); + this.datagramSocket = datagramSocket; + } + + @Override + public void run() { + try { + while (true) { + byte[] buf = new byte[1024]; + DatagramPacket packet = new DatagramPacket(buf, buf.length); + datagramSocket.receive(packet); + System.out.println("< received:\n" + + new String(packet.getData())); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/net/sourceforge/peers/nat/api/DataReceiver.java b/src/net/sourceforge/peers/nat/api/DataReceiver.java new file mode 100644 index 0000000..1416322 --- /dev/null +++ b/src/net/sourceforge/peers/nat/api/DataReceiver.java @@ -0,0 +1,26 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat.api; + + +public interface DataReceiver { + + public void dataReceived(byte[] data, String peerId); +} diff --git a/src/net/sourceforge/peers/nat/api/PeersClient.java b/src/net/sourceforge/peers/nat/api/PeersClient.java new file mode 100644 index 0000000..8ed9afc --- /dev/null +++ b/src/net/sourceforge/peers/nat/api/PeersClient.java @@ -0,0 +1,47 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat.api; + +public abstract class PeersClient { + + /** + * creates a new peers client + * @param myId the string identifier corresponding + * to the computer or to a person (email). + * @param dataReceiver object that will receive incoming traffic. + */ + public PeersClient(String myId, DataReceiver dataReceiver) { + + } + + /** + * creates a UDP connection to a peer. + * @param peerId unique peer identifier (email for example). + * @return an object that allows to send data to the peer. + */ + public abstract UDPTransport createUDPTransport(String peerId); + + /** + * creates a TCP connection to a peer. + * @param peerId unique peer identifier (email for example). + * @return an object that allows to send data to the peer. + */ + public abstract TCPTransport createTCPTransport(String peerId); +} diff --git a/src/net/sourceforge/peers/nat/api/TCPTransport.java b/src/net/sourceforge/peers/nat/api/TCPTransport.java new file mode 100644 index 0000000..325fe28 --- /dev/null +++ b/src/net/sourceforge/peers/nat/api/TCPTransport.java @@ -0,0 +1,24 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat.api; + +public interface TCPTransport extends Transport { + +} diff --git a/src/net/sourceforge/peers/nat/api/Transport.java b/src/net/sourceforge/peers/nat/api/Transport.java new file mode 100644 index 0000000..8aca84a --- /dev/null +++ b/src/net/sourceforge/peers/nat/api/Transport.java @@ -0,0 +1,25 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat.api; + +public interface Transport { + + public void sendData(byte[] data); +} diff --git a/src/net/sourceforge/peers/nat/api/UDPTransport.java b/src/net/sourceforge/peers/nat/api/UDPTransport.java new file mode 100644 index 0000000..566bff1 --- /dev/null +++ b/src/net/sourceforge/peers/nat/api/UDPTransport.java @@ -0,0 +1,24 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.nat.api; + +public interface UDPTransport extends Transport { + +} diff --git a/src/net/sourceforge/peers/rtp/RFC3551.java b/src/net/sourceforge/peers/rtp/RFC3551.java new file mode 100644 index 0000000..2da59da --- /dev/null +++ b/src/net/sourceforge/peers/rtp/RFC3551.java @@ -0,0 +1,34 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.rtp; + +public class RFC3551 { + + // payload types + + public static final int PAYLOAD_TYPE_PCMU = 0; + public static final int PAYLOAD_TYPE_PCMA = 8; + + // encoding names + + public static final String PCMU = "PCMU"; + public static final String PCMA = "PCMA"; + +} diff --git a/src/net/sourceforge/peers/rtp/RFC4733.java b/src/net/sourceforge/peers/rtp/RFC4733.java new file mode 100644 index 0000000..dae5cb2 --- /dev/null +++ b/src/net/sourceforge/peers/rtp/RFC4733.java @@ -0,0 +1,32 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.rtp; + +public class RFC4733 { + + // payload types + + public static final int PAYLOAD_TYPE_TELEPHONE_EVENT = 101; + + // encoding names + + public static final String TELEPHONE_EVENT = "telephone-event"; + +} diff --git a/src/net/sourceforge/peers/rtp/RtpListener.java b/src/net/sourceforge/peers/rtp/RtpListener.java new file mode 100644 index 0000000..7055649 --- /dev/null +++ b/src/net/sourceforge/peers/rtp/RtpListener.java @@ -0,0 +1,26 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.rtp; + +public interface RtpListener { + + public void receivedRtpPacket(RtpPacket rtpPacket); + +} diff --git a/src/net/sourceforge/peers/rtp/RtpPacket.java b/src/net/sourceforge/peers/rtp/RtpPacket.java new file mode 100644 index 0000000..1f598f0 --- /dev/null +++ b/src/net/sourceforge/peers/rtp/RtpPacket.java @@ -0,0 +1,124 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.rtp; + +public class RtpPacket { + + private int version; + private boolean padding; + private boolean extension; + private int csrcCount; + private boolean marker; + private int payloadType; + private int sequenceNumber; + private long timestamp; + private long ssrc; + private long[] csrcList; + private byte[] data; + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public boolean isPadding() { + return padding; + } + + public void setPadding(boolean padding) { + this.padding = padding; + } + + public boolean isExtension() { + return extension; + } + + public void setExtension(boolean extension) { + this.extension = extension; + } + + public int getCsrcCount() { + return csrcCount; + } + + public void setCsrcCount(int csrcCount) { + this.csrcCount = csrcCount; + } + + public boolean isMarker() { + return marker; + } + + public void setMarker(boolean marker) { + this.marker = marker; + } + + public int getPayloadType() { + return payloadType; + } + + public void setPayloadType(int payloadType) { + this.payloadType = payloadType; + } + + public int getSequenceNumber() { + return sequenceNumber; + } + + public void setSequenceNumber(int sequenceNumber) { + this.sequenceNumber = sequenceNumber; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getSsrc() { + return ssrc; + } + + public void setSsrc(long ssrc) { + this.ssrc = ssrc; + } + + public long[] getCsrcList() { + return csrcList; + } + + public void setCsrcList(long[] csrcList) { + this.csrcList = csrcList; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + +} diff --git a/src/net/sourceforge/peers/rtp/RtpParser.java b/src/net/sourceforge/peers/rtp/RtpParser.java new file mode 100644 index 0000000..0ffe6e4 --- /dev/null +++ b/src/net/sourceforge/peers/rtp/RtpParser.java @@ -0,0 +1,121 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.rtp; + +import net.sourceforge.peers.Logger; + +// RFC 3550 +public class RtpParser { + + private Logger logger; + + public RtpParser(Logger logger) { + this.logger = logger; + } + + public RtpPacket decode(byte[] packet) { + if (packet.length < 12) { + if (logger!=null) if (logger!=null) logger.error("RTP packet too short"); + return null; + } + RtpPacket rtpPacket = new RtpPacket(); + int b = (int)(packet[0] & 0xff); + rtpPacket.setVersion((b & 0xc0) >> 6); + rtpPacket.setPadding((b & 0x20) != 0); + rtpPacket.setExtension((b & 0x10) != 0); + rtpPacket.setCsrcCount(b & 0x0f); + b = (int)(packet[1] & 0xff); + rtpPacket.setMarker((b & 0x80) != 0); + rtpPacket.setPayloadType(b & 0x7f); + b = (int)(packet[2] & 0xff); + rtpPacket.setSequenceNumber(b * 256 + (int)(packet[3] & 0xff)); + b = (int)(packet[4] & 0xff); + rtpPacket.setTimestamp(b * 256 * 256 * 256 + + (int)(packet[5] & 0xff) * 256 * 256 + + (int)(packet[6] & 0xff) * 256 + + (int)(packet[7] & 0xff)); + b = (int)(packet[8] & 0xff); + rtpPacket.setSsrc(b * 256 * 256 * 256 + + (int)(packet[9] & 0xff) * 256 * 256 + + (int)(packet[10] & 0xff) * 256 + + (int)(packet[11] & 0xff)); + long[] csrcList = new long[rtpPacket.getCsrcCount()]; + for (int i = 0; i < csrcList.length; ++i) + csrcList[i] = (int)(packet[12 + i] & 0xff) << 24 + + (int)(packet[12 + i + 1] & 0xff) << 16 + + (int)(packet[12 + i + 2] & 0xff) << 8 + + (int)(packet[12 + i + 3] & 0xff); + rtpPacket.setCsrcList(csrcList); + int dataOffset = 12 + csrcList.length * 4; + int dataLength = packet.length - dataOffset; + byte[] data = new byte[dataLength]; + System.arraycopy(packet, dataOffset, data, 0, dataLength); + rtpPacket.setData(data); + return rtpPacket; + } + + public byte[] encode(RtpPacket rtpPacket) { + byte[] data = rtpPacket.getData(); + int packetLength = 12 + rtpPacket.getCsrcCount() * 4 + data.length; + byte[] packet = new byte[packetLength]; + int b = (rtpPacket.getVersion() << 6) + + ((rtpPacket.isPadding() ? 1 : 0) << 5) + + ((rtpPacket.isExtension() ? 1 : 0) << 4) + + (rtpPacket.getCsrcCount()); + packet[0] = new Integer(b).byteValue(); + b = ((rtpPacket.isMarker() ? 1 : 0) << 7) + + rtpPacket.getPayloadType(); + packet[1] = new Integer(b).byteValue(); + b = rtpPacket.getSequenceNumber() >> 8; + packet[2] = new Integer(b).byteValue(); + b = rtpPacket.getSequenceNumber() & 0xff; + packet[3] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getTimestamp() >> 24); + packet[4] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getTimestamp() >> 16); + packet[5] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getTimestamp() >> 8); + packet[6] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getTimestamp() & 0xff); + packet[7] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getSsrc() >> 24); + packet[8] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getSsrc() >> 16); + packet[9] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getSsrc() >> 8); + packet[10] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getSsrc() & 0xff); + packet[11] = new Integer(b).byteValue(); + for (int i = 0; i < rtpPacket.getCsrcCount(); ++i) { + b = (int)(rtpPacket.getCsrcList()[i] >> 24); + packet[12 + i * 4] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getCsrcList()[i] >> 16); + packet[12 + i * 4 + 1] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getCsrcList()[i] >> 8); + packet[12 + i * 4 + 2] = new Integer(b).byteValue(); + b = (int)(rtpPacket.getCsrcList()[i] & 0xff); + packet[12 + i * 4 + 3] = new Integer(b).byteValue(); + } + System.arraycopy(data, 0, packet, 12 + rtpPacket.getCsrcCount() * 4, + data.length); + return packet; + } + +} diff --git a/src/net/sourceforge/peers/rtp/RtpSession.java b/src/net/sourceforge/peers/rtp/RtpSession.java new file mode 100644 index 0000000..8da3482 --- /dev/null +++ b/src/net/sourceforge/peers/rtp/RtpSession.java @@ -0,0 +1,265 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.rtp; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.media.AbstractSoundManager; + +/** + * can be instantiated on UAC INVITE sending or on UAS 200 OK sending + */ +public class RtpSession { + + private InetAddress remoteAddress; + private int remotePort; + private DatagramSocket datagramSocket; + private ExecutorService executorService; + private List rtpListeners; + private RtpParser rtpParser; + private FileOutputStream rtpSessionOutput; + private FileOutputStream rtpSessionInput; + private boolean mediaDebug; + private Logger logger; + private String peersHome; + + public RtpSession(InetAddress localAddress, DatagramSocket datagramSocket, + boolean mediaDebug, Logger logger, String peersHome) { + this.mediaDebug = mediaDebug; + this.logger = logger; + this.peersHome = peersHome; + this.datagramSocket = datagramSocket; + rtpListeners = new ArrayList(); + rtpParser = new RtpParser(logger); + executorService = Executors.newSingleThreadExecutor(); + } + + public synchronized void start() { + if (mediaDebug) { + SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + String date = simpleDateFormat.format(new Date()); + String dir = peersHome + File.separator + + AbstractSoundManager.MEDIA_DIR + File.separator; + String fileName = dir + date + "_rtp_session.output"; + try { + rtpSessionOutput = new FileOutputStream(fileName); + fileName = dir + date + "_rtp_session.input"; + rtpSessionInput = new FileOutputStream(fileName); + } catch (FileNotFoundException e) { + if (logger!=null) if (logger!=null) logger.error("cannot create file", e); + return; + } + } + executorService.submit(new Receiver()); + } + + public void stop() { + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + public Void run() { + executorService.shutdown(); + return null; + } + } + ); + } + + public void addRtpListener(RtpListener rtpListener) { + rtpListeners.add(rtpListener); + } + + public synchronized void send(RtpPacket rtpPacket) { + if (datagramSocket == null) { + return; + } + byte[] buf = rtpParser.encode(rtpPacket); + final DatagramPacket datagramPacket = + new DatagramPacket(buf, buf.length, + remoteAddress, remotePort); + + if (!datagramSocket.isClosed()) { + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public Void run() { + try { + datagramSocket.send(datagramPacket); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot send rtp packet", e); + } catch (SecurityException e) { + if (logger!=null) if (logger!=null) logger.error("security exception", e); + } + return null; + } + } + ); + + if (mediaDebug) { + try { + rtpSessionOutput.write(buf); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot write to file", e); + } + } + } + } + + public void setRemoteAddress(InetAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + + public void setRemotePort(int remotePort) { + this.remotePort = remotePort; + } + + private void closeFileAndDatagramSocket() { + if (mediaDebug) { + try { + rtpSessionOutput.close(); + rtpSessionInput.close(); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot close file", e); + } + } + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public Void run() { + datagramSocket.close(); + return null; + } + } + ); + + } + + class Receiver implements Runnable { + + @Override + public void run() { + int receiveBufferSize; + try { + receiveBufferSize = datagramSocket.getReceiveBufferSize(); + } catch (SocketException e) { + if (logger!=null) if (logger!=null) logger.error("cannot get datagram socket receive buffer size", + e); + return; + } + byte[] buf = new byte[receiveBufferSize]; + final DatagramPacket datagramPacket = new DatagramPacket(buf, + buf.length); + final int noException = 0; + final int socketTimeoutException = 1; + final int ioException = 2; + int result = AccessController.doPrivileged( + new PrivilegedAction() { + public Integer run() { + try { + datagramSocket.receive(datagramPacket); + } catch (SocketTimeoutException e) { + return socketTimeoutException; + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot receive packet", e); + return ioException; + } + return noException; + } + }); + switch (result) { + case socketTimeoutException: + try { + executorService.execute(this); + } catch (RejectedExecutionException rej) { + closeFileAndDatagramSocket(); + } + return; + case ioException: + return; + case noException: + break; + default: + break; + } + InetAddress remoteAddress = datagramPacket.getAddress(); + if (remoteAddress != null && + !remoteAddress.equals(RtpSession.this.remoteAddress)) { + RtpSession.this.remoteAddress = remoteAddress; + } + int remotePort = datagramPacket.getPort(); + if (remotePort != RtpSession.this.remotePort) { + RtpSession.this.remotePort = remotePort; + } + byte[] data = datagramPacket.getData(); + int offset = datagramPacket.getOffset(); + int length = datagramPacket.getLength(); + byte[] trimmedData = new byte[length]; + System.arraycopy(data, offset, trimmedData, 0, length); + if (mediaDebug) { + try { + rtpSessionInput.write(trimmedData); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("cannot write to file", e); + return; + } + } + RtpPacket rtpPacket = rtpParser.decode(trimmedData); + for (RtpListener rtpListener: rtpListeners) { + rtpListener.receivedRtpPacket(rtpPacket); + } + try { + executorService.execute(this); + } catch (RejectedExecutionException rej) { + closeFileAndDatagramSocket(); + } + } + + } + + public boolean isSocketClosed() { + if (datagramSocket == null) { + return true; + } + return datagramSocket.isClosed(); + } + +} diff --git a/src/net/sourceforge/peers/sdp/Codec.java b/src/net/sourceforge/peers/sdp/Codec.java new file mode 100644 index 0000000..cb8dcee --- /dev/null +++ b/src/net/sourceforge/peers/sdp/Codec.java @@ -0,0 +1,67 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +import net.sourceforge.peers.media.MediaManager; + +public class Codec { + + private int payloadType; + private String name; + + public int getPayloadType() { + return payloadType; + } + + public void setPayloadType(int payloadType) { + this.payloadType = payloadType; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Codec)) { + return false; + } + Codec codec = (Codec)obj; + if (codec.getName() == null) { + return name == null; + } + return codec.getName().equalsIgnoreCase(name); + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(RFC4566.TYPE_ATTRIBUTE).append(RFC4566.SEPARATOR); + buf.append(RFC4566.ATTR_RTPMAP).append(RFC4566.ATTR_SEPARATOR); + buf.append(payloadType).append(" ").append(name).append("/"); + buf.append(MediaManager.DEFAULT_CLOCK).append("\r\n"); + return buf.toString(); + } + +} diff --git a/src/net/sourceforge/peers/sdp/MediaDescription.java b/src/net/sourceforge/peers/sdp/MediaDescription.java new file mode 100644 index 0000000..7fa5ae0 --- /dev/null +++ b/src/net/sourceforge/peers/sdp/MediaDescription.java @@ -0,0 +1,118 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau + */ + +package net.sourceforge.peers.sdp; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Hashtable; +import java.util.List; + +public class MediaDescription { + + private String type; + private InetAddress ipAddress; + // attributes not codec-related + private Hashtable attributes; + private int port; + private List codecs; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Hashtable getAttributes() { + return attributes; + } + + public void setAttributes(Hashtable attributes) { + this.attributes = attributes; + } + + public InetAddress getIpAddress() { + return ipAddress; + } + + public void setIpAddress(InetAddress ipAddress) { + this.ipAddress = ipAddress; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public List getCodecs() { + return codecs; + } + + public void setCodecs(List codecs) { + this.codecs = codecs; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(RFC4566.TYPE_MEDIA).append(RFC4566.SEPARATOR); + buf.append(type).append(" ").append(port); + buf.append(" RTP/AVP"); + for (Codec codec: codecs) { + buf.append(" "); + buf.append(codec.getPayloadType()); + } + buf.append("\r\n"); + if (ipAddress != null) { + int ipVersion; + if (ipAddress instanceof Inet4Address) { + ipVersion = 4; + } else if (ipAddress instanceof Inet6Address) { + ipVersion = 6; + } else { + throw new RuntimeException("unknown ip version: " + ipAddress); + } + buf.append(RFC4566.TYPE_CONNECTION).append(RFC4566.SEPARATOR); + buf.append("IN IP").append(ipVersion).append(" "); + buf.append(ipAddress.getHostAddress()).append("\r\n"); + } + for (Codec codec: codecs) { + buf.append(codec.toString()); + } + if (attributes != null) { + for (String attributeName: attributes.keySet()) { + buf.append(RFC4566.TYPE_ATTRIBUTE).append(RFC4566.SEPARATOR); + buf.append(attributeName); + String attributeValue = attributes.get(attributeName); + if (attributeValue != null && !"".equals(attributeValue.trim())) { + buf.append(":").append(attributeValue); + } + buf.append("\r\n"); + } + } + return buf.toString(); + } + +} diff --git a/src/net/sourceforge/peers/sdp/MediaDestination.java b/src/net/sourceforge/peers/sdp/MediaDestination.java new file mode 100644 index 0000000..fd43cb2 --- /dev/null +++ b/src/net/sourceforge/peers/sdp/MediaDestination.java @@ -0,0 +1,52 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +public class MediaDestination { + + private String destination; + private int port; + private Codec codec; + + public String getDestination() { + return destination; + } + + public void setDestination(String destination) { + this.destination = destination; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public Codec getCodec() { + return codec; + } + + public void setCodec(Codec codec) { + this.codec = codec; + } + +} diff --git a/src/net/sourceforge/peers/sdp/NoCodecException.java b/src/net/sourceforge/peers/sdp/NoCodecException.java new file mode 100644 index 0000000..b0faca9 --- /dev/null +++ b/src/net/sourceforge/peers/sdp/NoCodecException.java @@ -0,0 +1,26 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +public class NoCodecException extends Exception { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/net/sourceforge/peers/sdp/RFC4566.java b/src/net/sourceforge/peers/sdp/RFC4566.java new file mode 100644 index 0000000..0a45cc7 --- /dev/null +++ b/src/net/sourceforge/peers/sdp/RFC4566.java @@ -0,0 +1,49 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +public class RFC4566 { + + public static final char VERSION = '0'; + + public static final char TYPE_VERSION = 'v'; + public static final char TYPE_ORIGIN = 'o'; + public static final char TYPE_SUBJECT = 's'; + public static final char TYPE_INFO = 'i'; + public static final char TYPE_URI = 'u'; + public static final char TYPE_EMAIL = 'e'; + public static final char TYPE_PHONE = 'p'; + public static final char TYPE_CONNECTION = 'c'; + public static final char TYPE_BANDWITH = 'b'; + public static final char TYPE_TIME = 't'; + public static final char TYPE_REPEAT = 'r'; + public static final char TYPE_ZONE = 'z'; + public static final char TYPE_KEY = 'k'; + public static final char TYPE_ATTRIBUTE = 'a'; + public static final char TYPE_MEDIA = 'm'; + + public static final char SEPARATOR = '='; + public static final char ATTR_SEPARATOR = ':'; + + public static final String MEDIA_AUDIO = "audio"; + + public static final String ATTR_RTPMAP = "rtpmap"; + public static final String ATTR_SENDRECV = "sendrecv"; +} diff --git a/src/net/sourceforge/peers/sdp/SDPManager.java b/src/net/sourceforge/peers/sdp/SDPManager.java new file mode 100644 index 0000000..636677b --- /dev/null +++ b/src/net/sourceforge/peers/sdp/SDPManager.java @@ -0,0 +1,150 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010, 2012 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Random; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.rtp.RFC3551; +import net.sourceforge.peers.rtp.RFC4733; +import net.sourceforge.peers.sip.core.useragent.UserAgent; + +public class SDPManager { + + private SdpParser sdpParser; + private UserAgent userAgent; + private List supportedCodecs; + private Random random; + + private Logger logger; + + public SDPManager(UserAgent userAgent, Logger logger) { + this.userAgent = userAgent; + this.logger = logger; + sdpParser = new SdpParser(); + supportedCodecs = new ArrayList(); + random = new Random(); + //TODO retrieve codecs from configuration file + Codec codec = new Codec(); + codec.setPayloadType(RFC3551.PAYLOAD_TYPE_PCMU); + codec.setName(RFC3551.PCMU); + supportedCodecs.add(codec); + codec = new Codec(); + codec.setPayloadType(RFC3551.PAYLOAD_TYPE_PCMA); + codec.setName(RFC3551.PCMA); + supportedCodecs.add(codec); + codec = new Codec(); + codec.setPayloadType(RFC4733.PAYLOAD_TYPE_TELEPHONE_EVENT); + codec.setName(RFC4733.TELEPHONE_EVENT); + //TODO add fmtp:101 0-15 attribute + supportedCodecs.add(codec); + } + + public SessionDescription parse(byte[] sdp) { + try { + return sdpParser.parse(sdp); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } + return null; + } + + public MediaDestination getMediaDestination( + SessionDescription sessionDescription) throws NoCodecException { + InetAddress destAddress = sessionDescription.getIpAddress(); + List mediaDescriptions = sessionDescription.getMediaDescriptions(); + for (MediaDescription mediaDescription: mediaDescriptions) { + if (RFC4566.MEDIA_AUDIO.equals(mediaDescription.getType())) { + for (Codec offerCodec: mediaDescription.getCodecs()) { + if (supportedCodecs.contains(offerCodec)) { + String offerCodecName = offerCodec.getName(); + if (offerCodecName.equalsIgnoreCase(RFC3551.PCMU) || + offerCodecName.equalsIgnoreCase(RFC3551.PCMA)) { + int destPort = mediaDescription.getPort(); + if (mediaDescription.getIpAddress() != null) { + destAddress = mediaDescription.getIpAddress(); + } + MediaDestination mediaDestination = + new MediaDestination(); + mediaDestination.setDestination( + destAddress.getHostAddress()); + mediaDestination.setPort(destPort); + mediaDestination.setCodec(offerCodec); + return mediaDestination; + } + } + } + } + } + throw new NoCodecException(); + } + + public SessionDescription createSessionDescription(SessionDescription offer, + int localRtpPort) + throws IOException { + SessionDescription sessionDescription = new SessionDescription(); + sessionDescription.setUsername("user1"); + sessionDescription.setId(random.nextInt(Integer.MAX_VALUE)); + sessionDescription.setVersion(random.nextInt(Integer.MAX_VALUE)); + Config config = userAgent.getConfig(); + InetAddress inetAddress = config.getPublicInetAddress(); + if (inetAddress == null) { + inetAddress = config.getLocalInetAddress(); + } + sessionDescription.setIpAddress(inetAddress); + sessionDescription.setName("-"); + sessionDescription.setAttributes(new Hashtable()); + List codecs; + if (offer == null) { + codecs = supportedCodecs; + } else { + codecs = new ArrayList(); + for (MediaDescription mediaDescription: + offer.getMediaDescriptions()) { + if (RFC4566.MEDIA_AUDIO.equals(mediaDescription.getType())) { + for (Codec codec: mediaDescription.getCodecs()) { + if (supportedCodecs.contains(codec)) { + codecs.add(codec); + } + } + } + } + } + MediaDescription mediaDescription = new MediaDescription(); + Hashtable attributes = new Hashtable(); + attributes.put(RFC4566.ATTR_SENDRECV, ""); + mediaDescription.setAttributes(attributes); + mediaDescription.setType(RFC4566.MEDIA_AUDIO); + mediaDescription.setPort(localRtpPort); + mediaDescription.setCodecs(codecs); + List mediaDescriptions = + new ArrayList(); + mediaDescriptions.add(mediaDescription); + sessionDescription.setMediaDescriptions(mediaDescriptions); + return sessionDescription; + } + +} diff --git a/src/net/sourceforge/peers/sdp/SDPMessage.java b/src/net/sourceforge/peers/sdp/SDPMessage.java new file mode 100644 index 0000000..d3f5139 --- /dev/null +++ b/src/net/sourceforge/peers/sdp/SDPMessage.java @@ -0,0 +1,25 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +public class SDPMessage { + + +} diff --git a/src/net/sourceforge/peers/sdp/SdpLine.java b/src/net/sourceforge/peers/sdp/SdpLine.java new file mode 100644 index 0000000..c50ae9d --- /dev/null +++ b/src/net/sourceforge/peers/sdp/SdpLine.java @@ -0,0 +1,38 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +public class SdpLine { + private char type; + private String value; + public char getType() { + return type; + } + public void setType(char type) { + this.type = type; + } + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } + +} diff --git a/src/net/sourceforge/peers/sdp/SdpParser.java b/src/net/sourceforge/peers/sdp/SdpParser.java new file mode 100644 index 0000000..5004fef --- /dev/null +++ b/src/net/sourceforge/peers/sdp/SdpParser.java @@ -0,0 +1,236 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import net.sourceforge.peers.rtp.RFC3551; + +public class SdpParser { + + public SessionDescription parse(byte[] body) throws IOException { + if (body == null || body.length == 0) { + return null; + } + ByteArrayInputStream in = new ByteArrayInputStream(body); + InputStreamReader inputStreamReader = new InputStreamReader(in); + BufferedReader reader = new BufferedReader(inputStreamReader); + SessionDescription sessionDescription = new SessionDescription(); + + //version + + String line = reader.readLine(); + if (line.length() < 3) { + return null; + } + if (line.charAt(0) != RFC4566.TYPE_VERSION + || line.charAt(1) != RFC4566.SEPARATOR + || line.charAt(2) != RFC4566.VERSION) { + return null; + } + + //origin + + line = reader.readLine(); + if (line.length() < 3) { + return null; + } + if (line.charAt(0) != RFC4566.TYPE_ORIGIN + || line.charAt(1) != RFC4566.SEPARATOR) { + return null; + } + line = line.substring(2); + String[] originArr = line.split(" "); + if (originArr == null || originArr.length != 6) { + return null; + } + sessionDescription.setUsername(originArr[0]); + sessionDescription.setId(Long.parseLong(originArr[1])); + sessionDescription.setVersion(Long.parseLong(originArr[2])); + sessionDescription.setIpAddress(InetAddress.getByName(originArr[5])); + + //name + + line = reader.readLine(); + if (line.length() < 3) { + return null; + } + if (line.charAt(0) != RFC4566.TYPE_SUBJECT + || line.charAt(1) != RFC4566.SEPARATOR) { + return null; + } + sessionDescription.setName(line.substring(2)); + + //session connection and attributes + Hashtable sessionAttributes = new Hashtable(); + sessionDescription.setAttributes(sessionAttributes); + + while ((line = reader.readLine()) != null + && line.charAt(0) != RFC4566.TYPE_MEDIA) { + if (line.length() > 3 + && line.charAt(0) == RFC4566.TYPE_CONNECTION + && line.charAt(1) == RFC4566.SEPARATOR) { + String connection = parseConnection(line.substring(2)); + if (connection == null) { + continue; + } + sessionDescription.setIpAddress(InetAddress.getByName(connection)); + } else if (line.length() > 3 + && line.charAt(0) == RFC4566.TYPE_ATTRIBUTE + && line.charAt(1) == RFC4566.SEPARATOR) { + String value = line.substring(2); + int pos = value.indexOf(RFC4566.ATTR_SEPARATOR); + if (pos > -1) { + sessionAttributes.put(value.substring(0, pos), + value.substring(pos + 1)); + } else { + sessionAttributes.put(value, ""); + } + } + } + if (line == null) { + return null; + } + //we are at the first media line + + ArrayList mediaLines = new ArrayList(); + do { + if (line.length() < 3) { + return null; + } + if (line.charAt(1) != RFC4566.SEPARATOR) { + return null; + } + SdpLine mediaLine = new SdpLine(); + mediaLine.setType(line.charAt(0)); + mediaLine.setValue(line.substring(2)); + mediaLines.add(mediaLine); + } + while ((line = reader.readLine()) != null); + + ArrayList mediaDescriptions = new ArrayList(); + sessionDescription.setMediaDescriptions(mediaDescriptions); + + for (SdpLine sdpLine : mediaLines) { + MediaDescription mediaDescription; + if (sdpLine.getType() == RFC4566.TYPE_MEDIA) { + String[] mediaArr = sdpLine.getValue().split(" "); + if (mediaArr == null || mediaArr.length < 4) { + return null; + } + mediaDescription = new MediaDescription(); + mediaDescription.setType(mediaArr[0]); + //TODO manage port range + mediaDescription.setPort(Integer.parseInt(mediaArr[1])); + mediaDescription.setAttributes(new Hashtable()); + List codecs = new ArrayList(); + for (int i = 3; i < mediaArr.length; ++i) { + int payloadType = Integer.parseInt(mediaArr[i]); + Codec codec = new Codec(); + codec.setPayloadType(payloadType); + String name; + switch (payloadType) { + case RFC3551.PAYLOAD_TYPE_PCMU: + name = RFC3551.PCMU; + break; + case RFC3551.PAYLOAD_TYPE_PCMA: + name = RFC3551.PCMA; + break; + default: + name = "unsupported"; + break; + } + codec.setName(name); + codecs.add(codec); + //TODO check that sdp offer without rtpmap works + } + mediaDescription.setCodecs(codecs); + mediaDescriptions.add(mediaDescription); + } else { + mediaDescription = mediaDescriptions.get(mediaDescriptions.size() - 1); + String sdpLineValue = sdpLine.getValue(); + if (sdpLine.getType() == RFC4566.TYPE_CONNECTION) { + String ipAddress = parseConnection(sdpLineValue); + mediaDescription.setIpAddress(InetAddress.getByName(ipAddress)); + } else if (sdpLine.getType() == RFC4566.TYPE_ATTRIBUTE) { + Hashtable attributes = mediaDescription.getAttributes(); + int pos = sdpLineValue.indexOf(RFC4566.ATTR_SEPARATOR); + if (pos > -1) { + String name = sdpLineValue.substring(0, pos); + String value = sdpLineValue.substring(pos + 1); + pos = value.indexOf(" "); + if (pos > -1) { + int payloadType; + try { + payloadType = Integer.parseInt(value.substring(0, pos)); + List codecs = mediaDescription.getCodecs(); + for (Codec codec: codecs) { + if (codec.getPayloadType() == payloadType) { + value = value.substring(pos + 1); + pos = value.indexOf("/"); + if (pos > -1) { + value = value.substring(0, pos); + codec.setName(value); + } + break; + } + } + } catch (NumberFormatException e) { + attributes.put(name, value); + } + } else { + attributes.put(name, value); + } + } else { + attributes.put(sdpLineValue, ""); + } + } + } + } + sessionDescription.setMediaDescriptions(mediaDescriptions); + + for (MediaDescription description : mediaDescriptions) { + if (description.getIpAddress() == null) { + InetAddress sessionAddress = sessionDescription.getIpAddress(); + if (sessionAddress == null) { + return null; + } + description.setIpAddress(sessionAddress); + } + } + return sessionDescription; + } + + private String parseConnection(String line) { + String[] connectionArr = line.split(" "); + if (connectionArr == null || connectionArr.length != 3) { + return null; + } + return connectionArr[2]; + } + +} diff --git a/src/net/sourceforge/peers/sdp/SessionDescription.java b/src/net/sourceforge/peers/sdp/SessionDescription.java new file mode 100644 index 0000000..8131407 --- /dev/null +++ b/src/net/sourceforge/peers/sdp/SessionDescription.java @@ -0,0 +1,129 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sdp; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Hashtable; +import java.util.List; + +public class SessionDescription { + + private long id; + private long version; + private String name; + private String username; + private InetAddress ipAddress; + private List mediaDescriptions; + private Hashtable attributes; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public InetAddress getIpAddress() { + return ipAddress; + } + + public void setIpAddress(InetAddress ipAddress) { + this.ipAddress = ipAddress; + } + + public List getMediaDescriptions() { + return mediaDescriptions; + } + + public void setMediaDescriptions(List mediaDescriptions) { + this.mediaDescriptions = mediaDescriptions; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + public Hashtable getAttributes() { + return attributes; + } + + public void setAttributes(Hashtable attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("v=0\r\n"); + buf.append("o=").append(username).append(" ").append(id); + buf.append(" ").append(version); + int ipVersion; + if (ipAddress instanceof Inet4Address) { + ipVersion = 4; + } else if (ipAddress instanceof Inet6Address) { + ipVersion = 6; + } else { + throw new RuntimeException("unknown ip version: " + ipAddress); + } + buf.append(" IN IP").append(ipVersion).append(" "); + String hostAddress = ipAddress.getHostAddress(); + buf.append(hostAddress).append("\r\n"); + buf.append("s=").append(name).append("\r\n"); + buf.append("c=IN IP").append(ipVersion).append(" "); + buf.append(hostAddress).append("\r\n"); + buf.append("t=0 0\r\n"); + for (String attributeName: attributes.keySet()) { + String attributeValue = attributes.get(attributeName); + buf.append("a=").append(attributeName); + if (attributeValue != null && !"".equals(attributeValue.trim())) { + buf.append(":"); + buf.append(attributeValue); + buf.append("\r\n"); + } + } + for (MediaDescription mediaDescription: mediaDescriptions) { + buf.append(mediaDescription.toString()); + } + return buf.toString(); + } +} diff --git a/src/net/sourceforge/peers/sip/AbstractState.java b/src/net/sourceforge/peers/sip/AbstractState.java new file mode 100644 index 0000000..5f0ca8b --- /dev/null +++ b/src/net/sourceforge/peers/sip/AbstractState.java @@ -0,0 +1,43 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip; + +import net.sourceforge.peers.Logger; + +public abstract class AbstractState { + + protected String id; + protected Logger logger; + + public AbstractState(String id, Logger logger) { + this.id = id; + this.logger = logger; + } + + public void log(AbstractState state) { + StringBuffer buf = new StringBuffer(); + buf.append("SM ").append(id).append(" ["); + buf.append(JavaUtils.getShortClassName(this.getClass())).append(" -> "); + buf.append(JavaUtils.getShortClassName(state.getClass())).append("] "); + buf.append(new Exception().getStackTrace()[1].getMethodName()); + if (logger!=null) logger.debug(buf.toString()); + } + +} diff --git a/src/net/sourceforge/peers/sip/JavaUtils.java b/src/net/sourceforge/peers/sip/JavaUtils.java new file mode 100644 index 0000000..fb2b439 --- /dev/null +++ b/src/net/sourceforge/peers/sip/JavaUtils.java @@ -0,0 +1,29 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip; + +public class JavaUtils { + + public static String getShortClassName(Class c) { + String name = c.getName(); + return name.substring(name.lastIndexOf('.') + 1); + } + +} diff --git a/src/net/sourceforge/peers/sip/RFC2617.java b/src/net/sourceforge/peers/sip/RFC2617.java new file mode 100644 index 0000000..cf41032 --- /dev/null +++ b/src/net/sourceforge/peers/sip/RFC2617.java @@ -0,0 +1,47 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package net.sourceforge.peers.sip; + +public class RFC2617 { + + // SCHEMES + + public static final String SCHEME_DIGEST = "Digest"; + + // PARAMETERS + + public static final String PARAM_NONCE = "nonce"; + public static final String PARAM_OPAQUE = "opaque"; + public static final String PARAM_REALM = "realm"; + public static final String PARAM_RESPONSE = "response"; + public static final String PARAM_URI = "uri"; + public static final String PARAM_USERNAME = "username"; + public static final String PARAM_QOP = "qop"; + public static final String PARAM_CNONCE = "cnonce"; + public static final String PARAM_NC = "nc"; + public static final String PARAM_ALGORITHM= "algorithm"; + + // MISCELLANEOUS + + public static final char PARAM_SEPARATOR = ','; + public static final char PARAM_VALUE_SEPARATOR = '='; + public static final char PARAM_VALUE_DELIMITER = '"'; + public static final char DIGEST_SEPARATOR = ':'; +} diff --git a/src/net/sourceforge/peers/sip/RFC3261.java b/src/net/sourceforge/peers/sip/RFC3261.java new file mode 100644 index 0000000..94c288b --- /dev/null +++ b/src/net/sourceforge/peers/sip/RFC3261.java @@ -0,0 +1,169 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip; + +public final class RFC3261 { + + //SYNTAX ENCODING + + //HEADERS + + //Methods + + public static final String METHOD_INVITE = "INVITE"; + public static final String METHOD_ACK = "ACK"; + public static final String METHOD_REGISTER = "REGISTER"; + public static final String METHOD_BYE = "BYE"; + public static final String METHOD_OPTIONS = "OPTIONS"; + public static final String METHOD_CANCEL = "CANCEL"; + + //Classical form + + public static final String HDR_ALLOW = "Allow"; + public static final String HDR_AUTHORIZATION = "Authorization"; + public static final String HDR_CALLID = "Call-ID"; + public static final String HDR_CONTACT = "Contact"; + public static final String HDR_CONTENT_ENCODING = "Content-Encoding"; + public static final String HDR_CONTENT_LENGTH = "Content-Length"; + public static final String HDR_CONTENT_TYPE = "Content-Type"; + public static final String HDR_CSEQ = "CSeq"; + public static final String HDR_FROM = "From"; + public static final String HDR_MAX_FORWARDS = "Max-Forwards"; + public static final String HDR_RECORD_ROUTE = "Record-Route"; + public static final String HDR_PROXY_AUTHENTICATE = "Proxy-Authenticate"; + public static final String HDR_PROXY_AUTHORIZATION = "Proxy-Authorization"; + public static final String HDR_ROUTE = "Route"; + public static final String HDR_SUBJECT = "Subject"; + public static final String HDR_SUPPORTED = "Supported"; + public static final String HDR_TO = "To"; + public static final String HDR_VIA = "Via"; + public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; + + //Compact form + + public static final char COMPACT_HDR_CALLID = 'i'; + public static final char COMPACT_HDR_CONTACT = 'm'; + public static final char COMPACT_HDR_CONTENT_ENCODING = 'e'; + public static final char COMPACT_HDR_CONTENT_LENGTH = 'l'; + public static final char COMPACT_HDR_CONTENT_TYPE = 'c'; + public static final char COMPACT_HDR_FROM = 'f'; + public static final char COMPACT_HDR_SUBJECT = 's'; + public static final char COMPACT_HDR_SUPPORTED = 'k'; + public static final char COMPACT_HDR_TO = 't'; + public static final char COMPACT_HDR_VIA = 'v'; + + //Parameters + + public static final String PARAM_BRANCH = "branch"; + public static final String PARAM_EXPIRES = "expires"; + public static final String PARAM_MADDR = "maddr"; + public static final String PARAM_RECEIVED = "received"; + public static final String PARAM_RPORT = "rport"; + public static final String PARAM_SENTBY = "sent-by"; + public static final String PARAM_TAG = "tag"; + public static final String PARAM_TRANSPORT = "transport"; + public static final String PARAM_TTL = "ttl"; + + public static final String PARAM_SEPARATOR = ";"; + public static final String PARAM_ASSIGNMENT = "="; + + //Miscellaneous + + public static final char FIELD_NAME_SEPARATOR = ':'; + public static final String DEFAULT_SIP_VERSION = "SIP/2.0"; + public static final String CRLF = "\r\n"; + public static final String IPV4_TTL = "1"; + public static final char AT = '@'; + public static final String LOOSE_ROUTING = "lr"; + public static final char LEFT_ANGLE_BRACKET = '<'; + public static final char RIGHT_ANGLE_BRACKET = '>'; + public static final String HEADER_SEPARATOR = ","; + + //STATUS CODES + public static final int CODE_MIN_PROV = 100; + public static final int CODE_MIN_SUCCESS = 200; + public static final int CODE_MIN_REDIR = 300; + public static final int CODE_MAX = 699; + + public static final int CODE_100_TRYING = 100; + public static final int CODE_180_RINGING = 180; + public static final int CODE_200_OK = 200; + public static final int CODE_401_UNAUTHORIZED = 401; + public static final int CODE_405_METHOD_NOT_ALLOWED = 405; + public static final int CODE_407_PROXY_AUTHENTICATION_REQUIRED = 407; + public static final int CODE_481_CALL_TRANSACTION_DOES_NOT_EXIST = 481; + public static final int CODE_486_BUSYHERE = 486; + public static final int CODE_487_REQUEST_TERMINATED = 487; + public static final int CODE_500_SERVER_INTERNAL_ERROR = 500; + + //REASON PHRASES + public static final String REASON_180_RINGING = "Ringing"; + public static final String REASON_200_OK = "OK"; + public static final String REASON_405_METHOD_NOT_ALLOWED = + "Method Not Allowed"; + public static final String REASON_481_CALL_TRANSACTION_DOES_NOT_EXIST = + "Call/Transaction Does Not Exist"; + public static final String REASON_486_BUSYHERE = "Busy Here"; + public static final String REASON_487_REQUEST_TERMINATED = + "Request Terminated"; + public static final String REASON_500_SERVER_INTERNAL_ERROR = + "Server Internal Error"; + + //TRANSPORT + + public static final String TRANSPORT_UDP = "UDP"; + public static final String TRANSPORT_TCP = "TCP"; + public static final String TRANSPORT_SCTP = "SCTP"; + public static final String TRANSPORT_TLS = "TLS"; + public static final int TRANSPORT_UDP_USUAL_MAX_SIZE = 1300; + public static final int TRANSPORT_UDP_MAX_SIZE = 65535; + public static final char TRANSPORT_VIA_SEP = '/'; + public static final char TRANSPORT_VIA_SEP2 = ' '; + public static final int TRANSPORT_DEFAULT_PORT = 5060; + public static final int TRANSPORT_TLS_PORT = 5061; + public static final char TRANSPORT_PORT_SEP = ':'; + + + //TRANSACTION + + + //TRANSACTION USER + + public static final int DEFAULT_MAXFORWARDS = 70; + public static final String BRANCHID_MAGIC_COOKIE = "z9hG4bK"; + public static final String SIP_SCHEME = "sip"; + public static final char SCHEME_SEPARATOR = ':'; + + //TIMERS (in milliseconds) + + public static final int TIMER_T1 = 500; + public static final int TIMER_T2 = 4000; + public static final int TIMER_T4 = 5000; + public static final int TIMER_INVITE_CLIENT_TRANSACTION = 32000; + + + //TRANSACTION USER + + + //CORE + + public static final String CONTENT_TYPE_SDP = "application/sdp"; + +} diff --git a/src/net/sourceforge/peers/sip/Utils.java b/src/net/sourceforge/peers/sip/Utils.java new file mode 100644 index 0000000..9282333 --- /dev/null +++ b/src/net/sourceforge/peers/sip/Utils.java @@ -0,0 +1,128 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip; + + +import java.net.InetAddress; + +import net.sourceforge.peers.sip.core.useragent.UAS; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldMultiValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transport.SipMessage; + + +public class Utils { + + public static final String PEERSHOME_SYSTEM_PROPERTY = "peers.home"; + public static final String DEFAULT_PEERS_HOME = "."; + + public final static SipHeaderFieldValue getTopVia(SipMessage sipMessage) { + SipHeaders sipHeaders = sipMessage.getSipHeaders(); + SipHeaderFieldName viaName = new SipHeaderFieldName(RFC3261.HDR_VIA); + SipHeaderFieldValue via = sipHeaders.get(viaName); + if (via instanceof SipHeaderFieldMultiValue) { + via = ((SipHeaderFieldMultiValue)via).getValues().get(0); + } + return via; + } + + public final static String generateTag() { + return randomString(8); + } + + public final static String generateCallID(InetAddress inetAddress) { + //TODO make a hash using current time millis, public ip @, private @, and a random string + StringBuffer buf = new StringBuffer(); + buf.append(randomString(8)); + buf.append('-'); + buf.append(String.valueOf(System.currentTimeMillis())); + buf.append('@'); + buf.append(inetAddress.getHostName()); + return buf.toString(); + } + + public final static String generateBranchId() { + StringBuffer buf = new StringBuffer(); + buf.append(RFC3261.BRANCHID_MAGIC_COOKIE); + //TODO must be unique across space and time... + buf.append(randomString(9)); + return buf.toString(); + } + + public final static String getMessageCallId(SipMessage sipMessage) { + SipHeaderFieldValue callId = sipMessage.getSipHeaders().get( + new SipHeaderFieldName(RFC3261.HDR_CALLID)); + return callId.getValue(); + } + + public final static String randomString(int length) { + String chars = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIFKLMNOPRSTUVWXYZ" + + "0123456789"; + StringBuffer buf = new StringBuffer(length); + for (int i = 0; i < length; ++i) { + int pos = (int)Math.round(Math.random() * (chars.length() - 1)); + buf.append(chars.charAt(pos)); + } + return buf.toString(); + } + + public final static void copyHeader(SipMessage src, SipMessage dst, String name) { + SipHeaderFieldName sipHeaderFieldName = new SipHeaderFieldName(name); + SipHeaderFieldValue sipHeaderFieldValue = src.getSipHeaders().get(sipHeaderFieldName); + if (sipHeaderFieldValue != null) { + dst.getSipHeaders().add(sipHeaderFieldName, sipHeaderFieldValue); + } + } + + public final static String getUserPart(String sipUri) { + int start = sipUri.indexOf(RFC3261.SCHEME_SEPARATOR); + int end = sipUri.indexOf(RFC3261.AT); + return sipUri.substring(start + 1, end); + } + + /** + * adds Max-Forwards Supported and Require headers + * @param headers + */ + public final static void addCommonHeaders(SipHeaders headers) { + //Max-Forwards + + headers.add(new SipHeaderFieldName(RFC3261.HDR_MAX_FORWARDS), + new SipHeaderFieldValue( + String.valueOf(RFC3261.DEFAULT_MAXFORWARDS))); + + //TODO Supported and Require + } + + public final static String generateAllowHeader() { + StringBuffer buf = new StringBuffer(); + for (String supportedMethod: UAS.SUPPORTED_METHODS) { + buf.append(supportedMethod); + buf.append(", "); + } + int bufLength = buf.length(); + buf.delete(bufLength - 2, bufLength); + return buf.toString(); + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/ChallengeManager.java b/src/net/sourceforge/peers/sip/core/useragent/ChallengeManager.java new file mode 100644 index 0000000..d0a54d2 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/ChallengeManager.java @@ -0,0 +1,299 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2008-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import java.util.UUID; +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC2617; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; + +public class ChallengeManager implements MessageInterceptor { + + public static final String ALGORITHM_MD5 = "MD5"; + + private String username; + private String password; + private String realm; + private String nonce; + private String opaque; + private String requestUri; + private String digest; + private String profileUri; + private String qop; + private String cnonce; + + private static volatile int nonceCount = 1; + private String nonceCountHex; + + private Config config; + private Logger logger; + + // FIXME what happens if a challenge is received for a register-refresh + // and another challenge is received in the mean time for an invite? + private int statusCode; + private SipHeaderFieldValue contact; + + private InitialRequestManager initialRequestManager; + private MidDialogRequestManager midDialogRequestManager; + private DialogManager dialogManager; + + public ChallengeManager(Config config, + InitialRequestManager initialRequestManager, + MidDialogRequestManager midDialogRequestManager, + DialogManager dialogManager, Logger logger) { + this.config = config; + this.initialRequestManager = initialRequestManager; + this.midDialogRequestManager = midDialogRequestManager; + this.dialogManager = dialogManager; + this.logger = logger; + init(); + } + + private void init() { + username = config.getUserPart(); + password = config.getPassword(); + profileUri = RFC3261.SIP_SCHEME + RFC3261.SCHEME_SEPARATOR + + username + RFC3261.AT + config.getDomain(); + } + + private String md5hash(String message) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance(ALGORITHM_MD5); + } catch (NoSuchAlgorithmException e) { + if (logger!=null) logger.error("no such algorithm " + ALGORITHM_MD5, e); + return null; + } + byte[] messageBytes = message.getBytes(); + byte[] messageMd5 = messageDigest.digest(messageBytes); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(out); + for (byte b : messageMd5) { + int u_b = (b < 0) ? 256 + b : b; + printStream.printf("%02x", u_b); + } + return out.toString(); + } + + public void handleChallenge(SipRequest sipRequest, + SipResponse sipResponse) { + init(); + statusCode = sipResponse.getStatusCode(); + SipHeaders responseHeaders = sipResponse.getSipHeaders(); + SipHeaders requestHeaders = sipRequest.getSipHeaders(); + contact = requestHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CONTACT)); + SipHeaderFieldValue authenticate; + SipHeaderFieldName authenticateHeaderName; + if (statusCode == RFC3261.CODE_401_UNAUTHORIZED) { + authenticateHeaderName = new SipHeaderFieldName( + RFC3261.HDR_WWW_AUTHENTICATE); + } else if (statusCode == RFC3261.CODE_407_PROXY_AUTHENTICATION_REQUIRED) { + authenticateHeaderName = new SipHeaderFieldName( + RFC3261.HDR_PROXY_AUTHENTICATE); + } else { + return; + } + authenticate = responseHeaders.get(authenticateHeaderName); + if (authenticate == null) { + return; + } + if (!authenticate.getValue().startsWith(RFC2617.SCHEME_DIGEST)) { + logger.info("unsupported challenge scheme in header: " + + authenticate); + return; + } + String headerValue = authenticate.getValue(); + realm = getParameter(RFC2617.PARAM_REALM, headerValue); + nonce = getParameter(RFC2617.PARAM_NONCE, headerValue); + opaque = getParameter(RFC2617.PARAM_OPAQUE, headerValue); + qop = getParameter(RFC2617.PARAM_QOP, headerValue); + if( "auth".equals(qop)) { + nonceCountHex = String.format("%08X", nonceCount++); + } + String method = sipRequest.getMethod(); + requestUri = sipRequest.getRequestUri().toString(); + cnonce = UUID.randomUUID().toString(); + digest = getRequestDigest(method); + + // FIXME message should be copied "as is" not created anew from scratch + // and this technique is not clean + String callId = responseHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CALLID)).getValue(); + Dialog dialog = dialogManager.getDialog(callId); + if (dialog != null) { + midDialogRequestManager.generateMidDialogRequest( + dialog, RFC3261.METHOD_BYE, this); + } else { + SipHeaderFieldValue from = requestHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_FROM)); + String fromTag = from.getParam(new SipHeaderParamName( + RFC3261.PARAM_TAG)); + try { + initialRequestManager.createInitialRequest( + requestUri, method, profileUri, callId, fromTag, this); + } catch (SipUriSyntaxException e) { + if (logger!=null) logger.error("syntax error", e); + } + } + } + + private String getRequestDigest(String method) { + StringBuffer buf = new StringBuffer(); + buf.append(username); + buf.append(RFC2617.DIGEST_SEPARATOR); + buf.append(realm); + buf.append(RFC2617.DIGEST_SEPARATOR); + buf.append(password); + String ha1 = md5hash(buf.toString()); + buf = new StringBuffer(); + buf.append(method); + buf.append(RFC2617.DIGEST_SEPARATOR); + buf.append(requestUri); + String ha2 = md5hash(buf.toString()); + buf = new StringBuffer(); + buf.append(ha1); + buf.append(RFC2617.DIGEST_SEPARATOR); + buf.append(nonce); + buf.append(RFC2617.DIGEST_SEPARATOR); + if("auth".equals(qop)) { + buf.append(nonceCountHex); + buf.append(RFC2617.DIGEST_SEPARATOR); + buf.append(cnonce); + buf.append(RFC2617.DIGEST_SEPARATOR); + buf.append(qop); + buf.append(RFC2617.DIGEST_SEPARATOR); + } + buf.append(ha2); + return md5hash(buf.toString()); + } + + private String getParameter(String paramName, String header) { + int paramPos = header.indexOf(paramName); + if (paramPos < 0) { + return null; + } + int paramNameLength = paramName.length(); + if (paramPos + paramNameLength + 3 > header.length()) { + logger.info("Malformed " + RFC3261.HDR_WWW_AUTHENTICATE + " header"); + return null; + } + if (header.charAt(paramPos + paramNameLength) != + RFC2617.PARAM_VALUE_SEPARATOR) { + logger.info("Malformed " + RFC3261.HDR_WWW_AUTHENTICATE + " header"); + return null; + } + if (header.charAt(paramPos + paramNameLength + 1) != + RFC2617.PARAM_VALUE_DELIMITER) { + logger.info("Malformed " + RFC3261.HDR_WWW_AUTHENTICATE + " header"); + return null; + } + header = header.substring(paramPos + paramNameLength + 2); + int endDelimiter = header.indexOf(RFC2617.PARAM_VALUE_DELIMITER); + if (endDelimiter < 0) { + logger.info("Malformed " + RFC3261.HDR_WWW_AUTHENTICATE + " header"); + return null; + } + return header.substring(0, endDelimiter); + } + + /** add xxxAuthorization header */ + public void postProcess(SipMessage sipMessage) { + if (realm == null || nonce == null || digest == null) { + return; + } + SipHeaders sipHeaders = sipMessage.getSipHeaders(); + String cseq = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CSEQ)).getValue(); + String method = cseq.substring(cseq.trim().lastIndexOf(' ') + 1); + digest = getRequestDigest(method); + StringBuffer buf = new StringBuffer(); + buf.append(RFC2617.SCHEME_DIGEST).append(" "); + appendParameter(buf, RFC2617.PARAM_USERNAME, username); + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_REALM, realm); + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_NONCE, nonce); + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_URI, requestUri); + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_RESPONSE, digest); + if("auth".equals(qop)) { + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_NC, nonceCountHex); + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_CNONCE, cnonce); + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_QOP, qop); + } + if (opaque != null) { + buf.append(RFC2617.PARAM_SEPARATOR).append(" "); + appendParameter(buf, RFC2617.PARAM_OPAQUE, opaque); + } + SipHeaderFieldName authorizationName; + if (statusCode == RFC3261.CODE_401_UNAUTHORIZED) { + authorizationName = new SipHeaderFieldName( + RFC3261.HDR_AUTHORIZATION); + } else if (statusCode == RFC3261.CODE_407_PROXY_AUTHENTICATION_REQUIRED) { + authorizationName = new SipHeaderFieldName( + RFC3261.HDR_PROXY_AUTHORIZATION); + } else { + return; + } + sipHeaders.add(authorizationName, + new SipHeaderFieldValue(buf.toString())); + // manage authentication on unregister challenge... + if (contact != null) { + SipHeaderParamName expiresName = + new SipHeaderParamName(RFC3261.PARAM_EXPIRES); + String expiresString = contact.getParam(expiresName); + if (expiresString != null && Integer.parseInt(expiresString) == 0) { + SipHeaderFieldValue requestContact = + sipHeaders.get(new SipHeaderFieldName(RFC3261.HDR_CONTACT)); + requestContact.addParam(expiresName, expiresString); + } + } + } + + private void appendParameter(StringBuffer buf, String name, String value) { + buf.append(name); + buf.append(RFC2617.PARAM_VALUE_SEPARATOR); + buf.append(RFC2617.PARAM_VALUE_DELIMITER); + buf.append(value); + buf.append(RFC2617.PARAM_VALUE_DELIMITER); + } + + } diff --git a/src/net/sourceforge/peers/sip/core/useragent/InitialRequestManager.java b/src/net/sourceforge/peers/sip/core/useragent/InitialRequestManager.java new file mode 100644 index 0000000..261df58 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/InitialRequestManager.java @@ -0,0 +1,312 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.core.useragent.handlers.ByeHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.CancelHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.InviteHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.OptionsHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.RegisterHandler; +import net.sourceforge.peers.sip.syntaxencoding.NameAddress; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransactionUser; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class InitialRequestManager extends RequestManager + implements ServerTransactionUser { + + public InitialRequestManager(UserAgent userAgent, + InviteHandler inviteHandler, + CancelHandler cancelHandler, + ByeHandler byeHandler, + OptionsHandler optionsHandler, + RegisterHandler registerHandler, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, + Logger logger) { + super(userAgent, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager, + logger); + registerHandler.setInitialRequestManager(this); + } + + /** + * gives a new request outside of a dialog + * + * @param requestUri + * @param method + * @return + * @throws SipUriSyntaxException + */ + public SipRequest getGenericRequest(String requestUri, String method, + String profileUri, String callId, String fromTag) + throws SipUriSyntaxException { + //8.1.1 + SipRequest request = new SipRequest(method, new SipURI(requestUri)); + SipHeaders headers = request.getSipHeaders(); + //String hostAddress = utils.getMyAddress().getHostAddress(); + + //Via + + //TODO no Via should be added directly by UAC, Via is normally added by Transaction layer + +// StringBuffer viaBuf = new StringBuffer(); +// viaBuf.append(RFC3261.DEFAULT_SIP_VERSION); +// // TODO choose real transport +// viaBuf.append("/UDP "); +// viaBuf.append(hostAddress); +// SipHeaderFieldValue via = new SipHeaderFieldValue(viaBuf.toString()); +// via.addParam(new SipHeaderParamName(RFC3261.PARAM_BRANCHID), +// utils.generateBranchId()); +// headers.add(new SipHeaderFieldName(RFC3261.HDR_VIA), via); + + Utils.addCommonHeaders(headers); + + //To + + NameAddress to = new NameAddress(requestUri); + headers.add(new SipHeaderFieldName(RFC3261.HDR_TO), + new SipHeaderFieldValue(to.toString())); + + //From + + NameAddress fromNA = new NameAddress(profileUri); + SipHeaderFieldValue from = new SipHeaderFieldValue(fromNA.toString()); + String localFromTag; + if (fromTag != null) { + localFromTag = fromTag; + } else { + localFromTag = Utils.generateTag(); + } + from.addParam(new SipHeaderParamName(RFC3261.PARAM_TAG), localFromTag); + headers.add(new SipHeaderFieldName(RFC3261.HDR_FROM), from); + + //Call-ID + + SipHeaderFieldName callIdName = + new SipHeaderFieldName(RFC3261.HDR_CALLID); + String localCallId; + if (callId != null) { + localCallId = callId; + } else { + localCallId = Utils.generateCallID( + userAgent.getConfig().getLocalInetAddress()); + } + headers.add(callIdName, new SipHeaderFieldValue(localCallId)); + + //CSeq + + headers.add(new SipHeaderFieldName(RFC3261.HDR_CSEQ), + new SipHeaderFieldValue(userAgent.generateCSeq(method))); + + return request; + } + + public SipRequest createInitialRequest(String requestUri, String method, + String profileUri) throws SipUriSyntaxException { + return createInitialRequest(requestUri, method, profileUri, null); + } + + public SipRequest createInitialRequest(String requestUri, String method, + String profileUri, String callId) throws SipUriSyntaxException { + + return createInitialRequest(requestUri, method, profileUri, callId, + null, null); + } + + public SipRequest createInitialRequest(String requestUri, String method, + String profileUri, String callId, String fromTag, + MessageInterceptor messageInterceptor) + throws SipUriSyntaxException { + + SipRequest sipRequest = getGenericRequest(requestUri, method, + profileUri, callId, fromTag); + + // TODO add route header for outbound proxy give it to xxxHandler to create + // clientTransaction + SipURI outboundProxy = userAgent.getOutboundProxy(); + if (outboundProxy != null) { + NameAddress outboundProxyNameAddress = + new NameAddress(outboundProxy.toString()); + sipRequest.getSipHeaders().add(new SipHeaderFieldName(RFC3261.HDR_ROUTE), + new SipHeaderFieldValue(outboundProxyNameAddress.toString()), 0); + } + ClientTransaction clientTransaction = null; + if (RFC3261.METHOD_INVITE.equals(method)) { + clientTransaction = inviteHandler.preProcessInvite(sipRequest); + } else if (RFC3261.METHOD_REGISTER.equals(method)) { + clientTransaction = registerHandler.preProcessRegister(sipRequest); + } + createInitialRequestEnd(sipRequest, clientTransaction, profileUri, + messageInterceptor, true); + return sipRequest; + } + + private void createInitialRequestEnd(SipRequest sipRequest, + ClientTransaction clientTransaction, String profileUri, + MessageInterceptor messageInterceptor, boolean addContact) { + if (clientTransaction == null) { + if (logger!=null) logger.error("method not supported"); + return; + } + if (addContact) { + addContact(sipRequest, clientTransaction.getContact(), profileUri); + } + if (messageInterceptor != null) { + messageInterceptor.postProcess(sipRequest); + } + // TODO create message receiver on client transport port + clientTransaction.start(); + } + + public void createCancel(SipRequest inviteRequest, + MidDialogRequestManager midDialogRequestManager, String profileUri) { + SipHeaders inviteHeaders = inviteRequest.getSipHeaders(); + SipHeaderFieldValue callId = inviteHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CALLID)); + SipRequest sipRequest; + try { + sipRequest = getGenericRequest( + inviteRequest.getRequestUri().toString(), RFC3261.METHOD_CANCEL, + profileUri, callId.getValue(), null); + } catch (SipUriSyntaxException e) { + if (logger!=null) logger.error("syntax error", e); + return; + } + + ClientTransaction clientTransaction = null; + clientTransaction = cancelHandler.preProcessCancel(sipRequest, + inviteRequest, midDialogRequestManager); + if (clientTransaction != null) { + createInitialRequestEnd(sipRequest, clientTransaction, profileUri, + null, false); + } + + + } + + public void manageInitialRequest(SipRequest sipRequest) { + SipHeaders headers = sipRequest.getSipHeaders(); + + // TODO authentication + + //method inspection + SipResponse sipResponse = null; + if (!UAS.SUPPORTED_METHODS.contains(sipRequest.getMethod())) { + //TODO generate 405 (using 8.2.6 &) with Allow header + //(20.5) and send it + sipResponse = generateResponse(sipRequest, null, + RFC3261.CODE_405_METHOD_NOT_ALLOWED, + RFC3261.REASON_405_METHOD_NOT_ALLOWED); + SipHeaders sipHeaders = sipResponse.getSipHeaders(); + sipHeaders.add(new SipHeaderFieldName(RFC3261.HDR_ALLOW), + new SipHeaderFieldValue(Utils.generateAllowHeader())); + } + + + SipHeaderFieldValue contentType = + headers.get(new SipHeaderFieldName(RFC3261.HDR_CONTENT_TYPE)); + if (contentType != null) { + if (!RFC3261.CONTENT_TYPE_SDP.equals(contentType.getValue())) { + //TODO generate 415 with a Accept header listing supported content types + //8.2.3 + } + } + + + //etc. + + if (sipResponse != null) { + ServerTransaction serverTransaction = + transactionManager.createServerTransaction( + sipResponse, userAgent.getSipPort(), RFC3261.TRANSPORT_UDP, + this, sipRequest); + serverTransaction.start(); + serverTransaction.receivedRequest(sipRequest); + serverTransaction.sendReponse(sipResponse); + } + + //TODO create server transaction + String method = sipRequest.getMethod(); + if (RFC3261.METHOD_INVITE.equals(method)) { + inviteHandler.handleInitialInvite(sipRequest); + } else if (RFC3261.METHOD_CANCEL.equals(method)) { + cancelHandler.handleCancel(sipRequest); + } else if (RFC3261.METHOD_OPTIONS.equals(method)) { + optionsHandler.handleOptions(sipRequest); + } + } + + public void addContact(SipRequest sipRequest, String contactEnd, + String profileUri) { + SipHeaders sipHeaders = sipRequest.getSipHeaders(); + + + + //Contact + + StringBuffer contactBuf = new StringBuffer(); + contactBuf.append(RFC3261.SIP_SCHEME); + contactBuf.append(RFC3261.SCHEME_SEPARATOR); + String userPart = Utils.getUserPart(profileUri); + contactBuf.append(userPart); + contactBuf.append(RFC3261.AT); + contactBuf.append(contactEnd); + + NameAddress contactNA = new NameAddress(contactBuf.toString()); + SipHeaderFieldValue contact = + new SipHeaderFieldValue(contactNA.toString()); + sipHeaders.add(new SipHeaderFieldName(RFC3261.HDR_CONTACT), + new SipHeaderFieldValue(contact.toString())); + } + + /////////////////////////////////////////////////////////// + // ServerTransactionUser methods + /////////////////////////////////////////////////////////// + + @Override + public void transactionFailure() { + // TODO Auto-generated method stub + + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/MessageInterceptor.java b/src/net/sourceforge/peers/sip/core/useragent/MessageInterceptor.java new file mode 100644 index 0000000..107d02a --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/MessageInterceptor.java @@ -0,0 +1,27 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import net.sourceforge.peers.sip.transport.SipMessage; + +public interface MessageInterceptor { + + public void postProcess(SipMessage sipMessage); +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/MidDialogRequestManager.java b/src/net/sourceforge/peers/sip/core/useragent/MidDialogRequestManager.java new file mode 100644 index 0000000..3a0a51d --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/MidDialogRequestManager.java @@ -0,0 +1,253 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Hashtable; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.core.useragent.handlers.ByeHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.CancelHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.InviteHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.OptionsHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.RegisterHandler; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.ClientTransactionUser; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransactionUser; +import net.sourceforge.peers.sip.transaction.Transaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public class MidDialogRequestManager extends RequestManager + implements ClientTransactionUser, ServerTransactionUser { + + public MidDialogRequestManager(UserAgent userAgent, + InviteHandler inviteHandler, + CancelHandler cancelHandler, + ByeHandler byeHandler, + OptionsHandler optionsHandler, + RegisterHandler registerHandler, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, + Logger logger) { + super(userAgent, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager, + logger); + } + + + //////////////////////////////////////////////// + // methods for UAC + //////////////////////////////////////////////// + + public void generateMidDialogRequest(Dialog dialog, + String method, MessageInterceptor messageInterceptor) { + + + SipRequest sipRequest = dialog.buildSubsequentRequest(RFC3261.METHOD_BYE); + + if (RFC3261.METHOD_BYE.equals(method)) { + byeHandler.preprocessBye(sipRequest, dialog); + } + //TODO check that subsequent request is supported before client + //transaction creation + if (!RFC3261.METHOD_INVITE.equals(method)) { + ClientTransaction clientTransaction = createNonInviteClientTransaction( + sipRequest, null, byeHandler); + if (messageInterceptor != null) { + messageInterceptor.postProcess(sipRequest); + } + if (clientTransaction != null) { + clientTransaction.start(); + } + } else { + //TODO client transaction user is managed by invite handler directly + } + + + } + + + public ClientTransaction createNonInviteClientTransaction( + SipRequest sipRequest, String branchId, + ClientTransactionUser clientTransactionUser) { + //8.1.2 + SipURI destinationUri = RequestManager.getDestinationUri(sipRequest, + logger); + + //TODO if header route is present, addrspec = toproute.nameaddress.addrspec + String transport = RFC3261.TRANSPORT_UDP; + Hashtable params = destinationUri.getUriParameters(); + if (params != null) { + String reqUriTransport = params.get(RFC3261.PARAM_TRANSPORT); + if (reqUriTransport != null) { + transport = reqUriTransport; + } + } + int port = destinationUri.getPort(); + if (port == SipURI.DEFAULT_PORT) { + port = RFC3261.TRANSPORT_DEFAULT_PORT; + } + SipURI sipUri = userAgent.getConfig().getOutboundProxy(); + if (sipUri == null) { + sipUri = destinationUri; + } + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(sipUri.getHost()); + } catch (UnknownHostException e) { + if (logger!=null) logger.error("unknown host: " + sipUri.getHost(), e); + return null; + } + ClientTransaction clientTransaction = transactionManager + .createClientTransaction(sipRequest, inetAddress, port, transport, + branchId, clientTransactionUser); + return clientTransaction; + } + + + + + + + + + + + + //////////////////////////////////////////////// + // methods for UAS + //////////////////////////////////////////////// + + public void manageMidDialogRequest(SipRequest sipRequest, Dialog dialog) { + SipHeaders sipHeaders = sipRequest.getSipHeaders(); + SipHeaderFieldValue cseq = + sipHeaders.get(new SipHeaderFieldName(RFC3261.HDR_CSEQ)); + String cseqStr = cseq.getValue(); + int pos = cseqStr.indexOf(' '); + if (pos < 0) { + pos = cseqStr.indexOf('\t'); + } + int newCseq = Integer.parseInt(cseqStr.substring(0, pos)); + int oldCseq = dialog.getRemoteCSeq(); + if (oldCseq == Dialog.EMPTY_CSEQ) { + dialog.setRemoteCSeq(newCseq); + } else if (newCseq < oldCseq) { + // out of order + // RFC3261 12.2.2 p77 + // TODO test out of order in-dialog-requests + SipResponse sipResponse = generateResponse(sipRequest, dialog, + RFC3261.CODE_500_SERVER_INTERNAL_ERROR, + RFC3261.REASON_500_SERVER_INTERNAL_ERROR); + ServerTransaction serverTransaction = + transactionManager.createServerTransaction( + sipResponse, + userAgent.getSipPort(), + RFC3261.TRANSPORT_UDP, + this, sipRequest); + serverTransaction.start(); + serverTransaction.receivedRequest(sipRequest); + serverTransaction.sendReponse(sipResponse); + } else { + dialog.setRemoteCSeq(newCseq); + } + + String method = sipRequest.getMethod(); + if (RFC3261.METHOD_BYE.equals(method)) { + byeHandler.handleBye(sipRequest, dialog); + } else if (RFC3261.METHOD_INVITE.equals(method)) { + inviteHandler.handleReInvite(sipRequest, dialog); + } else if (RFC3261.METHOD_ACK.equals(method)) { + inviteHandler.handleAck(sipRequest, dialog); + } else if (RFC3261.METHOD_OPTIONS.equals(method)) { + optionsHandler.handleOptions(sipRequest); + } + } + + /////////////////////////////////////// + // ServerTransactionUser methods + /////////////////////////////////////// + @Override + public void transactionFailure() { + // TODO Auto-generated method stub + + } + + + /////////////////////////////////////// + // ClientTransactionUser methods + /////////////////////////////////////// + // callbacks employed for cancel responses (ignored) + @Override + public void transactionTimeout(ClientTransaction clientTransaction) { + // TODO Auto-generated method stub + + } + + + @Override + public void provResponseReceived(SipResponse sipResponse, + Transaction transaction) { + // TODO Auto-generated method stub + + } + + + @Override + public void errResponseReceived(SipResponse sipResponse) { + // TODO Auto-generated method stub + + } + + + @Override + public void successResponseReceived(SipResponse sipResponse, + Transaction transaction) { + // TODO Auto-generated method stub + + } + + + @Override + public void transactionTransportError() { + // TODO Auto-generated method stub + + } +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/RequestManager.java b/src/net/sourceforge/peers/sip/core/useragent/RequestManager.java new file mode 100644 index 0000000..d0b091c --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/RequestManager.java @@ -0,0 +1,138 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.core.useragent.handlers.ByeHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.CancelHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.InviteHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.OptionsHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.RegisterHandler; +import net.sourceforge.peers.sip.syntaxencoding.NameAddress; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public abstract class RequestManager { + + public static SipURI getDestinationUri(SipRequest sipRequest, + Logger logger) { + SipHeaders requestHeaders = sipRequest.getSipHeaders(); + SipURI destinationUri = null; + SipHeaderFieldValue route = requestHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_ROUTE)); + if (route != null) { + try { + destinationUri = new SipURI( + NameAddress.nameAddressToUri(route.toString())); + } catch (SipUriSyntaxException e) { + if (logger!=null) logger.error("syntax error", e); + } + } + if (destinationUri == null) { + destinationUri = sipRequest.getRequestUri(); + } + return destinationUri; + } + + public static SipResponse generateResponse(SipRequest sipRequest, + Dialog dialog, int statusCode, String reasonPhrase) { + //8.2.6.2 + SipResponse sipResponse = new SipResponse(statusCode, reasonPhrase); + SipHeaders requestHeaders = sipRequest.getSipHeaders(); + SipHeaders responseHeaders = sipResponse.getSipHeaders(); + SipHeaderFieldName fromName = new SipHeaderFieldName(RFC3261.HDR_FROM); + responseHeaders.add(fromName, requestHeaders.get(fromName)); + SipHeaderFieldName callIdName = new SipHeaderFieldName(RFC3261.HDR_CALLID); + responseHeaders.add(callIdName, requestHeaders.get(callIdName)); + SipHeaderFieldName cseqName = new SipHeaderFieldName(RFC3261.HDR_CSEQ); + responseHeaders.add(cseqName, requestHeaders.get(cseqName)); + SipHeaderFieldName viaName = new SipHeaderFieldName(RFC3261.HDR_VIA); + responseHeaders.add(viaName, requestHeaders.get(viaName));//TODO check ordering + SipHeaderFieldName toName = new SipHeaderFieldName(RFC3261.HDR_TO); + SipHeaderFieldValue toValue = requestHeaders.get(toName); + SipHeaderParamName toTagParamName = new SipHeaderParamName(RFC3261.PARAM_TAG); + String toTag = toValue.getParam(toTagParamName); + if (toTag == null) { + if (dialog != null) { + toTag = dialog.getLocalTag(); + toValue.addParam(toTagParamName, toTag); + } + } + responseHeaders.add(toName, toValue); + return sipResponse; + } + + protected InviteHandler inviteHandler; + protected CancelHandler cancelHandler; + protected ByeHandler byeHandler; + protected OptionsHandler optionsHandler; + protected RegisterHandler registerHandler; + + protected UserAgent userAgent; + protected TransactionManager transactionManager; + protected TransportManager transportManager; + protected Logger logger; + + public RequestManager(UserAgent userAgent, + InviteHandler inviteHandler, + CancelHandler cancelHandler, + ByeHandler byeHandler, + OptionsHandler optionsHandler, + RegisterHandler registerHandler, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, + Logger logger) { + this.userAgent = userAgent; + this.inviteHandler = inviteHandler; + this.cancelHandler = cancelHandler; + this.byeHandler = byeHandler; + this.optionsHandler = optionsHandler; + this.registerHandler = registerHandler; + this.transactionManager = transactionManager; + this.transportManager = transportManager; + this.logger = logger; + } + + public InviteHandler getInviteHandler() { + return inviteHandler; + } + + public ByeHandler getByeHandler() { + return byeHandler; + } + + public RegisterHandler getRegisterHandler() { + return registerHandler; + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/SipEvent.java b/src/net/sourceforge/peers/sip/core/useragent/SipEvent.java new file mode 100644 index 0000000..8d446f7 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/SipEvent.java @@ -0,0 +1,46 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import net.sourceforge.peers.sip.transport.SipMessage; + +public class SipEvent { + + public enum EventType { + ERROR, RINGING, INCOMING_CALL, CALLEE_PICKUP; + } + + private EventType eventType; + private SipMessage sipMessage; + + public SipEvent(EventType type, SipMessage sipMessage) { + this.eventType = type; + this.sipMessage = sipMessage; + } + + public SipMessage getSipMessage() { + return sipMessage; + } + + public EventType getEventType() { + return eventType; + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/SipListener.java b/src/net/sourceforge/peers/sip/core/useragent/SipListener.java new file mode 100644 index 0000000..de9c5ad --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/SipListener.java @@ -0,0 +1,44 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + + +package net.sourceforge.peers.sip.core.useragent; + +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; + +public interface SipListener { + + public void registering(SipRequest sipRequest); + + public void registerSuccessful(SipResponse sipResponse); + + public void registerFailed(SipResponse sipResponse); + + public void incomingCall(SipRequest sipRequest, SipResponse provResponse); + + public void remoteHangup(SipRequest sipRequest); + + public void ringing(SipResponse sipResponse); + + public void calleePickup(SipResponse sipResponse); + + public void error(SipResponse sipResponse); + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/UAC.java b/src/net/sourceforge/peers/sip/core/useragent/UAC.java new file mode 100644 index 0000000..79f32bc --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/UAC.java @@ -0,0 +1,228 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.InviteClientTransaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transactionuser.DialogState; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class UAC { + + private InitialRequestManager initialRequestManager; + private MidDialogRequestManager midDialogRequestManager; + + private String registerCallID; + private String profileUri; + + //FIXME + private UserAgent userAgent; + private TransactionManager transactionManager; + private DialogManager dialogManager; + private List guiClosedCallIds; + private Logger logger; + + /** + * should be instanciated only once, it was a singleton. + */ + public UAC(UserAgent userAgent, + InitialRequestManager initialRequestManager, + MidDialogRequestManager midDialogRequestManager, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, + Logger logger) { + this.userAgent = userAgent; + this.initialRequestManager = initialRequestManager; + this.midDialogRequestManager = midDialogRequestManager; + this.dialogManager = dialogManager; + this.transactionManager = transactionManager; + this.logger = logger; + guiClosedCallIds = Collections.synchronizedList(new ArrayList()); + profileUri = RFC3261.SIP_SCHEME + RFC3261.SCHEME_SEPARATOR + + userAgent.getUserpart() + RFC3261.AT + userAgent.getDomain(); + } + + /** + * For the moment we consider that only one profile uri is used at a time. + * @throws SipUriSyntaxException + */ + SipRequest register() throws SipUriSyntaxException { + String domain = userAgent.getDomain(); + String requestUri = RFC3261.SIP_SCHEME + RFC3261.SCHEME_SEPARATOR + + domain; + SipListener sipListener = userAgent.getSipListener(); + profileUri = RFC3261.SIP_SCHEME + RFC3261.SCHEME_SEPARATOR + + userAgent.getUserpart() + RFC3261.AT + domain; + registerCallID = Utils.generateCallID( + userAgent.getConfig().getLocalInetAddress()); + SipRequest sipRequest = initialRequestManager.createInitialRequest( + requestUri, RFC3261.METHOD_REGISTER, profileUri, + registerCallID); + if (sipListener != null) { + sipListener.registering(sipRequest); + } + return sipRequest; + } + + void unregister() throws SipUriSyntaxException { + if (getInitialRequestManager().getRegisterHandler().isRegistered()) { + String requestUri = RFC3261.SIP_SCHEME + RFC3261.SCHEME_SEPARATOR + + userAgent.getDomain(); + MessageInterceptor messageInterceptor = new MessageInterceptor() { + + @Override + public void postProcess(SipMessage sipMessage) { + initialRequestManager.registerHandler.unregister(); + SipHeaders sipHeaders = sipMessage.getSipHeaders(); + SipHeaderFieldValue contact = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CONTACT)); + contact.addParam(new SipHeaderParamName(RFC3261.PARAM_EXPIRES), + "0"); + } + + }; + // for any reason, asterisk requires a new Call-ID to unregister + registerCallID = Utils.generateCallID( + userAgent.getConfig().getLocalInetAddress()); + initialRequestManager.createInitialRequest(requestUri, + RFC3261.METHOD_REGISTER, profileUri, registerCallID, null, + messageInterceptor); + } + } + + SipRequest invite(String requestUri, String callId) + throws SipUriSyntaxException { + return initialRequestManager.createInitialRequest(requestUri, + RFC3261.METHOD_INVITE, profileUri, callId); + + } + + private SipRequest getInviteWithAuth(String callId) { + List clientTransactions = + transactionManager.getClientTransactionsFromCallId(callId, + RFC3261.METHOD_INVITE); + SipRequest sipRequestNoAuth = null; + for (ClientTransaction clientTransaction: clientTransactions) { + InviteClientTransaction inviteClientTransaction = + (InviteClientTransaction)clientTransaction; + SipRequest sipRequest = inviteClientTransaction.getRequest(); + SipHeaders sipHeaders = sipRequest.getSipHeaders(); + SipHeaderFieldName authorization = new SipHeaderFieldName( + RFC3261.HDR_AUTHORIZATION); + SipHeaderFieldValue value = sipHeaders.get(authorization); + if (value == null) { + SipHeaderFieldName proxyAuthorization = new SipHeaderFieldName( + RFC3261.HDR_PROXY_AUTHORIZATION); + value = sipHeaders.get(proxyAuthorization); + } + if (value != null) { + return sipRequest; + } + sipRequestNoAuth = sipRequest; + } + return sipRequestNoAuth; + } + + void terminate(SipRequest sipRequest) { + String callId = Utils.getMessageCallId(sipRequest); + if (!guiClosedCallIds.contains(callId)) { + guiClosedCallIds.add(callId); + } + Dialog dialog = dialogManager.getDialog(callId); + SipRequest inviteWithAuth = getInviteWithAuth(callId); + if (dialog != null) { + SipRequest originatingRequest; + if (inviteWithAuth != null) { + originatingRequest = inviteWithAuth; + } else { + originatingRequest = sipRequest; + } + ClientTransaction clientTransaction = + transactionManager.getClientTransaction(originatingRequest); + if (clientTransaction != null) { + synchronized (clientTransaction) { + DialogState dialogState = dialog.getState(); + if (dialog.EARLY.equals(dialogState)) { + initialRequestManager.createCancel(inviteWithAuth, + midDialogRequestManager, profileUri); + } else if (dialog.CONFIRMED.equals(dialogState)) { + // clientTransaction not yet removed + midDialogRequestManager.generateMidDialogRequest( + dialog, RFC3261.METHOD_BYE, null); + guiClosedCallIds.remove(callId); + } + } + } else { + // clientTransaction Terminated and removed + if (logger!=null) logger.debug("clientTransaction null"); + midDialogRequestManager.generateMidDialogRequest( + dialog, RFC3261.METHOD_BYE, null); + guiClosedCallIds.remove(callId); + } + } else { + InviteClientTransaction inviteClientTransaction = + (InviteClientTransaction)transactionManager + .getClientTransaction(inviteWithAuth); + if (inviteClientTransaction == null) { + if (logger!=null) logger.error("cannot find invite client transaction" + + " for call " + callId); + } else { + SipResponse sipResponse = + inviteClientTransaction.getLastResponse(); + if (sipResponse != null) { + int statusCode = sipResponse.getStatusCode(); + if (statusCode < RFC3261.CODE_200_OK) { + initialRequestManager.createCancel(inviteWithAuth, + midDialogRequestManager, profileUri); + } + } + } + } + userAgent.getMediaManager().stopSession(); + } + + public InitialRequestManager getInitialRequestManager() { + return initialRequestManager; + } + + public List getGuiClosedCallIds() { + return guiClosedCallIds; + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/UAS.java b/src/net/sourceforge/peers/sip/core/useragent/UAS.java new file mode 100644 index 0000000..f27a9b9 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/UAS.java @@ -0,0 +1,133 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import java.net.SocketException; +import java.util.ArrayList; + +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.SipServerTransportUser; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class UAS implements SipServerTransportUser { + + public final static ArrayList SUPPORTED_METHODS; + + static { + SUPPORTED_METHODS = new ArrayList(); + SUPPORTED_METHODS.add(RFC3261.METHOD_INVITE); + SUPPORTED_METHODS.add(RFC3261.METHOD_ACK); + SUPPORTED_METHODS.add(RFC3261.METHOD_CANCEL); + SUPPORTED_METHODS.add(RFC3261.METHOD_OPTIONS); + SUPPORTED_METHODS.add(RFC3261.METHOD_BYE); + }; + + private InitialRequestManager initialRequestManager; + private MidDialogRequestManager midDialogRequestManager; + + private DialogManager dialogManager; + + /** + * should be instanciated only once, it was a singleton. + */ + public UAS(UserAgent userAgent, + InitialRequestManager initialRequestManager, + MidDialogRequestManager midDialogRequestManager, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager) throws SocketException { + this.initialRequestManager = initialRequestManager; + this.midDialogRequestManager = midDialogRequestManager; + this.dialogManager = dialogManager; + transportManager.setSipServerTransportUser(this); + transportManager.createServerTransport( + RFC3261.TRANSPORT_UDP, userAgent.getConfig().getSipPort()); + } + + public void messageReceived(SipMessage sipMessage) { + if (sipMessage instanceof SipRequest) { + requestReceived((SipRequest) sipMessage); + } else if (sipMessage instanceof SipResponse) { + responseReceived((SipResponse) sipMessage); + } else { + throw new RuntimeException("unknown message type"); + } + } + + private void responseReceived(SipResponse sipResponse) { + + } + + private void requestReceived(SipRequest sipRequest) { + //TODO 8.2 + + //TODO JTA to make request processing atomic + + SipHeaders headers = sipRequest.getSipHeaders(); + + //TODO find whether the request is within an existing dialog or not + SipHeaderFieldValue to = + headers.get(new SipHeaderFieldName(RFC3261.HDR_TO)); + String toTag = to.getParam(new SipHeaderParamName(RFC3261.PARAM_TAG)); + if (toTag != null) { + Dialog dialog = dialogManager.getDialog(sipRequest); + if (dialog != null) { + //this is a mid-dialog request + midDialogRequestManager.manageMidDialogRequest(sipRequest, dialog); + //TODO continue processing + } else { + //TODO reject the request with a 481 Call/Transaction Does Not Exist + + } + } else { + + initialRequestManager.manageInitialRequest(sipRequest); + + } + } + + void acceptCall(SipRequest sipRequest, Dialog dialog) { + initialRequestManager.getInviteHandler().acceptCall(sipRequest, + dialog); + } + + void rejectCall(SipRequest sipRequest) { + initialRequestManager.getInviteHandler().rejectCall(sipRequest); + } + + public InitialRequestManager getInitialRequestManager() { + return initialRequestManager; + } + + public MidDialogRequestManager getMidDialogRequestManager() { + return midDialogRequestManager; + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/UserAgent.java b/src/net/sourceforge/peers/sip/core/useragent/UserAgent.java new file mode 100644 index 0000000..57245e0 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/UserAgent.java @@ -0,0 +1,396 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent; + +import java.io.File; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.FileLogger; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.XmlConfig; +import net.sourceforge.peers.media.AbstractSoundManager; +import net.sourceforge.peers.media.Echo; +import net.sourceforge.peers.media.MediaManager; +import net.sourceforge.peers.media.MediaMode; +import net.sourceforge.peers.sdp.SDPManager; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.core.useragent.handlers.ByeHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.CancelHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.InviteHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.OptionsHandler; +import net.sourceforge.peers.sip.core.useragent.handlers.RegisterHandler; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transaction.Transaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +@SuppressWarnings("unused") +public class UserAgent { + + public final static String CONFIG_FILE = "conf" + File.separator + "peers.xml"; + public final static int RTP_DEFAULT_PORT = 8000; + + private String peersHome; + private Logger logger; + private Config config; + + private List peers; + //private List dialogs; + + //TODO factorize echo and captureRtpSender + private Echo echo; + + private UAC uac; + private UAS uas; + + private ChallengeManager challengeManager; + + private DialogManager dialogManager; + private TransactionManager transactionManager; + private TransportManager transportManager; + + private int cseqCounter; + private SipListener sipListener; + + private SDPManager sdpManager; + private AbstractSoundManager soundManager; + private MediaManager mediaManager; + + public UserAgent(SipListener sipListener, String peersHome, + Logger logger, AbstractSoundManager soundManager) + throws SocketException { + this(sipListener, null, peersHome, logger, soundManager); + } + + public UserAgent(SipListener sipListener, Config config, + Logger logger, AbstractSoundManager soundManager) + throws SocketException { + this(sipListener, config, null, logger, soundManager); + } + + private UserAgent(SipListener sipListener, Config config, String peersHome, + Logger logger, AbstractSoundManager soundManager) + throws SocketException { + this.sipListener = sipListener; + if (peersHome == null) { + peersHome = Utils.DEFAULT_PEERS_HOME; + } + this.peersHome = peersHome; +// if (logger == null) { +// logger = new FileLogger(this.peersHome); +// } + this.logger = logger; + if (config == null) { + config = new XmlConfig(this.peersHome + File.separator + + CONFIG_FILE, this.logger); + } + this.config = config; + + cseqCounter = 1; + + StringBuffer buf = new StringBuffer(); + buf.append("starting user agent ["); + buf.append("myAddress: "); + buf.append(config.getLocalInetAddress().getHostAddress()).append(", "); + buf.append("sipPort: "); + buf.append(config.getSipPort()).append(", "); + buf.append("userpart: "); + buf.append(config.getUserPart()).append(", "); + buf.append("domain: "); + buf.append(config.getDomain()).append("]"); + if (logger!=null) logger.info(buf.toString()); + + //transaction user + + dialogManager = new DialogManager(logger); + + //transaction + + transactionManager = new TransactionManager(logger); + + //transport + + transportManager = new TransportManager(transactionManager, + config, logger); + + transactionManager.setTransportManager(transportManager); + + //core + + InviteHandler inviteHandler = new InviteHandler(this, + dialogManager, + transactionManager, + transportManager, + logger); + CancelHandler cancelHandler = new CancelHandler(this, + dialogManager, + transactionManager, + transportManager, + logger); + ByeHandler byeHandler = new ByeHandler(this, + dialogManager, + transactionManager, + transportManager, + logger); + OptionsHandler optionsHandler = new OptionsHandler(this, + transactionManager, + transportManager, + logger); + RegisterHandler registerHandler = new RegisterHandler(this, + transactionManager, + transportManager, + logger); + + InitialRequestManager initialRequestManager = + new InitialRequestManager( + this, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager, + logger); + MidDialogRequestManager midDialogRequestManager = + new MidDialogRequestManager( + this, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager, + logger); + + uas = new UAS(this, + initialRequestManager, + midDialogRequestManager, + dialogManager, + transactionManager, + transportManager); + + uac = new UAC(this, + initialRequestManager, + midDialogRequestManager, + dialogManager, + transactionManager, + transportManager, + logger); + + challengeManager = new ChallengeManager(config, + initialRequestManager, + midDialogRequestManager, + dialogManager, + logger); + registerHandler.setChallengeManager(challengeManager); + inviteHandler.setChallengeManager(challengeManager); + byeHandler.setChallengeManager(challengeManager); + + peers = new ArrayList(); + //dialogs = new ArrayList(); + + sdpManager = new SDPManager(this, logger); + inviteHandler.setSdpManager(sdpManager); + optionsHandler.setSdpManager(sdpManager); + // soundManager = new SoundManager(config.isMediaDebug(), logger, + // this.peersHome); + this.soundManager = soundManager; + mediaManager = new MediaManager(this, logger); + } + + // client methods + + public void close() { + transportManager.closeTransports(); + config.setPublicInetAddress(null); + } + + public SipRequest register() throws SipUriSyntaxException { + return uac.register(); + } + + public void unregister() throws SipUriSyntaxException { + uac.unregister(); + } + + public SipRequest invite(String requestUri, String callId) + throws SipUriSyntaxException { + return uac.invite(requestUri, callId); + } + + public void terminate(SipRequest sipRequest) { + uac.terminate(sipRequest); + } + + public void acceptCall(SipRequest sipRequest, Dialog dialog) { + uas.acceptCall(sipRequest, dialog); + } + + public void rejectCall(SipRequest sipRequest) { + uas.rejectCall(sipRequest); + } + + + /** + * Gives the sipMessage if sipMessage is a SipRequest or + * the SipRequest corresponding to the SipResponse + * if sipMessage is a SipResponse + * @param sipMessage + * @return null if sipMessage is neither a SipRequest neither a SipResponse + */ + public SipRequest getSipRequest(SipMessage sipMessage) { + if (sipMessage instanceof SipRequest) { + return (SipRequest) sipMessage; + } else if (sipMessage instanceof SipResponse) { + SipResponse sipResponse = (SipResponse) sipMessage; + Transaction transaction = (Transaction)transactionManager + .getClientTransaction(sipResponse); + if (transaction == null) { + transaction = (Transaction)transactionManager + .getServerTransaction(sipResponse); + } + if (transaction == null) { + return null; + } + return transaction.getRequest(); + } else { + return null; + } + } + +// public List getDialogs() { +// return dialogs; +// } + + public List getPeers() { + return peers; + } + +// public Dialog getDialog(String peer) { +// for (Dialog dialog : dialogs) { +// String remoteUri = dialog.getRemoteUri(); +// if (remoteUri != null) { +// if (remoteUri.contains(peer)) { +// return dialog; +// } +// } +// } +// return null; +// } + + public String generateCSeq(String method) { + StringBuffer buf = new StringBuffer(); + buf.append(cseqCounter++); + buf.append(' '); + buf.append(method); + return buf.toString(); + } + + public boolean isRegistered() { + return uac.getInitialRequestManager().getRegisterHandler() + .isRegistered(); + } + + public UAS getUas() { + return uas; + } + + public UAC getUac() { + return uac; + } + + public DialogManager getDialogManager() { + return dialogManager; + } + + public int getSipPort() { + return transportManager.getSipPort(); + } + + public int getRtpPort() { + return config.getRtpPort(); + } + + public String getDomain() { + return config.getDomain(); + } + + public String getUserpart() { + return config.getUserPart(); + } + + public MediaMode getMediaMode() { + return config.getMediaMode(); + } + + public boolean isMediaDebug() { + return config.isMediaDebug(); + } + + public SipURI getOutboundProxy() { + return config.getOutboundProxy(); + } + + public Echo getEcho() { + return echo; + } + + public void setEcho(Echo echo) { + this.echo = echo; + } + + public SipListener getSipListener() { + return sipListener; + } + + public AbstractSoundManager getSoundManager() { + return soundManager; + } + + public MediaManager getMediaManager() { + return mediaManager; + } + + public Config getConfig() { + return config; + } + + public String getPeersHome() { + return peersHome; + } + + public TransportManager getTransportManager() { + return transportManager; + } +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/ByeHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/ByeHandler.java new file mode 100644 index 0000000..f170c32 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/ByeHandler.java @@ -0,0 +1,183 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.core.useragent.RequestManager; +import net.sourceforge.peers.sip.core.useragent.SipListener; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.ClientTransactionUser; +import net.sourceforge.peers.sip.transaction.NonInviteClientTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransactionUser; +import net.sourceforge.peers.sip.transaction.Transaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class ByeHandler extends DialogMethodHandler + implements ServerTransactionUser, ClientTransactionUser { + + public ByeHandler(UserAgent userAgent, DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(userAgent, dialogManager, transactionManager, transportManager, + logger); + } + + //////////////////////////////////////////////// + // methods for UAC + //////////////////////////////////////////////// + + public void preprocessBye(SipRequest sipRequest, Dialog dialog) { + + // 15.1.1 + + String addrSpec = sipRequest.getRequestUri().toString(); + userAgent.getPeers().remove(addrSpec); + challengeManager.postProcess(sipRequest); + } + + + + + + + //////////////////////////////////////////////// + // methods for UAS + //////////////////////////////////////////////// + + public void handleBye(SipRequest sipRequest, Dialog dialog) { + dialog.receivedOrSentBye(); + //String remoteUri = dialog.getRemoteUri(); + + String addrSpec = sipRequest.getRequestUri().toString(); + userAgent.getPeers().remove(addrSpec); + dialogManager.removeDialog(dialog.getId()); + if (logger!=null) logger.debug("removed dialog " + dialog.getId()); + userAgent.getMediaManager().stopSession(); + + SipResponse sipResponse = + RequestManager.generateResponse( + sipRequest, + dialog, + RFC3261.CODE_200_OK, + RFC3261.REASON_200_OK); + + // TODO determine port and transport for server transaction>transport + // from initial invite + // FIXME determine port and transport for server transaction>transport + ServerTransaction serverTransaction = transactionManager + .createServerTransaction( + sipResponse, + userAgent.getSipPort(), + RFC3261.TRANSPORT_UDP, + this, + sipRequest); + + serverTransaction.start(); + + serverTransaction.receivedRequest(sipRequest); + + serverTransaction.sendReponse(sipResponse); + + dialogManager.removeDialog(dialog.getId()); + + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.remoteHangup(sipRequest); + } + +// setChanged(); +// notifyObservers(sipRequest); + } + + /////////////////////////////////////// + //ServerTransactionUser methods + /////////////////////////////////////// + public void transactionFailure() { + // TODO Auto-generated method stub + + } + + /////////////////////////////////////// + //ClientTransactionUser methods + /////////////////////////////////////// + @Override + public void transactionTimeout(ClientTransaction clientTransaction) { + // TODO Auto-generated method stub + + } + + @Override + public void provResponseReceived(SipResponse sipResponse, + Transaction transaction) { + // TODO Auto-generated method stub + + } + + @Override + public void errResponseReceived(SipResponse sipResponse) { + int statusCode = sipResponse.getStatusCode(); + if (statusCode == RFC3261.CODE_401_UNAUTHORIZED + || statusCode == RFC3261.CODE_407_PROXY_AUTHENTICATION_REQUIRED + && !challenged) { + NonInviteClientTransaction nonInviteClientTransaction = + (NonInviteClientTransaction) + transactionManager.getClientTransaction(sipResponse); + SipRequest sipRequest = nonInviteClientTransaction.getRequest(); + String password = userAgent.getConfig().getPassword(); + if (password != null && !"".equals(password.trim())) { + challengeManager.handleChallenge(sipRequest, + sipResponse); + } + challenged = true; + } else { + challenged = false; + } + } + + @Override + public void successResponseReceived(SipResponse sipResponse, + Transaction transaction) { + Dialog dialog = dialogManager.getDialog(sipResponse); + if (dialog == null) { + return; + } + dialog.receivedOrSentBye(); + dialogManager.removeDialog(dialog.getId()); + if (logger!=null) logger.debug("removed dialog " + dialog.getId()); + } + + @Override + public void transactionTransportError() { + // TODO Auto-generated method stub + + } + + + + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/CancelHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/CancelHandler.java new file mode 100644 index 0000000..a25f23a --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/CancelHandler.java @@ -0,0 +1,172 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.core.useragent.MidDialogRequestManager; +import net.sourceforge.peers.sip.core.useragent.SipListener; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.InviteClientTransaction; +import net.sourceforge.peers.sip.transaction.InviteServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransactionUser; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class CancelHandler extends DialogMethodHandler + implements ServerTransactionUser { + + public CancelHandler(UserAgent userAgent, DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(userAgent, dialogManager, transactionManager, transportManager, + logger); + } + + ////////////////////////////////////////////////////////// + // UAS methods + ////////////////////////////////////////////////////////// + + public void handleCancel(SipRequest sipRequest) { + SipHeaderFieldValue topVia = Utils.getTopVia(sipRequest); + String branchId = topVia.getParam(new SipHeaderParamName( + RFC3261.PARAM_BRANCH)); + InviteServerTransaction inviteServerTransaction = + (InviteServerTransaction)transactionManager + .getServerTransaction(branchId,RFC3261.METHOD_INVITE); + SipResponse cancelResponse; + if (inviteServerTransaction == null) { + //TODO generate CANCEL 481 Call Leg/Transaction Does Not Exist + cancelResponse = buildGenericResponse(sipRequest, + RFC3261.CODE_481_CALL_TRANSACTION_DOES_NOT_EXIST, + RFC3261.REASON_481_CALL_TRANSACTION_DOES_NOT_EXIST); + } else { + cancelResponse = buildGenericResponse(sipRequest, + RFC3261.CODE_200_OK, RFC3261.REASON_200_OK); + } + ServerTransaction cancelServerTransaction = transactionManager + .createServerTransaction(cancelResponse, + userAgent.getSipPort(), + RFC3261.TRANSPORT_UDP, this, sipRequest); + cancelServerTransaction.start(); + cancelServerTransaction.receivedRequest(sipRequest); + cancelServerTransaction.sendReponse(cancelResponse); + if (cancelResponse.getStatusCode() != RFC3261.CODE_200_OK) { + return; + } + + SipResponse lastResponse = inviteServerTransaction.getLastResponse(); + if (lastResponse != null && + lastResponse.getStatusCode() >= RFC3261.CODE_200_OK) { + return; + } + + SipResponse inviteResponse = buildGenericResponse( + inviteServerTransaction.getRequest(), + RFC3261.CODE_487_REQUEST_TERMINATED, + RFC3261.REASON_487_REQUEST_TERMINATED); + inviteServerTransaction.sendReponse(inviteResponse); + + Dialog dialog = dialogManager.getDialog(lastResponse); + dialog.receivedOrSent300To699(); + + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.remoteHangup(sipRequest); + } + } + + ////////////////////////////////////////////////////////// + // UAC methods + ////////////////////////////////////////////////////////// + + public ClientTransaction preProcessCancel(SipRequest cancelGenericRequest, + SipRequest inviteRequest, + MidDialogRequestManager midDialogRequestManager) { + //TODO + //p. 54 §9.1 + + SipHeaders cancelHeaders = cancelGenericRequest.getSipHeaders(); + SipHeaders inviteHeaders = inviteRequest.getSipHeaders(); + + //cseq + SipHeaderFieldName cseqName = new SipHeaderFieldName(RFC3261.HDR_CSEQ); + SipHeaderFieldValue cancelCseq = cancelHeaders.get(cseqName); + SipHeaderFieldValue inviteCseq = inviteHeaders.get(cseqName); + cancelCseq.setValue(inviteCseq.getValue().replace(RFC3261.METHOD_INVITE, + RFC3261.METHOD_CANCEL)); + + + //from + SipHeaderFieldName fromName = new SipHeaderFieldName(RFC3261.HDR_FROM); + SipHeaderFieldValue cancelFrom = cancelHeaders.get(fromName); + SipHeaderFieldValue inviteFrom = inviteHeaders.get(fromName); + cancelFrom.setValue(inviteFrom.getValue()); + SipHeaderParamName tagParam = new SipHeaderParamName(RFC3261.PARAM_TAG); + cancelFrom.removeParam(tagParam); + cancelFrom.addParam(tagParam, inviteFrom.getParam(tagParam)); + + //top-via +// cancelHeaders.add(new SipHeaderFieldName(RFC3261.HDR_VIA), +// Utils.getInstance().getTopVia(inviteRequest)); + SipHeaderFieldValue topVia = Utils.getTopVia(inviteRequest); + String branchId = topVia.getParam(new SipHeaderParamName(RFC3261.PARAM_BRANCH)); + + //route + SipHeaderFieldName routeName = new SipHeaderFieldName(RFC3261.HDR_ROUTE); + SipHeaderFieldValue inviteRoute = inviteHeaders.get(routeName); + if (inviteRoute != null) { + cancelHeaders.add(routeName, inviteRoute); + } + + InviteClientTransaction inviteClientTransaction = + (InviteClientTransaction)transactionManager.getClientTransaction( + inviteRequest); + if (inviteClientTransaction != null) { + SipResponse lastResponse = inviteClientTransaction.getLastResponse(); + if (lastResponse != null && + lastResponse.getStatusCode() >= RFC3261.CODE_200_OK) { + return null; + } + } else { + if (logger!=null) if (logger!=null) logger.error("cannot retrieve invite client transaction for" + + " request " + inviteRequest); + } + + return midDialogRequestManager.createNonInviteClientTransaction( + cancelGenericRequest, branchId, midDialogRequestManager); + } + + public void transactionFailure() { + // TODO Auto-generated method stub + + } +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/DialogMethodHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/DialogMethodHandler.java new file mode 100644 index 0000000..4007884 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/DialogMethodHandler.java @@ -0,0 +1,282 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.TimerTask; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.syntaxencoding.NameAddress; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldMultiValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transaction.Transaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public abstract class DialogMethodHandler extends MethodHandler { + + protected DialogManager dialogManager; + + public DialogMethodHandler(UserAgent userAgent, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(userAgent, transactionManager, transportManager, logger); + this.dialogManager = dialogManager; + } + + protected Dialog buildDialogForUas(SipResponse sipResponse, + SipRequest sipRequest) { + //12.1.1 + + //prepare response + + SipHeaders reqHeaders = sipRequest.getSipHeaders(); + SipHeaders respHeaders = sipResponse.getSipHeaders(); + + //copy record-route + SipHeaderFieldName recordRouteName = + new SipHeaderFieldName(RFC3261.HDR_RECORD_ROUTE); + SipHeaderFieldValue reqRecRoute = reqHeaders.get(recordRouteName); + if (reqRecRoute != null) { + respHeaders.add(recordRouteName, reqRecRoute); + } + + //FIXME Contact header should probably added in response here. + + SipHeaderFieldName contactName = new SipHeaderFieldName(RFC3261.HDR_CONTACT); + + Dialog dialog = dialogManager.createDialog(sipResponse); + + //build dialog state + + //route set + SipHeaderFieldValue recordRoute = + respHeaders.get(new SipHeaderFieldName(RFC3261.HDR_RECORD_ROUTE)); + ArrayList routeSet = new ArrayList(); + if (recordRoute != null) { + if (recordRoute instanceof SipHeaderFieldMultiValue) { + SipHeaderFieldMultiValue multiRecordRoute = + (SipHeaderFieldMultiValue) recordRoute; + for (SipHeaderFieldValue routeValue : multiRecordRoute.getValues()) { + routeSet.add(routeValue.getValue()); + } + } else { + routeSet.add(recordRoute.getValue()); + } + } + dialog.setRouteSet(routeSet); + + //remote target + SipHeaderFieldValue reqContact = reqHeaders.get(contactName); + String remoteTarget = reqContact.getValue(); + if (remoteTarget.indexOf(RFC3261.LEFT_ANGLE_BRACKET) > -1) { + remoteTarget = NameAddress.nameAddressToUri(remoteTarget); + } + dialog.setRemoteTarget(remoteTarget); + + //remote cseq + SipHeaderFieldName cseqName = new SipHeaderFieldName(RFC3261.HDR_CSEQ); + SipHeaderFieldValue cseq = reqHeaders.get(cseqName); + String remoteCseq = cseq.getValue().substring(0, cseq.getValue().indexOf(' ')); + dialog.setRemoteCSeq(Integer.parseInt(remoteCseq)); + + //callid + SipHeaderFieldName callidName = new SipHeaderFieldName(RFC3261.HDR_CALLID); + SipHeaderFieldValue callid = reqHeaders.get(callidName); + dialog.setCallId(callid.getValue()); + + //local tag + SipHeaderFieldName toName = new SipHeaderFieldName(RFC3261.HDR_TO); + SipHeaderFieldValue to = respHeaders.get(toName); + SipHeaderParamName tagName = new SipHeaderParamName(RFC3261.PARAM_TAG); + String toTag = to.getParam(tagName); + dialog.setLocalTag(toTag); + + //remote tag + SipHeaderFieldName fromName = new SipHeaderFieldName(RFC3261.HDR_FROM); + SipHeaderFieldValue from = reqHeaders.get(fromName); + String fromTag = from.getParam(tagName); + dialog.setRemoteTag(fromTag); + + //remote uri + + String remoteUri = from.getValue(); + if (remoteUri.indexOf(RFC3261.LEFT_ANGLE_BRACKET) > -1) { + remoteUri = NameAddress.nameAddressToUri(remoteUri); + } + dialog.setRemoteUri(remoteUri); + + //local uri + + String localUri = to.getValue(); + if (localUri.indexOf(RFC3261.LEFT_ANGLE_BRACKET) > -1) { + localUri = NameAddress.nameAddressToUri(localUri); + } + dialog.setLocalUri(localUri); + + return dialog; + } + + //TODO throw an exception if dialog elements are lacking when dialog is built from 200 + protected Dialog buildOrUpdateDialogForUac(SipResponse sipResponse, + Transaction transaction) { + SipHeaders headers = sipResponse.getSipHeaders(); + + Dialog dialog = dialogManager.getDialog(sipResponse); + if (dialog == null) { + dialog = dialogManager.createDialog(sipResponse); + } + + //12.1.2 + + //TODO if request uri contains sips scheme or if sent over tls => dialog.setSecure(true) + + //route set + + ArrayList routeSet = computeRouteSet(headers); + if (routeSet != null) { + dialog.setRouteSet(routeSet); + } + + //remote target + + SipHeaderFieldValue contact = headers.get(new SipHeaderFieldName(RFC3261.HDR_CONTACT)); + if (logger!=null) logger.debug("Contact: " + contact); + if (contact != null) { + String remoteTarget = NameAddress.nameAddressToUri(contact.toString()); + dialog.setRemoteTarget(remoteTarget); + } + + SipHeaders requestSipHeaders = transaction.getRequest().getSipHeaders(); + + //local cseq + + String requestCSeq = requestSipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CSEQ)).toString(); + requestCSeq = requestCSeq.substring(0, requestCSeq.indexOf(' ')); + dialog.setLocalCSeq(Integer.parseInt(requestCSeq)); + + //callID + + //already done in createDialog() +// String requestCallID = requestSipHeaders.get( +// new SipHeaderFieldName(RFC3261.HDR_CALLID)).toString(); +// dialog.setCallId(requestCallID); + + //local tag + + //already done in createDialog() +// SipHeaderFieldValue requestFrom = requestSipHeaders.get( +// new SipHeaderFieldName(RFC3261.HDR_FROM)); +// String requestFromTag = +// requestFrom.getParam(new SipHeaderParamName(RFC3261.PARAM_TAG)); +// dialog.setLocalTag(requestFromTag); + + //remote tag + + //already done in createDialog() +// dialog.setRemoteTag(toTag); + + //remote uri + + SipHeaderFieldValue to = headers.get(new SipHeaderFieldName(RFC3261.HDR_TO)); + if (to != null) { + String remoteUri = to.getValue(); + if (remoteUri.indexOf(RFC3261.LEFT_ANGLE_BRACKET) > -1) { + remoteUri = NameAddress.nameAddressToUri(remoteUri); + } + dialog.setRemoteUri(remoteUri); + } + + //local uri + SipHeaderFieldValue requestFrom = requestSipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_FROM)); + String localUri = requestFrom.getValue(); + if (localUri.indexOf(RFC3261.LEFT_ANGLE_BRACKET) > -1) { + localUri = NameAddress.nameAddressToUri(localUri); + } + dialog.setLocalUri(localUri); + + return dialog; + } + + protected ArrayList computeRouteSet(SipHeaders headers) { + SipHeaderFieldValue recordRoute = + headers.get(new SipHeaderFieldName(RFC3261.HDR_RECORD_ROUTE)); + ArrayList routeSet = new ArrayList(); + if (recordRoute != null) { + if (recordRoute instanceof SipHeaderFieldMultiValue) { + List values = + ((SipHeaderFieldMultiValue)recordRoute).getValues(); + for (SipHeaderFieldValue value : values) { + //reverse order + routeSet.add(0, value.toString()); + } + } else { + routeSet.add(recordRoute.toString()); + } + } + return routeSet; + } + + //TODO see if AckHandler is usable + class AckTimerTask extends TimerTask { + + private String toUri; + + public AckTimerTask(String toUri) { + super(); + this.toUri = toUri; + } + + @Override + public void run() { + ArrayList purgedDialogs = new ArrayList(); + Collection dialogs = dialogManager.getDialogCollection(); + for (Dialog dialog : dialogs) { + String remoteUri = dialog.getRemoteUri(); + if (remoteUri.equals(toUri) && + !dialog.CONFIRMED.equals(dialog.getState())) { + dialog.receivedOrSentBye(); + purgedDialogs.add(dialog); + } + } + for (Dialog dialog : purgedDialogs) { + dialogs.remove(dialog); + } + } + + } + + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/InviteHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/InviteHandler.java new file mode 100644 index 0000000..e1d1ead --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/InviteHandler.java @@ -0,0 +1,694 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Timer; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.media.MediaManager; +import net.sourceforge.peers.sdp.Codec; +import net.sourceforge.peers.sdp.MediaDestination; +import net.sourceforge.peers.sdp.NoCodecException; +import net.sourceforge.peers.sdp.SessionDescription; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.core.useragent.RequestManager; +import net.sourceforge.peers.sip.core.useragent.SipListener; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.syntaxencoding.NameAddress; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.ClientTransactionUser; +import net.sourceforge.peers.sip.transaction.InviteClientTransaction; +import net.sourceforge.peers.sip.transaction.InviteServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransactionUser; +import net.sourceforge.peers.sip.transaction.Transaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transactionuser.Dialog; +import net.sourceforge.peers.sip.transactionuser.DialogManager; +import net.sourceforge.peers.sip.transport.MessageSender; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class InviteHandler extends DialogMethodHandler + implements ServerTransactionUser, ClientTransactionUser { + + public static final int TIMEOUT = 100; + + private MediaDestination mediaDestination; + private Timer ackTimer; + private boolean initialIncomingInvite; + + public InviteHandler(UserAgent userAgent, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(userAgent, dialogManager, transactionManager, transportManager, + logger); + ackTimer = new Timer(getClass().getSimpleName() + " Ack " + + Timer.class.getSimpleName()); + } + + + ////////////////////////////////////////////////////////// + // UAS methods + ////////////////////////////////////////////////////////// + + public void handleInitialInvite(SipRequest sipRequest) { + initialIncomingInvite = true; + //generate 180 Ringing + SipResponse sipResponse = buildGenericResponse(sipRequest, + RFC3261.CODE_180_RINGING, RFC3261.REASON_180_RINGING); + Dialog dialog = buildDialogForUas(sipResponse, sipRequest); + //here dialog is already stored in dialogs in DialogManager + + InviteServerTransaction inviteServerTransaction = (InviteServerTransaction) + transactionManager.createServerTransaction(sipResponse, + userAgent.getSipPort(), RFC3261.TRANSPORT_UDP, this, + sipRequest); + + inviteServerTransaction.start(); + + inviteServerTransaction.receivedRequest(sipRequest); + + //TODO send 180 more than once + inviteServerTransaction.sendReponse(sipResponse); + + dialog.receivedOrSent1xx(); + + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.incomingCall(sipRequest, sipResponse); + } + + List peers = userAgent.getPeers(); + String responseTo = sipRequest.getSipHeaders().get( + new SipHeaderFieldName(RFC3261.HDR_FROM)).getValue(); + if (!peers.contains(responseTo)) { + peers.add(responseTo); + } + + } + + public void handleReInvite(SipRequest sipRequest, Dialog dialog) { + if (logger!=null) logger.debug("handleReInvite"); + initialIncomingInvite = false; + SipHeaders sipHeaders = sipRequest.getSipHeaders(); + + // 12.2.2 update dialog + SipHeaderFieldValue contact = + sipHeaders.get(new SipHeaderFieldName(RFC3261.HDR_CONTACT)); + if (contact != null) { + String contactStr = contact.getValue(); + if (contactStr.indexOf(RFC3261.LEFT_ANGLE_BRACKET) > -1) { + contactStr = NameAddress.nameAddressToUri(contactStr); + } + dialog.setRemoteTarget(contactStr); + } + + + // update session + sendSuccessfulResponse(sipRequest, dialog); + + } + + private DatagramSocket getDatagramSocket() { + DatagramSocket datagramSocket = userAgent.getMediaManager() + .getDatagramSocket(); + if (datagramSocket == null) { // initial invite success response + // AccessController.doPrivileged added for plugin compatibility + datagramSocket = AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public DatagramSocket run() { + DatagramSocket datagramSocket = null; + int rtpPort = userAgent.getConfig().getRtpPort(); + try { + if (rtpPort == 0) { + int localPort = -1; + while (localPort % 2 != 0) { + datagramSocket = new DatagramSocket(); + localPort = datagramSocket.getLocalPort(); + if (localPort % 2 != 0) { + datagramSocket.close(); + } + } + } else { + datagramSocket = new DatagramSocket(rtpPort); + } + } catch (SocketException e) { + if (logger!=null) if (logger!=null) logger.error("cannot create datagram socket ", e); + } + + return datagramSocket; + } + } + ); + if (logger!=null) logger.debug("new rtp DatagramSocket " + datagramSocket.hashCode()); + try { + datagramSocket.setSoTimeout(TIMEOUT); + } catch (SocketException e) { + if (logger!=null) if (logger!=null) logger.error("cannot set timeout on datagram socket ", e); + } + userAgent.getMediaManager().setDatagramSocket(datagramSocket); + } + return datagramSocket; + } + + @SuppressWarnings("unlikely-arg-type") + private synchronized void sendSuccessfulResponse(SipRequest sipRequest, Dialog dialog) { + SipHeaders reqHeaders = sipRequest.getSipHeaders(); + SipHeaderFieldValue contentType = + reqHeaders.get(new SipHeaderFieldName(RFC3261.HDR_CONTENT_TYPE)); + + + if (RFC3261.CONTENT_TYPE_SDP.equals(contentType)) { + //TODO +// String sdpResponse; +// try { +// sdpResponse = sdpManager.handleOffer( +// new String(sipRequest.getBody())); +// } catch (NoCodecException e) { +// sdpResponse = sdpManager.generateErrorResponse(); +// } + } else { + // TODO manage empty bodies and non-application/sdp content type + } + + + //TODO if mode autoanswer just send 200 without asking any question + SipResponse sipResponse = + RequestManager.generateResponse( + sipRequest, + dialog, + RFC3261.CODE_200_OK, + RFC3261.REASON_200_OK); + + // TODO 13.3 dialog invite-specific processing + + // TODO timer if there is an Expires header in INVITE + + // TODO 3xx + + // TODO 486 or 600 + + byte[] offerBytes = sipRequest.getBody(); + SessionDescription answer; + try { + DatagramSocket datagramSocket = getDatagramSocket(); + + if (offerBytes != null && contentType != null && + RFC3261.CONTENT_TYPE_SDP.equals(contentType.getValue())) { + // create response in 200 + try { + SessionDescription offer = sdpManager.parse(offerBytes); + answer = sdpManager.createSessionDescription(offer, + datagramSocket.getLocalPort()); + mediaDestination = sdpManager.getMediaDestination(offer); + } catch (NoCodecException e) { + answer = sdpManager.createSessionDescription(null, + datagramSocket.getLocalPort()); + } + } else { + // create offer in 200 (never tested...) + answer = sdpManager.createSessionDescription(null, + datagramSocket.getLocalPort()); + } + sipResponse.setBody(answer.toString().getBytes()); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } + + SipHeaders respHeaders = sipResponse.getSipHeaders(); + respHeaders.add(new SipHeaderFieldName(RFC3261.HDR_CONTENT_TYPE), + new SipHeaderFieldValue(RFC3261.CONTENT_TYPE_SDP)); + + ArrayList routeSet = dialog.getRouteSet(); + if (routeSet != null) { + SipHeaderFieldName recordRoute = new SipHeaderFieldName(RFC3261.HDR_RECORD_ROUTE); + for (String route : routeSet) { + respHeaders.add(recordRoute, new SipHeaderFieldValue(route)); + } + } + + // TODO determine port and transport for server transaction>transport + // from initial invite + // FIXME determine port and transport for server transaction>transport + ServerTransaction serverTransaction = transactionManager + .getServerTransaction(sipRequest); + if (serverTransaction == null) { + // in re-INVITE case, no serverTransaction has been created + serverTransaction = (InviteServerTransaction) + transactionManager.createServerTransaction(sipResponse, + userAgent.getSipPort(), RFC3261.TRANSPORT_UDP, this, + sipRequest); + } + serverTransaction.start(); + + serverTransaction.receivedRequest(sipRequest); + + serverTransaction.sendReponse(sipResponse); + // TODO manage retransmission of the response (send to the transport) + // until ACK arrives, if no ACK is received within 64*T1, confirm dialog + // and terminate it with a BYE + +// logger.getInstance().debug("before dialog.receivedOrSent2xx();"); +// logger.getInstance().debug("dialog state: " + dialog.getState()); + } + + public void acceptCall(SipRequest sipRequest, Dialog dialog) { + sendSuccessfulResponse(sipRequest, dialog); + + dialog.receivedOrSent2xx(); +// logger.getInstance().debug("dialog state: " + dialog.getState()); +// logger.getInstance().debug("after dialog.receivedOrSent2xx();"); + +// setChanged(); +// notifyObservers(sipRequest); + } + + public void rejectCall(SipRequest sipRequest) { + //TODO generate 486, etc. + SipHeaders reqHeaders = sipRequest.getSipHeaders(); + SipHeaderFieldValue callId = reqHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CALLID)); + + Dialog dialog = dialogManager.getDialog(callId.getValue()); + + //TODO manage auto reject Do not disturb (DND) + SipResponse sipResponse = + RequestManager.generateResponse( + sipRequest, + dialog, + RFC3261.CODE_486_BUSYHERE, + RFC3261.REASON_486_BUSYHERE); + + // TODO determine port and transport for server transaction>transport + // from initial invite + // FIXME determine port and transport for server transaction>transport + ServerTransaction serverTransaction = transactionManager + .getServerTransaction(sipRequest); + + serverTransaction.start(); + + serverTransaction.receivedRequest(sipRequest); + + serverTransaction.sendReponse(sipResponse); + + dialog.receivedOrSent300To699(); + + userAgent.getMediaManager().setDatagramSocket(null); +// setChanged(); +// notifyObservers(sipRequest); + } + + ////////////////////////////////////////////////////////// + // UAC methods + ////////////////////////////////////////////////////////// + + public ClientTransaction preProcessInvite(SipRequest sipRequest) + throws SipUriSyntaxException { + + //8.1.2 + SipHeaders requestHeaders = sipRequest.getSipHeaders(); + SipURI destinationUri = RequestManager.getDestinationUri(sipRequest, + logger); + + //TODO if header route is present, addrspec = toproute.nameaddress.addrspec + + String transport = RFC3261.TRANSPORT_UDP; + Hashtable params = destinationUri.getUriParameters(); + if (params != null) { + String reqUriTransport = params.get(RFC3261.PARAM_TRANSPORT); + if (reqUriTransport != null) { + transport = reqUriTransport; + } + } + int port = destinationUri.getPort(); + if (port == SipURI.DEFAULT_PORT) { + port = RFC3261.TRANSPORT_DEFAULT_PORT; + } + SipURI sipUri = userAgent.getConfig().getOutboundProxy(); + if (sipUri == null) { + sipUri = destinationUri; + } + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(sipUri.getHost()); + } catch (UnknownHostException e) { + throw new SipUriSyntaxException("unknown host: " + + sipUri.getHost(), e); + } + ClientTransaction clientTransaction = transactionManager + .createClientTransaction(sipRequest, inetAddress, + port, transport, null, this); + DatagramSocket datagramSocket; + synchronized (this) { + datagramSocket = getDatagramSocket(); + } + try { + SessionDescription sessionDescription = + sdpManager.createSessionDescription(null, + datagramSocket.getLocalPort()); + sipRequest.setBody(sessionDescription.toString().getBytes()); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } + requestHeaders.add(new SipHeaderFieldName(RFC3261.HDR_CONTENT_TYPE), + new SipHeaderFieldValue(RFC3261.CONTENT_TYPE_SDP)); + return clientTransaction; + } + + public void preProcessReInvite(SipRequest sipRequest) { + //TODO + } + + ////////////////////////////////////////////////////////// + // ClientTransactionUser methods + ////////////////////////////////////////////////////////// + + public void errResponseReceived(final SipResponse sipResponse) { + Dialog dialog = dialogManager.getDialog(sipResponse); + if (dialog != null) { + dialog.receivedOrSent300To699(); + dialogManager.removeDialog(dialog.getId()); + } + int statusCode = sipResponse.getStatusCode(); + if (statusCode == RFC3261.CODE_401_UNAUTHORIZED + || statusCode == RFC3261.CODE_407_PROXY_AUTHENTICATION_REQUIRED + && !challenged) { + InviteClientTransaction inviteClientTransaction = + (InviteClientTransaction) + transactionManager.getClientTransaction(sipResponse); + SipRequest sipRequest = inviteClientTransaction.getRequest(); + String password = userAgent.getConfig().getPassword(); + if (password != null && !"".equals(password.trim())) { + challengeManager.handleChallenge(sipRequest, + sipResponse); + } + challenged = true; + return; + } else { + challenged = false; + } + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.error(sipResponse); + } + List guiClosedCallIds = userAgent.getUac().getGuiClosedCallIds(); + String callId = Utils.getMessageCallId(sipResponse); + if (guiClosedCallIds.contains(callId)) { + guiClosedCallIds.remove(callId); + } + userAgent.getMediaManager().setDatagramSocket(null); + } + + public void provResponseReceived(SipResponse sipResponse, Transaction transaction) { + // dialog may have already been created if a previous 1xx has + // already been received + Dialog dialog = dialogManager.getDialog(sipResponse); + boolean isFirstProvRespWithToTag = false; + if (dialog == null) { + SipHeaderFieldValue to = sipResponse.getSipHeaders().get( + new SipHeaderFieldName(RFC3261.HDR_TO)); + String toTag = to.getParam(new SipHeaderParamName(RFC3261.PARAM_TAG)); + if (toTag != null) { + dialog = dialogManager.createDialog(sipResponse); + isFirstProvRespWithToTag = true; + } else { + //TODO maybe stop retransmissions + } + } + + if (dialog != null) { + buildOrUpdateDialogForUac(sipResponse, transaction); + } + +// +// if (dialog == null && sipResponse.getStatusCode() != RFC3261.CODE_100_TRYING) { +// if (logger!=null) logger.debug("dialog not found for prov response"); +// isFirstProvRespWithToTag = true; +// SipHeaderFieldValue to = sipResponse.getSipHeaders() +// .get(new SipHeaderFieldName(RFC3261.HDR_TO)); +// String toTag = to.getParam(new SipHeaderParamName(RFC3261.PARAM_TAG)); +// if (toTag != null) { +// dialog = buildOrUpdateDialogForUac(sipResponse, transaction); +// } +// } + //TODO this notification is probably useless because dialog state modification + // thereafter always notify dialog observers + if (isFirstProvRespWithToTag) { + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.ringing(sipResponse); + } + dialog.receivedOrSent1xx(); + } + List guiClosedCallIds = userAgent.getUac().getGuiClosedCallIds(); + String callId = Utils.getMessageCallId(sipResponse); + if (guiClosedCallIds.contains(callId)) { + SipRequest sipRequest = transaction.getRequest(); + if (logger!=null) logger.debug("cancel after prov response: sipRequest " + sipRequest + + ", sipResponse " + sipResponse); + userAgent.terminate(sipRequest); + } + } + + public void successResponseReceived(SipResponse sipResponse, Transaction transaction) { + SipHeaders responseHeaders = sipResponse.getSipHeaders(); + String cseq = responseHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CSEQ)).getValue(); + String method = cseq.substring(cseq.trim().lastIndexOf(' ') + 1); + if (!RFC3261.METHOD_INVITE.equals(method)) { + return; + } + + challenged = false; + + + + + + + //13.2.2.4 + + List peers = userAgent.getPeers(); + String responseTo = responseHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_TO)).getValue(); + if (!peers.contains(responseTo)) { + peers.add(responseTo); + //timer used to purge dialogs which are not confirmed + //after a given time + ackTimer.schedule(new AckTimerTask(responseTo), + 64 * RFC3261.TIMER_T1); + } + + Dialog dialog = dialogManager.getDialog(sipResponse); + + if (dialog != null) { + //dialog already created with a 180 for example + dialog.setRouteSet(computeRouteSet(sipResponse.getSipHeaders())); + } + dialog = buildOrUpdateDialogForUac(sipResponse, transaction); + + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.calleePickup(sipResponse); + } + + //added for media + SessionDescription sessionDescription = + sdpManager.parse(sipResponse.getBody()); + try { + mediaDestination = sdpManager.getMediaDestination(sessionDescription); + } catch (NoCodecException e) { + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } + String remoteAddress = mediaDestination.getDestination(); + int remotePort = mediaDestination.getPort(); + Codec codec = mediaDestination.getCodec(); + String localAddress = userAgent.getConfig() + .getLocalInetAddress().getHostAddress(); + + userAgent.getMediaManager().successResponseReceived(localAddress, + remoteAddress, remotePort, codec); + + //switch to confirmed state + dialog.receivedOrSent2xx(); + + //generate ack + //p. 82 §3 + SipRequest ack = dialog.buildSubsequentRequest(RFC3261.METHOD_ACK); + + + //update CSeq + + SipHeaders ackHeaders = ack.getSipHeaders(); + SipHeaderFieldName cseqName = new SipHeaderFieldName(RFC3261.HDR_CSEQ); + SipHeaderFieldValue ackCseq = ackHeaders.get(cseqName); + + SipRequest request = transaction.getRequest(); + SipHeaders requestHeaders = request.getSipHeaders(); + SipHeaderFieldValue requestCseq = requestHeaders.get(cseqName); + + ackCseq.setValue(requestCseq.toString().replace(RFC3261.METHOD_INVITE, RFC3261.METHOD_ACK)); + + //add Via with only the branchid parameter + + SipHeaderFieldValue via = new SipHeaderFieldValue(""); + SipHeaderParamName branchIdName = new SipHeaderParamName(RFC3261.PARAM_BRANCH); + via.addParam(branchIdName, Utils.generateBranchId()); + + ackHeaders.add(new SipHeaderFieldName(RFC3261.HDR_VIA), via, 0); + + //TODO authentication headers + + if (request.getBody() == null && sipResponse.getBody() != null) { + //TODO add a real SDP answer + ack.setBody(sipResponse.getBody()); + } + + //TODO check if sdp is acceptable + + SipURI destinationUri = RequestManager.getDestinationUri(ack, logger); + challengeManager.postProcess(ack); + + //TODO if header route is present, addrspec = toproute.nameaddress.addrspec + + String transport = RFC3261.TRANSPORT_UDP; + Hashtable params = destinationUri.getUriParameters(); + if (params != null) { + String reqUriTransport = params.get(RFC3261.PARAM_TRANSPORT); + if (reqUriTransport != null) { + transport = reqUriTransport; + } + } + int port = destinationUri.getPort(); + if (port == SipURI.DEFAULT_PORT) { + port = RFC3261.TRANSPORT_DEFAULT_PORT; + } + + SipURI sipUri = userAgent.getConfig().getOutboundProxy(); + if (sipUri == null) { + sipUri = destinationUri; + } + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(sipUri.getHost()); + } catch (UnknownHostException e) { + if (logger!=null) if (logger!=null) logger.error("unknown host: " + sipUri.getHost(), e); + return; + } + try { + MessageSender sender = transportManager.createClientTransport( + ack, inetAddress, port, transport); + sender.sendMessage(ack); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + } + + + + List guiClosedCallIds = userAgent.getUac().getGuiClosedCallIds(); + String callId = Utils.getMessageCallId(sipResponse); + if (guiClosedCallIds.contains(callId)) { + userAgent.terminate(request); + } + + + } + + public void handleAck(SipRequest ack, Dialog dialog) { + // TODO determine if ACK is ACK of an initial INVITE or a re-INVITE + // in first case, captureRtpSender and incomingRtpReader must be + // created, in the second case, they must be updated. + + if (logger!=null) logger.debug("handleAck"); + + if (mediaDestination == null) { + SipHeaders reqHeaders = ack.getSipHeaders(); + SipHeaderFieldValue contentType = + reqHeaders.get(new SipHeaderFieldName(RFC3261.HDR_CONTENT_TYPE)); + byte[] offerBytes = ack.getBody(); + + if (offerBytes != null && contentType != null && + RFC3261.CONTENT_TYPE_SDP.equals(contentType.getValue())) { + // create response in 200 + try { + SessionDescription answer = sdpManager.parse(offerBytes); + mediaDestination = sdpManager.getMediaDestination(answer); + } catch (NoCodecException e) { + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + return; + } + } + } + String destAddress = mediaDestination.getDestination(); + int destPort = mediaDestination.getPort(); + Codec codec = mediaDestination.getCodec(); + + MediaManager mediaManager = userAgent.getMediaManager(); + if (initialIncomingInvite) { + mediaManager.handleAck(destAddress, destPort, codec); + } else { + mediaManager.updateRemote(destAddress, destPort, codec); + } + + } + + public void transactionTimeout(ClientTransaction clientTransaction) { + // TODO Auto-generated method stub + + } + + public void transactionTransportError() { + // TODO Auto-generated method stub + + } + + ////////////////////////////////////////////////////////// + // ServerTransactionUser methods + ////////////////////////////////////////////////////////// + + public void transactionFailure() { + // TODO manage transaction failure (ACK was not received) + + } + + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/MethodHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/MethodHandler.java new file mode 100644 index 0000000..f87e30a --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/MethodHandler.java @@ -0,0 +1,88 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sdp.SDPManager; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.core.useragent.ChallengeManager; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public abstract class MethodHandler { + + protected UserAgent userAgent; + protected TransactionManager transactionManager; + protected TransportManager transportManager; + protected ChallengeManager challengeManager; + protected SDPManager sdpManager; + protected boolean challenged; + protected Logger logger; + + public MethodHandler(UserAgent userAgent, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + this.userAgent = userAgent; + this.transactionManager = transactionManager; + this.transportManager = transportManager; + this.logger = logger; + challenged = false; + } + + protected SipResponse buildGenericResponse(SipRequest sipRequest, + int statusCode, String reasonPhrase) { + //8.2.6 + SipResponse sipResponse = new SipResponse(statusCode, reasonPhrase); + SipHeaders respHeaders = sipResponse.getSipHeaders(); + SipHeaders reqHeaders = sipRequest.getSipHeaders(); + SipHeaderFieldName fromName = new SipHeaderFieldName(RFC3261.HDR_FROM); + respHeaders.add(fromName, reqHeaders.get(fromName)); + SipHeaderFieldName callIdName = new SipHeaderFieldName(RFC3261.HDR_CALLID); + respHeaders.add(callIdName, reqHeaders.get(callIdName)); + SipHeaderFieldName cseqName = new SipHeaderFieldName(RFC3261.HDR_CSEQ); + respHeaders.add(cseqName, reqHeaders.get(cseqName)); + SipHeaderFieldName viaName = new SipHeaderFieldName(RFC3261.HDR_VIA); + respHeaders.add(viaName, reqHeaders.get(viaName)); + SipHeaderFieldName toName = new SipHeaderFieldName(RFC3261.HDR_TO); + String to = reqHeaders.get(toName).getValue(); + SipHeaderFieldValue toValue = new SipHeaderFieldValue(to); + toValue.addParam(new SipHeaderParamName(RFC3261.PARAM_TAG), + Utils.randomString(10));// TODO 19.3 + respHeaders.add(toName, toValue); + return sipResponse; + } + + public void setChallengeManager(ChallengeManager challengeManager) { + this.challengeManager = challengeManager; + } + + public void setSdpManager(SDPManager sdpManager) { + this.sdpManager = sdpManager; + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/OptionsHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/OptionsHandler.java new file mode 100644 index 0000000..790ffa7 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/OptionsHandler.java @@ -0,0 +1,82 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010, 2012 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import java.io.IOException; +import java.util.Random; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sdp.SessionDescription; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransactionUser; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class OptionsHandler extends MethodHandler + implements ServerTransactionUser { + + public static final int MAX_PORTS = 65536; + + public OptionsHandler(UserAgent userAgent, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(userAgent, transactionManager, transportManager, logger); + } + + public void handleOptions(SipRequest sipRequest) { + SipResponse sipResponse = buildGenericResponse(sipRequest, + RFC3261.CODE_200_OK, RFC3261.REASON_200_OK); + int localPort = new Random().nextInt(MAX_PORTS); + try { + SessionDescription sessionDescription = + sdpManager.createSessionDescription(null, localPort); + sipResponse.setBody(sessionDescription.toString().getBytes()); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } + SipHeaders sipHeaders = sipResponse.getSipHeaders(); + sipHeaders.add(new SipHeaderFieldName(RFC3261.HDR_CONTENT_TYPE), + new SipHeaderFieldValue(RFC3261.CONTENT_TYPE_SDP)); + sipHeaders.add(new SipHeaderFieldName(RFC3261.HDR_ALLOW), + new SipHeaderFieldValue(Utils.generateAllowHeader())); + ServerTransaction serverTransaction = + transactionManager.createServerTransaction( + sipResponse, userAgent.getSipPort(), RFC3261.TRANSPORT_UDP, + this, sipRequest); + serverTransaction.start(); + serverTransaction.receivedRequest(sipRequest); + serverTransaction.sendReponse(sipResponse); + } + + @Override + public void transactionFailure() { + // TODO Auto-generated method stub + + } + +} diff --git a/src/net/sourceforge/peers/sip/core/useragent/handlers/RegisterHandler.java b/src/net/sourceforge/peers/sip/core/useragent/handlers/RegisterHandler.java new file mode 100644 index 0000000..2af1584 --- /dev/null +++ b/src/net/sourceforge/peers/sip/core/useragent/handlers/RegisterHandler.java @@ -0,0 +1,293 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.core.useragent.handlers; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Hashtable; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.core.useragent.InitialRequestManager; +import net.sourceforge.peers.sip.core.useragent.RequestManager; +import net.sourceforge.peers.sip.core.useragent.SipListener; +import net.sourceforge.peers.sip.core.useragent.UserAgent; +import net.sourceforge.peers.sip.syntaxencoding.NameAddress; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.ClientTransactionUser; +import net.sourceforge.peers.sip.transaction.NonInviteClientTransaction; +import net.sourceforge.peers.sip.transaction.Transaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + +public class RegisterHandler extends MethodHandler + implements ClientTransactionUser { + + public static final int REFRESH_MARGIN = 10; // seconds + + private InitialRequestManager initialRequestManager; + + private Timer timer; + + private String requestUriStr; + private String profileUriStr; + private String callIDStr; + + //FIXME should be on a profile based context + private boolean unregisterInvoked; + private boolean registered; + + public RegisterHandler(UserAgent userAgent, + TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(userAgent, transactionManager, transportManager, logger); + } + + //TODO factorize common code here and in invitehandler + public synchronized ClientTransaction preProcessRegister(SipRequest sipRequest) + throws SipUriSyntaxException { + registered = false; + unregisterInvoked = false; + SipHeaders sipHeaders = sipRequest.getSipHeaders(); + SipURI destinationUri = RequestManager.getDestinationUri(sipRequest, + logger); + int port = destinationUri.getPort(); + if (port == SipURI.DEFAULT_PORT) { + port = RFC3261.TRANSPORT_DEFAULT_PORT; + } + //TODO if header route is present, addrspec = toproute.nameaddress.addrspec + String transport = RFC3261.TRANSPORT_UDP; + Hashtable params = destinationUri.getUriParameters(); + if (params != null) { + String reqUriTransport = params.get(RFC3261.PARAM_TRANSPORT); + if (reqUriTransport != null) { + transport = reqUriTransport; + } + } + SipURI sipUri = userAgent.getConfig().getOutboundProxy(); + if (sipUri == null) { + sipUri = destinationUri; + } + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(sipUri.getHost()); + } catch (UnknownHostException e) { + throw new SipUriSyntaxException("unknown host: " + + sipUri.getHost(), e); + } + ClientTransaction clientTransaction = transactionManager + .createClientTransaction(sipRequest, inetAddress, port, + transport, null, this); + //TODO 10.2 + SipHeaderFieldValue to = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_TO)); + SipHeaderFieldValue from = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_FROM)); + String fromValue = from.getValue(); + to.setValue(fromValue); + requestUriStr = destinationUri.toString(); + profileUriStr = NameAddress.nameAddressToUri(fromValue); + callIDStr = sipHeaders.get(new SipHeaderFieldName(RFC3261.HDR_CALLID)) + .toString(); + return clientTransaction; + } + + public void unregister() { + timer.cancel(); + unregisterInvoked = true; + challenged = false; + } + + ////////////////////////////////////////////////////////// + // ClientTransactionUser methods + ////////////////////////////////////////////////////////// + + public void errResponseReceived(SipResponse sipResponse) { + String password = userAgent.getConfig().getPassword(); + if (password != null && !"".equals(password.trim())) { + int statusCode = sipResponse.getStatusCode(); + if (statusCode == RFC3261.CODE_401_UNAUTHORIZED + || statusCode == + RFC3261.CODE_407_PROXY_AUTHENTICATION_REQUIRED) { + if (challenged) { + notifyListener(sipResponse); + } else { + challenged = true; + NonInviteClientTransaction nonInviteClientTransaction = + (NonInviteClientTransaction) + transactionManager.getClientTransaction(sipResponse); + SipRequest sipRequest = + nonInviteClientTransaction.getRequest(); + challengeManager.handleChallenge(sipRequest, sipResponse); + } + } else { // not 401 nor 407 + SipHeaders sipHeaders = sipResponse.getSipHeaders(); + SipHeaderFieldName viaName = new SipHeaderFieldName( + RFC3261.HDR_VIA); + SipHeaderFieldValue via = sipHeaders.get(viaName); + SipHeaderParamName receivedName = new SipHeaderParamName( + RFC3261.PARAM_RECEIVED); + String viaValue = via.getValue(); + int pos = viaValue.indexOf(" "); + if (pos > -1) { + viaValue = viaValue.substring(pos + 1); + pos = viaValue.indexOf(RFC3261.TRANSPORT_PORT_SEP); + if (pos > -1) { + viaValue = viaValue.substring(0, pos); + } else { + pos = viaValue.indexOf(RFC3261.PARAM_SEPARATOR); + if (pos > -1) { + viaValue = viaValue.substring(0, pos); + } + } + } + String received = via.getParam(receivedName); + if (received != null && !"".equals(received.trim())) { + if (viaValue.equals(received)) { + notifyListener(sipResponse); + } else { // received != via ip address + try { + InetAddress receivedInetAddress = + InetAddress.getByName(received); + Config config = userAgent.getConfig(); + config.setPublicInetAddress(receivedInetAddress); + userAgent.register(); + } catch (UnknownHostException e) { + notifyListener(sipResponse); + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } catch (SipUriSyntaxException e) { + notifyListener(sipResponse); + if (logger!=null) if (logger!=null) logger.error(e.getMessage(), e); + } + } + } else { // received not provided + notifyListener(sipResponse); + } + } + } else { // no password configured + notifyListener(sipResponse); + } + } + + private void notifyListener(SipResponse sipResponse) { + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.registerFailed(sipResponse); + } + challenged = false; + } + + public void provResponseReceived(SipResponse sipResponse, + Transaction transaction) { + //meaningless + } + + public synchronized void successResponseReceived(SipResponse sipResponse, + Transaction transaction) { + // 1. retrieve request corresponding to response + // 2. if request was not an unregister, extract contact and expires, + // and start register refresh timer + // 3. notify sip listener of register success event. + SipRequest sipRequest = transaction.getRequest(); + SipHeaderFieldName contactName = new SipHeaderFieldName( + RFC3261.HDR_CONTACT); + SipHeaderFieldValue requestContact = sipRequest.getSipHeaders() + .get(contactName); + SipHeaderParamName expiresParam = new SipHeaderParamName( + RFC3261.PARAM_EXPIRES); + String expires = requestContact.getParam(expiresParam); + challenged = false; + if (!"0".equals(expires)) { + // each contact contains an expires parameter giving the expiration + // in seconds. Thus the binding must be refreshed before it expires. + SipHeaders sipHeaders = sipResponse.getSipHeaders(); + SipHeaderFieldValue responseContact = sipHeaders.get(contactName); + if (responseContact == null) { + return; + } + expires = responseContact.getParam(expiresParam); + // patch mobicents simple application + registered = true; + int delay = -1; + if (expires == null || "".equals(expires.trim())) { + delay = 3600; + } + if (!unregisterInvoked) { + if (delay == -1) { + delay = Integer.parseInt(expires) - REFRESH_MARGIN; + } + timer = new Timer(getClass().getSimpleName() + + " refresh timer"); + timer.schedule(new RefreshTimerTask(), delay * 1000); + } + } + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.registerSuccessful(sipResponse); + } + } + + public void transactionTimeout(ClientTransaction clientTransaction) { + SipListener sipListener = userAgent.getSipListener(); + if (sipListener != null) { + sipListener.registerFailed(null); + } + } + + public void transactionTransportError() { + //TODO alert user + } + + public boolean isRegistered() { + return registered; + } + + ////////////////////////////////////////////////////////// + // TimerTask + ////////////////////////////////////////////////////////// + + class RefreshTimerTask extends TimerTask { + @Override + public void run() { + try { + initialRequestManager.createInitialRequest(requestUriStr, + RFC3261.METHOD_REGISTER, profileUriStr, callIDStr); + } catch (SipUriSyntaxException e) { + if (logger!=null) if (logger!=null) logger.error("syntax error", e); + } + } + } + + public void setInitialRequestManager(InitialRequestManager initialRequestManager) { + this.initialRequestManager = initialRequestManager; + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/NameAddress.java b/src/net/sourceforge/peers/sip/syntaxencoding/NameAddress.java new file mode 100644 index 0000000..2e03044 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/NameAddress.java @@ -0,0 +1,66 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import net.sourceforge.peers.sip.RFC3261; + +public class NameAddress { + + public static String nameAddressToUri(String nameAddress) { + int leftPos = nameAddress.indexOf(RFC3261.LEFT_ANGLE_BRACKET); + int rightPos = nameAddress.indexOf(RFC3261.RIGHT_ANGLE_BRACKET); + if (leftPos < 0 || rightPos < 0) { + return nameAddress; + } + return nameAddress.substring(leftPos + 1, rightPos); + } + + protected String addrSpec; + protected String displayName; + + public NameAddress(String addrSpec) { + super(); + this.addrSpec = addrSpec; + } + + public NameAddress(String addrSpec, String displayName) { + super(); + this.addrSpec = addrSpec; + this.displayName = displayName; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + if (displayName != null) { + buf.append(displayName); + buf.append(' '); + } + buf.append(RFC3261.LEFT_ANGLE_BRACKET); + buf.append(addrSpec); + buf.append(RFC3261.RIGHT_ANGLE_BRACKET); + return buf.toString(); + } + + public String getAddrSpec() { + return addrSpec; + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/RunnableSipParser.java b/src/net/sourceforge/peers/sip/syntaxencoding/RunnableSipParser.java new file mode 100644 index 0000000..ae13a56 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/RunnableSipParser.java @@ -0,0 +1,29 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +public class RunnableSipParser implements Runnable { + + public void run() { + // TODO Auto-generated method stub + + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeader.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeader.java new file mode 100644 index 0000000..48ecebf --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeader.java @@ -0,0 +1,54 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +public class SipHeader { + + private SipHeaderFieldName name; + private SipHeaderFieldValue value; + + SipHeader(SipHeaderFieldName name, SipHeaderFieldValue value) { + super(); + this.name = name; + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SipHeader) { + SipHeader objHdr = (SipHeader) obj; + return name.equals(objHdr.name); + } + return false; + } + + public SipHeaderFieldName getName() { + return name; + } + + public SipHeaderFieldValue getValue() { + return value; + } + + public void setValue(SipHeaderFieldValue value) { + this.value = value; + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldMultiValue.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldMultiValue.java new file mode 100644 index 0000000..9f2381d --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldMultiValue.java @@ -0,0 +1,49 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import java.util.List; + +public class SipHeaderFieldMultiValue extends SipHeaderFieldValue { + + private List values; + + private static String toString(List list) { + if (list == null) { + return null; + } + String arrToString = list.toString(); + return arrToString.substring(1, arrToString.length() - 1); + } + + public SipHeaderFieldMultiValue(List values) { + super(toString(values)); + this.values = values; + } + + public List getValues() { + return values; + } + + @Override + public String toString() { + return toString(values); + } +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldName.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldName.java new file mode 100644 index 0000000..ad49802 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldName.java @@ -0,0 +1,63 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +public class SipHeaderFieldName { + + private final static SipHeadersTable SIP_HEADER_TABLE = + new SipHeadersTable(); + + private String name; + + public SipHeaderFieldName(String name) { + super(); + if (name.length() == 1) { + this.name = SIP_HEADER_TABLE.getLongForm(name.charAt(0)); + } else { + this.name = name; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + String objName = ((SipHeaderFieldName)obj).getName(); + if (name.equalsIgnoreCase(objName)) { + return true; + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldValue.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldValue.java new file mode 100644 index 0000000..1209781 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderFieldValue.java @@ -0,0 +1,103 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import java.util.HashMap; + +import net.sourceforge.peers.sip.RFC3261; + + +public class SipHeaderFieldValue { + + private String value; + + private HashMap params; + + public SipHeaderFieldValue(String value) { + int startPos = value.indexOf(RFC3261.RIGHT_ANGLE_BRACKET); + int pos; + if (startPos > -1) { + pos = value.indexOf(RFC3261.PARAM_SEPARATOR, startPos); + } else { + pos = value.indexOf(RFC3261.PARAM_SEPARATOR); + } + String paramsString; + if (pos > -1) { + this.value = value.substring(0,pos); + paramsString = value.substring(pos); + } else { + this.value = value; + paramsString = ""; + } + params = new HashMap(); + if (paramsString.contains(RFC3261.PARAM_SEPARATOR)) { + String[] arr = paramsString.split(RFC3261.PARAM_SEPARATOR); + if (arr.length > 1) { + for (int i = 1; i < arr.length; ++i) { + String paramName = arr[i]; + String paramValue = ""; + pos = paramName.indexOf(RFC3261.PARAM_ASSIGNMENT); + if (pos > -1) { + paramName = arr[i].substring(0, pos); + paramValue = arr[i].substring(pos + 1); + } + params.put(new SipHeaderParamName(paramName), paramValue); + } + } + } + } + + public String getParam(SipHeaderParamName name) { + return params.get(name); + } + + public void addParam(SipHeaderParamName name, String value) { + params.put(name, value); + } + + public void removeParam(SipHeaderParamName name) { + params.remove(name); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + if (params == null || params.isEmpty()) { + return value; + } + StringBuffer buf = new StringBuffer(value); + for (SipHeaderParamName name: params.keySet()) { + buf.append(RFC3261.PARAM_SEPARATOR).append(name); + String value = params.get(name); + if (!"".equals(value.trim())) { + buf.append(RFC3261.PARAM_ASSIGNMENT).append(value); + } + } + return buf.toString(); + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderParamName.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderParamName.java new file mode 100644 index 0000000..38a3c3d --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaderParamName.java @@ -0,0 +1,56 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +public class SipHeaderParamName { + + private String name; + + public SipHeaderParamName(String name) { + this.name = name; + } + + + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + String objName = ((SipHeaderParamName)obj).getName(); + if (name.equalsIgnoreCase(objName)) { + return true; + } + return false; + } + + @Override + public int hashCode() { + return name.toLowerCase().hashCode(); + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaders.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaders.java new file mode 100644 index 0000000..c617e87 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeaders.java @@ -0,0 +1,102 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import java.util.ArrayList; + +import net.sourceforge.peers.sip.RFC3261; + + + +public class SipHeaders { + + private ArrayList headers; + + public SipHeaders() { + headers = new ArrayList(); + } + + /** + * + * @param name + * @param value + * @param index -1 to add at the end + */ + public void add(SipHeaderFieldName name, SipHeaderFieldValue value, int index) { + SipHeader header = new SipHeader(name, value); + if (headers.contains(header)) { + header = headers.get(headers.indexOf(header)); + SipHeaderFieldValue oldValue = header.getValue(); + //TODO check is header can be multi valued + if (oldValue instanceof SipHeaderFieldMultiValue) { + SipHeaderFieldMultiValue oldMultiVal = (SipHeaderFieldMultiValue) oldValue; + oldMultiVal.getValues().add(value); + } else { + ArrayList arr = new ArrayList(); + arr.add(oldValue); + arr.add(value); + header.setValue(new SipHeaderFieldMultiValue(arr)); + } + } else { + if (index == -1) { + headers.add(header); + } else { + headers.add(index, header); + } + } + } + + public void add(SipHeaderFieldName name, SipHeaderFieldValue value) { + add(name, value, -1); + } + + @SuppressWarnings("unlikely-arg-type") + public void remove(SipHeaderFieldName name) { + headers.remove(name); + } + + public boolean contains(SipHeaderFieldName name) { + return headers.contains(new SipHeader(name, null)); + } + + public SipHeaderFieldValue get(SipHeaderFieldName name) { + int index = headers.indexOf(new SipHeader(name, null)); + if (index < 0) { + return null; + } + return headers.get(index).getValue(); + } + + public int getCount() { + return headers.size(); + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + for (SipHeader header : headers) { + buf.append(header.getName().toString()); + buf.append(": "); + buf.append(header.getValue()); + buf.append(RFC3261.CRLF); + } + return buf.toString(); + } +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipHeadersTable.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeadersTable.java new file mode 100644 index 0000000..0bc916c --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipHeadersTable.java @@ -0,0 +1,53 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import java.util.HashMap; + +import net.sourceforge.peers.sip.RFC3261; + + +public class SipHeadersTable { + + private HashMap headers; + + /** + * should be instanciated only once, it was a singleton. + */ + public SipHeadersTable() { + headers = new HashMap(); + //RFC 3261 Section 10 + headers.put(RFC3261.COMPACT_HDR_CALLID, RFC3261.HDR_CALLID); + headers.put(RFC3261.COMPACT_HDR_CONTACT, RFC3261.HDR_CONTACT); + headers.put(RFC3261.COMPACT_HDR_CONTENT_ENCODING, RFC3261.HDR_CONTENT_ENCODING); + headers.put(RFC3261.COMPACT_HDR_CONTENT_LENGTH, RFC3261.HDR_CONTENT_LENGTH); + headers.put(RFC3261.COMPACT_HDR_CONTENT_TYPE, RFC3261.HDR_CONTENT_TYPE); + headers.put(RFC3261.COMPACT_HDR_FROM, RFC3261.HDR_FROM); + headers.put(RFC3261.COMPACT_HDR_SUBJECT, RFC3261.HDR_SUBJECT); + headers.put(RFC3261.COMPACT_HDR_SUPPORTED, RFC3261.HDR_SUPPORTED); + headers.put(RFC3261.COMPACT_HDR_TO, RFC3261.HDR_TO); + headers.put(RFC3261.COMPACT_HDR_VIA, RFC3261.HDR_VIA); + } + + public String getLongForm(char compactForm) { + return headers.get(compactForm); + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipParser.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipParser.java new file mode 100644 index 0000000..5d6f1e0 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipParser.java @@ -0,0 +1,189 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; + + +public class SipParser { + + private BufferedReader reader; + + private final static int BUFF_SIZE = 1024; + + private List singleValueHeaders; + + public SipParser() { + singleValueHeaders = new ArrayList(); + singleValueHeaders.add(new SipHeaderFieldName( + RFC3261.HDR_WWW_AUTHENTICATE)); + singleValueHeaders.add(new SipHeaderFieldName( + RFC3261.HDR_AUTHORIZATION)); + singleValueHeaders.add(new SipHeaderFieldName( + RFC3261.HDR_PROXY_AUTHENTICATE)); + singleValueHeaders.add(new SipHeaderFieldName( + RFC3261.HDR_PROXY_AUTHORIZATION)); + singleValueHeaders.add(new SipHeaderFieldName( + RFC3261.HDR_SUPPORTED)); + singleValueHeaders.add(new SipHeaderFieldName( + RFC3261.HDR_SUBJECT)); + } + + public synchronized SipMessage parse(InputStream in) + throws IOException, SipParserException { + + InputStreamReader inputStreamReader = new InputStreamReader(in); + reader = new BufferedReader(inputStreamReader); + + String startLine = reader.readLine(); + while (startLine == null || startLine.equals("")) { + startLine = reader.readLine(); + } + SipMessage sipMessage; + if (startLine.toUpperCase().startsWith(RFC3261.DEFAULT_SIP_VERSION)) { + sipMessage = parseSipResponse(startLine); + } + else { + sipMessage = parseSipRequest(startLine); + } + parseHeaders(sipMessage); + parseBody(sipMessage); + return sipMessage; + } + + private SipRequest parseSipRequest(String startLine) throws SipParserException { + String[] params = startLine.split(" "); + if (params.length != 3) { + throw new SipParserException("invalid request line"); + } + if (!RFC3261.DEFAULT_SIP_VERSION.equals(params[2].toUpperCase())) { + throw new SipParserException("unsupported SIP version"); + } + SipURI requestUri; + try { + requestUri = new SipURI(params[1]); + } catch (SipUriSyntaxException e) { + throw new SipParserException(e); + } + return new SipRequest(params[0], requestUri); + } + + private SipResponse parseSipResponse(String startLine) throws SipParserException { + String[] params = startLine.split(" "); + if (params.length < 3) { + throw new SipParserException("incorrect status line"); + } + if (!RFC3261.DEFAULT_SIP_VERSION.equals(params[0].toUpperCase())) { + throw new SipParserException("unsupported SIP version"); + } + StringBuffer buf = new StringBuffer(); + for (int i = 2; i < params.length; ++i) { + buf.append(params[i]).append(" "); + } + buf.deleteCharAt(buf.length() - 1); + return new SipResponse(Integer.parseInt(params[1]), buf.toString()); + } + + private void parseHeaders(SipMessage sipMessage) throws IOException, SipParserException { + SipHeaders sipHeaders = new SipHeaders(); + String headerLine = reader.readLine(); + if (headerLine == null) { + throw new SipParserException(sipMessage.toString()); + } + while (!"".equals(headerLine)) { + String nextLine = reader.readLine(); + if (nextLine != null && + (nextLine.startsWith(" ") || nextLine.startsWith("\t"))) { + StringBuffer buf = new StringBuffer(headerLine); + while (nextLine != null && + (nextLine.startsWith(" ") || nextLine.startsWith("\t"))) { + buf.append(' '); + buf.append(nextLine.trim()); + nextLine = reader.readLine(); + } + headerLine = buf.toString(); + } + if (headerLine == null) { + throw new SipParserException(sipMessage.toString()); + } + int columnPos = headerLine.indexOf(RFC3261.FIELD_NAME_SEPARATOR); + if (columnPos < 0) { + throw new SipParserException("Invalid header line"); + } + SipHeaderFieldName sipHeaderName = new SipHeaderFieldName( + headerLine.substring(0, columnPos).trim()); + String value = headerLine.substring(columnPos + 1).trim(); + SipHeaderFieldValue sipHeaderValue; + if (!singleValueHeaders.contains(sipHeaderName) && + value.indexOf(RFC3261.HEADER_SEPARATOR) > -1) { + String[] values = value.split(RFC3261.HEADER_SEPARATOR); + List list = + new ArrayList(); + for (String s: values) { + list.add(new SipHeaderFieldValue(s)); + } + sipHeaderValue = new SipHeaderFieldMultiValue(list); + } else { + sipHeaderValue = new SipHeaderFieldValue(value); + } + sipHeaders.add(sipHeaderName, sipHeaderValue); + headerLine = nextLine; + } + sipMessage.setSipHeaders(sipHeaders); + } + + public void parseBody(SipMessage sipMessage) throws IOException, SipParserException { + SipHeaderFieldValue contentLengthValue = + sipMessage.getSipHeaders().get(new SipHeaderFieldName( + RFC3261.HDR_CONTENT_LENGTH)); + if (contentLengthValue == null) { + return; + } + int length = Integer.parseInt(contentLengthValue.toString()); + byte[] buff = new byte[BUFF_SIZE]; + int i; + int count = 0; + while (count < length && (i = reader.read()) != -1) { + if (count >= buff.length) { + byte[] aux = new byte[buff.length + BUFF_SIZE]; + System.arraycopy(buff, 0, aux, 0, buff.length); + buff = aux; + + } + buff[count++] = (byte)i; + } + if (count != buff.length) { + byte[] aux = new byte[count]; + System.arraycopy(buff, 0, aux, 0, count); + buff = aux; + } + sipMessage.setBody(buff); + } +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipParserException.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipParserException.java new file mode 100644 index 0000000..02f1553 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipParserException.java @@ -0,0 +1,42 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +public class SipParserException extends Exception { + + private static final long serialVersionUID = 1L; + + public SipParserException() { + super(); + } + + public SipParserException(String message, Throwable cause) { + super(message, cause); + } + + public SipParserException(String message) { + super(message); + } + + public SipParserException(Throwable cause) { + super(cause); + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipURI.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipURI.java new file mode 100644 index 0000000..b522603 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipURI.java @@ -0,0 +1,136 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +import java.util.Hashtable; + +import net.sourceforge.peers.sip.RFC3261; + + + +public class SipURI { + + public final static int DEFAULT_PORT = -1; + + private String stringRepresentation; + /** + * telephone-subscriber and optional port are not managed + */ + private String userinfo; + private String host; + private int port = DEFAULT_PORT; + /** + * Use empty strings in value if the parameter has no value + */ + private Hashtable uriParameters; + //headers not implemented + //private Hashtable headers; + + public SipURI(String sipUri) + throws SipUriSyntaxException { + stringRepresentation = sipUri; + StringBuffer buf = new StringBuffer(sipUri); + String scheme = RFC3261.SIP_SCHEME + RFC3261.SCHEME_SEPARATOR; + if (!sipUri.startsWith(scheme)) { + throw new SipUriSyntaxException("SIP URI must start with " + scheme); + } + buf.delete(0, scheme.length()); + int atPos = buf.indexOf("@"); + if (atPos == 0) { + throw new SipUriSyntaxException("userinfo cannot start with a '@'"); + } + if (atPos > 0) { + userinfo = buf.substring(0, atPos); + buf.delete(0, atPos + 1); + } + int endHostport = buf.indexOf(";"); + if (endHostport == 0) { + throw new SipUriSyntaxException("hostport not present or it cannot start with ';'"); + } + if (endHostport < 0) { + endHostport = buf.length(); + } + String hostport = buf.substring(0, endHostport); + buf.delete(0, endHostport); + int colonPos = hostport.indexOf(':'); + if (colonPos > -1) { + if (colonPos == hostport.length() - 1) { + throw new SipUriSyntaxException("hostport cannot terminate with a ':'"); + } + port = Integer.parseInt(hostport.substring(colonPos + 1)); + } else { + colonPos = hostport.length(); + } + host = hostport.substring(0, colonPos); + if (buf.length() == 1) { + //if there is only one ';' at the end of the uri => do not + //parse uri-parameters and headers + buf.deleteCharAt(0); + } + if (buf.length() <= 0) { + return; + } + uriParameters = new Hashtable(); + while (buf.length() > 0) { + buf.deleteCharAt(0);//delete the first ';' + int nextSemicolon = buf.indexOf(";"); + if (nextSemicolon < 0) { + nextSemicolon = buf.length(); + } + int nextEquals = buf.indexOf("="); + if (nextEquals < 0) { + nextEquals = nextSemicolon; + } + if (nextEquals > nextSemicolon) { + nextEquals = nextSemicolon; + } + int afterEquals; + if (nextEquals + 1 > nextSemicolon) { + afterEquals = nextSemicolon; + } else { + afterEquals = nextEquals + 1; + } + uriParameters.put(buf.substring(0, nextEquals), buf.substring(afterEquals, nextSemicolon)); + buf.delete(0, nextSemicolon); + } + } + + @Override + public String toString() { + return stringRepresentation; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public Hashtable getUriParameters() { + return uriParameters; + } + + public String getUserinfo() { + return userinfo; + } + +} diff --git a/src/net/sourceforge/peers/sip/syntaxencoding/SipUriSyntaxException.java b/src/net/sourceforge/peers/sip/syntaxencoding/SipUriSyntaxException.java new file mode 100644 index 0000000..a9aeb17 --- /dev/null +++ b/src/net/sourceforge/peers/sip/syntaxencoding/SipUriSyntaxException.java @@ -0,0 +1,43 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.syntaxencoding; + +public class SipUriSyntaxException extends Exception { + + private static final long serialVersionUID = 1L; + + public SipUriSyntaxException() { + super(); + } + + public SipUriSyntaxException(String message, Throwable cause) { + super(message, cause); + } + + public SipUriSyntaxException(String message) { + super(message); + } + + public SipUriSyntaxException(Throwable cause) { + super(cause); + } + + +} diff --git a/src/net/sourceforge/peers/sip/transaction/ClientTransaction.java b/src/net/sourceforge/peers/sip/transaction/ClientTransaction.java new file mode 100644 index 0000000..a42ff57 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/ClientTransaction.java @@ -0,0 +1,29 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.sip.transport.SipResponse; + +public interface ClientTransaction { + + public void receivedResponse(SipResponse sipResponse); + public void start(); + public String getContact(); +} diff --git a/src/net/sourceforge/peers/sip/transaction/ClientTransactionUser.java b/src/net/sourceforge/peers/sip/transaction/ClientTransactionUser.java new file mode 100644 index 0000000..c280a53 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/ClientTransactionUser.java @@ -0,0 +1,31 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.sip.transport.SipResponse; + +public interface ClientTransactionUser { + public void transactionTimeout(ClientTransaction clientTransaction); + public void provResponseReceived(SipResponse sipResponse, Transaction transaction); + //TODO eventually pass transaction to the transaction user + public void errResponseReceived(SipResponse sipResponse);//3XX is considered as an error response + public void successResponseReceived(SipResponse sipResponse, Transaction transaction); + public void transactionTransportError(); +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransaction.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransaction.java new file mode 100644 index 0000000..4b90256 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransaction.java @@ -0,0 +1,251 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transport.MessageSender; +import net.sourceforge.peers.sip.transport.SipClientTransportUser; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public class InviteClientTransaction extends InviteTransaction + implements ClientTransaction, SipClientTransportUser { + + public final InviteClientTransactionState INIT; + public final InviteClientTransactionState CALLING; + public final InviteClientTransactionState PROCEEDING; + public final InviteClientTransactionState COMPLETED; + public final InviteClientTransactionState TERMINATED; + + protected ClientTransactionUser transactionUser; + protected String transport; + + private InviteClientTransactionState state; + //private SipClientTransport sipClientTransport; + private MessageSender messageSender; + private int nbRetrans; + private SipRequest ack; + private int remotePort; + private InetAddress remoteInetAddress; + + InviteClientTransaction(String branchId, InetAddress inetAddress, + int port, String transport, SipRequest sipRequest, + ClientTransactionUser transactionUser, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager, Logger logger) { + super(branchId, timer, transportManager, transactionManager, + logger); + + this.transport = transport; + + SipHeaderFieldValue via = new SipHeaderFieldValue(""); + via.addParam(new SipHeaderParamName(RFC3261.PARAM_BRANCH), branchId); + sipRequest.getSipHeaders().add(new SipHeaderFieldName(RFC3261.HDR_VIA), via, 0); + + nbRetrans = 0; + + INIT = new InviteClientTransactionStateInit(getId(), this, logger); + state = INIT; + CALLING = new InviteClientTransactionStateCalling(getId(), this, + logger); + PROCEEDING = new InviteClientTransactionStateProceeding(getId(), this, + logger); + COMPLETED = new InviteClientTransactionStateCompleted(getId(), this, + logger); + TERMINATED = new InviteClientTransactionStateTerminated(getId(), this, + logger); + + //17.1.1.2 + + request = sipRequest; + this.transactionUser = transactionUser; + + remotePort = port; + remoteInetAddress = inetAddress; + + try { + messageSender = transportManager.createClientTransport( + request, remoteInetAddress, remotePort, transport); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + + } + + public void setState(InviteClientTransactionState state) { + this.state.log(state); + this.state = state; + if(TERMINATED.equals(state)) { + //transactionManager.removeClientTransaction(branchId, method); + transactionManager = null; + } + } + + public void start() { + state.start(); + //send request using transport information and sipRequest +// try { +// sipClientTransport = SipTransportFactory.getInstance() +// .createClientTransport(this, request, remoteInetAddress, +// remotePort, transport); +// sipClientTransport.send(request); +// } catch (IOException e) { +// //e.printStackTrace(); +// transportError(); +// } + + try { + messageSender.sendMessage(request); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + if (logger!=null) logger.debug("InviteClientTransaction.start"); + + if (RFC3261.TRANSPORT_UDP.equals(transport)) { + //start timer A with value T1 for retransmission + timer.schedule(new TimerA(), RFC3261.TIMER_T1); + } + + //TODO start timer B with value 64*T1 for transaction timeout + timer.schedule(new TimerB(), 64 * RFC3261.TIMER_T1); + } + + public synchronized void receivedResponse(SipResponse sipResponse) { + responses.add(sipResponse); + // 17.1.1 + int statusCode = sipResponse.getStatusCode(); + if (statusCode < RFC3261.CODE_MIN_PROV) { + if (logger!=null) logger.error("invalid response code"); + } else if (statusCode < RFC3261.CODE_MIN_SUCCESS) { + state.received1xx(); + } else if (statusCode < RFC3261.CODE_MIN_REDIR) { + state.received2xx(); + } else if (statusCode <= RFC3261.CODE_MAX) { + state.received300To699(); + } else { + if (logger!=null) logger.error("invalid response code"); + } + } + + public void transportError() { + state.transportError(); + } + + void createAndSendAck() { + + //p.126 last paragraph + + //17.1.1.3 + ack = new SipRequest(RFC3261.METHOD_ACK, request.getRequestUri()); + SipHeaderFieldValue topVia = Utils.getTopVia(request); + SipHeaders ackSipHeaders = ack.getSipHeaders(); + ackSipHeaders.add(new SipHeaderFieldName(RFC3261.HDR_VIA), topVia); + Utils.copyHeader(request, ack, RFC3261.HDR_CALLID); + Utils.copyHeader(request, ack, RFC3261.HDR_FROM); + Utils.copyHeader(getLastResponse(), ack, RFC3261.HDR_TO); + //TODO what happens if a prov response is received after a 200+ ... + SipHeaders requestSipHeaders = request.getSipHeaders(); + SipHeaderFieldName cseqName = new SipHeaderFieldName(RFC3261.HDR_CSEQ); + SipHeaderFieldValue cseq = requestSipHeaders.get(cseqName); + cseq.setValue(cseq.toString().replace(RFC3261.METHOD_INVITE, RFC3261.METHOD_ACK)); + ackSipHeaders.add(cseqName, cseq); + Utils.copyHeader(request, ack, RFC3261.HDR_ROUTE); + + sendAck(); + } + + void sendAck() { + //ack is passed to the transport layer... + //TODO manage ACK retrans + //sipClientTransport.send(ack); + try { + messageSender.sendMessage(ack); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + } + + void sendRetrans() { + ++nbRetrans; + //sipClientTransport.send(request); + try { + messageSender.sendMessage(request); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + timer.schedule(new TimerA(), (long)Math.pow(2, nbRetrans) * RFC3261.TIMER_T1); + } + + public void requestTransportError(SipRequest sipRequest, Exception e) { + // TODO Auto-generated method stub + + } + + public void responseTransportError(Exception e) { + // TODO Auto-generated method stub + + } + + class TimerA extends TimerTask { + @Override + public void run() { + state.timerAFires(); + } + } + + class TimerB extends TimerTask { + @Override + public void run() { + state.timerBFires(); + } + } + + class TimerD extends TimerTask { + @Override + public void run() { + state.timerDFires(); + } + } + + public String getContact() { + if (messageSender != null) { + return messageSender.getContact(); + } + return null; + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionState.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionState.java new file mode 100644 index 0000000..0e42895 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionState.java @@ -0,0 +1,45 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.AbstractState; + + +public abstract class InviteClientTransactionState extends AbstractState { + + protected InviteClientTransaction inviteClientTransaction; + + public InviteClientTransactionState(String id, + InviteClientTransaction inviteClientTransaction, Logger logger) { + super(id, logger); + this.inviteClientTransaction = inviteClientTransaction; + } + + public void start() {} + public void timerAFires() {} + public void timerBFires() {} + public void received2xx() {} + public void received1xx() {} + public void received300To699() {} + public void transportError() {} + public void timerDFires() {} + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateCalling.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateCalling.java new file mode 100644 index 0000000..0f8b2a2 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateCalling.java @@ -0,0 +1,82 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + + +public class InviteClientTransactionStateCalling extends InviteClientTransactionState { + + public InviteClientTransactionStateCalling(String id, + InviteClientTransaction inviteClientTransaction, Logger logger) { + super(id, inviteClientTransaction, logger); + } + + @Override + public void timerAFires() { + InviteClientTransactionState nextState = inviteClientTransaction.CALLING; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.sendRetrans(); + } + + @Override + public void timerBFires() { + timerBFiresOrTransportError(); + } + + @Override + public void transportError() { + timerBFiresOrTransportError(); + } + + private void timerBFiresOrTransportError() { + InviteClientTransactionState nextState = inviteClientTransaction.TERMINATED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.transactionUser.transactionTimeout( + inviteClientTransaction); + } + + @Override + public void received2xx() { + InviteClientTransactionState nextState = inviteClientTransaction.TERMINATED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.transactionUser.successResponseReceived( + inviteClientTransaction.getLastResponse(), inviteClientTransaction); + } + + @Override + public void received1xx() { + InviteClientTransactionState nextState = inviteClientTransaction.PROCEEDING; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.transactionUser.provResponseReceived( + inviteClientTransaction.getLastResponse(), inviteClientTransaction); + } + + @Override + public void received300To699() { + InviteClientTransactionState nextState = inviteClientTransaction.COMPLETED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.createAndSendAck(); + inviteClientTransaction.transactionUser.errResponseReceived( + inviteClientTransaction.getLastResponse()); + } + + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateCompleted.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateCompleted.java new file mode 100644 index 0000000..1d7969f --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateCompleted.java @@ -0,0 +1,59 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + +public class InviteClientTransactionStateCompleted extends + InviteClientTransactionState { + + public InviteClientTransactionStateCompleted(String id, + InviteClientTransaction inviteClientTransaction, Logger logger) { + super(id, inviteClientTransaction, logger); + int delay = 0; + if (RFC3261.TRANSPORT_UDP.equals(inviteClientTransaction.transport)) { + delay = RFC3261.TIMER_INVITE_CLIENT_TRANSACTION; + } + inviteClientTransaction.timer.schedule(inviteClientTransaction.new TimerD(), delay); + } + + @Override + public void received300To699() { + InviteClientTransactionState nextState = inviteClientTransaction.COMPLETED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.sendAck(); + } + + @Override + public void transportError() { + InviteClientTransactionState nextState = inviteClientTransaction.TERMINATED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.transactionUser.transactionTransportError(); + } + + @Override + public void timerDFires() { + InviteClientTransactionState nextState = inviteClientTransaction.TERMINATED; + inviteClientTransaction.setState(nextState); + } + + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateInit.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateInit.java new file mode 100644 index 0000000..6014615 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateInit.java @@ -0,0 +1,37 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class InviteClientTransactionStateInit extends InviteClientTransactionState { + + public InviteClientTransactionStateInit(String id, + InviteClientTransaction inviteClientTransaction, Logger logger) { + super(id, inviteClientTransaction, logger); + } + + @Override + public void start() { + InviteClientTransactionState nextState = inviteClientTransaction.CALLING; + inviteClientTransaction.setState(nextState); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateProceeding.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateProceeding.java new file mode 100644 index 0000000..b39af48 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateProceeding.java @@ -0,0 +1,58 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class InviteClientTransactionStateProceeding extends + InviteClientTransactionState { + + public InviteClientTransactionStateProceeding(String id, + InviteClientTransaction inviteClientTransaction, Logger logger) { + super(id, inviteClientTransaction, logger); + } + + @Override + public void received1xx() { + InviteClientTransactionState nextState = inviteClientTransaction.PROCEEDING; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.transactionUser.provResponseReceived( + inviteClientTransaction.getLastResponse(), inviteClientTransaction); + } + + @Override + public void received2xx() { + InviteClientTransactionState nextState = inviteClientTransaction.TERMINATED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.transactionUser.successResponseReceived( + inviteClientTransaction.getLastResponse(), inviteClientTransaction); + } + + @Override + public void received300To699() { + InviteClientTransactionState nextState = inviteClientTransaction.COMPLETED; + inviteClientTransaction.setState(nextState); + inviteClientTransaction.createAndSendAck(); + inviteClientTransaction.transactionUser.errResponseReceived( + inviteClientTransaction.getLastResponse()); + } + + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateTerminated.java b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateTerminated.java new file mode 100644 index 0000000..5e7a0f5 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteClientTransactionStateTerminated.java @@ -0,0 +1,32 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class InviteClientTransactionStateTerminated extends + InviteClientTransactionState { + + public InviteClientTransactionStateTerminated(String id, + InviteClientTransaction inviteClientTransaction, Logger logger) { + super(id, inviteClientTransaction, logger); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransaction.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransaction.java new file mode 100644 index 0000000..bfdef14 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransaction.java @@ -0,0 +1,182 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.SipServerTransportUser; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public class InviteServerTransaction extends InviteTransaction + implements ServerTransaction, SipServerTransportUser { + + public final InviteServerTransactionState INIT; + public final InviteServerTransactionState PROCEEDING; + public final InviteServerTransactionState COMPLETED; + public final InviteServerTransactionState CONFIRMED; + public final InviteServerTransactionState TERMINATED; + + protected String transport; + protected int nbRetrans; + protected ServerTransactionUser serverTransactionUser; + + private InviteServerTransactionState state; + //private SipServerTransport sipServerTransport; + private int port; + + InviteServerTransaction(String branchId, int port, String transport, + SipResponse sipResponse, ServerTransactionUser serverTransactionUser, + SipRequest sipRequest, Timer timer, TransactionManager transactionManager, + TransportManager transportManager, Logger logger) { + super(branchId, timer, transportManager, transactionManager, logger); + + INIT = new InviteServerTransactionStateInit(getId(), this, logger); + state = INIT; + PROCEEDING = new InviteServerTransactionStateProceeding(getId(), this, + logger); + COMPLETED = new InviteServerTransactionStateCompleted(getId(), this, + logger); + CONFIRMED = new InviteServerTransactionStateConfirmed(getId(), this, + logger); + TERMINATED = new InviteServerTransactionStateTerminated(getId(), this, + logger); + + this.request = sipRequest; + this.port = port; + this.transport = transport; + responses.add(sipResponse); + nbRetrans = 0; + this.serverTransactionUser = serverTransactionUser; + //TODO pass INV to TU, send 100 if TU won't in 200ms + } + + public void start() { + state.start(); + +// sipServerTransport = SipTransportFactory.getInstance() +// .createServerTransport(this, port, transport); + try { + transportManager.createServerTransport(transport, port); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + } + } + + public void receivedRequest(SipRequest sipRequest) { + String method = sipRequest.getMethod(); + if (RFC3261.METHOD_INVITE.equals(method)) { + state.receivedInvite(); + } else { + // if not INVITE, we consider that a ACK is received + // in the case the call was not successful + state.receivedAck(); + } + + } + + public void sendReponse(SipResponse sipResponse) { + //TODO check that a retransmission response will be considered as + //equal (for contains) to the first response + if (!responses.contains(sipResponse)) { + responses.add(sipResponse); + } + int statusCode = sipResponse.getStatusCode(); + if (statusCode == RFC3261.CODE_MIN_PROV) { + // TODO 100 trying + } else if (statusCode < RFC3261.CODE_MIN_SUCCESS) { + state.received101To199(); + } else if (statusCode < RFC3261.CODE_MIN_REDIR) { + state.received2xx(); + } else if (statusCode <= RFC3261.CODE_MAX) { + state.received300To699(); + } else { + if (logger!=null) logger.error("invalid response code"); + } + } + + public void setState(InviteServerTransactionState state) { + this.state.log(state); + this.state = state; + } + + public void messageReceived(SipMessage sipMessage) { + // TODO Auto-generated method stub + + } + + void sendLastResponse() { + //sipServerTransport.sendResponse(responses.get(responses.size() - 1)); + int nbOfResponses = responses.size(); + if (nbOfResponses > 0) { + try { + transportManager.sendResponse(responses.get(nbOfResponses - 1)); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + } + } + } + + public SipResponse getLastResponse() { + int nbOfResponses = responses.size(); + if (nbOfResponses > 0) { + return responses.get(nbOfResponses - 1); + } + return null; + } + + // TODO send provional response + /* + * maybe the 200 response mechanism could be retrieved for 1xx responses. + */ + +// void stopSipServerTransport() { +// sipServerTransport.stop(); +// } + + class TimerG extends TimerTask { + @Override + public void run() { + state.timerGFires(); + } + } + + class TimerH extends TimerTask { + @Override + public void run() { + state.timerHFiresOrTransportError(); + } + } + + class TimerI extends TimerTask { + @Override + public void run() { + state.timerIFires(); + } + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionState.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionState.java new file mode 100644 index 0000000..5537c7e --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionState.java @@ -0,0 +1,46 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.AbstractState; + +public abstract class InviteServerTransactionState extends AbstractState { + + protected InviteServerTransaction inviteServerTransaction; + + public InviteServerTransactionState(String id, + InviteServerTransaction inviteServerTransaction, Logger logger) { + super(id, logger); + this.inviteServerTransaction = inviteServerTransaction; + } + + public void start() {} + public void receivedInvite() {} + public void received101To199() {} + public void transportError() {} + public void received2xx() {} + public void received300To699() {} + public void timerGFires() {} + public void timerHFiresOrTransportError() {} + public void receivedAck() {} + public void timerIFires() {} + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateCompleted.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateCompleted.java new file mode 100644 index 0000000..820673e --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateCompleted.java @@ -0,0 +1,74 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + +public class InviteServerTransactionStateCompleted extends + InviteServerTransactionState { + + public InviteServerTransactionStateCompleted(String id, + InviteServerTransaction inviteServerTransaction, Logger logger) { + super(id, inviteServerTransaction, logger); + } + + @Override + public void timerGFires() { + InviteServerTransactionState nextState = inviteServerTransaction.COMPLETED; + inviteServerTransaction.setState(nextState); + inviteServerTransaction.sendLastResponse(); + long delay = (long)Math.pow(2, + ++inviteServerTransaction.nbRetrans) * RFC3261.TIMER_T1; + inviteServerTransaction.timer.schedule( + inviteServerTransaction.new TimerG(), + Math.min(delay, RFC3261.TIMER_T2)); + } + + @Override + public void timerHFiresOrTransportError() { + InviteServerTransactionState nextState = inviteServerTransaction.TERMINATED; + inviteServerTransaction.setState(nextState); + inviteServerTransaction.serverTransactionUser.transactionFailure(); + } + + @Override + public void receivedAck() { + InviteServerTransactionState nextState = inviteServerTransaction.CONFIRMED; + inviteServerTransaction.setState(nextState); + int delay; + if (RFC3261.TRANSPORT_UDP.equals(inviteServerTransaction.transport)) { + delay = RFC3261.TIMER_T4; + } else { + delay = 0; + } + inviteServerTransaction.timer.schedule( + inviteServerTransaction.new TimerI(), delay); + } + + @Override + public void receivedInvite() { + InviteServerTransactionState nextState = inviteServerTransaction.COMPLETED; + inviteServerTransaction.setState(nextState); + // retransmission + inviteServerTransaction.sendLastResponse(); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateConfirmed.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateConfirmed.java new file mode 100644 index 0000000..cf4164f --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateConfirmed.java @@ -0,0 +1,45 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class InviteServerTransactionStateConfirmed extends + InviteServerTransactionState { + + public InviteServerTransactionStateConfirmed(String id, + InviteServerTransaction inviteServerTransaction, Logger logger) { + super(id, inviteServerTransaction, logger); + } + + @Override + public void timerIFires() { + InviteServerTransactionState nextState = + inviteServerTransaction.TERMINATED; + inviteServerTransaction.setState(nextState); + // TODO destroy invite server transaction immediately + // (dereference it in transaction manager serverTransactions hashtable) + + inviteServerTransaction.transactionManager.removeServerTransaction( + inviteServerTransaction.branchId, + inviteServerTransaction.method); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateInit.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateInit.java new file mode 100644 index 0000000..a3e5251 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateInit.java @@ -0,0 +1,37 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class InviteServerTransactionStateInit extends + InviteServerTransactionState { + + public InviteServerTransactionStateInit(String id, + InviteServerTransaction inviteServerTransaction, Logger logger) { + super(id, inviteServerTransaction, logger); + } + + @Override + public void start() { + InviteServerTransactionState nextState = inviteServerTransaction.PROCEEDING; + inviteServerTransaction.setState(nextState); + } +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateProceeding.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateProceeding.java new file mode 100644 index 0000000..324431c --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateProceeding.java @@ -0,0 +1,74 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + +public class InviteServerTransactionStateProceeding extends + InviteServerTransactionState { + + public InviteServerTransactionStateProceeding(String id, + InviteServerTransaction inviteServerTransaction, Logger logger) { + super(id, inviteServerTransaction, logger); + } + + @Override + public void received101To199() { + InviteServerTransactionState nextState = inviteServerTransaction.PROCEEDING; + inviteServerTransaction.setState(nextState); + //TODO inviteServerTransaction.sendProvisionalResponse(); + inviteServerTransaction.sendLastResponse(); + } + + @Override + public void transportError() { + InviteServerTransactionState nextState = inviteServerTransaction.TERMINATED; + inviteServerTransaction.setState(nextState); + } + + @Override + public void received2xx() { + InviteServerTransactionState nextState = inviteServerTransaction.TERMINATED; + inviteServerTransaction.setState(nextState); + inviteServerTransaction.sendLastResponse(); + } + + @Override + public void received300To699() { + InviteServerTransactionState nextState = inviteServerTransaction.COMPLETED; + inviteServerTransaction.setState(nextState); + inviteServerTransaction.sendLastResponse(); + if (RFC3261.TRANSPORT_UDP.equals(inviteServerTransaction.transport)) { + inviteServerTransaction.timer.schedule( + inviteServerTransaction.new TimerG(), RFC3261.TIMER_T1); + } + inviteServerTransaction.timer.schedule( + inviteServerTransaction.new TimerH(), 64 * RFC3261.TIMER_T1); + } + + @Override + public void receivedInvite() { + InviteServerTransactionState nextState = inviteServerTransaction.PROCEEDING; + inviteServerTransaction.setState(nextState); + } + + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateTerminated.java b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateTerminated.java new file mode 100644 index 0000000..c8e9099 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteServerTransactionStateTerminated.java @@ -0,0 +1,32 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class InviteServerTransactionStateTerminated extends + InviteServerTransactionState { + + public InviteServerTransactionStateTerminated(String id, + InviteServerTransaction inviteServerTransaction, Logger logger) { + super(id, inviteServerTransaction, logger); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/InviteTransaction.java b/src/net/sourceforge/peers/sip/transaction/InviteTransaction.java new file mode 100644 index 0000000..579f55a --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/InviteTransaction.java @@ -0,0 +1,37 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.util.Timer; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transport.TransportManager; + +public abstract class InviteTransaction extends Transaction { + + protected InviteTransaction(String branchId, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager, Logger logger) { + super(branchId, RFC3261.METHOD_INVITE, timer, transportManager, + transactionManager, logger); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransaction.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransaction.java new file mode 100644 index 0000000..d8bf5be --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransaction.java @@ -0,0 +1,202 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.transport.MessageSender; +import net.sourceforge.peers.sip.transport.SipClientTransportUser; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public class NonInviteClientTransaction extends NonInviteTransaction + implements ClientTransaction, SipClientTransportUser { + + public final NonInviteClientTransactionState INIT; + public final NonInviteClientTransactionState TRYING; + public final NonInviteClientTransactionState PROCEEDING; + public final NonInviteClientTransactionState COMPLETED; + public final NonInviteClientTransactionState TERMINATED; + + protected ClientTransactionUser transactionUser; + protected String transport; + protected int nbRetrans; + + private NonInviteClientTransactionState state; + //private SipClientTransport sipClientTransport; + private MessageSender messageSender; + private int remotePort; + private InetAddress remoteInetAddress; + + NonInviteClientTransaction(String branchId, InetAddress inetAddress, + int port, String transport, SipRequest sipRequest, + ClientTransactionUser transactionUser, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager, Logger logger) { + super(branchId, sipRequest.getMethod(), timer, transportManager, + transactionManager, logger); + + this.transport = transport; + + SipHeaderFieldValue via = new SipHeaderFieldValue(""); + via.addParam(new SipHeaderParamName(RFC3261.PARAM_BRANCH), branchId); + sipRequest.getSipHeaders().add(new SipHeaderFieldName(RFC3261.HDR_VIA), via, 0); + + nbRetrans = 0; + + INIT = new NonInviteClientTransactionStateInit(getId(), this, logger); + state = INIT; + TRYING = new NonInviteClientTransactionStateTrying(getId(), this, + logger); + PROCEEDING = new NonInviteClientTransactionStateProceeding(getId(), + this, logger); + COMPLETED = new NonInviteClientTransactionStateCompleted(getId(), + this, logger); + TERMINATED = new NonInviteClientTransactionStateTerminated(getId(), + this, logger); + + request = sipRequest; + this.transactionUser = transactionUser; + + remotePort = port; + remoteInetAddress = inetAddress; + + try { + messageSender = transportManager.createClientTransport( + request, remoteInetAddress, remotePort, transport); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + //TODO send request + } + + public void setState(NonInviteClientTransactionState state) { + this.state.log(state); + this.state = state; + } + + public void start() { + state.start(); + + //17.1.2.2 + +// try { +// sipClientTransport = SipTransportFactory.getInstance() +// .createClientTransport(this, request, remoteInetAddress, +// remotePort, transport); +// sipClientTransport.send(request); +// } catch (IOException e) { +// //e.printStackTrace(); +// transportError(); +// } + try { + messageSender.sendMessage(request); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + + if (RFC3261.TRANSPORT_UDP.equals(transport)) { + //start timer E with value T1 for retransmission + timer.schedule(new TimerE(), RFC3261.TIMER_T1); + } + + timer.schedule(new TimerF(), 64 * RFC3261.TIMER_T1); + } + + void sendRetrans(long delay) { + //sipClientTransport.send(request); + try { + messageSender.sendMessage(request); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + transportError(); + } + timer.schedule(new TimerE(), delay); + } + + public void transportError() { + state.transportError(); + } + + public synchronized void receivedResponse(SipResponse sipResponse) { + responses.add(sipResponse); + // 17.1.1 + int statusCode = sipResponse.getStatusCode(); + if (statusCode < RFC3261.CODE_MIN_PROV) { + if (logger!=null) logger.error("invalid response code"); + } else if (statusCode < RFC3261.CODE_MIN_SUCCESS) { + state.received1xx(); + } else if (statusCode <= RFC3261.CODE_MAX) { + state.received200To699(); + } else { + if (logger!=null) logger.error("invalid response code"); + } + } + + public void requestTransportError(SipRequest sipRequest, Exception e) { + // TODO Auto-generated method stub + + } + + public void responseTransportError(Exception e) { + // TODO Auto-generated method stub + + } + + class TimerE extends TimerTask { + @Override + public void run() { + state.timerEFires(); + } + } + + class TimerF extends TimerTask { + @Override + public void run() { + state.timerFFires(); + } + } + + class TimerK extends TimerTask { + @Override + public void run() { + state.timerKFires(); + } + } + + public String getContact() { + if (messageSender != null) { + return messageSender.getContact(); + } + return null; + } +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionState.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionState.java new file mode 100644 index 0000000..4dcf91a --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionState.java @@ -0,0 +1,44 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.AbstractState; + +public abstract class NonInviteClientTransactionState extends AbstractState { + + protected NonInviteClientTransaction nonInviteClientTransaction; + + public NonInviteClientTransactionState(String id, + NonInviteClientTransaction nonInviteClientTransaction, + Logger logger) { + super(id, logger); + this.nonInviteClientTransaction = nonInviteClientTransaction; + } + + public void start() {} + public void timerEFires() {} + public void timerFFires() {} + public void transportError() {} + public void received1xx() {} + public void received200To699() {} + public void timerKFires() {} + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateCompleted.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateCompleted.java new file mode 100644 index 0000000..ca541eb --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateCompleted.java @@ -0,0 +1,48 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + +public class NonInviteClientTransactionStateCompleted extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateCompleted(String id, + NonInviteClientTransaction nonInviteClientTransaction, + Logger logger) { + super(id, nonInviteClientTransaction, logger); + int delay = 0; + if (RFC3261.TRANSPORT_UDP.equals( + nonInviteClientTransaction.transport)) { + delay = RFC3261.TIMER_T4; + } + nonInviteClientTransaction.timer.schedule( + nonInviteClientTransaction.new TimerK(), delay); + } + + @Override + public void timerKFires() { + NonInviteClientTransactionState nextState = + nonInviteClientTransaction.TERMINATED; + nonInviteClientTransaction.setState(nextState); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateInit.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateInit.java new file mode 100644 index 0000000..d61d0d7 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateInit.java @@ -0,0 +1,38 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class NonInviteClientTransactionStateInit extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateInit(String id, + NonInviteClientTransaction nonInviteClientTransaction, + Logger logger) { + super(id, nonInviteClientTransaction, logger); + } + + @Override + public void start() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.TRYING; + nonInviteClientTransaction.setState(nextState); + } +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateProceeding.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateProceeding.java new file mode 100644 index 0000000..eda980d --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateProceeding.java @@ -0,0 +1,81 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transport.SipResponse; + +public class NonInviteClientTransactionStateProceeding extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateProceeding(String id, + NonInviteClientTransaction nonInviteClientTransaction, + Logger logger) { + super(id, nonInviteClientTransaction, logger); + } + + @Override + public void timerEFires() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.PROCEEDING; + nonInviteClientTransaction.setState(nextState); + ++nonInviteClientTransaction.nbRetrans; + nonInviteClientTransaction.sendRetrans(RFC3261.TIMER_T2); + } + + @Override + public void timerFFires() { + timerFFiresOrTransportError(); + } + + @Override + public void transportError() { + timerFFiresOrTransportError(); + } + + private void timerFFiresOrTransportError() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.TERMINATED; + nonInviteClientTransaction.setState(nextState); + nonInviteClientTransaction.transactionUser.transactionTimeout( + nonInviteClientTransaction); + } + + @Override + public void received1xx() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.PROCEEDING; + nonInviteClientTransaction.setState(nextState); + } + + @Override + public void received200To699() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.COMPLETED; + nonInviteClientTransaction.setState(nextState); + SipResponse response = nonInviteClientTransaction.getLastResponse(); + int code = response.getStatusCode(); + if (code < RFC3261.CODE_MIN_REDIR) { + nonInviteClientTransaction.transactionUser.successResponseReceived( + response, nonInviteClientTransaction); + } else { + nonInviteClientTransaction.transactionUser.errResponseReceived( + response); + } + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateTerminated.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateTerminated.java new file mode 100644 index 0000000..7273559 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateTerminated.java @@ -0,0 +1,33 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class NonInviteClientTransactionStateTerminated extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateTerminated(String id, + NonInviteClientTransaction nonInviteClientTransaction, + Logger logger) { + super(id, nonInviteClientTransaction, logger); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateTrying.java b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateTrying.java new file mode 100644 index 0000000..fcce37b --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteClientTransactionStateTrying.java @@ -0,0 +1,84 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transport.SipResponse; + +public class NonInviteClientTransactionStateTrying extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateTrying(String id, + NonInviteClientTransaction nonInviteClientTransaction, + Logger logger) { + super(id, nonInviteClientTransaction, logger); + } + + @Override + public void timerEFires() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.TRYING; + nonInviteClientTransaction.setState(nextState); + long delay = (long)Math.pow(2, + ++nonInviteClientTransaction.nbRetrans) * RFC3261.TIMER_T1; + nonInviteClientTransaction.sendRetrans(Math.min(delay, RFC3261.TIMER_T2)); + } + + @Override + public void timerFFires() { + timerFFiresOrTransportError(); + } + + @Override + public void transportError() { + timerFFiresOrTransportError(); + } + + private void timerFFiresOrTransportError() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.TERMINATED; + nonInviteClientTransaction.setState(nextState); + nonInviteClientTransaction.transactionUser.transactionTimeout( + nonInviteClientTransaction); + } + + @Override + public void received1xx() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.PROCEEDING; + nonInviteClientTransaction.setState(nextState); + nonInviteClientTransaction.transactionUser.provResponseReceived( + nonInviteClientTransaction.getLastResponse(), nonInviteClientTransaction); + } + + @Override + public void received200To699() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.COMPLETED; + nonInviteClientTransaction.setState(nextState); + SipResponse response = nonInviteClientTransaction.getLastResponse(); + int code = response.getStatusCode(); + if (code < RFC3261.CODE_MIN_REDIR) { + nonInviteClientTransaction.transactionUser.successResponseReceived( + response, nonInviteClientTransaction); + } else { + nonInviteClientTransaction.transactionUser.errResponseReceived( + response); + } + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransaction.java b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransaction.java new file mode 100644 index 0000000..f876702 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransaction.java @@ -0,0 +1,128 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public class NonInviteServerTransaction extends NonInviteTransaction + implements ServerTransaction/*, SipServerTransportUser*/ { + + public final NonInviteServerTransactionState TRYING; + public final NonInviteServerTransactionState PROCEEDING; + public final NonInviteServerTransactionState COMPLETED; + public final NonInviteServerTransactionState TERMINATED; + + protected ServerTransactionUser serverTransactionUser; + protected Timer timer; + protected String transport; + + private NonInviteServerTransactionState state; + //private int port; + + NonInviteServerTransaction(String branchId, int port, String transport, + String method, ServerTransactionUser serverTransactionUser, + SipRequest sipRequest, Timer timer, TransportManager transportManager, + TransactionManager transactionManager, Logger logger) { + super(branchId, method, timer, transportManager, transactionManager, + logger); + + TRYING = new NonInviteServerTransactionStateTrying(getId(), this, + logger); + state = TRYING; + PROCEEDING = new NonInviteServerTransactionStateProceeding(getId(), + this, logger); + COMPLETED = new NonInviteServerTransactionStateCompleted(getId(), this, + logger); + TERMINATED = new NonInviteServerTransactionStateTerminated(getId(), + this, logger); + + //this.port = port; + this.transport = transport; + this.serverTransactionUser = serverTransactionUser; + request = sipRequest; +// sipServerTransport = SipTransportFactory.getInstance() +// .createServerTransport(this, port, transport); + try { + transportManager.createServerTransport(transport, port); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + } + + //TODO pass request to TU + } + + public void setState(NonInviteServerTransactionState state) { + this.state.log(state); + this.state = state; + } + + public void receivedRequest(SipRequest sipRequest) { + state.receivedRequest(); + } + + public void sendReponse(SipResponse sipResponse) { + responses.add(sipResponse); + int statusCode = sipResponse.getStatusCode(); + if (statusCode < RFC3261.CODE_200_OK) { + state.received1xx(); + } else if (statusCode <= RFC3261.CODE_MAX) { + state.received200To699(); + } + } + + void sendLastResponse() { + //sipServerTransport.sendResponse(responses.get(responses.size() - 1)); + int nbOfResponses = responses.size(); + if (nbOfResponses > 0) { + try { + transportManager.sendResponse(responses.get(nbOfResponses - 1)); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + } + } + } + + public void start() { + // TODO Auto-generated method stub + + } + +// public void messageReceived(SipMessage sipMessage) { +// // TODO Auto-generated method stub +// +// } + + class TimerJ extends TimerTask { + @Override + public void run() { + state.timerJFires(); + } + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionState.java b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionState.java new file mode 100644 index 0000000..25d81e1 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionState.java @@ -0,0 +1,43 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.AbstractState; + +//17.2.2 +public abstract class NonInviteServerTransactionState extends AbstractState { + + protected NonInviteServerTransaction nonInviteServerTransaction; + + public NonInviteServerTransactionState(String id, + NonInviteServerTransaction nonInviteServerTransaction, + Logger logger) { + super(id, logger); + this.nonInviteServerTransaction = nonInviteServerTransaction; + } + + public void received200To699() {} + public void received1xx() {} + public void receivedRequest() {} + public void transportError() {} + public void timerJFires() {} +} + diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateCompleted.java b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateCompleted.java new file mode 100644 index 0000000..524c837 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateCompleted.java @@ -0,0 +1,52 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class NonInviteServerTransactionStateCompleted extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateCompleted(String id, + NonInviteServerTransaction nonInviteServerTransaction, + Logger logger) { + super(id, nonInviteServerTransaction, logger); + } + + @Override + public void timerJFires() { + NonInviteServerTransactionState nextState = nonInviteServerTransaction.TERMINATED; + nonInviteServerTransaction.setState(nextState); + } + + @Override + public void transportError() { + NonInviteServerTransactionState nextState = nonInviteServerTransaction.TERMINATED; + nonInviteServerTransaction.setState(nextState); + } + + @Override + public void receivedRequest() { + NonInviteServerTransactionState nextState = nonInviteServerTransaction.COMPLETED; + nonInviteServerTransaction.setState(nextState); + nonInviteServerTransaction.sendLastResponse(); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateProceeding.java b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateProceeding.java new file mode 100644 index 0000000..9fa5ce6 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateProceeding.java @@ -0,0 +1,73 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + +public class NonInviteServerTransactionStateProceeding extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateProceeding(String id, + NonInviteServerTransaction nonInviteServerTransaction, + Logger logger) { + super(id, nonInviteServerTransaction, logger); + } + + @Override + public void received1xx() { + NonInviteServerTransactionState nextState = + nonInviteServerTransaction.PROCEEDING; + nonInviteServerTransaction.setState(nextState); + nonInviteServerTransaction.sendLastResponse(); + } + + @Override + public void received200To699() { + NonInviteServerTransactionState nextState = + nonInviteServerTransaction.COMPLETED; + nonInviteServerTransaction.setState(nextState); + nonInviteServerTransaction.sendLastResponse(); + int timeout; + if (RFC3261.TRANSPORT_UDP.equals( + nonInviteServerTransaction.transport)) { + timeout = 64 * RFC3261.TIMER_T1; + } else { + timeout = 0; + } + nonInviteServerTransaction.timer.schedule( + nonInviteServerTransaction.new TimerJ(), timeout); + } + + @Override + public void transportError() { + NonInviteServerTransactionState nextState = + nonInviteServerTransaction.TERMINATED; + nonInviteServerTransaction.setState(nextState); + } + + @Override + public void receivedRequest() { + NonInviteServerTransactionState nextState = + nonInviteServerTransaction.PROCEEDING; + nonInviteServerTransaction.setState(nextState); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateTerminated.java b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateTerminated.java new file mode 100644 index 0000000..f0d36f9 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateTerminated.java @@ -0,0 +1,33 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class NonInviteServerTransactionStateTerminated extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateTerminated(String id, + NonInviteServerTransaction nonInviteServerTransaction, + Logger logger) { + super(id, nonInviteServerTransaction, logger); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateTrying.java b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateTrying.java new file mode 100644 index 0000000..d91ac2e --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteServerTransactionStateTrying.java @@ -0,0 +1,48 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.Logger; + +public class NonInviteServerTransactionStateTrying extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateTrying(String id, + NonInviteServerTransaction nonInviteServerTransaction, + Logger logger) { + super(id, nonInviteServerTransaction, logger); + } + + @Override + public void received1xx() { + NonInviteServerTransactionState nextState = + nonInviteServerTransaction.PROCEEDING; + nonInviteServerTransaction.setState(nextState); + nonInviteServerTransaction.sendLastResponse(); + } + + @Override + public void received200To699() { + NonInviteServerTransactionState nextState = + nonInviteServerTransaction.COMPLETED; + nonInviteServerTransaction.setState(nextState); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/NonInviteTransaction.java b/src/net/sourceforge/peers/sip/transaction/NonInviteTransaction.java new file mode 100644 index 0000000..d4ef96f --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/NonInviteTransaction.java @@ -0,0 +1,36 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.util.Timer; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.transport.TransportManager; + +public abstract class NonInviteTransaction extends Transaction { + + protected NonInviteTransaction(String branchId, String method, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager, Logger logger) { + super(branchId, method, timer, transportManager, transactionManager, + logger); + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/ServerTransaction.java b/src/net/sourceforge/peers/sip/transaction/ServerTransaction.java new file mode 100644 index 0000000..bcd8f5c --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/ServerTransaction.java @@ -0,0 +1,32 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; + +public interface ServerTransaction { + + public void start(); + + public void receivedRequest(SipRequest sipRequest); + + public void sendReponse(SipResponse sipResponse); +} diff --git a/src/net/sourceforge/peers/sip/transaction/ServerTransactionUser.java b/src/net/sourceforge/peers/sip/transaction/ServerTransactionUser.java new file mode 100644 index 0000000..b17e708 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/ServerTransactionUser.java @@ -0,0 +1,25 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +public interface ServerTransactionUser { + + public void transactionFailure(); +} diff --git a/src/net/sourceforge/peers/sip/transaction/SipListeningPoint.java b/src/net/sourceforge/peers/sip/transaction/SipListeningPoint.java new file mode 100644 index 0000000..98ab23e --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/SipListeningPoint.java @@ -0,0 +1,64 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + + +public class SipListeningPoint { + + private int localPort; + private String localTransport; + + public SipListeningPoint(int localPort, String localTransport) { + super(); + this.localPort = localPort; + this.localTransport = localTransport; + } + + @Override + public boolean equals(Object obj) { + if (obj.getClass() != SipListeningPoint.class) { + return false; + } + SipListeningPoint other = (SipListeningPoint)obj; + return localPort == other.localPort && + localTransport.equals(other.localTransport); + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(':').append(localPort).append('/').append(localTransport); + return buf.toString(); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + public int getlocalPort() { + return localPort; + } + + public String getlocalTransport() { + return localTransport; + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/Transaction.java b/src/net/sourceforge/peers/sip/transaction/Transaction.java new file mode 100644 index 0000000..2d01588 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/Transaction.java @@ -0,0 +1,79 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Timer; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public abstract class Transaction { + + public static final char ID_SEPARATOR = '|'; + + protected String branchId; + protected String method; + + protected SipRequest request; + protected List responses; + + protected Timer timer; + protected TransportManager transportManager; + protected TransactionManager transactionManager; + + protected Logger logger; + + protected Transaction(String branchId, String method, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager, Logger logger) { + this.branchId = branchId; + this.method = method; + this.timer = timer; + this.transportManager = transportManager; + this.transactionManager = transactionManager; + this.logger = logger; + responses = Collections.synchronizedList(new ArrayList()); + } + + protected String getId() { + StringBuffer buf = new StringBuffer(); + buf.append(branchId).append(ID_SEPARATOR); + buf.append(method); + return buf.toString(); + } + + public SipResponse getLastResponse() { + if (responses.isEmpty()) { + return null; + } + return responses.get(responses.size() - 1); + } + + public SipRequest getRequest() { + return request; + } + +} diff --git a/src/net/sourceforge/peers/sip/transaction/TransactionManager.java b/src/net/sourceforge/peers/sip/transaction/TransactionManager.java new file mode 100644 index 0000000..96dd0ba --- /dev/null +++ b/src/net/sourceforge/peers/sip/transaction/TransactionManager.java @@ -0,0 +1,211 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transaction; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Timer; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipRequest; +import net.sourceforge.peers.sip.transport.SipResponse; +import net.sourceforge.peers.sip.transport.TransportManager; + + +public class TransactionManager { + + protected Timer timer; + + // TODO remove client transactions when they reach terminated state + // TODO check that server transactions are removed in all transitions to terminated + private Hashtable clientTransactions; + private Hashtable serverTransactions; + + private TransportManager transportManager; + private Logger logger; + + public TransactionManager(Logger logger) { + this.logger = logger; + clientTransactions = new Hashtable(); + serverTransactions = new Hashtable(); + timer = new Timer(TransactionManager.class.getSimpleName() + + " " + Timer.class.getSimpleName()); + } + + public ClientTransaction createClientTransaction(SipRequest sipRequest, + InetAddress inetAddress, int port, String transport, + String pBranchId, ClientTransactionUser clientTransactionUser) { + String branchId; + if (pBranchId == null || "".equals(pBranchId.trim()) + || !pBranchId.startsWith(RFC3261.BRANCHID_MAGIC_COOKIE)) { + branchId = Utils.generateBranchId(); + } else { + branchId = pBranchId; + } + String method = sipRequest.getMethod(); + ClientTransaction clientTransaction; + if (RFC3261.METHOD_INVITE.equals(method)) { + clientTransaction = new InviteClientTransaction(branchId, + inetAddress, port, transport, sipRequest, clientTransactionUser, + timer, transportManager, this, logger); + } else { + clientTransaction = new NonInviteClientTransaction(branchId, + inetAddress, port, transport, sipRequest, clientTransactionUser, + timer, transportManager, this, logger); + } + clientTransactions.put(getTransactionId(branchId, method), + clientTransaction); + return clientTransaction; + } + + public ServerTransaction createServerTransaction(SipResponse sipResponse, + int port, String transport, + ServerTransactionUser serverTransactionUser, + SipRequest sipRequest) { + SipHeaderFieldValue via = Utils.getTopVia(sipResponse); + String branchId = via.getParam(new SipHeaderParamName( + RFC3261.PARAM_BRANCH)); + String cseq = sipResponse.getSipHeaders().get( + new SipHeaderFieldName(RFC3261.HDR_CSEQ)).toString(); + String method = cseq.substring(cseq.lastIndexOf(' ') + 1); + ServerTransaction serverTransaction; + // TODO create server transport user and pass it to server transaction + if (RFC3261.METHOD_INVITE.equals(method)) { + serverTransaction = new InviteServerTransaction(branchId, port, + transport, sipResponse, serverTransactionUser, sipRequest, + timer, this, transportManager, logger); + // serverTransaction = new InviteServerTransaction(branchId); + } else { + serverTransaction = new NonInviteServerTransaction(branchId, port, + transport, method, serverTransactionUser, sipRequest, timer, + transportManager, this, logger); + } + serverTransactions.put(getTransactionId(branchId, method), + serverTransaction); + return serverTransaction; + } + + public ClientTransaction getClientTransaction(SipMessage sipMessage) { + SipHeaderFieldValue via = Utils.getTopVia(sipMessage); + String branchId = via.getParam(new SipHeaderParamName( + RFC3261.PARAM_BRANCH)); + String cseq = sipMessage.getSipHeaders().get( + new SipHeaderFieldName(RFC3261.HDR_CSEQ)).toString(); + String method = cseq.substring(cseq.lastIndexOf(' ') + 1); + return clientTransactions.get(getTransactionId(branchId, method)); + } + + public List getClientTransactionsFromCallId(String callId, + String method) { + ArrayList clientTransactionsFromCallId = + new ArrayList(); + for (ClientTransaction clientTransaction: clientTransactions.values()) { + Transaction transaction = (Transaction)clientTransaction; + SipRequest sipRequest = transaction.getRequest(); + String reqCallId = Utils.getMessageCallId(sipRequest); + String reqMethod = sipRequest.getMethod(); + if (callId.equals(reqCallId) && method.equals(reqMethod)) { + clientTransactionsFromCallId.add(clientTransaction); + } + } + return clientTransactionsFromCallId; + } + + public ServerTransaction getServerTransaction(SipMessage sipMessage) { + SipHeaderFieldValue via = Utils.getTopVia(sipMessage); + String branchId = via.getParam(new SipHeaderParamName( + RFC3261.PARAM_BRANCH)); + String method; + if (sipMessage instanceof SipRequest) { + method = ((SipRequest)sipMessage).getMethod(); + } else { + String cseq = sipMessage.getSipHeaders().get( + new SipHeaderFieldName(RFC3261.HDR_CSEQ)).toString(); + method = cseq.substring(cseq.lastIndexOf(' ') + 1); + } + if (RFC3261.METHOD_ACK.equals(method)) { + method = RFC3261.METHOD_INVITE; +// InviteServerTransaction inviteServerTransaction = +// (InviteServerTransaction) +// serverTransactions.get(getTransactionId(branchId, method)); +// if (inviteServerTransaction == null) { +// Logger.debug("received ACK for unknown transaction" + +// " branchId = " + branchId + ", method = " + method); +// } else { +// SipResponse sipResponse = +// inviteServerTransaction.getLastResponse(); +// if (sipResponse == null) { +// Logger.debug("received ACK but no response sent " + +// "branchId = " + branchId + ", method = " + method); +// } else { +// int statusCode = sipResponse.getStatusCode(); +// if (statusCode >= RFC3261.CODE_MIN_SUCCESS && +// statusCode < RFC3261.CODE_MIN_REDIR) { +// // success response => ACK is not in INVITE server +// // transaction +// return null; +// } else { +// // error => ACK belongs to INVITE server transaction +// return inviteServerTransaction; +// } +// } +// } + // TODO if positive response, ACK does not belong to transaction + // retrieve transaction and take responses from transaction + // and check if a positive response has been received + // if it is the case, a new standalone transaction must be created + // for the ACK + } + return serverTransactions.get(getTransactionId(branchId, method)); + } + + public ServerTransaction getServerTransaction(String branchId, String method) { + return serverTransactions.get(getTransactionId(branchId, method)); + } + + void removeServerTransaction(String branchId, String method) { + serverTransactions.remove(getTransactionId(branchId, method)); + } + + void removeClientTransaction(String branchId, String method) { + clientTransactions.remove(getTransactionId(branchId, method)); + } + + private String getTransactionId(String branchId, String method) { + StringBuffer buf = new StringBuffer(); + buf.append(branchId); + buf.append(Transaction.ID_SEPARATOR); + buf.append(method); + return buf.toString(); + } + + public void setTransportManager(TransportManager transportManager) { + this.transportManager = transportManager; + } + +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/Dialog.java b/src/net/sourceforge/peers/sip/transactionuser/Dialog.java new file mode 100644 index 0000000..c51e433 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/Dialog.java @@ -0,0 +1,259 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import java.util.ArrayList; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.syntaxencoding.NameAddress; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldMultiValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; +import net.sourceforge.peers.sip.syntaxencoding.SipUriSyntaxException; +import net.sourceforge.peers.sip.transport.SipRequest; + + +public class Dialog { + + public static final char ID_SEPARATOR = '|'; + public static final int EMPTY_CSEQ = -1; + + public final DialogState INIT; + public final DialogState EARLY; + public final DialogState CONFIRMED; + public final DialogState TERMINATED; + + private DialogState state; + + private String callId; + private String localTag; + private String remoteTag; + + private int localCSeq; + private int remoteCSeq; + private String localUri; + private String remoteUri; + private String remoteTarget; + private boolean secure; + private ArrayList routeSet; + private Logger logger; + + Dialog(String callId, String localTag, String remoteTag, Logger logger) { + super(); + this.callId = callId; + this.localTag = localTag; + this.remoteTag = remoteTag; + this.logger = logger; + + INIT = new DialogStateInit(getId(), this, logger); + state = INIT; + EARLY = new DialogStateEarly(getId(), this, logger); + CONFIRMED = new DialogStateConfirmed(getId(), this, logger); + TERMINATED = new DialogStateTerminated(getId(), this, logger); + + localCSeq = EMPTY_CSEQ; + remoteCSeq = EMPTY_CSEQ; + } + + public void receivedOrSent1xx() { + state.receivedOrSent101To199(); + } + + public void receivedOrSent2xx() { + state.receivedOrSent2xx(); + } + + public void receivedOrSent300To699() { + state.receivedOrSent300To699(); + } + + public void receivedOrSentBye() { + state.receivedOrSentBye(); + } + + public void setState(DialogState state) { + this.state.log(state); + this.state = state; + } + + public SipRequest buildSubsequentRequest(String method) { + //12.2.1.1 + SipURI sipUri; + try { + sipUri = new SipURI(remoteTarget); + } catch (SipUriSyntaxException e) { + throw new RuntimeException(e); + //TODO check remote target when message is received + } + SipRequest subsequentRequest = new SipRequest(method, sipUri); + SipHeaders headers = subsequentRequest.getSipHeaders(); + + //To + + SipHeaderFieldValue to = new SipHeaderFieldValue( + new NameAddress(remoteUri).toString()); + if (remoteTag != null) { + to.addParam(new SipHeaderParamName(RFC3261.PARAM_TAG), remoteTag); + } + headers.add(new SipHeaderFieldName(RFC3261.HDR_TO), to); + + //From + + SipHeaderFieldValue from = new SipHeaderFieldValue( + new NameAddress(localUri).toString()); + if (localTag != null) { + from.addParam(new SipHeaderParamName(RFC3261.PARAM_TAG), localTag); + } + headers.add(new SipHeaderFieldName(RFC3261.HDR_FROM), from); + + //Call-ID + + SipHeaderFieldValue callIdValue = new SipHeaderFieldValue(callId); + headers.add(new SipHeaderFieldName(RFC3261.HDR_CALLID), callIdValue); + + //CSeq + + if (localCSeq == Dialog.EMPTY_CSEQ) { + localCSeq = ((int)(System.currentTimeMillis() / 1000) & 0xFFFFFFFE) >> 1; + } else { + localCSeq++; + } + headers.add(new SipHeaderFieldName(RFC3261.HDR_CSEQ), + new SipHeaderFieldValue(localCSeq + " " + method)); + + //Route + + if (!routeSet.isEmpty()) { + if (routeSet.get(0).contains(RFC3261.LOOSE_ROUTING)) { + ArrayList routes = new ArrayList(); + for (String route : routeSet) { + routes.add(new SipHeaderFieldValue(route)); + } + headers.add(new SipHeaderFieldName(RFC3261.HDR_ROUTE), + new SipHeaderFieldMultiValue(routes)); + } else { + if (logger!=null) logger.error("Trying to forward to a strict router, forbidden in this implementation"); + } + } + + Utils.addCommonHeaders(headers); + + return subsequentRequest; + } + + public String getId() { + StringBuffer buf = new StringBuffer(); + buf.append(callId).append(ID_SEPARATOR); + buf.append(localTag).append(ID_SEPARATOR); + buf.append(remoteTag); + return buf.toString(); + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public int getLocalCSeq() { + return localCSeq; + } + + public void setLocalCSeq(int localCSeq) { + this.localCSeq = localCSeq; + } + + public String getLocalUri() { + return localUri; + } + + public void setLocalUri(String localUri) { + this.localUri = localUri; + } + + public int getRemoteCSeq() { + return remoteCSeq; + } + + public void setRemoteCSeq(int remoteCSeq) { + this.remoteCSeq = remoteCSeq; + } + + public String getRemoteTarget() { + return remoteTarget; + } + + public void setRemoteTarget(String remoteTarget) { + this.remoteTarget = remoteTarget; + } + + public String getRemoteUri() { + return remoteUri; + } + + public void setRemoteUri(String remoteUri) { + this.remoteUri = remoteUri; + } + + public ArrayList getRouteSet() { + return routeSet; + } + + public void setRouteSet(ArrayList routeSet) { + this.routeSet = routeSet; + } + + public boolean isSecure() { + return secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public String getLocalTag() { + return localTag; + } + + public void setLocalTag(String localTag) { + this.localTag = localTag; + } + + public String getRemoteTag() { + return remoteTag; + } + + public void setRemoteTag(String remoteTag) { + this.remoteTag = remoteTag; + } + + public DialogState getState() { + return state; + } + + +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/DialogManager.java b/src/net/sourceforge/peers/sip/transactionuser/DialogManager.java new file mode 100644 index 0000000..5a35ca9 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/DialogManager.java @@ -0,0 +1,116 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import java.util.Collection; +import java.util.Hashtable; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.transport.SipMessage; +import net.sourceforge.peers.sip.transport.SipResponse; + + +public class DialogManager { + + private Hashtable dialogs; + private Logger logger; + + public DialogManager(Logger logger) { + this.logger = logger; + dialogs = new Hashtable(); + } + + /** + * @param sipResponse sip response must contain a To tag, a + * From tag and a Call-ID + * @return the new Dialog created + */ + public synchronized Dialog createDialog(SipResponse sipResponse) { + SipHeaders sipHeaders = sipResponse.getSipHeaders(); + String callID = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CALLID)).toString(); + SipHeaderFieldValue from = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_FROM)); + SipHeaderFieldValue to = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_TO)); + String fromTag = from.getParam(new SipHeaderParamName(RFC3261.PARAM_TAG)); + String toTag = to.getParam(new SipHeaderParamName(RFC3261.PARAM_TAG)); + Dialog dialog; + if (sipHeaders.get(new SipHeaderFieldName(RFC3261.HDR_VIA)) == null) { + //createDialog is called from UAS side, in layer Transaction User + dialog = new Dialog(callID, toTag, fromTag, logger); + } else { + //createDialog is called from UAC side, in syntax encoding layer + dialog = new Dialog(callID, fromTag, toTag, logger); + } + dialogs.put(dialog.getId(), dialog); + return dialog; + } + + public void removeDialog(String dialogId) { + dialogs.remove(dialogId); + } + + public synchronized Dialog getDialog(SipMessage sipMessage) { + SipHeaders sipHeaders = sipMessage.getSipHeaders(); + String callID = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_CALLID)).toString(); + SipHeaderFieldValue from = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_FROM)); + SipHeaderFieldValue to = sipHeaders.get( + new SipHeaderFieldName(RFC3261.HDR_TO)); + SipHeaderParamName tagName = new SipHeaderParamName(RFC3261.PARAM_TAG); + String fromTag = from.getParam(tagName); + String toTag = to.getParam(tagName); + Dialog dialog = dialogs.get(getDialogId(callID, fromTag, toTag)); + if (dialog != null) { + return dialog; + } + return dialogs.get(getDialogId(callID, toTag, fromTag)); + } + + public synchronized Dialog getDialog(String callId) { + for (Dialog dialog : dialogs.values()) { + if (dialog.getCallId().equals(callId)) { + return dialog; + } + } + return null; + } + + private String getDialogId(String callID, String localTag, String remoteTag) { + StringBuffer buf = new StringBuffer(); + buf.append(callID); + buf.append(Dialog.ID_SEPARATOR); + buf.append(localTag); + buf.append(Dialog.ID_SEPARATOR); + buf.append(remoteTag); + return buf.toString(); + } + + public Collection getDialogCollection() { + return dialogs.values(); + } +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/DialogState.java b/src/net/sourceforge/peers/sip/transactionuser/DialogState.java new file mode 100644 index 0000000..9f6a3e0 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/DialogState.java @@ -0,0 +1,40 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.AbstractState; + +public abstract class DialogState extends AbstractState { + + protected Dialog dialog; + + public DialogState(String id, Dialog dialog, Logger logger) { + super(id, logger); + this.dialog = dialog; + } + + public void receivedOrSent101To199() {} + public void receivedOrSent2xx() {} + public void receivedOrSent300To699() {} + //sent or received a BYE for RFC3261 + public void receivedOrSentBye() {} + +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/DialogStateConfirmed.java b/src/net/sourceforge/peers/sip/transactionuser/DialogStateConfirmed.java new file mode 100644 index 0000000..c0a43f2 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/DialogStateConfirmed.java @@ -0,0 +1,54 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import net.sourceforge.peers.Logger; + +public class DialogStateConfirmed extends DialogState { + + public DialogStateConfirmed(String id, Dialog dialog, Logger logger) { + super(id, dialog, logger); + } + + @Override + public void receivedOrSent101To199() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent2xx() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent300To699() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSentBye() { + DialogState nextState = dialog.TERMINATED; + dialog.setState(nextState); + } + +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/DialogStateEarly.java b/src/net/sourceforge/peers/sip/transactionuser/DialogStateEarly.java new file mode 100644 index 0000000..57ddfc6 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/DialogStateEarly.java @@ -0,0 +1,53 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import net.sourceforge.peers.Logger; + +public class DialogStateEarly extends DialogState { + + public DialogStateEarly(String id, Dialog dialog, Logger logger) { + super(id, dialog, logger); + } + + @Override + public void receivedOrSent101To199() { + DialogState nextState = dialog.EARLY; + dialog.setState(nextState); + } + + @Override + public void receivedOrSent2xx() { + DialogState nextState = dialog.CONFIRMED; + dialog.setState(nextState); + } + + @Override + public void receivedOrSent300To699() { + DialogState nextState = dialog.TERMINATED; + dialog.setState(nextState); + } + + @Override + public void receivedOrSentBye() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/DialogStateInit.java b/src/net/sourceforge/peers/sip/transactionuser/DialogStateInit.java new file mode 100644 index 0000000..3a38911 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/DialogStateInit.java @@ -0,0 +1,53 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import net.sourceforge.peers.Logger; + +public class DialogStateInit extends DialogState { + + public DialogStateInit(String id, Dialog dialog, Logger logger) { + super(id, dialog, logger); + } + + @Override + public void receivedOrSent101To199() { + DialogState nextState = dialog.EARLY; + dialog.setState(nextState); + } + + @Override + public void receivedOrSent2xx() { + DialogState nextState = dialog.CONFIRMED; + dialog.setState(nextState); + } + + @Override + public void receivedOrSent300To699() { + DialogState nextState = dialog.TERMINATED; + dialog.setState(nextState); + } + + @Override + public void receivedOrSentBye() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } +} diff --git a/src/net/sourceforge/peers/sip/transactionuser/DialogStateTerminated.java b/src/net/sourceforge/peers/sip/transactionuser/DialogStateTerminated.java new file mode 100644 index 0000000..af0af08 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transactionuser/DialogStateTerminated.java @@ -0,0 +1,54 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transactionuser; + +import net.sourceforge.peers.Logger; + +public class DialogStateTerminated extends DialogState { + + public DialogStateTerminated(String id, Dialog dialog, Logger logger) { + super(id, dialog, logger); + } + + @Override + public void receivedOrSent101To199() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent2xx() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent300To699() { + if (logger!=null) logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSentBye() { + //ignore bye retransmissions +// if (logger!=null) logger.error(id + " invalid transition"); +// throw new IllegalStateException(); + } +} diff --git a/src/net/sourceforge/peers/sip/transport/MessageReceiver.java b/src/net/sourceforge/peers/sip/transport/MessageReceiver.java new file mode 100644 index 0000000..4e006b9 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/MessageReceiver.java @@ -0,0 +1,206 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipParserException; +import net.sourceforge.peers.sip.transaction.ClientTransaction; +import net.sourceforge.peers.sip.transaction.ServerTransaction; +import net.sourceforge.peers.sip.transaction.TransactionManager; + +public abstract class MessageReceiver implements Runnable { + + public static final int BUFFER_SIZE = 2048;//FIXME should correspond to MTU 1024; + public static final String CHARACTER_ENCODING = "US-ASCII"; + + protected int port; + private boolean isListening; + + //private UAS uas; + private SipServerTransportUser sipServerTransportUser; + private TransactionManager transactionManager; + private TransportManager transportManager; + private Config config; + protected Logger logger; + + public MessageReceiver(int port, TransactionManager transactionManager, + TransportManager transportManager, Config config, Logger logger) { + super(); + this.port = port; + this.transactionManager = transactionManager; + this.transportManager = transportManager; + this.config = config; + this.logger = logger; + isListening = true; + } + + public void run() { + while (isListening) { + try { + listen(); + } catch (IOException e) { + if (logger!=null) logger.error("input/output error", e); + } + } + } + + protected abstract void listen() throws IOException; + + protected boolean isRequest(byte[] message) { + String beginning = null; + try { + beginning = new String(message, 0, + RFC3261.DEFAULT_SIP_VERSION.length(), CHARACTER_ENCODING); + } catch (UnsupportedEncodingException e) { + if (logger!=null) logger.error("unsupported encoding", e); + } + if (RFC3261.DEFAULT_SIP_VERSION.equals(beginning)) { + return false; + } + return true; + } + + protected void processMessage(byte[] message, InetAddress sourceIp, + int sourcePort, String transport) throws IOException { + ByteArrayInputStream byteArrayInputStream = + new ByteArrayInputStream(message); + InputStreamReader inputStreamReader = new InputStreamReader( + byteArrayInputStream); + BufferedReader reader = new BufferedReader(inputStreamReader); + String startLine = reader.readLine(); + while ("".equals(startLine)) { + startLine = reader.readLine(); + } + if (startLine == null) { + return; + } + if (!startLine.contains(RFC3261.DEFAULT_SIP_VERSION)) { + // keep-alive, send back to sender + SipTransportConnection sipTransportConnection = + new SipTransportConnection(config.getLocalInetAddress(), + port, sourceIp, sourcePort, transport); + MessageSender messageSender = transportManager.getMessageSender( + sipTransportConnection); + if (messageSender != null) { + messageSender.sendBytes(message); + } + return; + } + StringBuffer direction = new StringBuffer(); + direction.append("RECEIVED from ").append(sourceIp.getHostAddress()); + direction.append("/").append(sourcePort); + if (logger!=null) logger.traceNetwork(new String(message), + direction.toString()); + SipMessage sipMessage = null; + try { + sipMessage = transportManager.sipParser.parse( + new ByteArrayInputStream(message)); + } catch (IOException e) { + if (logger!=null) if (logger!=null) logger.error("input/output error", e); + } catch (SipParserException e) { + if (logger!=null) if (logger!=null) logger.error("SIP parser error", e); + } + if (sipMessage == null) { + return; + } + + // RFC3261 18.2 + + if (sipMessage instanceof SipRequest) { + SipRequest sipRequest = (SipRequest)sipMessage; + + + SipHeaderFieldValue topVia = Utils.getTopVia(sipRequest); + String sentBy = + topVia.getParam(new SipHeaderParamName(RFC3261.PARAM_SENTBY)); + if (sentBy != null) { + int colonPos = sentBy.indexOf(RFC3261.TRANSPORT_PORT_SEP); + if (colonPos < 0) { + colonPos = sentBy.length(); + } + sentBy = sentBy.substring(0, colonPos); + if (!InetAddress.getByName(sentBy).equals(sourceIp)) { + topVia.addParam(new SipHeaderParamName( + RFC3261.PARAM_RECEIVED), + sourceIp.getHostAddress()); + } + } + //RFC3581 + //TODO check rport configuration + SipHeaderParamName rportName = new SipHeaderParamName( + RFC3261.PARAM_RPORT); + String rport = topVia.getParam(rportName); + if (rport != null && "".equals(rport)) { + topVia.removeParam(rportName); + topVia.addParam(rportName, String.valueOf(sourcePort)); + } + + ServerTransaction serverTransaction = + transactionManager.getServerTransaction(sipRequest); + if (serverTransaction == null) { + //uas.messageReceived(sipMessage); + sipServerTransportUser.messageReceived(sipMessage); + } else { + serverTransaction.receivedRequest(sipRequest); + } + } else { + SipResponse sipResponse = (SipResponse)sipMessage; + ClientTransaction clientTransaction = + transactionManager.getClientTransaction(sipResponse); + if (logger!=null) logger.debug("ClientTransaction = " + clientTransaction); + if (clientTransaction == null) { + //uas.messageReceived(sipMessage); + sipServerTransportUser.messageReceived(sipMessage); + } else { + clientTransaction.receivedResponse(sipResponse); + } + } + } + + public synchronized void setListening(boolean isListening) { + this.isListening = isListening; + } + + public synchronized boolean isListening() { + return isListening; + } + + public void setSipServerTransportUser( + SipServerTransportUser sipServerTransportUser) { + this.sipServerTransportUser = sipServerTransportUser; + } + +// public void setUas(UAS uas) { +// this.uas = uas; +// } + +} diff --git a/src/net/sourceforge/peers/sip/transport/MessageSender.java b/src/net/sourceforge/peers/sip/transport/MessageSender.java new file mode 100644 index 0000000..383a205 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/MessageSender.java @@ -0,0 +1,103 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + + +public abstract class MessageSender { + + public static final int KEEY_ALIVE_INTERVAL = 25; // seconds + + protected InetAddress inetAddress; + protected int port; + protected int localPort; + private Config config; + private String transportName; + private Timer timer; + protected Logger logger; + + public MessageSender(int localPort, InetAddress inetAddress, + int port, Config config, + String transportName, Logger logger) { + super(); + this.localPort = localPort; + this.inetAddress = inetAddress; + this.port = port; + this.config = config; + this.transportName = transportName; + timer = new Timer(getClass().getSimpleName() + " " + + Timer.class.getSimpleName()); + this.logger = logger; + //TODO check config + timer.scheduleAtFixedRate(new KeepAlive(), 0, + 1000 * KEEY_ALIVE_INTERVAL); + } + + public abstract void sendMessage(SipMessage sipMessage) throws IOException; + public abstract void sendBytes(byte[] bytes) throws IOException; + + public String getContact() { + StringBuffer buf = new StringBuffer(); + InetAddress myAddress = config.getPublicInetAddress(); + if (myAddress == null) { + myAddress = config.getLocalInetAddress(); + } + buf.append(myAddress.getHostAddress()); + buf.append(RFC3261.TRANSPORT_PORT_SEP); + //buf.append(config.getSipPort()); + buf.append(localPort); + buf.append(RFC3261.PARAM_SEPARATOR); + buf.append(RFC3261.PARAM_TRANSPORT); + buf.append(RFC3261.PARAM_ASSIGNMENT); + buf.append(transportName); + return buf.toString(); + } + + public int getLocalPort() { + return localPort; + } + + public void stopKeepAlives() { + timer.cancel(); + } + + class KeepAlive extends TimerTask { + + @Override + public void run() { + byte[] bytes = (RFC3261.CRLF + RFC3261.CRLF).getBytes(); + try { + sendBytes(bytes); + } catch (IOException e) { + if (logger!=null) logger.error(e.getMessage(), e); + } + } + + } + +} diff --git a/src/net/sourceforge/peers/sip/transport/SipClientTransportUser.java b/src/net/sourceforge/peers/sip/transport/SipClientTransportUser.java new file mode 100644 index 0000000..a0f3cff --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/SipClientTransportUser.java @@ -0,0 +1,27 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + + +public interface SipClientTransportUser { + + public void requestTransportError(SipRequest sipRequest, Exception e); + public void responseTransportError(Exception e); +} diff --git a/src/net/sourceforge/peers/sip/transport/SipMessage.java b/src/net/sourceforge/peers/sip/transport/SipMessage.java new file mode 100644 index 0000000..4d1fbb7 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/SipMessage.java @@ -0,0 +1,80 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; + +public abstract class SipMessage { + + protected String sipVersion; + protected SipHeaders sipHeaders; + protected byte[] body; + + public SipMessage() { + sipVersion = RFC3261.DEFAULT_SIP_VERSION; + sipHeaders = new SipHeaders(); + } + + public String getSipVersion() { + return sipVersion; + } + + public void setSipHeaders(SipHeaders sipHeaders) { + this.sipHeaders = sipHeaders; + } + + public SipHeaders getSipHeaders() { + return sipHeaders; + } + + public byte[] getBody() { + return body; + } + + public void setBody(byte[] body) { + SipHeaderFieldName contentLengthName = + new SipHeaderFieldName(RFC3261.HDR_CONTENT_LENGTH); + SipHeaderFieldValue contentLengthValue = + sipHeaders.get(contentLengthName); + if (contentLengthValue == null) { + contentLengthValue = new SipHeaderFieldValue( + String.valueOf(body.length)); + sipHeaders.add(contentLengthName, contentLengthValue); + } else { + contentLengthValue.setValue(String.valueOf(body.length)); + } + this.body = body; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(sipHeaders.toString()); + buf.append(RFC3261.CRLF); + if (body != null) { + buf.append(new String(body)); + } + return buf.toString(); + } + +} diff --git a/src/net/sourceforge/peers/sip/transport/SipRequest.java b/src/net/sourceforge/peers/sip/transport/SipRequest.java new file mode 100644 index 0000000..25bad49 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/SipRequest.java @@ -0,0 +1,53 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.syntaxencoding.SipURI; + +public class SipRequest extends SipMessage { + protected String method; + protected SipURI requestUri; + //protected String requestUri; + + public SipRequest(String method, SipURI requestUri) { + super(); + this.method = method; + this.requestUri = requestUri; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(method).append(' ').append(requestUri).append( + ' ').append(RFC3261.DEFAULT_SIP_VERSION).append(RFC3261.CRLF); + buf.append(super.toString()); + return buf.toString(); + } + + public String getMethod() { + return method; + } + + public SipURI getRequestUri() { + return requestUri; + } + +} diff --git a/src/net/sourceforge/peers/sip/transport/SipResponse.java b/src/net/sourceforge/peers/sip/transport/SipResponse.java new file mode 100644 index 0000000..8c03cb1 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/SipResponse.java @@ -0,0 +1,50 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import net.sourceforge.peers.sip.RFC3261; + +public class SipResponse extends SipMessage { + protected int statusCode; + protected String reasonPhrase; + + public SipResponse(int statusCode, String reasonPhrase) { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(RFC3261.DEFAULT_SIP_VERSION).append(' ').append(statusCode + ).append(' ').append(reasonPhrase).append(RFC3261.CRLF); + buf.append(super.toString()); + return buf.toString(); + } + + public int getStatusCode() { + return statusCode; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + +} diff --git a/src/net/sourceforge/peers/sip/transport/SipServerTransportUser.java b/src/net/sourceforge/peers/sip/transport/SipServerTransportUser.java new file mode 100644 index 0000000..ee6bf00 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/SipServerTransportUser.java @@ -0,0 +1,25 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +public interface SipServerTransportUser { + + public void messageReceived(SipMessage sipMessage); +} diff --git a/src/net/sourceforge/peers/sip/transport/SipTransportConnection.java b/src/net/sourceforge/peers/sip/transport/SipTransportConnection.java new file mode 100644 index 0000000..9a221a1 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/SipTransportConnection.java @@ -0,0 +1,123 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import java.net.InetAddress; + +import net.sourceforge.peers.sip.RFC3261; + +public class SipTransportConnection { + + public static final int EMPTY_PORT = -1; + + private InetAddress localInetAddress; + private int localPort = EMPTY_PORT; + + private InetAddress remoteInetAddress; + private int remotePort = EMPTY_PORT; + + private String transport;// UDP, TCP or SCTP + + public SipTransportConnection(InetAddress localInetAddress, + int localPort, InetAddress remoteInetAddress, int remotePort, + String transport) { + this.localInetAddress = localInetAddress; + this.localPort = localPort; + this.remoteInetAddress = remoteInetAddress; + this.remotePort = remotePort; + this.transport = transport; + } + + @Override + public boolean equals(Object obj) { + if (obj.getClass() != SipTransportConnection.class) { + return false; + } + SipTransportConnection other = (SipTransportConnection)obj; + if (!transport.equalsIgnoreCase(other.transport)) { + return false; + } + if (RFC3261.TRANSPORT_UDP.equalsIgnoreCase(transport)) { + return localInetAddress.equals(other.localInetAddress) && + localPort == other.localPort; + } + return false; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + appendInetAddress(buf, localInetAddress); + buf.append(':'); + appendPort(buf, localPort); + buf.append('/'); + if (!RFC3261.TRANSPORT_UDP.equalsIgnoreCase(transport)) { + appendInetAddress(buf, remoteInetAddress); + buf.append(':'); + appendPort(buf, remotePort); + buf.append('/'); + } + buf.append(transport.toUpperCase()); + return buf.toString(); + } + + private void appendInetAddress(StringBuffer buf, InetAddress inetAddress) { + if (inetAddress != null) { + buf.append(inetAddress.getHostAddress()); + } else { + buf.append("-"); + } + } + + private void appendPort(StringBuffer buf, int port) { + if (port != EMPTY_PORT) { + buf.append(port); + } else { + buf.append("-"); + } + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + public InetAddress getLocalInetAddress() { + return localInetAddress; + } + + public int getLocalPort() { + return localPort; + } + + public InetAddress getRemoteInetAddress() { + return remoteInetAddress; + } + + public int getRemotePort() { + return remotePort; + } + + public String getTransport() { + return transport; + } + + +} diff --git a/src/net/sourceforge/peers/sip/transport/TransportManager.java b/src/net/sourceforge/peers/sip/transport/TransportManager.java new file mode 100644 index 0000000..c1668b0 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/TransportManager.java @@ -0,0 +1,478 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007-2013 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import static net.sourceforge.peers.sip.RFC3261.DEFAULT_SIP_VERSION; +import static net.sourceforge.peers.sip.RFC3261.IPV4_TTL; +import static net.sourceforge.peers.sip.RFC3261.PARAM_MADDR; +import static net.sourceforge.peers.sip.RFC3261.PARAM_TTL; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_DEFAULT_PORT; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_PORT_SEP; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_SCTP; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_TCP; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_TLS_PORT; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_UDP; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_UDP_USUAL_MAX_SIZE; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_VIA_SEP; +import static net.sourceforge.peers.sip.RFC3261.TRANSPORT_VIA_SEP2; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Hashtable; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.Utils; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderFieldValue; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaderParamName; +import net.sourceforge.peers.sip.syntaxencoding.SipHeaders; +import net.sourceforge.peers.sip.syntaxencoding.SipParser; +import net.sourceforge.peers.sip.transaction.TransactionManager; + + +public class TransportManager { + + public static final int SOCKET_TIMEOUT = RFC3261.TIMER_T1; + + private static int NO_TTL = -1; + + private Logger logger; + + //private UAS uas; + private SipServerTransportUser sipServerTransportUser; + + protected SipParser sipParser; + + private Hashtable datagramSockets; + private Hashtable messageSenders; + private Hashtable messageReceivers; + + private TransactionManager transactionManager; + + private Config config; + private int sipPort; + + public TransportManager(TransactionManager transactionManager, + Config config, Logger logger) { + sipParser = new SipParser(); + datagramSockets = new Hashtable(); + messageSenders = new Hashtable(); + messageReceivers = new Hashtable(); + this.transactionManager = transactionManager; + this.config = config; + this.logger = logger; + } + + public MessageSender createClientTransport(SipRequest sipRequest, + InetAddress inetAddress, int port, String transport) + throws IOException { + return createClientTransport(sipRequest, inetAddress, port, transport, + NO_TTL); + } + + public MessageSender createClientTransport(SipRequest sipRequest, + InetAddress inetAddress, int port, String transport, int ttl) + throws IOException { + //18.1 + + //via created by transaction layer to add branchid + SipHeaderFieldValue via = Utils.getTopVia(sipRequest); + StringBuffer buf = new StringBuffer(DEFAULT_SIP_VERSION); + buf.append(TRANSPORT_VIA_SEP); + if (sipRequest.toString().getBytes().length > TRANSPORT_UDP_USUAL_MAX_SIZE) { + transport = TRANSPORT_TCP; + } + buf.append(transport); + if (inetAddress.isMulticastAddress()) { + SipHeaderParamName maddrName = new SipHeaderParamName(PARAM_MADDR); + via.addParam(maddrName, inetAddress.getHostAddress()); + if (inetAddress instanceof Inet4Address) { + SipHeaderParamName ttlName = new SipHeaderParamName(PARAM_TTL); + via.addParam(ttlName, IPV4_TTL); + } + } + //RFC3581 + //TODO check config + via.addParam(new SipHeaderParamName(RFC3261.PARAM_RPORT), ""); + + buf.append(TRANSPORT_VIA_SEP2);//space + + //TODO user server connection + + InetAddress myAddress = config.getPublicInetAddress(); + if (myAddress == null) { + myAddress = config.getLocalInetAddress(); + } + + buf.append(myAddress.getHostAddress()); //TODO use getHostName if real DNS + buf.append(TRANSPORT_PORT_SEP); + + + if (sipPort < 1) { + //use default port + if (TRANSPORT_TCP.equals(transport) || TRANSPORT_UDP.equals(transport) + || TRANSPORT_SCTP.equals(transport)) { + sipPort = TRANSPORT_DEFAULT_PORT; + } else if (TRANSPORT_SCTP.equals(transport)) { + sipPort = TRANSPORT_TLS_PORT; + } else { + throw new RuntimeException("unknown transport type"); + } + } + buf.append(sipPort); + //TODO add sent-by (p. 143) Before... + + via.setValue(buf.toString()); + + SipTransportConnection connection = new SipTransportConnection( + config.getLocalInetAddress(), sipPort, inetAddress, port, + transport); + + MessageSender messageSender = messageSenders.get(connection); + if (messageSender == null) { + messageSender = createMessageSender(connection); + } + return messageSender; + } + + private String threadName(int port) { + return getClass().getSimpleName() + " " + port; + } + + public void createServerTransport(String transportType, int port) + throws SocketException { + SipTransportConnection conn = new SipTransportConnection( + config.getLocalInetAddress(), port, null, + SipTransportConnection.EMPTY_PORT, transportType); + + MessageReceiver messageReceiver = messageReceivers.get(conn); + if (messageReceiver == null) { + messageReceiver = createMessageReceiver(conn); + new Thread(messageReceiver, threadName(port)).start(); + } + if (!messageReceiver.isListening()) { + new Thread(messageReceiver, threadName(port)).start(); + } + } + + public void sendResponse(SipResponse sipResponse) throws IOException { + //18.2.2 + SipHeaderFieldValue topVia = Utils.getTopVia(sipResponse); + String topViaValue = topVia.getValue(); + StringBuffer buf = new StringBuffer(topViaValue); + String hostport = null; + int i = topViaValue.length() - 1; + while (i > 0) { + char c = buf.charAt(i); + if (c == ' ' || c == '\t') { + hostport = buf.substring(i + 1); + break; + } + --i; + } + if (hostport == null) { + throw new RuntimeException("host or ip address not found in top via"); + } + String host; + int port; + int colonPos = hostport.indexOf(RFC3261.TRANSPORT_PORT_SEP); + if (colonPos > -1) { + host = hostport.substring(0, colonPos); + port = Integer.parseInt( + hostport.substring(colonPos + 1, hostport.length())); + } else { + host = hostport; + port = RFC3261.TRANSPORT_DEFAULT_PORT; + } + + String transport; + if (buf.indexOf(RFC3261.TRANSPORT_TCP) > -1) { + transport = RFC3261.TRANSPORT_TCP; + } else if (buf.indexOf(RFC3261.TRANSPORT_UDP) > -1) { + transport = RFC3261.TRANSPORT_UDP; + } else { + if (logger!=null) logger.error("no transport found in top via header," + + " discarding response"); + return; + } + + String received = + topVia.getParam(new SipHeaderParamName(RFC3261.PARAM_RECEIVED)); + if (received != null) { + host = received; + } + //RFC3581 + //TODO check config + String rport = topVia.getParam(new SipHeaderParamName( + RFC3261.PARAM_RPORT)); + if (rport != null && !"".equals(rport.trim())) { + port = Integer.parseInt(rport); + } + SipTransportConnection connection; + try { + connection = new SipTransportConnection(config.getLocalInetAddress(), + sipPort, InetAddress.getByName(host), + port, transport); + } catch (UnknownHostException e) { + if (logger!=null) logger.error("unknwon host", e); + return; + } + + //actual sending + + //TODO manage maddr parameter in top via for multicast + if (buf.indexOf(RFC3261.TRANSPORT_TCP) > -1) { +// Socket socket = (Socket)factory.connections.get(connection); +// if (!socket.isClosed()) { +// try { +// socket.getOutputStream().write(data); +// } catch (IOException e) { +// e.printStackTrace(); +// return; +// //TODO +// } +// } else { +// try { +// socket = new Socket(host, port); +// factory.connections.put(connection, socket); +// socket.getOutputStream().write(data); +// } catch (IOException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// /* +// * TODO +// * If connection attempt fails, use the procedures in RFC3263 +// * for servers in order to determine the IP address and +// * port to open the connection and send the response to. +// */ +// return; +// } +// } + } else { + MessageSender messageSender = messageSenders.get(connection); + if (messageSender == null) { + messageSender = createMessageSender(connection); + } + //add contact header + SipHeaderFieldName contactName = new SipHeaderFieldName(RFC3261.HDR_CONTACT); + SipHeaders respHeaders = sipResponse.getSipHeaders(); + StringBuffer contactBuf = new StringBuffer(); + contactBuf.append(RFC3261.LEFT_ANGLE_BRACKET); + contactBuf.append(RFC3261.SIP_SCHEME); + contactBuf.append(RFC3261.SCHEME_SEPARATOR); + contactBuf.append(messageSender.getContact()); + contactBuf.append(RFC3261.RIGHT_ANGLE_BRACKET); + respHeaders.add(contactName, new SipHeaderFieldValue(contactBuf.toString())); + messageSender.sendMessage(sipResponse); + + } + + + } + + private MessageSender createMessageSender(final SipTransportConnection conn) + throws IOException { + MessageSender messageSender = null; + Object socket = null; + if (RFC3261.TRANSPORT_UDP.equalsIgnoreCase(conn.getTransport())) { + //TODO use Utils.getMyAddress to create socket on appropriate NIC + DatagramSocket datagramSocket = datagramSockets.get(conn); + if (datagramSocket == null) { + if (logger!=null) logger.debug("new DatagramSocket(" + conn.getLocalPort() + + ", " + conn.getLocalInetAddress() + ")"); + // AccessController.doPrivileged added for plugin compatibility + datagramSocket = AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public DatagramSocket run() { + try { + return new DatagramSocket(conn.getLocalPort(), + conn.getLocalInetAddress()); + } catch (SocketException e) { + if (logger!=null) logger.error("cannot create socket", e); + } catch (SecurityException e) { + if (logger!=null) logger.error("security exception", e); + } + return null; + } + } + ); + if (datagramSocket == null) { + throw new SocketException(); + } + datagramSocket.setSoTimeout(SOCKET_TIMEOUT); + datagramSockets.put(conn, datagramSocket); + logger.info("added datagram socket " + conn); + } + socket = datagramSocket; + messageSender = new UdpMessageSender(conn.getRemoteInetAddress(), + conn.getRemotePort(), datagramSocket, config, logger); + } else { + // TODO + // messageReceiver = new TcpMessageReceiver(port); + } + messageSenders.put(conn, messageSender); + //when a mesage is sent over a transport, the transport layer + //must also be able to receive messages on this transport + +// MessageReceiver messageReceiver = +// createMessageReceiver(conn, socket); + MessageReceiver messageReceiver = messageReceivers.get(conn); + if (messageReceiver == null) { + messageReceiver = createMessageReceiver(conn, socket); + new Thread(messageReceiver, threadName(conn.getLocalPort())).start(); + } +// if (RFC3261.TRANSPORT_UDP.equalsIgnoreCase(conn.getTransport())) { +// messageSender = new UdpMessageSender(conn.getRemoteInetAddress(), +// conn.getRemotePort(), (DatagramSocket)socket, config, logger); +// messageSenders.put(conn, messageSender); +// } + return messageSender; + } + + private MessageReceiver createMessageReceiver(SipTransportConnection conn, + Object socket) throws IOException { + MessageReceiver messageReceiver = null; + if (RFC3261.TRANSPORT_UDP.equalsIgnoreCase(conn.getTransport())) { + DatagramSocket datagramSocket = (DatagramSocket)socket; + messageReceiver = new UdpMessageReceiver(datagramSocket, + transactionManager, this, config, logger); + messageReceiver.setSipServerTransportUser(sipServerTransportUser); + } + messageReceivers.put(conn, messageReceiver); + return messageReceiver; + } + + private MessageReceiver createMessageReceiver(final SipTransportConnection conn) + throws SocketException { + MessageReceiver messageReceiver = null; + SipTransportConnection sipTransportConnection = conn; + if (RFC3261.TRANSPORT_UDP.equals(conn.getTransport())) { + DatagramSocket datagramSocket = datagramSockets.get(conn); + if (datagramSocket == null) { + if (logger!=null) logger.debug("new DatagramSocket(" + conn.getLocalPort() + + ", " + conn.getLocalInetAddress()); + // AccessController.doPrivileged added for plugin compatibility + datagramSocket = AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public DatagramSocket run() { + try { + return new DatagramSocket(conn.getLocalPort(), + conn.getLocalInetAddress()); + } catch (SocketException e) { + if (logger!=null) if (logger!=null) logger.error("cannot create socket", e); + } catch (SecurityException e) { + if (logger!=null) if (logger!=null) logger.error("security exception", e); + } + return null; + } + } + ); + datagramSocket.setSoTimeout(SOCKET_TIMEOUT); + if (conn.getLocalPort() == 0) { + sipTransportConnection = new SipTransportConnection( + conn.getLocalInetAddress(), + datagramSocket.getLocalPort(), + conn.getRemoteInetAddress(), + conn.getRemotePort(), + conn.getTransport()); + //config.setSipPort(datagramSocket.getLocalPort()); + } + sipPort = datagramSocket.getLocalPort(); + datagramSockets.put(sipTransportConnection, datagramSocket); + if (logger!=null) logger.info("added datagram socket " + sipTransportConnection); + } + messageReceiver = new UdpMessageReceiver(datagramSocket, + transactionManager, this, config, logger); + messageReceiver.setSipServerTransportUser(sipServerTransportUser); + //TODO create also tcp receiver using a recursive call + } else { + //TODO + //messageReceiver = new TcpMessageReceiver(port); + } + messageReceivers.put(sipTransportConnection, messageReceiver); + if (logger!=null) logger.info("added " + sipTransportConnection + ": " + messageReceiver + + " to message receivers"); + return messageReceiver; + } + + public void setSipServerTransportUser( + SipServerTransportUser sipServerTransportUser) { + this.sipServerTransportUser = sipServerTransportUser; + } + + public void closeTransports() { + for (MessageReceiver messageReceiver: messageReceivers.values()) { + messageReceiver.setListening(false); + } + for (MessageSender messageSender: messageSenders.values()) { + messageSender.stopKeepAlives(); + } + try + { + Thread.sleep(SOCKET_TIMEOUT); + } + catch (InterruptedException e) + { + return; + } + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public Void run() { + for (DatagramSocket datagramSocket: datagramSockets.values()) { + datagramSocket.close(); + } + return null; + } + } + ); + + datagramSockets.clear(); + messageReceivers.clear(); + messageSenders.clear(); + } + + public MessageSender getMessageSender( + SipTransportConnection sipTransportConnection) { + return messageSenders.get(sipTransportConnection); + } + + public int getSipPort() { + return sipPort; + } + + public void setSipPort(int sipPort) { + this.sipPort = sipPort; + } + +} diff --git a/src/net/sourceforge/peers/sip/transport/UdpMessageReceiver.java b/src/net/sourceforge/peers/sip/transport/UdpMessageReceiver.java new file mode 100644 index 0000000..03e0506 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/UdpMessageReceiver.java @@ -0,0 +1,90 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; +import net.sourceforge.peers.sip.transaction.TransactionManager; + + +public class UdpMessageReceiver extends MessageReceiver { + + private DatagramSocket datagramSocket; + + public UdpMessageReceiver(DatagramSocket datagramSocket, + TransactionManager transactionManager, + TransportManager transportManager, Config config, + Logger logger) + throws SocketException { + super(datagramSocket.getLocalPort(), transactionManager, + transportManager, config, logger); + this.datagramSocket = datagramSocket; + } + + @Override + protected void listen() throws IOException { + byte[] buf = new byte[BUFFER_SIZE]; + final DatagramPacket packet = new DatagramPacket(buf, buf.length); + final int noException = 0; + final int socketTimeoutException = 1; + final int ioException = 2; + // AccessController.doPrivileged added for plugin compatibility + int result = AccessController.doPrivileged( + new PrivilegedAction() { + public Integer run() { + try { + datagramSocket.receive(packet); + } catch (SocketTimeoutException e) { + return socketTimeoutException; + } catch (IOException e) { + if (logger!=null) logger.error("cannot receive packet", e); + return ioException; + } + return noException; + } + }); + switch (result) { + case socketTimeoutException: + return; + case ioException: + throw new IOException(); + case noException: + break; + default: + break; + } + byte[] trimmedPacket = new byte[packet.getLength()]; + System.arraycopy(packet.getData(), 0, + trimmedPacket, 0, trimmedPacket.length); + processMessage(trimmedPacket, packet.getAddress(), + packet.getPort(), RFC3261.TRANSPORT_UDP); + } + + +} diff --git a/src/net/sourceforge/peers/sip/transport/UdpMessageSender.java b/src/net/sourceforge/peers/sip/transport/UdpMessageSender.java new file mode 100644 index 0000000..e8ef119 --- /dev/null +++ b/src/net/sourceforge/peers/sip/transport/UdpMessageSender.java @@ -0,0 +1,88 @@ +/* + This file is part of Peers, a java SIP softphone. + + 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 3 of the License, or + 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, see . + + Copyright 2007, 2008, 2009, 2010 Yohann Martineau +*/ + +package net.sourceforge.peers.sip.transport; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import net.sourceforge.peers.Config; +import net.sourceforge.peers.Logger; +import net.sourceforge.peers.sip.RFC3261; + + +public class UdpMessageSender extends MessageSender { + + private DatagramSocket datagramSocket; + + public UdpMessageSender(InetAddress inetAddress, int port, + DatagramSocket datagramSocket, Config config, + Logger logger) throws SocketException { + super(datagramSocket.getLocalPort(), inetAddress, port, + config, RFC3261.TRANSPORT_UDP, logger); + this.datagramSocket = datagramSocket; + } + + @Override + public synchronized void sendMessage(SipMessage sipMessage) throws IOException { + if (logger!=null) logger.debug("UdpMessageSender.sendMessage"); + if (sipMessage == null) { + return; + } + byte[] buf = sipMessage.toString().getBytes(); + sendBytes(buf); + StringBuffer direction = new StringBuffer(); + direction.append("SENT to ").append(inetAddress.getHostAddress()); + direction.append("/").append(port); + if (logger!=null) logger.traceNetwork(new String(buf), direction.toString()); + } + + @Override + public synchronized void sendBytes(byte[] bytes) throws IOException { + if (logger!=null) logger.debug("UdpMessageSender.sendBytes"); + final DatagramPacket packet = new DatagramPacket(bytes, bytes.length, + inetAddress, port); + if (logger!=null) logger.debug("UdpMessageSender.sendBytes " + bytes.length + + " " + inetAddress + ":" + port); + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public Void run() { + try { + if (logger!=null) logger.debug(datagramSocket.getLocalAddress().toString()); + datagramSocket.send(packet); + } catch (Throwable t) { + if (logger!=null) if (logger!=null) logger.error("throwable", new Exception(t)); + } + return null; + } + } + ); + + if (logger!=null) logger.debug("UdpMessageSender.sendBytes packet sent"); + } + +} diff --git a/src/test/JavaSipEvents.java b/src/test/JavaSipEvents.java new file mode 100644 index 0000000..c2a5c8d --- /dev/null +++ b/src/test/JavaSipEvents.java @@ -0,0 +1,15 @@ +package test; + +import code.jSIPClient.SIP_Request; +import code.jSIPClient.SIP_Response; + +public interface JavaSipEvents { + public void Registering(SIP_Request req); + public void RegisterSuccesful(SIP_Response resp); + public void RegisterFailed(SIP_Response resp); + public void IncomingCall(SIP_Request req, SIP_Response resp); + public void RemoteHangUp(SIP_Request req); + public void Ringing(SIP_Response resp); + public void CalleePickup(SIP_Response resp); + +} diff --git a/src/test/test.java b/src/test/test.java new file mode 100644 index 0000000..c5b6c00 --- /dev/null +++ b/src/test/test.java @@ -0,0 +1,11 @@ +package test; + +public class test { + public static void main(String[] args) { + //System.setProperty("jna.debug_load", "true"); + //testbass.testbass(); + //testvapix.testvapix(); + testsipclient.testsip("192.168.5.1", "1002", "1234"); + } + +} diff --git a/src/test/testbass.java b/src/test/testbass.java new file mode 100644 index 0000000..807e760 --- /dev/null +++ b/src/test/testbass.java @@ -0,0 +1,314 @@ +package test; + +import java.io.File; +import java.text.MessageFormat; +import java.util.stream.Stream; + +import com.sun.jna.Platform; + +import anywheresoftware.b4a.keywords.DateTime; +import bass.BassLibrary; +import bass.BassMixLibrary; +import bass.BassLibrary.BASS_DEVICEINFO; +import bass.BassLibrary.BASS_FILEPROCS; + +@SuppressWarnings("all") +public class testbass { + static long recordtimestamp = 0; + static long dsptimestamp = 0; + + public static void testbass() { + search_for_existing_library(); + + get_versions(); + + scan_playback_devices(); + //test_playback_fileproc(1); + //test_mixer(1); + //test_playback_streamproc(1); + + scan_recording_devices(); + //test_recording(0); + } + + private static void test_playback_fileproc(int devid) { + if (BassLibrary.BASS.BASS_Init(devid, 8000, 0)) { + raise_log("Init success for for device "+devid); + } else bass_error("BASS_Init device "+devid); + + BASS_FILEPROCS bf = new BASS_FILEPROCS( + // filecloseproc + (user)->{ + raise_log("FILECLOSEPROC called"); + }, + // filelenproc + (user)->{ + raise_log("FILELENPROC called"); + return 0; + }, + // filereadproc + (buffer, length, user)->{ + raise_log("FILEREADPROC called"); + return 0; + }, + // fileseekproc + (offset, user)->{ + raise_log("FILESEEKPROC called"); + return false; + } + ); + + int handle = BassLibrary.BASS.BASS_StreamCreateFileUser(BassLibrary.Constant.STREAMFILE_BUFFER, 0, bf, null); + + if (handle!=0) { + raise_log("handle = "+handle); + if (BassLibrary.BASS.BASS_StreamFree(handle)) + raise_log("Stream freed"); + else bass_error("BASS_StreamFree"); + } else bass_error("BASS_StreamCreateFileUser"); + + if (BassLibrary.BASS.BASS_Free()) { + raise_log("Playback freed"); + } else bass_error("BASS_Free"); + } + + private static void test_mixer(int devid) { + if (BassLibrary.BASS.BASS_Init(devid, 8000, 0)) { + raise_log("Init success for device "+devid); + } else bass_error("BASS_Init device "+devid); + + int mixhandle = BassMixLibrary.BASSMIX.BASS_Mixer_StreamCreate(8000, 1, 0); + if (mixhandle!=0) { + raise_log("Mixer created, handle = "+mixhandle); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + raise_log("Mixer Interrupted"); + } + + if (BassLibrary.BASS.BASS_StreamFree(mixhandle)) { + raise_log("Mixer freed"); + } else bass_error("BASS_StreamFree"); + + } else bass_error("BASS_Mixer_StreamCreate"); + if (BassLibrary.BASS.BASS_Free()) { + raise_log("Playback freed"); + } else bass_error("BASS_Free"); + } + + private static void test_playback_streamproc(int devid) { + if (BassLibrary.BASS.BASS_Init(devid, 8000, 0)) { + raise_log("Init success for device "+devid); + } else bass_error("BASS_Init device "+devid); + + int handle = BassLibrary.BASS.BASS_StreamCreate(8000, 1, 0, (hh, buffer, length, user)->{ + raise_log(MessageFormat.format("STREAMPROC triggered, handle={0}, length={1}", hh,length)); + return 0; + }, null); + + + if (handle!=0) { + raise_log("StreamCreate handle = "+handle); + + int dsphandle = BassLibrary.BASS.BASS_ChannelSetDSP(handle, (dsph, hh, buffer, length, user )->{ + long delta = DateTime.getNow() - dsptimestamp; + dsptimestamp = DateTime.getNow(); + raise_log(MessageFormat.format("DSP triggered, handle={0}, dsphandle={1}, length={2}, interval={3} ms", hh, dsph, length, delta)); + }, null, 0); + + if (dsphandle!=0) { + dsptimestamp = DateTime.getNow(); + raise_log("DSP created, handle = "+dsphandle); + } else bass_error("BASS_ChannelSetDSP"); + + if (BassLibrary.BASS.BASS_ChannelStart(handle)) { + raise_log("BASS_ChannelStart success"); + } else bass_error("BASS_ChannelStart"); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + raise_log("STREAMPROC PUSH Interrupt Exception"); + } + + if (dsphandle!=0) { + if (BassLibrary.BASS.BASS_ChannelRemoveDSP(handle, dsphandle)) { + raise_log("DSP Removed"); + } else bass_error("BASS_ChannelRemoveDSP"); + } + + if (BassLibrary.BASS.BASS_StreamFree(handle)) { + raise_log("handle freed"); + } else bass_error("BASS_STREAMFREE STREAMPROCPUSH"); + + if (BassLibrary.BASS.BASS_Free()) { + raise_log("Playback freed"); + } else bass_error("BASS_FREE"); + } else bass_error("BASS_StreamCreate STREAMPROCPUSH"); + } + + private static void test_playback_streamproc_push(int devid) { + if (BassLibrary.BASS.BASS_Init(devid, 8000, 0)) { + raise_log("Init success for device "+devid); + } else bass_error("BASS_Init device "+devid); + + int handle = BassLibrary.BASS.BASS_StreamCreate(8000, 1, 0, BassLibrary.Constant.STREAMPROC_PUSH, null); + if (handle!=0) { + raise_log("StreamCreate handle = "+handle); + + int dsphandle = BassLibrary.BASS.BASS_ChannelSetDSP(handle, (dsph, hh, buffer, length, user )->{ + long delta = DateTime.getNow() - dsptimestamp; + dsptimestamp = DateTime.getNow(); + raise_log(MessageFormat.format("DSP triggered, handle={0}, dsphandle={1}, length={2}, interval={3} ms", hh, dsph, length, delta)); + }, null, 0); + + if (dsphandle!=0) { + dsptimestamp = DateTime.getNow(); + raise_log("DSP created, handle = "+dsphandle); + } else bass_error("BASS_ChannelSetDSP"); + + if (BassLibrary.BASS.BASS_ChannelPlay(handle,true)) { + raise_log("BASS_ChannelStart success"); + } else bass_error("BASS_ChannelStart"); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + raise_log("STREAMPROC PUSH Interrupt Exception"); + } + + if (dsphandle!=0) { + if (BassLibrary.BASS.BASS_ChannelRemoveDSP(handle, dsphandle)) { + raise_log("DSP Removed"); + } else bass_error("BASS_ChannelRemoveDSP"); + } + + if (BassLibrary.BASS.BASS_StreamFree(handle)) { + raise_log("handle freed"); + } else bass_error("BASS_STREAMFREE STREAMPROCPUSH"); + + if (BassLibrary.BASS.BASS_Free()) { + raise_log("Playback freed"); + } else bass_error("BASS_FREE"); + } else bass_error("BASS_StreamCreate STREAMPROCPUSH"); + } + + private static void search_for_existing_library() { + System.out.println("Resource Prefix = "+Platform.RESOURCE_PREFIX); + String[] classpath = System.getProperty("java.class.path").split(";"); + //System.out.println("Class path = "); + //Stream.of(classpath).forEach(cp -> System.out.println(cp)); + String jnalibpath = System.getProperty("jna.library.path"); + System.out.println("JNA library path = "+jnalibpath); + String[] javalibpath = System.getProperty("java.library.path").split(";"); + + + System.out.println("Searching for BASS Library at Java Library Path : "); + Stream.of(javalibpath).forEach(jp -> { + File bb = new File(jp,System.mapLibraryName("bass")); + if (bb.exists()) { + System.out.println("Found "+bb.getAbsolutePath()); + } + File bc = new File(jp, System.mapLibraryName("bassmix")); + if (bc.exists()) { + System.out.println("Found "+bc.getAbsolutePath()); + } + }); + } + + private static void get_versions() { + int v1 = BassLibrary.BASS.BASS_GetVersion(); + System.out.println("Bass Version="+Integer.toHexString(v1)); + int v2 = BassMixLibrary.BASSMIX.BASS_Mixer_GetVersion(); + System.out.println("BassMix Version="+Integer.toHexString(v2)); + } + + private static void scan_playback_devices() { + int ii = 0; + while(true) { + BASS_DEVICEINFO di = new BASS_DEVICEINFO(); + if (BassLibrary.BASS.BASS_GetDeviceInfo(ii, di)) { + raise_log("BASS_GetDeviceInfo index="+ii+" success"); + raise_log("DeviceInfo = "+di.toString()); + ii++; + } else { + bass_error("BASS_GetDeviceInfo ii="+ii); + break; + } + } + } + + private static void scan_recording_devices() { + int ii = 0; + while(true) { + BASS_DEVICEINFO di = new BASS_DEVICEINFO(); + if (BassLibrary.BASS.BASS_RecordGetDeviceInfo(ii, di)) { + raise_log("BASS_RecordGetDeviceInfo index="+ii+" success"); + raise_log("DeviceInfo = "+di.toString()); + ii++; + } else { + bass_error("BASS_RecordGetDeviceInfo ii="+ii); + break; + } + } + + } + + private static void test_recording(int devid) { + if (BassLibrary.BASS.BASS_RecordInit(devid)) { + raise_log(MessageFormat.format("BASS_RecordInit device {0} success",devid)); + + // pake cara biasa +// int rechandle = BassLibrary.BASS.BASS_RecordStart(8000, 1, BassLibrary.BassConstant.BASS_RECORD_PAUSE, new RECORDPROC() { +// +// @Override +// public boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user) { +// long delta = DateTime.getNow() - recordtimestamp; +// recordtimestamp = DateTime.getNow(); +// raise_log(MessageFormat.format("RECORDPROC triggered, handle={0}, length={1}, Interval={2} ms", handle, length, delta)); +// return true; +// } +// +// }, null); + + // pakai lambda + int rechandle = BassLibrary.BASS.BASS_RecordStart(8000, 1, BassLibrary.Constant.BASS_RECORD_PAUSE, (handle, buffer, length, user)->{ + long delta = DateTime.getNow() - recordtimestamp; + recordtimestamp = DateTime.getNow(); + raise_log(MessageFormat.format("RECORDPROC triggered, handle={0}, length={1}, Interval={2} ms", handle, length, delta)); + return true; + }, null); + raise_log(MessageFormat.format("Record Handle = {0}", rechandle)); + + if (rechandle!=0) { + if (BassLibrary.BASS.BASS_ChannelStart(rechandle)) { + raise_log("Recording Started"); + recordtimestamp = DateTime.getNow(); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + raise_log("Recording Sleep exception = "+e.getMessage()); + } + + } else bass_error("BASS_ChannelStart recordhandle"); + + if (BassLibrary.BASS.BASS_ChannelFree(rechandle)) { + raise_log("Recording Stopped"); + } else bass_error("BASS_ChannelStop recordhandle"); + + if (BassLibrary.BASS.BASS_RecordFree()) { + raise_log("Recording channel freed"); + } else bass_error("BASS_RecordFree"); + } else bass_error("BASS_RecordStart"); + } else bass_error("BASS_RecordInit 0"); + } + + private static void raise_log(String msg) { + System.out.println(msg); + } + private static void bass_error(String functionname) { + System.out.println("BASS error on function "+functionname+", Code="+BassLibrary.BASS.BASS_ErrorGetCode()); + } + + +} diff --git a/src/test/testsipclient.java b/src/test/testsipclient.java new file mode 100644 index 0000000..284b7b5 --- /dev/null +++ b/src/test/testsipclient.java @@ -0,0 +1,150 @@ +package test; + +import java.io.File; +import java.text.MessageFormat; + +import code.jSIPClient; +import code.jSIPClient.SIP_Request; +import code.jSIPClient.SIP_Response; + +public class testsipclient { + private static jSIPClient client; + private static String server_ip; + + public static void testsip(String serverip, String username, String password) { + server_ip = serverip; + client = new jSIPClient(); + client.Connect(serverip, username, password, "192.168.5.154"); + SipEvents se = new SipEvents(client); + client.SetJavaSipEvent(se); + + //calltest("2001"); + //streamtest("2001","lagu8khz16bitmono.wav"); + //streamtest("2001","all_of_me.mp3"); + //streamtest("2001","store_closing_example.mp3"); + } + + @SuppressWarnings("unused") + private static void streamtest(String numbertocall, String filename) { + // kasih waktu supaya registered ke server + try { + Thread.sleep(5*1000); + } catch (InterruptedException e) { + + } + + File currentfolder = new File(""); + + File ff = new File(currentfolder.getAbsolutePath(),filename); + if (ff.exists()) { + System.out.println("File found : "+ff.getAbsolutePath()); + + String extensiontocall = MessageFormat.format("sip:{0}@{1}", numbertocall, server_ip); + System.out.println("Calling extension "+extensiontocall); + + client.StreamFile(extensiontocall, ff.getAbsolutePath()); + + + // call duration = 10 detik + try { + Thread.sleep(53*1000); + } catch (InterruptedException e) { + + } + client.HangUp(); + + } else System.out.println("File not found : "+ff.getAbsolutePath()); + + } + + @SuppressWarnings("unused") + private static void calltest(String numbertocall) { + // kasih waktu supaya registered ke server + try { + Thread.sleep(5*1000); + } catch (InterruptedException e) { + + } + + String extensiontocall = MessageFormat.format("sip:{0}@{1}", numbertocall, server_ip); + System.out.println("Calling extension "+extensiontocall); + client.Call(extensiontocall); + + // call duration = 10 detik + try { + Thread.sleep(10*1000); + } catch (InterruptedException e) { + + } + client.HangUp(); + } + + +} + +class SipEvents implements JavaSipEvents{ + private final jSIPClient client; + + public SipEvents(jSIPClient _client) { + client = _client; + } + + @Override + public void Registering(SIP_Request req) { + System.out.println("Registering, Request = "); + System.out.println(req); + System.out.println(""); + } + + @Override + public void RegisterSuccesful(SIP_Response resp) { + System.out.println("Register Succesful, Response = "); + System.out.println(resp); + System.out.println(""); + } + + @Override + public void RegisterFailed(SIP_Response resp) { + System.out.println("Register Failed, Response = "); + System.out.println(resp); + System.out.println(""); + } + + @Override + public void IncomingCall(SIP_Request req, SIP_Response resp) { + System.out.println("Incoming Call"); + System.out.println("Request = "); + System.out.println(req); + System.out.println("Response = "); + System.out.println(resp); + System.out.println(""); + + client.AcceptIncomingCall(req); + + } + + @Override + public void RemoteHangUp(SIP_Request req) { + System.out.println("Remote HangUp, Request = "); + System.out.println(req); + System.out.println(""); + + client.HangUp(); + } + + @Override + public void Ringing(SIP_Response resp) { + System.out.println("Ringing, Response = "); + System.out.println(resp); + System.out.println(""); + + } + + @Override + public void CalleePickup(SIP_Response resp) { + System.out.println("Callee Pickup, Response = "); + System.out.println(resp); + System.out.println(""); + } + +} diff --git a/src/test/testvapix.java b/src/test/testvapix.java new file mode 100644 index 0000000..0371726 --- /dev/null +++ b/src/test/testvapix.java @@ -0,0 +1,295 @@ +package test; + + +import java.io.File; + +import com.google.gson.Gson; +import com.sun.jna.Pointer; + +import bass.BassAACLibrary; +import bass.BassLibrary; +import bass.BassLibrary.BASS_DEVICEINFO; +import bass.BassLibrary.BASS_FILEPROCS; +import bass.BassLibrary.FILECLOSEPROC; +import bass.BassLibrary.FILELENPROC; +import bass.BassLibrary.FILEREADPROC; +import bass.BassLibrary.FILESEEKPROC; +import code.JVAPIX; +import code.VapixAudio; +import code.common; +import code.VapixAudio.AudioControlServiceApi.VolumeRange; +import code.VapixAudio.AudioRelayServiceApi.AudioPeer; +import code.VapixAudio.AudioRelayServiceApi.DiscoveredAudioPeer; +import code.VapixAudio.AudioRelayServiceApi.PeerStatus; +import code.VapixAudio.AudioRelayServiceApi.SoundConfiguration; +import code.VapixAudio.CallServiceApi.SIPAccounts.SIPAccount; +import code.VapixAudio.CallServiceApi.SIPAccounts.SIPAccountStatus; +import code.VapixAudio.MediaApi.MediaClip; +import code.VapixDevice.DownloadResult; +import lombok.val; + +@SuppressWarnings("all") +public class testvapix { + static Gson gs = new Gson(); + static File currentpath = new File(""); + + public static void testvapix() { + String[] targets = new String[] {"192.168.5.153","192.168.5.182","192.168.5.192"}; + System.out.println("Axis Audio test"); + JVAPIX ja = new JVAPIX(); + + String ip = targets[2]; + if (ja.Connect(ip, 80, "root", "pass","192.168.5.154")) { + Object xx = ja.GetConnectedDevice(ip); + if (xx instanceof VapixAudio) { + VapixAudio va = (VapixAudio) xx; + + //vapix_test_callservice(va); + //vapix_test_audiocontrolservice(va); + //vapix_test_audiorelayservice(va); + //vapix_test_mediaclip(va,0); + vapix_test_audioreceive(va); + + } + } + } + + private static void vapix_test_mediaclip(VapixAudio va, final int outputid) { + if (va.MediaApi.IsSupported(outputid)) { + System.out.println("MediaClip API supported on output "+outputid); + + if (va.MediaApi.GetAvailableMediaClips()) { + + if (va.MediaApi.AvailableMediaClips!=null && va.MediaApi.AvailableMediaClips.length>0) { + for(MediaClip mcx : va.MediaApi.AvailableMediaClips) { + System.out.println(gs.toJson(mcx)); + } + } + } else System.out.println("GetAvailableMediaClips failed, or no clips installed"); + + String[] acf = va.MediaApi.AudioClipFormat(); + if (acf!=null && acf.length>0) { + System.out.println("Audio Clip Format : "); + for(String format : acf) { + System.out.println(format); + } + } + + // coba play yang simple +// if (va.MediaApi.PlayClip(0, 47)) { +// System.out.println("Play Clip success"); +// +// } + + // coba play dan di stop setelah 10 detik +// System.out.println(va.MediaApi.PlayClip2(1, 46, 1, 100)); + //common.Sleep(10000); + //if (va.MediaApi.StopClip()) System.out.println("Stop success"); + + //metode download jadinya byte array +// byte[] data = va.MediaApi.DownloadClip(45); +// if (data!=null && data.length>0) { +// System.out.println("Download success, size = "+data.length); +// } else System.out.println("Download failed"); + + //metode download jadinya langsung file +// DownloadResult dr = va.MediaApi.DownloadClip(45, currentpath.getAbsolutePath()); +// if (dr!=null) { +// System.out.println("Download success, result = "+dr.toString()); +// } else System.out.println("Download failed"); + + //va.MediaApi.UploadClip("Soundtrack","all_of_me.mp3"); + //va.MediaApi.PlayClip(0, 0); + //va.MediaApi.RemoveClip(0); + //va.MediaApi.RenameClip(0, "AllOfMe"); + } else System.out.println("MediaClip API not supported on output "+outputid); + } + + private static void vapix_test_audiorelayservice(VapixAudio va) { + if (va.AudioRelayServiceApi.IsSupported()) { + System.out.println("AudioRelayService Supported"); + // + AudioPeer[] ap = va.AudioRelayServiceApi.GetAudioPeers(); + if (ap!=null && ap.length>0) { + for(AudioPeer apx : ap) { + System.out.println("Configuration = "+gs.toJson(apx.Configuration)); + System.out.println("Metadata = "+gs.toJson(apx.MetaData)); + System.out.println("ConnectionStatus = "+apx.ConnectionStatus); + System.out.println("OutputGainDefinitions = "+gs.toJson(apx.OutputGainDefinitions)); + } + } else System.out.println("AudioPeer not available"); + + // + PeerStatus[] ps = va.AudioRelayServiceApi.GetAudioPeerStatus(); + if (ps!=null && ps.length>0) { + for(PeerStatus psx : ps) { + System.out.println(gs.toJson(psx)); + } + } else System.out.println("PeerStatus not available"); + + + // + DiscoveredAudioPeer[] dap = va.AudioRelayServiceApi.GetDiscoveredAudioPeers(); + if (dap!=null && dap.length>0) { + for(DiscoveredAudioPeer dapx : dap) { + System.out.println(gs.toJson(dapx)); + } + } else System.out.println("DiscoveredAudioPeer not available"); + + + // + SoundConfiguration sc = va.AudioRelayServiceApi.GetSoundConfiguration(); + System.out.println(sc!=null ? sc.toString() : "SoundConfiguration not available"); + + if (va.AudioRelayServiceApi.SetSoundConfiguration_MasterVolume(0,false)) System.out.println("SetSoundConfiguration Success"); else System.out.println("SetSoundConfiguration failed"); + + } else System.out.println("AudioRelayService not supported"); + } + + + + private static void vapix_test_callservice(VapixAudio va) { + if (va.CallServiceApi.IsSupported()) { + System.out.println("CallService Supported"); + va.CallServiceApi.SIPConfiguration.Get(); + va.CallServiceApi.SIPAccounts.Get(); + SIPAccount[] accounts = va.CallServiceApi.SIPAccounts.Registered_SIP_Accounts(); + for(SIPAccount sa : accounts) { + + System.out.println(sa.toString()); + SIPAccountStatus sas = va.CallServiceApi.SIPAccounts.GetSIPAccountStatus(sa.Id); + System.out.println(sas.toString()); + } + + //va.CallServiceApi.SIPAccounts.GetCallStatuses(); + //va.CallServiceApi.SIPAccounts.GetCallStatus("In-0-1681892244.939170-1d7474353e0c79d205a0fd114f663425@192.168"); + //if (va.CallServiceApi.SIPAccounts.TerminateCall("In-0-1681892244.939170-1d7474353e0c79d205a0fd114f663425@192.168")) System.out.println("Terminate cal success"); + +// String callid = va.CallServiceApi.SIPAccounts.Call("3000"); +// if (common.ValidString(callid)) { +// System.out.println("Call success "); +// va.CallServiceApi.SIPAccounts.GetCallStatuses(); +// common.Sleep(20*1000); +// if (va.CallServiceApi.SIPAccounts.TerminateCall(callid)) { +// System.out.println("Terminate call success"); +// } else System.out.println("Terminate Call failed"); +// } else System.out.println("Call failed"); +// + + } else System.out.println("CallService is not supported"); + } + + private static void vapix_test_audiocontrolservice(VapixAudio va) { + if (va.AudioControlServiceApi.IsSupported()) { + System.out.println("AudioControlService supported"); + va.AudioControlServiceApi.GetControlCapabilities(); + va.AudioControlServiceApi.GetVolume(); + if (va.AudioControlServiceApi.SetVolume()) System.out.println("SetVolume success"); else System.out.println("SetVolume failed"); + + String[] chid = va.AudioControlServiceApi.ControlCapabilities.ChannelIds; + if (common.ValidArrayOfString(chid)) { + for(String ch : chid) System.out.println(ch); + } + String[] inid = va.AudioControlServiceApi.ControlCapabilities.InputIds; + if (common.ValidArrayOfString(inid)) { + for(String id : inid) System.out.println(id); + } + + VolumeRange[] volrange = va.AudioControlServiceApi.ControlCapabilities.VolumeRanges; + if (volrange != null && volrange.length>0) { + for(VolumeRange vv : volrange) { + System.out.println(vv.toString()); + } + } else System.out.println("VolumeRange length =0 or null"); + + } else System.out.println("AudioControlService not supported"); + + } + + private static void vapix_test_audioreceive(VapixAudio va) { + if (va.AudioApi.IsSupported()) { + System.out.println("AudioAPI Supported"); + + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + if (BassLibrary.BASS.BASS_GetDeviceInfo(1, dev)) { + System.out.println(dev.toString()); + } else System.out.println("BASS_GetDeviceInfo failed, error="+BassLibrary.BASS.GetBassError("BASS_GetDeviceInfo")); + BassLibrary.BASS.BASS_Init(1, 48000, 0); + + + BASS_FILEPROCS bfp = new BASS_FILEPROCS(); + bfp.FILECLOSEPROC = new FILECLOSEPROC() { + + @Override + public void FILECLOSEPROC(Pointer user) { + System.out.println("FILECLOSEPROC called"); + + } + + }; + bfp.FILELENPROC = new FILELENPROC() { + + @Override + public long FILELENPROC(Pointer user) { + System.out.println("FILELENPROC called"); + return 0; + } + + }; + bfp.FILEREADPROC = new FILEREADPROC() { + + @Override + public int FILEREADPROC(Pointer buffer, int length, Pointer user) { + System.out.println("FILREADPROC called, length="+length); + byte[] buf = va.AudioApi.GetReceivedBuffers_bySize(length); + if (buf!=null && buf.length>0) { + System.out.println("FILEREADPROC called, buf.length="+buf.length); + buffer.write(0, buf, 0, buf.length); + return buf.length; + } else System.out.println("FILEREADPROC called but buf is invalid"); + return -1; + } + + }; + bfp.FILESEEKPROC = new FILESEEKPROC() { + + @Override + public boolean FILESEEKPROC(long offset, Pointer user) { + System.out.println("FILESEEKPROC called"); + return false; + } + + }; + + if (va.AudioApi.Start_Receive()) { + System.out.println("Wait until buffer enough"); + while(va.AudioApi.AvailableReceivedBytes()<8000) { + common.Sleep(1); + } + System.out.println("Buffer size = "+va.AudioApi.AvailableReceivedBytes()); + int handle = BassAACLibrary.BASS_AAC.BASS_AAC_StreamCreateFileUser(BassLibrary.Constant.STREAMFILE_BUFFER, 0, bfp, null); + if (handle!=0) { + System.out.println("BASS AAC Stream created"); + if (BassLibrary.BASS.BASS_ChannelStart(handle)) { + System.out.println("ChannelStart success"); + } else System.out.println("ChannelStart failed, error="+BassLibrary.BASS.GetBassError("BASS_ChannelStart")); + } else System.out.println("BASS AAC Stream not created, code = "+BassLibrary.BASS.BASS_ErrorGetCode()); + + + common.Sleep(10); + va.AudioApi.Stop_Receive(); + + if (handle!=0) { + if (BassLibrary.BASS.BASS_ChannelFree(handle)) { + System.out.println("ChannelFree success"); + } else System.out.println("ChannelFree failed, error="+BassLibrary.BASS.GetBassError("BASS_ChannelFree")); + + } + + } else System.out.println("Start_Receive failed"); + + + } else System.out.println("AudioAPI not supported"); + } + +} diff --git a/src/win32-x86-64/bass.dll b/src/win32-x86-64/bass.dll new file mode 100644 index 0000000..5ba4da7 Binary files /dev/null and b/src/win32-x86-64/bass.dll differ diff --git a/src/win32-x86-64/bass_aac.dll b/src/win32-x86-64/bass_aac.dll new file mode 100644 index 0000000..858cfca Binary files /dev/null and b/src/win32-x86-64/bass_aac.dll differ diff --git a/src/win32-x86-64/bassenc.dll b/src/win32-x86-64/bassenc.dll new file mode 100644 index 0000000..0894aca Binary files /dev/null and b/src/win32-x86-64/bassenc.dll differ diff --git a/src/win32-x86-64/bassenc_aac.dll b/src/win32-x86-64/bassenc_aac.dll new file mode 100644 index 0000000..fcf5bbd Binary files /dev/null and b/src/win32-x86-64/bassenc_aac.dll differ diff --git a/src/win32-x86-64/bassenc_flac.dll b/src/win32-x86-64/bassenc_flac.dll new file mode 100644 index 0000000..6cdee6f Binary files /dev/null and b/src/win32-x86-64/bassenc_flac.dll differ diff --git a/src/win32-x86-64/bassenc_mp3.dll b/src/win32-x86-64/bassenc_mp3.dll new file mode 100644 index 0000000..85ab127 Binary files /dev/null and b/src/win32-x86-64/bassenc_mp3.dll differ diff --git a/src/win32-x86-64/bassenc_ogg.dll b/src/win32-x86-64/bassenc_ogg.dll new file mode 100644 index 0000000..eebc87d Binary files /dev/null and b/src/win32-x86-64/bassenc_ogg.dll differ diff --git a/src/win32-x86-64/bassenc_opus.dll b/src/win32-x86-64/bassenc_opus.dll new file mode 100644 index 0000000..3ebb494 Binary files /dev/null and b/src/win32-x86-64/bassenc_opus.dll differ diff --git a/src/win32-x86-64/bassmix.dll b/src/win32-x86-64/bassmix.dll new file mode 100644 index 0000000..a34eedc Binary files /dev/null and b/src/win32-x86-64/bassmix.dll differ diff --git a/src/win32-x86/bass.dll b/src/win32-x86/bass.dll new file mode 100644 index 0000000..10114b6 Binary files /dev/null and b/src/win32-x86/bass.dll differ diff --git a/src/win32-x86/bass_aac.dll b/src/win32-x86/bass_aac.dll new file mode 100644 index 0000000..bfc0813 Binary files /dev/null and b/src/win32-x86/bass_aac.dll differ diff --git a/src/win32-x86/bassenc.dll b/src/win32-x86/bassenc.dll new file mode 100644 index 0000000..d61ca83 Binary files /dev/null and b/src/win32-x86/bassenc.dll differ diff --git a/src/win32-x86/bassenc_aac.dll b/src/win32-x86/bassenc_aac.dll new file mode 100644 index 0000000..81833c1 Binary files /dev/null and b/src/win32-x86/bassenc_aac.dll differ diff --git a/src/win32-x86/bassenc_flac.dll b/src/win32-x86/bassenc_flac.dll new file mode 100644 index 0000000..ac971f2 Binary files /dev/null and b/src/win32-x86/bassenc_flac.dll differ diff --git a/src/win32-x86/bassenc_mp3.dll b/src/win32-x86/bassenc_mp3.dll new file mode 100644 index 0000000..0c5785a Binary files /dev/null and b/src/win32-x86/bassenc_mp3.dll differ diff --git a/src/win32-x86/bassenc_ogg.dll b/src/win32-x86/bassenc_ogg.dll new file mode 100644 index 0000000..26eda2b Binary files /dev/null and b/src/win32-x86/bassenc_ogg.dll differ diff --git a/src/win32-x86/bassenc_opus.dll b/src/win32-x86/bassenc_opus.dll new file mode 100644 index 0000000..44c9b57 Binary files /dev/null and b/src/win32-x86/bassenc_opus.dll differ diff --git a/src/win32-x86/bassmix.dll b/src/win32-x86/bassmix.dll new file mode 100644 index 0000000..e413823 Binary files /dev/null and b/src/win32-x86/bassmix.dll differ