/*
* 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.server;
import com.flazr.rtmp.message.BytesRead;
import com.flazr.rtmp.message.ChunkSize;
import com.flazr.rtmp.message.Control;
import com.flazr.rtmp.RtmpMessage;
import com.flazr.rtmp.RtmpReader;
import com.flazr.rtmp.RtmpPublisher;
import com.flazr.rtmp.RtmpWriter;
import com.flazr.rtmp.message.Audio;
import com.flazr.rtmp.message.Command;
import com.flazr.rtmp.message.DataMessage;
import com.flazr.rtmp.message.Metadata;
import com.flazr.rtmp.message.SetPeerBw;
import com.flazr.rtmp.message.Video;
import com.flazr.rtmp.message.WindowAckSize;
import com.flazr.util.ChannelUtils;
import java.util.ArrayList;
import java.util.List;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
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.SimpleChannelHandler;
import org.jboss.netty.channel.WriteCompletionEvent;
import org.jboss.netty.channel.group.ChannelGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ChannelPipelineCoverage("one")
public class ServerHandler extends SimpleChannelHandler {
private static final Logger logger = LoggerFactory.getLogger(ServerHandler.class);
private int bytesReadWindow = 2500000;
private long bytesRead;
private long bytesReadLastSent;
private long bytesWritten;
private int bytesWrittenWindow = 2500000;
private int bytesWrittenLastReceived;
private ServerApplication application;
private String clientId;
private String playName;
private int streamId;
private int bufferDuration;
private RtmpPublisher publisher;
private ServerStream subscriberStream;
private RtmpWriter recorder;
private boolean aggregateModeEnabled = true;
public void setAggregateModeEnabled(boolean aggregateModeEnabled) {
this.aggregateModeEnabled = aggregateModeEnabled;
}
@Override
public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) {
RtmpServer.CHANNELS.add(e.getChannel());
logger.info("opened channel: {}", e);
}
@Override
public void exceptionCaught(final ChannelHandlerContext ctx, final ExceptionEvent e) {
ChannelUtils.exceptionCaught(e);
}
@Override
public void channelClosed(final ChannelHandlerContext ctx, final ChannelStateEvent e) {
logger.info("channel closed: {}", e);
if(publisher != null) {
publisher.close();
}
if(recorder != null) {
recorder.close();
}
unpublishIfLive();
}
@Override
public void writeComplete(final ChannelHandlerContext ctx, final WriteCompletionEvent e) throws Exception {
bytesWritten += e.getWrittenAmount();
super.writeComplete(ctx, e);
}
@Override
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent me) {
if(publisher != null && publisher.handle(me)) {
return;
}
final Channel channel = me.getChannel();
final RtmpMessage message = (RtmpMessage) me.getMessage();
bytesRead += message.getHeader().getSize();
if((bytesRead - bytesReadLastSent) > bytesReadWindow) {
logger.info("sending bytes read ack after: {}", bytesRead);
BytesRead ack = new BytesRead(bytesRead);
channel.write(ack);
bytesReadLastSent = bytesRead;
}
switch(message.getHeader().getMessageType()) {
case CHUNK_SIZE: // handled by decoder
break;
case CONTROL:
final Control control = (Control) message;
switch(control.getType()) {
case SET_BUFFER:
logger.debug("received set buffer: {}", control);
bufferDuration = control.getBufferLength();
if(publisher != null) {
publisher.setBufferDuration(bufferDuration);
}
break;
default:
logger.info("ignored control: {}", control);
}
break;
case COMMAND_AMF0:
case COMMAND_AMF3:
final Command command = (Command) message;
final String name = command.getName();
if(name.equals("connect")) {
connectResponse(channel, command);
} else if(name.equals("createStream")) {
streamId = 1;
channel.write(Command.createStreamSuccess(command.getTransactionId(), streamId));
} else if(name.equals("play")) {
playResponse(channel, command);
} else if(name.equals("deleteStream")) {
int deleteStreamId = ((Double) command.getArg(0)).intValue();
logger.info("deleting stream id: {}", deleteStreamId);
// TODO ?
} else if(name.equals("closeStream")) {
final int clientStreamId = command.getHeader().getStreamId();
logger.info("closing stream id: {}", clientStreamId); // TODO
unpublishIfLive();
} else if(name.equals("pause")) {
pauseResponse(channel, command);
} else if(name.equals("seek")) {
seekResponse(channel, command);
} else if(name.equals("publish")) {
publishResponse(channel, command);
} else {
logger.warn("ignoring command: {}", command);
fireNext(channel);
}
return; // NOT break
case METADATA_AMF0:
case METADATA_AMF3:
final Metadata meta = (Metadata) message;
if(meta.getName().equals("onMetaData")) {
logger.info("adding onMetaData message: {}", meta);
meta.setDuration(-1);
subscriberStream.addConfigMessage(meta);
}
broadcast(message);
break;
case AUDIO:
case VIDEO:
if(((DataMessage) message).isConfig()) {
logger.info("adding config message: {}", message);
subscriberStream.addConfigMessage(message);
}
case AGGREGATE:
broadcast(message);
break;
case BYTES_READ:
final BytesRead bytesReadByClient = (BytesRead) message;
bytesWrittenLastReceived = bytesReadByClient.getValue();
logger.debug("bytes read ack from client: {}, actual: {}", bytesReadByClient, bytesWritten);
break;
case WINDOW_ACK_SIZE:
WindowAckSize was = (WindowAckSize) message;
if(was.getValue() != bytesReadWindow) {
channel.write(SetPeerBw.dynamic(bytesReadWindow));
}
break;
case SET_PEER_BW:
SetPeerBw spb = (SetPeerBw) message;
if(spb.getValue() != bytesWrittenWindow) {
channel.write(new WindowAckSize(bytesWrittenWindow));
}
break;
default:
logger.warn("ignoring message: {}", message);
}
fireNext(channel);
}
private void fireNext(final Channel channel) {
if(publisher != null && publisher.isStarted() && !publisher.isPaused()) {
publisher.fireNext(channel, 0);
}
}
//==========================================================================
private RtmpMessage[] getStartMessages(final RtmpMessage variation) {
final List<RtmpMessage> list = new ArrayList<RtmpMessage>();
list.add(new ChunkSize(4096));
list.add(Control.streamIsRecorded(streamId));
list.add(Control.streamBegin(streamId));
if(variation != null) {
list.add(variation);
}
list.add(Command.playStart(playName, clientId));
list.add(Metadata.rtmpSampleAccess());
list.add(Audio.empty());
list.add(Metadata.dataStart());
return list.toArray(new RtmpMessage[list.size()]);
}
private void broadcast(final RtmpMessage message) {
subscriberStream.getSubscribers().write(message);
if(recorder != null) {
recorder.write(message);
}
}
private void writeToStream(final Channel channel, final RtmpMessage message) {
if(message.getHeader().getChannelId() > 2) {
message.getHeader().setStreamId(streamId);
}
channel.write(message);
}
//==========================================================================
private void connectResponse(final Channel channel, final Command connect) {
final String appName = (String) connect.getObject().get("app");
clientId = channel.getId() + "";
application = ServerApplication.get(appName); // TODO auth, validation
logger.info("connect, client id: {}, application: {}", clientId, application);
channel.write(new WindowAckSize(bytesWrittenWindow));
channel.write(SetPeerBw.dynamic(bytesReadWindow));
channel.write(Control.streamBegin(streamId));
final Command result = Command.connectSuccess(connect.getTransactionId());
channel.write(result);
channel.write(Command.onBWDone());
}
private void playResponse(final Channel channel, final Command play) {
int playStart = -2;
int playLength = -1;
if(play.getArgCount() > 1) {
playStart = ((Double) play.getArg(1)).intValue();
}
if(play.getArgCount() > 2) {
playLength = ((Double) play.getArg(2)).intValue();
}
final boolean playReset;
if(play.getArgCount() > 3) {
playReset = ((Boolean) play.getArg(3));
} else {
playReset = true;
}
final Command playResetCommand = playReset ? Command.playReset(playName, clientId) : null;
final String clientPlayName = (String) play.getArg(0);
final ServerStream stream = application.getStream(clientPlayName);
logger.debug("play name {}, start {}, length {}, reset {}",
new Object[]{clientPlayName, playStart, playLength, playReset});
if(stream.isLive()) {
for(final RtmpMessage message : getStartMessages(playResetCommand)) {
writeToStream(channel, message);
}
boolean videoConfigPresent = false;
for(RtmpMessage message : stream.getConfigMessages()) {
logger.info("writing start meta / config: {}", message);
if(message.getHeader().isVideo()) {
videoConfigPresent = true;
}
writeToStream(channel, message);
}
if(!videoConfigPresent) {
writeToStream(channel, Video.empty());
}
stream.getSubscribers().add(channel);
logger.info("client requested live stream: {}, added to stream: {}", clientPlayName, stream);
return;
}
if(!clientPlayName.equals(playName)) {
playName = clientPlayName;
final RtmpReader reader = application.getReader(playName);
if(reader == null) {
channel.write(Command.playFailed(playName, clientId));
return;
}
publisher = new RtmpPublisher(reader, streamId, bufferDuration, true, aggregateModeEnabled) {
@Override protected RtmpMessage[] getStopMessages(long timePosition) {
return new RtmpMessage[] {
Metadata.onPlayStatus(timePosition / (double) 1000, bytesWritten),
Command.playStop(playName, clientId),
Control.streamEof(streamId)
};
}
};
}
publisher.start(channel, playStart, playLength, getStartMessages(playResetCommand));
}
private void pauseResponse(final Channel channel, final Command command) {
if(publisher == null) {
logger.debug("cannot pause when live");
return;
}
final boolean paused = ((Boolean) command.getArg(0));
final int clientTimePosition = ((Double) command.getArg(1)).intValue();
logger.debug("pause request: {}, client time position: {}", paused, clientTimePosition);
if(!paused) {
logger.debug("doing unpause, seeking and playing");
final Command unpause = Command.unpauseNotify(playName, clientId);
publisher.start(channel, clientTimePosition, getStartMessages(unpause));
} else {
publisher.pause();
}
}
private void seekResponse(final Channel channel, final Command command) {
if(publisher == null) {
logger.debug("cannot seek when live");
return;
}
final int clientTimePosition = ((Double) command.getArg(0)).intValue();
if (!publisher.isPaused()) {
final Command seekNotify = Command.seekNotify(streamId, clientTimePosition, playName, clientId);
publisher.start(channel, clientTimePosition, getStartMessages(seekNotify));
} else {
logger.debug("ignoring seek when paused, client time position: {}", clientTimePosition);
}
}
private void publishResponse(final Channel channel, final Command command) {
if(command.getArgCount() > 1) { // publish
final String streamName = (String) command.getArg(0);
final String publishTypeString = (String) command.getArg(1);
logger.info("publish, stream name: {}, type: {}", streamName, publishTypeString);
subscriberStream = application.getStream(streamName, publishTypeString); // TODO append, record
if(subscriberStream.getPublisher() != null) {
logger.info("disconnecting publisher client, stream already in use");
ChannelFuture future = channel.write(Command.publishBadName(streamId));
future.addListener(ChannelFutureListener.CLOSE);
return;
}
subscriberStream.setPublisher(channel);
channel.write(Command.publishStart(streamName, clientId, streamId));
channel.write(new ChunkSize(4096));
channel.write(Control.streamBegin(streamId));
final ServerStream.PublishType publishType = subscriberStream.getPublishType();
logger.info("created publish stream: {}", subscriberStream);
switch(publishType) {
case LIVE:
final ChannelGroup subscribers = subscriberStream.getSubscribers();
subscribers.write(Command.publishNotify(streamId));
writeToStream(subscribers, Video.empty());
writeToStream(subscribers, Metadata.rtmpSampleAccess());
writeToStream(subscribers, Audio.empty());
writeToStream(subscribers, Metadata.dataStart());
break;
case RECORD:
recorder = application.getWriter(streamName);
break;
case APPEND:
logger.warn("append not implemented yet, un-publishing...");
unpublishIfLive();
break;
}
} else { // un-publish
final boolean publish = (Boolean) command.getArg(0);
if(!publish) {
unpublishIfLive();
}
}
}
// TODO cleanup
private void writeToStream(final ChannelGroup channelGroup, final RtmpMessage message) {
if(message.getHeader().getChannelId() > 2) {
message.getHeader().setStreamId(streamId);
}
channelGroup.write(message);
}
private void unpublishIfLive() {
if(subscriberStream != null && subscriberStream.getPublisher() != null) {
final Channel channel = subscriberStream.getPublisher();
if(channel.isWritable()) {
channel.write(Command.unpublishSuccess(subscriberStream.getName(), clientId, streamId));
}
subscriberStream.getSubscribers().write(Command.unpublishNotify(streamId));
subscriberStream.setPublisher(null);
logger.debug("publisher disconnected, stream un-published");
}
if(recorder != null) {
recorder.close();
recorder = null;
}
}
}