/**
 * Copyright (c) 2006 - 2009 Smaxe Ltd (www.smaxe.com).
 * All rights reserved.
 */

import com.smaxe.io.ByteArray;
import com.smaxe.uv.client.ICamera;
import com.smaxe.uv.client.IMicrophone;
import com.smaxe.uv.client.camera.AbstractCamera;
import com.smaxe.uv.client.rtmp.INetConnection;
import com.smaxe.uv.client.rtmp.INetStream;
import com.smaxe.uv.stream.MediaDataFactory;

import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

/**
 * <code>ExRtmpDesktopPublisher</code> - publishes part of Desktop screen to the RTMP server.
 * <p> Note:
 * <br> - This example encodes desktop using ScreenVideo codec implementation.
 * <br> - Voice publisher example is available at <a href="http://www.smaxe.com/source.jsf?id=ExRtmpVoicePublisher.java" target="_blank">Voice publisher (Java class)</a>
 * <br> - ExRtmpDesktopPublisher eXtension that adds upload bandwidth management is available at
 * <a href="http://www.smaxe.com/source.jsf?id=ExRtmpDesktopPublisherX.java" target="_blank">ExRtmpDesktopPublisher eXtenstion (Java class)</a>
 * <br> - Desktop Publisher example based on ScreenVideo2 codec is provided to customers for free during support period.
 * 
 * @author Andrei Sochirca
 * @see <a href="http://www.smaxe.com/product.jsf?id=juv-rtmp-client" target="_blank">JUV RTMP Client</a>
 * @see <a href="http://www.smaxe.com/product.jsf?id=juv-rtmfp-client" target="_blank">JUV RTMFP/RTMP Client</a>
 */
