/**
 * 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();
        }
    }
}