diff --git a/.gitignore b/.gitignore
index 1f94fa77f..33bf5abbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.idea
.gradle
build
+*.jar
diff --git a/app/app.iml b/app/app.iml
index 3ffde8fd5..a62797c67 100644
--- a/app/app.iml
+++ b/app/app.iml
@@ -104,6 +104,7 @@
+
diff --git a/app/build.gradle b/app/build.gradle
index b470c45fc..eb1ba58d7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -24,6 +24,7 @@ dependencies {
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4'
+ compile group: 'commons-io', name: 'commons-io', version: '2.5'
compile 'org.jetbrains:annotations-java5:15.0'
}
diff --git a/app/src/main/java/com/haishinkit/MainActivity.java b/app/src/main/java/com/haishinkit/MainActivity.java
index d6a9f303e..d8e14f38e 100644
--- a/app/src/main/java/com/haishinkit/MainActivity.java
+++ b/app/src/main/java/com/haishinkit/MainActivity.java
@@ -20,9 +20,10 @@ protected void onCreate(Bundle savedInstanceState) {
stream = new RTMPStream(connection);
cameraView = new CameraView(this);
+ stream.attachCamera(cameraView.getCamera());
setContentView(cameraView);
+
connection.connect("rtmp://192.168.179.3/live");
- //stream.attachCamera(cameraView.getCamera());
stream.publish("live");
}
}
diff --git a/app/src/main/java/com/haishinkit/amf/AMF0Serializer.java b/app/src/main/java/com/haishinkit/amf/AMF0Serializer.java
index fc7efc4fb..fa28ca2c9 100644
--- a/app/src/main/java/com/haishinkit/amf/AMF0Serializer.java
+++ b/app/src/main/java/com/haishinkit/amf/AMF0Serializer.java
@@ -95,6 +95,9 @@ public AMF0Serializer putObject(final Object value) {
if (value instanceof Integer) {
return putDouble(((Integer) value).doubleValue());
}
+ if (value instanceof Short) {
+ return putDouble(((Short) value).doubleValue());
+ }
if (value instanceof Boolean) {
return putBoolean((Boolean) value);
}
diff --git a/app/src/main/java/com/haishinkit/iso/AVCConfigurationRecord.java b/app/src/main/java/com/haishinkit/iso/AVCConfigurationRecord.java
index 8c8378421..ab70ccfb4 100644
--- a/app/src/main/java/com/haishinkit/iso/AVCConfigurationRecord.java
+++ b/app/src/main/java/com/haishinkit/iso/AVCConfigurationRecord.java
@@ -1,10 +1,6 @@
package com.haishinkit.iso;
import android.media.MediaFormat;
-
-import com.haishinkit.util.ByteBufferUtils;
-import com.haishinkit.util.Log;
-
import org.apache.commons.lang3.builder.ToStringBuilder;
import java.nio.ByteBuffer;
@@ -160,15 +156,15 @@ public byte getNALULength() {
}
public ByteBuffer toByteBuffer() {
- int capacity = 5 + 2;
+ int capacity = 5;
List sequenceParameterSets = getSequenceParameterSets();
List pictureParameterSets = getPictureParameterSets();
- for (int i = 0; i < sequenceParameterSets.size(); ++i) {
- capacity += 3;
+ for (byte[] sps : sequenceParameterSets) {
+ capacity += 3 + sps.length;
}
- for (int i = 0; i < pictureParameterSets.size(); ++i) {
- capacity += 3;
+ for (byte[] psp : pictureParameterSets) {
+ capacity += 3 + psp.length;
}
ByteBuffer buffer = ByteBuffer.allocate(capacity);
@@ -194,6 +190,8 @@ public ByteBuffer toByteBuffer() {
buffer.put(pps);
}
+ buffer.flip();
+
return buffer;
}
diff --git a/app/src/main/java/com/haishinkit/iso/AVCFormatUtils.java b/app/src/main/java/com/haishinkit/iso/AVCFormatUtils.java
new file mode 100644
index 000000000..5ddf3a19c
--- /dev/null
+++ b/app/src/main/java/com/haishinkit/iso/AVCFormatUtils.java
@@ -0,0 +1,34 @@
+package com.haishinkit.iso;
+
+import com.haishinkit.util.Log;
+
+import java.lang.reflect.Array;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public final class AVCFormatUtils {
+ private AVCFormatUtils() {
+ }
+
+ public final static ByteBuffer toNALFileFormat(final ByteBuffer buffer) {
+ ByteBuffer result = ByteBuffer.allocate(buffer.remaining());
+ result.put(buffer);
+ result.flip();
+ int length = 0;
+ int position = -1;
+ int remaining = result.remaining() - 3;
+ for (int i = 0; i < remaining; ++i) {
+ if (result.get(i) == 0x00 && result.get(i + 1) == 0x00 && result.get(i + 2) == 0x00 && result.get(i + 3) == 0x01) {
+ if (0 <= position) {
+ result.putInt(position, length - 3);
+ }
+ position = i;
+ length = 0;
+ } else {
+ ++length;
+ }
+ }
+ result.putInt(position, length);
+ return result;
+ }
+}
diff --git a/app/src/main/java/com/haishinkit/media/EncoderBase.java b/app/src/main/java/com/haishinkit/media/EncoderBase.java
index d1841bca0..8e9f48b8c 100644
--- a/app/src/main/java/com/haishinkit/media/EncoderBase.java
+++ b/app/src/main/java/com/haishinkit/media/EncoderBase.java
@@ -2,6 +2,10 @@
import android.media.MediaCodec;
+import com.haishinkit.util.Log;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
import java.nio.ByteBuffer;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -52,7 +56,7 @@ public final void stopRunning() {
running.set(false);
}
- public synchronized final void encodeBytes(byte[] data) {
+ public synchronized final void encodeBytes(byte[] data, long presentationTimeUs) {
if (!running.get()) {
return;
}
@@ -66,7 +70,7 @@ public synchronized final void encodeBytes(byte[] data) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(data);
- codec.queueInputBuffer(inputBufferIndex, 0, data.length, 0, 0);
+ codec.queueInputBuffer(inputBufferIndex, 0, data.length, presentationTimeUs, 0);
}
int outputBufferIndex = 0;
@@ -98,7 +102,7 @@ public synchronized final void encodeBytes(byte[] data) {
}
} while (0 <= outputBufferIndex);
} catch (Exception e) {
- e.printStackTrace();
+ Log.w(getClass().getName(), e.toString());
}
}
diff --git a/app/src/main/java/com/haishinkit/media/H264Encoder.java b/app/src/main/java/com/haishinkit/media/H264Encoder.java
index 4628986e0..b955eb955 100644
--- a/app/src/main/java/com/haishinkit/media/H264Encoder.java
+++ b/app/src/main/java/com/haishinkit/media/H264Encoder.java
@@ -24,7 +24,7 @@ protected MediaCodec createMediaCodec() throws IOException {
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
- mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
+ mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);
codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return codec;
}
diff --git a/app/src/main/java/com/haishinkit/media/IEncoder.java b/app/src/main/java/com/haishinkit/media/IEncoder.java
index ae3ccaf61..533a084a3 100644
--- a/app/src/main/java/com/haishinkit/media/IEncoder.java
+++ b/app/src/main/java/com/haishinkit/media/IEncoder.java
@@ -5,5 +5,5 @@
public interface IEncoder extends IRunnable {
public IEncoderListener getListener();
public IEncoder setListener(final IEncoderListener listener);
- public void encodeBytes(byte[] data);
+ public void encodeBytes(byte[] data, long presentationTimeUs);
}
diff --git a/app/src/main/java/com/haishinkit/net/Socket.java b/app/src/main/java/com/haishinkit/net/Socket.java
index e93fbc32a..4e4dce801 100644
--- a/app/src/main/java/com/haishinkit/net/Socket.java
+++ b/app/src/main/java/com/haishinkit/net/Socket.java
@@ -2,6 +2,7 @@
import com.haishinkit.util.Log;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import java.io.IOException;
@@ -35,11 +36,12 @@ public void run() {
network.start();
}
- public final void close() {
+ public void close(final boolean disconnected) {
+ IOUtils.closeQuietly(socket);
try {
- socket.close();
- } catch (IOException e) {
- Log.e(getClass().getName(), e.toString());
+ network.join();
+ } catch (InterruptedException e) {
+ Log.w(getClass().getName() + "#close", e.toString());
}
}
@@ -67,7 +69,7 @@ private void doInput() {
listen(buffer);
inputBuffer = buffer.slice();
} catch (IOException e) {
- Log.w(getClass().getName(), e.toString());
+ close(true);
}
}
@@ -80,14 +82,14 @@ private void doOutput() {
outputStream.flush();
outputQueue.remove(buffer);
} catch (IOException e) {
- Log.e(getClass().getName(), e.toString());
+ //IOUtils.closeQuietly(socket);
+ Log.e(getClass().getName() + "#doOutput", e.toString());
}
}
}
}
private void doConnection(final String dstName, final int dstPort) {
- Log.v(getClass().getName(), dstName + ":" + dstPort);
try {
socket = new java.net.Socket(dstName, dstPort);
if (socket.isConnected()) {
@@ -106,7 +108,8 @@ public void run() {
doInput();
}
} catch (Exception e) {
- Log.v(getClass().getName(), e.toString());
+ Log.e(getClass().getName() + "#doOutput", e.toString());
+ close(true);
}
}
diff --git a/app/src/main/java/com/haishinkit/rtmp/RTMPChunk.java b/app/src/main/java/com/haishinkit/rtmp/RTMPChunk.java
index e86030fd6..96a4a0aaa 100644
--- a/app/src/main/java/com/haishinkit/rtmp/RTMPChunk.java
+++ b/app/src/main/java/com/haishinkit/rtmp/RTMPChunk.java
@@ -1,14 +1,11 @@
package com.haishinkit.rtmp;
-import java.nio.BufferOverflowException;
import java.util.List;
import java.util.ArrayList;
import java.nio.ByteBuffer;
import com.haishinkit.lang.IRawValue;
import com.haishinkit.rtmp.messages.RTMPMessage;
-import com.haishinkit.util.ByteBufferUtils;
-import com.haishinkit.util.Log;
public enum RTMPChunk implements IRawValue {
ZERO((byte) 0x00),
@@ -51,13 +48,13 @@ public List encode(RTMPSocket socket, RTMPMessage message) {
throw new IllegalArgumentException();
}
- ByteBuffer payload = message.encode(socket);
+ final ByteBuffer payload = message.encode(socket);
payload.flip();
- List list = new ArrayList();
- int length = payload.limit();
- int timestamp = message.getTimestamp();
- int chunkSize = socket.getChunkSizeC();
+ final List list = new ArrayList();
+ final int length = payload.limit();
+ final int timestamp = message.getTimestamp();
+ final int chunkSize = socket.getChunkSizeS();
ByteBuffer buffer = ByteBuffer.allocate(length(message.getChunkStreamID()) + (length < chunkSize ? length : chunkSize));
message.setLength(length);
diff --git a/app/src/main/java/com/haishinkit/rtmp/RTMPConnection.java b/app/src/main/java/com/haishinkit/rtmp/RTMPConnection.java
index 1bb0d51c3..f93ff53ed 100644
--- a/app/src/main/java/com/haishinkit/rtmp/RTMPConnection.java
+++ b/app/src/main/java/com/haishinkit/rtmp/RTMPConnection.java
@@ -16,7 +16,7 @@
import com.haishinkit.net.IResponder;
import com.haishinkit.rtmp.messages.RTMPCommandMessage;
import com.haishinkit.rtmp.messages.RTMPMessage;
-import com.haishinkit.rtmp.messages.RTMPSetPeerBandwidthMessage;
+import com.haishinkit.rtmp.messages.RTMPSetChunkSizeMessage;
import com.haishinkit.util.EventUtils;
import com.haishinkit.util.Log;
@@ -145,8 +145,8 @@ public void handleEvent(final Event event) {
switch (data.get("code").toString()) {
case "NetConnection.Connect.Success":
connection.getSocket().setChunkSizeS(RTMPConnection.DEFAULT_CHUNK_SIZE_S);
- connection.getSocket().doOutput(RTMPChunk.ONE,
- new RTMPSetPeerBandwidthMessage()
+ connection.getSocket().doOutput(RTMPChunk.ZERO,
+ new RTMPSetChunkSizeMessage()
.setSize(RTMPConnection.DEFAULT_CHUNK_SIZE_S)
.setChunkStreamID(RTMPChunk.CONTROL)
);
@@ -266,7 +266,7 @@ public void close() {
if (!isConnected()) {
return;
}
- socket.close();
+ socket.close(false);
}
Map getMessages() {
@@ -297,14 +297,14 @@ void listen(final ByteBuffer buffer) {
if (!payload.hasRemaining()) {
payload.flip();
message.decode(payload).execute(this);
- Log.v(getClass().getName(), message.toString());
+ Log.w(getClass().getName(), message.toString());
payloads.remove(payload);
}
} else {
message = chunk.decode(streamID, this, buffer);
if (message.getLength() <= chunkSizeC) {
message.decode(buffer).execute(this);
- Log.v(getClass().getName(), message.toString());
+ Log.w(getClass().getName(), message.toString());
} else {
payload = ByteBuffer.allocate(message.getLength());
payload.put(buffer.array(), buffer.position(), chunkSizeC);
diff --git a/app/src/main/java/com/haishinkit/rtmp/RTMPMuxer.java b/app/src/main/java/com/haishinkit/rtmp/RTMPMuxer.java
index df982be59..6cfd1e93e 100644
--- a/app/src/main/java/com/haishinkit/rtmp/RTMPMuxer.java
+++ b/app/src/main/java/com/haishinkit/rtmp/RTMPMuxer.java
@@ -7,15 +7,18 @@
import com.haishinkit.flv.FlameType;
import com.haishinkit.flv.VideoCodec;
import com.haishinkit.iso.AVCConfigurationRecord;
+import com.haishinkit.iso.AVCFormatUtils;
import com.haishinkit.media.IEncoderListener;
import com.haishinkit.rtmp.messages.RTMPAVCVideoMessage;
import com.haishinkit.rtmp.messages.RTMPMessage;
+import com.haishinkit.util.ByteBufferUtils;
+import com.haishinkit.util.Log;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-public class RTMPMuxer implements IEncoderListener {
+public final class RTMPMuxer implements IEncoderListener {
private final RTMPStream stream;
private Map timestamps = new ConcurrentHashMap();
@@ -24,16 +27,17 @@ public RTMPMuxer(final RTMPStream stream) {
}
@Override
- public void onFormatChanged(final String mime, final MediaFormat mediaFormat) {
+ public final void onFormatChanged(final String mime, final MediaFormat mediaFormat) {
RTMPMessage message = null;
switch (mime) {
case "video/avc":
AVCConfigurationRecord config = new AVCConfigurationRecord(mediaFormat);
message = new RTMPAVCVideoMessage()
- .setFrame(FlameType.COMMAND.rawValue())
+ .setPacketType(AVCPacketType.SEQ.rawValue())
+ .setFrame(FlameType.KEY.rawValue())
.setCodec(VideoCodec.AVC.rawValue())
- .setFrame(AVCPacketType.SEQ.rawValue())
.setPayload(config.toByteBuffer())
+ .setChunkStreamID(RTMPChunk.VIDEO)
.setStreamID(stream.getId());
break;
default:
@@ -45,12 +49,10 @@ public void onFormatChanged(final String mime, final MediaFormat mediaFormat) {
}
@Override
- public void onSampleOutput(final String mime, final MediaCodec.BufferInfo info, final ByteBuffer buffer) {
+ public final void onSampleOutput(final String mime, final MediaCodec.BufferInfo info, final ByteBuffer buffer) {
int timestamp = 0;
- RTMPChunk chunk = RTMPChunk.ZERO;
RTMPMessage message = null;
if (timestamps.containsKey(mime)) {
- chunk = RTMPChunk.ONE;
timestamp = new Double(info.presentationTimeUs - timestamps.get(mime).doubleValue()).intValue();
}
switch (mime) {
@@ -60,7 +62,7 @@ public void onSampleOutput(final String mime, final MediaCodec.BufferInfo info,
.setPacketType(AVCPacketType.NAL.rawValue())
.setFrame(keyframe ? FlameType.KEY.rawValue() : FlameType.INTER.rawValue())
.setCodec(VideoCodec.AVC.rawValue())
- .setPayload(buffer)
+ .setPayload(AVCFormatUtils.toNALFileFormat(buffer))
.setChunkStreamID(RTMPChunk.VIDEO)
.setTimestamp(timestamp)
.setStreamID(stream.getId());
@@ -69,7 +71,7 @@ public void onSampleOutput(final String mime, final MediaCodec.BufferInfo info,
break;
}
if (message != null) {
- stream.connection.getSocket().doOutput(chunk, message);
+ stream.connection.getSocket().doOutput(RTMPChunk.ONE, message);
}
timestamps.put(mime, info.presentationTimeUs);
}
diff --git a/app/src/main/java/com/haishinkit/rtmp/RTMPSocket.java b/app/src/main/java/com/haishinkit/rtmp/RTMPSocket.java
index ee2a635c5..060af0898 100644
--- a/app/src/main/java/com/haishinkit/rtmp/RTMPSocket.java
+++ b/app/src/main/java/com/haishinkit/rtmp/RTMPSocket.java
@@ -2,9 +2,11 @@
import java.nio.ByteBuffer;
import com.haishinkit.net.Socket;
+import com.haishinkit.util.ByteBufferUtils;
import com.haishinkit.util.Log;
import com.haishinkit.rtmp.messages.RTMPMessage;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
public final class RTMPSocket extends Socket {
@@ -64,14 +66,15 @@ public void doOutput(final RTMPChunk chunk, final RTMPMessage message) {
if (chunk == null || message == null) {
throw new IllegalArgumentException();
}
+ Log.w(getClass().getName() + "#doOutput", message.toString());
for (ByteBuffer buffer : chunk.encode(this, message)) {
+ Log.w(getClass().getName() + "#doOutput", ByteBufferUtils.toHexString(buffer));
doOutput(buffer);
}
}
@Override
protected void onConnect() {
- Log.v(getClass().getName() + "#onConnect", "");
chunkSizeC = RTMPChunk.DEFAULT_SIZE;
chunkSizeS = RTMPChunk.DEFAULT_SIZE;
handshake.clear();
diff --git a/app/src/main/java/com/haishinkit/rtmp/RTMPStream.java b/app/src/main/java/com/haishinkit/rtmp/RTMPStream.java
index e8e4e4b90..fbd70ebef 100644
--- a/app/src/main/java/com/haishinkit/rtmp/RTMPStream.java
+++ b/app/src/main/java/com/haishinkit/rtmp/RTMPStream.java
@@ -10,8 +10,10 @@
import com.haishinkit.media.H264Encoder;
import com.haishinkit.media.IEncoder;
import com.haishinkit.rtmp.messages.RTMPCommandMessage;
+import com.haishinkit.rtmp.messages.RTMPDataMessage;
import com.haishinkit.rtmp.messages.RTMPMessage;
import com.haishinkit.util.EventUtils;
+import com.haishinkit.util.Log;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -129,15 +131,29 @@ public Byte rawValue() {
private ReadyState readyState = ReadyState.INITIALIZED;
private List messages = new ArrayList();
private final IEventListener listener = new EventListener(this);
+ private final Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {
+ @Override
+ public void onPreviewFrame(byte[] bytes, Camera camera) {
+ switch (readyState) {
+ case PUBLISHING:
+ getEncoderByName("video/avc").setListener(getMuxer());
+ getEncoderByName("video/avc").startRunning();
+ break;
+ default:
+ break;
+ }
+ getEncoderByName("video/avc").encodeBytes(bytes, System.currentTimeMillis());
+ }
+ };
public RTMPStream(final RTMPConnection connection) {
super(null);
- addEventListener(Event.RTMP_STATUS, listener);
this.connection = connection;
this.connection.addEventListener(Event.RTMP_STATUS, listener);
if (this.connection.isConnected()) {
this.connection.createStream(this);
}
+ addEventListener(Event.RTMP_STATUS, listener);
}
public void attachCamera(final Camera camera) {
@@ -148,14 +164,7 @@ public void attachCamera(final Camera camera) {
parameters.setPreviewFormat(ImageFormat.NV21);
parameters.setPreviewSize(320, 240);
camera.setParameters(parameters);
- camera.setPreviewCallback(new Camera.PreviewCallback() {
- @Override
- public void onPreviewFrame(byte[] bytes, Camera camera) {
- getEncoderByName("video/avc").encodeBytes(bytes);
- }
- });
- // TODO: For debugging
- //getEncoderByName("video/avc").startRunning();
+ camera.setPreviewCallback(previewCallback);
}
public void publish(final String name) {
@@ -239,6 +248,17 @@ public void play(final Object... arguments) {
}
}
+ public void send(final String handlerName, final Object... arguments) {
+ if (readyState == ReadyState.INITIALIZED || readyState == ReadyState.CLOSED) {
+ return;
+ }
+ connection.getSocket().doOutput(RTMPChunk.ZERO, new RTMPDataMessage(connection.getObjectEncoding())
+ .setHandlerName(handlerName)
+ .setArguments(arguments == null ? null : Arrays.asList(arguments))
+ .setStreamID(getId())
+ );
+ }
+
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
@@ -267,6 +287,7 @@ RTMPStream setReadyState(final ReadyState readyState) {
messages.clear();
break;
case PUBLISHING:
+ // send("@setDataFrame", "onMetaData", createMetaData());
for (IEncoder encoder : encoders.values()) {
encoder.setListener(getMuxer());
encoder.startRunning();
@@ -284,6 +305,11 @@ protected RTMPMuxer getMuxer() {
return muxer;
}
+ protected Map createMetaData() {
+ Map data = new HashMap();
+ return data;
+ }
+
protected IEncoder getEncoderByName(final String mime) {
if (!encoders.containsKey(mime)) {
switch (mime) {
diff --git a/app/src/main/java/com/haishinkit/rtmp/messages/RTMPDataMessage.java b/app/src/main/java/com/haishinkit/rtmp/messages/RTMPDataMessage.java
index dc46980e1..2ce035fd4 100644
--- a/app/src/main/java/com/haishinkit/rtmp/messages/RTMPDataMessage.java
+++ b/app/src/main/java/com/haishinkit/rtmp/messages/RTMPDataMessage.java
@@ -1,10 +1,13 @@
package com.haishinkit.rtmp.messages;
import com.haishinkit.amf.AMF0Deserializer;
+import com.haishinkit.amf.AMF0Serializer;
import com.haishinkit.rtmp.RTMPConnection;
import com.haishinkit.rtmp.RTMPObjectEncoding;
import com.haishinkit.rtmp.RTMPSocket;
+import org.apache.commons.lang3.NotImplementedException;
+
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +44,19 @@ public RTMPDataMessage setArguments(final List