public final class ExRtmpDesktopPublisher 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)
        
        // YouTube-specific:
        // - YouTube requires both audio and video stream
        com.smaxe.uv.client.rtmp.License.setKey("SET-YOUR-KEY");
        
        
        final String url = "rtmp://localhost:1935/live";
        final String stream = "desktop";
        
        final DesktopCamera camera = new DesktopCamera(0 /*x*/, 0 /*y*/, 320 /*width*/, 240 /*height*/);
        
        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, null /*microphone*/, 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, null /*microphone*/, camera);
            }
        }, "ExRtmpDesktopPublisher-Publisher").start();
    }
    
    /**
     * <code>DesktopCamera</code> - {@link ICamera} implementation that captures desktop.
     * 
     * @author Andrei Sochirca
     */
    public final static class DesktopCamera extends AbstractCamera
    {
        /**
         * <code>CaptureRunnable</code> - {@link Runnable} implementation
         * that captures desktop.
         */
        private final class CaptureRunnable extends Object implements Runnable
        {
            // fields
            private volatile int x = 0;
            private volatile int y = 0;
            private volatile int width = 320;
            private volatile int height = 240;
            
            private volatile boolean active = true;
            
            private Deflater deflater = new Deflater();
            
            /**
             * Constructor.
             * 
             * @param x
             * @param y
             * @param width
             * @param height
             */
            public CaptureRunnable(final int x, final int y, final int width, final int height)
            {
                this.x = x;
                this.y = y;
                this.width = width;
                this.height = height;
            }
            
            /**
             * Sets the origin.
             * 
             * @param x
             * @param y
             */
            public void setOrigin(final int x, final int y)
            {
                this.x = x;
                this.y = y;
            }
            
            /**
             * Releases the capture resources.
             */
            public void release()
            {
                active = false;
            }
            
            // Runnable implementation
            
            public void run()
            {
                final int blockWidth = 32;
                final int blockHeight = 32;
                final float frameRate = 5.f;
                
                long frames = 0;
                
                try
                {
                    final Robot robot = new Robot();
                    
                    final long itime = System.nanoTime();
                    
                    long duration = 0;
                    int[] prgb = null;
                    
                    while (active)
                    {
                        final long ctime = System.nanoTime();
                        final long mediaTimestamp = (ctime - itime) / 1000000;
                        
                        try
                        {
                            final int[] rgb = toRGB(robot.createScreenCapture(new Rectangle(x, y, width, height)));
                            final byte[] packet = encode(rgb, prgb, width, height, blockWidth, blockHeight);
                            
                            if (packet != null)
                            {
                                fireOnVideoData(MediaDataFactory.create((int) (mediaTimestamp - duration), mediaTimestamp,
                                        new ByteArray(packet)));
                            }
                            
                            duration = mediaTimestamp;
                            prgb = rgb;
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                        
                        if (frames++ > 0)
                        {
                            Thread.sleep(Math.max((int) (((frames - 1) * 1000000000d / frameRate) - ctime + itime) / 1000000, 10));
                        }
                        
                        if (frames % 20 == 0) prgb = null;
                    }
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
            
            // inner use methods
            /**
             * Encodes the frame.
             * 
             * @param rgb frame rgb
             * @param width frame width
             * @param height frame height
             * @param prgb previous rgb data
             * @return encoded frame bytes, <code>null</code> if frame wasn't changed
             * @throws Exception if an exception occurred
             */
            private byte[] encode(int[] rgb, int[] prgb, final int width, final int height, final int blockWidth, final int blockHeight) throws Exception
            {
                if (prgb != null && prgb.length != rgb.length) prgb = null;
                
                boolean isKeyFrame = true;
                
                ByteArrayOutputStream baos = new ByteArrayOutputStream(64 * 1024);
                
                // tag byte will be replaced later
                baos.write(0 /* tag */);
                
                // write header
                final int wh = width + ((blockWidth / 16 - 1) << 12);
                final int hh = height + ((blockHeight / 16 - 1) << 12);
                
                writeShort(baos, wh);
                writeShort(baos, hh);
                
                // write content
                int y0 = height;
                int x0 = 0;
                int bwidth = blockWidth;
                int bheight = blockHeight;
                byte[] buf = new byte[3 * blockWidth];
                int changedBlocks = 0;
                
                while (y0 > 0)
                {
                    bheight = Math.min(y0, blockHeight);
                    y0 -= bheight;
                    
                    bwidth = blockWidth;
                    x0 = 0;
                    
                    while (x0 < width)
                    {
                        bwidth = (x0 + blockWidth > width) ? width - x0 : blockWidth;
                        
                        final boolean changed = isImageBlockChanged(rgb, prgb, width, height, x0, y0, bwidth, bheight);
                        
                        if (changed)
                        {
                            changedBlocks++;
                            
                            ByteArrayOutputStream blaos = new ByteArrayOutputStream(4 * 1024);
                            
                            DeflaterOutputStream dos = new DeflaterOutputStream(blaos, deflater);
                            
                            for (int y = 0; y < bheight; y++)
                            {
                                for (int offset = (y0 + bheight - y - 1) * width + x0, i = 0; i < bwidth; i++)
                                {
                                    int pixel = rgb[offset + i];
                                    
                                    buf[3 * i + 0] = (byte) (pixel & 0xFF);
                                    buf[3 * i + 1] = (byte) ((pixel >> 8) & 0xFF);
                                    buf[3 * i + 2] = (byte) ((pixel >> 16) & 0xFF);
                                }
                                
                                dos.write(buf, 0, 3 * bwidth);
                            }
                            
                            dos.finish();
                            deflater.reset();
                            
                            final byte[] bbuf = blaos.toByteArray();
                            final int written = bbuf.length;
                            
                            // write DataSize
                            writeShort(baos, written);
                            // write Data
                            baos.write(bbuf, 0, written);
                        }
                        else
                        {
                            isKeyFrame = false;
                            // write DataSize
                            writeShort(baos, 0);
                        }
                        
                        x0 += bwidth;
                    }
                }
                
                if (changedBlocks == 0) return null;
                
                byte[] data = baos.toByteArray();
                
                data[0] = (byte) getTag(isKeyFrame ? 0x01 /*key-frame*/ : 0x02 /*inter-frame*/, 0x03 /*ScreenVideo codec*/);
                
                return data;
            }
            
            /**
             * Writes short value to the {@link OutputStream <tt>os</tt>}.
             * 
             * @param os
             * @param n
             * @throws Exception if an exception occurred
             */
            private void writeShort(OutputStream os, final int n) throws Exception
            {
                os.write((n >> 8) & 0xFF);
                os.write((n >> 0) & 0xFF);
            }
            
            /**
             * @param frame
             * @param codec
             * @return tag
             */
            private int getTag(final int frame, final int codec)
            {
                return ((frame & 0x0F) << 4) + ((codec & 0x0F) << 0);
            }
            
            /**
             * Checks if image block is changed.
             * 
             * @param rgb current RGB frame
             * @param prgb current RGB frame
             * @param width frame width
             * @param height frame height
             * @param x0
             * @param y0
             * @param blockWidth
             * @param blockHeight
             * @return <code>true</code> if changed, otherwise <code>false</code>
             */
            private boolean isImageBlockChanged(int[] rgb, int[] prgb,
                    int width, int height, int x0, int y0, int blockWidth, int blockHeight)
            {
                if (prgb == null) return true;
                
                for (int y = Math.min(y0 + blockHeight - 1, height - 1); y >= y0; y--)
                {
                    for (int x = x0, xn = Math.min(x0 + blockHeight, width); x < xn; x++)
                    {
                        final int off = x + width * y;
                        
                        if (rgb[off] != prgb[off]) return true;
                    }
                }
                
                return false;
            }
            
            /**
             * @param image
             * @return RGB image content
             */
            private int[] toRGB(BufferedImage image)
            {
                return ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
            }
        }
        
        // fields
        private CaptureRunnable capture = null;
        private Thread t = null;
        
        /**
         * Constructor.
         */
        public DesktopCamera()
        {
            this(0 /*x*/, 0 /*y*/, 320 /*width*/, 240 /*height*/);
        }
        
        /**
         * Constructor.
         * 
         * @param x
         * @param y
         * @param width
         * @param height
         */
        public DesktopCamera(final int x, final int y, final int width, final int height)
        {
            capture = new CaptureRunnable(x, y, width, height);
        }
        
        /**
         * Starts desktop capture.
         */
        public void start()
        {
            if (t == null)
            {
                t = new Thread(capture, "ExRtmpDesktopPublisher-DesktopCamera");
                t.start();
            }
        }
        
        /**
         * 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 DesktopCamera 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*/);
                                
                                camera.start();
                            }
                        }
                    }
                });
                
                
                stream.publish(streamName, INetStream.LIVE);
                
                while (!disconnected)
                {
                    try
                    {
                        Thread.sleep(1000);
                    }
                    catch (Exception e) {/*ignore*/}
                }
            }
            
            connection.close();
            camera.release();
        }
    }
}