package org.deviceconnect.android.deviceplugin.webrtc.core; import android.content.Context; import android.media.AudioManager; import android.util.Log; import org.deviceconnect.android.deviceplugin.webrtc.BuildConfig; import org.deviceconnect.server.DConnectServerConfig; import org.deviceconnect.server.DConnectServerError; import org.deviceconnect.server.DConnectServerEventListener; import org.deviceconnect.server.http.HttpRequest; import org.deviceconnect.server.http.HttpResponse; import org.deviceconnect.server.nanohttpd.DConnectServerNanoHttpd; import org.deviceconnect.server.websocket.DConnectWebSocket; import org.webrtc.voiceengine.WebRtcAudioTrackModule; import java.nio.ByteBuffer; import java.util.Map; import java.util.UUID; public class AudioTrackExternal extends WebRtcAudioTrackModule { private static final boolean DEBUG = BuildConfig.DEBUG; private static final String TAG = "WebAudioTrack"; private static final String MIME_TYPE = "audio/wav"; private final AudioManager mAudioManager; private int mSampleRate = 0; private int mChannels = 0; // Default audio data format is PCM 16 bit per sample. // Guaranteed to be supported by all devices. private static final int BITS_PER_SAMPLE = 16; // Requested size of each recorded buffer provided to the client. private static final int CALLBACK_BUFFER_SIZE_MS = 10; // Average number of callbacks per second. private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS; private ByteBuffer mByteBuffer; private AudioTrackThread mTrackThread; private final DConnectServerNanoHttpd mServerNanoHttpd; /** * path. */ private String mPath = "/remote/audio/" + UUID.randomUUID().toString(); public AudioTrackExternal(final Context context, final int port) { mAudioManager = (AudioManager) context.getSystemService( Context.AUDIO_SERVICE); DConnectServerConfig.Builder builder = new DConnectServerConfig.Builder(); builder.port(port).isSsl(false) .cachePath(context.getCacheDir().getAbsolutePath()) .documentRootPath(context.getFilesDir().getAbsolutePath()); mServerNanoHttpd = new DConnectServerNanoHttpd(builder.build(), context); mServerNanoHttpd.setServerEventListener(new DConnectServerEventListener() { @Override public boolean onReceivedHttpRequest(final HttpRequest req, final HttpResponse res) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#onReceivedHttpRequest: "); } return true; } @Override public void onError(final DConnectServerError errorCode) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#onError: " + errorCode); } shutdownWebSocketServer(); } @Override public void onServerLaunched() { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#onServerLaunched: "); } } @Override public void onWebSocketConnected(final DConnectWebSocket webSocket) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#onWebSocketConnected: " + webSocket); } if (!webSocket.getUri().equals(mPath)) { webSocket.disconnect(); } } @Override public void onWebSocketDisconnected(final DConnectWebSocket webSocket) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#onWebSocketDisconnected: "); } } @Override public void onWebSocketMessage(final DConnectWebSocket webSocket, final String message) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#onWebSocketMessage: "); } } }); mServerNanoHttpd.start(); } @Override public void initPlayout(int sampleRate, int channels) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#initPlayout " + sampleRate + ", " + channels); } int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8); int bytesSize = bytesPerFrame * (sampleRate / BUFFERS_PER_SECOND); mByteBuffer = ByteBuffer.allocateDirect(bytesSize); cacheDirectBufferAddress(mByteBuffer); mSampleRate = sampleRate; mChannels = channels; if (DEBUG) { Log.i(TAG, "bytesPerFrame=" + bytesPerFrame); Log.i(TAG, "bytesSize=" + bytesSize); } } public String getUrl() { return "http://localhost:" + mServerNanoHttpd.getConfig().getPort() + mPath; } public String getMimeType() { return MIME_TYPE; } public int getSampleRate() { return mSampleRate; } public int getChannels() { return mChannels; } public int getSampleSize() { return BITS_PER_SAMPLE; } public int getBlockSize() { return (mChannels * (BITS_PER_SAMPLE / 8)) * (mSampleRate / BUFFERS_PER_SECOND); } @Override public boolean startPlayout() { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#startPlayout "); } if (mTrackThread != null) { mTrackThread.joinThread(); mTrackThread = null; } mTrackThread = new AudioTrackThread("WebRtcAudio-AudioTrackExternal"); mTrackThread.start(); return true; } @Override public boolean stopPlayout() { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#stopPlayout "); } if (mTrackThread != null) { mTrackThread.joinThread(); mTrackThread = null; } shutdownWebSocketServer(); return true; } @Override public int getStreamMaxVolume() { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#getStreamMaxVolume "); } return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL); } @Override public boolean setStreamVolume(int volume) { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#setStreamVolume " + volume); } return true; } @Override public int getStreamVolume() { if (DEBUG) { Log.i(TAG, "AudioTrackExternal#getStreamVolume "); } return mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); } private void shutdownWebSocketServer() { mServerNanoHttpd.shutdown(); } private class AudioTrackThread extends Thread { private volatile boolean keepAlive = true; public AudioTrackThread(String name) { super(name); } @Override public void run() { android.os.Process.setThreadPriority( android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); if (DEBUG) { Log.i(TAG, "AudioTrackExternal server is started."); } final int T = 4; final int sizeInBytes = mByteBuffer.capacity(); final byte[] buf = new byte[sizeInBytes * T]; final int delayTime = T * CALLBACK_BUFFER_SIZE_MS; int count = 0; while (keepAlive) { long time = System.currentTimeMillis(); getPlayoutData(sizeInBytes); System.arraycopy(mByteBuffer.array(), mByteBuffer.arrayOffset(), buf, count * sizeInBytes, sizeInBytes); count++; if (count == T) { Map<String, DConnectWebSocket> webSocketMap = mServerNanoHttpd.getWebSockets(); for (Map.Entry<String, DConnectWebSocket> e : webSocketMap.entrySet()) { e.getValue().sendMessage(buf); } if ((System.currentTimeMillis() - time) < delayTime) { try { Thread.sleep(System.currentTimeMillis() - time); } catch (InterruptedException e) { break; } } count = 0; } mByteBuffer.rewind(); } if (DEBUG) { Log.i(TAG, "AudioTrackExternal server is stopped."); } } public void joinThread() { keepAlive = false; interrupt(); while (isAlive()) { try { join(); } catch (InterruptedException e) { // Ignore. } } } } }