/*
* Flazr <http://flazr.com> Copyright (C) 2009 Peter Thomas.
*
* This file is part of Flazr.
*
* Flazr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flazr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Flazr. If not, see <http://www.gnu.org/licenses/>.
*/
package com.flazr.rtmp.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.red5.server.api.so.IClientSharedObject;
import org.red5.server.so.ClientSharedObject;
import org.red5.server.so.SharedObjectMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.flazr.io.flv.FlvWriter;
import com.flazr.rtmp.LoopedReader;
import com.flazr.rtmp.RtmpMessage;
import com.flazr.rtmp.RtmpPublisher;
import com.flazr.rtmp.RtmpReader;
import com.flazr.rtmp.RtmpWriter;
import com.flazr.rtmp.message.ChunkSize;
import com.flazr.rtmp.message.Command;
import com.flazr.rtmp.message.Control;
import com.flazr.rtmp.message.Metadata;
import com.flazr.rtmp.message.SetPeerBw;
import com.flazr.rtmp.message.WindowAckSize;
import com.flazr.util.ChannelUtils;
import com.flazr.util.Utils;
@SuppressWarnings("deprecation")
@ChannelPipelineCoverage("one")
public class MultiStreamHandler extends SimpleChannelUpstreamHandler {
protected static final Logger logger = LoggerFactory.getLogger(MultiStreamHandler.class);
private int transactionId = 1;
protected Map<Integer, String> transactionToCommandMap;
protected ClientOptions options;
protected byte[] swfvBytes;
/**
* Shared objects map
*/
private volatile ConcurrentMap<String, ClientSharedObject> sharedObjects = new ConcurrentHashMap<String, ClientSharedObject>();
protected RtmpWriter writer;
protected int bytesReadWindow = 2500000;
protected int bytesWrittenWindow = 2500000;
protected List<Stream> publishStreamList = new ArrayList<Stream>();
protected boolean connected;
protected class Stream {
public static final int WAITING_FOR_CREATE_COMMAND = 1;
public static final int WAITING_FOR_BEGIN_COMMAND = 2;
public static final int WAITING_FOR_CREATE_RESULT = 3;
public static final int STARTED = 4;
public static final int CLOSED = 0;
public String streamName;
public int streamId;
public int state;
public RtmpPublisher publisher;
public RtmpReader reader;
}
private Map<Integer, Integer> streamChannel = new HashMap<Integer, Integer>();
protected synchronized int holdChannel(int streamId) {
Integer next = streamChannel.get(streamId);
if (next != null)
return next;
if (streamChannel.isEmpty())
next = 8;
else
next = Collections.max(streamChannel.values()) + 1;
streamChannel.put(streamId, next);
return next;
}
protected synchronized void releaseChannel(int streamId) {
streamChannel.remove(streamId);
}
/**
* Connect to client shared object.
*
* @param name Client shared object name
* @param persistent SO persistence flag
* @return Client shared object instance
*/
public synchronized IClientSharedObject getSharedObject(String name, boolean persistent) {
logger.debug("getSharedObject name: {} persistent {}", new Object[] { name, persistent });
ClientSharedObject result = sharedObjects.get(name);
if (result != null) {
if (result.isPersistentObject() != persistent) {
throw new RuntimeException("Already connected to a shared object with this name, but with different persistence.");
}
return result;
}
result = new ClientSharedObject(name, persistent);
sharedObjects.put(name, result);
return result;
}
/** {@inheritDoc} */
protected void onSharedObject(Channel channel, SharedObjectMessage object) {
logger.debug("onSharedObject");
ClientSharedObject so = sharedObjects.get(object.getName());
if (so == null) {
logger.error("Ignoring request for non-existend SO: {}", object);
return;
}
if (so.isPersistentObject() != object.isPersistent()) {
logger.error("Ignoring request for wrong-persistent SO: {}", object);
return;
}
logger.debug("Received SO request: {}", object);
so.dispatchEvent(object);
}
public void setSwfvBytes(byte[] swfvBytes) {
this.swfvBytes = swfvBytes;
logger.info("set swf verification bytes: {}", Utils.toHex(swfvBytes));
}
public MultiStreamHandler(ClientOptions options) {
this.options = options;
transactionToCommandMap = new HashMap<Integer, String>();
}
public void writeCommandExpectingResult(Channel channel, Command command) {
final int id = transactionId++;
command.setTransactionId(id);
transactionToCommandMap.put(id, command.getName());
logger.info("sending command (expecting result): {}", command);
channel.write(command);
}
public void writeCommand(Channel channel, RtmpMessage message) {
logger.info("sending command (without expecting result): {}", message);
channel.write(message);
}
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
logger.info("channel opened: {}", e);
super.channelOpen(ctx, e);
}
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
logger.info("handshake complete, sending 'connect'");
writeCommandExpectingResult(e.getChannel(), Command.connect(options));
}
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
logger.info("channel closed: {}", e);
if(writer != null) {
writer.close();
}
for (Stream stream : publishStreamList) {
if (stream.publisher != null)
stream.publisher.close();
}
super.channelClosed(ctx, e);
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent me) {
boolean handledByPublisher = false;
for (Stream stream : publishStreamList) {
if (stream.publisher != null && stream.publisher.handle(me))
handledByPublisher = true;
}
if (handledByPublisher)
return;
final Channel channel = me.getChannel();
final RtmpMessage message = (RtmpMessage) me.getMessage();
switch(message.getHeader().getMessageType()) {
case CHUNK_SIZE: onChunkSize(channel, message); break;
case CONTROL: onControl(channel, (Control) message); break;
case METADATA_AMF0:
case METADATA_AMF3: onMetadata(channel, (Metadata) message); break;
case AUDIO:
case VIDEO:
case AGGREGATE: onMultimedia(channel, message); break;
case COMMAND_AMF0:
case COMMAND_AMF3: onCommand(channel, (Command) message); break;
case BYTES_READ: onBytesRead(channel, message); break;
case WINDOW_ACK_SIZE: onWindowAckSize(channel, (WindowAckSize) message); break;
case SET_PEER_BW: onSetPeerBw(channel, (SetPeerBw) message); break;
case SHARED_OBJECT_AMF0:
case SHARED_OBJECT_AMF3: onSharedObject(channel, (SharedObjectMessage) message); break;
default: onUnknown(channel, message); break;
}
// don't put together received messages handling and publishing stuff
// if(publisher != null && publisher.isStarted()) { // TODO better state machine
// publisher.fireNext(channel, 0);
// }
if (connected) {
for (Stream stream : publishStreamList) {
if (stream.state == Stream.WAITING_FOR_CREATE_COMMAND) {
stream.state = Stream.WAITING_FOR_CREATE_RESULT;
writeCommandExpectingResult(channel, Command.createStream());
}
}
}
}
protected void onUnknown(Channel channel, RtmpMessage message) {
logger.info("ignoring rtmp message: {}", message);
}
protected void onSetPeerBw(Channel channel, SetPeerBw spb) {
logger.debug("set peer bandwidth: " + spb);
if(spb.getValue() != bytesWrittenWindow) {
writeCommand(channel, new WindowAckSize(bytesWrittenWindow));
}
}
protected void onWindowAckSize(Channel channel, WindowAckSize was) {
logger.debug("window ack size: " + was);
if(was.getValue() != bytesReadWindow) {
writeCommand(channel, SetPeerBw.dynamic(bytesReadWindow));
}
}
protected void onBytesRead(Channel channel, RtmpMessage message) {
// logger.debug("ack from server: {}", message);
}
protected void onCommand(Channel channel, Command command) {
String name = command.getName();
logger.debug("server command: {}", name);
if(name.equals("_result")) {
String resultFor = transactionToCommandMap.get(command.getTransactionId());
if (resultFor == null) {
logger.warn("result for method without tracked transaction");
} else {
onCommandResult(channel, command, resultFor);
}
} else if(name.equals("onStatus")) {
@SuppressWarnings("unchecked")
final Map<String, Object> args = (Map<String, Object>) command.getArg(0);
onCommandStatus(channel, command, args);
} else if(name.equals("close")) {
logger.info("server called close, closing channel");
channel.close();
} else if(name.equals("_error")) {
logger.error("closing channel, server resonded with error: {}", command);
channel.close();
} else {
onCommandCustom(channel, command, name);
}
}
protected void onCommandCustom(Channel channel, Command command, String name) {
logger.warn("ignoring server command: {}", command);
}
protected void onCommandStatus(Channel channel, Command command,
Map<String, Object> args) {
final String code = (String) args.get("code");
final String level = (String) args.get("level");
final String description = (String) args.get("description");
final String application = (String) args.get("application");
final String messageStr = level + " onStatus message, code: " + code + ", description: " + description + ", application: " + application;
final int streamId = ((Double) args.get("clientid")).intValue();
Stream stream = getStreamById(streamId);
logger.debug("=========================> onCommandStatus Command: " + command);
logger.debug("=========================> streamId: " + streamId);
logger.debug("=========================> publishStreamList.size(): " + publishStreamList.size());
logger.debug("=========================> streamName: " + stream.streamName);
// http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/events/NetStatusEvent.html
if (level.equals("status")) {
logger.info(messageStr);
if (code.equals("NetStream.Publish.Start")
&& stream != null && stream.publisher != null && !stream.publisher.isStarted()) {
logger.debug("starting the publisher after NetStream.Publish.Start");
stream.publisher.start(channel, options.getStart(), options.getLength(), new ChunkSize(4096));
stream.state = Stream.STARTED;
} else if (code.equals("NetStream.Unpublish.Success")
&& stream != null && stream.publisher != null) {
logger.info("unpublish success, closing channel");
closeStream(channel, stream);
// ChannelFuture future = writeCommand(channel, Command.closeStream(streamId));
// future.addListener(ChannelFutureListener.CLOSE);
} else if (code.equals("NetStream.Play.Stop")) {
closeStream(channel, stream);
// channel.close();
}
} else if (level.equals("warning")) {
logger.warn(messageStr);
if (code.equals("NetStream.Play.InsufficientBW")) {
closeStream(channel, stream);
// ChannelFuture future = writeCommand(channel, Command.closeStream(streamId));
// future.addListener(ChannelFutureListener.CLOSE);
}
} else if (level.equals("error")) {
logger.error(messageStr);
// channel.close();
}
}
private void closeStream(Channel channel, Stream stream) {
writeCommand(channel, Command.closeStream(stream.streamId));
stream.state = Stream.CLOSED;
}
protected void onCommandResult(Channel channel, Command command,
String resultFor) {
logger.info("result for method call: {}", resultFor);
if (resultFor.equals("connect")) {
connected = true;
} else if (resultFor.equals("createStream")) {
onCommandResultCreateStream(channel, command);
} else {
logger.warn("un-handled server result for: {}", resultFor);
}
}
protected void onCommandResultCreateStream(Channel channel, Command command) {
final int streamId = ((Double) command.getArg(0)).intValue();
logger.debug("streamId to use: {}", streamId);
if(options.getPublishType() != null) { // TODO append, record
Stream stream = getStreamByState(Stream.WAITING_FOR_CREATE_RESULT);
if (stream == null) {
logger.error("Inconsistent state - received a create stream result, but there's no stream to publish");
return;
}
RtmpReader reader;
if(options.getFileToPublish() != null) {
reader = RtmpPublisher.getReader(options.getFileToPublish());
} else {
reader = stream.reader;
}
if(options.getLoop() > 1) {
reader = new LoopedReader(reader, options.getLoop());
}
// \TODO check the "useSharedTimer" argument
stream.publisher = new RtmpPublisher(reader, streamId, options.getBuffer(), false, false) {
@Override protected RtmpMessage[] getStopMessages(long timePosition) {
return new RtmpMessage[]{Command.unpublish(streamId)};
}
};
ClientOptions tmpOptions = new ClientOptions();
tmpOptions.setStreamName(stream.streamName);
tmpOptions.setPublishType(options.getPublishType());
stream.streamId = streamId;
stream.state = Stream.WAITING_FOR_BEGIN_COMMAND;
Command publish = Command.publish(streamId, holdChannel(streamId), tmpOptions);
stream.publisher.setChannelId(publish.getHeader().getChannelId());
writeCommand(channel, publish);
return;
} else {
writer = options.getWriterToSave();
if(writer == null && options.getSaveAs() != null) {
writer = new FlvWriter(options.getStart(), options.getSaveAs());
}
writeCommand(channel, Command.play(streamId, options));
writeCommand(channel, Control.setBuffer(streamId, 0));
}
}
protected void onMultimedia(Channel channel, RtmpMessage message) {
if (writer != null)
writer.write(message);
logger.debug("=========================> onMultimedia RtmpMessage: " + message);
// bytesRead += message.getHeader().getSize();
// if((bytesRead - bytesReadLastSent) > bytesReadWindow) {
// logger.debug("sending bytes read ack {}", bytesRead);
// bytesReadLastSent = bytesRead;
// writeCommand(channel, new BytesRead(bytesRead));
// }
}
protected void onMetadata(Channel channel, Metadata metadata) {
if(metadata.getName().equals("onMetaData") && writer != null) {
logger.debug("writing 'onMetaData': {}", metadata);
writer.write(metadata);
} else {
logger.debug("ignoring metadata: {}", metadata);
}
}
protected Stream getStreamById(int id) {
for (Stream s : publishStreamList)
if (s.streamId == id)
return s;
return null;
}
protected Stream getStreamByState(int state) {
for (Stream s : publishStreamList)
if (s.state == state)
return s;
return null;
}
protected void onControl(Channel channel, Control control) {
if (control.getType() != Control.Type.PING_REQUEST)
logger.debug("control: {}", control);
switch(control.getType()) {
case PING_REQUEST:
final int time = control.getTime();
Control pong = Control.pingResponse(time);
// logger.debug("server ping: {}", time);
// logger.debug("sending ping response: {}", pong);
if (channel.isWritable())
writeCommand(channel, pong);
break;
case SWFV_REQUEST:
if(swfvBytes == null) {
logger.warn("swf verification not initialized!"
+ " not sending response, server likely to stop responding / disconnect");
} else {
Control swfv = Control.swfvResponse(swfvBytes);
logger.info("sending swf verification response: {}", swfv);
writeCommand(channel, swfv);
}
break;
case STREAM_BEGIN:
int streamId = control.getStreamId();
Stream stream = getStreamById(streamId);
if (stream != null && stream.publisher != null && !stream.publisher.isStarted()) {
stream.publisher.start(channel, options.getStart(),
options.getLength(), new ChunkSize(4096));
stream.state = Stream.STARTED;
return;
}
// streamId == 0 is not a real stream
if (streamId != 0) {
writeCommand(channel, Control.setBuffer(streamId, options.getBuffer()));
}
break;
default:
logger.debug("ignoring control message: {}", control);
}
}
protected void onChunkSize(Channel channel, RtmpMessage message) {
// handled by decoder
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
ChannelUtils.exceptionCaught(e);
}
public void addPublishStream(String streamName, RtmpReader reader) {
logger.debug("=========================> Adding a stream to the publishStreamList");
Stream stream = new Stream();
stream.streamName = streamName;
stream.state = Stream.WAITING_FOR_CREATE_COMMAND;
stream.reader = reader;
publishStreamList.add(stream);
}
}