package com.gvaneyck.rtmp.encoding;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Encodes AMF3 data and packets for RTMP
*
* @author Gabriel Van Eyck
*/
public class AMF3Encoder {
/** RNG used for generating MessageIDs */
private static Random rand = new Random();
/** Used for generating timestamps in headers */
private long startTime = System.currentTimeMillis();
/**
* Adds headers to provided data
*
* @param data
* @return The data with headers added
*/
public byte[] addHeaders(byte[] data) {
List<Byte> result = new ArrayList<Byte>();
// Header byte
result.add((byte)0x03);
// Timestamp
long timediff = System.currentTimeMillis() - startTime;
result.add((byte)((timediff & 0xFF0000) >> 16));
result.add((byte)((timediff & 0x00FF00) >> 8));
result.add((byte)(timediff & 0x0000FF));
// Body size
result.add((byte)((data.length & 0xFF0000) >> 16));
result.add((byte)((data.length & 0x00FF00) >> 8));
result.add((byte)(data.length & 0x0000FF));
// Content type
result.add((byte)0x11);
// Source ID
result.add((byte)0x00);
result.add((byte)0x00);
result.add((byte)0x00);
result.add((byte)0x00);
// Add body
for (int i = 0; i < data.length; i++) {
result.add(data[i]);
if (i % 128 == 127 && i != data.length - 1)
result.add((byte)0xC3);
}
byte[] ret = new byte[result.size()];
for (int i = 0; i < ret.length; i++)
ret[i] = result.get(i);
return ret;
}
/**
* Encodes the given parameters as a connect packet
*
* @param params The connection parameters
* @return The connection packet
* @throws NotImplementedException
* @throws EncodingException
*/
public byte[] encodeConnect(Map<String, Object> params) throws EncodingException, NotImplementedException {
List<Byte> result = new ArrayList<Byte>();
writeStringAMF0(result, "connect");
writeIntAMF0(result, 1); // invokeId
// Write params
result.add((byte)0x11); // AMF3 object
result.add((byte)0x09); // Array
writeAssociativeArray(result, params);
// Write service call args
result.add((byte)0x01);
result.add((byte)0x00); // false
writeStringAMF0(result, "nil"); // "nil"
writeStringAMF0(result, ""); // ""
// Set up CommandMessage
TypedObject cm = new TypedObject("flex.messaging.messages.CommandMessage");
cm.put("messageRefType", null);
cm.put("operation", 5);
cm.put("correlationId", "");
cm.put("clientId", null);
cm.put("destination", "");
cm.put("messageId", randomUID());
cm.put("timestamp", 0d);
cm.put("timeToLive", 0d);
cm.put("body", new TypedObject());
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("DSMessagingVersion", 1d);
headers.put("DSId", "my-rtmps");
cm.put("headers", headers);
// Write CommandMessage
result.add((byte)0x11); // AMF3 object
encode(result, cm);
byte[] ret = new byte[result.size()];
for (int i = 0; i < ret.length; i++)
ret[i] = result.get(i);
ret = addHeaders(ret);
ret[7] = (byte)0x14; // Change message type
return ret;
}
/**
* Encodes the given data as a connect packet
*
* @param id The invoke ID
* @param params The data to invoke
* @return The invoke packet
* @throws NotImplementedException
* @throws EncodingException
*/
public byte[] encodeInvoke(int id, Object data) throws EncodingException, NotImplementedException {
List<Byte> result = new ArrayList<Byte>();
result.add((byte)0x00); // version
result.add((byte)0x05); // type?
writeIntAMF0(result, id); // invoke ID
result.add((byte)0x05); // ???
result.add((byte)0x11); // AMF3 object
encode(result, data);
byte[] ret = new byte[result.size()];
for (int i = 0; i < ret.length; i++)
ret[i] = result.get(i);
ret = addHeaders(ret);
return ret;
}
/**
* Encodes an object as AMF3
*
* @param obj The object to encode
* @return The encoded object
* @throws NotImplementedException
* @throws EncodingException
*/
public byte[] encode(Object obj) throws EncodingException, NotImplementedException {
List<Byte> result = new ArrayList<Byte>();
encode(result, obj);
byte[] ret = new byte[result.size()];
for (int i = 0; i < ret.length; i++)
ret[i] = result.get(i);
return ret;
}
/**
* Encodes an object as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param obj The object to encode
* @throws EncodingException
* @throws NotImplementedException
*/
@SuppressWarnings("unchecked")
public void encode(List<Byte> ret, Object obj) throws EncodingException, NotImplementedException {
if (obj == null) {
ret.add((byte)0x01);
}
else if (obj instanceof Boolean) {
boolean val = (Boolean)obj;
if (val)
ret.add((byte)0x03);
else
ret.add((byte)0x02);
}
else if (obj instanceof Integer) {
ret.add((byte)0x04);
writeInt(ret, (Integer)obj);
}
else if (obj instanceof Double) {
ret.add((byte)0x05);
writeDouble(ret, (Double)obj);
}
else if (obj instanceof String) {
ret.add((byte)0x06);
writeString(ret, (String)obj);
}
else if (obj instanceof Date) {
ret.add((byte)0x08);
writeDate(ret, (Date)obj);
}
// Must precede Object[] check
else if (obj instanceof Byte[]) {
ret.add((byte)0x0C);
writeByteArray(ret, (byte[])obj);
}
else if (obj instanceof Object[]) {
ret.add((byte)0x09);
writeArray(ret, (Object[])obj);
}
// Must precede Map check
else if (obj instanceof TypedObject) {
ret.add((byte)0x0A);
writeObject(ret, (TypedObject)obj);
}
else if (obj instanceof Map) {
ret.add((byte)0x09);
writeAssociativeArray(ret, (Map<String, Object>)obj);
}
else {
throw new EncodingException("Unexpected object type: " + obj.getClass().getName());
}
}
/**
* Encodes an integer as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The integer to encode
*/
private void writeInt(List<Byte> ret, int val) {
if (val < 0 || val >= 0x200000) {
ret.add((byte)(((val >> 22) & 0x7f) | 0x80));
ret.add((byte)(((val >> 15) & 0x7f) | 0x80));
ret.add((byte)(((val >> 8) & 0x7f) | 0x80));
ret.add((byte)(val & 0xff));
}
else {
if (val >= 0x4000) {
ret.add((byte)(((val >> 14) & 0x7f) | 0x80));
}
if (val >= 0x80) {
ret.add((byte)(((val >> 7) & 0x7f) | 0x80));
}
ret.add((byte)(val & 0x7f));
}
}
/**
* Encodes a double as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The double to encode
*/
private void writeDouble(List<Byte> ret, double val) {
if (Double.isNaN(val)) {
ret.add((byte)0x7F);
ret.add((byte)0xFF);
ret.add((byte)0xFF);
ret.add((byte)0xFF);
ret.add((byte)0xE0);
ret.add((byte)0x00);
ret.add((byte)0x00);
ret.add((byte)0x00);
}
else {
byte[] temp = new byte[8];
ByteBuffer.wrap(temp).putDouble(val);
for (byte b : temp)
ret.add(b);
}
}
/**
* Encodes a string as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The string to encode
* @throws EncodingException
*/
private void writeString(List<Byte> ret, String val) throws EncodingException {
byte[] temp = null;
try {
temp = val.getBytes("UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new EncodingException("Unable to encode string as UTF-8: " + val);
}
writeInt(ret, (temp.length << 1) | 1);
for (byte b : temp)
ret.add(b);
}
/**
* Encodes a date as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The date to encode
*/
private void writeDate(List<Byte> ret, Date val) {
ret.add((byte)0x01);
writeDouble(ret, (double)val.getTime());
}
/**
* Encodes an array as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The array to encode
* @throws EncodingException
* @throws NotImplementedException
*/
private void writeArray(List<Byte> ret, Object[] val) throws EncodingException, NotImplementedException {
writeInt(ret, (val.length << 1) | 1);
ret.add((byte)0x01);
for (Object obj : val)
encode(ret, obj);
}
/**
* Encodes an associative array as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The associative array to encode
* @throws EncodingException
* @throws NotImplementedException
*/
private void writeAssociativeArray(List<Byte> ret, Map<String, Object> val) throws EncodingException, NotImplementedException {
ret.add((byte)0x01);
for (String key : val.keySet()) {
writeString(ret, key);
encode(ret, val.get(key));
}
ret.add((byte)0x01);
}
/**
* Encodes an object as AMF3 to the given buffer
*
* @param ret The buffer to encode to
* @param val The object to encode
* @throws EncodingException
* @throws NotImplementedException
*/
private void writeObject(List<Byte> ret, TypedObject val) throws EncodingException, NotImplementedException {
if (val.type == null || val.type.equals("")) {
ret.add((byte)0x0B); // Dynamic class
ret.add((byte)0x01); // No class name
for (String key : val.keySet()) {
writeString(ret, key);
encode(ret, val.get(key));
}
ret.add((byte)0x01); // End of dynamic
}
else if (val.type.equals("flex.messaging.io.ArrayCollection")) {
ret.add((byte)0x07); // Externalizable
writeString(ret, val.type);
encode(ret, val.get("array"));
}
else {
writeInt(ret, (val.size() << 4) | 3); // Inline + member count
writeString(ret, val.type);
List<String> keyOrder = new ArrayList<String>();
for (String key : val.keySet()) {
writeString(ret, key);
keyOrder.add(key);
}
for (String key : keyOrder)
encode(ret, val.get(key));
}
}
/**
* Not implemented
*
* @param ret
* @param val
* @throws NotImplementedException
*/
private void writeByteArray(List<Byte> ret, byte[] val) throws NotImplementedException {
throw new NotImplementedException("Encoding byte arrays is not implemented");
}
/**
* Encodes an integer as AMF0 to the given buffer
*
* @param ret The buffer to encode to
* @param val The integer to encode
*/
private void writeIntAMF0(List<Byte> ret, int val) {
ret.add((byte)0x00);
byte[] temp = new byte[8];
ByteBuffer.wrap(temp).putDouble((double)val);
for (byte b : temp)
ret.add(b);
}
/**
* Encodes a string as AMF0 to the given buffer
*
* @param ret The buffer to encode to
* @param val The string to encode
* @throws EncodingException
*/
private void writeStringAMF0(List<Byte> ret, String val) throws EncodingException {
byte[] temp = null;
try {
temp = val.getBytes("UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new EncodingException("Unable to encode string as UTF-8: " + val);
}
ret.add((byte)0x02);
ret.add((byte)((temp.length & 0xFF00) >> 8));
ret.add((byte)(temp.length & 0x00FF));
for (byte b : temp)
ret.add(b);
}
/**
* Generates a random UID, used for messageIDs
*
* @return A random UID
*/
public static String randomUID() {
byte[] bytes = new byte[16];
rand.nextBytes(bytes);
StringBuilder ret = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
if (i == 4 || i == 6 || i == 8 || i == 10)
ret.append('-');
ret.append(String.format("%02X", bytes[i]));
}
return ret.toString();
}
}