/**
* Copyright (c) 2006 - 2009 Smaxe Ltd (www.smaxe.com).
* All rights reserved.
*/
import com.smaxe.uv.client.ICamera;
import com.smaxe.uv.client.IMicrophone;
import com.smaxe.uv.client.microphone.AbstractMicrophone;
import com.smaxe.uv.client.rtmp.INetConnection;
import com.smaxe.uv.client.rtmp.INetStream;
import com.smaxe.uv.stream.MediaDataFactory;
import lt.dkd.nellymoser.CodecImpl;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.TargetDataLine;
/**
* <code>ExRtmpVoicePublisher</code> - publishes captured voice to the RTMP server.
* <p> Note:
* <br> - It encodes audio using Nellymoser ASAO codec implementation mentioned
* at http://ffmpeg.org/ (June 16, 2008 news).
* <br> - Nellymoser ASAO codec is available at <a href="http://www.smaxe.com/source.jsf?id=lt/dkd/nellymoser/CodecImpl.java" target="_blank">Nellymoser ASAO (Java class)</a>
* <br> - Desktop publisher example is available at <a href="http://www.smaxe.com/source.jsf?id=ExDesktopPublisher.java" target="_blank">Desktop publisher (Java class)</a>
*
* @author Andrei Sochirca
* @see <a href="http://www.smaxe.com/product.jsf?id=juv-rtmp-client" target="_blank">JUV RTMP Client</a>
*/
public final class ExRtmpVoicePublisher extends Object
{
/**
* Entry point.
*
* @param args
* @throws Exception if an exception occurred
*/
public static void main(final String[] args) throws Exception
{
// NOTE:
// you can get Evaluation Key at:
// http://www.smaxe.com/order.jsf#request_evaluation_key
// or buy at:
// http://www.smaxe.com/order.jsf
// Android-specific:
// - please add permission to the AndroidManifest.xml :
// <uses-permission android:name="android.permission.INTERNET" />
// - please use separate thread to connect to the server (not UI thread) :
// NetConnection#connect() connects to the remote server in the invocation thread,
// so it causes NetworkOnMainThreadException on 4.0 (http://developer.android.com/reference/android/os/NetworkOnMainThreadException.html)
// Nginx-specific:
// - please use AMF0 encoding
com.smaxe.uv.client.rtmp.License.setKey("SET-YOUR-KEY");
final String url = "rtmp://localhost:1935/live";
final String stream = "voice";
final Microphone microphone = new Microphone();
microphone.start();
new Thread(new Runnable()
{
public void run()
{
final Publisher publisher = new Publisher();
// RTMP example
publisher.publish(new com.smaxe.uv.client.rtmp.NetConnection(), url, stream, microphone, null /*camera*/);
// RTMFP example
// Note: This classes are available in the JUV RTMFP/RTMP Client library (juv-rtmfp-client-*.jar)
// com.smaxe.uv.client.rtmfp.License.setKey("SET-YOUR-KEY");
//
// publisher.publish(new com.smaxe.uv.client.rtmfp.NetConnection(), url, stream, microphone, null /*camera*/);
}
}, "ExRtmpVoicePublisher-Publisher").start();
}
/**
* <code>Microphone</code> - {@link IMicrophone} implementation that captures audio device stream.
*/
public final static class Microphone extends AbstractMicrophone
{
/**
* <code>CaptureRunnable</code> - {@link Runnable} implementation
* that captures audio.
*/
private final class CaptureRunnable extends Object implements Runnable
{
// fields
private volatile boolean stopped = false;
private ExecutorService executor = null;
/**
* Constructor.
*/
public CaptureRunnable()
{
executor = Executors.newSingleThreadExecutor();
}
/**
* Starts the capture.
*/
public void start()
{
stopped = false;
}
/**
* Stops the capture.
*/
public void stop()
{
stopped = true;
}
/**
* Releases the capture resources.
*/
public void release()
{
executor.shutdown();
}
// Runnable implementation
public void run()
{
final AudioFormat audioFormat = new AudioFormat(8000f /*sample rate*/,
16 /*sample size in bits*/, 1 /*channels*/, true /*signed*/, false /*big endian*/);
final DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
try
{
final TargetDataLine targetDataLine = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
// opens line if necessary
if (!targetDataLine.isOpen())
{
targetDataLine.open();
}
// starts data line
targetDataLine.start();
Thread capture = new Thread(new Runnable()
{
public void run()
{
while (!stopped)
{
byte[] buf = new byte[512];
int offset = 0;
while (offset < buf.length)
{
offset += targetDataLine.read(buf, offset, buf.length - offset);
}
encode(buf);
}
}
});
capture.start();
while (!stopped)
{
try
{
Thread.sleep(1 * 1000);
}
catch (Exception e) {/*ignore*/}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
// inner use methods
private final float[] state = new float[64];
/**
* Encodes audio data.
*
* @param audio
*/
private void encode(final byte[] audio)
{
executor.execute(new Runnable()
{
public void run()
{
byte[] encoded = new byte[64];
CodecImpl.encode(state, toFloats(audio), encoded);
byte[] data = new byte[1 + encoded.length];
data[0] = 82;// Audio.TAG_NELLYMOSER;
System.arraycopy(encoded, 0, data, 1, encoded.length);
fireOnAudioData(MediaDataFactory.create(32 /*rtime*/, data));
}
});
}
/**
* @param bytes
* @return floats
*/
private float[] toFloats(final byte[] bytes)
{
float[] floats = new float[bytes.length >> 1];
for (int i = 0, n = floats.length; i < n; i++)
{
floats[i] = (bytes[i * 2 + 1] << 8) + (bytes[i * 2 + 0] << 0);
}
return floats;
}
}
// fields
private CaptureRunnable capture = null;
private Thread t = null;
/**
* Constructor.
*/
public Microphone()
{
capture = new CaptureRunnable();
}
/**
* Starts audio capture.
*/
public void start()
{
if (t == null)
{
t = new Thread(capture);
t.start();
}
capture.start();
}
/**
* Stops video capture.
*/
public void stop()
{
capture.stop();
}
/**
* Releases the resources.
*/
public void release()
{
capture.release();
t = null;
}
}
/**
* <code>Publisher</code> - publisher.
*/
public static final class Publisher extends Object
{
/**
* <code>NetConnectionListener</code> - {@link INetConnection} listener implementation.
*/
private final class NetConnectionListener extends INetConnection.ListenerAdapter
{
/**
* Constructor.
*/
public NetConnectionListener()
{
}
@Override
public void onAsyncError(final INetConnection source, final String message, final Exception e)
{
System.out.println("Publisher#NetConnection#onAsyncError: " + message + " " + e);
}
@Override
public void onIOError(final INetConnection source, final String message)
{
System.out.println("Publisher#NetConnection#onIOError: " + message);
}
@Override
public void onNetStatus(final INetConnection source, final Map<String, Object> info)
{
System.out.println("Publisher#NetConnection#onNetStatus: " + info);
final Object code = info.get("code");
if (INetConnection.CONNECT_SUCCESS.equals(code))
{
}
else
{
disconnected = true;
}
}
}
// fields
private volatile boolean disconnected = false;
/**
* Publishes the stream.
*
* @param connection
* @param url
* @param streamName
* @param microphone microphone
* @param camera camera
*/
public void publish(final INetConnection connection, final String url, final String streamName, final IMicrophone microphone, final ICamera camera)
{
connection.addEventListener(new NetConnectionListener());
connection.connect(url);
// wait till connected
while (!connection.connected() && !disconnected)
{
try
{
Thread.sleep(100);
}
catch (Exception e) {/*ignore*/}
}
if (!disconnected)
{
final INetStream stream = connection.createNetStream();
stream.addEventListener(new INetStream.ListenerAdapter()
{
@Override
public void onNetStatus(final INetStream source, final Map<String, Object> info)
{
System.out.println("Publisher#NetStream#onNetStatus: " + info);
final Object code = info.get("code");
if (INetStream.PUBLISH_START.equals(code))
{
if (microphone != null)
{
stream.attachAudio(microphone);
}
if (camera != null)
{
stream.attachCamera(camera, -1 /*snapshotMilliseconds*/);
}
}
}
});
stream.publish(streamName, INetStream.LIVE);
}
while (!disconnected)
{
try
{
Thread.sleep(100);
}
catch (Exception e) {/*ignore*/}
}
connection.close();
}
}
}