Package org.red5.server.stream

Source Code of org.red5.server.stream.PlayEngine

/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2014 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.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

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.io.amf.Output;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.scope.IBroadcastScope;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IPlaylistSubscriberStream;
import org.red5.server.api.stream.ISubscriberStream;
import org.red5.server.api.stream.OperationNotSupportedException;
import org.red5.server.api.stream.StreamState;
import org.red5.server.api.stream.support.DynamicPlayItem;
import org.red5.server.messaging.AbstractMessage;
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.IMessageInput;
import org.red5.server.messaging.IMessageOutput;
import org.red5.server.messaging.IPassive;
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.InMemoryPushPushPipe;
import org.red5.server.messaging.OOBControlMessage;
import org.red5.server.messaging.PipeConnectionEvent;
import org.red5.server.net.rtmp.event.Aggregate;
import org.red5.server.net.rtmp.event.AudioData;
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.event.VideoData.FrameType;
import org.red5.server.net.rtmp.message.Constants;
import org.red5.server.net.rtmp.message.Header;
import org.red5.server.net.rtmp.status.Status;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.net.rtmpt.RTMPTConnection;
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;

/**
* A play engine for playing an IPlayItem.
*
* @author The Red5 Project
* @author Steven Gong
* @author Paul Gregoire (mondain@gmail.com)
* @author Dan Rossi
* @author Tiago Daniel Jacobs (tiago@imdt.com.br)
* @author Vladimir Hmelyoff (vlhm@splitmedialabs.com)
*/
public final class PlayEngine implements IFilter, IPushableConsumer, IPipeConnectionListener {

  private static final Logger log = Red5LoggerFactory.getLogger(PlayEngine.class);

  private IMessageInput msgIn;

  private IMessageOutput msgOut;

  private final ISubscriberStream subscriberStream;

  private ISchedulingService schedulingService;

  private IConsumerService consumerService;

  private IProviderService providerService;

  private int streamId;

  /**
   * Receive video?
   */
  private boolean receiveVideo = true;

  /**
   * Receive audio?
   */
  private boolean receiveAudio = true;

  private boolean pullMode;

  private String waitLiveJob;

  private boolean waiting;

  /**
   * timestamp of first sent packet
   */
  private int streamStartTS;

  private IPlayItem currentItem;

  private RTMPMessage pendingMessage;

  /**
   * Interval in ms to check for buffer underruns in VOD streams.
   */
  private int bufferCheckInterval = 0;

  /**
   * Number of pending messages at which a <code>NetStream.Play.InsufficientBW</code>
   * message is generated for VOD streams.
   */
  private int underrunTrigger = 10;

  /**
   * threshold for number of pending video frames
   */
  private int maxPendingVideoFramesThreshold = 10;

  /**
   * if we have more than 1 pending video frames, but less than maxPendingVideoFrames,
   * continue sending until there are this many sequential frames with more than 1 pending
   */
  private int maxSequentialPendingVideoFrames = 10;

  /**
   * the number of sequential video frames with > 0 pending frames
   */
  private int numSequentialPendingVideoFrames = 0;

  /**
   * State machine for video frame dropping in live streams
   */
  private IFrameDropper videoFrameDropper = new VideoFrameDropper();

  private int timestampOffset = 0;

  /**
   * Timestamp of the last message sent to the client.
   */
  private int lastMessageTs = -1;

  /**
   * Number of bytes sent.
   */
  private AtomicLong bytesSent = new AtomicLong(0);

  /**
   * Start time of stream playback.
   * It's not a time when the stream is being played but the time when the stream should be played if it's played
   * from the very beginning.
   * Eg. A stream is played at timestamp 5s on 1:00:05. The playbackStart is 1:00:00.
   */
  private volatile long playbackStart;

  /**
   * Flag denoting whether or not the push and pull job is scheduled. The job makes sure messages are sent to the client.
   */
  private volatile String pullAndPush;

  /**
   * Flag denoting whether or not the job that closes stream after buffer runs out is scheduled.
   */
  private volatile String deferredStop;

  /**
   * Monitor guarding completion of a given push/pull run.
   * Used to wait for job cancellation to finish.
   */
  private final AtomicBoolean pushPullRunning = new AtomicBoolean(false);

  /**
   * Offset in milliseconds where the stream started.
   */
  private int streamOffset;

  /**
   * Timestamp when buffer should be checked for underruns next.
   */
  private long nextCheckBufferUnderrun;

  /**
   * Send blank audio packet next?
   */
  private boolean sendBlankAudio;

  /**
   * decision: 0 for Live, 1 for File, 2 for Wait, 3 for N/A
   */
  private int playDecision = 3;

  /**
   * List of pending operations
   */
  private ConcurrentLinkedQueue<Runnable> pendingOperations;

  /**
   * Constructs a new PlayEngine.
   */
  private PlayEngine(Builder builder) {
    subscriberStream = builder.subscriberStream;
    schedulingService = builder.schedulingService;
    consumerService = builder.consumerService;
    providerService = builder.providerService;
    // get the stream id
    streamId = subscriberStream.getStreamId();
    // create pending operation list
    pendingOperations = new ConcurrentLinkedQueue<Runnable>();
  }

  /**
   * Builder pattern
   */
  public final static class Builder {
    //Required for play engine
    private ISubscriberStream subscriberStream;

    //Required for play engine
    private ISchedulingService schedulingService;

    //Required for play engine
    private IConsumerService consumerService;

    //Required for play engine
    private IProviderService providerService;

    public Builder(ISubscriberStream subscriberStream, ISchedulingService schedulingService, IConsumerService consumerService, IProviderService providerService) {
      this.subscriberStream = subscriberStream;
      this.schedulingService = schedulingService;
      this.consumerService = consumerService;
      this.providerService = providerService;
    }

    public PlayEngine build() {
      return new PlayEngine(this);
    }

  }

  public void setBufferCheckInterval(int bufferCheckInterval) {
    this.bufferCheckInterval = bufferCheckInterval;
  }

  public void setUnderrunTrigger(int underrunTrigger) {
    this.underrunTrigger = underrunTrigger;
  }

  void setMessageOut(IMessageOutput msgOut) {
    this.msgOut = msgOut;
  }

  /**
   * Start stream
   */
  public void start() {
    switch (subscriberStream.getState()) {
      case UNINIT:
        // allow start if uninitialized
        // change state to stopped
        subscriberStream.setState(StreamState.STOPPED);
        if (msgOut == null) {
          msgOut = consumerService.getConsumerOutput(subscriberStream);
          msgOut.subscribe(this, null);
        }
        break;
      default:
        throw new IllegalStateException(String.format("Cannot start in current state: %s", subscriberStream.getState()));
    }
  }

  /**
   * Play stream
   * @param item                  Playlist item
   * @throws StreamNotFoundException       Stream not found
   * @throws IllegalStateException         Stream is in stopped state
   * @throws IOException Stream had io exception
   */
  public void play(IPlayItem item) throws StreamNotFoundException, IllegalStateException, IOException {
    play(item, true);
  }

