Skip to content
Snippets Groups Projects
Commit 786c8fdf authored by Leo Ma's avatar Leo Ma
Browse files

Local recording


Signed-off-by: default avatarLeo Ma <begeekmyfriend@gmail.com>
parent d00cc02c
No related branches found
No related tags found
No related merge requests found
Showing
with 17 additions and 2176 deletions
package net.ossrs.yasea.demo;
 
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
 
import com.github.faucamp.simplertmp.RtmpHandler;
import net.ossrs.yasea.SrsCameraView;
import net.ossrs.yasea.SrsEncodeHandler;
import net.ossrs.yasea.SrsPublisher;
import net.ossrs.yasea.SrsRecordHandler;
 
import java.io.IOException;
import java.net.SocketException;
import java.util.Random;
public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
SrsRecordHandler.SrsRecordListener, SrsEncodeHandler.SrsEncodeListener {
 
private static final String TAG = "Yasea";
public class MainActivity extends Activity implements SrsRecordHandler.SrsRecordListener {
 
Button btnPublish = null;
Button btnSwitchCamera = null;
Button btnRecord = null;
Button btnSwitchEncoder = null;
 
private SharedPreferences sp;
private String rtmpUrl = "rtmp://ossrs.net/" + getRandomAlphaString(3) + '/' + getRandomAlphaDigitString(5);
private String recPath = Environment.getExternalStorageDirectory().getPath() + "/test.mp4";
 
private SrsPublisher mPublisher;
Loading
Loading
@@ -53,57 +39,17 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
// response screen rotation event
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
 
// restore data.
sp = getSharedPreferences("Yasea", MODE_PRIVATE);
rtmpUrl = sp.getString("rtmpUrl", rtmpUrl);
// initialize url.
final EditText efu = (EditText) findViewById(R.id.url);
efu.setText(rtmpUrl);
btnPublish = (Button) findViewById(R.id.publish);
btnSwitchCamera = (Button) findViewById(R.id.swCam);
btnRecord = (Button) findViewById(R.id.record);
btnSwitchEncoder = (Button) findViewById(R.id.swEnc);
btnSwitchCamera = findViewById(R.id.swCam);
btnRecord = findViewById(R.id.record);
btnSwitchEncoder = findViewById(R.id.swEnc);
 
mPublisher = new SrsPublisher((SrsCameraView) findViewById(R.id.preview));
mPublisher.setEncodeHandler(new SrsEncodeHandler(this));
mPublisher.setRtmpHandler(new RtmpHandler(this));
mPublisher.setRecordHandler(new SrsRecordHandler(this));
mPublisher.setPreviewResolution(640, 360);
mPublisher.setOutputResolution(360, 640);
mPublisher.setVideoSmoothMode();
mPublisher.startCamera();
 
btnPublish.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (btnPublish.getText().toString().contentEquals("publish")) {
rtmpUrl = efu.getText().toString();
SharedPreferences.Editor editor = sp.edit();
editor.putString("rtmpUrl", rtmpUrl);
editor.apply();
mPublisher.startPublish(rtmpUrl);
mPublisher.startCamera();
if (btnSwitchEncoder.getText().toString().contentEquals("soft encoder")) {
Toast.makeText(getApplicationContext(), "Use hard encoder", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "Use soft encoder", Toast.LENGTH_SHORT).show();
}
btnPublish.setText("stop");
btnSwitchEncoder.setEnabled(false);
} else if (btnPublish.getText().toString().contentEquals("stop")) {
mPublisher.stopPublish();
mPublisher.stopRecord();
btnPublish.setText("publish");
btnRecord.setText("record");
btnSwitchEncoder.setEnabled(true);
}
}
});
btnSwitchCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Loading
Loading
@@ -116,14 +62,11 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
public void onClick(View v) {
if (btnRecord.getText().toString().contentEquals("record")) {
if (mPublisher.startRecord(recPath)) {
btnRecord.setText("pause");
btnRecord.setText("stop");
}
} else if (btnRecord.getText().toString().contentEquals("pause")) {
mPublisher.pauseRecord();
btnRecord.setText("resume");
} else if (btnRecord.getText().toString().contentEquals("resume")) {
mPublisher.resumeRecord();
btnRecord.setText("pause");
} else {
mPublisher.stopRecord();
btnRecord.setText("record");
}
}
});
Loading
Loading
@@ -167,8 +110,6 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
@Override
protected void onResume() {
super.onResume();
final Button btn = (Button) findViewById(R.id.publish);
btn.setEnabled(true);
mPublisher.resumeRecord();
}
 
Loading
Loading
@@ -181,7 +122,6 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
@Override
protected void onDestroy() {
super.onDestroy();
mPublisher.stopPublish();
mPublisher.stopRecord();
}
 
Loading
Loading
@@ -192,40 +132,14 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
mPublisher.stopRecord();
btnRecord.setText("record");
mPublisher.setScreenOrientation(newConfig.orientation);
if (btnPublish.getText().toString().contentEquals("stop")) {
mPublisher.startEncode();
}
mPublisher.startEncode();
mPublisher.startCamera();
}
 
