/**
* Copyright (c) 2006 - 2010 Smaxe Ltd (www.smaxe.com).
* All rights reserved.
*/
import com.smaxe.uv.ProtocolLayerInfo;
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.client.rtmp.License;
import com.smaxe.uv.client.rtmp.NetConnection;
import com.smaxe.uv.client.rtmp.NetStream;
import com.smaxe.uv.stream.MediaDataFactory;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/**
* <code>ExRtmpDesktopPublisherX</code> - publishes part of Desktop screen to the RTMP server and
* provides means to manage upload bandwidth.
* <p> Note:
* <br> - This example encodes desktop using ScreenVideo codec implementation.
*
* @author Andrei Sochirca
*/
public final class ExRtmpDesktopPublisherX 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
License.setKey("SET-YOUR-KEY");
final String url = "rtmp://localhost:1935/live";
final DesktopCamera camera = new DesktopCamera(0 /*x*/, 0 /*y*/, 320 /*width*/, 240 /*height*/);
new Thread(new Runnable()
{
public void run()
{
Publisher publisher = new Publisher();
publisher.publish(url, "desktop", null /*microphone*/, camera);
}
}).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;
}
/**
* Starts the capture.
*/
public void start()
{
}
/**
* Stops the capture.
*/
public void stop()
{
}
/**
* Releases the capture resources.
*/
public void release()
{
active = false;
}
// Runnable implementation
public void run()
{
final int blockWidth = 32;
final int blockHeight = 32;
final int timeBetweenFrames = 100; // 1000 / frameRate
int frameCounter = 0;
try
{
Robot robot = new Robot();
byte[] previous = null;
while (active)
{
final long ctime = System.currentTimeMillis();
BufferedImage image = robot.createScreenCapture(new Rectangle(x, y, width, height));
byte[] current = toBGR(image);
try
{
final byte[] packet = encode(current, previous, blockWidth, blockHeight, width, height);
fireOnVideoData(MediaDataFactory.create(timeBetweenFrames, packet));
previous = current;
if (++frameCounter % 10 == 0) previous = null;
}
catch (Exception e)
{
e.printStackTrace();
}
final int spent = (int) (System.currentTimeMillis() - ctime);
Thread.sleep(Math.max(0, timeBetweenFrames - spent));
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
// inner use methods
/**
* @param image
* @return BGR image content
*/
private byte[] toBGR(BufferedImage image)
{
final int width = image.getWidth();
final int height = image.getHeight();
byte[] buf = new byte[3 * width * height];
final DataBuffer buffer = image.getData().getDataBuffer();
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
final int rgb = buffer.getElem(y * width + x);
final int offset = 3 * (y * width + x);
buf[offset + 0] = (byte) (rgb & 0xFF);
buf[offset + 1] = (byte) ((rgb >> 8) & 0xFF);
buf[offset + 2] = (byte) ((rgb >> 16) & 0xFF);
}
}
return buf;
}
/**
* Performs 'ScreenVideo' encode.
*
* @param current
* @param previous
* @param blockWidth
* @param blockHeight
* @param width
* @param height
* @return buffer
* @throws Exception
*/
private byte[] encode(final byte[] current, final byte[] previous, final int blockWidth, final int blockHeight, final int width, final int height) throws Exception
{
ByteArrayOutputStream baos = new ByteArrayOutputStream(16 * 1024);
if (previous == null)
{
baos.write(getTag(0x01 /*key-frame*/, 0x03 /*ScreenVideo codec*/));
}
else
{
baos.write(getTag(0x02 /*inter-frame*/, 0x03 /*ScreenVideo codec*/));
}
// 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;
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 = isChanged(current, previous, x0, y0, bwidth, bheight, width, height);
if (changed)
{
ByteArrayOutputStream blaos = new ByteArrayOutputStream(4 * 1024);
DeflaterOutputStream dos = new DeflaterOutputStream(blaos, deflater);
for (int y = 0; y < bheight; y++)
{
dos.write(current, 3 * ((y0 + bheight - y - 1) * width + x0), 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
{
// write DataSize
writeShort(baos, 0);
}
x0 += bwidth;
}
}
return baos.toByteArray();
}
/**
* 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);
}
/**
* Checks if image block is changed.
*
* @param current
* @param previous
* @param x0
* @param y0
* @param blockWidth
* @param blockHeight
* @param width
* @param height
* @return <code>true</code> if changed, otherwise <code>false</code>
*/
public boolean isChanged(final byte[] current, final byte[] previous, final int x0, final int y0, final int blockWidth, final int blockHeight, final int width, final int height)
{
if (previous == null) return true;
for (int y = y0, ny = y0 + blockHeight; y < ny; y++)
{
final int foff = 3 * (x0 + width * y);
final int poff = 3 * (x0 + width * y);
for (int i = 0, ni = 3 * blockWidth; i < ni; i++)
{
if (current[foff + i] != previous[poff + i]) return true;
}
}
return false;
}
/**
* @param frame
* @param codec
* @return tag
*/
public int getTag(final int frame, final int codec)
{
return ((frame & 0x0F) << 4) + ((codec & 0x0F) << 0);
}
}
// 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);
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 NetConnection} listener implementation.
*/
private final class NetConnectionListener extends NetConnection.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 (NetConnection.CONNECT_SUCCESS.equals(code))
{
}
else
if (NetConnection.CONNECT_BANDWIDTH.equals(code))
{
final ProtocolLayerInfo connectionInfo = (ProtocolLayerInfo) info.get("info");
final long serverReadBytes = (Long) info.get("acknowledgement");
final long uploadBufferSize = (Long) info.get("uploadBufferSize");
final long diff = connectionInfo.writtenBytes - serverReadBytes;
// TO DO
// keep track of diff... increasing over time means
// that outgoing bandwidth exceeds connection upload bandwidth
// keep track of uploadBufferSize.. increasing over time means
// that outgoing bandwidth exceeds NetConnection.Configuration.MAX_UPLOAD_BANDWIDTH property
}
else
{
disconnected = true;
}
}
}
// fields
private volatile boolean disconnected = false;
/**
* Publishes the stream.
*
* @param url
* @param streamName
* @param microphone microphone
* @param camera camera
*/
public void publish(final String url, final String streamName, final IMicrophone microphone, final DesktopCamera camera)
{
final NetConnection connection = new NetConnection();
connection.configuration().put(NetConnection.Configuration.INACTIVITY_TIMEOUT, -1);
connection.configuration().put(NetConnection.Configuration.RECEIVE_BUFFER_SIZE, 256 * 1024);
connection.configuration().put(NetConnection.Configuration.SEND_BUFFER_SIZE, 256 * 1024);
// BANDWIDTH MANAGEMENT
// 'Acknowledgement' event provides you with number of bytes read by the server
connection.configuration().put(NetConnection.Configuration.ENABLE_ACKNOWLEDGEMENT_EVENT_NOTIFICATION, true);
// Server notifies about number of read bytes each 128Kib (defined by property)
// Note: Wowza server doesn't support this feature and notifies client every 640Kib
connection.configuration().put(NetConnection.Configuration.WINDOW_ACKNOWLEDGEMENT_SIZE, 128 * 1024);
// if max upload bandwidth is defined then library tries to do not exceed this value
// by sending video frames in chunks (you can change it after connection
// is established using NetConnection#setMaxUploadBandwidth(bandwidth) method)
// Note: this feature is available in 1.5.7 or later
connection.configuration().put(NetConnection.Configuration.MAX_UPLOAD_BANDWIDTH, 32 * 1024 /*bytes per second*/);
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 NetStream stream = new NetStream(connection);
stream.addEventListener(new NetStream.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 (NetStream.PUBLISH_START.equals(code))
{
if (microphone != null)
{
stream.attachAudio(microphone);
}
if (camera != null)
{
stream.attachCamera(camera, -1 /*snapshotMilliseconds*/);
camera.start();
}
}
}
});
stream.publish(streamName, NetStream.LIVE);
}
while (!disconnected)
{
try
{
Thread.sleep(100);
}
catch (Exception e) {/*ignore*/}
}
connection.close();
}
}
}