/* * RED5 Open Source Media Server - https://github.com/Red5/ Copyright 2006-2016 by respective authors (see below). All rights reserved. Licensed under the Apache License, Version * 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless * required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package org.red5.server.stream.consumer; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.mina.core.buffer.IoBuffer; import org.red5.server.api.stream.IClientStream; import org.red5.server.messaging.IMessage; import org.red5.server.messaging.IMessageComponent; import org.red5.server.messaging.IPipe; import org.red5.server.messaging.IPipeConnectionListener; import org.red5.server.messaging.IPushableConsumer; import org.red5.server.messaging.OOBControlMessage; import org.red5.server.messaging.PipeConnectionEvent; import org.red5.server.net.rtmp.Channel; import org.red5.server.net.rtmp.RTMPConnection; import org.red5.server.net.rtmp.event.AudioData; import org.red5.server.net.rtmp.event.BaseEvent; import org.red5.server.net.rtmp.event.BytesRead; import org.red5.server.net.rtmp.event.ChunkSize; import org.red5.server.net.rtmp.event.FlexStreamSend; import org.red5.server.net.rtmp.event.IRTMPEvent; import org.red5.server.net.rtmp.event.Notify; import org.red5.server.net.rtmp.event.Ping; import org.red5.server.net.rtmp.event.VideoData; import org.red5.server.net.rtmp.message.Constants; import org.red5.server.net.rtmp.message.Header; import org.red5.server.stream.message.RTMPMessage; import org.red5.server.stream.message.ResetMessage; import org.red5.server.stream.message.StatusMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * RTMP connection consumer. */ public class ConnectionConsumer implements IPushableConsumer, IPipeConnectionListener { private static final Logger log = LoggerFactory.getLogger(ConnectionConsumer.class); /** * Connection consumer class name */ public static final String KEY = ConnectionConsumer.class.getName(); /** * Connection object */ private RTMPConnection conn; /** * Video channel */ private Channel video; /** * Audio channel */ private Channel audio; /** * Data channel */ private Channel data; /** * Chunk size. Packets are sent chunk-by-chunk. */ private int chunkSize = 1024; //TODO: Not sure of the best value here /** * Whether or not the chunk size has been sent. This seems to be required for h264. */ private AtomicBoolean chunkSizeSent = new AtomicBoolean(false); /** * Create RTMP connection consumer for given connection and channels. * * @param conn * RTMP connection * @param videoChannel * Video channel * @param audioChannel * Audio channel * @param dataChannel * Data channel */ public ConnectionConsumer(RTMPConnection conn, Channel videoChannel, Channel audioChannel, Channel dataChannel) { log.debug("Channel ids - video: {} audio: {} data: {}", new Object[] { videoChannel, audioChannel, dataChannel }); this.conn = conn; this.video = videoChannel; this.audio = audioChannel; this.data = dataChannel; } /** * Create connection consumer without an RTMP connection. * * @param videoChannel * video channel * @param audioChannel * audio channel * @param dataChannel * data channel */ public ConnectionConsumer(Channel videoChannel, Channel audioChannel, Channel dataChannel) { this.video = videoChannel; this.audio = audioChannel; this.data = dataChannel; } /** {@inheritDoc} */ public void pushMessage(IPipe pipe, IMessage message) { //log.trace("pushMessage - type: {}", message.getMessageType()); if (message instanceof ResetMessage) { //ignore } else if (message instanceof StatusMessage) { StatusMessage statusMsg = (StatusMessage) message; data.sendStatus(statusMsg.getBody()); } else if (message instanceof RTMPMessage) { // make sure chunk size has been sent sendChunkSize(); // cast to rtmp message RTMPMessage rtmpMsg = (RTMPMessage) message; IRTMPEvent msg = rtmpMsg.getBody(); // get timestamp int eventTime = msg.getTimestamp(); log.debug("Message timestamp: {}", eventTime); if (eventTime < 0) { eventTime += Integer.MIN_VALUE; msg.setTimestamp(eventTime); log.debug("Message has negative timestamp, applying {} offset: {}", Integer.MIN_VALUE, eventTime); } // get the data type byte dataType = msg.getDataType(); if (log.isTraceEnabled()) { log.trace("Data type: {} source type: {}", dataType, ((BaseEvent) msg).getSourceType()); } // create a new header for the consumer final Header header = new Header(); header.setTimerBase(eventTime); // data buffer IoBuffer buf = null; switch (dataType) { case Constants.TYPE_AGGREGATE: //log.trace("Aggregate data"); data.write(msg); break; case Constants.TYPE_AUDIO_DATA: //log.trace("Audio data"); buf = ((AudioData) msg).getData(); if (buf != null) { AudioData audioData = new AudioData(buf.asReadOnlyBuffer()); audioData.setHeader(header); audioData.setTimestamp(header.getTimer()); audioData.setSourceType(((AudioData) msg).getSourceType()); audio.write(audioData); } else { log.warn("Audio data was not found"); } break; case Constants.TYPE_VIDEO_DATA: //log.trace("Video data"); buf = ((VideoData) msg).getData(); if (buf != null) { VideoData videoData = new VideoData(buf.asReadOnlyBuffer()); videoData.setHeader(header); videoData.setTimestamp(header.getTimer()); videoData.setSourceType(((VideoData) msg).getSourceType()); video.write(videoData); } else { log.warn("Video data was not found"); } break; case Constants.TYPE_PING: //log.trace("Ping"); Ping ping = (Ping) msg; ping.setHeader(header); conn.ping(ping); break; case Constants.TYPE_STREAM_METADATA: if (log.isTraceEnabled()) { log.trace("Meta data: {}", (Notify) msg); } //Notify notify = new Notify(((Notify) msg).getData().asReadOnlyBuffer()); Notify notify = (Notify) msg; notify.setHeader(header); notify.setTimestamp(header.getTimer()); data.write(notify); break; case Constants.TYPE_FLEX_STREAM_SEND: //if (log.isTraceEnabled()) { //log.trace("Flex stream send: {}", (Notify) msg); //} FlexStreamSend send = null; if (msg instanceof FlexStreamSend) { send = (FlexStreamSend) msg; } else { send = new FlexStreamSend(((Notify) msg).getData().asReadOnlyBuffer()); } send.setHeader(header); send.setTimestamp(header.getTimer()); data.write(send); break; case Constants.TYPE_BYTES_READ: //log.trace("Bytes read"); BytesRead bytesRead = (BytesRead) msg; bytesRead.setHeader(header); bytesRead.setTimestamp(header.getTimer()); conn.getChannel((byte) 2).write(bytesRead); break; default: //log.trace("Default: {}", dataType); data.write(msg); } } else { log.debug("Unhandled push message: {}", message); if (log.isTraceEnabled()) { Class<? extends IMessage> clazz = message.getClass(); log.trace("Class info - name: {} declaring: {} enclosing: {}", new Object[] { clazz.getName(), clazz.getDeclaringClass(), clazz.getEnclosingClass() }); } } } /** {@inheritDoc} */ public void onPipeConnectionEvent(PipeConnectionEvent event) { if (event.getType().equals(PipeConnectionEvent.EventType.PROVIDER_DISCONNECT)) { closeChannels(); } } /** {@inheritDoc} */ public void onOOBControlMessage(IMessageComponent source, IPipe pipe, OOBControlMessage oobCtrlMsg) { if ("ConnectionConsumer".equals(oobCtrlMsg.getTarget())) { String serviceName = oobCtrlMsg.getServiceName(); log.trace("Service name: {}", serviceName); if ("pendingCount".equals(serviceName)) { oobCtrlMsg.setResult(conn.getPendingMessages()); } else if ("pendingVideoCount".equals(serviceName)) { IClientStream stream = conn.getStreamByChannelId(video.getId()); if (stream != null) { oobCtrlMsg.setResult(conn.getPendingVideoMessages(stream.getStreamId())); } else { oobCtrlMsg.setResult(0L); } } else if ("writeDelta".equals(serviceName)) { //TODO: Revisit the max stream value later long maxStream = 120 * 1024; // Return the current delta between sent bytes and bytes the client // reported to have received, and the interval the client should use // for generating BytesRead messages (half of the allowed bandwidth). oobCtrlMsg.setResult(new Long[] { conn.getWrittenBytes() - conn.getClientBytesRead(), maxStream / 2 }); } else if ("chunkSize".equals(serviceName)) { int newSize = (Integer) oobCtrlMsg.getServiceParamMap().get("chunkSize"); if (newSize != chunkSize) { chunkSize = newSize; chunkSizeSent.set(false); sendChunkSize(); } } } } /** * Send the chunk size */ private void sendChunkSize() { if (chunkSizeSent.compareAndSet(false, true)) { log.debug("Sending chunk size: {}", chunkSize); ChunkSize chunkSizeMsg = new ChunkSize(chunkSize); conn.getChannel((byte) 2).write(chunkSizeMsg); } } /** * Close all the channels */ private void closeChannels() { conn.closeChannel(video.getId()); conn.closeChannel(audio.getId()); conn.closeChannel(data.getId()); } }