/* * 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 com.flazr.io.flv.FlvWriter; import com.flazr.rtmp.LoopedReader; 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.BytesRead; import com.flazr.rtmp.message.ChunkSize; import com.flazr.rtmp.message.WindowAckSize; import com.flazr.rtmp.message.Command; import com.flazr.rtmp.message.Metadata; import com.flazr.rtmp.message.SetPeerBw; import com.flazr.util.ChannelUtils; import com.flazr.util.Utils; import java.util.HashMap; import java.util.Map; 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.SimpleChannelUpstreamHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ChannelPipelineCoverage("one") public class ClientHandler extends SimpleChannelUpstreamHandler { private static final Logger logger = LoggerFactory.getLogger(ClientHandler.class); private int transactionId = 1; private Map<Integer, String> transactionToCommandMap; private ClientOptions options; private byte[] swfvBytes; private RtmpWriter writer; private int bytesReadWindow = 2500000; private long bytesRead; private long bytesReadLastSent; private int bytesWrittenWindow = 2500000; private RtmpPublisher publisher; private int streamId; public void setSwfvBytes(byte[] swfvBytes) { this.swfvBytes = swfvBytes; logger.info("set swf verification bytes: {}", Utils.toHex(swfvBytes)); } public ClientHandler(ClientOptions options) { this.options = options; transactionToCommandMap = new HashMap<Integer, String>(); } private 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); } @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(); } if(publisher != null) { publisher.close(); } super.channelClosed(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(); switch(message.getHeader().getMessageType()) { case CHUNK_SIZE: // handled by decoder break; case CONTROL: Control control = (Control) message; logger.debug("control: {}", control); switch(control.getType()) { case PING_REQUEST: final int time = control.getTime(); logger.debug("server ping: {}", time); Control pong = Control.pingResponse(time); logger.debug("sending ping response: {}", pong); channel.write(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); channel.write(swfv); } break; case STREAM_BEGIN: if(publisher != null && !publisher.isStarted()) { publisher.start(channel, options.getStart(), options.getLength(), new ChunkSize(4096)); return; } if(streamId !=0) { channel.write(Control.setBuffer(streamId, options.getBuffer())); } break; default: logger.debug("ignoring control message: {}", control); } break; case METADATA_AMF0: case METADATA_AMF3: Metadata metadata = (Metadata) message; if(metadata.getName().equals("onMetaData")) { logger.debug("writing 'onMetaData': {}", metadata); writer.write(message); } else { logger.debug("ignoring metadata: {}", metadata); } break; case AUDIO: case VIDEO: case AGGREGATE: writer.write(message); bytesRead += message.getHeader().getSize(); if((bytesRead - bytesReadLastSent) > bytesReadWindow) { logger.debug("sending bytes read ack {}", bytesRead); bytesReadLastSent = bytesRead; channel.write(new BytesRead(bytesRead)); } break; case COMMAND_AMF0: case COMMAND_AMF3: Command command = (Command) message; String name = command.getName(); logger.debug("server command: {}", name); if(name.equals("_result")) { String resultFor = transactionToCommandMap.get(command.getTransactionId()); logger.info("result for method call: {}", resultFor); if(resultFor.equals("connect")) { writeCommandExpectingResult(channel, Command.createStream()); } else if(resultFor.equals("createStream")) { streamId = ((Double) command.getArg(0)).intValue(); logger.debug("streamId to use: {}", streamId); if(options.getPublishType() != null) { // TODO append, record RtmpReader reader; if(options.getFileToPublish() != null) { reader = RtmpPublisher.getReader(options.getFileToPublish()); } else { reader = options.getReaderToPublish(); } if(options.getLoop() > 1) { reader = new LoopedReader(reader, options.getLoop()); } publisher = new RtmpPublisher(reader, streamId, options.getBuffer(), false, false) { @Override protected RtmpMessage[] getStopMessages(long timePosition) { return new RtmpMessage[]{Command.unpublish(streamId)}; } }; channel.write(Command.publish(streamId, options)); return; } else { writer = options.getWriterToSave(); if(writer == null) { writer = new FlvWriter(options.getStart(), options.getSaveAs()); } channel.write(Command.play(streamId, options)); channel.write(Control.setBuffer(streamId, 0)); } } else { logger.warn("un-handled server result for: {}", resultFor); } } else if(name.equals("onStatus")) { final Map<String, Object> temp = (Map) command.getArg(0); final String code = (String) temp.get("code"); logger.info("onStatus code: {}", code); if (code.equals("NetStream.Failed") // TODO cleanup || code.equals("NetStream.Play.Failed") || code.equals("NetStream.Play.Stop") || code.equals("NetStream.Play.StreamNotFound")) { logger.info("disconnecting, code: {}, bytes read: {}", code, bytesRead); channel.close(); return; } if(code.equals("NetStream.Publish.Start") && publisher != null && !publisher.isStarted()) { publisher.start(channel, options.getStart(), options.getLength(), new ChunkSize(4096)); return; } if (publisher != null && code.equals("NetStream.Unpublish.Success")) { logger.info("unpublish success, closing channel"); ChannelFuture future = channel.write(Command.closeStream(streamId)); future.addListener(ChannelFutureListener.CLOSE); return; } } else if(name.equals("close")) { logger.info("server called close, closing channel"); channel.close(); return; } else if(name.equals("_error")) { logger.error("closing channel, server resonded with error: {}", command); channel.close(); return; } else { logger.warn("ignoring server command: {}", command); } break; case BYTES_READ: logger.info("ack from server: {}", message); 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.info("ignoring rtmp message: {}", message); } if(publisher != null && publisher.isStarted()) { // TODO better state machine publisher.fireNext(channel, 0); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { ChannelUtils.exceptionCaught(e); } }