private static String getRandomAlphaString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
private static String getRandomAlphaDigitString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
private void handleException(Exception e) {
try {
Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
mPublisher.stopPublish();
mPublisher.stopRecord();
btnPublish.setText("publish");
btnRecord.setText("record");
btnSwitchEncoder.setEnabled(true);
} catch (Exception e1) {
Loading
Loading
@@ -233,81 +147,6 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
}
}
 
// Implementation of SrsRtmpListener.
@Override
public void onRtmpConnecting(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpConnected(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpVideoStreaming() {
}
@Override
public void onRtmpAudioStreaming() {
}
@Override
public void onRtmpStopped() {
Toast.makeText(getApplicationContext(), "Stopped", Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpDisconnected() {
Toast.makeText(getApplicationContext(), "Disconnected", Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpVideoFpsChanged(double fps) {
Log.i(TAG, String.format("Output Fps: %f", fps));
}
@Override
public void onRtmpVideoBitrateChanged(double bitrate) {
int rate = (int) bitrate;
if (rate / 1000 > 0) {
Log.i(TAG, String.format("Video bitrate: %f kbps", bitrate / 1000));
} else {
Log.i(TAG, String.format("Video bitrate: %d bps", rate));
}
}
@Override
public void onRtmpAudioBitrateChanged(double bitrate) {
int rate = (int) bitrate;
if (rate / 1000 > 0) {
Log.i(TAG, String.format("Audio bitrate: %f kbps", bitrate / 1000));
} else {
Log.i(TAG, String.format("Audio bitrate: %d bps", rate));
}
}
@Override
public void onRtmpSocketException(SocketException e) {
handleException(e);
}
@Override
public void onRtmpIOException(IOException e) {
handleException(e);
}
@Override
public void onRtmpIllegalArgumentException(IllegalArgumentException e) {
handleException(e);
}
@Override
public void onRtmpIllegalStateException(IllegalStateException e) {
handleException(e);
}
// Implementation of SrsRecordHandler.
 
@Override
Loading
Loading
@@ -339,21 +178,4 @@ public class MainActivity extends Activity implements RtmpHandler.RtmpListener,
public void onRecordIllegalArgumentException(IllegalArgumentException e) {
handleException(e);
}
// Implementation of SrsEncodeHandler.
@Override
public void onNetworkWeak() {
Toast.makeText(getApplicationContext(), "Network weak", Toast.LENGTH_SHORT).show();
}
@Override
public void onNetworkResume() {
Toast.makeText(getApplicationContext(), "Network resume", Toast.LENGTH_SHORT).show();
}
@Override
public void onEncodeIllegalArgumentException(IllegalArgumentException e) {
handleException(e);
}
}
Loading
Loading
@@ -6,21 +6,17 @@
tools:context="net.ossrs.yasea.demo.MainActivity">
<net.ossrs.yasea.SrsCameraView
android:id="@+id/preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:id="@+id/preview"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
android:layout_alignParentTop="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="publish"
android:id="@+id/publish"
android:text="record"
android:id="@+id/record"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
Loading
Loading
@@ -30,32 +26,17 @@
android:layout_height="wrap_content"
android:text="switch"
android:id="@+id/swCam"
android:layout_alignBottom="@+id/publish"
android:layout_toRightOf="@+id/publish" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="record"
android:id="@+id/record"
android:layout_alignBottom="@+id/publish"
android:layout_toRightOf="@id/swCam" />
android:layout_alignBottom="@+id/record"
android:layout_toRightOf="@+id/record" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="soft encoder"
android:id="@+id/swEnc"
android:layout_alignBottom="@+id/publish"
android:layout_toRightOf="@id/record"
android:layout_alignBottom="@+id/record"
android:layout_toRightOf="@id/swCam"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:id="@+id/url"
android:textColor="@color/accent_material_light" />
</RelativeLayout>
package com.github.faucamp.simplertmp;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* Some helper utilities for SHA256, mostly (used during handshake)
* This is separated in order to be more easily replaced on platforms that
* do not have the javax.crypto.* and/or java.security.* packages
*
* This implementation is directly inspired by the RTMPHandshake class of the
* Red5 Open Source Flash Server project
*
* @author francois
*/
public class Crypto {
private static final String TAG = "Crypto";
private Mac hmacSHA256;
public Crypto() {
try {
hmacSHA256 = Mac.getInstance("HmacSHA256");
} catch (SecurityException e) {
Log.e(TAG, "Security exception when getting HMAC", e);
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "HMAC SHA256 does not exist");
}
}
/**
* Calculates an HMAC SHA256 hash using a default key length.
*
*
* @param input
* @param key
* @return hmac hashed bytes
*/
public byte[] calculateHmacSHA256(byte[] input, byte[] key) {
byte[] output = null;
try {
hmacSHA256.init(new SecretKeySpec(key, "HmacSHA256"));
output = hmacSHA256.doFinal(input);
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key", e);
}
return output;
}
/**
* Calculates an HMAC SHA256 hash using a set key length.
*
* @param input
* @param key
* @param length
* @return hmac hashed bytes
*/
public byte[] calculateHmacSHA256(byte[] input, byte[] key, int length) {
byte[] output = null;
try {
hmacSHA256.init(new SecretKeySpec(key, 0, length, "HmacSHA256"));
output = hmacSHA256.doFinal(input);
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key", e);
}
return output;
}
}
package com.github.faucamp.simplertmp;
import java.util.concurrent.atomic.AtomicInteger;
import com.github.faucamp.simplertmp.io.RtmpConnection;
/**
* Srs implementation of an RTMP publisher
*
* @author francois, leoma
*/
public class DefaultRtmpPublisher implements RtmpPublisher {
private RtmpConnection rtmpConnection;
public DefaultRtmpPublisher(RtmpHandler handler) {
rtmpConnection = new RtmpConnection(handler);
}
@Override
public boolean connect(String url) {
return rtmpConnection.connect(url);
}
@Override
public boolean publish(String publishType) {
return rtmpConnection.publish(publishType);
}
@Override
public void close() {
rtmpConnection.close();
}
@Override
public void publishVideoData(byte[] data, int size, int dts) {
rtmpConnection.publishVideoData(data, size, dts);
}
@Override
public void publishAudioData(byte[] data, int size, int dts) {
rtmpConnection.publishAudioData(data, size, dts);
}
@Override
public AtomicInteger getVideoFrameCacheNumber() {
return rtmpConnection.getVideoFrameCacheNumber();
}
@Override
public final String getServerIpAddr() {
return rtmpConnection.getServerIpAddr();
}
@Override
public final int getServerPid() {
return rtmpConnection.getServerPid();
}
@Override
public final int getServerId() {
return rtmpConnection.getServerId();
}
@Override
public void setVideoResolution(int width, int height) {
rtmpConnection.setVideoResolution(width, height);
}
}
package com.github.faucamp.simplertmp;
import android.os.Handler;
import android.os.Message;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.SocketException;
/**
* Created by leo.ma on 2016/11/3.
*/
public class RtmpHandler extends Handler {
private static final int MSG_RTMP_CONNECTING = 0;
private static final int MSG_RTMP_CONNECTED = 1;
private static final int MSG_RTMP_VIDEO_STREAMING = 2;
private static final int MSG_RTMP_AUDIO_STREAMING = 3;
private static final int MSG_RTMP_STOPPED = 4;
private static final int MSG_RTMP_DISCONNECTED = 5;
private static final int MSG_RTMP_VIDEO_FPS_CHANGED = 6;
private static final int MSG_RTMP_VIDEO_BITRATE_CHANGED = 7;
private static final int MSG_RTMP_AUDIO_BITRATE_CHANGED = 8;
private static final int MSG_RTMP_SOCKET_EXCEPTION = 9;
private static final int MSG_RTMP_IO_EXCEPTION = 10;
private static final int MSG_RTMP_ILLEGAL_ARGUMENT_EXCEPTION = 11;
private static final int MSG_RTMP_ILLEGAL_STATE_EXCEPTION = 12;
private WeakReference<RtmpListener> mWeakListener;
public RtmpHandler(RtmpListener listener) {
mWeakListener = new WeakReference<>(listener);
}
public void notifyRtmpConnecting(String msg) {
obtainMessage(MSG_RTMP_CONNECTING, msg).sendToTarget();
}
public void notifyRtmpConnected(String msg) {
obtainMessage(MSG_RTMP_CONNECTED, msg).sendToTarget();
}
public void notifyRtmpVideoStreaming() {
sendEmptyMessage(MSG_RTMP_VIDEO_STREAMING);
}
public void notifyRtmpAudioStreaming() {
sendEmptyMessage(MSG_RTMP_AUDIO_STREAMING);
}
public void notifyRtmpStopped() {
sendEmptyMessage(MSG_RTMP_STOPPED);
}
public void notifyRtmpDisconnected() {
sendEmptyMessage(MSG_RTMP_DISCONNECTED);
}
public void notifyRtmpVideoFpsChanged(double fps) {
obtainMessage(MSG_RTMP_VIDEO_FPS_CHANGED, fps).sendToTarget();
}
public void notifyRtmpVideoBitrateChanged(double bitrate) {
obtainMessage(MSG_RTMP_VIDEO_BITRATE_CHANGED, bitrate).sendToTarget();
}
public void notifyRtmpAudioBitrateChanged(double bitrate) {
obtainMessage(MSG_RTMP_AUDIO_BITRATE_CHANGED, bitrate).sendToTarget();
}
public void notifyRtmpSocketException(SocketException e) {
obtainMessage(MSG_RTMP_SOCKET_EXCEPTION, e).sendToTarget();
}
public void notifyRtmpIOException(IOException e) {
obtainMessage(MSG_RTMP_IO_EXCEPTION, e).sendToTarget();
}
public void notifyRtmpIllegalArgumentException(IllegalArgumentException e) {
obtainMessage(MSG_RTMP_ILLEGAL_ARGUMENT_EXCEPTION, e).sendToTarget();
}
public void notifyRtmpIllegalStateException(IllegalStateException e) {
obtainMessage(MSG_RTMP_ILLEGAL_STATE_EXCEPTION, e).sendToTarget();
}
@Override // runs on UI thread
public void handleMessage(Message msg) {
RtmpListener listener = mWeakListener.get();
if (listener == null) {
return;
}
switch (msg.what) {
case MSG_RTMP_CONNECTING:
listener.onRtmpConnecting((String) msg.obj);
break;
case MSG_RTMP_CONNECTED:
listener.onRtmpConnected((String) msg.obj);
break;
case MSG_RTMP_VIDEO_STREAMING:
listener.onRtmpVideoStreaming();
break;
case MSG_RTMP_AUDIO_STREAMING:
listener.onRtmpAudioStreaming();
break;
case MSG_RTMP_STOPPED:
listener.onRtmpStopped();
break;
case MSG_RTMP_DISCONNECTED:
listener.onRtmpDisconnected();
break;
case MSG_RTMP_VIDEO_FPS_CHANGED:
listener.onRtmpVideoFpsChanged((double) msg.obj);
break;
case MSG_RTMP_VIDEO_BITRATE_CHANGED:
listener.onRtmpVideoBitrateChanged((double) msg.obj);
break;
case MSG_RTMP_AUDIO_BITRATE_CHANGED:
listener.onRtmpAudioBitrateChanged((double) msg.obj);
break;
case MSG_RTMP_SOCKET_EXCEPTION:
listener.onRtmpSocketException((SocketException) msg.obj);
break;
case MSG_RTMP_IO_EXCEPTION:
listener.onRtmpIOException((IOException) msg.obj);
break;
case MSG_RTMP_ILLEGAL_ARGUMENT_EXCEPTION:
listener.onRtmpIllegalArgumentException((IllegalArgumentException) msg.obj);
break;
case MSG_RTMP_ILLEGAL_STATE_EXCEPTION:
listener.onRtmpIllegalStateException((IllegalStateException) msg.obj);
break;
default:
throw new RuntimeException("unknown msg " + msg.what);
}
}
public interface RtmpListener {
void onRtmpConnecting(String msg);
void onRtmpConnected(String msg);
void onRtmpVideoStreaming();
void onRtmpAudioStreaming();
void onRtmpStopped();
void onRtmpDisconnected();
void onRtmpVideoFpsChanged(double fps);
void onRtmpVideoBitrateChanged(double bitrate);
void onRtmpAudioBitrateChanged(double bitrate);
void onRtmpSocketException(SocketException e);
void onRtmpIOException(IOException e);
void onRtmpIllegalArgumentException(IllegalArgumentException e);
void onRtmpIllegalStateException(IllegalStateException e);
}
}
package com.github.faucamp.simplertmp;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Simple RTMP publisher, using vanilla Java networking (no NIO)
* This was created primarily to address a NIO bug in Android 2.2 when
* used with Apache Mina, but also to provide an easy-to-use way to access
* RTMP streams
*
* @author francois, leo
*/
public interface RtmpPublisher {
/**
* Issues an RTMP "connect" command and wait for the response.
*
* @param url specify the RTMP url
* @return If succeeded return true else return false
*/
boolean connect(String url);
/**
* Issues an RTMP "publish" command and write the media content stream packets (audio and video).
*
* @param publishType specify the way to publish raw RTMP packets among "live", "record" and "append"
* @return If succeeded return true else return false
* @throws IllegalStateException if the client is not connected to a RTMP server
*/
boolean publish(String publishType);
/**
* Stop and close the current RTMP streaming client.
*/
void close();
/**
* publish a video content packet to server
*
* @param data video stream byte array
* @param size video stream byte size (not the whole length of byte array)
* @param dts video stream decoding timestamp
*/
void publishVideoData(byte[] data, int size, int dts);
/**
* publish an audio content packet to server
*
* @param data audio stream byte array
* @param size audio stream byte size (not the whole length of byte array)
* @param dts audio stream decoding timestamp
*/
void publishAudioData(byte[] data, int size, int dts);
/**
* obtain video frame number cached in publisher
*/
AtomicInteger getVideoFrameCacheNumber();
/**
* obtain the IP address of the peer if any
*/
String getServerIpAddr();
/**
* obtain the PID of the peer if any
*/
int getServerPid();
/**
* obtain the ID of the peer if any
*/
int getServerId();
/**
* set video resolution
*
* @param width video width
* @param height video height
*/
void setVideoResolution(int width, int height);
}
package com.github.faucamp.simplertmp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Misc utility method
* @author francois
*/
public class Util {
private static final String HEXES = "0123456789ABCDEF";
public static void writeUnsignedInt32(OutputStream out, int value) throws IOException {
out.write((byte) (value >>> 24));
out.write((byte) (value >>> 16));
out.write((byte) (value >>> 8));
out.write((byte) value);
}
public static int readUnsignedInt32(InputStream in) throws IOException {
return ((in.read() & 0xff) << 24) | ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff);
}
public static int readUnsignedInt24(InputStream in) throws IOException {
return ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff);
}
public static int readUnsignedInt16(InputStream in) throws IOException {
return ((in.read() & 0xff) << 8) | (in.read() & 0xff);
}
public static void writeUnsignedInt24(OutputStream out, int value) throws IOException {
out.write((byte) (value >>> 16));
out.write((byte) (value >>> 8));
out.write((byte) value);
}
public static void writeUnsignedInt16(OutputStream out, int value) throws IOException {
out.write((byte) (value >>> 8));
out.write((byte) value);
}
public static int toUnsignedInt32(byte[] bytes) {
return (((int) bytes[0] & 0xff) << 24) | (((int)bytes[1] & 0xff) << 16) | (((int)bytes[2] & 0xff) << 8) | ((int)bytes[3] & 0xff);
}
public static int toUnsignedInt32LittleEndian(byte[] bytes) {
return ((bytes[3] & 0xff) << 24) | ((bytes[2] & 0xff) << 16) | ((bytes[1] & 0xff) << 8) | (bytes[0] & 0xff);
}
public static void writeUnsignedInt32LittleEndian(OutputStream out, int value) throws IOException {
out.write((byte) value);
out.write((byte) (value >>> 8));
out.write((byte) (value >>> 16));
out.write((byte) (value >>> 24));
}
public static int toUnsignedInt24(byte[] bytes) {
return ((bytes[1] & 0xff) << 16) | ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff);
}
public static int toUnsignedInt16(byte[] bytes) {
return ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff);
}
public static String toHexString(byte[] raw) {
if (raw == null) {
return null;
}
final StringBuilder hex = new StringBuilder(2 * raw.length);
for (final byte b : raw) {
hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
}
return hex.toString();
}
public static String toHexString(byte b) {
return new StringBuilder().append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).toString();
}
/**
* Reads bytes from the specified inputstream into the specified target buffer until it is filled up
*/
public static void readBytesUntilFull(InputStream in, byte[] targetBuffer) throws IOException {
int totalBytesRead = 0;
int read;
final int targetBytes = targetBuffer.length;
do {
read = in.read(targetBuffer, totalBytesRead, (targetBytes - totalBytesRead));
if (read != -1) {
totalBytesRead += read;
} else {
throw new IOException("Unexpected EOF reached before read buffer was filled");
}
} while (totalBytesRead < targetBytes);
}
public static byte[] toByteArray(double d) {
long l = Double.doubleToRawLongBits(d);
return new byte[]{
(byte) ((l >> 56) & 0xff),
(byte) ((l >> 48) & 0xff),
(byte) ((l >> 40) & 0xff),
(byte) ((l >> 32) & 0xff),
(byte) ((l >> 24) & 0xff),
(byte) ((l >> 16) & 0xff),
(byte) ((l >> 8) & 0xff),
(byte) (l & 0xff),};
}
public static byte[] unsignedInt32ToByteArray(int value) throws IOException {
return new byte[]{
(byte) (value >>> 24),
(byte) (value >>> 16),
(byte) (value >>> 8),
(byte) value};
}
public static double readDouble(InputStream in) throws IOException {
long bits = ((long) (in.read() & 0xff) << 56) | ((long) (in.read() & 0xff) << 48) | ((long) (in.read() & 0xff) << 40) | ((long) (in.read() & 0xff) << 32) | ((in.read() & 0xff) << 24) | ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff);
return Double.longBitsToDouble(bits);
}
public static void writeDouble(OutputStream out, double d) throws IOException {
long l = Double.doubleToRawLongBits(d);
out.write(new byte[]{
(byte) ((l >> 56) & 0xff),
(byte) ((l >> 48) & 0xff),
(byte) ((l >> 40) & 0xff),
(byte) ((l >> 32) & 0xff),
(byte) ((l >> 24) & 0xff),
(byte) ((l >> 16) & 0xff),
(byte) ((l >> 8) & 0xff),
(byte) (l & 0xff)});
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import com.github.faucamp.simplertmp.Util;
/**
* AMF Array
*
* @author francois
*/
public class AmfArray implements AmfData {
private List<AmfData> items;
private int size = -1;
@Override
public void writeTo(OutputStream out) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
int length = Util.readUnsignedInt32(in);
size = 5; // 1 + 4
items = new ArrayList<AmfData>(length);
for (int i = 0; i < length; i++) {
AmfData dataItem = AmfDecoder.readFrom(in);
size += dataItem.getSize();
items.add(dataItem);
}
}
@Override
public int getSize() {
if (size == -1) {
size = 5; // 1 + 4
if (items != null) {
for (AmfData dataItem : items) {
size += dataItem.getSize();
}
}
}
return size;
}
/** @return the amount of items in this the array */
public int getLength() {
return items != null ? items.size() : 0;
}
public List<AmfData> getItems() {
if (items == null) {
items = new ArrayList<AmfData>();
}
return items;
}
public void addItem(AmfData dataItem) {
getItems().add(this);
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
*
* @author francois
*/
public class AmfBoolean implements AmfData {
private boolean value;
public boolean isValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
public AmfBoolean(boolean value) {
this.value = value;
}
public AmfBoolean() {
}
@Override
public void writeTo(OutputStream out) throws IOException {
out.write(AmfType.BOOLEAN.getValue());
out.write(value ? 0x01 : 0x00);
}
@Override
public void readFrom(InputStream in) throws IOException {
value = (in.read() == 0x01) ? true : false;
}
public static boolean readBooleanFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
return (in.read() == 0x01) ? true : false;
}
@Override
public int getSize() {
return 2;
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Base AMF data object. All other AMF data type instances derive from this
* (including AmfObject)
*
* @author francois
*/
public interface AmfData {
/**
* Write/Serialize this AMF data intance (Object/string/integer etc) to
* the specified OutputStream
*/
void writeTo(OutputStream out) throws IOException;
/**
* Read and parse bytes from the specified input stream to populate this
* AMFData instance (deserialize)
*
* @return the amount of bytes read
*/
void readFrom(InputStream in) throws IOException;
/** @return the amount of bytes required for this object */
int getSize();
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
/**
*
* @author francois
*/
public class AmfDecoder {
public static AmfData readFrom(InputStream in) throws IOException {
byte amfTypeByte = (byte) in.read();
AmfType amfType = AmfType.valueOf(amfTypeByte);
AmfData amfData;
switch (amfType) {
case NUMBER:
amfData = new AmfNumber();
break;
case BOOLEAN:
amfData = new AmfBoolean();
break;
case STRING:
amfData = new AmfString();
break;
case OBJECT:
amfData = new AmfObject();
break;
case NULL:
return new AmfNull();
case UNDEFINED:
return new AmfUndefined();
case MAP:
amfData = new AmfMap();
break;
case ARRAY:
amfData = new AmfArray();
break;
default:
throw new IOException("Unknown/unimplemented AMF data type: " + amfType);
}
amfData.readFrom(in);
return amfData;
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import com.github.faucamp.simplertmp.Util;
/**
* AMF map; that is, an "object"-like structure of key/value pairs, but with
* an array-like size indicator at the start (which is seemingly always 0)
*
* @author francois
*/
public class AmfMap extends AmfObject {
@Override
public void writeTo(OutputStream out) throws IOException {
// Begin the map/object/array/whatever exactly this is
out.write(AmfType.MAP.getValue());
// Write the "array size"
Util.writeUnsignedInt32(out, properties.size());
// Write key/value pairs in this object
for (Map.Entry<String, AmfData> entry : properties.entrySet()) {
// The key must be a STRING type, and thus the "type-definition" byte is implied (not included in message)
AmfString.writeStringTo(out, entry.getKey(), true);
entry.getValue().writeTo(out);
}
// End the object
out.write(OBJECT_END_MARKER);
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
int length = Util.readUnsignedInt32(in); // Seems this is always 0
super.readFrom(in);
size += 4; // Add the bytes read for parsing the array size (length)
}
@Override
public int getSize() {
if (size == -1) {
size = super.getSize();
size += 4; // array length bytes
}
return size;
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
*
* @author francois
*/
public class AmfNull implements AmfData {
@Override
public void writeTo(OutputStream out) throws IOException {
out.write(AmfType.NULL.getValue());
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
}
public static void writeNullTo(OutputStream out) throws IOException {
out.write(AmfType.NULL.getValue());
}
@Override
public int getSize() {
return 1;
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.github.faucamp.simplertmp.Util;
/**
* AMF0 Number data type
*
* @author francois
*/
public class AmfNumber implements AmfData {
double value;
/** Size of an AMF number, in bytes (including type bit) */
public static final int SIZE = 9;
public AmfNumber(double value) {
this.value = value;
}
public AmfNumber() {
}
public double getValue() {
return value;
}
public void setValue(double value) {
this.value = value;
}
@Override
public void writeTo(OutputStream out) throws IOException {
out.write(AmfType.NUMBER.getValue());
Util.writeDouble(out, value);
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
value = Util.readDouble(in);
}
public static double readNumberFrom(InputStream in) throws IOException {
// Skip data type byte
in.read();
return Util.readDouble(in);
}
public static void writeNumberTo(OutputStream out, double number) throws IOException {
out.write(AmfType.NUMBER.getValue());
Util.writeDouble(out, number);
}
@Override
public int getSize() {
return SIZE;
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* AMF object
*
* @author francois
*/
public class AmfObject implements AmfData {
protected Map<String, AmfData> properties = new LinkedHashMap<String, AmfData>();
protected int size = -1;
/** Byte sequence that marks the end of an AMF object */
protected static final byte[] OBJECT_END_MARKER = new byte[]{0x00, 0x00, 0x09};
public AmfObject() {
}
public AmfData getProperty(String key) {
return properties.get(key);
}
public void setProperty(String key, AmfData value) {
properties.put(key, value);
}
public void setProperty(String key, boolean value) {
properties.put(key, new AmfBoolean(value));
}
public void setProperty(String key, String value) {
properties.put(key, new AmfString(value, false));
}
public void setProperty(String key, int value) {
properties.put(key, new AmfNumber(value));
}
public void setProperty(String key, double value) {
properties.put(key, new AmfNumber(value));
}
@Override
public void writeTo(OutputStream out) throws IOException {
// Begin the object
out.write(AmfType.OBJECT.getValue());
// Write key/value pairs in this object
for (Map.Entry<String, AmfData> entry : properties.entrySet()) {
// The key must be a STRING type, and thus the "type-definition" byte is implied (not included in message)
AmfString.writeStringTo(out, entry.getKey(), true);
entry.getValue().writeTo(out);
}
// End the object
out.write(OBJECT_END_MARKER);
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
size = 1;
InputStream markInputStream = in.markSupported() ? in : new BufferedInputStream(in);
while (true) {
// Look for the 3-byte object end marker [0x00 0x00 0x09]
markInputStream.mark(3);
byte[] endMarker = new byte[3];
markInputStream.read(endMarker);
if (endMarker[0] == OBJECT_END_MARKER[0] && endMarker[1] == OBJECT_END_MARKER[1] && endMarker[2] == OBJECT_END_MARKER[2]) {
// End marker found
size += 3;
return;
} else {
// End marker not found; reset the stream to the marked position and read an AMF property
markInputStream.reset();
// Read the property key...
String key = AmfString.readStringFrom(in, true);
size += AmfString.sizeOf(key, true);
// ...and the property value
AmfData value = AmfDecoder.readFrom(markInputStream);
size += value.getSize();
properties.put(key, value);
}
}
}
@Override
public int getSize() {
if (size == -1) {
size = 1; // object marker
for (Map.Entry<String, AmfData> entry : properties.entrySet()) {
size += AmfString.sizeOf(entry.getKey(), true);
size += entry.getValue().getSize();
}
size += 3; // end of object marker
}
return size;
}
}
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.String;
import android.util.Log;
import com.github.faucamp.simplertmp.Util;
/**
*
* @author francois
*/
public class AmfString implements AmfData {
private static final String TAG = "AmfString";
private String value;
private boolean key;
private int size = -1;
public AmfString() {
}
public AmfString(String value, boolean isKey) {
this.value = value;
this.key = isKey;
}
public AmfString(String value) {
this(value, false);
}
public AmfString(boolean isKey) {
this.key = isKey;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public boolean isKey() {
return key;
}
public void setKey(boolean key) {
this.key = key;
}
@Override
public void writeTo(OutputStream out) throws IOException {
// Strings are ASCII encoded
byte[] byteValue = this.value.getBytes("ASCII");
// Write the STRING data type definition (except if this String is used as a key)
if (!key) {
out.write(AmfType.STRING.getValue());
}
// Write 2 bytes indicating string length
Util.writeUnsignedInt16(out, byteValue.length);
// Write string
out.write(byteValue);
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
int length = Util.readUnsignedInt16(in);
size = 3 + length; // 1 + 2 + length
// Read string value
byte[] byteValue = new byte[length];
Util.readBytesUntilFull(in, byteValue);
value = new String(byteValue, "ASCII");
}
public static String readStringFrom(InputStream in, boolean isKey) throws IOException {
if (!isKey) {
// Read past the data type byte
in.read();
}
int length = Util.readUnsignedInt16(in);
// Read string value
byte[] byteValue = new byte[length];
Util.readBytesUntilFull(in, byteValue);
return new String(byteValue, "ASCII");
}
public static void writeStringTo(OutputStream out, String string, boolean isKey) throws IOException {
// Strings are ASCII encoded
byte[] byteValue = string.getBytes("ASCII");
// Write the STRING data type definition (except if this String is used as a key)
if (!isKey) {
out.write(AmfType.STRING.getValue());
}
// Write 2 bytes indicating string length
Util.writeUnsignedInt16(out, byteValue.length);
// Write string
out.write(byteValue);
}
@Override
public int getSize() {
if (size == -1) {
try {
size = (isKey() ? 0 : 1) + 2 + value.getBytes("ASCII").length;
} catch (UnsupportedEncodingException ex) {
Log.e(TAG, "AmfString.getSize(): caught exception", ex);
throw new RuntimeException(ex);
}
}
return size;
}
/** @return the byte size of the resulting AMF string of the specified value */
public static int sizeOf(String string, boolean isKey) {
try {
int size = (isKey ? 0 : 1) + 2 + string.getBytes("ASCII").length;
return size;
} catch (UnsupportedEncodingException ex) {
Log.e(TAG, "AmfString.SizeOf(): caught exception", ex);
throw new RuntimeException(ex);
}
}
}
package com.github.faucamp.simplertmp.amf;
import java.util.HashMap;
import java.util.Map;
/**
* AMF0 data type enum
*
* @author francois
*/
public enum AmfType {
/** Number (encoded as IEEE 64-bit double precision floating point number) */
NUMBER(0x00),
/** Boolean (Encoded as a single byte of value 0x00 or 0x01) */
BOOLEAN(0x01),
/** String (ASCII encoded) */
STRING(0x02),
/** Object - set of key/value pairs */
OBJECT(0x03),
NULL(0x05),
UNDEFINED(0x06),
MAP(0x08),
ARRAY(0x0A);
private byte value;
private static final Map<Byte, AmfType> quickLookupMap = new HashMap<Byte, AmfType>();
static {
for (AmfType amfType : AmfType.values()) {
quickLookupMap.put(amfType.getValue(), amfType);
}
}
private AmfType(int intValue) {
this.value = (byte) intValue;
}
public byte getValue() {
return value;
}
public static AmfType valueOf(byte amfTypeByte) {
return quickLookupMap.get(amfTypeByte);
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.github.faucamp.simplertmp.amf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
*
* @author leoma
*/
public class AmfUndefined implements AmfData {
@Override
public void writeTo(OutputStream out) throws IOException {
out.write(AmfType.UNDEFINED.getValue());
}
@Override
public void readFrom(InputStream in) throws IOException {
// Skip data type byte (we assume it's already read)
}
public static void writeUndefinedTo(OutputStream out) throws IOException {
out.write(AmfType.UNDEFINED.getValue());
}
@Override
public int getSize() {
return 1;
}
}
package com.github.faucamp.simplertmp.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import com.github.faucamp.simplertmp.Util;
import com.github.faucamp.simplertmp.packets.RtmpHeader;
/**
* Chunk stream channel information
*
* @author francois, leo
*/
public class ChunkStreamInfo {
public static final byte RTMP_CID_PROTOCOL_CONTROL = 0x02;
public static final byte RTMP_CID_OVER_CONNECTION = 0x03;
public static final byte RTMP_CID_OVER_CONNECTION2 = 0x04;
public static final byte RTMP_CID_OVER_STREAM = 0x05;
public static final byte RTMP_CID_VIDEO = 0x06;
public static final byte RTMP_CID_AUDIO = 0x07;
private RtmpHeader prevHeaderRx;
private RtmpHeader prevHeaderTx;
private static long sessionBeginTimestamp;
private long realLastTimestamp = System.nanoTime() / 1000000; // Do not use wall time!
private ByteArrayOutputStream baos = new ByteArrayOutputStream(1024 * 128);
/** @return the previous header that was received on this channel, or <code>null</code> if no previous header was received */
public RtmpHeader prevHeaderRx() {
return prevHeaderRx;
}
/** Sets the previous header that was received on this channel, or <code>null</code> if no previous header was sent */
public void setPrevHeaderRx(RtmpHeader previousHeader) {
this.prevHeaderRx = previousHeader;
}
/** @return the previous header that was transmitted on this channel */
public RtmpHeader getPrevHeaderTx() {
return prevHeaderTx;
}
public boolean canReusePrevHeaderTx(RtmpHeader.MessageType forMessageType) {
return (prevHeaderTx != null && prevHeaderTx.getMessageType() == forMessageType);
}
/** Sets the previous header that was transmitted on this channel */
public void setPrevHeaderTx(RtmpHeader prevHeaderTx) {
this.prevHeaderTx = prevHeaderTx;
}
/** Sets the session beginning timestamp for all chunks */
public static void markSessionTimestampTx() {
sessionBeginTimestamp = System.nanoTime() / 1000000;
}
/** Utility method for calculating & synchronizing transmitted timestamps */
public long markAbsoluteTimestampTx() {
return System.nanoTime() / 1000000 - sessionBeginTimestamp;
}
/** Utility method for calculating & synchronizing transmitted timestamp deltas */
public long markDeltaTimestampTx() {
long currentTimestamp = System.nanoTime() / 1000000;
long diffTimestamp = currentTimestamp - realLastTimestamp;
realLastTimestamp = currentTimestamp;
return diffTimestamp;
}
/** @return <code>true</code> if all packet data has been stored, or <code>false</code> if not */
public boolean storePacketChunk(InputStream in, int chunkSize) throws IOException {
final int remainingBytes = prevHeaderRx.getPacketLength() - baos.size();
byte[] chunk = new byte[Math.min(remainingBytes, chunkSize)];
Util.readBytesUntilFull(in, chunk);
baos.write(chunk);
return (baos.size() == prevHeaderRx.getPacketLength());
}
public ByteArrayInputStream getStoredPacketInputStream() {
ByteArrayInputStream bis = new ByteArrayInputStream(baos.toByteArray());
baos.reset();
return bis;
}
/** Clears all currently-stored packet chunks (used when an ABORT packet is received) */
public void clearStoredChunks() {
baos.reset();
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment