AAccount AAccount - 5 months ago 45
Java Question

Force Java Android Socket to Send Data Immediately

As a hobby project, I'm writing an android voip client. When writing voice data to the socket (Vars.mediaSocket), many times, the data isn't immediately sent out over the wifi but just stalls and then all at once it will send 20 seconds worth of voice. Then it will stall again and wait for 30 seconds and then send 30 seconds of voice. The wait is not consistent but after a while it will continuously send voice data immediately. I've tried everything from using DataOutputStream to setting the socket output buffer size, setting the sendbuffer size huge, small, and lastly, buffering the voice data from its 32 byte chunks to anything from 128bytes to 32kb.

Utils.logcat(Const.LOGD, encTag, "MediaCodec encoder thread has started");
isEncoding = true;
byte[] amrbuffer = new byte[32];
short[] wavbuffer = new short[160];
int outputCounter = 0;

//setup the wave audio recorder. since it is released and restarted, it needs to be setup here and not onCreate
wavRecorder = null; //remove pointer to the old recorder for safety
wavRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLESWAV, AudioFormat.CHANNEL_IN_MONO, FORMAT, 160);
wavRecorder.startRecording();
AmrEncoder.init(0);

while(!micMute)
{
int totalRead = 0, dataRead;
while(totalRead < 160)
{//although unlikely to be necessary, buffer the mic input
dataRead = wavRecorder.read(wavbuffer, totalRead, 160 - totalRead);
totalRead = totalRead + dataRead;
}
int encodeLength = AmrEncoder.encode(AmrEncoder.Mode.MR122.ordinal(), wavbuffer, amrbuffer);

try
{
Vars.mediaSocket.getOutputStream().write(amrbuffer);
Vars.mediaSocket.getOutputStream().flush();
}
catch (IOException i)
{
Utils.logcat(Const.LOGE, encTag, "Cannot send amr out the media socket");
Utils.dumpException(tag, i);
}


Is there something I'm missing? To simulate a second cell phone, I have another client which just simply reads the voice data, throws it away, and reads again in a loop. I can confirm in the simulated second cell phone when the real cell phone stops sending voice, the simulated one's socket.read hangs until the real one starts sending voice again.

I'm really hoping not to have to write a jni for the socket as I don't know anything about that and was hoping I could write the app as a standard java app.

CASE CLOSED: turned out to be a server side bug but the simplifying back to basics suggestions is still a good idea.

EJP EJP
Answer

You are adding most of the latency yourself by reading large amounts of data before writing any of it. You should just use the standard Java copy loop:

byte[] buffer = new byte[8192];
int count;
while ((count = in.read(buffer)) > 0)
{
    out.write(buffer, 0, count);
}

You need to adapt this to incorporate your codec step. Note that you don't need a buffer the size of the entire input. You can tune its size to suit yourself but 8192 is a good starting point. You can increase it to say 32k but don't decrease it. If your codec needs the data in fixed-size chunks, use a buffer of that size and DataInputStream.readFully(). But the larger the buffer the more the latency.

EDIT Specific issues with your code:

byte[] amrbuffer = new byte[AMRBUFFERSIZE];
byte[] outputbuffer = new byte [outputBufferSize];

Remove (see below).

short[] wavbuffer = new short[WAVBUFFERSIZE];
int outputCounter = 0;

Remove outputCounter.

        //setup the wave audio recorder. since it is released and restarted, it needs to be setup here and not onCreate
        wavRecorder = null; //remove pointer to the old recorder for safety

Pointless. Remove.

        wavRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLESWAV, AudioFormat.CHANNEL_IN_MONO, FORMAT, WAVBUFFERSIZE);
        wavRecorder.startRecording();
        AmrEncoder.init(0);

OK.

        try
        {
            Vars.mediaSocket.setSendBufferSize(outputBufferSize);
        }
        catch (SocketException e)
        {
            e.printStackTrace();
        }

Pointless. Remove. The socket send buffer should be as large as possible. Unless you know that its default size is < outputBufferSize there is no benefit to this. In any case we are getting rid of outputBuffer altogether.

        while(!micMute)
        {
            int totalRead = 0, dataRead;
            while(totalRead < WAVBUFFERSIZE)
            {//although unlikely to be necessary, buffer the mic input
                dataRead = wavRecorder.read(wavbuffer, totalRead, WAVBUFFERSIZE - totalRead);
                totalRead = totalRead + dataRead;
            }
            int encodeLength = AmrEncoder.encode(AmrEncoder.Mode.MR122.ordinal(), wavbuffer, amrbuffer);

OK.

            if(outputCounter == outputBufferSize)
            {
                Utils.logcat(Const.LOGD, encTag, "Sending output buffer");
                try
                {
                    Vars.mediaSocket.getOutputStream().write(outputbuffer);
                    Vars.mediaSocket.getOutputStream().flush();
                }
                catch (IOException i)
                {
                    Utils.logcat(Const.LOGE, encTag, "Cannot send amr out the media socket");
                    Utils.dumpException(tag, i);
                }
                outputCounter = 0;
            }
            System.arraycopy(amrbuffer, 0, outputbuffer, outputCounter, encodeLength);
            outputCounter = outputCounter + encodeLength;
            Utils.logcat(Const.LOGD, encTag, "Output buffer fill: " + outputCounter);

Remove all the above and substitute

        Vars.mediaSocket.getOutputStream().write(amrbuffer, 0, encodeLength);

This also means you can get rid of 'outputBuffer' as promised.

NB Don't flush inside loops. As a matter of fact flushing a socket output stream does nothing, but the general principle still holds.

Comments