package com.laifeng.sopcastsdk.stream.sender.rtmp.io; import android.util.Log; import com.laifeng.sopcastsdk.entity.Frame; import com.laifeng.sopcastsdk.stream.amf.AmfMap; import com.laifeng.sopcastsdk.stream.amf.AmfNull; import com.laifeng.sopcastsdk.stream.amf.AmfNumber; import com.laifeng.sopcastsdk.stream.amf.AmfObject; import com.laifeng.sopcastsdk.stream.amf.AmfString; import com.laifeng.sopcastsdk.stream.packer.rtmp.RtmpPacker; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Abort; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Audio; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.ChunkHeader; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Command; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Data; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Handshake; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Chunk; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.MessageType; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.UserControl; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.Video; import com.laifeng.sopcastsdk.stream.sender.rtmp.packets.WindowAckSize; import com.laifeng.sopcastsdk.stream.sender.sendqueue.ISendQueue; import com.laifeng.sopcastsdk.stream.sender.sendqueue.NormalSendQueue; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Main RTMP connection implementation class * * @author francois, leoma */ public class RtmpConnection implements OnReadListener, OnWriteListener { private static final String TAG = "RtmpConnection"; private static final Pattern rtmpUrlPattern = Pattern.compile("^rtmp://([^/:]+)(:(\\d+))*/([^/]+)(/(.*))*$"); private RtmpConnectListener listener; private Socket socket; private SessionInfo sessionInfo; private ReadThread readThread; private WriteThread writeThread; private State state = State.INIT; private int transactionIdCounter = 0; private int currentStreamId = -1; private ConnectData connectData; private int videoWidth, videoHeight; private int audioSampleRate, audioSampleSize; private boolean isAudioStereo; private boolean publishPermitted; private ISendQueue mSendQueue; public enum State { INIT, HANDSHAKE, CONNECTING, CREATE_STREAM, PUBLISHING, LIVING } public static class ConnectData { public String appName; public String streamName; public String swfUrl; public String tcUrl; public String pageUrl; public int port; public String host; } public void setConnectListener(RtmpConnectListener listener) { this.listener = listener; } public void setSendQueue(ISendQueue sendQueue) { mSendQueue = sendQueue; } public void connect(String url) { state = State.INIT; connectData = parseRtmpUrl(url); if(connectData == null) { if(listener != null) { listener.onUrlInvalid(); } return; } String host = connectData.host; int port = connectData.port; String appName = connectData.appName; String streamName = connectData.streamName; Log.d(TAG, "connect() called. Host: " + host + ", port: " + port + ", appName: " + appName + ", publishPath: " + streamName); socket = new Socket(); SocketAddress socketAddress = new InetSocketAddress(host, port); try { socket.connect(socketAddress, 3000); } catch (IOException e) { e.printStackTrace(); if(listener != null) { listener.onSocketConnectFail(); } return; } if(listener != null) { listener.onSocketConnectSuccess(); } BufferedInputStream in = null; BufferedOutputStream out = null; state = State.HANDSHAKE; try { Log.d(TAG, "connect(): socket connection established, doing handhake..."); in = new BufferedInputStream(socket.getInputStream()); out = new BufferedOutputStream(socket.getOutputStream()); handshake(in, out); } catch (IOException e) { e.printStackTrace(); state = State.INIT; clearSocket(); if(listener != null) { listener.onHandshakeFail(); } return; } if(listener != null) { listener.onHandshakeSuccess(); } sessionInfo = new SessionInfo(); readThread = new ReadThread(in, sessionInfo); writeThread = new WriteThread(out, sessionInfo); readThread.setOnReadListener(this); writeThread.setWriteListener(this); writeThread.setSendQueue(mSendQueue); readThread.start(); writeThread.start(); rtmpConnect(); } private ConnectData parseRtmpUrl(String url) { ConnectData connectData = null; Matcher matcher = rtmpUrlPattern.matcher(url); if (matcher.matches()) { connectData = new ConnectData(); connectData.tcUrl = url.substring(0, url.lastIndexOf('/')); connectData.swfUrl = ""; connectData.pageUrl = ""; connectData.host = matcher.group(1); String portStr = matcher.group(3); connectData.port = portStr != null ? Integer.parseInt(portStr) : 1935; connectData.appName = matcher.group(4); connectData.streamName = matcher.group(6); } return connectData; } private void handshake(InputStream in, OutputStream out) throws IOException { Handshake handshake = new Handshake(); handshake.writeC0(out); handshake.writeC1(out); // Write C1 without waiting for S0 out.flush(); handshake.readS0(in); handshake.readS1(in); handshake.writeC2(out); handshake.readS2(in); } private void clearSocket() { if (socket != null && socket.isConnected()) { try { socket.close(); socket = null; } catch (Exception e) { e.printStackTrace(); } } } private void rtmpConnect() { SessionInfo.markSessionTimestampTx(); Command invoke = new Command("connect", ++transactionIdCounter); AmfObject args = new AmfObject(); args.setProperty("app", connectData.appName); args.setProperty("flashVer", "LNX 11,2,202,233"); // Flash player OS: Linux, version: 11.2.202.233 args.setProperty("swfUrl", connectData.swfUrl); args.setProperty("tcUrl", connectData.tcUrl); args.setProperty("fpad", false); args.setProperty("capabilities", 239); args.setProperty("audioCodecs", 3575); args.setProperty("videoCodecs", 252); args.setProperty("videoFunction", 1); args.setProperty("pageUrl", connectData.pageUrl); args.setProperty("objectEncoding", 0); invoke.addData(args); Frame<Chunk> frame = new Frame(invoke, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame); state = State.CONNECTING; } @Override public void onChunkRead(Chunk chunk) { ChunkHeader chunkHeader = chunk.getChunkHeader(); MessageType messageType = chunkHeader.getMessageType(); switch (messageType) { case ABORT: readThread.clearStoredChunks(((Abort) chunk).getChunkStreamId()); break; case USER_CONTROL_MESSAGE: UserControl ping = (UserControl) chunk; if(ping.getType() == UserControl.Type.PING_REQUEST) { Log.d(TAG, "Sending PONG reply.."); UserControl pong = new UserControl(); pong.setType(UserControl.Type.PONG_REPLY); pong.setEventData(ping.getEventData()[0]); Frame<Chunk> frame = new Frame(pong, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame); } else if(ping.getType() == UserControl.Type.STREAM_EOF) { Log.d(TAG, "Stream EOF reached"); } break; case WINDOW_ACKNOWLEDGEMENT_SIZE: WindowAckSize windowAckSize = (WindowAckSize) chunk; int size = windowAckSize.getAcknowledgementWindowSize(); Log.d(TAG, "Setting acknowledgement window size: " + size); sessionInfo.setAcknowledgmentWindowSize(size); // Set socket option try { socket.setSendBufferSize(size); } catch (SocketException e) { e.printStackTrace(); } break; case SET_PEER_BANDWIDTH: int acknowledgementWindowsize = sessionInfo.getAcknowledgementWindowSize(); Log.d(TAG, "Send acknowledgement window size: " + acknowledgementWindowsize); Chunk setPeerBandwidth = new WindowAckSize(acknowledgementWindowsize); Frame<Chunk> frame = new Frame(setPeerBandwidth, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame); break; case COMMAND_AMF0: handleRxCommandInvoke((Command) chunk); break; default: Log.w(TAG, "Not handling unimplemented/unknown packet of type: " + chunkHeader.getMessageType()); break; } } private void handleRxCommandInvoke(Command command) { String commandName = command.getCommandName(); if(commandName.equals("_result")) { String method = sessionInfo.takeInvokedCommand(command.getTransactionId()); Log.d(TAG, "Got result for invoked method: " + method); if ("connect".equals(method)) { if(listener != null) { listener.onRtmpConnectSuccess(); } createStream(); } else if("createStream".equals(method)) { currentStreamId = (int) ((AmfNumber) command.getData().get(1)).getValue(); if(listener != null) { listener.onCreateStreamSuccess(); } fmlePublish(); } } else if(commandName.equals("_error")) { String method = sessionInfo.takeInvokedCommand(command.getTransactionId()); Log.d(TAG, "Got error for invoked method: " + method); if ("connect".equals(method)) { stop(); if(listener != null) { listener.onRtmpConnectFail(); } } else if("createStream".equals(method)) { stop(); if(listener != null) { listener.onCreateStreamFail(); } } } else if(commandName.equals("onStatus")) { String code = ((AmfString) ((AmfObject) command.getData().get(1)).getProperty("code")).getValue(); if (code.equals("NetStream.Publish.Start")) { Log.d(TAG, "Got publish start success"); state = State.LIVING; if(listener != null) { listener.onPublishSuccess(); } onMetaData(); // We can now publish AV data publishPermitted = true; } else { Log.d(TAG, "Got publish start fail"); stop(); if(listener != null) { listener.onPublishFail(); } } } else { Log.d(TAG, "Got Command result: " + commandName); } } private void createStream() { state = State.CREATE_STREAM; Log.d(TAG, "createStream(): Sending releaseStream command..."); // transactionId == 2 Command releaseStream = new Command("releaseStream", ++transactionIdCounter); releaseStream.getChunkHeader().setChunkStreamId(SessionInfo.RTMP_STREAM_CHANNEL); releaseStream.addData(new AmfNull()); // command object: null for "createStream" releaseStream.addData(connectData.streamName); // command object: null for "releaseStream" Frame<Chunk> frame1 = new Frame(releaseStream, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame1); Log.d(TAG, "createStream(): Sending FCPublish command..."); // transactionId == 3 Command FCPublish = new Command("FCPublish", ++transactionIdCounter); FCPublish.getChunkHeader().setChunkStreamId(SessionInfo.RTMP_STREAM_CHANNEL); FCPublish.addData(new AmfNull()); // command object: null for "FCPublish" FCPublish.addData(connectData.streamName); Frame<Chunk> frame2 = new Frame(FCPublish, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame2); Log.d(TAG, "createStream(): Sending createStream command..."); // transactionId == 4 Command createStream = new Command("createStream", ++transactionIdCounter); createStream.addData(new AmfNull()); // command object: null for "createStream" Frame<Chunk> frame3 = new Frame(createStream, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame3); } private void fmlePublish() { if (currentStreamId == -1 || connectData == null) { return; } state = State.PUBLISHING; Log.d(TAG, "fmlePublish(): Sending publish command..."); // transactionId == 0 Command publish = new Command("publish", 0); publish.getChunkHeader().setChunkStreamId(SessionInfo.RTMP_STREAM_CHANNEL); publish.getChunkHeader().setMessageStreamId(currentStreamId); publish.addData(new AmfNull()); // command object: null for "publish" publish.addData(connectData.streamName); publish.addData("live"); Frame<Chunk> frame = new Frame(publish, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame); } private void onMetaData() { if (currentStreamId == -1) { return; } Log.d(TAG, "onMetaData(): Sending empty onMetaData..."); Data metadata = new Data("@setDataFrame"); metadata.getChunkHeader().setMessageStreamId(currentStreamId); metadata.addData("onMetaData"); AmfMap ecmaArray = new AmfMap(); ecmaArray.setProperty("duration", 0); ecmaArray.setProperty("width", videoWidth); ecmaArray.setProperty("height", videoHeight); ecmaArray.setProperty("videodatarate", 0); ecmaArray.setProperty("framerate", 0); ecmaArray.setProperty("audiodatarate", 0); ecmaArray.setProperty("audiosamplerate", audioSampleRate); ecmaArray.setProperty("audiosamplesize", audioSampleSize); ecmaArray.setProperty("stereo", isAudioStereo); ecmaArray.setProperty("filesize", 0); metadata.addData(ecmaArray); Frame<Chunk> frame = new Frame(metadata, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame); } public void setVideoParams(int width, int height) { videoWidth = width; videoHeight = height; } public void setAudioParams(int sampleRate, int sampleSize, boolean isStereo) { audioSampleRate = sampleRate; audioSampleSize = sampleSize; isAudioStereo = isStereo; } public void publishAudioData(byte[] data, int type) { if (currentStreamId == -1) { return; } if (!publishPermitted) { return; } Audio audio = new Audio(); audio.setData(data); audio.getChunkHeader().setMessageStreamId(currentStreamId); Frame<Chunk> frame; if(type == RtmpPacker.FIRST_AUDIO) { frame = new Frame(audio, type, Frame.FRAME_TYPE_CONFIGURATION); } else { frame = new Frame(audio, type, Frame.FRAME_TYPE_AUDIO); } mSendQueue.putFrame(frame); } public void publishVideoData(byte[] data, int type) { if (currentStreamId == -1) { return; } if (!publishPermitted) { return; } Video video = new Video(); video.setData(data); video.getChunkHeader().setMessageStreamId(currentStreamId); Frame<Chunk> frame; if(type == RtmpPacker.FIRST_VIDEO) { frame = new Frame(video, type, Frame.FRAME_TYPE_CONFIGURATION); } else if(type == RtmpPacker.KEY_FRAME){ frame = new Frame(video, type, Frame.FRAME_TYPE_KEY_FRAME); } else { frame = new Frame(video, type, Frame.FRAME_TYPE_INTER_FRAME); } mSendQueue.putFrame(frame); } public void closeStream() throws IllegalStateException { if (currentStreamId == -1) { return; } if (!publishPermitted) { return; } Log.d(TAG, "closeStream(): setting current stream ID to -1"); Command closeStream = new Command("closeStream", 0); closeStream.getChunkHeader().setChunkStreamId(SessionInfo.RTMP_STREAM_CHANNEL); closeStream.getChunkHeader().setMessageStreamId(currentStreamId); closeStream.addData(new AmfNull()); Frame<Chunk> frame = new Frame(closeStream, RtmpPacker.CONFIGRATION, Frame.FRAME_TYPE_CONFIGURATION); mSendQueue.putFrame(frame); } public void stop() { closeStream(); if(readThread != null) { readThread.setOnReadListener(null); readThread.shutdown(); } if(writeThread != null) { writeThread.setWriteListener(null); writeThread.shutdown(); } clearSocket(); currentStreamId = -1; transactionIdCounter = 0; state = State.INIT; } public State getState() { return state; } @Override public void onStreamEnd() { stop(); if(listener != null) { listener.onStreamEnd(); } } @Override public void onDisconnect() { stop(); if(listener != null) { listener.onSocketDisconnect(); } } }