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