org.red5.server.stream.ClientBroadcastStream.java Source code

Java tutorial

Introduction

Here is the source code for org.red5.server.stream.ClientBroadcastStream.java

Source

/*
 * 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;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.StandardMBean;

import org.apache.commons.lang3.StringUtils;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.codec.IAudioStreamCodec;
import org.red5.codec.IStreamCodecInfo;
import org.red5.codec.IVideoStreamCodec;
import org.red5.codec.StreamCodecInfo;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
import org.red5.server.api.event.IEvent;
import org.red5.server.api.event.IEventDispatcher;
import org.red5.server.api.event.IEventListener;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.statistics.IClientBroadcastStreamStatistics;
import org.red5.server.api.statistics.support.StatisticsCounter;
import org.red5.server.api.stream.IClientBroadcastStream;
import org.red5.server.api.stream.IStreamAwareScopeHandler;
import org.red5.server.api.stream.IStreamCapableConnection;
import org.red5.server.api.stream.IStreamListener;
import org.red5.server.api.stream.IStreamPacket;
import org.red5.server.jmx.mxbeans.ClientBroadcastStreamMXBean;
import org.red5.server.messaging.IConsumer;
import org.red5.server.messaging.IFilter;
import org.red5.server.messaging.IMessage;
import org.red5.server.messaging.IMessageComponent;
import org.red5.server.messaging.IMessageOutput;
import org.red5.server.messaging.IPipe;
import org.red5.server.messaging.IPipeConnectionListener;
import org.red5.server.messaging.IProvider;
import org.red5.server.messaging.IPushableConsumer;
import org.red5.server.messaging.OOBControlMessage;
import org.red5.server.messaging.PipeConnectionEvent;
import org.red5.server.net.rtmp.event.AudioData;
import org.red5.server.net.rtmp.event.IRTMPEvent;
import org.red5.server.net.rtmp.event.Invoke;
import org.red5.server.net.rtmp.event.Notify;
import org.red5.server.net.rtmp.event.VideoData;
import org.red5.server.net.rtmp.status.Status;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.stream.message.RTMPMessage;
import org.red5.server.stream.message.StatusMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jmx.export.annotation.ManagedResource;

/**
 * Represents live stream broadcasted from client. As Flash Media Server, Red5 supports recording mode for live streams, that is, broadcasted stream has broadcast mode. It can be either "live" or "record" and latter causes server-side application to record broadcasted stream.
 *
 * Note that recorded streams are recorded as FLV files.
 *
 * This type of stream uses two different pipes for live streaming and recording.
 * 
 * @author The Red5 Project
 * @author Steven Gong
 * @author Paul Gregoire (mondain@gmail.com)
 * @author Vladimir Hmelyoff (vlhm@splitmedialabs.com)
 */