  /**
   * Play stream
   * @param item                  Playlist item
   * @param withReset        Send reset status before playing.
   * @throws StreamNotFoundException       Stream not found
   * @throws IllegalStateException         Stream is in stopped state
   * @throws IOException Stream had IO exception
   */
  public void play(IPlayItem item, boolean withReset) throws StreamNotFoundException, IllegalStateException, IOException {
    // cannot play if state is not stopped
    switch (subscriberStream.getState()) {
      case STOPPED:
        //allow play if stopped
        if (msgIn != null) {
          msgIn.unsubscribe(this);
          msgIn = null;
        }
        break;
      default:
        throw new IllegalStateException("Cannot play from non-stopped state");
    }
    // Play type determination
    // http://livedocs.adobe.com/flex/3/langref/flash/net/NetStream.html#play%28%29
    // The start time, in seconds. Allowed values are -2, -1, 0, or a positive number.
    // The default value is -2, which looks for a live stream, then a recorded stream,
    // and if it finds neither, opens a live stream.
    // If -1, plays only a live stream.
    // If 0 or a positive number, plays a recorded stream, beginning start seconds in.
    //
    // -2: live then recorded, -1: live, >=0: recorded
    int type = (int) (item.getStart() / 1000);
    log.debug("Type {}", type);
    // see if it's a published stream
    IScope thisScope = subscriberStream.getScope();
    final String itemName = item.getName();
    //check for input and type
    IProviderService.INPUT_TYPE sourceType = providerService.lookupProviderInput(thisScope, itemName, type);

    boolean isPublishedStream = sourceType == IProviderService.INPUT_TYPE.LIVE;
    boolean isPublishedStreamWait = sourceType == IProviderService.INPUT_TYPE.LIVE_WAIT;
    boolean isFileStream = sourceType == IProviderService.INPUT_TYPE.VOD;

    boolean sendNotifications = true;

    // decision: 0 for Live, 1 for File, 2 for Wait, 3 for N/A
    switch (type) {
      case -2:
        if (isPublishedStream) {
          playDecision = 0;
        } else if (isFileStream) {
          playDecision = 1;
        } else if (isPublishedStreamWait) {
          playDecision = 2;
        }
        break;
      case -1:
        if (isPublishedStream) {
          playDecision = 0;
        } else {
          playDecision = 2;
        }
        break;
      default:
        if (isFileStream) {
          playDecision = 1;
        }
        break;
    }
    log.debug("Play decision is {} (0=Live, 1=File, 2=Wait, 3=N/A)", playDecision);
    IMessage msg = null;
    currentItem = item;
    long itemLength = item.getLength();
    log.debug("Item length: {}", itemLength);
    switch (playDecision) {
      case 0:
        //get source input without create
        msgIn = providerService.getLiveProviderInput(thisScope, itemName, false);
        if (msgIn == null) {
          sendStreamNotFoundStatus(currentItem);
          throw new StreamNotFoundException(itemName);
        } else {
          //drop all frames up to the next keyframe
          videoFrameDropper.reset(IFrameDropper.SEND_KEYFRAMES_CHECK);
          if (msgIn instanceof IBroadcastScope) {
            IBroadcastStream stream = (IBroadcastStream) ((IBroadcastScope) msgIn).getClientBroadcastStream();
            if (stream != null && stream.getCodecInfo() != null) {
              IVideoStreamCodec videoCodec = stream.getCodecInfo().getVideoCodec();
              if (videoCodec != null) {
                if (withReset) {
                  sendReset();
                  sendResetStatus(item);
                  sendStartStatus(item);
                }
                sendNotifications = false;
              }
            }
          }
          //Subscribe to stream (ClientBroadcastStream.onPipeConnectionEvent)
          if (msgIn != null) {
            msgIn.subscribe(this, null);
            //execute the processes to get Live playback setup
            playLive();
          } else {
            sendStreamNotFoundStatus(currentItem);
            throw new StreamNotFoundException(itemName);
          }
        }
        break;
      case 2:
        //get source input with create
        msgIn = providerService.getLiveProviderInput(thisScope, itemName, true);
        msgIn.subscribe(this, null);
        waiting = true;
        if (type == -1 && itemLength >= 0) {
          log.debug("Creating wait job");
          // Wait given timeout for stream to be published
          waitLiveJob = schedulingService.addScheduledOnceJob(itemLength, new IScheduledJob() {
            public void execute(ISchedulingService service) {
              //set the msgIn if its null
              if (msgIn == null) {
                connectToProvider(itemName);
              }
              waitLiveJob = null;
              waiting = false;
              subscriberStream.onChange(StreamState.END);
            }
          });
        } else if (type == -2) {
          log.debug("Creating wait job");
          // Wait x seconds for the stream to be published
          waitLiveJob = schedulingService.addScheduledOnceJob(15000, new IScheduledJob() {
            public void execute(ISchedulingService service) {
              //set the msgIn if its null
              if (msgIn == null) {
                connectToProvider(itemName);
              }
              waitLiveJob = null;
              waiting = false;
            }
          });
        } else {
          connectToProvider(itemName);
        }
        break;
      case 1:
        msgIn = providerService.getVODProviderInput(thisScope, itemName);
        if (msgIn == null) {
          sendStreamNotFoundStatus(currentItem);
          throw new StreamNotFoundException(itemName);
        } else if (msgIn.subscribe(this, null)) {
          //execute the processes to get VOD playback setup
          msg = playVOD(withReset, itemLength);
        } else {
          log.error("Input source subscribe failed");
          throw new IOException(String.format("Subscribe to %s failed", itemName));
        }
        break;
      default:
        sendStreamNotFoundStatus(currentItem);
        throw new StreamNotFoundException(itemName);
    }
    //continue with common play processes (live and vod)
    if (sendNotifications) {
      if (withReset) {
        sendReset();
        sendResetStatus(item);
      }
      sendStartStatus(item);
      if (!withReset) {
        sendSwitchStatus();
      }
      // if its dynamic playback send the complete status
      if (item instanceof DynamicPlayItem) {
        sendTransitionStatus();
      }
    }
    if (msg != null) {
      sendMessage((RTMPMessage) msg);
    }
    subscriberStream.onChange(StreamState.PLAYING, currentItem, !pullMode);
    if (withReset) {
      long currentTime = System.currentTimeMillis();
      playbackStart = currentTime - streamOffset;
      nextCheckBufferUnderrun = currentTime + bufferCheckInterval;
      if (currentItem.getLength() != 0) {
        ensurePullAndPushRunning();
      }
    }
  }

