/** * This is the MainActivity.java file from the AJVoIP_Streaming2 example project, a simple test application for AJVoIP demonstrating capturing it's media streams. * See the "How to get the audio stream" and "Work with audio streams" FAQ point in the JVoIP documentation for the details. * * In this simple example we just capture the audio stream received from the remote peer and save it in a stream.wav file. * * (Of course, the same could be done using the AJVoIP voice recording functionality (voicerecording parameter / API_VoiceRecord function), * but this is just a demonstration and instead of saving the stream to file, you might process it in real time after your needs, for example using speech to text for translation or other purposes). * * You might also stream something back to the remote peer using the StreamSoundBuff or StreamSoundStream API functions. * * IMPORTANT: Make sure to copy/include the AJVoIP.aar file to your project required libraries list !!! * * just copy the AJVoIP.aar into your \app\libs\ folder and make sure that in your build.gradle you declare the libs dependencies like this: implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) * ...or add it explicitly. In Android Studio it can be done from File -> Project Structure -> Dependencies -> select your app module and add to declared dependencies with the + button * more help here: https://stackoverflow.com/questions/16682847/how-to-manually-include-external-aar-package-using-gradle-for-android * * Optionally copy also the ajvoip-sources.jar file for better inline documentation in your IDE. It might need to be selected from "Choose Sources" if not recognized automatically. */ package com.ajvoiptest; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import java.util.Calendar; import java.util.TimeZone; import androidx.core.app.ActivityCompat; import com.mizuvoip.jvoip.*; //import the MizuTech SIP library as described above! public class MainActivity extends Activity { public static String LOGTAG = "AJVoIP"; EditText mParams = null; EditText mDestNumber = null; Button mBtnStart = null; Button mBtnCall = null; Button mBtnHangup = null; Button mBtnTest = null; TextView mStatus = null; TextView mNotifications = null; SipStack mysipclient = null; Context ctx = null; public static MainActivity instance = null; byte[] audiobuffer = null; //we will collect the received audio packets into this buffer int audiobufferlen = 0; //written size @Override protected void onCreate(Bundle savedInstanceState) { //Message messageToMainThread = new Message(); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ctx = this; instance = this; mParams = (EditText) findViewById(R.id.parameters_view); mDestNumber = (EditText) findViewById(R.id.dest_number); mBtnStart = (Button) findViewById(R.id.btn_start); mBtnCall = (Button) findViewById(R.id.btn_call); mBtnHangup = (Button) findViewById(R.id.btn_hangup); mBtnTest = (Button) findViewById(R.id.btn_test); mStatus = (TextView) findViewById(R.id.status); mNotifications = (TextView) findViewById(R.id.notifications); mNotifications.setMovementMethod(new ScrollingMovementMethod()); DisplayLogs("oncreate"); //SIP stack parameters separated by CRLF. Will be passed to AJVoIP with the SetParameters API call (you might also use the SetParameter API to pass the parameters separately) //Add other settings after your needs. See the documentation "Parameters" chapter for the full list of available settings. //default parameters: StringBuilder parameters = new StringBuilder(); parameters.append("loglevel=5\r\n"); //for development you should set the loglevel to 5. for production you should set the loglevel to 1 //parameters.append("register=1\r\n"); //auto register (set to 0 if you don't need to register or if you wish to call the Register explicitely later or set to 2 if must register) //parameters.append("proxyaddress=1\r\n"); //set this if you have a (outbound) proxy //parameters.append("transport=0\r\n"); //the default transport for signaling is -1/auto (UDP with failover to TCP). Set to 0 if your server is listening on UDP only, 1 if you need TCP or to 2 if you need TLS parameters.append("serveraddress=voip.mizu-voip.com\r\n"); parameters.append("username=ajvoiptest\r\n"); parameters.append("password=ajvoip1234\r\n"); //streaming related settings (see the "How to get the audio stream" FAQ point in the documentation): parameters.append("sendmedia_mode=3\r\n"); //this is optional because it will be set automatically anyway by API_SetMediaListener parameters.append("sendmedia_direction=1\r\n"); //this is optional because it will be set automatically anyway by API_SetMediaListener parameters.append("sendmedia_media=1\r\n"); //we are interested only for audio in this example parameters.append("sendmedia_aformat=16000\r\n"); //raw PCM 16kHz wideband /* //disable playback and record to/from the local audio device and other related settings: parameters.append("voicerecording=1\r\n"); parameters.append("useaudiodevicerecord=false\r\n"); parameters.append("useaudiodeviceplayback=false\r\n"); parameters.append("enablesounds=0\r\n"); parameters.append("audiomanagermode=-8\r\n"); parameters.append("focusaudio=1\r\n"); parameters.append("checkvolumesettings=0\r\n"); parameters.append("vibrate=0\r\n"); parameters.append("playring=0\r\n"); parameters.append("ringincall=0\r\n"); parameters.append("beeponconnect=0\r\n"); parameters.append("agc=0\r\n"); parameters.append("aec=0\r\n"); parameters.append("denoise=0\r\n"); parameters.append("syncvoicerec=0\r\n"); parameters.append("mediatimeout=0\r\n"); */ mParams.setText(parameters.toString()); mDestNumber.setText("testivr3"); //default call-to number for our test (testivr3 is a music IVR access number on our test server at voip.mizu-voip.com) DisplayStatus("Ready."); mBtnStart.setOnClickListener(new View.OnClickListener() { public void onClick(View v) //Start button click { DisplayLogs("Start on click"); try{ // start SipStack if it's not already running if (mysipclient == null) //check if AJVoIP instance already exists { DisplayLogs("Starting SIPStack"); //initialize the SIP engine mysipclient = new SipStack(); mysipclient.Init(ctx, "startsipstack=0"); SetParameters(); //pass the configuration (parameters can be changed also later at run-time) //subscribe to notification events mysipclient.SetNotificationListener(new MyNotificationListener()); //subscribe to media packets mysipclient.SetMediaListener(new MySIPMediaListener()); DisplayLogs("SIPStack Start"); //start the SIP engine mysipclient.Start(); //mysipclient.Register(); instance.CheckPermissions(); DisplayLogs("SIPStack Started"); } else { DisplayLogs("SIPStack already started"); } }catch (Exception e) { DisplayLogs("ERROR, StartSIPStack"); } } }); mBtnCall.setOnClickListener(new View.OnClickListener() { public void onClick(View v) //Call button click { DisplayLogs("Call on click"); String number = mDestNumber.getText().toString().trim(); if (number == null || number.length() < 1) { DisplayStatus("ERROR, Invalid destination number"); return; } if (mysipclient == null) { DisplayStatus("ERROR, cannot initiate call because SipStack is not started"); return; } instance.CheckPermissions(); if (mysipclient.Call(-1, number)) { /* optinal flags (you might set these also for incoming calls): getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if(Build.VERSION.SDK_INT >= 24) getWindow().setSustainedPerformanceMode(true); instance.setShowWhenLocked(true); instance.setTurnScreenOn(true); */ } } }); mBtnHangup.setOnClickListener(new View.OnClickListener() { public void onClick(View v) //Hangup button click { DisplayLogs("Hangup on click"); if (mysipclient == null) DisplayStatus("ERROR, cannot hangup because SipStack is not started"); else mysipclient.Hangup(); } }); mBtnTest.setOnClickListener(new View.OnClickListener() { public void onClick(View v) //Test button click { //any test code here if (mysipclient == null) { DisplayStatus("ERROR, SipStack not started"); } else { DisplayLogs("Toggle loudspeaker (while in call: "+Integer.toString(mysipclient.IsInCall())+")"); mysipclient.SetSpeakerMode(!mysipclient.IsLoudspeaker()); } } }); } /** * Pass the parameters to AJVoIP */ public void SetParameters() { String params = mParams.getText().toString(); if (params == null || mysipclient == null) return; params = params.trim(); DisplayLogs("SetParameters: " + params); mysipclient.SetParameters(params); //we could also set the parameters individually like: //mysipclient.SetParameter("loglevel",5); } /** * Write the collected audio buffer to a wav file */ void WriteWaveFile() { try{ if(audiobuffer == null || audiobufferlen < 1) return; int framecount = audiobufferlen/2; //recording in 16 bit mono, so two bytes means one frame DisplayLogs("Storing audio to stream.wav ("+ Integer.toString(framecount)+" frames)"); //We are using the WaveFile class from AJVoIP here to compose the correct wave header (same format as we sepecified with the sendmedia_aformat parameter) WavFile wavFile = WavFile.newWavFile(new java.io.File("stream.wav"), 1, framecount, 16, 16000, false); //the sample-rate (16000) must be the same as the configured sendmedia_aformat int[] framebuff = new int[2]; for(int i = 0; i < framecount; i++) { framebuff[0] = bytesToInt16(audiobuffer, i * 2); wavFile.writeFrames(framebuff, 1); } wavFile.close(); }catch(Throwable e) { DisplayLogs("ERROR, catch on WriteWaveFile"); } audiobufferlen = 0; audiobuffer = null; } /** * helper function to convert byte buffer to audio frame */ int bytesToInt16(byte[] buffer, int byteOffset) { return (buffer[byteOffset + 1] << 8) | (buffer[byteOffset] & 0xFF); } /** * Check for audio record permission */ void CheckPermissions() { if (Build.VERSION.SDK_INT >= 23 && ctx.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { //we might need RECORD_AUDIO permission before to make/receive any call if you wish to record audio from the local audio device DisplayStatus("Microphone permission required"); ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.RECORD_AUDIO}, 555); } } /** * You will receive the notification events from AJVoIP in this class by overriding the SIPNotificationListener base class member functions. * Don't block or wait in the overwritten functions for too long. * See the "Notifications" chapter in the documentation and/or the following javadoc for the details: * https://www.mizu-voip.com/Portals/0/Files/ajvoip_javadoc/index.html */ class MyNotificationListener extends SIPNotificationListener { //here are some examples about how to handle the notifications: @Override public void onAll(SIPNotification e) { //we receive all notifications (also) here. we just print them from here DisplayLogs(e.getNotificationTypeText()+" notification received: " + e.toString()); } //handle connection (REGISTER) state @Override public void onRegister( SIPNotification.Register e) { //check if/when we are registered to the SIP server if(!e.getIsMain()) return; //we ignore secondary accounts here switch(e.getStatus()) { case SIPNotification.Register.STATUS_INPROGRESS: DisplayStatus("Registering..."); break; case SIPNotification.Register.STATUS_SUCCESS: DisplayStatus("Registered successfully."); break; case SIPNotification.Register.STATUS_FAILED: DisplayStatus("Register failed because "+e.getReason()); break; case SIPNotification.Register.STATUS_UNREGISTERED: DisplayStatus("Unregistered."); break; } } //an example for STATUS handling @Override public void onStatus( SIPNotification.Status e) { if(e.getLine() == -1) return; //we are ignoring the global state here (but you might check only the global state instead or look for the particular lines separately if you must handle multiple simultaneous calls) //log call state if (e.getStatus() >= SIPNotification.Status.STATUS_CALL_SETUP && e.getStatus() <= SIPNotification.Status.STATUS_CALL_FINISHED) { DisplayStatus("Call state is: " + e.getStatusText()); } //catch outgoing call connect if (e.getStatus() == SIPNotification.Status.STATUS_CALL_CONNECT && e.getEndpointType() == SIPNotification.Status.DIRECTION_OUT) { DisplayStatus("Outgoing call connected to " + e.getPeer()); /* //there are many things we can do on call connect. for example: app.webphoneobj.API_Dtmf(e.getLine(),"1"); //send DTMF digit 1 app.webphoneobj.API_PlaySound(e.getLine(), "mysound.wav", 0, false, true, true, -1,"",false); //stream an audio file */ } //catch incoming calls else if (e.getStatus() == SIPNotification.Status.STATUS_CALL_RINGING && e.getEndpointType() == SIPNotification.Status.DIRECTION_IN) { DisplayStatus("Incoming call from " + e.getPeerDisplayname()); //auto accepting the incoming call (instead of auto accept, you might present an Accept/Reject button for the user which will call API_Accept / API_Reject) mysipclient.Accept(e.getLine()); } //catch incoming call connect else if (e.getStatus() == SIPNotification.Status.STATUS_CALL_CONNECT && e.getEndpointType() == SIPNotification.Status.DIRECTION_IN) { DisplayStatus("Incoming call connected"); } else if (e.getStatus() == SIPNotification.Status.STATUS_CALL_FINISHED) { DisplayStatus("Disconnected"); WriteWaveFile(); } } //print important events (EVENT) @Override public void onEvent( SIPNotification.Event e) { DisplayStatus("Important event: "+e.getText()); } //IM handling @Override public void onChat( SIPNotification.Chat e) { DisplayStatus("Message from "+e.getPeer()+": "+e.getMsg()); //auto answer mysipclient.SendChat(-1, e.getPeer(),"Received"); } /* Change the above function bodies after your app logic. Override other members if you need to handle more notifications as required for your use-case See the AJVoIP example for a temple to handle all members: https://www.mizu-voip.com/Portals/0/Files/AJVoIPTest.zip */ } /** * You will receive the Media events from JVoIP in this class by overriding the onMedia member function. * Don't block or wait in these functions for too long. * See the "Media" function below the "Notifications" chapter in the documentation for the details. */ class MySIPMediaListener extends SIPMediaListener { public MySIPMediaListener() { } //we receive all media packets here @Override public void onMedia(SIPNotification.Media packet) { try{ //app.Log("Media packet received: " + packet.toString()); if(packet.isEof()) { //app.WriteWaveFile(); return; //ignore } //echo back mysipclient.StreamSoundBuff(1,packet.getLine(),packet.getData(),packet.getLength(),packet.getRate()); //in this simple example we just collect the packets here and will write them in a wav file at the end if(audiobuffer == null) audiobuffer = new byte[1000000]; //set the default capacity to ~1MB if(audiobufferlen + packet.getLength() >= audiobuffer.length) { //expand buffer if not enough room byte[] tmpbuff = new byte[audiobufferlen]; System.arraycopy(audiobuffer, 0, tmpbuff, 0, audiobufferlen); audiobuffer = new byte[(audiobuffer.length+packet.getLength())*3]; System.arraycopy(tmpbuff, 0, audiobuffer, 0, audiobufferlen); } //append System.arraycopy(packet.getData(), 0, audiobuffer, audiobufferlen, packet.getLength()); audiobufferlen += packet.getLength(); }catch(Throwable e) { DisplayLogs("ERROR, onMedia "+e.getMessage()); } } } //eof MySIPMediaListener class public void DisplayStatus(String stat) { try{ if (stat == null) return; if (mStatus != null) { if ( stat.length() > 70) mStatus.setText(stat.substring(0,60)+"..."); else mStatus.setText(stat); } DisplayLogs(stat); }catch(Throwable e){ Log.e(LOGTAG, "ERROR, DisplayStatus", e); } } public void DisplayLogs(String logmsg) { try{ if (logmsg == null || logmsg.length() < 1) return; if ( logmsg.length() > 2500) logmsg = logmsg.substring(0,300)+"..."; logmsg = "["+ new java.text.SimpleDateFormat("HH:mm:ss:SSS").format(Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTime()) + "] " + logmsg + "\r\n"; Log.v(LOGTAG, logmsg); if (mNotifications != null) mNotifications.append(logmsg); }catch(Throwable e){ Log.e(LOGTAG, "ERROR, DisplayLogs", e); } } @Override public void onResume() { super.onResume(); } @Override protected void onDestroy() { try{ super.onDestroy(); DisplayLogs("ondestroy"); if (mysipclient != null) { DisplayLogs("Stop SipStack"); mysipclient.Stop(true); mysipclient = null; WriteWaveFile(); } }catch(Throwable e){ Log.e(LOGTAG, "ERROR, on destroy", e); } } }