commit db13021be9d837ec9caef0986624aa12dc2d73ce Author: rdkartono Date: Mon Nov 18 12:09:33 2024 +0700 adaptation of old eclipse code to Intellij diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a1757ae --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..93d2ad4 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/libraries/google_code_gson.xml b/.idea/libraries/google_code_gson.xml new file mode 100644 index 0000000..f62abb1 --- /dev/null +++ b/.idea/libraries/google_code_gson.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/io_javalin.xml b/.idea/libraries/io_javalin.xml new file mode 100644 index 0000000..d7ab6e0 --- /dev/null +++ b/.idea/libraries/io_javalin.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/net_java_dev_jna.xml b/.idea/libraries/net_java_dev_jna.xml new file mode 100644 index 0000000..8705a10 --- /dev/null +++ b/.idea/libraries/net_java_dev_jna.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/projectlombok_lombok.xml b/.idea/libraries/projectlombok_lombok.xml new file mode 100644 index 0000000..8fe179f --- /dev/null +++ b/.idea/libraries/projectlombok_lombok.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/tinylog.xml b/.idea/libraries/tinylog.xml new file mode 100644 index 0000000..6f50db9 --- /dev/null +++ b/.idea/libraries/tinylog.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..af89096 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,50 @@ + + + + + + + + + Android + + + CodePlugin DevKit + + + ComplianceLintAndroid + + + CorrectnessLintAndroid + + + Java + + + Java language level migration aidsJava + + + LintAndroid + + + PerformanceLintAndroid + + + Plugin DevKit + + + UsabilityLintAndroid + + + + + Android + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f8dd09c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SIPIntercom.iml b/SIPIntercom.iml new file mode 100644 index 0000000..dc9b352 --- /dev/null +++ b/SIPIntercom.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/linux-aarch64/libbass.so b/libs/linux-aarch64/libbass.so new file mode 100644 index 0000000..ab2a517 Binary files /dev/null and b/libs/linux-aarch64/libbass.so differ diff --git a/libs/linux-armhf/libbass.so b/libs/linux-armhf/libbass.so new file mode 100644 index 0000000..ec90dc0 Binary files /dev/null and b/libs/linux-armhf/libbass.so differ diff --git a/libs/linux-x86-64/libbass.so b/libs/linux-x86-64/libbass.so new file mode 100644 index 0000000..3317ed4 Binary files /dev/null and b/libs/linux-x86-64/libbass.so differ diff --git a/libs/linux-x86/libbass.so b/libs/linux-x86/libbass.so new file mode 100644 index 0000000..74a204d Binary files /dev/null and b/libs/linux-x86/libbass.so differ diff --git a/libs/win32-arm64/bass.dll b/libs/win32-arm64/bass.dll new file mode 100644 index 0000000..7df046a Binary files /dev/null and b/libs/win32-arm64/bass.dll differ diff --git a/libs/win32-x86-64/bass.dll b/libs/win32-x86-64/bass.dll new file mode 100644 index 0000000..e02c370 Binary files /dev/null and b/libs/win32-x86-64/bass.dll differ diff --git a/libs/win32-x86/bass.dll b/libs/win32-x86/bass.dll new file mode 100644 index 0000000..10114b6 Binary files /dev/null and b/libs/win32-x86/bass.dll differ diff --git a/src/Audio/AudioEvent1.java b/src/Audio/AudioEvent1.java new file mode 100644 index 0000000..5df85d2 --- /dev/null +++ b/src/Audio/AudioEvent1.java @@ -0,0 +1,7 @@ +package Audio; + +public interface AudioEvent1 { + void fileconvertfinish(String source, String destination); + void filestreaming(String filename, int handle, byte[] data); + void inputstreaming(String inputname, int handle, byte[] data); +} diff --git a/src/Audio/Bass.java b/src/Audio/Bass.java new file mode 100644 index 0000000..15e318c --- /dev/null +++ b/src/Audio/Bass.java @@ -0,0 +1,1092 @@ +package Audio; + +import com.sun.jna.*; + + +@SuppressWarnings("unused") +public interface Bass extends Library { + + Bass Instance = (Bass) Native.load("bass", Bass.class); + int BASSVERSION = 0x204; // API version + String BASSVERSIONTEXT = "2.4"; + + // Error codes returned by BASS_ErrorGetCode + int BASS_OK = 0; // all is OK + int BASS_ERROR_MEM = 1; // memory error + int BASS_ERROR_FILEOPEN = 2; // can't open the file + int BASS_ERROR_DRIVER = 3; // can't find a free/valid driver + int BASS_ERROR_BUFLOST = 4; // the sample buffer was lost + int BASS_ERROR_HANDLE = 5; // invalid handle + int BASS_ERROR_FORMAT = 6; // unsupported sample format + int BASS_ERROR_POSITION = 7; // invalid position + int BASS_ERROR_INIT = 8; // BASS_Init has not been successfully called + int BASS_ERROR_START = 9; // BASS_Start has not been successfully called + int BASS_ERROR_SSL = 10; // SSL/HTTPS support isn't available + int BASS_ERROR_REINIT = 11; // device needs to be reinitialized + int BASS_ERROR_ALREADY = 14; // already initialized/paused/whatever + int BASS_ERROR_NOTAUDIO = 17; // file does not contain audio + int BASS_ERROR_NOCHAN = 18; // can't get a free channel + int BASS_ERROR_ILLTYPE = 19; // an illegal type was specified + int BASS_ERROR_ILLPARAM = 20; // an illegal parameter was specified + int BASS_ERROR_NO3D = 21; // no 3D support + int BASS_ERROR_NOEAX = 22; // no EAX support + int BASS_ERROR_DEVICE = 23; // illegal device number + int BASS_ERROR_NOPLAY = 24; // not playing + int BASS_ERROR_FREQ = 25; // illegal sample rate + int BASS_ERROR_NOTFILE = 27; // the stream is not a file stream + int BASS_ERROR_NOHW = 29; // no hardware voices available + int BASS_ERROR_EMPTY = 31; // the file has no sample data + int BASS_ERROR_NONET = 32; // no internet connection could be opened + int BASS_ERROR_CREATE = 33; // couldn't create the file + int BASS_ERROR_NOFX = 34; // effects are not available + int BASS_ERROR_NOTAVAIL = 37; // requested data/action is not available + int BASS_ERROR_DECODE = 38; // the channel is a "decoding channel" + int BASS_ERROR_DX = 39; // a sufficient DirectX version is not installed + int BASS_ERROR_TIMEOUT = 40; // connection timedout + int BASS_ERROR_FILEFORM = 41; // unsupported file format + int BASS_ERROR_SPEAKER = 42; // unavailable speaker + int BASS_ERROR_VERSION = 43; // invalid BASS version (used by add-ons) + int BASS_ERROR_CODEC = 44; // codec is not available/supported + int BASS_ERROR_ENDED = 45; // the channel/file has ended + int BASS_ERROR_BUSY = 46; // the device is busy + int BASS_ERROR_UNSTREAMABLE = 47; // unstreamable file + int BASS_ERROR_PROTOCOL = 48; // unsupported protocol + int BASS_ERROR_DENIED = 49; // access denied + int BASS_ERROR_UNKNOWN = -1; // some other mystery problem + + int BASS_ERROR_JAVA_CLASS = 500; // object class problem + + // BASS_SetConfig options + int BASS_CONFIG_BUFFER = 0; + int BASS_CONFIG_UPDATEPERIOD = 1; + int BASS_CONFIG_GVOL_SAMPLE = 4; + int BASS_CONFIG_GVOL_STREAM = 5; + int BASS_CONFIG_GVOL_MUSIC = 6; + int BASS_CONFIG_CURVE_VOL = 7; + int BASS_CONFIG_CURVE_PAN = 8; + int BASS_CONFIG_FLOATDSP = 9; + int BASS_CONFIG_3DALGORITHM = 10; + int BASS_CONFIG_NET_TIMEOUT = 11; + int BASS_CONFIG_NET_BUFFER = 12; + int BASS_CONFIG_PAUSE_NOPLAY = 13; + int BASS_CONFIG_NET_PREBUF = 15; + int BASS_CONFIG_NET_PASSIVE = 18; + int BASS_CONFIG_REC_BUFFER = 19; + int BASS_CONFIG_NET_PLAYLIST = 21; + int BASS_CONFIG_MUSIC_VIRTUAL = 22; + int BASS_CONFIG_VERIFY = 23; + int BASS_CONFIG_UPDATETHREADS = 24; + int BASS_CONFIG_DEV_BUFFER = 27; + int BASS_CONFIG_DEV_DEFAULT = 36; + int BASS_CONFIG_NET_READTIMEOUT = 37; + int BASS_CONFIG_HANDLES = 41; + int BASS_CONFIG_SRC = 43; + int BASS_CONFIG_SRC_SAMPLE = 44; + int BASS_CONFIG_ASYNCFILE_BUFFER = 45; + int BASS_CONFIG_OGG_PRESCAN = 47; + int BASS_CONFIG_DEV_NONSTOP = 50; + int BASS_CONFIG_VERIFY_NET = 52; + int BASS_CONFIG_DEV_PERIOD = 53; + int BASS_CONFIG_FLOAT = 54; + int BASS_CONFIG_NET_SEEK = 56; + int BASS_CONFIG_AM_DISABLE = 58; + int BASS_CONFIG_NET_PLAYLIST_DEPTH = 59; + int BASS_CONFIG_NET_PREBUF_WAIT = 60; + int BASS_CONFIG_ANDROID_SESSIONID = 62; + int BASS_CONFIG_ANDROID_AAUDIO = 67; + int BASS_CONFIG_SAMPLE_ONEHANDLE = 69; + int BASS_CONFIG_DEV_TIMEOUT = 70; + int BASS_CONFIG_NET_META = 71; + int BASS_CONFIG_NET_RESTRATE = 72; + int BASS_CONFIG_REC_DEFAULT = 73; + int BASS_CONFIG_NORAMP = 74; + + // BASS_SetConfigPtr options + int BASS_CONFIG_NET_AGENT = 16; + int BASS_CONFIG_NET_PROXY = 17; + int BASS_CONFIG_LIBSSL = 64; + int BASS_CONFIG_FILENAME = 75; + + int BASS_CONFIG_THREAD = 0x40000000; // flag: thread-specific setting + + // BASS_Init flags + int BASS_DEVICE_8BITS = 1; // unused + int BASS_DEVICE_MONO = 2; // mono + int BASS_DEVICE_3D = 4; // unused + int BASS_DEVICE_16BITS = 8; // limit output to 16-bit + int BASS_DEVICE_REINIT = 128; // reinitialize + int BASS_DEVICE_LATENCY = 0x100; // unused + int BASS_DEVICE_SPEAKERS = 0x800; // force enabling of speaker assignment + int BASS_DEVICE_NOSPEAKER = 0x1000; // ignore speaker arrangement + int BASS_DEVICE_DMIX = 0x2000; // use ALSA "dmix" plugin + int BASS_DEVICE_FREQ = 0x4000; // set device sample rate + int BASS_DEVICE_STEREO = 0x8000; // limit output to stereo + int BASS_DEVICE_AUDIOTRACK = 0x20000; // use AudioTrack output + int BASS_DEVICE_DSOUND = 0x40000; // use DirectSound output + int BASS_DEVICE_SOFTWARE = 0x80000; // disable hardware/fastpath output + + @Structure.FieldOrder({"name", "driver", "flags"}) + class BASS_DEVICEINFO extends Structure { + public String name; // description + public String driver; // driver + public int flags; + } + + // BASS_DEVICEINFO flags + int BASS_DEVICE_ENABLED = 1; + int BASS_DEVICE_DEFAULT = 2; + int BASS_DEVICE_INIT = 4; + + @Structure.FieldOrder({"flags", "hwsize", "hwfree", "freesam", "free3d", "minrate", "maxrate", "eax", "minbuf", "dsver", "latency", "initflags", "speakers", "freq"}) + 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 + } + + // Recording device info structure + @Structure.FieldOrder({"flags", "formats", "inputs", "singlein", "freq"}) + 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 + } + + // Sample info structure + @Structure.FieldOrder({"freq", "chans", "flags", "length", "max", "origres", "chans", "mingap", "mode3d", "mindist", "maxdist", "iangle", "oangle", "outvol", "vam", "priority"}) + 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 + } + + int BASS_SAMPLE_8BITS = 1; // 8 bit + int BASS_SAMPLE_FLOAT = 256; // 32-bit floating-point + int BASS_SAMPLE_MONO = 2; // mono + int BASS_SAMPLE_LOOP = 4; // looped + int BASS_SAMPLE_3D = 8; // 3D functionality + int BASS_SAMPLE_SOFTWARE = 16; // unused + int BASS_SAMPLE_MUTEMAX = 32; // mute at max distance (3D only) + int BASS_SAMPLE_VAM = 64; // unused + int BASS_SAMPLE_FX = 128; // unused + int BASS_SAMPLE_OVER_VOL = 0x10000; // override lowest volume + int BASS_SAMPLE_OVER_POS = 0x20000; // override longest playing + int BASS_SAMPLE_OVER_DIST = 0x30000; // override furthest from listener (3D only) + + int BASS_STREAM_PRESCAN = 0x20000; // scan file for accurate seeking and length + int BASS_STREAM_AUTOFREE = 0x40000; // automatically free the stream when it stops/ends + int BASS_STREAM_RESTRATE = 0x80000; // restrict the download rate of internet file streams + int BASS_STREAM_BLOCK = 0x100000; // download/play internet file stream in small blocks + int BASS_STREAM_DECODE = 0x200000; // don't play the stream, only decode (BASS_ChannelGetData) + int BASS_STREAM_STATUS = 0x800000; // give server status info (HTTP/ICY tags) in DOWNLOADPROC + + int BASS_MP3_IGNOREDELAY = 0x200; // ignore LAME/Xing/VBRI/iTunes delay & padding info + int BASS_MP3_SETPOS = BASS_STREAM_PRESCAN; + + int BASS_MUSIC_FLOAT = BASS_SAMPLE_FLOAT; + int BASS_MUSIC_MONO = BASS_SAMPLE_MONO; + int BASS_MUSIC_LOOP = BASS_SAMPLE_LOOP; + int BASS_MUSIC_3D = BASS_SAMPLE_3D; + int BASS_MUSIC_FX = BASS_SAMPLE_FX; + int BASS_MUSIC_AUTOFREE = BASS_STREAM_AUTOFREE; + int BASS_MUSIC_DECODE = BASS_STREAM_DECODE; + int BASS_MUSIC_PRESCAN = BASS_STREAM_PRESCAN; // calculate playback length + int BASS_MUSIC_CALCLEN = BASS_MUSIC_PRESCAN; + int BASS_MUSIC_RAMP = 0x200; // normal ramping + int BASS_MUSIC_RAMPS = 0x400; // sensitive ramping + int BASS_MUSIC_SURROUND = 0x800; // surround sound + int BASS_MUSIC_SURROUND2 = 0x1000; // surround sound (mode 2) + int BASS_MUSIC_FT2PAN = 0x2000; // apply FastTracker 2 panning to XM files + int BASS_MUSIC_FT2MOD = 0x2000; // play .MOD as FastTracker 2 does + int BASS_MUSIC_PT1MOD = 0x4000; // play .MOD as ProTracker 1 does + int BASS_MUSIC_NONINTER = 0x10000; // non-interpolated sample mixing + int BASS_MUSIC_SINCINTER = 0x800000; // sinc interpolated sample mixing + int BASS_MUSIC_POSRESET = 0x8000; // stop all notes when moving position + int BASS_MUSIC_POSRESETEX = 0x400000; // stop all notes and reset bmp/etc when moving position + int BASS_MUSIC_STOPBACK = 0x80000; // stop the music on a backwards jump effect + int BASS_MUSIC_NOSAMPLE = 0x100000; // don't load the samples + + // Speaker assignment flags + int BASS_SPEAKER_FRONT = 0x1000000; // front speakers + int BASS_SPEAKER_REAR = 0x2000000; // rear speakers + int BASS_SPEAKER_CENLFE = 0x3000000; // center & LFE speakers (5.1) + int BASS_SPEAKER_SIDE = 0x4000000; // side speakers (7.1) + static int BASS_SPEAKER_N(int n) { return n<<24; } // n'th pair of speakers (max 15) + int BASS_SPEAKER_LEFT = 0x10000000; // modifier: left + int BASS_SPEAKER_RIGHT = 0x20000000; // modifier: right + int BASS_SPEAKER_FRONTLEFT = BASS_SPEAKER_FRONT | BASS_SPEAKER_LEFT; + int BASS_SPEAKER_FRONTRIGHT = BASS_SPEAKER_FRONT | BASS_SPEAKER_RIGHT; + int BASS_SPEAKER_REARLEFT = BASS_SPEAKER_REAR | BASS_SPEAKER_LEFT; + int BASS_SPEAKER_REARRIGHT = BASS_SPEAKER_REAR | BASS_SPEAKER_RIGHT; + int BASS_SPEAKER_CENTER = BASS_SPEAKER_CENLFE | BASS_SPEAKER_LEFT; + int BASS_SPEAKER_LFE = BASS_SPEAKER_CENLFE | BASS_SPEAKER_RIGHT; + int BASS_SPEAKER_SIDELEFT = BASS_SPEAKER_SIDE | BASS_SPEAKER_LEFT; + int BASS_SPEAKER_SIDERIGHT = BASS_SPEAKER_SIDE | BASS_SPEAKER_RIGHT; + int BASS_SPEAKER_REAR2 = BASS_SPEAKER_SIDE; + int BASS_SPEAKER_REAR2LEFT = BASS_SPEAKER_SIDELEFT; + int BASS_SPEAKER_REAR2RIGHT = BASS_SPEAKER_SIDERIGHT; + + int BASS_ASYNCFILE = 0x40000000; // read file asynchronously + + int BASS_RECORD_PAUSE = 0x8000; // start recording paused + + // Channel info structure + @Structure.FieldOrder({"freq", "chans", "flags", "ctype", "origres", "plugin", "sample", "filename"}) + 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; + } + + int BASS_ORIGRES_FLOAT = 0x10000; + + // BASS_CHANNELINFO types + int BASS_CTYPE_SAMPLE = 1; + int BASS_CTYPE_RECORD = 2; + int BASS_CTYPE_STREAM = 0x10000; + int BASS_CTYPE_STREAM_VORBIS = 0x10002; + int BASS_CTYPE_STREAM_OGG = 0x10002; + int BASS_CTYPE_STREAM_MP1 = 0x10003; + int BASS_CTYPE_STREAM_MP2 = 0x10004; + int BASS_CTYPE_STREAM_MP3 = 0x10005; + int BASS_CTYPE_STREAM_AIFF = 0x10006; + int BASS_CTYPE_STREAM_CA = 0x10007; + int BASS_CTYPE_STREAM_MF = 0x10008; + int BASS_CTYPE_STREAM_AM = 0x10009; + int BASS_CTYPE_STREAM_SAMPLE = 0x1000a; + int BASS_CTYPE_STREAM_DUMMY = 0x18000; + int BASS_CTYPE_STREAM_DEVICE = 0x18001; + int BASS_CTYPE_STREAM_WAV = 0x40000; // WAVE flag (LOWORD=codec) + int BASS_CTYPE_STREAM_WAV_PCM = 0x50001; + int BASS_CTYPE_STREAM_WAV_FLOAT = 0x50003; + int BASS_CTYPE_MUSIC_MOD = 0x20000; + int BASS_CTYPE_MUSIC_MTM = 0x20001; + int BASS_CTYPE_MUSIC_S3M = 0x20002; + int BASS_CTYPE_MUSIC_XM = 0x20003; + int BASS_CTYPE_MUSIC_IT = 0x20004; + int BASS_CTYPE_MUSIC_MO3 = 0x00100; // MO3 flag + + @Structure.FieldOrder({"ctype", "name", "exts"}) + class BASS_PLUGINFORM extends Structure { + int ctype; // channel type + String name; // format description + String exts; // file extension filter (*.ext1;*.ext2;etc...) + } + + @Structure.FieldOrder({"version", "formatc", "formats"}) + class BASS_PLUGININFO extends Structure { + int version; // version (same form as BASS_GetVersion) + int formatc; // number of formats + BASS_PLUGINFORM[] formats; // the array of formats + } + + // 3D vector (for 3D positions/velocities/orientations) + class BASS_3DVECTOR { + BASS_3DVECTOR() {} + BASS_3DVECTOR(float _x, float _y, float _z) { x=_x; y=_y; z=_z; } + float x; // +=right, -=left + float y; // +=up, -=down + float z; // +=front, -=behind + } + + // 3D channel modes + int BASS_3DMODE_NORMAL = 0; // normal 3D processing + int BASS_3DMODE_RELATIVE = 1; // position is relative to the listener + int BASS_3DMODE_OFF = 2; // no 3D processing + + // software 3D mixing algorithms (used with BASS_CONFIG_3DALGORITHM) + int BASS_3DALG_DEFAULT = 0; + int BASS_3DALG_OFF = 1; + int BASS_3DALG_FULL = 2; + int BASS_3DALG_LIGHT = 3; + + // BASS_SampleGetChannel flags + int BASS_SAMCHAN_NEW = 1; // get a new playback channel + int BASS_SAMCHAN_STREAM = 2; // create a stream + + 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. */ + } + + int BASS_STREAMPROC_END = 0x80000000; // end of user stream flag + + // Special STREAMPROCs + int STREAMPROC_DUMMY = 0; // "dummy" stream + int STREAMPROC_PUSH = -1; // push stream + int STREAMPROC_DEVICE = -2; // device mix stream + int STREAMPROC_DEVICE_3D = -3; // device 3D mix stream + + // BASS_StreamCreateFileUser file systems + int STREAMFILE_NOBUFFER = 0; + int STREAMFILE_BUFFER = 1; + int STREAMFILE_BUFFERPUSH = 2; + + interface BASS_FILEPROCS extends Callback + { + // User file stream callback functions + void FILECLOSEPROC(Pointer user); + long FILELENPROC(Pointer user) ; + int FILEREADPROC(Pointer buffer, int length, Pointer user); + boolean FILESEEKPROC(long offset, Pointer user); + } + + // BASS_StreamPutFileData options + int BASS_FILEDATA_END = 0; // end & close the file + + // BASS_StreamGetFilePosition modes + int BASS_FILEPOS_CURRENT = 0; + int BASS_FILEPOS_DECODE = BASS_FILEPOS_CURRENT; + int BASS_FILEPOS_DOWNLOAD = 1; + int BASS_FILEPOS_END = 2; + int BASS_FILEPOS_START = 3; + int BASS_FILEPOS_CONNECTED = 4; + int BASS_FILEPOS_BUFFER = 5; + int BASS_FILEPOS_SOCKET = 6; + int BASS_FILEPOS_ASYNCBUF = 7; + int BASS_FILEPOS_SIZE = 8; + int BASS_FILEPOS_BUFFERING = 9; + int BASS_FILEPOS_AVAILABLE = 10; + + 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 */ + } + + // BASS_ChannelSetSync types + int BASS_SYNC_POS = 0; + int BASS_SYNC_END = 2; + int BASS_SYNC_META = 4; + int BASS_SYNC_SLIDE = 5; + int BASS_SYNC_STALL = 6; + int BASS_SYNC_DOWNLOAD = 7; + int BASS_SYNC_FREE = 8; + int BASS_SYNC_SETPOS = 11; + int BASS_SYNC_MUSICPOS = 10; + int BASS_SYNC_MUSICINST = 1; + int BASS_SYNC_MUSICFX = 3; + int BASS_SYNC_OGG_CHANGE = 12; + int BASS_SYNC_DEV_FAIL = 14; + int BASS_SYNC_DEV_FORMAT = 15; + int BASS_SYNC_THREAD = 0x20000000; // flag: call sync in other thread + int BASS_SYNC_MIXTIME = 0x40000000; // flag: sync at mixtime, else at playtime + int BASS_SYNC_ONETIME = 0x80000000; // flag: sync only once, else continuously + + 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 */ + } + + 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 */ + } + + 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 */ + } + + // BASS_ChannelIsActive return values + int BASS_ACTIVE_STOPPED = 0; + int BASS_ACTIVE_PLAYING =1; + int BASS_ACTIVE_STALLED = 2; + int BASS_ACTIVE_PAUSED = 3; + int BASS_ACTIVE_PAUSED_DEVICE = 4; + + // Channel attributes + int BASS_ATTRIB_FREQ = 1; + int BASS_ATTRIB_VOL = 2; + int BASS_ATTRIB_PAN = 3; + int BASS_ATTRIB_EAXMIX = 4; + int BASS_ATTRIB_NOBUFFER = 5; + int BASS_ATTRIB_VBR = 6; + int BASS_ATTRIB_CPU = 7; + int BASS_ATTRIB_SRC = 8; + int BASS_ATTRIB_NET_RESUME = 9; + int BASS_ATTRIB_SCANINFO = 10; + int BASS_ATTRIB_NORAMP = 11; + int BASS_ATTRIB_BITRATE = 12; + int BASS_ATTRIB_BUFFER = 13; + int BASS_ATTRIB_GRANULE = 14; + int BASS_ATTRIB_USER = 15; + int BASS_ATTRIB_TAIL = 16; + int BASS_ATTRIB_PUSH_LIMIT = 17; + int BASS_ATTRIB_DOWNLOADPROC = 18; + int BASS_ATTRIB_VOLDSP = 19; + int BASS_ATTRIB_VOLDSP_PRIORITY = 20; + int BASS_ATTRIB_MUSIC_AMPLIFY = 0x100; + int BASS_ATTRIB_MUSIC_PANSEP = 0x101; + int BASS_ATTRIB_MUSIC_PSCALER = 0x102; + int BASS_ATTRIB_MUSIC_BPM = 0x103; + int BASS_ATTRIB_MUSIC_SPEED = 0x104; + int BASS_ATTRIB_MUSIC_VOL_GLOBAL = 0x105; + int BASS_ATTRIB_MUSIC_VOL_CHAN = 0x200; // + channel # + int BASS_ATTRIB_MUSIC_VOL_INST = 0x300; // + instrument # + + // BASS_ChannelSlideAttribute flags + int BASS_SLIDE_LOG = 0x1000000; + + // BASS_ChannelGetData flags + int BASS_DATA_AVAILABLE = 0; // query how much data is buffered + int BASS_DATA_NOREMOVE = 0x10000000; // flag: don't remove data from recording buffer + int BASS_DATA_FIXED = 0x20000000; // unused + int BASS_DATA_FLOAT = 0x40000000; // flag: return floating-point sample data + int BASS_DATA_FFT256 = 0x80000000; // 256 sample FFT + int BASS_DATA_FFT512 = 0x80000001; // 512 FFT + int BASS_DATA_FFT1024 = 0x80000002; // 1024 FFT + int BASS_DATA_FFT2048 = 0x80000003; // 2048 FFT + int BASS_DATA_FFT4096 = 0x80000004; // 4096 FFT + int BASS_DATA_FFT8192 = 0x80000005; // 8192 FFT + int BASS_DATA_FFT16384 = 0x80000006; // 16384 FFT + int BASS_DATA_FFT32768 = 0x80000007; // 32768 FFT + int BASS_DATA_FFT_INDIVIDUAL = 0x10; // FFT flag: FFT for each channel, else all combined + int BASS_DATA_FFT_NOWINDOW = 0x20; // FFT flag: no Hanning window + int BASS_DATA_FFT_REMOVEDC = 0x40; // FFT flag: pre-remove DC bias + int BASS_DATA_FFT_COMPLEX = 0x80; // FFT flag: return complex data + int BASS_DATA_FFT_NYQUIST = 0x100; // FFT flag: return extra Nyquist value + + // BASS_ChannelGetLevelEx flags + int BASS_LEVEL_MONO = 1; // get mono level + int BASS_LEVEL_STEREO = 2; // get stereo level + int BASS_LEVEL_RMS = 4; // get RMS levels + int BASS_LEVEL_VOLPAN = 8; // apply VOL/PAN attributes to the levels + int BASS_LEVEL_NOREMOVE = 16; // don't remove data from recording buffer + + // BASS_ChannelGetTags types : what's returned + int BASS_TAG_ID3 = 0; // ID3v1 tags : TAG_ID3 + int BASS_TAG_ID3V2 = 1; // ID3v2 tags : ByteBuffer + int BASS_TAG_OGG = 2; // OGG comments : String array + int BASS_TAG_HTTP = 3; // HTTP headers : String array + int BASS_TAG_ICY = 4; // ICY headers : String array + int BASS_TAG_META = 5; // ICY metadata : String + int BASS_TAG_APE = 6; // APE tags : String array + int BASS_TAG_MP4 = 7; // MP4/iTunes metadata : String array + int BASS_TAG_VENDOR = 9; // OGG encoder : String + int BASS_TAG_LYRICS3 = 10; // Lyric3v2 tag : String + int BASS_TAG_WAVEFORMAT = 14; // WAVE format : ByteBuffer containing WAVEFORMATEEX structure + int BASS_TAG_AM_NAME = 16; // Android Media codec name : String + int BASS_TAG_ID3V2_2 = 17; // ID3v2 tags (2nd block) : ByteBuffer + int BASS_TAG_AM_MIME = 18; // Android Media MIME type : String + int BASS_TAG_LOCATION = 19; // redirected URL : String + int BASS_TAG_RIFF_INFO = 0x100; // RIFF "INFO" tags : String array + int BASS_TAG_RIFF_BEXT = 0x101; // RIFF/BWF "bext" tags : TAG_BEXT + int BASS_TAG_RIFF_CART = 0x102; // RIFF/BWF "cart" tags : TAG_CART + int BASS_TAG_RIFF_DISP = 0x103; // RIFF "DISP" text tag : String + int BASS_TAG_RIFF_CUE = 0x104; // RIFF "cue " chunk : TAG_CUE structure + int BASS_TAG_RIFF_SMPL = 0x105; // RIFF "smpl" chunk : TAG_SMPL structure + int BASS_TAG_APE_BINARY = 0x1000; // + index #, binary APE tag : TAG_APE_BINARY + int BASS_TAG_MUSIC_NAME = 0x10000; // MOD music name : String + int BASS_TAG_MUSIC_MESSAGE = 0x10001; // MOD message : String + int BASS_TAG_MUSIC_ORDERS = 0x10002; // MOD order list : ByteBuffer + int BASS_TAG_MUSIC_AUTH = 0x10003; // MOD author : UTF-8 string + int BASS_TAG_MUSIC_INST = 0x10100; // + instrument #, MOD instrument name : String + int BASS_TAG_MUSIC_CHAN = 0x10200; // + channel #, MOD channel name : String + int BASS_TAG_MUSIC_SAMPLE = 0x10300; // + sample #, MOD sample name : String + int BASS_TAG_BYTEBUFFER = 0x10000000; // flag: return a ByteBuffer instead of a String or TAG_ID3 + + // ID3v1 tag structure + @Structure.FieldOrder({"id", "title", "artist", "album", "year", "comment", "genre", "track"}) + class TAG_ID3 extends Structure { + String id; + String title; + String artist; + String album; + String year; + String comment; + byte genre; + byte track; + } + + // Binary APE tag structure + @Structure.FieldOrder({"key", "data", "length"}) + class TAG_APE_BINARY extends Structure { + String key; + Pointer data; + int length; + } + + // BASS_ChannelGetLength/GetPosition/SetPosition modes + int BASS_POS_BYTE = 0; // byte position + int BASS_POS_MUSIC_ORDER = 1; // order.row position, MAKELONG(order,row) + int BASS_POS_OGG = 3; // OGG bitstream number + int BASS_POS_END = 0x10; // trimmed end position + int BASS_POS_LOOP = 0x11; // loop start positiom + int BASS_POS_FLUSH = 0x1000000; // flag: flush decoder/FX buffers + int BASS_POS_RESET = 0x2000000; // flag: reset user file buffers + int BASS_POS_RELATIVE = 0x4000000; // flag: seek relative to the current position + int BASS_POS_INEXACT = 0x8000000; // flag: allow seeking to inexact position + int BASS_POS_DECODE = 0x10000000; // flag: get the decoding (not playing) position + int BASS_POS_DECODETO = 0x20000000; // flag: decode to the position instead of seeking + int BASS_POS_SCAN = 0x40000000; // flag: scan to the position + + // BASS_ChannelSetDevice/GetDevice option + int BASS_NODEVICE = 0x20000; + + // BASS_RecordSetInput flags + int BASS_INPUT_OFF = 0x10000; + int BASS_INPUT_ON = 0x20000; + int BASS_INPUT_TYPE_MASK = 0xff000000; + int BASS_INPUT_TYPE_UNDEF=0x00000000; + int BASS_INPUT_TYPE_DIGITAL=0x01000000; + int BASS_INPUT_TYPE_LINE=0x02000000; + int BASS_INPUT_TYPE_MIC=0x03000000; + int BASS_INPUT_TYPE_SYNTH=0x04000000; + int BASS_INPUT_TYPE_CD=0x05000000; + int BASS_INPUT_TYPE_PHONE=0x06000000; + int BASS_INPUT_TYPE_SPEAKER=0x07000000; + int BASS_INPUT_TYPE_WAVE=0x08000000; + int BASS_INPUT_TYPE_AUX=0x09000000; + int BASS_INPUT_TYPE_ANALOG=0x0a000000; + + + // DX8 effect types, use with BASS_ChannelSetFX + int BASS_FX_DX8_CHORUS = 0; + int BASS_FX_DX8_COMPRESSOR = 1; + int BASS_FX_DX8_DISTORTION = 2; + int BASS_FX_DX8_ECHO = 3; + int BASS_FX_DX8_FLANGER = 4; + int BASS_FX_DX8_GARGLE = 5; + int BASS_FX_DX8_I3DL2REVERB = 6; + int BASS_FX_DX8_PARAMEQ = 7; + int BASS_FX_DX8_REVERB = 8; + int BASS_FX_VOLUME = 9; + + @Structure.FieldOrder({"fWetDryMix", "fDepth", "fFeedback", "fFrequency", "lWaveform", "fDelay", "lPhase"}) + class BASS_DX8_CHORUS extends Structure { + float fWetDryMix; + float fDepth; + float fFeedback; + float fFrequency; + int lWaveform; // 0=triangle, 1=sine + float fDelay; + int lPhase; // BASS_DX8_PHASE_xxx + } + + @Structure.FieldOrder({"fGain","fEdge","fPostEQCenterFrequency","fPostEQBandwidth","fPreLowpassCutoff"}) + class BASS_DX8_DISTORTION extends Structure { + float fGain; + float fEdge; + float fPostEQCenterFrequency; + float fPostEQBandwidth; + float fPreLowpassCutoff; + } + + @Structure.FieldOrder({"fWetDryMix","fFeedback","fLeftDelay","fRightDelay","lPanDelay"}) + class BASS_DX8_ECHO extends Structure { + float fWetDryMix; + float fFeedback; + float fLeftDelay; + float fRightDelay; + boolean lPanDelay; + } + + @Structure.FieldOrder({"fWetDryMix","fDepth","fFeedback","fFrequency","lWaveform","fDelay","lPhase"}) + class BASS_DX8_FLANGER extends Structure { + float fWetDryMix; + float fDepth; + float fFeedback; + float fFrequency; + int lWaveform; // 0=triangle, 1=sine + float fDelay; + int lPhase; // BASS_DX8_PHASE_xxx + } + + @Structure.FieldOrder({"fCenter","fBandwidth","fGain"}) + class BASS_DX8_PARAMEQ extends Structure { + float fCenter; + float fBandwidth; + float fGain; + } + + @Structure.FieldOrder({"fInGain","fReverbMix","fReverbTime","fHighFreqRTRatio"}) + class BASS_DX8_REVERB extends Structure { + float fInGain; + float fReverbMix; + float fReverbTime; + float fHighFreqRTRatio; + } + + int BASS_DX8_PHASE_NEG_180 = 0; + int BASS_DX8_PHASE_NEG_90 = 1; + int BASS_DX8_PHASE_ZERO = 2; + int BASS_DX8_PHASE_90 = 3; + int BASS_DX8_PHASE_180 = 4; + + @Structure.FieldOrder({"fTarget","fCurrent","fTime","lCurve"}) + class BASS_FX_VOLUME_PARAM extends Structure { + float fTarget; + float fCurrent; + float fTime; + int lCurve; + } + + class FloatValue { + public float value; + } + + boolean BASS_SetConfig(int option, int value); + int BASS_GetConfig(int option); + boolean BASS_SetConfigPtr(int option, Pointer value); + Object BASS_GetConfigPtr(int option); + int BASS_GetVersion(); + int BASS_ErrorGetCode(); + boolean BASS_GetDeviceInfo(int device, BASS_DEVICEINFO info); + boolean BASS_Init(int device, int freq, int flags); + boolean BASS_Free(); + boolean BASS_SetDevice(int device); + int BASS_GetDevice(); + boolean BASS_GetInfo(BASS_INFO info); + boolean BASS_Start(); + boolean BASS_Stop(); + boolean BASS_Pause(); + int BASS_IsStarted(); + boolean BASS_Update(int length); + float BASS_GetCPU(); + boolean BASS_SetVolume(float volume); + float BASS_GetVolume(); + + boolean BASS_Set3DFactors(float distf, float rollf, float doppf); + boolean BASS_Get3DFactors(FloatValue distf, FloatValue rollf, FloatValue doppf); + boolean BASS_Set3DPosition(BASS_3DVECTOR pos, BASS_3DVECTOR vel, BASS_3DVECTOR front, BASS_3DVECTOR top); + boolean BASS_Get3DPosition(BASS_3DVECTOR pos, BASS_3DVECTOR vel, BASS_3DVECTOR front, BASS_3DVECTOR top); + void BASS_Apply3D(); + + int BASS_PluginLoad(String file, int flags); + boolean BASS_PluginFree(int handle); + boolean BASS_PluginEnable(int handle, boolean enable); + BASS_PLUGININFO BASS_PluginGetInfo(int handle); + + int BASS_SampleLoad(String file, long offset, int length, int max, int flags); + int BASS_SampleLoad(Pointer file, long offset, int length, int max, int flags); + int BASS_SampleCreate(int length, int freq, int chans, int max, int flags); + boolean BASS_SampleFree(int handle); + boolean BASS_SampleSetData(int handle, Pointer buffer); + boolean BASS_SampleGetData(int handle, Pointer buffer); + boolean BASS_SampleGetInfo(int handle, BASS_SAMPLE info); + boolean BASS_SampleSetInfo(int handle, BASS_SAMPLE info); + int BASS_SampleGetChannel(int handle, boolean onlynew); + int BASS_SampleGetChannels(int handle, int[] channels); + boolean BASS_SampleStop(int handle); + + int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user); + int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags); + int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags); + int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); + int BASS_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); + boolean BASS_StreamFree(int handle); + long BASS_StreamGetFilePosition(int handle, int mode); + int BASS_StreamPutData(int handle, Pointer buffer, int length); + int BASS_StreamPutFileData(int handle, Pointer buffer, int length); + + int BASS_MusicLoad(String file, long offset, int length, int flags, int freq); + int BASS_MusicLoad(Pointer file, long offset, int length, int flags, int freq); + boolean BASS_MusicFree(int handle); + + boolean BASS_RecordGetDeviceInfo(int device, BASS_DEVICEINFO info); + boolean BASS_RecordInit(int device); + boolean BASS_RecordFree(); + boolean BASS_RecordSetDevice(int device); + int BASS_RecordGetDevice(); + boolean BASS_RecordGetInfo(BASS_RECORDINFO info); + String BASS_RecordGetInputName(int input); + boolean BASS_RecordSetInput(int input, int flags, float volume); + int BASS_RecordGetInput(int input, FloatValue volume); + int BASS_RecordStart(int freq, int chans, int flags, RECORDPROC proc, Pointer user); + + double BASS_ChannelBytes2Seconds(int handle, long pos); + long BASS_ChannelSeconds2Bytes(int handle, double pos); + int BASS_ChannelGetDevice(int handle); + boolean BASS_ChannelSetDevice(int handle, int device); + int BASS_ChannelIsActive(int handle); + boolean BASS_ChannelGetInfo(int handle, BASS_CHANNELINFO info); + Object BASS_ChannelGetTags(int handle, int tags); + long BASS_ChannelFlags(int handle, int flags, int mask); + boolean BASS_ChannelLock(int handle, boolean lock); + boolean BASS_ChannelFree(int handle); + boolean BASS_ChannelPlay(int handle, boolean restart); + boolean BASS_ChannelStart(int handle); + boolean BASS_ChannelStop(int handle); + boolean BASS_ChannelPause(int handle); + boolean BASS_ChannelUpdate(int handle, int length); + boolean BASS_ChannelSetAttribute(int handle, int attrib, float value); + boolean BASS_ChannelGetAttribute(int handle, int attrib, FloatValue value); + boolean BASS_ChannelSlideAttribute(int handle, int attrib, float value, int time); + boolean BASS_ChannelIsSliding(int handle, int attrib); + boolean BASS_ChannelSetAttributeEx(int handle, int attrib, Pointer value, int size); + boolean BASS_ChannelSetAttributeDOWNLOADPROC(int handle, DOWNLOADPROC proc, Pointer user); + int BASS_ChannelGetAttributeEx(int handle, int attrib, Pointer value, int size); + boolean BASS_ChannelSet3DAttributes(int handle, int mode, float min, float max, int iangle, int oangle, float outvol); + boolean BASS_ChannelGet3DAttributes(int handle, Integer mode, FloatValue min, FloatValue max, Integer iangle, Integer oangle, FloatValue outvol); + boolean BASS_ChannelSet3DPosition(int handle, BASS_3DVECTOR pos, BASS_3DVECTOR orient, BASS_3DVECTOR vel); + boolean BASS_ChannelGet3DPosition(int handle, BASS_3DVECTOR pos, BASS_3DVECTOR orient, BASS_3DVECTOR vel); + long BASS_ChannelGetLength(int handle, int mode); + boolean BASS_ChannelSetPosition(int handle, long pos, int mode); + long BASS_ChannelGetPosition(int handle, int mode); + int BASS_ChannelGetLevel(int handle); + boolean BASS_ChannelGetLevelEx(int handle, float[] levels, float length, int flags); + int BASS_ChannelGetData(int handle, Pointer buffer, int length); + int BASS_ChannelSetSync(int handle, int type, long param, SYNCPROC proc, Pointer user); + boolean BASS_ChannelRemoveSync(int handle, int sync); + boolean BASS_ChannelSetLink(int handle, int chan); + boolean BASS_ChannelRemoveLink(int handle, int chan); + int BASS_ChannelSetDSP(int handle, DSPPROC proc, Pointer user, int priority); + boolean BASS_ChannelRemoveDSP(int handle, int dsp); + int BASS_ChannelSetFX(int handle, int type, int priority); + boolean BASS_ChannelRemoveFX(int handle, int fx); + + boolean BASS_FXSetParameters(int handle, Object params); + boolean BASS_FXGetParameters(int handle, Object params); + boolean BASS_FXSetPriority(int handle, int priority); + boolean BASS_FXReset(int handle); + // gak bisa + int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user); + + default String GetBassError(String function, int err) { + if (err==0) + return function+": SUCCESS"; + else if (err==BASS_ERROR_DEVICE) + return function+": Device Is Invalid"; + else if (err==BASS_ERROR_INIT) + return function+": BASS_Init/BASS_RecordInit has not been called"; + else if (err==BASS_ERROR_MEM) + return function+": Insufficient Memory"; + else if (err==BASS_ERROR_HANDLE) + return function+": Invalid Handle"; + else if (err==BASS_ERROR_NO3D) + return function+": Couldnt initialize 3D support"; + else if (err==BASS_ERROR_SPEAKER) + return function+": Specified SPEAKERS flags are invalid"; + else if (err==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": + if (err == BASS_ERROR_BUSY) { + return str.append("Device curently being reinitialized").toString(); + } + return str.append("Unknown error code=").append(err).toString(); + case "bass_init": + case "bass_recordinit": + switch(err) { + case BASS_ERROR_NOTAVAIL : + str.append("BASS_DEVICE_REINIT cannot be used when Device is -1. Use Real Device Number instead"); + return str.toString(); + case BASS_ERROR_ALREADY : + str.append("Device already Initialized"); + return str.toString(); + case BASS_ERROR_DRIVER : + str.append("Device Driver not available"); + return str.toString(); + case BASS_ERROR_BUSY : + str.append("Device has exclusive use from other"); + return str.toString(); + case BASS_ERROR_FORMAT : + str.append("Format invalid, Try changing Frequency parameter"); + return str.toString(); + default : + str.append("Unknown error code=").append(err); + return str.toString(); + + } + case "bass_streamcreate": + case "bass_streamcreateconst": + switch(err) { + case BASS_ERROR_NOTAVAIL : + str.append("BASS_STREAM_FREE cannot be combined with BASS_STREAM_DECODE"); + return str.toString(); + case BASS_ERROR_FORMAT : + str.append("Sample format is not supported"); + return str.toString(); + case BASS_ERROR_SPEAKER : + str.append("Specified Speaker Flag are invalid"); + return str.toString(); + default : + str.append("Unknown error code=").append(err); + return str.toString(); + } + case "bass_streamcreatefile" : + switch(err) { + case BASS_ERROR_NOTAVAIL : + str.append("BASS_STREAM_AUTOFREE cannot be combined with BASS_STREAM_DECODE"); + return str.toString(); + case BASS_ERROR_ILLPARAM : + str.append("Length must be specified when streaming from memory"); + return str.toString(); + case BASS_ERROR_FILEOPEN: + str.append("File Cant be opened"); + return str.toString(); + case BASS_ERROR_FILEFORM: + str.append("File Format not recognized"); + return str.toString(); + case BASS_ERROR_NOTAUDIO: + str.append("File doesnt contain audio, or contain video but video is disabled"); + return str.toString(); + case BASS_ERROR_CODEC: + str.append("File use a codec that is not supported"); + return str.toString(); + case BASS_ERROR_FORMAT: + str.append("Sample Format is not supported"); + return str.toString(); + case BASS_ERROR_SPEAKER: + str.append("Specified SPEAKER flag is invalid"); + return str.toString(); + default: + str.append("Unknown error code=").append(err); + return str.toString(); + } + case "bass_channelgetdata": + switch(err) { + case BASS_ERROR_ENDED : + return str.append("Channel has reached the end").toString(); + + case 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 BASS_ERROR_ILLPARAM: + return str.append("invalid flags were used").toString(); + + default: + return str.append("Unknown error code=").append(err).toString(); + + } + case "bass_mixer_streamcreate": + switch(err) { + case BASS_ERROR_NOTAVAIL: + return str.append("BASS_STREAM_AUTOFREE cannot be combined with BASS_STREAM_DECODE").toString(); + + case BASS_ERROR_FORMAT: + return str.append("Sample format is not supported").toString(); + + default: + return str.append("Unknown error code=").append(err).toString(); + + } + case "bass_mixer_streamaddchannel": + switch(err) { + case BASS_ERROR_DECODE: + return str.append("Source is not decoding channel").toString(); + + case BASS_ERROR_ALREADY: + return str.append("Source already added to a mixer").toString(); + + default: + return str.append("Unknown error code=").append(err).toString(); + + } + case "bass_streamfree": + case "bass_channelfree": + if (err == BASS_ERROR_NOTAVAIL) { + return str.append("Device Streams STREAMPROC_DEVICE cannot be freed").toString(); + } + return str.append("Unknown error code=").append(err).toString(); + case "bass_channelstart": + case "bass_channelplay": + switch(err) { + case BASS_ERROR_DECODE: + return str.append("Handle is Decoding Channel, cant be played").toString(); + + case BASS_ERROR_START: + return str.append("Output is paused/stopped").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_channelsetdsp": + if (err == BASS_ERROR_ILLPARAM) { + return str.append("DSPPROC cant NULL").toString(); + } + return str.append("Unknown error code=").append(err).toString(); + case "bass_channelsetattribute": + switch(err) { + case BASS_ERROR_ILLTYPE: + return str.append("Attribute Not Valid").toString(); + case BASS_ERROR_ILLPARAM : + return str.append("Valud not valid").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_channelgetattribute": + switch(err) { + case BASS_ERROR_NOTAVAIL: + return str.append("Attribute Not Available").toString(); + case BASS_ERROR_ILLTYPE: + return str.append("Attribute Not Valid").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_setvolume": + case "bass_getvolume": + switch(err) { + case BASS_ERROR_NOTAVAIL: + return str.append("No Volume Control for No-Sound device").toString(); + case BASS_ERROR_ILLPARAM: + return str.append("Volume is invalid").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_recordgetdeviceinfo": + if (err == BASS_ERROR_DX) { + return str.append("DirectX not installed").toString(); + } + return str.append("Unknown error code=").append(err).toString(); + case "bass_recordgetinputname": + switch(err) { + case BASS_ERROR_ILLPARAM: + return str.append("Input is Invalid").toString(); + case BASS_ERROR_NOTAVAIL: + return str.append("Master Input is not available").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_recordstart": + switch(err) { + case BASS_ERROR_BUSY: + return str.append("Device is Busy. Another Recording need to be stopped before starting another one").toString(); + case BASS_ERROR_DENIED: + return str.append("Recording permission denied").toString(); + case BASS_ERROR_FORMAT: + return str.append("Sample format is not supported").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_recordsetinput": + case "bass_recordgetinput": + switch(err) { + case BASS_ERROR_ILLPARAM : + return str.append("Input or Volume is invalid").toString(); + case BASS_ERROR_NOTAVAIL: + return str.append("Input doesnt have volume control").toString(); + default: + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_pluginload": + switch(err) { + case BASS_ERROR_FILEOPEN: + return str.append("Plugin could not be opened").toString(); + case BASS_ERROR_FILEFORM: + return str.append("File is not plugin").toString(); + case BASS_ERROR_VERSION: + return str.append("Plugin version mismatch").toString(); + case 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 BASS_ERROR_FORMAT: + return str.append("Format not supported").toString(); + case BASS_ERROR_ILLPARAM: + return str.append("options contains invalid setting").toString(); + case BASS_ERROR_FILEOPEN: + return str.append("File could not be opened").toString(); + case BASS_ERROR_FILEFORM: + return str.append("File Format invalid").toString(); + case BASS_ERROR_NOTAVAIL: + return str.append("Function not available without FPU").toString(); + case BASS_ERROR_CREATE: + return str.append("Target file could not be created").toString(); + default : + return str.append("Unknown error code=").append(err).toString(); + } + case "bass_streamputdata": + switch(err) { + case BASS_ERROR_NOTAVAIL : + str.append("Stream is not using push system"); + return str.toString(); + case BASS_ERROR_ILLPARAM: + str.append("Length is not valid"); + return str.toString(); + case BASS_ERROR_ENDED: + str.append("Stream has ended"); + return str.toString(); + default: + str.append("Unknown error code = ").append(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 + */ + default String GetBassError(String function) { + return GetBassError(function, Instance.BASS_ErrorGetCode()); + + } + +} diff --git a/src/Audio/BassAAC.java b/src/Audio/BassAAC.java new file mode 100644 index 0000000..ba0f003 --- /dev/null +++ b/src/Audio/BassAAC.java @@ -0,0 +1,45 @@ +package Audio; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import Audio.Bass.BASS_FILEPROCS; +import Audio.Bass.DOWNLOADPROC; + +/** + * BASS AAC Library + * @author rdkartono + * + */ +@SuppressWarnings("unused") +public interface BassAAC extends Library { + + BassAAC Instance = (BassAAC) Native.load("bass_aac",BassAAC.class); + interface Constant{ + // additional error codes returned by BASS_ErrorGetCode + int BASS_ERROR_MP4_NOSTREAM = 6000; // non-streamable due to MP4 atom order ("mdat" before "moov") + + // Additional BASS_SetConfig options + int BASS_CONFIG_MP4_VIDEO = 0x10700; // play the audio from MP4 videos + int BASS_CONFIG_AAC_MP4 = 0x10701; // support MP4 in BASS_AAC_StreamCreateXXX functions (no need for BASS_MP4_StreamCreateXXX) + int BASS_CONFIG_AAC_PRESCAN = 0x10702; // pre-scan ADTS AAC files for seek points and accurate length + + // Additional BASS_AAC_StreamCreateFile/etc flags + int BASS_AAC_FRAME960 = 0x1000; // 960 samples per frame + int BASS_AAC_STEREO = 0x400000; // downmatrix to stereo + + // BASS_CHANNELINFO type + int BASS_CTYPE_STREAM_AAC = 0x10b00; // AAC + int BASS_CTYPE_STREAM_MP4 = 0x10b01; // AAC in MP4 + } + + + int BASS_AAC_StreamCreateFile(String file, long offset, long length, int flags); + int BASS_AAC_StreamCreateFile(Pointer file, long offset, long length, int flags); + int BASS_AAC_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); + int BASS_AAC_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); + int BASS_MP4_StreamCreateFile(String file, long offset, long length, int flags); + int BASS_MP4_StreamCreateFile(Pointer file, long offset, long length, int flags); + int BASS_MP4_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); +} diff --git a/src/Audio/BassEnc.java b/src/Audio/BassEnc.java new file mode 100644 index 0000000..e745b4c --- /dev/null +++ b/src/Audio/BassEnc.java @@ -0,0 +1,173 @@ +package Audio; +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 + * + */ +@SuppressWarnings("unused") +interface BassEnc extends Library { + BassEnc Instance = (BassEnc) Native.load("bassenc", BassEnc.class); + + 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 */ + } + + 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 */ + } + + 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) */ + } + + 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) */ + } + + 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 */ + } + + + int BASS_Encode_GetVersion(); + + int BASS_Encode_Start(int handle, String cmdline, int flags, ENCODEPROC proc, Pointer user); + int BASS_Encode_StartLimit(int handle, String cmdline, int flags, ENCODEPROC proc, Pointer user, int limit); + int BASS_Encode_StartUser(int handle, String file, int flags, ENCODERPROC proc, Pointer user); + boolean BASS_Encode_AddChunk(int handle, String id, Pointer buffer, int length); + boolean BASS_Encode_Write(int handle, Pointer buffer, int length); + boolean BASS_Encode_Stop(int handle); + boolean BASS_Encode_StopEx(int handle, boolean queue); + boolean BASS_Encode_SetPaused(int handle, boolean paused); + int BASS_Encode_IsActive(int handle); + boolean BASS_Encode_SetNotify(int handle, ENCODENOTIFYPROC proc, Pointer user); + long BASS_Encode_GetCount(int handle, int count); + boolean BASS_Encode_SetChannel(int handle, int channel); + int BASS_Encode_GetChannel(int handle); + boolean BASS_Encode_UserOutput(int handle, long offset, Pointer buffer, int length); + + 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); + boolean BASS_Encode_CastSetTitle(int handle, String title, String url); + boolean BASS_Encode_CastSendMeta(int handle, int type, Pointer data, int length); + String BASS_Encode_CastGetStats(int handle, int type, String pass); + + int BASS_Encode_ServerInit(int handle, String port, int buffer, int burst, int flags, ENCODECLIENTPROC proc, Pointer user); + boolean BASS_Encode_ServerKick(int handle, String client); + + interface Constant{ + // Additional error codes returned by BASS_ErrorGetCode + int BASS_ERROR_CAST_DENIED = 2100; // access denied (invalid password) + int BASS_ERROR_SERVER_CERT = 2101; // missing/invalid certificate + + // Additional BASS_SetConfig options + int BASS_CONFIG_ENCODE_PRIORITY = 0x10300; + int BASS_CONFIG_ENCODE_QUEUE = 0x10301; + int BASS_CONFIG_ENCODE_CAST_TIMEOUT = 0x10310; + + // Additional BASS_SetConfigPtr options + int BASS_CONFIG_ENCODE_CAST_PROXY = 0x10311; + int BASS_CONFIG_ENCODE_CAST_BIND = 0x10312; + int BASS_CONFIG_ENCODE_SERVER_CERT = 0x10320; + int BASS_CONFIG_ENCODE_SERVER_KEY = 0x10321; + + // BASS_Encode_Start flags + int BASS_ENCODE_NOHEAD = 1; // don't send a WAV header to the encoder + int BASS_ENCODE_FP_8BIT = 2; // convert floating-point sample data to 8-bit integer + int BASS_ENCODE_FP_16BIT = 4; // convert floating-point sample data to 16-bit integer + int BASS_ENCODE_FP_24BIT = 6; // convert floating-point sample data to 24-bit integer + int BASS_ENCODE_FP_32BIT = 8; // convert floating-point sample data to 32-bit integer + int BASS_ENCODE_FP_AUTO = 14; // convert floating-point sample data back to channel's format + int BASS_ENCODE_BIGEND = 16; // big-endian sample data + int BASS_ENCODE_PAUSE = 32; // start encording paused + int BASS_ENCODE_PCM = 64; // write PCM sample data (no encoder) + int BASS_ENCODE_RF64 = 128; // send an RF64 header + int BASS_ENCODE_QUEUE = 0x200; // queue data to feed encoder asynchronously + int BASS_ENCODE_WFEXT = 0x400; // WAVEFORMATEXTENSIBLE "fmt" chunk + int BASS_ENCODE_CAST_NOLIMIT = 0x1000; // don't limit casting data rate + int BASS_ENCODE_LIMIT = 0x2000; // limit data rate to real-time + int BASS_ENCODE_AIFF = 0x4000; // send an AIFF header rather than WAV + int BASS_ENCODE_DITHER = 0x8000; // apply dither when converting floating-point sample data to integer + int BASS_ENCODE_AUTOFREE = 0x40000; // free the encoder when the channel is freed + + // BASS_Encode_GetCount counts + int BASS_ENCODE_COUNT_IN = 0; // sent to encoder + int BASS_ENCODE_COUNT_OUT = 1; // received from encoder + int BASS_ENCODE_COUNT_CAST = 2; // sent to cast server + int BASS_ENCODE_COUNT_QUEUE = 3; // queued + int BASS_ENCODE_COUNT_QUEUE_LIMIT = 4; // queue limit + int BASS_ENCODE_COUNT_QUEUE_FAIL = 5; // failed to queue + int BASS_ENCODE_COUNT_IN_FP = 6; // sent to encoder before floating-point conversion + + // BASS_Encode_CastInit content MIME types + String BASS_ENCODE_TYPE_MP3 = "audio/mpeg"; + String BASS_ENCODE_TYPE_OGG = "audio/ogg"; + String BASS_ENCODE_TYPE_AAC = "audio/aacp"; + + // BASS_Encode_CastInit flags + int BASS_ENCODE_CAST_PUBLIC = 1; // add to public directory + int BASS_ENCODE_CAST_PUT = 2; // use PUT method + int BASS_ENCODE_CAST_SSL = 4; // use SSL/TLS encryption + + // BASS_Encode_CastGetStats types + int BASS_ENCODE_STATS_SHOUT = 0; // Shoutcast stats + int BASS_ENCODE_STATS_ICE = 1; // Icecast mount-point stats + int BASS_ENCODE_STATS_ICESERV = 2; // Icecast server stats + + // BASS_Encode_ServerInit flags + int BASS_ENCODE_SERVER_NOHTTP = 1; // no HTTP headers + int BASS_ENCODE_SERVER_META = 2; // Shoutcast metadata + int BASS_ENCODE_SERVER_SSL = 4; // support SSL/TLS encryption + int BASS_ENCODE_SERVER_SSLONLY = 8; // require SSL/TLS encryption + + // Encoder notifications + int BASS_ENCODE_NOTIFY_ENCODER = 1; // encoder died + int BASS_ENCODE_NOTIFY_CAST = 2; // cast server connection died + int BASS_ENCODE_NOTIFY_SERVER = 3; // server died + int BASS_ENCODE_NOTIFY_CAST_TIMEOUT = 0x10000; // cast timeout + int BASS_ENCODE_NOTIFY_QUEUE_FULL = 0x10001; // queue is out of space + int BASS_ENCODE_NOTIFY_FREE = 0x10002; // encoder has been freed + + } +} diff --git a/src/Audio/BassEncAAC.java b/src/Audio/BassEncAAC.java new file mode 100644 index 0000000..2b25a8c --- /dev/null +++ b/src/Audio/BassEncAAC.java @@ -0,0 +1,22 @@ +package Audio; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import Audio.BassEnc.ENCODEPROCEX; + +/** + * Bass Encoder AAC Plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +@SuppressWarnings("unused") +public interface BassEncAAC extends Library { + + BassEncAAC Instance = (BassEncAAC) Native.load("bassenc_aac",BassEncAAC.class); + int BASS_Encode_AAC_GetVersion(); + + int BASS_Encode_AAC_Start(int handle, String options, int flags, ENCODEPROCEX proc, Pointer user); + int BASS_Encode_AAC_StartFile(int handle, String options, int flags, String filename); +} diff --git a/src/Audio/BassEncMP3.java b/src/Audio/BassEncMP3.java new file mode 100644 index 0000000..4075fc4 --- /dev/null +++ b/src/Audio/BassEncMP3.java @@ -0,0 +1,23 @@ +package Audio; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import Audio.BassEnc.ENCODEPROCEX; + +/** + * BASS Encoder MP3 plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +@SuppressWarnings("unused") +public interface BassEncMP3 extends Library{ + BassEncMP3 Instance = (BassEncMP3) Native.load("bassenc_mp3",BassEncMP3.class); + + int BASS_Encode_MP3_GetVersion(); + + int BASS_Encode_MP3_Start(int handle, String options, int flags, ENCODEPROCEX proc, Pointer user); + int BASS_Encode_MP3_StartFile(int handle, String options, int flags, String filename); +} diff --git a/src/Audio/BassEncOGG.java b/src/Audio/BassEncOGG.java new file mode 100644 index 0000000..292a00a --- /dev/null +++ b/src/Audio/BassEncOGG.java @@ -0,0 +1,28 @@ +package Audio; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import Audio.BassEnc.ENCODEPROC; + +/** + * Bass Encoder OGG Plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +@SuppressWarnings("unused") +public interface BassEncOGG extends Library { + BassEncOGG Instance = (BassEncOGG) Native.load("bassenc_ogg",BassEncOGG.class); + int BASS_Encode_OGG_GetVersion(); + + int BASS_Encode_OGG_Start(int handle, String options, int flags, ENCODEPROC proc, Pointer user); + int BASS_Encode_OGG_StartFile(int handle, String options, int flags, String filename); + boolean BASS_Encode_OGG_NewStream(int handle, String options, int flags); + + interface Constant{ + // BASS_Encode_OGG_NewStream flags + int BASS_ENCODE_OGG_RESET = 0x1000000; + } +} diff --git a/src/Audio/BassEncOPUS.java b/src/Audio/BassEncOPUS.java new file mode 100644 index 0000000..e0992db --- /dev/null +++ b/src/Audio/BassEncOPUS.java @@ -0,0 +1,28 @@ +package Audio; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import Audio.BassEnc.ENCODEPROC; + +/** + * Bass Encoder OPUS Plugin + * Must load together with BassEncLibrary + * @author rdkartono + * + */ +@SuppressWarnings("unused") +public interface BassEncOPUS extends Library{ + BassEncOPUS BASSENC_OPUS = (BassEncOPUS) Native.load("bassenc_opus",BassEncOPUS.class); + interface Constant{ + // BASS_Encode_OPUS_NewStream flags + int BASS_ENCODE_OPUS_RESET = 0x1000000; + int BASS_ENCODE_OPUS_CTLONLY = 0x2000000; + } + int BASS_Encode_OPUS_GetVersion(); + + int BASS_Encode_OPUS_Start(int handle, String options, int flags, ENCODEPROC proc, Pointer user); + int BASS_Encode_OPUS_StartFile(int handle, String options, int flags, String filename); + boolean BASS_Encode_OPUS_NewStream(int handle, String options, int flags); +} diff --git a/src/Audio/BassFileReader.java b/src/Audio/BassFileReader.java new file mode 100644 index 0000000..1ef757b --- /dev/null +++ b/src/Audio/BassFileReader.java @@ -0,0 +1,140 @@ +package Audio; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import org.pmw.tinylog.Logger; +import peers.media.SoundSource; + +/** + * Pengganti /AxisAudio/src/net/sourceforge/peers/media/FileReader.java + * karena terbatas codec nya + * @author rdkartono + * + */ +@SuppressWarnings("FieldCanBeLocal") +public class BassFileReader implements SoundSource { + // 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 = Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_16BITS; + // flag ketika open file, BASS_STREAM_DECODE karena akan plug ke mixer + private final int openflag = Bass.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 = Bass.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 final BassFileReaderListener listener; // belum kepake + private final Bass BASS = Bass.Instance; + private final BassMix BASSMIX = BassMix.Instance; + + public BassFileReader(final String filename, BassFileReaderListener listener) { + this.filename = filename; + this.listener = listener; + if (BASS.BASS_GetVersion()!=0) { + if (BASSMIX.BASS_Mixer_GetVersion()!=0) { + boolean initresult = 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 = "+BASS.GetBassError("BASS_Init")); + + filehandle = BASS.BASS_StreamCreateFile(false, filename, 0, 0, openflag); + if (filehandle!=0){ + filesize = BASS.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE); + duration = BASS.BASS_ChannelBytes2Seconds(filehandle, filesize); + if (listener!=null) listener.fileopened(filename, filesize, duration); + + mixerhandle = BASSMIX.BASS_Mixer_StreamCreate(mixerfreq, mixerchannel, mixerflag); + if (mixerhandle!=0) { + if (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 = "+BASS.GetBassError("BASS_Mixer_StreamAddChannel")); + close(); + } + } else { + logger_error("BassFileReader BASS_Mixer_StreamCreate error = "+BASS.GetBassError("BASS_Mixer_StreamCreate")); + close(); + } + } else logger_error("BassFileReader BASS_StreamCreateFile error = "+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 (BASS.BASS_StreamFree(mixerhandle)) { + logger_info("mixer closed in BassFileReader"); + } else logger_error("BassFileReader mixerhandle BASS_StreamFree error = "+BASS.GetBassError("BASS_StreamFree")); + mixerhandle = 0; + } + if (filehandle!=0) { + if (BASS.BASS_StreamFree(filehandle)) { + logger_info(filename+" closed in BassFileReader"); + } else logger_error("BassFileReader filehandle BASS_StreamFree error = "+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 = 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 = "+BASS.GetBassError("BASS_ChannelGetData")); + } else { + // selesai + if (listener!=null) listener.filereadfinish(filename); + logger_info("BassFileReader BASS_ChannelGetData readsize = "+readsize+", closing it"); + close(); + } + } + return null; + } + + + private void logger_error(String msg) { + Logger.error(msg); + + } + + private void logger_info(String msg) { + Logger.info(msg); + } + +} diff --git a/src/Audio/BassFileReaderListener.java b/src/Audio/BassFileReaderListener.java new file mode 100644 index 0000000..917acfd --- /dev/null +++ b/src/Audio/BassFileReaderListener.java @@ -0,0 +1,14 @@ +package Audio; + +/** + * Java Event untuk BassFileReader + * @author rdkartono + * + */ +public interface BassFileReaderListener { + void fileopened(String filename, long filesize, double duration); + void fileclosed(String filename); + void filereadresult(String filename, int readlength, byte[] pcmdata); + void filereadstart(String filename); + void filereadfinish(String filename); +} diff --git a/src/Audio/BassMix.java b/src/Audio/BassMix.java new file mode 100644 index 0000000..709122a --- /dev/null +++ b/src/Audio/BassMix.java @@ -0,0 +1,133 @@ +package Audio; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import Audio.Bass.SYNCPROC; + +/** + * Bass Mixer Library + * @author rdkartono + * + */ +@SuppressWarnings("unused") +public interface BassMix extends Library { + BassMix Instance = (BassMix) Native.load("bassmix",BassMix.class); + + interface Constant{ + // Additional BASS_SetConfig options + int BASS_CONFIG_MIXER_BUFFER = 0x10601; + int BASS_CONFIG_MIXER_POSEX = 0x10602; + int BASS_CONFIG_SPLIT_BUFFER = 0x10610; + + // BASS_Mixer_StreamCreate flags + int BASS_MIXER_RESUME = 0x1000; // resume stalled immediately upon new/unpaused source + int BASS_MIXER_POSEX = 0x2000; // enable BASS_Mixer_ChannelGetPositionEx support + int BASS_MIXER_NOSPEAKER = 0x4000; // ignore speaker arrangement + int BASS_MIXER_QUEUE = 0x8000; // queue sources + int BASS_MIXER_END = 0x10000; // end the stream when there are no sources + int BASS_MIXER_NONSTOP = 0x20000; // don't stall when there are no sources + + // BASS_Mixer_StreamAddChannel/Ex flags + int BASS_MIXER_CHAN_ABSOLUTE = 0x1000; // start is an absolute position + int BASS_MIXER_CHAN_BUFFER = 0x2000; // buffer data for BASS_Mixer_ChannelGetData/Level + int BASS_MIXER_CHAN_LIMIT = 0x4000; // limit mixer processing to the amount available from this source + int BASS_MIXER_CHAN_MATRIX = 0x10000; // matrix mixing + int BASS_MIXER_CHAN_PAUSE = 0x20000; // don't process the source + int BASS_MIXER_CHAN_DOWNMIX = 0x400000; // downmix to stereo/mono + int BASS_MIXER_CHAN_NORAMPIN = 0x800000; // don't ramp-in the start + int BASS_MIXER_BUFFER = BASS_MIXER_CHAN_BUFFER; + int BASS_MIXER_LIMIT = BASS_MIXER_CHAN_LIMIT; + int BASS_MIXER_MATRIX = BASS_MIXER_CHAN_MATRIX; + int BASS_MIXER_PAUSE = BASS_MIXER_CHAN_PAUSE; + int BASS_MIXER_DOWNMIX = BASS_MIXER_CHAN_DOWNMIX; + int BASS_MIXER_NORAMPIN = BASS_MIXER_CHAN_NORAMPIN; + + // Mixer attributes + int BASS_ATTRIB_MIXER_LATENCY = 0x15000; + int BASS_ATTRIB_MIXER_THREADS = 0x15001; + int BASS_ATTRIB_MIXER_VOL = 0x15002; + + // Additional BASS_Mixer_ChannelIsActive return values + int BASS_ACTIVE_WAITING = 5; + int BASS_ACTIVE_QUEUED = 6; + + // BASS_Split_StreamCreate flags + int BASS_SPLIT_SLAVE = 0x1000; // only read buffered data + int BASS_SPLIT_POS = 0x2000; + + // Splitter attributes + int BASS_ATTRIB_SPLIT_ASYNCBUFFER = 0x15010; + int BASS_ATTRIB_SPLIT_ASYNCPERIOD = 0x15011; + + // Envelope types + int BASS_MIXER_ENV_FREQ = 1; + int BASS_MIXER_ENV_VOL = 2; + int BASS_MIXER_ENV_PAN = 3; + int BASS_MIXER_ENV_LOOP = 0x10000; // flag: loop + int BASS_MIXER_ENV_REMOVE = 0x20000; // flag: remove at end + + // Additional sync types + int BASS_SYNC_MIXER_ENVELOPE = 0x10200; + int BASS_SYNC_MIXER_ENVELOPE_NODE = 0x10201; + int BASS_SYNC_MIXER_QUEUE = 0x10202; + + // Additional BASS_Mixer_ChannelSetPosition flag + int BASS_POS_MIXER_RESET = 0x10000; // flag: clear mixer's playback buffer + + // Additional BASS_Mixer_ChannelGetPosition mode + int BASS_POS_MIXER_DELAY = 5; + + // BASS_CHANNELINFO types + int BASS_CTYPE_STREAM_MIXER = 0x10800; + int BASS_CTYPE_STREAM_SPLIT = 0x10801; + } + + + // Envelope node + @Structure.FieldOrder({"pos, value"}) + 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; + } + + + + int BASS_Mixer_GetVersion(); + + int BASS_Mixer_StreamCreate(int freq, int chans, int flags); + boolean BASS_Mixer_StreamAddChannel(int handle, int channel, int flags); + boolean BASS_Mixer_StreamAddChannelEx(int handle, int channel, int flags, long start, long length); + int BASS_Mixer_StreamGetChannels(int handle, int[] channels, int count); + + int BASS_Mixer_ChannelGetMixer(int handle); + int BASS_Mixer_ChannelIsActive(int handle); + int BASS_Mixer_ChannelFlags(int handle, int flags, int mask); + boolean BASS_Mixer_ChannelRemove(int handle); + boolean BASS_Mixer_ChannelSetPosition(int handle, long pos, int mode); + long BASS_Mixer_ChannelGetPosition(int handle, int mode); + long BASS_Mixer_ChannelGetPositionEx(int channel, int mode, int delay); + int BASS_Mixer_ChannelGetLevel(int handle); + boolean BASS_Mixer_ChannelGetLevelEx(int handle, float[] levels, float length, int flags); + int BASS_Mixer_ChannelGetData(int handle, Pointer buffer, int length); + int BASS_Mixer_ChannelSetSync(int handle, int type, long param, SYNCPROC proc, Pointer user); + boolean BASS_Mixer_ChannelRemoveSync(int channel, int sync); + boolean BASS_Mixer_ChannelSetMatrix(int handle, float[][] matrix); + boolean BASS_Mixer_ChannelSetMatrixEx(int handle, float[][] matrix, float time); + boolean BASS_Mixer_ChannelGetMatrix(int handle, float[][] matrix); + boolean BASS_Mixer_ChannelSetEnvelope(int handle, int type, BASS_MIXER_NODE[] nodes, int count); + boolean BASS_Mixer_ChannelSetEnvelopePos(int handle, int type, long pos); + long BASS_Mixer_ChannelGetEnvelopePos(int handle, int type, Float value); + + int BASS_Split_StreamCreate(int channel, int flags, int[] chanmap); + int BASS_Split_StreamGetSource(int handle); + int BASS_Split_StreamGetSplits(int handle, int[] splits, int count); + boolean BASS_Split_StreamReset(int handle); + boolean BASS_Split_StreamResetEx(int handle, int offset); + int BASS_Split_StreamGetAvailable(int handle); + +} diff --git a/src/Audio/BassOPUS.java b/src/Audio/BassOPUS.java new file mode 100644 index 0000000..d70d4c3 --- /dev/null +++ b/src/Audio/BassOPUS.java @@ -0,0 +1,24 @@ +package Audio; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import Audio.Bass.BASS_FILEPROCS; +import Audio.Bass.DOWNLOADPROC; + +@SuppressWarnings("unused") +public interface BassOPUS extends Library { + BassOPUS Instance = (BassOPUS) Native.load("bassopus",BassOPUS.class); + interface Constant{ + // BASS_CHANNELINFO type + int BASS_CTYPE_STREAM_OPUS = 0x11200; + + // Additional attributes + int BASS_ATTRIB_OPUS_ORIGFREQ = 0x13000; + } + int BASS_OPUS_StreamCreateFile(String file, long offset, long length, int flags); + int BASS_OPUS_StreamCreateFile(Pointer file, long offset, long length, int flags); + int BASS_OPUS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); + int BASS_OPUS_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user); +} diff --git a/src/Audio/CompressInputStream.java b/src/Audio/CompressInputStream.java new file mode 100644 index 0000000..93c28ed --- /dev/null +++ b/src/Audio/CompressInputStream.java @@ -0,0 +1,162 @@ +package Audio; + +import org.jetbrains.annotations.NotNull; + +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 : ... + * 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 + * + */ +@SuppressWarnings("unused") +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 final Compressor alawcompressor=new ALawCompressor(); + static private final Compressor ulawcompressor=new uLawCompressor(); + + private final Compressor compressor; + + public CompressInputStream(InputStream in, boolean useALaw) { + 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(@NotNull byte[] b)throws IOException{ + return read(b,0,b.length); + } + + public int read(@NotNull 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/Audio/ConvertInputStream.java b/src/Audio/ConvertInputStream.java new file mode 100644 index 0000000..b43f99e --- /dev/null +++ b/src/Audio/ConvertInputStream.java @@ -0,0 +1,118 @@ +package Audio; + +import org.jetbrains.annotations.NotNull; + +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 : ... + * @author rdkartono + * + */ +@SuppressWarnings("unused") +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 final 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 final 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 final byte[] table; + + public ConvertInputStream(InputStream in, boolean useALaw2uLaw){ + 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(@NotNull 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... + * 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 + * + */ +@SuppressWarnings("unused") +public class DecompressInputStream extends FilterInputStream{ + + /* + Convert A-Law or u-Law byte stream into mono PCM 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); + + */ + + /* + Mathematical Tools in Signal Processing with C++ and Java Simulations + by Willi-Hans Steeb + International School for Scientific Computing + */ + + static private final int[] alawtable={ + 0x80ea,0x80eb,0x80e8,0x80e9,0x80ee,0x80ef,0x80ec,0x80ed, + 0x80e2,0x80e3,0x80e0,0x80e1,0x80e6,0x80e7,0x80e4,0x80e5, + 0x40f5,0xc0f5,0x40f4,0xc0f4,0x40f7,0xc0f7,0x40f6,0xc0f6, + 0x40f1,0xc0f1,0x40f0,0xc0f0,0x40f3,0xc0f3,0x40f2,0xc0f2, + 0x00aa,0x00ae,0x00a2,0x00a6,0x00ba,0x00be,0x00b2,0x00b6, + 0x008a,0x008e,0x0082,0x0086,0x009a,0x009e,0x0092,0x0096, + 0x00d5,0x00d7,0x00d1,0x00d3,0x00dd,0x00df,0x00d9,0x00db, + 0x00c5,0x00c7,0x00c1,0x00c3,0x00cd,0x00cf,0x00c9,0x00cb, + 0xa8fe,0xb8fe,0x88fe,0x98fe,0xe8fe,0xf8fe,0xc8fe,0xd8fe, + 0x28fe,0x38fe,0x08fe,0x18fe,0x68fe,0x78fe,0x48fe,0x58fe, + 0xa8ff,0xb8ff,0x88ff,0x98ff,0xe8ff,0xf8ff,0xc8ff,0xd8ff, + 0x28ff,0x38ff,0x08ff,0x18ff,0x68ff,0x78ff,0x48ff,0x58ff, + 0xa0fa,0xe0fa,0x20fa,0x60fa,0xa0fb,0xe0fb,0x20fb,0x60fb, + 0xa0f8,0xe0f8,0x20f8,0x60f8,0xa0f9,0xe0f9,0x20f9,0x60f9, + 0x50fd,0x70fd,0x10fd,0x30fd,0xd0fd,0xf0fd,0x90fd,0xb0fd, + 0x50fc,0x70fc,0x10fc,0x30fc,0xd0fc,0xf0fc,0x90fc,0xb0fc, + 0x8015,0x8014,0x8017,0x8016,0x8011,0x8010,0x8013,0x8012, + 0x801d,0x801c,0x801f,0x801e,0x8019,0x8018,0x801b,0x801a, + 0xc00a,0x400a,0xc00b,0x400b,0xc008,0x4008,0xc009,0x4009, + 0xc00e,0x400e,0xc00f,0x400f,0xc00c,0x400c,0xc00d,0x400d, + 0x0056,0x0052,0x005e,0x005a,0x0046,0x0042,0x004e,0x004a, + 0x0076,0x0072,0x007e,0x007a,0x0066,0x0062,0x006e,0x006a, + 0x002b,0x0029,0x002f,0x002d,0x0023,0x0021,0x0027,0x0025, + 0x003b,0x0039,0x003f,0x003d,0x0033,0x0031,0x0037,0x0035, + 0x5801,0x4801,0x7801,0x6801,0x1801,0x0801,0x3801,0x2801, + 0xd801,0xc801,0xf801,0xe801,0x9801,0x8801,0xb801,0xa801, + 0x5800,0x4800,0x7800,0x6800,0x1800,0x0800,0x3800,0x2800, + 0xd800,0xc800,0xf800,0xe800,0x9800,0x8800,0xb800,0xa800, + 0x6005,0x2005,0xe005,0xa005,0x6004,0x2004,0xe004,0xa004, + 0x6007,0x2007,0xe007,0xa007,0x6006,0x2006,0xe006,0xa006, + 0xb002,0x9002,0xf002,0xd002,0x3002,0x1002,0x7002,0x5002, + 0xb003,0x9003,0xf003,0xd003,0x3003,0x1003,0x7003,0x5003, + }; + + static private final int[] ulawtable={ + 0x8482,0x8486,0x848a,0x848e,0x8492,0x8496,0x849a,0x849e, + 0x84a2,0x84a6,0x84aa,0x84ae,0x84b2,0x84b6,0x84ba,0x84be, + 0x84c1,0x84c3,0x84c5,0x84c7,0x84c9,0x84cb,0x84cd,0x84cf, + 0x84d1,0x84d3,0x84d5,0x84d7,0x84d9,0x84db,0x84dd,0x84df, + 0x04e1,0x04e2,0x04e3,0x04e4,0x04e5,0x04e6,0x04e7,0x04e8, + 0x04e9,0x04ea,0x04eb,0x04ec,0x04ed,0x04ee,0x04ef,0x04f0, + 0xc4f0,0x44f1,0xc4f1,0x44f2,0xc4f2,0x44f3,0xc4f3,0x44f4, + 0xc4f4,0x44f5,0xc4f5,0x44f6,0xc4f6,0x44f7,0xc4f7,0x44f8, + 0xa4f8,0xe4f8,0x24f9,0x64f9,0xa4f9,0xe4f9,0x24fa,0x64fa, + 0xa4fa,0xe4fa,0x24fb,0x64fb,0xa4fb,0xe4fb,0x24fc,0x64fc, + 0x94fc,0xb4fc,0xd4fc,0xf4fc,0x14fd,0x34fd,0x54fd,0x74fd, + 0x94fd,0xb4fd,0xd4fd,0xf4fd,0x14fe,0x34fe,0x54fe,0x74fe, + 0x8cfe,0x9cfe,0xacfe,0xbcfe,0xccfe,0xdcfe,0xecfe,0xfcfe, + 0x0cff,0x1cff,0x2cff,0x3cff,0x4cff,0x5cff,0x6cff,0x7cff, + 0x88ff,0x90ff,0x98ff,0xa0ff,0xa8ff,0xb0ff,0xb8ff,0xc0ff, + 0xc8ff,0xd0ff,0xd8ff,0xe0ff,0xe8ff,0xf0ff,0xf8ff,0x0000, + 0x7c7d,0x7c79,0x7c75,0x7c71,0x7c6d,0x7c69,0x7c65,0x7c61, + 0x7c5d,0x7c59,0x7c55,0x7c51,0x7c4d,0x7c49,0x7c45,0x7c41, + 0x7c3e,0x7c3c,0x7c3a,0x7c38,0x7c36,0x7c34,0x7c32,0x7c30, + 0x7c2e,0x7c2c,0x7c2a,0x7c28,0x7c26,0x7c24,0x7c22,0x7c20, + 0xfc1e,0xfc1d,0xfc1c,0xfc1b,0xfc1a,0xfc19,0xfc18,0xfc17, + 0xfc16,0xfc15,0xfc14,0xfc13,0xfc12,0xfc11,0xfc10,0xfc0f, + 0x3c0f,0xbc0e,0x3c0e,0xbc0d,0x3c0d,0xbc0c,0x3c0c,0xbc0b, + 0x3c0b,0xbc0a,0x3c0a,0xbc09,0x3c09,0xbc08,0x3c08,0xbc07, + 0x5c07,0x1c07,0xdc06,0x9c06,0x5c06,0x1c06,0xdc05,0x9c05, + 0x5c05,0x1c05,0xdc04,0x9c04,0x5c04,0x1c04,0xdc03,0x9c03, + 0x6c03,0x4c03,0x2c03,0x0c03,0xec02,0xcc02,0xac02,0x8c02, + 0x6c02,0x4c02,0x2c02,0x0c02,0xec01,0xcc01,0xac01,0x8c01, + 0x7401,0x6401,0x5401,0x4401,0x3401,0x2401,0x1401,0x0401, + 0xf400,0xe400,0xd400,0xc400,0xb400,0xa400,0x9400,0x8400, + 0x7800,0x7000,0x6800,0x6000,0x5800,0x5000,0x4800,0x4000, + 0x3800,0x3000,0x2800,0x2000,0x1800,0x1000,0x0800,0x0000, + }; + + private final int[] table; + + public DecompressInputStream(InputStream in, boolean useALaw){ + super(in); + table=(useALaw)?alawtable:ulawtable; + } + + public int read()throws IOException{ + throw new IOException(getClass().getName()+".read() :\n\tDo not support simple read()."); + } + + public int read(@NotNull byte[] b)throws IOException{ + return read(b,0,b.length); + } + + public int read(@NotNull byte[] b, int off, int len)throws IOException{ + byte[] inb; + int value; + + inb=new byte[len>>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/Audio/G711.java b/src/Audio/G711.java new file mode 100644 index 0000000..9607a30 --- /dev/null +++ b/src/Audio/G711.java @@ -0,0 +1,222 @@ +package Audio; + +/** G.711 codec. + * This class provides methods for u-law, A-law and linear PCM conversions. + */ +@SuppressWarnings("unused") +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/Audio/JSIPAudio.java b/src/Audio/JSIPAudio.java new file mode 100644 index 0000000..987dbc8 --- /dev/null +++ b/src/Audio/JSIPAudio.java @@ -0,0 +1,873 @@ +package Audio; +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 com.sun.jna.Native; + +import Audio.Bass.BASS_DEVICEINFO; +import org.pmw.tinylog.Logger; + + +/** + * SIP Audio Converter + * @author rdkartono + * + */ +@SuppressWarnings({"unused", "ResultOfMethodCallIgnored"}) +public class JSIPAudio { + private final Bass BASS = Bass.Instance; + private final BassMix BASSMIX = BassMix.Instance; + private final BassEnc BASSENC = BassEnc.Instance; + private final BassEncAAC BASSENC_AAC = BassEncAAC.Instance; + private final String currentfolder ; + + 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 AAC AAC; + + public JSIPAudio(AudioEvent1 ulaw_event, AudioEvent1 pcm16_event, AudioEvent1 aac_event) { + currentfolder = new File("").getAbsolutePath(); + + inited_playback = new HashSet<>(); + inited_recorder = new HashSet<>(); + loaded_plugin = new HashSet<>(); + + extract_libraries(); + + ULaw = new ULaw(16,1,ulaw_event); + PCM16 = new PCM(16,1, pcm16_event); + AAC = new AAC(16,1, aac_event); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + Logger.info("JSIPAudio ShutdownHook called"); + inited_recorder.forEach(recdev->{ + if (BASS.BASS_RecordSetDevice(recdev)) { + if (BASS.BASS_RecordFree()) { + Logger.info("Recorder Device="+recdev+" freed"); + } else Logger.error(BASS.GetBassError("BASS_RecordFree")); + } else Logger.error(BASS.GetBassError("BASS_RecordSetDevice")); + + }); + inited_recorder.clear(); + + inited_playback.forEach(playdev ->{ + if (BASS.BASS_SetDevice(playdev)) { + if (BASS.BASS_Free()) { + Logger.info("Playback Device="+playdev+" freed"); + } else Logger.error(BASS.GetBassError("BASS_Free")); + } else Logger.error(BASS.GetBassError("BASS_SetDevice")); + + }); + inited_playback.clear(); + + loaded_plugin.forEach(ph ->{ + if (BASS.BASS_PluginFree(ph)) { + Logger.info("Plugin freed"); + } else Logger.error(BASS.GetBassError("BASS_PluginFree")); + }); + loaded_plugin.clear(); + })); + inited = true; + } + + private String extract_library(String folder, String libname){ + try { + File existinglib = new File(folder, System.mapLibraryName(libname)); + File xx = Native.extractFromResourcePath(libname); + + if (existinglib.exists() && existinglib.isFile() && existinglib.length()==xx.length()) { + xx.delete(); // gak perlu + return existinglib.getAbsolutePath(); + } else { + if (xx.renameTo(existinglib)) { + return existinglib.getAbsolutePath(); + } else return xx.getAbsolutePath(); + + } + } catch (IOException e) { + return MessageFormat.format("IOException Libname={0} Message={1}", libname, e.getMessage()); + } + } + + private String ToHexString(int value) { + return "0x"+Integer.toHexString(value).toUpperCase(); + } + + + private void extract_libraries() { + raise_log(extract_library(currentfolder, "bass")); + raise_log(extract_library(currentfolder, "bassmix")); + raise_log(extract_library(currentfolder, "bass_aac")); + raise_log(extract_library(currentfolder, "bassenc")); + raise_log(extract_library(currentfolder, "bassenc_aac")); + + raise_log("Bass Version = "+ToHexString(BASS.BASS_GetVersion())); + raise_log("BassMix Version = "+ToHexString(BASSMIX.BASS_Mixer_GetVersion())); + raise_log("BassEnc Version = "+ToHexString(BASSENC.BASS_Encode_GetVersion())); + raise_log("BassEnc_AAC Version = "+ToHexString(BASSENC_AAC.BASS_Encode_AAC_GetVersion())); + + int pluginhandle = BASS.BASS_PluginLoad("bass_aac", 0); + if (pluginhandle!=0) { + loaded_plugin.add(pluginhandle); + raise_log("Plugin AAC Loaded"); + } else raise_log(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 (!BASS.BASS_ChannelSetAttribute(handle, Bass.BASS_ATTRIB_VOL, value/100.0f)) { + raise_log(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) { + Bass.FloatValue val = new Bass.FloatValue(); + if (BASS.BASS_ChannelGetAttribute(handle, Bass.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 (BASS.BASS_SetDevice(devid)) { + if (BASS.BASS_SetVolume(value/100.0f)) { + return true; + } else raise_log(BASS.GetBassError("BASS_SetVolume")); + } else raise_log(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 (BASS.BASS_SetDevice(devid)) { + float val = BASS.BASS_GetVolume(); + if (val>=0) { + int result = (int)(val * 100); + if (result>100) result = 100; + return result; + } else raise_log(BASS.GetBassError("BASS_GetVolume")); + } else raise_log(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 (BASS.BASS_RecordSetInput(devid, Bass.BASS_INPUT_ON, value/100.0f)) { + return true; + } else { + raise_log(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) { + Bass.FloatValue val = new Bass.FloatValue(); + int result = BASS.BASS_RecordGetInput(devid, val); + if (result==-1) { + // error + raise_log(BASS.GetBassError("BASS_RecordGetInput")); + return -1; + } else { + return (int)(val.value * 100); + } + } + + /** + * 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; + List result = new ArrayList<>(); + do { + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + success = 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; + List result = new ArrayList<>(); + do { + BASS_DEVICEINFO dev = new BASS_DEVICEINFO(); + success = 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 (BASS.BASS_GetDeviceInfo(devid, dev)) { + return dev; + } else raise_log(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 (BASS.BASS_RecordGetDeviceInfo(devid, dev)) { + return dev; + } else raise_log(BASS.GetBassError("BASS_RecordGetDeviceInfo")); + return null; + } + + private void raise_log(String msg) { + Logger.info(msg); + } + + @SuppressWarnings("BusyWait") + public class ULaw extends basicfunctions{ + public final int[] samplingrate = {8000}; + public final int channel; + public final int bits; + public final AudioEvent1 event; + public ULaw(int bits, int channel, AudioEvent1 event) { + super(48000, Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_FREQ); + this.channel = channel; + this.bits = bits; + this.event = event; + } + + /** + * 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 = System.currentTimeMillis(); + int totalread = 0; + int readcount; + + try { + do { + Pointer ptr = new Memory(1024); + readcount = 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 = System.currentTimeMillis() - timestamp; + raise_log(MessageFormat.format("ULaw FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta)); + if (event!=null) event.fileconvertfinish(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(JSIPAudio.this::raise_log); + } + + + } else { + Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + 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 = 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); + if (event!=null) event.filestreaming(sourcefilename, op.getFilehandle(), ulaw); + } + + }, null, 0); + + if (dsphandle!=0) { + // sampe sini , DSP sudah created + long timestamp = System.currentTimeMillis(); + raise_log("Mixer Playback for "+sourcefilename+" started"); + // tinggal loop aja sampe selesai + do { + try { + Thread.sleep(100); + //Thread.yield(); + } catch (InterruptedException e) { + raise_log("Interrupt Exception="+e.getMessage()); + } + } while(BASS.BASS_ChannelIsActive(op.getFilehandle())== Bass.BASS_ACTIVE_PLAYING); + + // finish playback !! + long delta = System.currentTimeMillis() - timestamp; + raise_log("Mixer Playback for "+sourcefilename+" finished, duration="+delta+" ms"); + } else raise_log(BASS.GetBassError("BASS_ChannelSetDSP")); + + // sampe sini selesai semua + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + }); + + return op.getFilehandle(); + } else { + raise_log(BASS.GetBassError("BASS_ChannelStart")); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + } else { + Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + 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, (handle, buffer, length, user) -> { + + byte[] data = buffer.getByteArray(0,length); + byte[] ulaw = ConvertPCMToULaw(data); + // inputname ada di Pointer user + if (event!=null) event.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(JSIPAudio.this::raise_log); + Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log); + return 0; + } else return oi.getRecordhandle(); // aman + } else { + Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + + + } + + } + + public class PCM extends basicfunctions{ + public PCM(int bits, int channel, AudioEvent1 event) { + super(48000, Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_FREQ); + + this.channel = channel; + this.bits = bits; + this.MIME = bits==24 ? "audio/L24" : "audio/L16"; + this.event = event; + + } + + public final int[] samplingrate = {16000, 32000, 44100, 48000}; + public final int channel; + public final int bits; + public final String MIME; + public final AudioEvent1 event; + + private byte[] Convert_PCM16_to_PCM24(byte[] data) { + 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 = System.currentTimeMillis(); + int totalread = 0; + int readcount; + do { + Pointer ptr = new Memory(1024); + readcount = 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(JSIPAudio.this::raise_log); + + long delta = System.currentTimeMillis() - timestamp; + raise_log(MessageFormat.format("FileConvert from {0} to {1} finished, Bytes Read={2}, Conversion took {3} ms", sourcefilename, targetfilename, totalread, delta)); + if (event!=null) event.fileconvertfinish(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(JSIPAudio.this::raise_log); + } + } else { + Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + 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 = 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); + if (event!=null) event.filestreaming(sourcefilename, op.getFilehandle(),cc); + }, null, 0); + + if (dsphandle==0) { + raise_log(BASS.GetBassError("BASS_ChannelSetDSP")); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + // sampe sini berhasil + return op.getFilehandle(); + } + else { + raise_log(BASS.GetBassError("BASS_ChannelStart")); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + } else { + Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + 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, (handle, buffer, length, user) -> { + byte[] data = buffer.getByteArray(0, length); + String recdevname = user==null ? "" : user.getString(0); + if (bits==24) { + if (event!=null) event.inputstreaming(recdevname, handle, Convert_PCM16_to_PCM24(data)); + } else { + if (event!=null) event.inputstreaming(recdevname, handle, data); + } + return true; + }); + + if (oi.isSuccess()) { + inited_recorder.add(oi.getRecorderdev()); + return oi.getRecordhandle(); + } else { + Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + } + + } + + @SuppressWarnings("BusyWait") + public class AAC extends basicfunctions{ + public AAC(int bits, int channel, AudioEvent1 event) { + super(48000, Bass.BASS_DEVICE_MONO | Bass.BASS_DEVICE_FREQ); + + this.bits = bits; + this.channel = channel; + + this.event = event; + } + 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 AudioEvent1 event; + + + @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 = BASSENC_AAC.BASS_Encode_AAC_StartFile(op.getMixerhandle(), null, 0, targetfilename); + if (encodehandle==0) { + // gak bisa create encoder + raise_log(BASS.GetBassError("BASS_Encode_AAC_StartFile")); + // free handle + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + return false; + } + + // sampe sini encoder jalan + exec.submit(()->{ + do { + try { + Thread.sleep(100); + //Thread.yield(); + } catch (InterruptedException e) { + raise_log("InterruptException"); + } + } while (BASS.BASS_ChannelIsActive(op.getMixerhandle())== Bass.BASS_ACTIVE_PLAYING); + + // free encoder + BASSENC.BASS_Encode_Stop(encodehandle); + // free handle + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + + if (event!=null) event.fileconvertfinish(sourcefilename, targetfilename); + + }); + return true; + } else { + Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + 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 = BASSENC_AAC.BASS_Encode_AAC_Start(op.getMixerhandle(), null, 0, + (encode_handle, source_handle, buffer, length, offset, user)->{ + if (event!=null) event.filestreaming(sourcefilename, op.getFilehandle(), buffer.getByteArray(0,length)); + } + , null); + + if (encodehandle==0) { + raise_log(BASS.GetBassError("BASS_Encode_AAC_Start")); + + // free handle + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + exec.submit(()->{ + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + raise_log("InterruptException"); + } + } while(BASS.BASS_ChannelIsActive(op.getMixerhandle())== Bass.BASS_ACTIVE_PLAYING); + + BASSENC.BASS_Encode_Stop(encodehandle); + + // free handle + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + + }); + + return op.getFilehandle(); + } else { + raise_log(BASS.GetBassError("BASS_ChannelStart")); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + return 0; + } + + + } else { + Stream.of(op.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(op.CloseHandles()).forEach(JSIPAudio.this::raise_log); + 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 = BASSENC_AAC.BASS_Encode_AAC_Start(oi.getRecordhandle(), null, 0, (encode_handle, source_handle, buffer, length, offset, user)->{ + byte[] data = buffer.getByteArray(0, length); + if (event!=null) event.inputstreaming(oi.getDeviceName(), source_handle, data); + }, null); + + if (encodehandle!=0) { + return oi.getRecordhandle(); + } else { + raise_log(BASS.GetBassError("BASS_Encode_AAC_Start")); + Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log); + } + + } else { + Stream.of(oi.getErrorlist()).forEach(JSIPAudio.this::raise_log); + Stream.of(oi.StopRecord()).forEach(JSIPAudio.this::raise_log); + + } + + return 0; + + + } + + } +} diff --git a/src/Audio/basicfunctions.java b/src/Audio/basicfunctions.java new file mode 100644 index 0000000..4b89495 --- /dev/null +++ b/src/Audio/basicfunctions.java @@ -0,0 +1,311 @@ +package Audio; + +import java.util.ArrayList; +import java.util.List; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import Audio.Bass.BASS_DEVICEINFO; +import Audio.Bass.RECORDPROC; +import lombok.Getter; + +@SuppressWarnings("unused") +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; + private final Bass BASS = Bass.Instance; + private final BassMix BASSMIX = BassMix.Instance; + + 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 Recording Handle + * @return true if Input can be stopped + */ + public boolean CloseInputForStreaming(int rechandle) { + return BASS.BASS_ChannelFree(rechandle); + } + + /** + * 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 (!BASS.BASS_Init(device, devinit_freq, devinit_flag)) { + // ada gagal init, cek dulu + int err = BASS.BASS_ErrorGetCode(); + // gak boleh gagal + //raise_log(BASS.GetBassError("BASS_Init", err)); + return err == Bass.BASS_ERROR_ALREADY; + } + // 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 (!BASS.BASS_RecordInit(device)) { + // ada gagal init, cek dulu + int err = BASS.BASS_ErrorGetCode(); + // gak boleh gagal + //raise_log(BASS.GetBassError("BASS_RecordInit", err)); + return err == Bass.BASS_ERROR_ALREADY; + } + return true; + } + + /** + * OpenInput helper + * @author rdkartono + * + */ + protected class OpenInput{ + @Getter private final int recorderdev; + @Getter private final int samplingrate; + @Getter private boolean success = false; + @Getter private final String[] errorlist; + @Getter private int recordhandle=0; + private final RECORDPROC recproc; + private @Getter BASS_DEVICEINFO devinfo; + + /** + * 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 (!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 = BASS.BASS_RecordStart(samplingrate, 1, Bass.BASS_RECORD_PAUSE, recproc, user); + if (recordhandle!=0) { + success = true; + } else error.add(BASS.GetBassError("BASS_RecordStart")); + } else error.add(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 (BASS.BASS_ChannelStart(recordhandle)) { + // nothing... + } else error.add(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 (BASS.BASS_ChannelFree(recordhandle)) { + // nothing... + } else error.add(BASS.GetBassError("BASS_ChannelStop")); + recordhandle = 0; + } else error.add("RecordHandle is zero"); + + return error.toArray(new String[0]); + } + } + + /** + * OpenFile helper + * @author rdkartono + * + */ + @Getter + protected class OpenFile{ + private final int playbackdev; + private final int samplingrate; + private final String filename; + private final boolean decodingonly; + private boolean success = false; + private int filehandle; + private int mixerhandle; + private int mixerflag; + private final String[] errorlist; + private long ByteSize; + private double Duration; + private BASS_DEVICEINFO devinfo; + + /** + * 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 Decoded 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 = BASS.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE); + Duration = BASS.BASS_ChannelBytes2Seconds(filehandle, ByteSize); + + } + devinfo = new BASS_DEVICEINFO(); + if (!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 = BASS.BASS_StreamCreateFile(false,filename, 0, 0, Bass.BASS_STREAM_DECODE); + if (filehandle!=0) { + mixerflag = decodingonly ? Bass.BASS_STREAM_DECODE : 0; + + mixerhandle = BASSMIX.BASS_Mixer_StreamCreate(samplingrate, 1, mixerflag); + if (mixerhandle!=0) { + if (BASSMIX.BASS_Mixer_StreamAddChannel(mixerhandle, filehandle, Bass.BASS_STREAM_AUTOFREE)) { + success = true; + } else error.add(BASS.GetBassError("BASS_Mixer_StreamAddChannel")); + } else error.add(BASS.GetBassError("BASS_Mixer_StreamCreate")); + } else error.add(BASS.GetBassError("BASS_StreamCreateFile")); + } else error.add(BASS.GetBassError("Bass_Init")); + + return error.toArray(new String[0]); + } + + /** + * Start Playback + * @return true if it 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 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 (!BASSMIX.BASS_Mixer_ChannelRemove(filehandle)) { + error.add(BASS.GetBassError("BASS_Mixer_ChannelRemove")); + } + } + if (!BASS.BASS_ChannelFree(filehandle)) { + error.add(BASS.GetBassError("BASS_ChannelFree")); + } + + filehandle = 0; + } + if (mixerhandle!=0) { + if (!BASS.BASS_ChannelFree(mixerhandle)) { + error.add(BASS.GetBassError("BASS_ChannelFree")); + } + mixerhandle = 0; + } + + return error.toArray(new String[0]); + } + } +} diff --git a/src/Main.java b/src/Main.java new file mode 100644 index 0000000..f5620f8 --- /dev/null +++ b/src/Main.java @@ -0,0 +1,6 @@ +public class Main { + public static void main(String[] args) { + + System.out.println("Hello, World!"); + } +} \ No newline at end of file diff --git a/src/peers/Config.java b/src/peers/Config.java new file mode 100644 index 0000000..165e060 --- /dev/null +++ b/src/peers/Config.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 2010 Yohann Martineau +*/ + +package peers; + +import java.net.InetAddress; + +import peers.media.MediaMode; +import peers.sip.syntaxencoding.SipURI; + +public interface Config { + + public void save(); + public InetAddress getLocalInetAddress(); + public InetAddress getPublicInetAddress(); + public String getUserPart(); + public String getDomain(); + public String getPassword(); + public SipURI getOutboundProxy(); + public int getSipPort(); + public MediaMode getMediaMode(); + public boolean isMediaDebug(); + public String getMediaFile(); + public int getRtpPort(); + public void setLocalInetAddress(InetAddress inetAddress); + public void setPublicInetAddress(InetAddress inetAddress); + public void setUserPart(String userPart); + public void setDomain(String domain); + public void setPassword(String password); + public void setOutboundProxy(SipURI outboundProxy); + public void setSipPort(int sipPort); + public void setMediaMode(MediaMode mediaMode); + public void setMediaDebug(boolean mediaDebug); + public void setMediaFile(String mediaFile); + public void setRtpPort(int rtpPort); + +} diff --git a/src/peers/JavaConfig.java b/src/peers/JavaConfig.java new file mode 100644 index 0000000..c83939f --- /dev/null +++ b/src/peers/JavaConfig.java @@ -0,0 +1,156 @@ +/* + 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 peers; + +import java.net.InetAddress; + +import peers.media.MediaMode; +import 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/peers/XmlConfig.java b/src/peers/XmlConfig.java new file mode 100644 index 0000000..059b892 --- /dev/null +++ b/src/peers/XmlConfig.java @@ -0,0 +1,355 @@ +/* + 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 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 org.pmw.tinylog.Logger; +import peers.media.MediaMode; +import peers.sip.RFC3261; +import peers.sip.syntaxencoding.SipURI; +import 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 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) { + file = new File(fileName); + if (!file.exists()) { + Logger.debug("config file " + fileName + " not found"); + return; + } + DocumentBuilderFactory documentBuilderFactory = + DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + Logger.error("parser configuration exception", e); + return; + } + try { + document = documentBuilder.parse(file); + } catch (SAXException e) { + Logger.error("cannot parse " + fileName,e ); + return; + } catch (IOException e) { + 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) { + Logger.error("unknown host: " + address, e); + } + userPartNode = getFirstChild(documentElement, "userPart"); + if (isNullOrEmpty(userPartNode)) { + Logger.error("userpart not found in configuration file"); + } else { + userPart = userPartNode.getTextContent(); + } + domainNode = getFirstChild(documentElement, "domain"); + if (isNullOrEmpty(domainNode)) { + 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) { + 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())) { + 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) { + 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) { + Logger.error("cannot create transformer", e); + return; + } + FileWriter fileWriter; + try { + fileWriter = new FileWriter(file); + } catch (IOException e) { + 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) { + Logger.error("cannot save config file", e); + return; + } + 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/peers/javaxsound/JavaxSoundManager.java b/src/peers/javaxsound/JavaxSoundManager.java new file mode 100644 index 0000000..09c406a --- /dev/null +++ b/src/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 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 org.pmw.tinylog.Logger; +import peers.media.AbstractSoundManager; +import 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 String peersHome; + + public JavaxSoundManager(boolean mediaDebug, String peersHome) { + this.mediaDebug = mediaDebug; + 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() { + 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) { + 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) { + Logger.error("target line unavailable", e); + return null; + } catch (SecurityException e) { + Logger.error("security exception", e); + return null; + } catch (Throwable t) { + Logger.error("throwable " + t.getMessage()); + return null; + } + targetDataLine.start(); + try { + sourceDataLine = (SourceDataLine) AudioSystem.getLine(sourceInfo); + sourceDataLine.open(audioFormat); + + } catch (LineUnavailableException e) { + Logger.error("source line unavailable", e); + return null; + } + sourceDataLine.start(); + return null; + } + }); + + } + + @Override + public synchronized void close() { + Logger.debug("closeLines"); + if (microphoneOutput != null) { + try { + microphoneOutput.close(); + } catch (IOException e) { + Logger.error("cannot close file", e); + } + microphoneOutput = null; + } + if (speakerInput != null) { + try { + speakerInput.close(); + } catch (IOException e) { + 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) { + 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) { + Logger.error("cannot write to file", e); + return -1; + } + } + return numberOfBytesWritten; + } + +} diff --git a/src/peers/media/AbstractSoundManager.java b/src/peers/media/AbstractSoundManager.java new file mode 100644 index 0000000..236a476 --- /dev/null +++ b/src/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 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/peers/media/Capture.java b/src/peers/media/Capture.java new file mode 100644 index 0000000..1fa23a0 --- /dev/null +++ b/src/peers/media/Capture.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 2008, 2009, 2010, 2011 Yohann Martineau +*/ + +package peers.media; + +import org.pmw.tinylog.Logger; + +import java.io.IOException; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + + + + +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 CountDownLatch latch; + + public Capture(PipedOutputStream rawData, SoundSource soundSource, + CountDownLatch latch) { + this.rawData = rawData; + this.soundSource = soundSource; + 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) { + Logger.error("input/output error", e); + return; + } + } + latch.countDown(); + if (latch.getCount() != 0) { + try { + latch.await(); + } catch (InterruptedException e) { + Logger.error("interrupt exception", e); + } + } + } + + public synchronized void setStopped(boolean isStopped) { + this.isStopped = isStopped; + } + +} diff --git a/src/peers/media/CaptureRtpSender.java b/src/peers/media/CaptureRtpSender.java new file mode 100644 index 0000000..4f8d794 --- /dev/null +++ b/src/peers/media/CaptureRtpSender.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 2007-2013 Yohann Martineau +*/ + +package peers.media; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + + +import org.pmw.tinylog.Logger; +import peers.rtp.RFC3551; +import peers.rtp.RtpSession; +import 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, 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) { + Logger.error("input/output error", e); + return; + } + + PipedOutputStream encodedDataOutput = new PipedOutputStream(); + PipedInputStream encodedDataInput; + try { + encodedDataInput = new PipedInputStream(encodedDataOutput, + PIPE_SIZE); + } catch (IOException e) { + Logger.error("input/output error"); + rawDataInput.close(); + return; + } + capture = new Capture(rawDataOutput, soundSource, latch); + switch (codec.getPayloadType()) { + case RFC3551.PAYLOAD_TYPE_PCMU: + encoder = new PcmuEncoder(rawDataInput, encodedDataOutput, + mediaDebug, peersHome, latch); + break; + case RFC3551.PAYLOAD_TYPE_PCMA: + encoder = new PcmaEncoder(rawDataInput, encodedDataOutput, + mediaDebug, peersHome, latch); + break; + default: + encodedDataInput.close(); + rawDataInput.close(); + throw new RuntimeException("unknown payload type"); + } + rtpSender = new RtpSender(encodedDataInput, rtpSession, mediaDebug, + codec, 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/peers/media/Decoder.java b/src/peers/media/Decoder.java new file mode 100644 index 0000000..c9e2793 --- /dev/null +++ b/src/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 peers.media; + +public abstract class Decoder { + + public abstract byte[] process(byte[] media); + +} diff --git a/src/peers/media/DtmfFactory.java b/src/peers/media/DtmfFactory.java new file mode 100644 index 0000000..d209306 --- /dev/null +++ b/src/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 peers.media; + +import java.util.ArrayList; +import java.util.List; + +import peers.rtp.RFC4733; +import 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/peers/media/Echo.java b/src/peers/media/Echo.java new file mode 100644 index 0000000..370b9f5 --- /dev/null +++ b/src/peers/media/Echo.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 2009, 2010, 2012 Yohann Martineau +*/ + +package peers.media; + +import org.pmw.tinylog.Logger; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + + + +public class Echo implements Runnable { + + public static final int BUFFER_SIZE = 2048; + + private DatagramSocket datagramSocket; + private InetAddress remoteAddress; + private int remotePort; + private boolean isRunning; + + + public Echo(DatagramSocket datagramSocket, + String remoteAddress, int remotePort) + throws UnknownHostException { + this.datagramSocket = datagramSocket; + this.remoteAddress = InetAddress.getByName(remoteAddress); + this.remotePort = remotePort; + 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) { + Logger.debug("echo socket timeout"); + continue; + } + datagramPacket = new DatagramPacket(buf, + datagramPacket.getLength(), remoteAddress, remotePort); + datagramSocket.send(datagramPacket); + } + } catch (IOException e) { + Logger.error("input/output error", e); + } finally { + datagramSocket.close(); + } + + } + + public synchronized void stop() { + isRunning = false; + } +} diff --git a/src/peers/media/Encoder.java b/src/peers/media/Encoder.java new file mode 100644 index 0000000..24e594d --- /dev/null +++ b/src/peers/media/Encoder.java @@ -0,0 +1,149 @@ +/* + 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 peers.media; + +import org.pmw.tinylog.Logger; + +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; + + + + +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 String peersHome; + private CountDownLatch latch; + + public Encoder(PipedInputStream rawData, PipedOutputStream encodedData, + boolean mediaDebug, String peersHome, + CountDownLatch latch) { + this.rawData = rawData; + this.encodedData = encodedData; + this.mediaDebug = mediaDebug; + 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) { + 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) { + Logger.error("interrupt exception", e); + } + } + if (isStopped) { + break; + } + buffer = new byte[ready]; + rawData.read(buffer); + if (mediaDebug) { + try { + encoderInput.write(buffer); + } catch (IOException e) { + Logger.error("cannot write to file", e); + } + } + } catch (IOException e) { + Logger.error("input/output error", e); + return; + } + + byte[] ulawData = process(buffer); + if (mediaDebug) { + try { + encoderOutput.write(ulawData); + } catch (IOException e) { + Logger.error("cannot write to file", e); + break; + } + } + try { + encodedData.write(ulawData); + encodedData.flush(); + } catch (IOException e) { + Logger.error("input/output error", e); + return; + } + } + if (mediaDebug) { + try { + encoderOutput.close(); + encoderInput.close(); + } catch (IOException e) { + Logger.error("cannot close file", e); + return; + } + } + latch.countDown(); + if (latch.getCount() != 0) { + try { + latch.await(); + } catch (InterruptedException e) { + Logger.error("interrupt exception", e); + } + } + } + + public synchronized void setStopped(boolean isStopped) { + this.isStopped = isStopped; + } + + public abstract byte[] process(byte[] media); + +} diff --git a/src/peers/media/FileReader.java b/src/peers/media/FileReader.java new file mode 100644 index 0000000..0aacbb9 --- /dev/null +++ b/src/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 peers.media; + +import org.pmw.tinylog.Logger; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + + + +// 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; + + public FileReader(String fileName) { + try { + fileInputStream = new FileInputStream(fileName); + } catch (FileNotFoundException e) { + Logger.error("file not found: {}, exceptin : {}" , fileName, e); + } + } + + public synchronized void close() { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + 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) { + Logger.error("io exception {}", e); + } catch (InterruptedException e) { + Logger.error("file reader interrupted {}", e); + } + return null; + } + +} diff --git a/src/peers/media/IncomingRtpReader.java b/src/peers/media/IncomingRtpReader.java new file mode 100644 index 0000000..5e967a0 --- /dev/null +++ b/src/peers/media/IncomingRtpReader.java @@ -0,0 +1,69 @@ +/* + 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 peers.media; + +import java.io.IOException; + + +import org.pmw.tinylog.Logger; +import peers.rtp.RFC3551; +import peers.rtp.RtpListener; +import peers.rtp.RtpPacket; +import peers.rtp.RtpSession; +import 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) + throws IOException { + 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/peers/media/MediaManager.java b/src/peers/media/MediaManager.java new file mode 100644 index 0000000..7fc2dc5 --- /dev/null +++ b/src/peers/media/MediaManager.java @@ -0,0 +1,335 @@ +/* + 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 peers.media; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +import Audio.BassFileReader; + +import org.pmw.tinylog.Logger; +import peers.rtp.RtpPacket; +import peers.rtp.RtpSession; +import peers.sdp.Codec; +import 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 DatagramSocket datagramSocket; + //private FileReader fileReader; + private BassFileReader fileReader; + + public MediaManager(UserAgent userAgent) { + this.userAgent = userAgent; + 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) { + Logger.error("unknown host: " + localAddress, e); + return; + } + + rtpSession = new RtpSession(inetAddress, datagramSocket, + userAgent.isMediaDebug(), userAgent.getPeersHome()); + + try { + inetAddress = InetAddress.getByName(remoteAddress); + rtpSession.setRemoteAddress(inetAddress); + } catch (UnknownHostException e) { + Logger.error("unknown host: " + remoteAddress, e); + } + rtpSession.setRemotePort(remotePort); + + + try { + captureRtpSender = new CaptureRtpSender(rtpSession, + soundSource, userAgent.isMediaDebug(), codec, userAgent.getPeersHome()); + } catch (IOException e) { + Logger.error("input/output error", e); + return; + } + + try { + captureRtpSender.start(); + } catch (IOException e) { + 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); + } catch (IOException e) { + Logger.error("input/output error", e); + return; + } + + incomingRtpReader.start(); + break; + + case echo: + Echo echo; + try { + echo = new Echo(datagramSocket, remoteAddress, remotePort); + } catch (UnknownHostException e) { + 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); + fileReader = new BassFileReader(fileName,null); + + startRtpSessionOnSuccessResponse(localAddress, remoteAddress, + remotePort, codec, fileReader); + try { + incomingRtpReader = new IncomingRtpReader( + captureRtpSender.getRtpSession(), null, codec); + } catch (IOException e) { + 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(), userAgent.getPeersHome()); + + try { + InetAddress inetAddress = InetAddress.getByName(destAddress); + rtpSession.setRemoteAddress(inetAddress); + } catch (UnknownHostException e) { + Logger.error("unknown host: " + destAddress, e); + } + rtpSession.setRemotePort(destPort); + + try { + captureRtpSender = new CaptureRtpSender(rtpSession, + soundSource, userAgent.isMediaDebug(), codec, userAgent.getPeersHome()); + } catch (IOException e) { + Logger.error("input/output error", e); + return; + } + try { + captureRtpSender.start(); + } catch (IOException e) { + 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); + } catch (IOException e) { + Logger.error("input/output error", e); + return; + } + + incomingRtpReader.start(); + + break; + case echo: + Echo echo; + try { + echo = new Echo(datagramSocket, destAddress, destPort); + } catch (UnknownHostException e) { + 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); + fileReader = new BassFileReader(fileName, null); + + startRtpSession(destAddress, destPort, codec, fileReader); + try { + incomingRtpReader = new IncomingRtpReader(rtpSession, + null, codec); + } catch (IOException e) { + 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) { + 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) { + 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) { + 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/peers/media/MediaMode.java b/src/peers/media/MediaMode.java new file mode 100644 index 0000000..d92f9b5 --- /dev/null +++ b/src/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 peers.media; + +public enum MediaMode { + none, captureAndPlayback, echo, file +} diff --git a/src/peers/media/PcmaDecoder.java b/src/peers/media/PcmaDecoder.java new file mode 100644 index 0000000..9afdeb7 --- /dev/null +++ b/src/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 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/peers/media/PcmaEncoder.java b/src/peers/media/PcmaEncoder.java new file mode 100644 index 0000000..4511646 --- /dev/null +++ b/src/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 peers.media; + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + + + +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, String peersHome, + CountDownLatch latch) { + super(rawData, encodedData, mediaDebug, 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/peers/media/PcmuDecoder.java b/src/peers/media/PcmuDecoder.java new file mode 100644 index 0000000..8d98465 --- /dev/null +++ b/src/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 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/peers/media/PcmuEncoder.java b/src/peers/media/PcmuEncoder.java new file mode 100644 index 0000000..e9f7fd9 --- /dev/null +++ b/src/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 peers.media; + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CountDownLatch; + + + +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, String peersHome, + CountDownLatch latch) { + super(rawData, encodedData, mediaDebug, 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/peers/media/RtpSender.java b/src/peers/media/RtpSender.java new file mode 100644 index 0000000..0abd55b --- /dev/null +++ b/src/peers/media/RtpSender.java @@ -0,0 +1,201 @@ +/* + 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 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 org.pmw.tinylog.Logger; +import peers.rtp.RtpPacket; +import peers.rtp.RtpSession; +import 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 String peersHome; + private CountDownLatch latch; + + public RtpSender(PipedInputStream encodedData, RtpSession rtpSession, + boolean mediaDebug, Codec codec, 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) { + 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) { + 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) { + 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) { + 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) { + Logger.error("cannot close file", e); + return; + } + } + latch.countDown(); + if (latch.getCount() != 0) { + try { + latch.await(); + } catch (InterruptedException e) { + 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/peers/media/SoundSource.java b/src/peers/media/SoundSource.java new file mode 100644 index 0000000..c8deede --- /dev/null +++ b/src/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 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/peers/nat/Client.java b/src/peers/nat/Client.java new file mode 100644 index 0000000..427669a --- /dev/null +++ b/src/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 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/peers/nat/PeerManager.java b/src/peers/nat/PeerManager.java new file mode 100644 index 0000000..e188bf2 --- /dev/null +++ b/src/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 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/peers/nat/Server.java b/src/peers/nat/Server.java new file mode 100644 index 0000000..2fe7c6f --- /dev/null +++ b/src/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 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/peers/nat/UDPReceiver.java b/src/peers/nat/UDPReceiver.java new file mode 100644 index 0000000..efae8e3 --- /dev/null +++ b/src/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 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/peers/nat/api/DataReceiver.java b/src/peers/nat/api/DataReceiver.java new file mode 100644 index 0000000..7206c68 --- /dev/null +++ b/src/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 peers.nat.api; + + +public interface DataReceiver { + + public void dataReceived(byte[] data, String peerId); +} diff --git a/src/peers/nat/api/PeersClient.java b/src/peers/nat/api/PeersClient.java new file mode 100644 index 0000000..6f7a9eb --- /dev/null +++ b/src/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 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/peers/nat/api/TCPTransport.java b/src/peers/nat/api/TCPTransport.java new file mode 100644 index 0000000..37bf299 --- /dev/null +++ b/src/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 peers.nat.api; + +public interface TCPTransport extends Transport { + +} diff --git a/src/peers/nat/api/Transport.java b/src/peers/nat/api/Transport.java new file mode 100644 index 0000000..7769c63 --- /dev/null +++ b/src/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 peers.nat.api; + +public interface Transport { + + public void sendData(byte[] data); +} diff --git a/src/peers/nat/api/UDPTransport.java b/src/peers/nat/api/UDPTransport.java new file mode 100644 index 0000000..8aee6f1 --- /dev/null +++ b/src/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 peers.nat.api; + +public interface UDPTransport extends Transport { + +} diff --git a/src/peers/rtp/RFC3551.java b/src/peers/rtp/RFC3551.java new file mode 100644 index 0000000..a9bbef4 --- /dev/null +++ b/src/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 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/peers/rtp/RFC4733.java b/src/peers/rtp/RFC4733.java new file mode 100644 index 0000000..e8af36b --- /dev/null +++ b/src/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 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/peers/rtp/RtpListener.java b/src/peers/rtp/RtpListener.java new file mode 100644 index 0000000..25f710d --- /dev/null +++ b/src/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 peers.rtp; + +public interface RtpListener { + + void receivedRtpPacket(RtpPacket rtpPacket); + +} diff --git a/src/peers/rtp/RtpPacket.java b/src/peers/rtp/RtpPacket.java new file mode 100644 index 0000000..d862e9d --- /dev/null +++ b/src/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 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/peers/rtp/RtpParser.java b/src/peers/rtp/RtpParser.java new file mode 100644 index 0000000..e5b4c60 --- /dev/null +++ b/src/peers/rtp/RtpParser.java @@ -0,0 +1,119 @@ +/* + 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 peers.rtp; + + +import org.pmw.tinylog.Logger; + +// RFC 3550 +public class RtpParser { + + + + + public RtpPacket decode(byte[] packet) { + if (packet.length < 12) { + 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/peers/rtp/RtpSession.java b/src/peers/rtp/RtpSession.java new file mode 100644 index 0000000..b928b93 --- /dev/null +++ b/src/peers/rtp/RtpSession.java @@ -0,0 +1,264 @@ +/* + 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 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 org.pmw.tinylog.Logger; +import 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 String peersHome; + + public RtpSession(InetAddress localAddress, DatagramSocket datagramSocket, + boolean mediaDebug, String peersHome) { + this.mediaDebug = mediaDebug; + this.peersHome = peersHome; + this.datagramSocket = datagramSocket; + rtpListeners = new ArrayList(); + rtpParser = new RtpParser(); + 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) { + 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) { + Logger.error("cannot send rtp packet", e); + } catch (SecurityException e) { + Logger.error("security exception", e); + } + return null; + } + } + ); + + if (mediaDebug) { + try { + rtpSessionOutput.write(buf); + } catch (IOException e) { + 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) { + 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) { + 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) { + 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) { + 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/peers/sdp/Codec.java b/src/peers/sdp/Codec.java new file mode 100644 index 0000000..5462403 --- /dev/null +++ b/src/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 peers.sdp; + +import 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/peers/sdp/MediaDescription.java b/src/peers/sdp/MediaDescription.java new file mode 100644 index 0000000..cf950cb --- /dev/null +++ b/src/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 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/peers/sdp/MediaDestination.java b/src/peers/sdp/MediaDestination.java new file mode 100644 index 0000000..e272d45 --- /dev/null +++ b/src/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 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/peers/sdp/NoCodecException.java b/src/peers/sdp/NoCodecException.java new file mode 100644 index 0000000..d7b8f4d --- /dev/null +++ b/src/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 peers.sdp; + +public class NoCodecException extends Exception { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/peers/sdp/RFC4566.java b/src/peers/sdp/RFC4566.java new file mode 100644 index 0000000..519cc6e --- /dev/null +++ b/src/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 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/peers/sdp/SDPManager.java b/src/peers/sdp/SDPManager.java new file mode 100644 index 0000000..c8f466f --- /dev/null +++ b/src/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 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.rtp.RFC3551; +import peers.rtp.RFC4733; +import peers.sip.core.useragent.UserAgent; + +public class SDPManager { + + private SdpParser sdpParser; + private UserAgent userAgent; + private List supportedCodecs; + private Random random; + + + + public SDPManager(UserAgent userAgent) { + this.userAgent = userAgent; + 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) { + 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/peers/sdp/SDPMessage.java b/src/peers/sdp/SDPMessage.java new file mode 100644 index 0000000..5b07ca4 --- /dev/null +++ b/src/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 peers.sdp; + +public class SDPMessage { + + +} diff --git a/src/peers/sdp/SdpLine.java b/src/peers/sdp/SdpLine.java new file mode 100644 index 0000000..6e32d4e --- /dev/null +++ b/src/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 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/peers/sdp/SdpParser.java b/src/peers/sdp/SdpParser.java new file mode 100644 index 0000000..42ae60d --- /dev/null +++ b/src/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 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 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/peers/sdp/SessionDescription.java b/src/peers/sdp/SessionDescription.java new file mode 100644 index 0000000..bb167be --- /dev/null +++ b/src/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 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/peers/sip/AbstractState.java b/src/peers/sip/AbstractState.java new file mode 100644 index 0000000..7d51c6a --- /dev/null +++ b/src/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 peers.sip; + + +import org.pmw.tinylog.Logger; + +public abstract class AbstractState { + + protected String id; + + + public AbstractState(String id) { + this.id = id; + } + + 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()); + Logger.debug(buf.toString()); + } + +} diff --git a/src/peers/sip/JavaUtils.java b/src/peers/sip/JavaUtils.java new file mode 100644 index 0000000..ab462a9 --- /dev/null +++ b/src/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 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/peers/sip/RFC2617.java b/src/peers/sip/RFC2617.java new file mode 100644 index 0000000..40148ec --- /dev/null +++ b/src/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 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/peers/sip/RFC3261.java b/src/peers/sip/RFC3261.java new file mode 100644 index 0000000..c35cfaa --- /dev/null +++ b/src/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 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/peers/sip/Utils.java b/src/peers/sip/Utils.java new file mode 100644 index 0000000..1cd046a --- /dev/null +++ b/src/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 peers.sip; + + +import java.net.InetAddress; + +import peers.sip.core.useragent.UAS; +import peers.sip.syntaxencoding.SipHeaderFieldMultiValue; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaders; +import 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/peers/sip/core/useragent/ChallengeManager.java b/src/peers/sip/core/useragent/ChallengeManager.java new file mode 100644 index 0000000..7a3e554 --- /dev/null +++ b/src/peers/sip/core/useragent/ChallengeManager.java @@ -0,0 +1,300 @@ +/* + 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 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.sip.RFC2617; +import peers.sip.RFC3261; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import 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; + + + // 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) { + this.config = config; + this.initialRequestManager = initialRequestManager; + this.midDialogRequestManager = midDialogRequestManager; + this.dialogManager = dialogManager; + 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) { + 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) { + 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/peers/sip/core/useragent/InitialRequestManager.java b/src/peers/sip/core/useragent/InitialRequestManager.java new file mode 100644 index 0000000..078990d --- /dev/null +++ b/src/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 peers.sip.core.useragent; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.core.useragent.handlers.ByeHandler; +import peers.sip.core.useragent.handlers.CancelHandler; +import peers.sip.core.useragent.handlers.InviteHandler; +import peers.sip.core.useragent.handlers.OptionsHandler; +import peers.sip.core.useragent.handlers.RegisterHandler; +import peers.sip.syntaxencoding.NameAddress; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.ServerTransaction; +import peers.sip.transaction.ServerTransactionUser; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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 + ) { + super(userAgent, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager); + 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) { + 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) { + 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/peers/sip/core/useragent/MessageInterceptor.java b/src/peers/sip/core/useragent/MessageInterceptor.java new file mode 100644 index 0000000..31f0041 --- /dev/null +++ b/src/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 peers.sip.core.useragent; + +import peers.sip.transport.SipMessage; + +public interface MessageInterceptor { + + public void postProcess(SipMessage sipMessage); +} diff --git a/src/peers/sip/core/useragent/MidDialogRequestManager.java b/src/peers/sip/core/useragent/MidDialogRequestManager.java new file mode 100644 index 0000000..e7b6509 --- /dev/null +++ b/src/peers/sip/core/useragent/MidDialogRequestManager.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 peers.sip.core.useragent; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Hashtable; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.core.useragent.handlers.ByeHandler; +import peers.sip.core.useragent.handlers.CancelHandler; +import peers.sip.core.useragent.handlers.InviteHandler; +import peers.sip.core.useragent.handlers.OptionsHandler; +import peers.sip.core.useragent.handlers.RegisterHandler; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.ClientTransactionUser; +import peers.sip.transaction.ServerTransaction; +import peers.sip.transaction.ServerTransactionUser; +import peers.sip.transaction.Transaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(userAgent, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager); + } + + + //////////////////////////////////////////////// + // 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); + + //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) { + 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/peers/sip/core/useragent/RequestManager.java b/src/peers/sip/core/useragent/RequestManager.java new file mode 100644 index 0000000..f2d3fcc --- /dev/null +++ b/src/peers/sip/core/useragent/RequestManager.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 peers.sip.core.useragent; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.core.useragent.handlers.ByeHandler; +import peers.sip.core.useragent.handlers.CancelHandler; +import peers.sip.core.useragent.handlers.InviteHandler; +import peers.sip.core.useragent.handlers.OptionsHandler; +import peers.sip.core.useragent.handlers.RegisterHandler; +import peers.sip.syntaxencoding.NameAddress; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import peers.sip.transport.TransportManager; + + +public abstract class RequestManager { + + public static SipURI getDestinationUri(SipRequest sipRequest + ) { + 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) { + 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; + + public RequestManager(UserAgent userAgent, + InviteHandler inviteHandler, + CancelHandler cancelHandler, + ByeHandler byeHandler, + OptionsHandler optionsHandler, + RegisterHandler registerHandler, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager) { + this.userAgent = userAgent; + this.inviteHandler = inviteHandler; + this.cancelHandler = cancelHandler; + this.byeHandler = byeHandler; + this.optionsHandler = optionsHandler; + this.registerHandler = registerHandler; + this.transactionManager = transactionManager; + this.transportManager = transportManager; + } + + public InviteHandler getInviteHandler() { + return inviteHandler; + } + + public ByeHandler getByeHandler() { + return byeHandler; + } + + public RegisterHandler getRegisterHandler() { + return registerHandler; + } + +} diff --git a/src/peers/sip/core/useragent/SipEvent.java b/src/peers/sip/core/useragent/SipEvent.java new file mode 100644 index 0000000..2692cac --- /dev/null +++ b/src/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 peers.sip.core.useragent; + +import 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/peers/sip/core/useragent/SipListener.java b/src/peers/sip/core/useragent/SipListener.java new file mode 100644 index 0000000..da87b4e --- /dev/null +++ b/src/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 peers.sip.core.useragent; + +import peers.sip.transport.SipRequest; +import 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/peers/sip/core/useragent/UAC.java b/src/peers/sip/core/useragent/UAC.java new file mode 100644 index 0000000..09c5edd --- /dev/null +++ b/src/peers/sip/core/useragent/UAC.java @@ -0,0 +1,227 @@ +/* + 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 peers.sip.core.useragent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.InviteClientTransaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transactionuser.DialogState; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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; + + + /** + * should be instanciated only once, it was a singleton. + */ + public UAC(UserAgent userAgent, + InitialRequestManager initialRequestManager, + MidDialogRequestManager midDialogRequestManager, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager) { + this.userAgent = userAgent; + this.initialRequestManager = initialRequestManager; + this.midDialogRequestManager = midDialogRequestManager; + this.dialogManager = dialogManager; + this.transactionManager = transactionManager; + 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 + Logger.debug("clientTransaction null"); + midDialogRequestManager.generateMidDialogRequest( + dialog, RFC3261.METHOD_BYE, null); + guiClosedCallIds.remove(callId); + } + } else { + InviteClientTransaction inviteClientTransaction = + (InviteClientTransaction)transactionManager + .getClientTransaction(inviteWithAuth); + if (inviteClientTransaction == 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/peers/sip/core/useragent/UAS.java b/src/peers/sip/core/useragent/UAS.java new file mode 100644 index 0000000..9accbe6 --- /dev/null +++ b/src/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 peers.sip.core.useragent; + +import java.net.SocketException; +import java.util.ArrayList; + +import peers.sip.RFC3261; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import peers.sip.transport.SipServerTransportUser; +import 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/peers/sip/core/useragent/UserAgent.java b/src/peers/sip/core/useragent/UserAgent.java new file mode 100644 index 0000000..3fa19c6 --- /dev/null +++ b/src/peers/sip/core/useragent/UserAgent.java @@ -0,0 +1,382 @@ +/* + 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 peers.sip.core.useragent; + +import java.io.File; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; + +import org.pmw.tinylog.Logger; +import peers.Config; + +import peers.XmlConfig; +import peers.media.AbstractSoundManager; +import peers.media.Echo; +import peers.media.MediaManager; +import peers.media.MediaMode; +import peers.sdp.SDPManager; +import peers.sip.Utils; +import peers.sip.core.useragent.handlers.ByeHandler; +import peers.sip.core.useragent.handlers.CancelHandler; +import peers.sip.core.useragent.handlers.InviteHandler; +import peers.sip.core.useragent.handlers.OptionsHandler; +import peers.sip.core.useragent.handlers.RegisterHandler; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transaction.Transaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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 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, + AbstractSoundManager soundManager) + throws SocketException { + this(sipListener, null, peersHome, soundManager); + } + + public UserAgent(SipListener sipListener, Config config, + AbstractSoundManager soundManager) + throws SocketException { + this(sipListener, config, null, soundManager); + } + + private UserAgent(SipListener sipListener, Config config, String peersHome, + AbstractSoundManager soundManager) + throws SocketException { + this.sipListener = sipListener; + if (peersHome == null) { + peersHome = Utils.DEFAULT_PEERS_HOME; + } + this.peersHome = peersHome; + + if (config == null) { + config = new XmlConfig(this.peersHome + File.separator + + CONFIG_FILE); + } + 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("]"); + Logger.info(buf.toString()); + + //transaction user + + dialogManager = new DialogManager(); + + //transaction + + transactionManager = new TransactionManager(); + + //transport + + transportManager = new TransportManager(transactionManager, + config); + + transactionManager.setTransportManager(transportManager); + + //core + + InviteHandler inviteHandler = new InviteHandler(this, + dialogManager, + transactionManager, + transportManager); + CancelHandler cancelHandler = new CancelHandler(this, + dialogManager, + transactionManager, + transportManager); + ByeHandler byeHandler = new ByeHandler(this, + dialogManager, + transactionManager, + transportManager); + OptionsHandler optionsHandler = new OptionsHandler(this, + transactionManager, + transportManager); + RegisterHandler registerHandler = new RegisterHandler(this, + transactionManager, + transportManager); + + InitialRequestManager initialRequestManager = + new InitialRequestManager( + this, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager); + MidDialogRequestManager midDialogRequestManager = + new MidDialogRequestManager( + this, + inviteHandler, + cancelHandler, + byeHandler, + optionsHandler, + registerHandler, + dialogManager, + transactionManager, + transportManager); + + uas = new UAS(this, + initialRequestManager, + midDialogRequestManager, + dialogManager, + transactionManager, + transportManager); + + uac = new UAC(this, + initialRequestManager, + midDialogRequestManager, + dialogManager, + transactionManager, + transportManager); + + challengeManager = new ChallengeManager(config, + initialRequestManager, + midDialogRequestManager, + dialogManager); + registerHandler.setChallengeManager(challengeManager); + inviteHandler.setChallengeManager(challengeManager); + byeHandler.setChallengeManager(challengeManager); + + peers = new ArrayList(); + //dialogs = new ArrayList(); + + sdpManager = new SDPManager(this); + inviteHandler.setSdpManager(sdpManager); + optionsHandler.setSdpManager(sdpManager); + + this.soundManager = soundManager; + mediaManager = new MediaManager(this); + } + + // 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/peers/sip/core/useragent/handlers/ByeHandler.java b/src/peers/sip/core/useragent/handlers/ByeHandler.java new file mode 100644 index 0000000..5807b04 --- /dev/null +++ b/src/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 peers.sip.core.useragent.handlers; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.core.useragent.RequestManager; +import peers.sip.core.useragent.SipListener; +import peers.sip.core.useragent.UserAgent; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.ClientTransactionUser; +import peers.sip.transaction.NonInviteClientTransaction; +import peers.sip.transaction.ServerTransaction; +import peers.sip.transaction.ServerTransactionUser; +import peers.sip.transaction.Transaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import peers.sip.transport.TransportManager; + +public class ByeHandler extends DialogMethodHandler + implements ServerTransactionUser, ClientTransactionUser { + + public ByeHandler(UserAgent userAgent, DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager) { + super(userAgent, dialogManager, transactionManager, transportManager); + } + + //////////////////////////////////////////////// + // 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()); + 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()); + Logger.debug("removed dialog " + dialog.getId()); + } + + @Override + public void transactionTransportError() { + // TODO Auto-generated method stub + + } + + + + +} diff --git a/src/peers/sip/core/useragent/handlers/CancelHandler.java b/src/peers/sip/core/useragent/handlers/CancelHandler.java new file mode 100644 index 0000000..3acc7a6 --- /dev/null +++ b/src/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 peers.sip.core.useragent.handlers; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.core.useragent.MidDialogRequestManager; +import peers.sip.core.useragent.SipListener; +import peers.sip.core.useragent.UserAgent; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.InviteClientTransaction; +import peers.sip.transaction.InviteServerTransaction; +import peers.sip.transaction.ServerTransaction; +import peers.sip.transaction.ServerTransactionUser; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import peers.sip.transport.TransportManager; + +public class CancelHandler extends DialogMethodHandler + implements ServerTransactionUser { + + public CancelHandler(UserAgent userAgent, DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager) { + super(userAgent, dialogManager, transactionManager, transportManager); + } + + ////////////////////////////////////////////////////////// + // 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 { + 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/peers/sip/core/useragent/handlers/DialogMethodHandler.java b/src/peers/sip/core/useragent/handlers/DialogMethodHandler.java new file mode 100644 index 0000000..907e5e7 --- /dev/null +++ b/src/peers/sip/core/useragent/handlers/DialogMethodHandler.java @@ -0,0 +1,283 @@ +/* + 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 peers.sip.core.useragent.handlers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.TimerTask; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.core.useragent.UserAgent; +import peers.sip.syntaxencoding.NameAddress; +import peers.sip.syntaxencoding.SipHeaderFieldMultiValue; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transaction.Transaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import peers.sip.transport.TransportManager; + + +public abstract class DialogMethodHandler extends MethodHandler { + + protected DialogManager dialogManager; + + public DialogMethodHandler(UserAgent userAgent, + DialogManager dialogManager, + TransactionManager transactionManager, + TransportManager transportManager) { + super(userAgent, transactionManager, transportManager); + 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)); + 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/peers/sip/core/useragent/handlers/InviteHandler.java b/src/peers/sip/core/useragent/handlers/InviteHandler.java new file mode 100644 index 0000000..f890fc5 --- /dev/null +++ b/src/peers/sip/core/useragent/handlers/InviteHandler.java @@ -0,0 +1,693 @@ +/* + 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 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 org.pmw.tinylog.Logger; +import peers.media.MediaManager; +import peers.sdp.Codec; +import peers.sdp.MediaDestination; +import peers.sdp.NoCodecException; +import peers.sdp.SessionDescription; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.core.useragent.RequestManager; +import peers.sip.core.useragent.SipListener; +import peers.sip.core.useragent.UserAgent; +import peers.sip.syntaxencoding.NameAddress; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.ClientTransactionUser; +import peers.sip.transaction.InviteClientTransaction; +import peers.sip.transaction.InviteServerTransaction; +import peers.sip.transaction.ServerTransaction; +import peers.sip.transaction.ServerTransactionUser; +import peers.sip.transaction.Transaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transactionuser.Dialog; +import peers.sip.transactionuser.DialogManager; +import peers.sip.transport.MessageSender; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(userAgent, dialogManager, transactionManager, transportManager); + 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) { + 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) { + Logger.error("cannot create datagram socket ", e); + } + + return datagramSocket; + } + } + ); + Logger.debug("new rtp DatagramSocket " + datagramSocket.hashCode()); + try { + datagramSocket.setSoTimeout(TIMEOUT); + } catch (SocketException e) { + 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) { + 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); + + //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) { + 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) { +// 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(); + 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) { + 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); + 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) { + Logger.error("unknown host: " + sipUri.getHost(), e); + return; + } + try { + MessageSender sender = transportManager.createClientTransport( + ack, inetAddress, port, transport); + sender.sendMessage(ack); + } catch (IOException e) { + 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. + + 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) { + 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/peers/sip/core/useragent/handlers/MethodHandler.java b/src/peers/sip/core/useragent/handlers/MethodHandler.java new file mode 100644 index 0000000..bb6354c --- /dev/null +++ b/src/peers/sip/core/useragent/handlers/MethodHandler.java @@ -0,0 +1,86 @@ +/* + 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 peers.sip.core.useragent.handlers; + + +import peers.sdp.SDPManager; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.core.useragent.ChallengeManager; +import peers.sip.core.useragent.UserAgent; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transaction.TransactionManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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; + + public MethodHandler(UserAgent userAgent, + TransactionManager transactionManager, + TransportManager transportManager) { + this.userAgent = userAgent; + this.transactionManager = transactionManager; + this.transportManager = transportManager; + 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/peers/sip/core/useragent/handlers/OptionsHandler.java b/src/peers/sip/core/useragent/handlers/OptionsHandler.java new file mode 100644 index 0000000..3c2aa05 --- /dev/null +++ b/src/peers/sip/core/useragent/handlers/OptionsHandler.java @@ -0,0 +1,83 @@ +/* + 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 peers.sip.core.useragent.handlers; + +import java.io.IOException; +import java.util.Random; + + +import org.pmw.tinylog.Logger; +import peers.sdp.SessionDescription; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.core.useragent.UserAgent; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transaction.ServerTransaction; +import peers.sip.transaction.ServerTransactionUser; +import peers.sip.transaction.TransactionManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(userAgent, transactionManager, transportManager); + } + + 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) { + 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/peers/sip/core/useragent/handlers/RegisterHandler.java b/src/peers/sip/core/useragent/handlers/RegisterHandler.java new file mode 100644 index 0000000..be67137 --- /dev/null +++ b/src/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 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.sip.RFC3261; +import peers.sip.core.useragent.InitialRequestManager; +import peers.sip.core.useragent.RequestManager; +import peers.sip.core.useragent.SipListener; +import peers.sip.core.useragent.UserAgent; +import peers.sip.syntaxencoding.NameAddress; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.ClientTransactionUser; +import peers.sip.transaction.NonInviteClientTransaction; +import peers.sip.transaction.Transaction; +import peers.sip.transaction.TransactionManager; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(userAgent, transactionManager, transportManager); + } + + //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); + 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); + Logger.error(e.getMessage(), e); + } catch (SipUriSyntaxException e) { + notifyListener(sipResponse); + 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) { + Logger.error("syntax error", e); + } + } + } + + public void setInitialRequestManager(InitialRequestManager initialRequestManager) { + this.initialRequestManager = initialRequestManager; + } + +} diff --git a/src/peers/sip/syntaxencoding/NameAddress.java b/src/peers/sip/syntaxencoding/NameAddress.java new file mode 100644 index 0000000..ca646c9 --- /dev/null +++ b/src/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 peers.sip.syntaxencoding; + +import 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/peers/sip/syntaxencoding/RunnableSipParser.java b/src/peers/sip/syntaxencoding/RunnableSipParser.java new file mode 100644 index 0000000..f262ec0 --- /dev/null +++ b/src/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 peers.sip.syntaxencoding; + +public class RunnableSipParser implements Runnable { + + public void run() { + // TODO Auto-generated method stub + + } + +} diff --git a/src/peers/sip/syntaxencoding/SipHeader.java b/src/peers/sip/syntaxencoding/SipHeader.java new file mode 100644 index 0000000..693b621 --- /dev/null +++ b/src/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 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/peers/sip/syntaxencoding/SipHeaderFieldMultiValue.java b/src/peers/sip/syntaxencoding/SipHeaderFieldMultiValue.java new file mode 100644 index 0000000..16aedda --- /dev/null +++ b/src/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 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/peers/sip/syntaxencoding/SipHeaderFieldName.java b/src/peers/sip/syntaxencoding/SipHeaderFieldName.java new file mode 100644 index 0000000..7414fce --- /dev/null +++ b/src/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 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/peers/sip/syntaxencoding/SipHeaderFieldValue.java b/src/peers/sip/syntaxencoding/SipHeaderFieldValue.java new file mode 100644 index 0000000..535a961 --- /dev/null +++ b/src/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 peers.sip.syntaxencoding; + +import java.util.HashMap; + +import 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/peers/sip/syntaxencoding/SipHeaderParamName.java b/src/peers/sip/syntaxencoding/SipHeaderParamName.java new file mode 100644 index 0000000..ae56918 --- /dev/null +++ b/src/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 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/peers/sip/syntaxencoding/SipHeaders.java b/src/peers/sip/syntaxencoding/SipHeaders.java new file mode 100644 index 0000000..550128e --- /dev/null +++ b/src/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 peers.sip.syntaxencoding; + +import java.util.ArrayList; + +import 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/peers/sip/syntaxencoding/SipHeadersTable.java b/src/peers/sip/syntaxencoding/SipHeadersTable.java new file mode 100644 index 0000000..790de82 --- /dev/null +++ b/src/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 peers.sip.syntaxencoding; + +import java.util.HashMap; + +import 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/peers/sip/syntaxencoding/SipParser.java b/src/peers/sip/syntaxencoding/SipParser.java new file mode 100644 index 0000000..474971d --- /dev/null +++ b/src/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 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 peers.sip.RFC3261; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import 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/peers/sip/syntaxencoding/SipParserException.java b/src/peers/sip/syntaxencoding/SipParserException.java new file mode 100644 index 0000000..3d38b63 --- /dev/null +++ b/src/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 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/peers/sip/syntaxencoding/SipURI.java b/src/peers/sip/syntaxencoding/SipURI.java new file mode 100644 index 0000000..196b651 --- /dev/null +++ b/src/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 peers.sip.syntaxencoding; + +import java.util.Hashtable; + +import 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/peers/sip/syntaxencoding/SipUriSyntaxException.java b/src/peers/sip/syntaxencoding/SipUriSyntaxException.java new file mode 100644 index 0000000..429bdcb --- /dev/null +++ b/src/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 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/peers/sip/transaction/ClientTransaction.java b/src/peers/sip/transaction/ClientTransaction.java new file mode 100644 index 0000000..e56d944 --- /dev/null +++ b/src/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 peers.sip.transaction; + +import peers.sip.transport.SipResponse; + +public interface ClientTransaction { + + void receivedResponse(SipResponse sipResponse); + void start(); + String getContact(); +} diff --git a/src/peers/sip/transaction/ClientTransactionUser.java b/src/peers/sip/transaction/ClientTransactionUser.java new file mode 100644 index 0000000..91f707b --- /dev/null +++ b/src/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 peers.sip.transaction; + +import 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/peers/sip/transaction/InviteClientTransaction.java b/src/peers/sip/transaction/InviteClientTransaction.java new file mode 100644 index 0000000..2fb34eb --- /dev/null +++ b/src/peers/sip/transaction/InviteClientTransaction.java @@ -0,0 +1,247 @@ +/* + 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 peers.sip.transaction; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Timer; +import java.util.TimerTask; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transport.MessageSender; +import peers.sip.transport.SipClientTransportUser; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(branchId, timer, transportManager, transactionManager); + + 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); + state = INIT; + CALLING = new InviteClientTransactionStateCalling(getId(), this); + PROCEEDING = new InviteClientTransactionStateProceeding(getId(), this); + COMPLETED = new InviteClientTransactionStateCompleted(getId(), this); + TERMINATED = new InviteClientTransactionStateTerminated(getId(), this); + + //17.1.1.2 + + request = sipRequest; + this.transactionUser = transactionUser; + + remotePort = port; + remoteInetAddress = inetAddress; + + try { + messageSender = transportManager.createClientTransport( + request, remoteInetAddress, remotePort, transport); + } catch (IOException e) { + 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) { + Logger.error("input/output error", e); + transportError(); + } + 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) { + 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 { + 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) { + Logger.error("input/output error", e); + transportError(); + } + } + + void sendRetrans() { + ++nbRetrans; + //sipClientTransport.send(request); + try { + messageSender.sendMessage(request); + } catch (IOException e) { + 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/peers/sip/transaction/InviteClientTransactionState.java b/src/peers/sip/transaction/InviteClientTransactionState.java new file mode 100644 index 0000000..84e1552 --- /dev/null +++ b/src/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 peers.sip.transaction; + + +import peers.sip.AbstractState; + + +public abstract class InviteClientTransactionState extends AbstractState { + + protected InviteClientTransaction inviteClientTransaction; + + public InviteClientTransactionState(String id, + InviteClientTransaction inviteClientTransaction) { + super(id); + 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/peers/sip/transaction/InviteClientTransactionStateCalling.java b/src/peers/sip/transaction/InviteClientTransactionStateCalling.java new file mode 100644 index 0000000..1c6c345 --- /dev/null +++ b/src/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 peers.sip.transaction; + + + + +public class InviteClientTransactionStateCalling extends InviteClientTransactionState { + + public InviteClientTransactionStateCalling(String id, + InviteClientTransaction inviteClientTransaction) { + super(id, inviteClientTransaction); + } + + @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/peers/sip/transaction/InviteClientTransactionStateCompleted.java b/src/peers/sip/transaction/InviteClientTransactionStateCompleted.java new file mode 100644 index 0000000..791cad1 --- /dev/null +++ b/src/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 peers.sip.transaction; + + +import peers.sip.RFC3261; + +public class InviteClientTransactionStateCompleted extends + InviteClientTransactionState { + + public InviteClientTransactionStateCompleted(String id, + InviteClientTransaction inviteClientTransaction) { + super(id, inviteClientTransaction); + 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/peers/sip/transaction/InviteClientTransactionStateInit.java b/src/peers/sip/transaction/InviteClientTransactionStateInit.java new file mode 100644 index 0000000..3114c6f --- /dev/null +++ b/src/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 peers.sip.transaction; + + + +public class InviteClientTransactionStateInit extends InviteClientTransactionState { + + public InviteClientTransactionStateInit(String id, + InviteClientTransaction inviteClientTransaction) { + super(id, inviteClientTransaction); + } + + @Override + public void start() { + InviteClientTransactionState nextState = inviteClientTransaction.CALLING; + inviteClientTransaction.setState(nextState); + } + +} diff --git a/src/peers/sip/transaction/InviteClientTransactionStateProceeding.java b/src/peers/sip/transaction/InviteClientTransactionStateProceeding.java new file mode 100644 index 0000000..4bf4b6f --- /dev/null +++ b/src/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 peers.sip.transaction; + + + +public class InviteClientTransactionStateProceeding extends + InviteClientTransactionState { + + public InviteClientTransactionStateProceeding(String id, + InviteClientTransaction inviteClientTransaction) { + super(id, inviteClientTransaction); + } + + @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/peers/sip/transaction/InviteClientTransactionStateTerminated.java b/src/peers/sip/transaction/InviteClientTransactionStateTerminated.java new file mode 100644 index 0000000..e9daad0 --- /dev/null +++ b/src/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 peers.sip.transaction; + + + +public class InviteClientTransactionStateTerminated extends + InviteClientTransactionState { + + public InviteClientTransactionStateTerminated(String id, + InviteClientTransaction inviteClientTransaction) { + super(id, inviteClientTransaction); + } + +} diff --git a/src/peers/sip/transaction/InviteServerTransaction.java b/src/peers/sip/transaction/InviteServerTransaction.java new file mode 100644 index 0000000..7e426a8 --- /dev/null +++ b/src/peers/sip/transaction/InviteServerTransaction.java @@ -0,0 +1,179 @@ +/* + 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 peers.sip.transaction; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import peers.sip.transport.SipServerTransportUser; +import 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) { + super(branchId, timer, transportManager, transactionManager); + + INIT = new InviteServerTransactionStateInit(getId(), this); + state = INIT; + PROCEEDING = new InviteServerTransactionStateProceeding(getId(), this); + COMPLETED = new InviteServerTransactionStateCompleted(getId(), this); + CONFIRMED = new InviteServerTransactionStateConfirmed(getId(), this); + TERMINATED = new InviteServerTransactionStateTerminated(getId(), this); + + 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) { + 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 { + 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) { + 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/peers/sip/transaction/InviteServerTransactionState.java b/src/peers/sip/transaction/InviteServerTransactionState.java new file mode 100644 index 0000000..58f75d6 --- /dev/null +++ b/src/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 peers.sip.transaction; + + +import peers.sip.AbstractState; + +public abstract class InviteServerTransactionState extends AbstractState { + + protected InviteServerTransaction inviteServerTransaction; + + public InviteServerTransactionState(String id, + InviteServerTransaction inviteServerTransaction) { + super(id); + 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/peers/sip/transaction/InviteServerTransactionStateCompleted.java b/src/peers/sip/transaction/InviteServerTransactionStateCompleted.java new file mode 100644 index 0000000..f776ddc --- /dev/null +++ b/src/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 peers.sip.transaction; + + +import peers.sip.RFC3261; + +public class InviteServerTransactionStateCompleted extends + InviteServerTransactionState { + + public InviteServerTransactionStateCompleted(String id, + InviteServerTransaction inviteServerTransaction) { + super(id, inviteServerTransaction); + } + + @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/peers/sip/transaction/InviteServerTransactionStateConfirmed.java b/src/peers/sip/transaction/InviteServerTransactionStateConfirmed.java new file mode 100644 index 0000000..7cf2c33 --- /dev/null +++ b/src/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 peers.sip.transaction; + + + +public class InviteServerTransactionStateConfirmed extends + InviteServerTransactionState { + + public InviteServerTransactionStateConfirmed(String id, + InviteServerTransaction inviteServerTransaction) { + super(id, inviteServerTransaction); + } + + @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/peers/sip/transaction/InviteServerTransactionStateInit.java b/src/peers/sip/transaction/InviteServerTransactionStateInit.java new file mode 100644 index 0000000..6ff34a5 --- /dev/null +++ b/src/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 peers.sip.transaction; + + + +public class InviteServerTransactionStateInit extends + InviteServerTransactionState { + + public InviteServerTransactionStateInit(String id, + InviteServerTransaction inviteServerTransaction) { + super(id, inviteServerTransaction); + } + + @Override + public void start() { + InviteServerTransactionState nextState = inviteServerTransaction.PROCEEDING; + inviteServerTransaction.setState(nextState); + } +} diff --git a/src/peers/sip/transaction/InviteServerTransactionStateProceeding.java b/src/peers/sip/transaction/InviteServerTransactionStateProceeding.java new file mode 100644 index 0000000..dd4ea34 --- /dev/null +++ b/src/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 peers.sip.transaction; + + +import peers.sip.RFC3261; + +public class InviteServerTransactionStateProceeding extends + InviteServerTransactionState { + + public InviteServerTransactionStateProceeding(String id, + InviteServerTransaction inviteServerTransaction) { + super(id, inviteServerTransaction); + } + + @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/peers/sip/transaction/InviteServerTransactionStateTerminated.java b/src/peers/sip/transaction/InviteServerTransactionStateTerminated.java new file mode 100644 index 0000000..9ff9125 --- /dev/null +++ b/src/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 peers.sip.transaction; + + + +public class InviteServerTransactionStateTerminated extends + InviteServerTransactionState { + + public InviteServerTransactionStateTerminated(String id, + InviteServerTransaction inviteServerTransaction) { + super(id, inviteServerTransaction); + } + +} diff --git a/src/peers/sip/transaction/InviteTransaction.java b/src/peers/sip/transaction/InviteTransaction.java new file mode 100644 index 0000000..c5087eb --- /dev/null +++ b/src/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 peers.sip.transaction; + +import java.util.Timer; + + +import peers.sip.RFC3261; +import peers.sip.transport.TransportManager; + +public abstract class InviteTransaction extends Transaction { + + protected InviteTransaction(String branchId, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager) { + super(branchId, RFC3261.METHOD_INVITE, timer, transportManager, + transactionManager); + } + +} diff --git a/src/peers/sip/transaction/NonInviteClientTransaction.java b/src/peers/sip/transaction/NonInviteClientTransaction.java new file mode 100644 index 0000000..9cd26cd --- /dev/null +++ b/src/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 peers.sip.transaction; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Timer; +import java.util.TimerTask; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.transport.MessageSender; +import peers.sip.transport.SipClientTransportUser; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(branchId, sipRequest.getMethod(), timer, transportManager, + transactionManager); + + 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); + state = INIT; + TRYING = new NonInviteClientTransactionStateTrying(getId(), this); + PROCEEDING = new NonInviteClientTransactionStateProceeding(getId(), + this); + COMPLETED = new NonInviteClientTransactionStateCompleted(getId(), + this); + TERMINATED = new NonInviteClientTransactionStateTerminated(getId(), + this); + + request = sipRequest; + this.transactionUser = transactionUser; + + remotePort = port; + remoteInetAddress = inetAddress; + + try { + messageSender = transportManager.createClientTransport( + request, remoteInetAddress, remotePort, transport); + } catch (IOException e) { + 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) { + 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) { + 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) { + Logger.error("invalid response code"); + } else if (statusCode < RFC3261.CODE_MIN_SUCCESS) { + state.received1xx(); + } else if (statusCode <= RFC3261.CODE_MAX) { + state.received200To699(); + } else { + 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/peers/sip/transaction/NonInviteClientTransactionState.java b/src/peers/sip/transaction/NonInviteClientTransactionState.java new file mode 100644 index 0000000..695bcd0 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteClientTransactionState.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 peers.sip.transaction; + + +import peers.sip.AbstractState; + +public abstract class NonInviteClientTransactionState extends AbstractState { + + protected NonInviteClientTransaction nonInviteClientTransaction; + + public NonInviteClientTransactionState(String id, + NonInviteClientTransaction nonInviteClientTransaction) { + super(id); + 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/peers/sip/transaction/NonInviteClientTransactionStateCompleted.java b/src/peers/sip/transaction/NonInviteClientTransactionStateCompleted.java new file mode 100644 index 0000000..c9d7b0a --- /dev/null +++ b/src/peers/sip/transaction/NonInviteClientTransactionStateCompleted.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 peers.sip.transaction; + + +import peers.sip.RFC3261; + +public class NonInviteClientTransactionStateCompleted extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateCompleted(String id, + NonInviteClientTransaction nonInviteClientTransaction) { + super(id, nonInviteClientTransaction); + 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/peers/sip/transaction/NonInviteClientTransactionStateInit.java b/src/peers/sip/transaction/NonInviteClientTransactionStateInit.java new file mode 100644 index 0000000..12e9f14 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteClientTransactionStateInit.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 peers.sip.transaction; + + + +public class NonInviteClientTransactionStateInit extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateInit(String id, + NonInviteClientTransaction nonInviteClientTransaction) { + super(id, nonInviteClientTransaction); + } + + @Override + public void start() { + NonInviteClientTransactionState nextState = nonInviteClientTransaction.TRYING; + nonInviteClientTransaction.setState(nextState); + } +} diff --git a/src/peers/sip/transaction/NonInviteClientTransactionStateProceeding.java b/src/peers/sip/transaction/NonInviteClientTransactionStateProceeding.java new file mode 100644 index 0000000..9b5d9b5 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteClientTransactionStateProceeding.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 peers.sip.transaction; + + +import peers.sip.RFC3261; +import peers.sip.transport.SipResponse; + +public class NonInviteClientTransactionStateProceeding extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateProceeding(String id, + NonInviteClientTransaction nonInviteClientTransaction) { + super(id, nonInviteClientTransaction); + } + + @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/peers/sip/transaction/NonInviteClientTransactionStateTerminated.java b/src/peers/sip/transaction/NonInviteClientTransactionStateTerminated.java new file mode 100644 index 0000000..4dcd862 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteClientTransactionStateTerminated.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 peers.sip.transaction; + + + +public class NonInviteClientTransactionStateTerminated extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateTerminated(String id, + NonInviteClientTransaction nonInviteClientTransaction) { + super(id, nonInviteClientTransaction); + } + +} diff --git a/src/peers/sip/transaction/NonInviteClientTransactionStateTrying.java b/src/peers/sip/transaction/NonInviteClientTransactionStateTrying.java new file mode 100644 index 0000000..8aba1c5 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteClientTransactionStateTrying.java @@ -0,0 +1,83 @@ +/* + 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 peers.sip.transaction; + + +import peers.sip.RFC3261; +import peers.sip.transport.SipResponse; + +public class NonInviteClientTransactionStateTrying extends + NonInviteClientTransactionState { + + public NonInviteClientTransactionStateTrying(String id, + NonInviteClientTransaction nonInviteClientTransaction) { + super(id, nonInviteClientTransaction); + } + + @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/peers/sip/transaction/NonInviteServerTransaction.java b/src/peers/sip/transaction/NonInviteServerTransaction.java new file mode 100644 index 0000000..2c34b7e --- /dev/null +++ b/src/peers/sip/transaction/NonInviteServerTransaction.java @@ -0,0 +1,126 @@ +/* + 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 peers.sip.transaction; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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) { + super(branchId, method, timer, transportManager, transactionManager); + + TRYING = new NonInviteServerTransactionStateTrying(getId(), this); + state = TRYING; + PROCEEDING = new NonInviteServerTransactionStateProceeding(getId(), + this); + COMPLETED = new NonInviteServerTransactionStateCompleted(getId(), this); + TERMINATED = new NonInviteServerTransactionStateTerminated(getId(), + this); + + //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) { + 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) { + 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/peers/sip/transaction/NonInviteServerTransactionState.java b/src/peers/sip/transaction/NonInviteServerTransactionState.java new file mode 100644 index 0000000..5c6e0a1 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteServerTransactionState.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 peers.sip.transaction; + + +import peers.sip.AbstractState; + +//17.2.2 +public abstract class NonInviteServerTransactionState extends AbstractState { + + protected NonInviteServerTransaction nonInviteServerTransaction; + + public NonInviteServerTransactionState(String id, + NonInviteServerTransaction nonInviteServerTransaction) { + super(id); + this.nonInviteServerTransaction = nonInviteServerTransaction; + } + + public void received200To699() {} + public void received1xx() {} + public void receivedRequest() {} + public void transportError() {} + public void timerJFires() {} +} + diff --git a/src/peers/sip/transaction/NonInviteServerTransactionStateCompleted.java b/src/peers/sip/transaction/NonInviteServerTransactionStateCompleted.java new file mode 100644 index 0000000..ec3c767 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteServerTransactionStateCompleted.java @@ -0,0 +1,51 @@ +/* + 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 peers.sip.transaction; + + + +public class NonInviteServerTransactionStateCompleted extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateCompleted(String id, + NonInviteServerTransaction nonInviteServerTransaction) { + super(id, nonInviteServerTransaction); + } + + @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/peers/sip/transaction/NonInviteServerTransactionStateProceeding.java b/src/peers/sip/transaction/NonInviteServerTransactionStateProceeding.java new file mode 100644 index 0000000..11370dc --- /dev/null +++ b/src/peers/sip/transaction/NonInviteServerTransactionStateProceeding.java @@ -0,0 +1,72 @@ +/* + 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 peers.sip.transaction; + + +import peers.sip.RFC3261; + +public class NonInviteServerTransactionStateProceeding extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateProceeding(String id, + NonInviteServerTransaction nonInviteServerTransaction) { + super(id, nonInviteServerTransaction); + } + + @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/peers/sip/transaction/NonInviteServerTransactionStateTerminated.java b/src/peers/sip/transaction/NonInviteServerTransactionStateTerminated.java new file mode 100644 index 0000000..67c0462 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteServerTransactionStateTerminated.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 peers.sip.transaction; + + + +public class NonInviteServerTransactionStateTerminated extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateTerminated(String id, + NonInviteServerTransaction nonInviteServerTransaction) { + super(id, nonInviteServerTransaction); + } + +} diff --git a/src/peers/sip/transaction/NonInviteServerTransactionStateTrying.java b/src/peers/sip/transaction/NonInviteServerTransactionStateTrying.java new file mode 100644 index 0000000..8b02e4f --- /dev/null +++ b/src/peers/sip/transaction/NonInviteServerTransactionStateTrying.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 peers.sip.transaction; + + + +public class NonInviteServerTransactionStateTrying extends + NonInviteServerTransactionState { + + public NonInviteServerTransactionStateTrying(String id, + NonInviteServerTransaction nonInviteServerTransaction) { + super(id, nonInviteServerTransaction); + } + + @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/peers/sip/transaction/NonInviteTransaction.java b/src/peers/sip/transaction/NonInviteTransaction.java new file mode 100644 index 0000000..85a2f25 --- /dev/null +++ b/src/peers/sip/transaction/NonInviteTransaction.java @@ -0,0 +1,35 @@ +/* + 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 peers.sip.transaction; + +import java.util.Timer; + + +import peers.sip.transport.TransportManager; + +public abstract class NonInviteTransaction extends Transaction { + + protected NonInviteTransaction(String branchId, String method, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager) { + super(branchId, method, timer, transportManager, transactionManager); + } + +} diff --git a/src/peers/sip/transaction/ServerTransaction.java b/src/peers/sip/transaction/ServerTransaction.java new file mode 100644 index 0000000..8165b93 --- /dev/null +++ b/src/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 peers.sip.transaction; + +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; + +public interface ServerTransaction { + + public void start(); + + public void receivedRequest(SipRequest sipRequest); + + public void sendReponse(SipResponse sipResponse); +} diff --git a/src/peers/sip/transaction/ServerTransactionUser.java b/src/peers/sip/transaction/ServerTransactionUser.java new file mode 100644 index 0000000..3e47659 --- /dev/null +++ b/src/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 peers.sip.transaction; + +public interface ServerTransactionUser { + + public void transactionFailure(); +} diff --git a/src/peers/sip/transaction/SipListeningPoint.java b/src/peers/sip/transaction/SipListeningPoint.java new file mode 100644 index 0000000..14a653b --- /dev/null +++ b/src/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 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/peers/sip/transaction/Transaction.java b/src/peers/sip/transaction/Transaction.java new file mode 100644 index 0000000..fd54c52 --- /dev/null +++ b/src/peers/sip/transaction/Transaction.java @@ -0,0 +1,77 @@ +/* + 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 peers.sip.transaction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Timer; + + +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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 Transaction(String branchId, String method, Timer timer, + TransportManager transportManager, + TransactionManager transactionManager) { + this.branchId = branchId; + this.method = method; + this.timer = timer; + this.transportManager = transportManager; + this.transactionManager = transactionManager; + 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/peers/sip/transaction/TransactionManager.java b/src/peers/sip/transaction/TransactionManager.java new file mode 100644 index 0000000..70b29e6 --- /dev/null +++ b/src/peers/sip/transaction/TransactionManager.java @@ -0,0 +1,209 @@ +/* + 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 peers.sip.transaction; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Timer; + + +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipRequest; +import peers.sip.transport.SipResponse; +import 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; + + public TransactionManager() { + 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); + } else { + clientTransaction = new NonInviteClientTransaction(branchId, + inetAddress, port, transport, sipRequest, clientTransactionUser, + timer, transportManager, this); + } + 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); + // serverTransaction = new InviteServerTransaction(branchId); + } else { + serverTransaction = new NonInviteServerTransaction(branchId, port, + transport, method, serverTransactionUser, sipRequest, timer, + transportManager, this); + } + 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/peers/sip/transactionuser/Dialog.java b/src/peers/sip/transactionuser/Dialog.java new file mode 100644 index 0000000..92f8041 --- /dev/null +++ b/src/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 peers.sip.transactionuser; + +import java.util.ArrayList; + + +import org.pmw.tinylog.Logger; +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.syntaxencoding.NameAddress; +import peers.sip.syntaxencoding.SipHeaderFieldMultiValue; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipURI; +import peers.sip.syntaxencoding.SipUriSyntaxException; +import 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; + + + Dialog(String callId, String localTag, String remoteTag) { + super(); + this.callId = callId; + this.localTag = localTag; + this.remoteTag = remoteTag; + + INIT = new DialogStateInit(getId(), this); + state = INIT; + EARLY = new DialogStateEarly(getId(), this); + CONFIRMED = new DialogStateConfirmed(getId(), this); + TERMINATED = new DialogStateTerminated(getId(), this); + + 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 { + 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/peers/sip/transactionuser/DialogManager.java b/src/peers/sip/transactionuser/DialogManager.java new file mode 100644 index 0000000..d61b573 --- /dev/null +++ b/src/peers/sip/transactionuser/DialogManager.java @@ -0,0 +1,115 @@ +/* + 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 peers.sip.transactionuser; + +import java.util.Collection; +import java.util.Hashtable; + + +import peers.sip.RFC3261; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.transport.SipMessage; +import peers.sip.transport.SipResponse; + + +public class DialogManager { + + private Hashtable dialogs; + + + public DialogManager() { + 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); + } else { + //createDialog is called from UAC side, in syntax encoding layer + dialog = new Dialog(callID, fromTag, toTag); + } + 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/peers/sip/transactionuser/DialogState.java b/src/peers/sip/transactionuser/DialogState.java new file mode 100644 index 0000000..c1ba8ca --- /dev/null +++ b/src/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 peers.sip.transactionuser; + + +import peers.sip.AbstractState; + +public abstract class DialogState extends AbstractState { + + protected Dialog dialog; + + public DialogState(String id, Dialog dialog) { + super(id); + 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/peers/sip/transactionuser/DialogStateConfirmed.java b/src/peers/sip/transactionuser/DialogStateConfirmed.java new file mode 100644 index 0000000..30a8b18 --- /dev/null +++ b/src/peers/sip/transactionuser/DialogStateConfirmed.java @@ -0,0 +1,55 @@ +/* + 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 peers.sip.transactionuser; + + +import org.pmw.tinylog.Logger; + +public class DialogStateConfirmed extends DialogState { + + public DialogStateConfirmed(String id, Dialog dialog) { + super(id, dialog); + } + + @Override + public void receivedOrSent101To199() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent2xx() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent300To699() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSentBye() { + DialogState nextState = dialog.TERMINATED; + dialog.setState(nextState); + } + +} diff --git a/src/peers/sip/transactionuser/DialogStateEarly.java b/src/peers/sip/transactionuser/DialogStateEarly.java new file mode 100644 index 0000000..292d27c --- /dev/null +++ b/src/peers/sip/transactionuser/DialogStateEarly.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 peers.sip.transactionuser; + + +import org.pmw.tinylog.Logger; + +public class DialogStateEarly extends DialogState { + + public DialogStateEarly(String id, Dialog dialog) { + super(id, dialog); + } + + @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() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } +} diff --git a/src/peers/sip/transactionuser/DialogStateInit.java b/src/peers/sip/transactionuser/DialogStateInit.java new file mode 100644 index 0000000..e3b4c4c --- /dev/null +++ b/src/peers/sip/transactionuser/DialogStateInit.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 peers.sip.transactionuser; + + +import org.pmw.tinylog.Logger; + +public class DialogStateInit extends DialogState { + + public DialogStateInit(String id, Dialog dialog) { + super(id, dialog); + } + + @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() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } +} diff --git a/src/peers/sip/transactionuser/DialogStateTerminated.java b/src/peers/sip/transactionuser/DialogStateTerminated.java new file mode 100644 index 0000000..2b38e58 --- /dev/null +++ b/src/peers/sip/transactionuser/DialogStateTerminated.java @@ -0,0 +1,55 @@ +/* + 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 peers.sip.transactionuser; + + +import org.pmw.tinylog.Logger; + +public class DialogStateTerminated extends DialogState { + + public DialogStateTerminated(String id, Dialog dialog) { + super(id, dialog); + } + + @Override + public void receivedOrSent101To199() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent2xx() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSent300To699() { + Logger.error(id + " invalid transition"); + throw new IllegalStateException(); + } + + @Override + public void receivedOrSentBye() { + //ignore bye retransmissions +// Logger.error(id + " invalid transition"); +// throw new IllegalStateException(); + } +} diff --git a/src/peers/sip/transport/MessageReceiver.java b/src/peers/sip/transport/MessageReceiver.java new file mode 100644 index 0000000..0c1dbcf --- /dev/null +++ b/src/peers/sip/transport/MessageReceiver.java @@ -0,0 +1,205 @@ +/* + 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 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipParserException; +import peers.sip.transaction.ClientTransaction; +import peers.sip.transaction.ServerTransaction; +import 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; + + + public MessageReceiver(int port, TransactionManager transactionManager, + TransportManager transportManager, Config config) { + super(); + this.port = port; + this.transactionManager = transactionManager; + this.transportManager = transportManager; + this.config = config; + isListening = true; + } + + public void run() { + while (isListening) { + try { + listen(); + } catch (IOException e) { + 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) { + 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); + Logger.info(new String(message),direction.toString()); + SipMessage sipMessage = null; + try { + sipMessage = transportManager.sipParser.parse( + new ByteArrayInputStream(message)); + } catch (IOException e) { + Logger.error("input/output error", e); + } catch (SipParserException e) { + 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); + 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/peers/sip/transport/MessageSender.java b/src/peers/sip/transport/MessageSender.java new file mode 100644 index 0000000..64496ca --- /dev/null +++ b/src/peers/sip/transport/MessageSender.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-2013 Yohann Martineau +*/ + +package peers.sip.transport; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Timer; +import java.util.TimerTask; + +import org.pmw.tinylog.Logger; +import peers.Config; + +import 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; + + public MessageSender(int localPort, InetAddress inetAddress, + int port, Config config, + String transportName) { + super(); + this.localPort = localPort; + this.inetAddress = inetAddress; + this.port = port; + this.config = config; + this.transportName = transportName; + timer = new Timer(getClass().getSimpleName() + " " + + Timer.class.getSimpleName()); + //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) { + Logger.error(e.getMessage(), e); + } + } + + } + +} diff --git a/src/peers/sip/transport/SipClientTransportUser.java b/src/peers/sip/transport/SipClientTransportUser.java new file mode 100644 index 0000000..94d570e --- /dev/null +++ b/src/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 peers.sip.transport; + + +public interface SipClientTransportUser { + + public void requestTransportError(SipRequest sipRequest, Exception e); + public void responseTransportError(Exception e); +} diff --git a/src/peers/sip/transport/SipMessage.java b/src/peers/sip/transport/SipMessage.java new file mode 100644 index 0000000..1c4679d --- /dev/null +++ b/src/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 peers.sip.transport; + +import peers.sip.RFC3261; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import 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/peers/sip/transport/SipRequest.java b/src/peers/sip/transport/SipRequest.java new file mode 100644 index 0000000..69167f8 --- /dev/null +++ b/src/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 peers.sip.transport; + +import peers.sip.RFC3261; +import 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/peers/sip/transport/SipResponse.java b/src/peers/sip/transport/SipResponse.java new file mode 100644 index 0000000..95a52c5 --- /dev/null +++ b/src/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 peers.sip.transport; + +import 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/peers/sip/transport/SipServerTransportUser.java b/src/peers/sip/transport/SipServerTransportUser.java new file mode 100644 index 0000000..27f10f8 --- /dev/null +++ b/src/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 peers.sip.transport; + +public interface SipServerTransportUser { + + public void messageReceived(SipMessage sipMessage); +} diff --git a/src/peers/sip/transport/SipTransportConnection.java b/src/peers/sip/transport/SipTransportConnection.java new file mode 100644 index 0000000..167b99b --- /dev/null +++ b/src/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 peers.sip.transport; + +import java.net.InetAddress; + +import 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/peers/sip/transport/TransportManager.java b/src/peers/sip/transport/TransportManager.java new file mode 100644 index 0000000..7d302d1 --- /dev/null +++ b/src/peers/sip/transport/TransportManager.java @@ -0,0 +1,479 @@ +/* + 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 peers.sip.transport; + +import static peers.sip.RFC3261.DEFAULT_SIP_VERSION; +import static peers.sip.RFC3261.IPV4_TTL; +import static peers.sip.RFC3261.PARAM_MADDR; +import static peers.sip.RFC3261.PARAM_TTL; +import static peers.sip.RFC3261.TRANSPORT_DEFAULT_PORT; +import static peers.sip.RFC3261.TRANSPORT_PORT_SEP; +import static peers.sip.RFC3261.TRANSPORT_SCTP; +import static peers.sip.RFC3261.TRANSPORT_TCP; +import static peers.sip.RFC3261.TRANSPORT_TLS_PORT; +import static peers.sip.RFC3261.TRANSPORT_UDP; +import static peers.sip.RFC3261.TRANSPORT_UDP_USUAL_MAX_SIZE; +import static peers.sip.RFC3261.TRANSPORT_VIA_SEP; +import static 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.sip.RFC3261; +import peers.sip.Utils; +import peers.sip.syntaxencoding.SipHeaderFieldName; +import peers.sip.syntaxencoding.SipHeaderFieldValue; +import peers.sip.syntaxencoding.SipHeaderParamName; +import peers.sip.syntaxencoding.SipHeaders; +import peers.sip.syntaxencoding.SipParser; +import peers.sip.transaction.TransactionManager; + + +public class TransportManager { + + public static final int SOCKET_TIMEOUT = RFC3261.TIMER_T1; + + private static int NO_TTL = -1; + + + //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 ) { + sipParser = new SipParser(); + datagramSockets = new Hashtable(); + messageSenders = new Hashtable(); + messageReceivers = new Hashtable(); + this.transactionManager = transactionManager; + this.config = config; + } + + 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 { + 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) { + 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) { + 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) { + + Logger.error("cannot create socket", e); + } catch (SecurityException e) { + 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); + } 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); +// 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); + 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) { + 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) { + Logger.error("cannot create socket", e); + } catch (SecurityException e) { + 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); + Logger.info("added datagram socket " + sipTransportConnection); + } + messageReceiver = new UdpMessageReceiver(datagramSocket, + transactionManager, this, config); + messageReceiver.setSipServerTransportUser(sipServerTransportUser); + //TODO create also tcp receiver using a recursive call + } else { + //TODO + //messageReceiver = new TcpMessageReceiver(port); + } + messageReceivers.put(sipTransportConnection, messageReceiver); + 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/peers/sip/transport/UdpMessageReceiver.java b/src/peers/sip/transport/UdpMessageReceiver.java new file mode 100644 index 0000000..6607764 --- /dev/null +++ b/src/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 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.sip.RFC3261; +import peers.sip.transaction.TransactionManager; + + +public class UdpMessageReceiver extends MessageReceiver { + + private DatagramSocket datagramSocket; + + public UdpMessageReceiver(DatagramSocket datagramSocket, + TransactionManager transactionManager, + TransportManager transportManager, Config config) + throws SocketException { + super(datagramSocket.getLocalPort(), transactionManager, + transportManager, config); + 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) { + 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/peers/sip/transport/UdpMessageSender.java b/src/peers/sip/transport/UdpMessageSender.java new file mode 100644 index 0000000..96fa560 --- /dev/null +++ b/src/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 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 org.pmw.tinylog.Logger; +import peers.Config; + +import peers.sip.RFC3261; + + +public class UdpMessageSender extends MessageSender { + + private DatagramSocket datagramSocket; + + public UdpMessageSender(InetAddress inetAddress, int port, + DatagramSocket datagramSocket, Config config) throws SocketException { + super(datagramSocket.getLocalPort(), inetAddress, port, + config, RFC3261.TRANSPORT_UDP); + this.datagramSocket = datagramSocket; + } + + @Override + public synchronized void sendMessage(SipMessage sipMessage) throws IOException { + 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); + Logger.info(new String(buf), direction.toString()); + } + + @Override + public synchronized void sendBytes(byte[] bytes) throws IOException { + Logger.debug("UdpMessageSender.sendBytes"); + final DatagramPacket packet = new DatagramPacket(bytes, bytes.length, + inetAddress, port); + Logger.debug("UdpMessageSender.sendBytes " + bytes.length + + " " + inetAddress + ":" + port); + // AccessController.doPrivileged added for plugin compatibility + AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public Void run() { + try { + Logger.debug(datagramSocket.getLocalAddress().toString()); + datagramSocket.send(packet); + } catch (Throwable t) { + Logger.error("throwable", new Exception(t)); + } + return null; + } + } + ); + + Logger.debug("UdpMessageSender.sendBytes packet sent"); + } + +}