  /**
   * Performs the processes needed for live streams.
   * The following items are sent if they exist:
   * - Metadata
   * - Decoder configurations (ie. AVC codec)
   * - Most recent keyframe
   *
   * @throws IOException
   */
  private final void playLive() throws IOException {
    //change state
    subscriberStream.setState(StreamState.PLAYING);
    streamOffset = 0;
    streamStartTS = -1;
    if (msgIn != null && msgOut != null) {
      // get the stream so that we can grab any metadata and decoder configs
      IBroadcastStream stream = (IBroadcastStream) ((IBroadcastScope) msgIn).getClientBroadcastStream();
      // prevent an NPE when a play list is created and then immediately flushed
      if (stream != null) {
        Notify metaData = stream.getMetaData();
        //check for metadata to send
        if (metaData != null) {
          log.debug("Metadata is available");
          RTMPMessage metaMsg = RTMPMessage.build(metaData, 0);
          try {
            msgOut.pushMessage(metaMsg);
          } catch (IOException e) {
            log.warn("Error sending metadata", e);
          }
        } else {
          log.debug("No metadata available");
        }

        IStreamCodecInfo codecInfo = stream.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());
              //log.debug("Dump:\n{}", Hex.encodeHex(config.array()));
              VideoData conf = new VideoData(config.asReadOnlyBuffer());
              log.trace("Configuration ts: {}", conf.getTimestamp());
              RTMPMessage confMsg = RTMPMessage.build(conf);
              try {
                log.debug("Pushing decoder configuration");
                msgOut.pushMessage(confMsg);
              } finally {
                conf.release();
              }
            }
            //check for a keyframe to send
            IoBuffer keyFrame = videoCodec.getKeyframe();
            if (keyFrame != null) {
              log.debug("Keyframe is available");
              VideoData video = new VideoData(keyFrame.asReadOnlyBuffer());
              log.trace("Keyframe ts: {}", video.getTimestamp());
              //log.debug("Dump:\n{}", Hex.encodeHex(keyFrame.array()));
              RTMPMessage videoMsg = RTMPMessage.build(video);
              try {
                log.debug("Pushing keyframe");
                msgOut.pushMessage(videoMsg);
              } finally {
                video.release();
              }
            }
          } else {
            log.debug("Could not initialize stream output, videoCodec is null");
          }
          // SplitmediaLabs - begin AAC fix
          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());
              //log.debug("Dump:\n{}", Hex.encodeHex(config.array()));
              AudioData conf = new AudioData(config.asReadOnlyBuffer());
              log.trace("Configuration ts: {}", conf.getTimestamp());
              RTMPMessage confMsg = RTMPMessage.build(conf);
              try {
                log.debug("Pushing decoder configuration");
                msgOut.pushMessage(confMsg);
              } finally {
                conf.release();
              }
            }
          } else {
            log.debug("No decoder configuration available, audioCodec is null");
          }
        }
      }     
    } else {
      throw new IOException(String.format("A message pipe is null - in: %b out: %b", (msgIn == null), (msgOut == null)));
    }   
  }

  /**
   * Performs the processes needed for VOD / pre-recorded streams.
   *
   * @param withReset whether or not to perform reset on the stream
   * @param itemLength length of the item to be played
   * @return message for the consumer
   * @throws IOException
   */
  private final IMessage playVOD(boolean withReset, long itemLength) throws IOException {
    IMessage msg = null;
    //change state
    subscriberStream.setState(StreamState.PLAYING);
    streamOffset = 0;
    streamStartTS = -1;
    if (withReset) {
      releasePendingMessage();
    }
    sendVODInitCM(msgIn, currentItem);
    // Don't use pullAndPush to detect IOExceptions prior to sending
    // NetStream.Play.Start
    if (currentItem.getStart() > 0) {
      streamOffset = sendVODSeekCM(msgIn, (int) currentItem.getStart());
      // We seeked to the nearest keyframe so use real timestamp now
      if (streamOffset == -1) {
        streamOffset = (int) currentItem.getStart();
      }
    }
    msg = msgIn.pullMessage();
    if (msg instanceof RTMPMessage) {
      // Only send first video frame
      IRTMPEvent body = ((RTMPMessage) msg).getBody();
      if (itemLength == 0) {
        while (body != null && !(body instanceof VideoData)) {
          msg = msgIn.pullMessage();
          if (msg != null && msg instanceof RTMPMessage) {
            body = ((RTMPMessage) msg).getBody();
          } else {
            break;
          }
        }
      }
      if (body != null) {
        // Adjust timestamp when playing lists
        body.setTimestamp(body.getTimestamp() + timestampOffset);
      }
    }
    return msg;
  }

  /**
   * Connects to the data provider.
   *
   * @param itemName name of the item to play
   */
  private final void connectToProvider(String itemName) {
    log.debug("Attempting connection to {}", itemName);
    IScope thisScope = subscriberStream.getScope();
    msgIn = providerService.getLiveProviderInput(thisScope, itemName, true);
    if (msgIn != null) {
      log.debug("Provider: {}", msgIn);
      if (msgIn.subscribe(this, null)) {
        log.debug("Subscribed to {} provider", itemName);
        //execute the processes to get Live playback setup
        try {
          playLive();
        } catch (IOException e) {
          log.warn("Could not play live stream: {}", itemName, e);
        }
      } else {
        log.warn("Subscribe to {} provider failed", itemName);
      }
    } else {
      log.warn("Provider was not found for {}", itemName);
      StreamService.sendNetStreamStatus(subscriberStream.getConnection(), StatusCodes.NS_PLAY_STREAMNOTFOUND, "Stream was not found", itemName, Status.ERROR, streamId);
    }
  }

  /**
   * Pause at position
   *
   * @param position                  Position in file
   * @throws IllegalStateException    If stream is stopped
   */
  public void pause(int position) throws IllegalStateException {
    switch (subscriberStream.getState()) {
    // allow pause if playing or stopped
      case PLAYING:
      case STOPPED:
        subscriberStream.setState(StreamState.PAUSED);
        clearWaitJobs();
        sendClearPing();
        sendPauseStatus(currentItem);
        subscriberStream.onChange(StreamState.PAUSED, currentItem, position);
        break;
      default:
        throw new IllegalStateException("Cannot pause in current state");
    }
  }

  /**
   * Resume playback
   *
   * @param position                   Resumes playback
   * @throws IllegalStateException     If stream is stopped
   */
  public void resume(int position) throws IllegalStateException {
    switch (subscriberStream.getState()) {
    // allow resume from pause
      case PAUSED:
        subscriberStream.setState(StreamState.PLAYING);
        sendReset();
        sendResumeStatus(currentItem);
        if (pullMode) {
          sendVODSeekCM(msgIn, position);
          subscriberStream.onChange(StreamState.RESUMED, currentItem, position);
          playbackStart = System.currentTimeMillis() - position;
          if (currentItem.getLength() >= 0 && (position - streamOffset) >= currentItem.getLength()) {
            // Resume after end of stream
            stop();
          } else {
            ensurePullAndPushRunning();
          }
        } else {
          subscriberStream.onChange(StreamState.RESUMED, currentItem, position);
          videoFrameDropper.reset(VideoFrameDropper.SEND_KEYFRAMES_CHECK);
        }
        break;
      default:
        throw new IllegalStateException("Cannot resume from non-paused state");
    }
  }

  /**
   * Seek to a given position
   *
   * @param position                  Position
   * @throws IllegalStateException    If stream is in stopped state
   * @throws OperationNotSupportedException If this object doesn't support the operation.
   */
  public void seek(int position) throws IllegalStateException, OperationNotSupportedException {
    // add this pending seek operation to the list
    pendingOperations.add(new SeekRunnable(position));
    cancelDeferredStop();
  }

  /**
   * Stop playback
   *
   * @throws IllegalStateException    If stream is in stopped state
   */
  public void stop() throws IllegalStateException {
    switch (subscriberStream.getState()) {
    // allow stop if playing or paused
      case PLAYING:
      case PAUSED:
        subscriberStream.setState(StreamState.STOPPED);
        if (msgIn != null && !pullMode) {
          msgIn.unsubscribe(this);
          msgIn = null;
        }
        subscriberStream.onChange(StreamState.STOPPED, currentItem);
        clearWaitJobs();
        cancelDeferredStop();
        if (subscriberStream instanceof IPlaylistSubscriberStream) {
          IPlaylistSubscriberStream pss = (IPlaylistSubscriberStream) subscriberStream;
          if (!pss.hasMoreItems()) {
            releasePendingMessage();
            sendCompleteStatus();
            bytesSent.set(0);
            sendClearPing();
            sendStopStatus(currentItem);
          } else {
            if (lastMessageTs > 0) {
              // remember last timestamp so we can generate correct headers in playlists.
              timestampOffset = lastMessageTs;
            }
            pss.nextItem();
          }
        }
        break;
      case CLOSED:
        clearWaitJobs();
        if (deferredStop != null) {
          subscriberStream.cancelJob(deferredStop);
          deferredStop = null;
        }
      default:
        throw new IllegalStateException(String.format("Cannot stop in current state: %s", subscriberStream.getState()));
    }
    // once we've stopped there's no need for the deferred job
    if (deferredStop != null) {
      subscriberStream.cancelJob(deferredStop);
    }
  }

  /**
   * Close stream
   */
  public void close() {
    if (!subscriberStream.getState().equals(StreamState.CLOSED)) {
      if (msgIn != null) {
        msgIn.unsubscribe(this);
        msgIn = null;
      }
      subscriberStream.setState(StreamState.CLOSED);
      clearWaitJobs();
      releasePendingMessage();
      lastMessageTs = 0;
      // XXX is clear ping required?
      //sendClearPing();
      if (msgOut != null) {
        List<IConsumer> consumers = ((InMemoryPushPushPipe) msgOut).getConsumers();
        // i would assume a list of 1 in most cases
        if (!consumers.isEmpty()) {
          log.debug("Message out consumers: {}", consumers.size());
          for (IConsumer consumer : consumers) {
            ((InMemoryPushPushPipe) msgOut).unsubscribe(consumer);
          }
        }
        msgOut = null;
      }
    } else {
      log.debug("Stream is already in closed state");
    }
  }

  /**
   * Check if it's okay to send the client more data. This takes the configured
   * bandwidth as well as the requested client buffer into account.
   *
   * @param message
   * @return true if it is ok to send more, false otherwise
   */
  private boolean okayToSendMessage(IRTMPEvent message) {
    if (message instanceof IStreamData) {
      final long now = System.currentTimeMillis();
      // check client buffer size
      if (isClientBufferFull(now)) {
        return false;
      }
      // get pending message count
      long pending = pendingMessages();
      if (bufferCheckInterval > 0 && now >= nextCheckBufferUnderrun) {
        if (pending > underrunTrigger) {
          // client is playing behind speed, notify him
          sendInsufficientBandwidthStatus(currentItem);
        }
        nextCheckBufferUnderrun = now + bufferCheckInterval;
      }
      // check for under run
      if (pending > underrunTrigger) {
        // too many messages already queued on the connection
        return false;
      }
      return true;
    } else {
      String itemName = "Undefined";
      // if current item exists get the name to help debug this issue
      if (currentItem != null) {
        itemName = currentItem.getName();
      }
      Object[] errorItems = new Object[] { message.getClass(), message.getDataType(), itemName };
      throw new RuntimeException(String.format("Expected IStreamData but got %s (type %s) for %s", errorItems));
    }
  }

  /**
   * Estimate client buffer fill.
   * @param now The current timestamp being used.
   * @return True if it appears that the client buffer is full, otherwise false.
   */
  private boolean isClientBufferFull(final long now) {
    // check client buffer length when we've already sent some messages
    if (lastMessageTs > 0) {
      // duration the stream is playing / playback duration
      final long delta = now - playbackStart;
      // buffer size as requested by the client
      final long buffer = subscriberStream.getClientBufferDuration();
      // expected amount of data present in client buffer
      final long buffered = lastMessageTs - delta;
      log.trace("isClientBufferFull: timestamp {} delta {} buffered {} buffer duration {}", new Object[] { lastMessageTs, delta, buffered, buffer });
      // fix for SN-122, this sends double the size of the client buffer
      if (buffer > 0 && buffered > (buffer * 2)) {
        // client is likely to have enough data in the buffer
        return true;
      }
    }
    return false;
  }

  private boolean isClientBufferEmpty() {
    // check client buffer length when we've already sent some messages
    if (lastMessageTs >= 0) {
      // duration the stream is playing / playback duration
      final long delta = System.currentTimeMillis() - playbackStart;
      // expected amount of data present in client buffer
      final long buffered = lastMessageTs - delta;
      log.trace("isClientBufferEmpty: timestamp {} delta {} buffered {}", new Object[] { lastMessageTs, delta, buffered });
      if (buffered < 0) {
        return true;
      }
    }
    return false;
  }

  /**
   * Make sure the pull and push processing is running.
   */
  private void ensurePullAndPushRunning() {
    log.trace("State should be PLAYING to running this task: {}", subscriberStream.getState());
    if (pullMode && pullAndPush == null && subscriberStream.getState() == StreamState.PLAYING) {
      // client buffer is at least 100ms
      pullAndPush = subscriberStream.scheduleWithFixedDelay(new PullAndPushRunnable(), 10);
    }
  }

  /**
   * Clear all scheduled waiting jobs
   */
  private void clearWaitJobs() {
    log.debug("Clear wait jobs");
    if (pullAndPush != null) {
      subscriberStream.cancelJob(pullAndPush);
      releasePendingMessage();
      pullAndPush = null;
    }
    if (waitLiveJob != null) {
      schedulingService.removeScheduledJob(waitLiveJob);
      waitLiveJob = null;
    }
  }

  /**
   * Sends a status message.
   *
   * @param status
   */
  private void doPushMessage(Status status) {
    StatusMessage message = new StatusMessage();
    message.setBody(status);
    doPushMessage(message);
  }

  /**
   * Send message to output stream and handle exceptions.
   *
   * @param message The message to send.
   */
  private void doPushMessage(AbstractMessage message) {
    log.trace("doPushMessage: {}", message.getMessageType());
    if (msgOut != null) {
      try {
        msgOut.pushMessage(message);
        if (message instanceof RTMPMessage) {
          IRTMPEvent body = ((RTMPMessage) message).getBody();
          //update the last message sent's timestamp
          lastMessageTs = body.getTimestamp();
          IoBuffer streamData = null;
          if (body instanceof IStreamData && (streamData = ((IStreamData<?>) body).getData()) != null) {
            bytesSent.addAndGet(streamData.limit());
          }
        }
      } catch (IOException err) {
        log.error("Error while pushing message", err);
      }
    } else {
      log.warn("Push message failed due to null output pipe");
    }
  }

  /**
   * Send RTMP message
   * @param message        RTMP message
   */
  private void sendMessage(RTMPMessage messageIn) {
    IRTMPEvent event;
    IoBuffer dataReference;
    switch (messageIn.getBody().getDataType()) {
      case Constants.TYPE_AGGREGATE:
        dataReference = ((Aggregate) messageIn.getBody()).getData();
        event = new Aggregate(dataReference);
        event.setTimestamp(messageIn.getBody().getTimestamp());
        break;
      case Constants.TYPE_AUDIO_DATA:
        dataReference = ((AudioData) messageIn.getBody()).getData();
        event = new AudioData(dataReference);
        event.setTimestamp(messageIn.getBody().getTimestamp());
        break;
      case Constants.TYPE_VIDEO_DATA:
        dataReference = ((VideoData) messageIn.getBody()).getData();
        event = new VideoData(dataReference);
        event.setTimestamp(messageIn.getBody().getTimestamp());
        break;
      default:
        dataReference = ((Notify) messageIn.getBody()).getData();
        event = new Notify(dataReference);
        event.setTimestamp(messageIn.getBody().getTimestamp());
        break;
    }
    RTMPMessage messageOut = RTMPMessage.build(event);
    //get the current timestamp from the message
    int ts = messageOut.getBody().getTimestamp();
    if (log.isTraceEnabled()) {
      log.trace("sendMessage: streamStartTS={}, length={}, streamOffset={}, timestamp={}", new Object[] { streamStartTS, currentItem.getLength(), streamOffset, ts });
      final long delta = System.currentTimeMillis() - playbackStart;
      log.trace("clientBufferDetails: timestamp {} delta {} buffered {}", new Object[] { lastMessageTs, delta, lastMessageTs - delta });
    }
    // don't reset streamStartTS to 0 for live streams
    if ((streamStartTS == -1 && (ts > 0 || playDecision != 0)) || streamStartTS > ts) {
      log.debug("sendMessage: resetting streamStartTS");
      streamStartTS = ts;
      messageOut.getBody().setTimestamp(0);
    }
    //relative timestamp adjustment for live streams
    if (playDecision == 0 && streamStartTS > 0) {
      //subtract the offset time of when the stream started playing for the client
      ts -= streamStartTS;
      messageOut.getBody().setTimestamp(ts);
      if (log.isTraceEnabled()) {
        log.trace("sendMessage (updated): streamStartTS={}, length={}, streamOffset={}, timestamp={}", new Object[] { streamStartTS, currentItem.getLength(), streamOffset,
            ts });
      }
    }
    if (streamStartTS > -1 && currentItem.getLength() >= 0) {
      int duration = ts - streamStartTS;
      if (duration - streamOffset >= currentItem.getLength()) {
        // Sent enough data to client
        stop();
        return;
      }
    }
    doPushMessage(messageOut);
  }

  /**
   * Send clear ping. Lets client know that stream has no more data to
   * send.
   */
  private void sendClearPing() {
    Ping eof = new Ping();
    eof.setEventType(Ping.STREAM_PLAYBUFFER_CLEAR);
    eof.setValue2(streamId);
    // eos
    RTMPMessage eofMsg = RTMPMessage.build(eof);
    doPushMessage(eofMsg);
  }

  /**
   * Send reset message
   */
  private void sendReset() {
    if (pullMode) {
      Ping recorded = new Ping();
      recorded.setEventType(Ping.RECORDED_STREAM);
      recorded.setValue2(streamId);
      // recorded
      RTMPMessage recordedMsg = RTMPMessage.build(recorded);
      doPushMessage(recordedMsg);
    }

    Ping begin = new Ping();
    begin.setEventType(Ping.STREAM_BEGIN);
    begin.setValue2(streamId);
    // begin
    RTMPMessage beginMsg = RTMPMessage.build(begin);
    doPushMessage(beginMsg);
    // reset
    ResetMessage reset = new ResetMessage();
    doPushMessage(reset);
  }

  /**
   * Send reset status for item
   * @param item            Playlist item
   */
  private void sendResetStatus(IPlayItem item) {
    Status reset = new Status(StatusCodes.NS_PLAY_RESET);
    reset.setClientid(streamId);
    reset.setDetails(item.getName());
    reset.setDesciption(String.format("Playing and resetting %s.", item.getName()));

    doPushMessage(reset);
  }

  /**
   * Send playback start status notification
   * @param item            Playlist item
   */
  private void sendStartStatus(IPlayItem item) {
    Status start = new Status(StatusCodes.NS_PLAY_START);
    start.setClientid(streamId);
    start.setDetails(item.getName());
    start.setDesciption(String.format("Started playing %s.", item.getName()));

    doPushMessage(start);
  }

  /**
   * Send playback stoppage status notification
   * @param item            Playlist item
   */
  private void sendStopStatus(IPlayItem item) {
    Status stop = new Status(StatusCodes.NS_PLAY_STOP);
    stop.setClientid(streamId);
    stop.setDesciption(String.format("Stopped playing %s.", item.getName()));
    stop.setDetails(item.getName());

    doPushMessage(stop);
  }

  /**
   * Sends an onPlayStatus message.
   *
   * @param code
   * @param duration
   * @param bytes
   */
  private void sendOnPlayStatus(String code, int duration, long bytes) {
    IoBuffer buf = IoBuffer.allocate(255);
    buf.setAutoExpand(true);
    Output out = new Output(buf);
    out.writeString("onPlayStatus");
    Map<Object, Object> props = new HashMap<Object, Object>();
    props.put("code", code);
    props.put("level", "status");
    props.put("duration", duration);
    props.put("bytes", bytes);
    if (StatusCodes.NS_PLAY_TRANSITION_COMPLETE.equals(code)) {
      props.put("details", currentItem.getName());
      props.put("description", String.format("Transitioned to %s", currentItem.getName()));
      props.put("clientId", streamId);
      props.put("isFastPlay", false);
    }
    out.writeMap(props);
    buf.flip();

    IRTMPEvent event = new Notify(buf);
    if (lastMessageTs > 0) {
      event.setTimestamp(lastMessageTs);
    } else {
      event.setTimestamp(0);
    }
    RTMPMessage msg = RTMPMessage.build(event);
    doPushMessage(msg);
  }

  /**
   * Send playlist switch status notification
   */
  private void sendSwitchStatus() {
    // TODO: find correct duration to send
    sendOnPlayStatus(StatusCodes.NS_PLAY_SWITCH, 1, bytesSent.get());
  }

  /**
   * Send transition status notification
   */
  private void sendTransitionStatus() {
    sendOnPlayStatus(StatusCodes.NS_PLAY_TRANSITION_COMPLETE, 0, bytesSent.get());
  }

  /**
   * Send playlist complete status notification
   *
   */
  private void sendCompleteStatus() {
    // TODO: find correct duration to send
    sendOnPlayStatus(StatusCodes.NS_PLAY_COMPLETE, 1, bytesSent.get());
  }

  /**
   * Send seek status notification
   * @param item            Playlist item
   * @param position        Seek position
   */
  private void sendSeekStatus(IPlayItem item, int position) {
    Status seek = new Status(StatusCodes.NS_SEEK_NOTIFY);
    seek.setClientid(streamId);
    seek.setDetails(item.getName());
    seek.setDesciption(String.format("Seeking %d (stream ID: %d).", position, streamId));

    doPushMessage(seek);
  }

  /**
   * Send pause status notification
   * @param item            Playlist item
   */
  private void sendPauseStatus(IPlayItem item) {
    Status pause = new Status(StatusCodes.NS_PAUSE_NOTIFY);
    pause.setClientid(streamId);
    pause.setDetails(item.getName());

    doPushMessage(pause);
  }

  /**
   * Send resume status notification
   * @param item            Playlist item
   */
  private void sendResumeStatus(IPlayItem item) {
    Status resume = new Status(StatusCodes.NS_UNPAUSE_NOTIFY);
    resume.setClientid(streamId);
    resume.setDetails(item.getName());

    doPushMessage(resume);
  }

  /**
   * Send published status notification
   * @param item            Playlist item
   */
  private void sendPublishedStatus(IPlayItem item) {
    Status published = new Status(StatusCodes.NS_PLAY_PUBLISHNOTIFY);
    published.setClientid(streamId);
    published.setDetails(item.getName());

    doPushMessage(published);
  }

  /**
   * Send unpublished status notification
   * @param item            Playlist item
   */
  private void sendUnpublishedStatus(IPlayItem item) {
    Status unpublished = new Status(StatusCodes.NS_PLAY_UNPUBLISHNOTIFY);
    unpublished.setClientid(streamId);
    unpublished.setDetails(item.getName());

    doPushMessage(unpublished);
  }

  /**
   * Stream not found status notification
   * @param item            Playlist item
   */
  private void sendStreamNotFoundStatus(IPlayItem item) {
    Status notFound = new Status(StatusCodes.NS_PLAY_STREAMNOTFOUND);
    notFound.setClientid(streamId);
    notFound.setLevel(Status.ERROR);
    notFound.setDetails(item.getName());

    doPushMessage(notFound);
  }

  /**
   * Insufficient bandwidth notification
   * @param item            Playlist item
   */
  private void sendInsufficientBandwidthStatus(IPlayItem item) {
    Status insufficientBW = new Status(StatusCodes.NS_PLAY_INSUFFICIENT_BW);
    insufficientBW.setClientid(streamId);
    insufficientBW.setLevel(Status.WARNING);
    insufficientBW.setDetails(item.getName());
    insufficientBW.setDesciption("Data is playing behind the normal speed.");

    doPushMessage(insufficientBW);
  }

  /**
   * Send VOD init control message
   * @param msgIn           Message input
   * @param item            Playlist item
   */
  private void sendVODInitCM(IMessageInput msgIn, IPlayItem item) {
    OOBControlMessage oobCtrlMsg = new OOBControlMessage();
    oobCtrlMsg.setTarget(IPassive.KEY);
    oobCtrlMsg.setServiceName("init");
    Map<String, Object> paramMap = new HashMap<String, Object>(1);
    paramMap.put("startTS", (int) item.getStart());
    oobCtrlMsg.setServiceParamMap(paramMap);
    msgIn.sendOOBControlMessage(this, oobCtrlMsg);
  }

  /**
   * Send VOD seek control message
   * @param msgIn            Message input
   * @param position         Playlist item
   * @return                 Out-of-band control message call result or -1 on failure
   */
  private int sendVODSeekCM(IMessageInput msgIn, int position) {
    OOBControlMessage oobCtrlMsg = new OOBControlMessage();
    oobCtrlMsg.setTarget(ISeekableProvider.KEY);
    oobCtrlMsg.setServiceName("seek");
    Map<String, Object> paramMap = new HashMap<String, Object>(1);
    paramMap.put("position", position);
    oobCtrlMsg.setServiceParamMap(paramMap);
    msgIn.sendOOBControlMessage(this, oobCtrlMsg);
    if (oobCtrlMsg.getResult() instanceof Integer) {
      return (Integer) oobCtrlMsg.getResult();
    } else {
      return -1;
    }
  }

  /**
   * Send VOD check video control message
   *
   * @param msgIn
   * @return result of oob control message
   */
  private boolean sendCheckVideoCM(IMessageInput msgIn) {
    OOBControlMessage oobCtrlMsg = new OOBControlMessage();
    oobCtrlMsg.setTarget(IStreamTypeAwareProvider.KEY);
    oobCtrlMsg.setServiceName("hasVideo");
    msgIn.sendOOBControlMessage(this, oobCtrlMsg);
    if (oobCtrlMsg.getResult() instanceof Boolean) {
      return (Boolean) oobCtrlMsg.getResult();
    } else {
      return false;
    }
  }

  /** {@inheritDoc} */
  public void onOOBControlMessage(IMessageComponent source, IPipe pipe, OOBControlMessage oobCtrlMsg) {
    if ("ConnectionConsumer".equals(oobCtrlMsg.getTarget())) {
      if (source instanceof IProvider) {
        if (msgOut != null) {
          msgOut.sendOOBControlMessage((IProvider) source, oobCtrlMsg);
        } else {
          // this may occur when a attempts to play and then disconnects
          log.warn("Output is not available, message cannot be sent");
          close();
        }
      }
    }
  }

  /** {@inheritDoc} */
  public void onPipeConnectionEvent(PipeConnectionEvent event) {
    switch (event.getType()) {
      case PipeConnectionEvent.PROVIDER_CONNECT_PUSH:
        if (event.getProvider() != this) {
          if (waiting) {
            if (waitLiveJob != null) {
              schedulingService.removeScheduledJob(waitLiveJob);
            }
            waitLiveJob = null;
            waiting = false;
          }
          sendPublishedStatus(currentItem);
        }
        break;
      case PipeConnectionEvent.PROVIDER_DISCONNECT:
        if (pullMode) {
          sendStopStatus(currentItem);
        } else {
          sendUnpublishedStatus(currentItem);
        }
        break;
      case PipeConnectionEvent.CONSUMER_CONNECT_PULL:
        if (event.getConsumer() == this) {
          pullMode = true;
        }
        break;
      case PipeConnectionEvent.CONSUMER_CONNECT_PUSH:
        if (event.getConsumer() == this) {
          pullMode = false;
        }
        break;
      default:
    }
  }

  /** {@inheritDoc} */
  public void pushMessage(IPipe pipe, IMessage message) throws IOException {
    if (message instanceof RTMPMessage) {
      RTMPMessage rtmpMessage = (RTMPMessage) message;
      IRTMPEvent body = rtmpMessage.getBody();
      if (body instanceof IStreamData) {
        // the subscriber paused
        if (subscriberStream.getState() == StreamState.PAUSED) {
          log.debug("Dropping packet because we are paused");
          videoFrameDropper.dropPacket(rtmpMessage);
          return;
        }
        if (body instanceof VideoData) {
          if (msgIn instanceof IBroadcastScope) {
            IBroadcastStream stream = (IBroadcastStream) ((IBroadcastScope) msgIn).getClientBroadcastStream();
            if (stream != null && stream.getCodecInfo() != null) {
              IVideoStreamCodec videoCodec = stream.getCodecInfo().getVideoCodec();
              //dont try to drop frames if video codec is null - related to SN-77
              if (videoCodec != null && videoCodec.canDropFrames()) {
                if (!receiveVideo) {
                  // The client disabled video or the app doesn't have enough bandwidth
                  // allowed for this stream.
                  log.debug("Dropping packet because we cant receive video or token acquire failed");
                  videoFrameDropper.dropPacket(rtmpMessage);
                  return;
                }
                // Only check for frame dropping if the codec supports it
                long pendingVideos = pendingVideoMessages();
                if (!videoFrameDropper.canSendPacket(rtmpMessage, pendingVideos)) {
                  // Drop frame as it depends on other frames that were dropped before.
                  log.debug("Dropping packet because frame dropper says we cant send it");
                  return;
                }
                // increment the number of times we had pending video frames sequentially
                if (pendingVideos > 1) {
                  numSequentialPendingVideoFrames++;
                } else {
                  // reset number of sequential pending frames if 1 or 0 are pending.
                  numSequentialPendingVideoFrames = 0;
                }
                if (pendingVideos > maxPendingVideoFramesThreshold || numSequentialPendingVideoFrames > maxSequentialPendingVideoFrames) {
                  log.debug("Pending: {} Threshold: {} Sequential: {}", new Object[] { pendingVideos, maxPendingVideoFramesThreshold,
                      numSequentialPendingVideoFrames });
                  // We drop because the client has insufficient bandwidth.
                  long now = System.currentTimeMillis();
                  if (bufferCheckInterval > 0 && now >= nextCheckBufferUnderrun) {
                    // Notify client about frame dropping (keyframe)
                    sendInsufficientBandwidthStatus(currentItem);
                    nextCheckBufferUnderrun = now + bufferCheckInterval;
                  }
                  videoFrameDropper.dropPacket(rtmpMessage);
                  return;
                }
              }
            }
          }
        } else if (body instanceof AudioData) {
          if (!receiveAudio && sendBlankAudio) {
            // Send blank audio packet to reset player
            sendBlankAudio = false;
            body = new AudioData();
            if (lastMessageTs > 0) {
              body.setTimestamp(lastMessageTs);
            } else {
              body.setTimestamp(0);
            }
            rtmpMessage = RTMPMessage.build(body);
          } else if (!receiveAudio) {
            return;
          }
        }
        sendMessage(rtmpMessage);
      } else {
        throw new RuntimeException(String.format("Expected IStreamData but got %s (type %s)", body.getClass(), body.getDataType()));
      }
    } else if (message instanceof ResetMessage) {
      sendReset();
    } else {
      msgOut.pushMessage(message);
    }
  }

  /**
   * Get number of pending video messages
   * @return          Number of pending video messages
   */
  private long pendingVideoMessages() {
    if (msgOut != null) {
      OOBControlMessage pendingRequest = new OOBControlMessage();
      pendingRequest.setTarget("ConnectionConsumer");
      pendingRequest.setServiceName("pendingVideoCount");
      msgOut.sendOOBControlMessage(this, pendingRequest);
      if (pendingRequest.getResult() != null) {
        return (Long) pendingRequest.getResult();
      } else {
        return 0;
      }
    }
    return 0;
  }

  /**
   * Get number of pending messages to be sent
   * @return          Number of pending messages
   */
  private long pendingMessages() {
    return subscriberStream.getConnection().getPendingMessages();
  }

  public boolean isPullMode() {
    return pullMode;
  }

  public boolean isPaused() {
    return subscriberStream.isPaused();
  }

  /**
   * Returns the timestamp of the last message sent.
   *
   * @return last message timestamp
   */
  public int getLastMessageTimestamp() {
    return lastMessageTs;
  }

  public long getPlaybackStart() {
    return playbackStart;
  }

  public void sendBlankAudio(boolean sendBlankAudio) {
    this.sendBlankAudio = sendBlankAudio;
  }

  /**
   * Returns true if the engine currently receives audio.
   *
   * @return receive audio
   */
  public boolean receiveAudio() {
    return receiveAudio;
  }

  /**
   * Returns true if the engine currently receives audio and
   * sets the new value.
   *
   * @param receive new value
   * @return old value
   */
  public boolean receiveAudio(boolean receive) {
    boolean oldValue = receiveAudio;
    //set new value
    if (receiveAudio != receive) {
      receiveAudio = receive;
    }
    return oldValue;
  }

  /**
   * Returns true if the engine currently receives video.
   *
   * @return receive video
   */
  public boolean receiveVideo() {
    return receiveVideo;
  }

  /**
   * Returns true if the engine currently receives video and
   * sets the new value.
   * @param receive new value
   * @return old value
   */
  public boolean receiveVideo(boolean receive) {
    boolean oldValue = receiveVideo;
    //set new value
    if (receiveVideo != receive) {
      receiveVideo = receive;
    }
    return oldValue;
  }

  /**
   * Releases pending message body, nullifies pending message object
   */
  private void releasePendingMessage() {
    if (pendingMessage != null) {
      IRTMPEvent body = pendingMessage.getBody();
      if (body instanceof IStreamData && ((IStreamData<?>) body).getData() != null) {
        ((IStreamData<?>) body).getData().free();
      }
      pendingMessage = null;
    }
  }

  /**
   * Check if sending the given message was enabled by the client.
   *
   * @param message the message to check
   * @return <code>true</code> if the message should be sent, <code>false</code> otherwise (and the message is discarded)
   */
  protected boolean checkSendMessageEnabled(RTMPMessage message) {
    IRTMPEvent body = message.getBody();
    if (!receiveAudio && body instanceof AudioData) {
      // The user doesn't want to get audio packets
      ((IStreamData<?>) body).getData().free();
      if (sendBlankAudio) {
        // Send reset audio packet
        sendBlankAudio = false;
        body = new AudioData();
        // We need a zero timestamp
        if (lastMessageTs >= 0) {
          body.setTimestamp(lastMessageTs - timestampOffset);
        } else {
          body.setTimestamp(-timestampOffset);
        }
        message = RTMPMessage.build(body);
      } else {
        return false;
      }
    } else if (!receiveVideo && body instanceof VideoData) {
      // The user doesn't want to get video packets
      ((IStreamData<?>) body).getData().free();
      return false;
    }
    return true;
  }

  /**
   * Schedule a stop to be run from a separate thread to allow the background thread to stop cleanly.
   */
  private void runDeferredStop() {
    // Stop current jobs from running.
    clearWaitJobs();
    // Schedule deferred stop executor.
    log.trace("Ran deferred stop");
    if (deferredStop == null) {
      // set deferred stop if we get a job name returned
      deferredStop = subscriberStream.scheduleWithFixedDelay(new DeferredStopRunnable(), 100);
    }
  }

  private void cancelDeferredStop() {
    log.debug("Cancel deferred stop");
    if (deferredStop != null) {
      subscriberStream.cancelJob(deferredStop);
      deferredStop = null;
    }
    ensurePullAndPushRunning();
  }

  /**
   * Runnable worker to handle seek operations.
   */
  private final class SeekRunnable implements Runnable {

    private final int position;

    SeekRunnable(int position) {
      this.position = position;
    }

    @SuppressWarnings("incomplete-switch")
    public void run() {
      log.trace("Seek: {}", position);
      boolean startPullPushThread = false;
      switch (subscriberStream.getState()) {
        case PLAYING:
          startPullPushThread = true;
        case PAUSED:
        case STOPPED:
          //allow seek if playing, paused, or stopped
          if (!pullMode) {
            // throw new OperationNotSupportedException();
            throw new RuntimeException();
          }
          releasePendingMessage();
          clearWaitJobs();
          break;
        default:
          throw new IllegalStateException("Cannot seek in current state");
      }
      sendClearPing();
      sendReset();
      sendSeekStatus(currentItem, position);
      sendStartStatus(currentItem);
      int seekPos = sendVODSeekCM(msgIn, position);
      // we seeked to the nearest keyframe so use real timestamp now
      if (seekPos == -1) {
        seekPos = position;
      }
      //what should our start be?
      log.trace("Current playback start: {}", playbackStart);
      playbackStart = System.currentTimeMillis() - seekPos;
      log.trace("Playback start: {} seek pos: {}", playbackStart, seekPos);
      subscriberStream.onChange(StreamState.SEEK, currentItem, seekPos);
      // start off with not having sent any message
      boolean messageSent = false;
      // read our client state
      switch (subscriberStream.getState()) {
        case PAUSED:
        case STOPPED:
          // we send a single snapshot on pause
          if (sendCheckVideoCM(msgIn)) {
            IMessage msg = null;
            do {
              try {
                msg = msgIn.pullMessage();
              } catch (Throwable err) {
                log.error("Error while pulling message", err);
                msg = null;
              }
              if (msg instanceof RTMPMessage) {
                RTMPMessage rtmpMessage = (RTMPMessage) msg;
                IRTMPEvent body = rtmpMessage.getBody();
                if (body instanceof VideoData && ((VideoData) body).getFrameType() == FrameType.KEYFRAME) {
                  //body.setTimestamp(seekPos);
                  doPushMessage(rtmpMessage);
                  rtmpMessage.getBody().release();
                  messageSent = true;
                  lastMessageTs = body.getTimestamp();
                  break;
                }
              }
            } while (msg != null);
          }
      }
      // seeked past end of stream
      if (currentItem.getLength() >= 0 && (position - streamOffset) >= currentItem.getLength()) {
        stop();
      }
      // if no message has been sent by this point send an audio packet
      if (!messageSent) {
        // Send blank audio packet to notify client about new position
        log.debug("Sending blank audio packet");
        AudioData audio = new AudioData();
        audio.setTimestamp(seekPos);
        audio.setHeader(new Header());
        audio.getHeader().setTimer(seekPos);
        RTMPMessage audioMessage = RTMPMessage.build(audio);
        lastMessageTs = seekPos;
        doPushMessage(audioMessage);
        audioMessage.getBody().release();
      }

      if (!messageSent && subscriberStream.getState() == StreamState.PLAYING) {
        boolean isRTMPTPlayback = subscriberStream.getConnection() instanceof RTMPTConnection;

        // send all frames from last keyframe up to requested position and fill client buffer
        if (sendCheckVideoCM(msgIn)) {
          final long clientBuffer = subscriberStream.getClientBufferDuration();
          IMessage msg = null;
          int msgSent = 0;

          do {
            try {
              msg = msgIn != null ? msgIn.pullMessage() : null;
              if (msg instanceof RTMPMessage) {
                RTMPMessage rtmpMessage = (RTMPMessage) msg;
                IRTMPEvent body = rtmpMessage.getBody();
                if (body.getTimestamp() >= position + (clientBuffer * 2)) {
                  // client buffer should be full by now, continue regular pull/push
                  releasePendingMessage();
                  if (checkSendMessageEnabled(rtmpMessage)) {
                    pendingMessage = rtmpMessage;
                  }
                  break;
                }
                if (!checkSendMessageEnabled(rtmpMessage)) {
                  continue;
                }
                msgSent++;
                sendMessage(rtmpMessage);
              }
            } catch (Throwable err) {
              log.error("Error while pulling message", err);
              msg = null;
            }
          } while (!isRTMPTPlayback && (msg != null));

          log.trace("msgSent: {}", msgSent);
          playbackStart = System.currentTimeMillis() - lastMessageTs;
        }
      }
      // start pull-push
      if (startPullPushThread) {
        ensurePullAndPushRunning();
      }
    }
  }

  /**
   * Periodically triggered by executor to send messages to the client.
   */
  private final class PullAndPushRunnable implements IScheduledJob {

    /**
     * Trigger sending of messages.
     */
    public void execute(ISchedulingService svc) {
      // ensure the job is not already running
      if (pushPullRunning.compareAndSet(false, true)) {
        try {
          // handle any pending operations
          Runnable worker = null;
          while (!pendingOperations.isEmpty()) {
            log.debug("Pending operations: {}", pendingOperations.size());
            // remove the first operation and execute it
            worker = pendingOperations.remove();
            log.debug("Worker: {}", worker);
            // if the operation is seek, ensure it is the last request in the set
            while (worker instanceof SeekRunnable) {
              Runnable tmp = pendingOperations.peek();
              if (tmp != null && tmp instanceof SeekRunnable) {
                worker = pendingOperations.remove();
              } else {
                break;
              }
            }
            if (worker != null) {
              log.debug("Executing pending operation");
              worker.run();
            }
          }
          // receive then send if message is data (not audio or video)
          if (subscriberStream.getState() == StreamState.PLAYING && pullMode) {
            if (pendingMessage != null) {
              IRTMPEvent body = pendingMessage.getBody();
              if (okayToSendMessage(body)) {
                sendMessage(pendingMessage);
                releasePendingMessage();
              } else {
                return;
              }
            } else {
              IMessage msg = null;
              do {
                msg = msgIn.pullMessage();
                if (msg != null) {
                  if (msg instanceof RTMPMessage) {
                    RTMPMessage rtmpMessage = (RTMPMessage) msg;
                    if (checkSendMessageEnabled(rtmpMessage)) {
                      // Adjust timestamp when playing lists
                      IRTMPEvent body = rtmpMessage.getBody();
                      body.setTimestamp(body.getTimestamp() + timestampOffset);
                      if (okayToSendMessage(body)) {
                        log.trace("ts: {}", rtmpMessage.getBody().getTimestamp());
                        sendMessage(rtmpMessage);
                        ((IStreamData<?>) body).getData().free();
                      } else {
                        pendingMessage = rtmpMessage;
                      }
                      ensurePullAndPushRunning();
                      break;
                    }
                  }
                } else {
                  // No more packets to send
                  log.debug("Ran out of packets");
                  runDeferredStop();
                }
              } while (msg != null);
            }
          }
        } catch (IOException err) {
          // we couldn't get more data, stop stream.
          log.error("Error while getting message", err);
          runDeferredStop();
        } finally {
          // reset running flag
          pushPullRunning.compareAndSet(true, false);
        }
      } else {
        log.debug("Push / pull already running");
      }
    }
  }

  private class DeferredStopRunnable implements IScheduledJob {

    public void execute(ISchedulingService service) {
      if (isClientBufferEmpty()) {
        log.trace("Buffer is empty, stop will proceed");
        stop();
      }
    }

  }

  /**
   * @param maxPendingVideoFrames the maxPendingVideoFrames to set
   */
  public void setMaxPendingVideoFrames(int maxPendingVideoFrames) {
    this.maxPendingVideoFramesThreshold = maxPendingVideoFrames;
  }

  /**
   * @param maxSequentialPendingVideoFrames the maxSequentialPendingVideoFrames to set
   */
  public void setMaxSequentialPendingVideoFrames(int maxSequentialPendingVideoFrames) {
    this.maxSequentialPendingVideoFrames = maxSequentialPendingVideoFrames;
  }
}
TOP

Related Classes of org.red5.server.stream.PlayEngine

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.