@ManagedResource(objectName = "org.red5.server:type=ClientBroadcastStream", description = "ClientBroadcastStream")
public class ClientBroadcastStream extends AbstractClientStream
        implements IClientBroadcastStream, IFilter, IPushableConsumer, IPipeConnectionListener, IEventDispatcher,
        IClientBroadcastStreamStatistics, ClientBroadcastStreamMXBean {

    private static final Logger log = LoggerFactory.getLogger(ClientBroadcastStream.class);

    /**
     * Whether or not to automatically record the associated stream.
     */
    protected boolean automaticRecording;

    /**
     * Total number of bytes received.
     */
    protected long bytesReceived;

    /**
     * Is there need to check video codec?
     */
    protected boolean checkVideoCodec = false;

    /**
     * Is there need to check audio codec?
     */
    protected boolean checkAudioCodec = false;

    /**
     * Data is sent by chunks, each of them has size
     */
    protected int chunkSize;

    /**
     * Is this stream still active?
     */
    protected volatile boolean closed;

    /**
     * Output endpoint that providers use
     */
    protected transient IMessageOutput connMsgOut;

    /**
     * Stores timestamp of first packet
     */
    protected long firstPacketTime = -1;

    /**
     * Pipe for live streaming
     */
    protected transient IPipe livePipe;

    /**
     * Stream published name
     */
    protected String publishedName;

    /**
     * Streaming parameters
     */
    protected Map<String, String> parameters;

    /**
     * Is there need to send start notification?
     */
    protected boolean sendStartNotification = true;

    /**
     * Stores statistics about subscribers.
     */
    private transient StatisticsCounter subscriberStats = new StatisticsCounter();

    /**
     * Listeners to get notified about received packets.
     */
    protected transient Set<IStreamListener> listeners = new CopyOnWriteArraySet<IStreamListener>();

    /**
     * Recording listener
     */
    private transient WeakReference<IRecordingListener> recordingListener;

    protected long latestTimeStamp = -1;

    /**
     * Whether or not to register with JMX.
     */
    private boolean registerJMX = true;

    /**
     * Check and send notification if necessary
     * 
     * @param event
     *            Event
     */
    private void checkSendNotifications(IEvent event) {
        IEventListener source = event.getSource();
        sendStartNotifications(source);
    }

    /**
     * Closes stream, unsubscribes provides, sends stoppage notifications and broadcast close notification.
     */
    public void close() {
        log.debug("Stream close: {}", publishedName);
        if (closed) {
            log.debug("{} already closed", publishedName);
            return;
        }
        closed = true;
        if (livePipe != null) {
            livePipe.unsubscribe((IProvider) this);
        }
        // if we have a recording listener, inform that this stream is done
        if (recordingListener != null) {
            sendRecordStopNotify();
            notifyRecordingStop();
            // inform the listener to finish and close
            recordingListener.get().stop();
        }
        sendPublishStopNotify();
        // TODO: can we send the client something to make sure he stops sending data?
        if (connMsgOut != null) {
            connMsgOut.unsubscribe(this);
        }
        notifyBroadcastClose();
        // clear the listener after all the notifications have been sent
        if (recordingListener != null) {
            recordingListener.clear();
        }
        // clear listeners
        if (!listeners.isEmpty()) {
            listeners.clear();
        }
        // deregister with jmx
        unregisterJMX();
    }

    /**
     * Dispatches event
     * 
     * @param event
     *            Event to dispatch
     */
    public void dispatchEvent(IEvent event) {
        if (event instanceof IRTMPEvent && !closed) {
            switch (event.getType()) {
            case STREAM_CONTROL:
            case STREAM_DATA:
                // create the event
                IRTMPEvent rtmpEvent;
                try {
                    rtmpEvent = (IRTMPEvent) event;
                } catch (ClassCastException e) {
                    log.error("Class cast exception in event dispatch", e);
                    return;
                }
                int eventTime = -1;
                if (log.isTraceEnabled()) {
                    // If this is first packet save its timestamp; expect it is
                    // absolute? no matter: it's never used!
                    if (firstPacketTime == -1) {
                        firstPacketTime = rtmpEvent.getTimestamp();
                        log.trace(String.format("CBS=@%08x: rtmpEvent=%s creation=%s firstPacketTime=%d",
                                System.identityHashCode(this), rtmpEvent.getClass().getSimpleName(), creationTime,
                                firstPacketTime));
                    } else {
                        log.trace(
                                String.format("CBS=@%08x: rtmpEvent=%s creation=%s firstPacketTime=%d timestamp=%d",
                                        System.identityHashCode(this), rtmpEvent.getClass().getSimpleName(),
                                        creationTime, firstPacketTime, rtmpEvent.getTimestamp()));
                    }
                }
                //get the buffer only once per call
                IoBuffer buf = null;
                if (rtmpEvent instanceof IStreamData && (buf = ((IStreamData<?>) rtmpEvent).getData()) != null) {
                    bytesReceived += buf.limit();
                }
                // get stream codec
                IStreamCodecInfo codecInfo = getCodecInfo();
                StreamCodecInfo info = null;
                if (codecInfo instanceof StreamCodecInfo) {
                    info = (StreamCodecInfo) codecInfo;
                }
                //log.trace("Stream codec info: {}", info);
                if (rtmpEvent instanceof AudioData) {
                    IAudioStreamCodec audioStreamCodec = null;
                    if (checkAudioCodec) {
                        // dont try to read codec info from 0 length audio packets
                        if (buf.limit() > 0) {
                            audioStreamCodec = AudioCodecFactory.getAudioCodec(buf);
                            if (info != null) {
                                info.setAudioCodec(audioStreamCodec);
                            }
                            checkAudioCodec = false;
                        }
                    } else if (codecInfo != null) {
                        audioStreamCodec = codecInfo.getAudioCodec();
                    }
                    if (audioStreamCodec != null) {
                        audioStreamCodec.addData(buf);
                    }
                    if (info != null) {
                        info.setHasAudio(true);
                    }
                    eventTime = rtmpEvent.getTimestamp();
                    log.trace("Audio: {}", eventTime);
                } else if (rtmpEvent instanceof VideoData) {
                    IVideoStreamCodec videoStreamCodec = null;
                    if (checkVideoCodec) {
                        videoStreamCodec = VideoCodecFactory.getVideoCodec(buf);
                        if (info != null) {
                            info.setVideoCodec(videoStreamCodec);
                        }
                        checkVideoCodec = false;
                    } else if (codecInfo != null) {
                        videoStreamCodec = codecInfo.getVideoCodec();
                    }
                    if (videoStreamCodec != null) {
                        videoStreamCodec.addData(buf);
                    }
                    if (info != null) {
                        info.setHasVideo(true);
                    }
                    eventTime = rtmpEvent.getTimestamp();
                    log.trace("Video: {}", eventTime);
                } else if (rtmpEvent instanceof Invoke) {
                    Invoke invokeEvent = (Invoke) rtmpEvent;
                    log.debug("Invoke action: {}", invokeEvent.getAction());
                    eventTime = rtmpEvent.getTimestamp();
                    // event / stream listeners will not be notified of invokes
                    return;
                } else if (rtmpEvent instanceof Notify) {
                    Notify notifyEvent = (Notify) rtmpEvent;
                    log.debug("Notify action: {}", notifyEvent.getAction());
                    if (notifyEvent.getAction() != null && notifyEvent.getAction().equals("onMetaData")) {
                        // store the metadata
                        try {
                            log.debug("Setting metadata");
                            metaData = notifyEvent.duplicate();
                        } catch (Exception e) {
                            log.warn("Metadata could not be duplicated for this stream", e);
                        }
                    }
                    eventTime = rtmpEvent.getTimestamp();
                }
                // update last event time
                if (eventTime > latestTimeStamp) {
                    latestTimeStamp = eventTime;
                }
                // notify event listeners
                checkSendNotifications(event);
                // note this timestamp is set in event/body but not in the associated header
                try {
                    // route to live
                    if (livePipe != null) {
                        // create new RTMP message, initialize it and push through pipe
                        RTMPMessage msg = RTMPMessage.build(rtmpEvent, eventTime);
                        livePipe.pushMessage(msg);
                    } else {
                        log.debug("Live pipe was null, message was not pushed");
                    }
                } catch (IOException err) {
                    stop();
                }
                // notify listeners about received packet
                if (rtmpEvent instanceof IStreamPacket) {
                    for (IStreamListener listener : getStreamListeners()) {
                        try {
                            listener.packetReceived(this, (IStreamPacket) rtmpEvent);
                        } catch (Exception e) {
                            log.error("Error while notifying listener {}", listener, e);
                            if (listener instanceof RecordingListener) {
                                sendRecordFailedNotify(e.getMessage());
                            }
                        }
                    }
                }
                break;
            default:
                // ignored event
                log.debug("Ignoring event: {}", event.getType());
            }
        } else {
            log.debug("Event was of wrong type or stream is closed ({})", closed);
        }
    }

    /** {@inheritDoc} */
    public int getActiveSubscribers() {
        return subscriberStats.getCurrent();
    }

    /** {@inheritDoc} */
    public long getBytesReceived() {
        return bytesReceived;
    }

    /** {@inheritDoc} */
    public int getCurrentTimestamp() {
        return (int) latestTimeStamp;
    }

    /** {@inheritDoc} */
    public int getMaxSubscribers() {
        return subscriberStats.getMax();
    }

    /**
     * Getter for provider
     * 
     * @return Provider
     */
    public IProvider getProvider() {
        return this;
    }

    /**
     * Setter for stream published name
     * 
     * @param name
     *            Name that used for publishing. Set at client side when begin to broadcast with NetStream#publish.
     */
    public void setPublishedName(String name) {
        log.debug("setPublishedName: {}", name);
        // a publish name of "false" is a special case, used when stopping a stream
        if (StringUtils.isNotEmpty(name) && !"false".equals(name)) {
            this.publishedName = name;
            registerJMX();
        }
    }

    /**
     * Getter for published name
     * 
     * @return Stream published name
     */
    public String getPublishedName() {
        return publishedName;
    }

    /** {@inheritDoc} */
    public void setParameters(Map<String, String> params) {
        this.parameters = params;
    }

    /** {@inheritDoc} */
    public Map<String, String> getParameters() {
        return parameters;
    }

    /** {@inheritDoc} */
    public String getSaveFilename() {
        if (recordingListener != null) {
            return recordingListener.get().getFileName();
        }
        return null;
    }

    /** {@inheritDoc} */
    public IClientBroadcastStreamStatistics getStatistics() {
        return this;
    }

    /** {@inheritDoc} */
    public int getTotalSubscribers() {
        return subscriberStats.getTotal();
    }

    /**
     * @return the automaticRecording
     */
    public boolean isAutomaticRecording() {
        return automaticRecording;
    }

    /**
     * @param automaticRecording
     *            the automaticRecording to set
     */
    public void setAutomaticRecording(boolean automaticRecording) {
        this.automaticRecording = automaticRecording;
    }

    /**
     * @param registerJMX
     *            the registerJMX to set
     */
    public void setRegisterJMX(boolean registerJMX) {
        this.registerJMX = registerJMX;
    }

    /**
     * Notifies handler on stream broadcast close
     */
    private void notifyBroadcastClose() {
        final IStreamAwareScopeHandler handler = getStreamAwareHandler();
        if (handler != null) {
            try {
                handler.streamBroadcastClose(this);
            } catch (Throwable t) {
                log.error("Error in notifyBroadcastClose", t);
            }
        }
    }

    /**
     * Notifies handler on stream recording stop
     */
    private void notifyRecordingStop() {
        IStreamAwareScopeHandler handler = getStreamAwareHandler();
        if (handler != null) {
            try {
                handler.streamRecordStop(this);
            } catch (Throwable t) {
                log.error("Error in notifyBroadcastClose", t);
            }
        }
    }

    /**
     * Notifies handler on stream broadcast start
     */
    private void notifyBroadcastStart() {
        IStreamAwareScopeHandler handler = getStreamAwareHandler();
        if (handler != null) {
            try {
                handler.streamBroadcastStart(this);
            } catch (Throwable t) {
                log.error("Error in notifyBroadcastStart", t);
            }
        }
    }

    /**
     * Send OOB control message with chunk size
     */
    private void notifyChunkSize() {
        if (chunkSize > 0 && livePipe != null) {
            OOBControlMessage setChunkSize = new OOBControlMessage();
            setChunkSize.setTarget("ConnectionConsumer");
            setChunkSize.setServiceName("chunkSize");
            if (setChunkSize.getServiceParamMap() == null) {
                setChunkSize.setServiceParamMap(new HashMap<String, Object>());
            }
            setChunkSize.getServiceParamMap().put("chunkSize", chunkSize);
            livePipe.sendOOBControlMessage(getProvider(), setChunkSize);
        }
    }

    /**
     * Out-of-band control message handler
     *
     * @param source
     *            OOB message source
     * @param pipe
     *            Pipe that used to send OOB message
     * @param oobCtrlMsg
     *            Out-of-band control message
     */
    public void onOOBControlMessage(IMessageComponent source, IPipe pipe, OOBControlMessage oobCtrlMsg) {
        String target = oobCtrlMsg.getTarget();
        if ("ClientBroadcastStream".equals(target)) {
            String serviceName = oobCtrlMsg.getServiceName();
            if ("chunkSize".equals(serviceName)) {
                chunkSize = (Integer) oobCtrlMsg.getServiceParamMap().get("chunkSize");
                notifyChunkSize();
            } else {
                log.debug("Unhandled OOB control message for service: {}", serviceName);
            }
        } else {
            log.debug("Unhandled OOB control message to target: {}", target);
        }
    }

    /**
     * Pipe connection event handler
     * 
     * @param event
     *            Pipe connection event
     */
    @SuppressWarnings("unused")
    public void onPipeConnectionEvent(PipeConnectionEvent event) {
        switch (event.getType()) {
        case PROVIDER_CONNECT_PUSH:
            log.debug("Provider connect");
            if (event.getProvider() == this && event.getSource() != connMsgOut
                    && (event.getParamMap() == null || !event.getParamMap().containsKey("record"))) {
                livePipe = (IPipe) event.getSource();
                log.debug("Provider: {}", livePipe.getClass().getName());
                for (IConsumer consumer : livePipe.getConsumers()) {
                    subscriberStats.increment();
                }
            }
            break;
        case PROVIDER_DISCONNECT:
            log.debug("Provider disconnect");
            if (log.isDebugEnabled() && livePipe != null) {
                log.debug("Provider: {}", livePipe.getClass().getName());
            }
            if (livePipe == event.getSource()) {
                livePipe = null;
            }
            break;
        case CONSUMER_CONNECT_PUSH:
            log.debug("Consumer connect");
            IPipe pipe = (IPipe) event.getSource();
            if (log.isDebugEnabled() && pipe != null) {
                log.debug("Consumer: {}", pipe.getClass().getName());
            }
            if (livePipe == pipe) {
                notifyChunkSize();
            }
            subscriberStats.increment();
            break;
        case CONSUMER_DISCONNECT:
            log.debug("Consumer disconnect: {}", event.getSource().getClass().getName());
            subscriberStats.decrement();
            break;
        default:
        }
    }

    /**
     * Currently not implemented
     *
     * @param pipe
     *            Pipe
     * @param message
     *            Message
     */
    public void pushMessage(IPipe pipe, IMessage message) {
    }

    /**
     * Save broadcasted stream.
     *
     * @param name
     *            Stream name
     * @param isAppend
     *            Append mode
     * @throws IOException
     *             File could not be created/written to
     */
    public void saveAs(String name, boolean isAppend) throws IOException {
        log.debug("SaveAs - name: {} append: {}", name, isAppend);
        // get connection to check if client is still streaming
        IStreamCapableConnection conn = getConnection();
        if (conn == null) {
            throw new IOException("Stream is no longer connected");
        }
        // one recording listener at a time via this entry point
        if (recordingListener == null) {
            // XXX Paul: Revisit this section to allow for implementation of custom IRecordingListener
            //IRecordingListener listener = (IRecordingListener) ScopeUtils.getScopeService(conn.getScope(), IRecordingListener.class, RecordingListener.class, false);
            // create a recording listener
            IRecordingListener listener = new RecordingListener();
            log.debug("Created: {}", listener);
            // initialize the listener
            if (listener.init(conn, name, isAppend)) {
                // get decoder info if it exists for the stream
                IStreamCodecInfo codecInfo = getCodecInfo();
                log.debug("Codec info: {}", codecInfo);
                if (codecInfo instanceof StreamCodecInfo) {
                    StreamCodecInfo info = (StreamCodecInfo) codecInfo;
                    IVideoStreamCodec videoCodec = info.getVideoCodec();
                    log.debug("Video codec: {}", videoCodec);
                    if (videoCodec != null) {
                        //check for decoder configuration to send
                        IoBuffer config = videoCodec.getDecoderConfiguration();
                        if (config != null) {
                            log.debug("Decoder configuration is available for {}", videoCodec.getName());
                            VideoData videoConf = new VideoData(config.asReadOnlyBuffer());
                            try {
                                log.debug("Setting decoder configuration for recording");
                                listener.getFileConsumer().setVideoDecoderConfiguration(videoConf);
                            } finally {
                                videoConf.release();
                            }
                        }
                    } else {
                        log.debug("Could not initialize stream output, videoCodec is null.");
                    }
                    IAudioStreamCodec audioCodec = info.getAudioCodec();
                    log.debug("Audio codec: {}", audioCodec);
                    if (audioCodec != null) {
                        //check for decoder configuration to send
                        IoBuffer config = audioCodec.getDecoderConfiguration();
                        if (config != null) {
                            log.debug("Decoder configuration is available for {}", audioCodec.getName());
                            AudioData audioConf = new AudioData(config.asReadOnlyBuffer());
                            try {
                                log.debug("Setting decoder configuration for recording");
                                listener.getFileConsumer().setAudioDecoderConfiguration(audioConf);
                            } finally {
                                audioConf.release();
                            }
                        }
                    } else {
                        log.debug("No decoder configuration available, audioCodec is null.");
                    }
                }
                // set as primary listener
                recordingListener = new WeakReference<IRecordingListener>(listener);
                // add as a listener
                addStreamListener(listener);
                // start the listener thread
                listener.start();
            } else {
                log.warn("Recording listener failed to initialize for stream: {}", name);
            }
        } else {
            log.debug("Recording listener already exists for stream: {} auto record enabled: {}", name,
                    automaticRecording);
        }
    }

    /**
     * Sends publish start notifications
     */
    private void sendPublishStartNotify() {
        Status publishStatus = new Status(StatusCodes.NS_PUBLISH_START);
        publishStatus.setClientid(getStreamId());
        publishStatus.setDetails(getPublishedName());

        StatusMessage startMsg = new StatusMessage();
        startMsg.setBody(publishStatus);
        pushMessage(startMsg);
    }

    /**
     * Sends publish stop notifications
     */
    private void sendPublishStopNotify() {
        Status stopStatus = new Status(StatusCodes.NS_UNPUBLISHED_SUCCESS);
        stopStatus.setClientid(getStreamId());
        stopStatus.setDetails(getPublishedName());

        StatusMessage stopMsg = new StatusMessage();
        stopMsg.setBody(stopStatus);
        pushMessage(stopMsg);
    }

    /**
     * Sends record failed notifications
     */
    private void sendRecordFailedNotify(String reason) {
        Status failedStatus = new Status(StatusCodes.NS_RECORD_FAILED);
        failedStatus.setLevel(Status.ERROR);
        failedStatus.setClientid(getStreamId());
        failedStatus.setDetails(getPublishedName());
        failedStatus.setDesciption(reason);

        StatusMessage failedMsg = new StatusMessage();
        failedMsg.setBody(failedStatus);
        pushMessage(failedMsg);
    }

    /**
     * Sends record start notifications
     */
    private void sendRecordStartNotify() {
        Status recordStatus = new Status(StatusCodes.NS_RECORD_START);
        recordStatus.setClientid(getStreamId());
        recordStatus.setDetails(getPublishedName());

        StatusMessage startMsg = new StatusMessage();
        startMsg.setBody(recordStatus);
        pushMessage(startMsg);
    }

    /**
     * Sends record stop notifications
     */
    private void sendRecordStopNotify() {
        Status stopStatus = new Status(StatusCodes.NS_RECORD_STOP);
        stopStatus.setClientid(getStreamId());
        stopStatus.setDetails(getPublishedName());

        StatusMessage stopMsg = new StatusMessage();
        stopMsg.setBody(stopStatus);
        pushMessage(stopMsg);
    }

    /**
     * Pushes a message out to a consumer.
     * 
     * @param msg
     *            StatusMessage
     */
    protected void pushMessage(StatusMessage msg) {
        if (connMsgOut != null) {
            try {
                connMsgOut.pushMessage(msg);
            } catch (IOException err) {
                log.error("Error while pushing message: {}", msg, err);
            }
        } else {
            log.warn("Consumer message output is null");
        }
    }

    private void sendStartNotifications(IEventListener source) {
        if (sendStartNotification) {
            // notify handler that stream starts recording/publishing
            sendStartNotification = false;
            if (source instanceof IConnection) {
                IScope scope = ((IConnection) source).getScope();
                if (scope.hasHandler()) {
                    final Object handler = scope.getHandler();
                    if (handler instanceof IStreamAwareScopeHandler) {
                        if (recordingListener != null && recordingListener.get().isRecording()) {
                            // callback for record start
                            ((IStreamAwareScopeHandler) handler).streamRecordStart(this);
                        } else {
                            // delete any previously recorded versions of this now "live" stream per
                            // http://livedocs.adobe.com/flashmediaserver/3.0/hpdocs/help.html?content=00000186.html
                            try {
                                File file = getRecordFile(scope, publishedName);
                                if (file != null && file.exists()) {
                                    if (!file.delete()) {
                                        log.debug("File was not deleted: {}", file.getAbsoluteFile());
                                    }
                                }
                            } catch (Exception e) {
                                log.warn("Exception removing previously recorded file", e);
                            }
                            // callback for publish start
                            ((IStreamAwareScopeHandler) handler).streamPublishStart(this);
                        }
                    }
                }
            }
            // send start notifications
            sendPublishStartNotify();
            if (recordingListener != null && recordingListener.get().isRecording()) {
                sendRecordStartNotify();
            }
            notifyBroadcastStart();
        }
    }

    /**
     * Starts stream, creates pipes, connects
     */
    public void start() {
        log.info("Stream start: {}", publishedName);
        checkVideoCodec = true;
        checkAudioCodec = true;
        firstPacketTime = -1;
        latestTimeStamp = -1;
        bytesReceived = 0;
        IConsumerService consumerManager = (IConsumerService) getScope().getContext().getBean(IConsumerService.KEY);
        connMsgOut = consumerManager.getConsumerOutput(this);
        if (connMsgOut != null && connMsgOut.subscribe(this, null)) {
            setCodecInfo(new StreamCodecInfo());
            creationTime = System.currentTimeMillis();
            closed = false;
        } else {
            log.warn("Subscribe failed");
        }
    }

    /** {@inheritDoc} */
    public void startPublishing() {
        // We send the start messages before the first packet is received.
        // This is required so FME actually starts publishing.
        sendStartNotifications(Red5.getConnectionLocal());
        // force recording if set
        if (automaticRecording) {
            log.debug("Starting automatic recording of {}", publishedName);
            try {
                saveAs(publishedName, false);
            } catch (Exception e) {
                log.warn("Start of automatic recording failed", e);
            }
        }
    }

    /** {@inheritDoc} */
    public void stop() {
        log.info("Stream stop: {}", publishedName);
        stopRecording();
        close();
    }

    /**
     * Stops any currently active recording.
     */
    public void stopRecording() {
        IRecordingListener listener = null;
        if (recordingListener != null && (listener = recordingListener.get()).isRecording()) {
            sendRecordStopNotify();
            notifyRecordingStop();
            // remove the listener
            removeStreamListener(listener);
            // stop the recording listener
            listener.stop();
            // clear and null-out the thread local
            recordingListener.clear();
            recordingListener = null;
        }
    }

    public boolean isRecording() {
        return recordingListener != null && recordingListener.get().isRecording();
    }

    /** {@inheritDoc} */
    public void addStreamListener(IStreamListener listener) {
        listeners.add(listener);
    }

    /** {@inheritDoc} */
    public Collection<IStreamListener> getStreamListeners() {
        return listeners;
    }

    /** {@inheritDoc} */
    public void removeStreamListener(IStreamListener listener) {
        listeners.remove(listener);
    }

    /**
     * Get the file we'd be recording to based on scope and given name.
     * 
     * @param scope
     *            scope
     * @param name
     *            record name
     * @return file
     */
    protected File getRecordFile(IScope scope, String name) {
        return RecordingListener.getRecordFile(scope, name);
    }

    protected void registerJMX() {
        if (registerJMX) {
            // register with jmx
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            try {
                ObjectName oName = new ObjectName(
                        String.format("org.red5.server:type=ClientBroadcastStream,scope=%s,publishedName=%s",
                                getScope().getName(), publishedName));
                mbs.registerMBean(new StandardMBean(this, ClientBroadcastStreamMXBean.class, true), oName);
            } catch (InstanceAlreadyExistsException e) {
                log.debug("Instance already registered", e);
            } catch (Exception e) {
                log.warn("Error on jmx registration", e);
            }
        }
    }

    protected void unregisterJMX() {
        if (registerJMX) {
            if (StringUtils.isNotEmpty(publishedName) && !"false".equals(publishedName)) {
                MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
                try {
                    ObjectName oName = new ObjectName(
                            String.format("org.red5.server:type=ClientBroadcastStream,scope=%s,publishedName=%s",
                                    getScope().getName(), publishedName));
                    mbs.unregisterMBean(oName);
                } catch (Exception e) {
                    log.warn("Exception unregistering", e);
                }
            }
        }
    }

}