Package org.red5.server.stream

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

/*
* 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.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IPlaylistController;
import org.red5.server.api.stream.IServerStream;
import org.red5.server.api.stream.IStreamFilenameGenerator;
import org.red5.server.api.stream.IStreamFilenameGenerator.GenerationType;
import org.red5.server.api.stream.IStreamListener;
import org.red5.server.api.stream.IStreamPacket;
import org.red5.server.api.stream.ResourceExistException;
import org.red5.server.api.stream.ResourceNotFoundException;
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.AudioData;
import org.red5.server.net.rtmp.event.IRTMPEvent;
import org.red5.server.net.rtmp.event.VideoData;
import org.red5.server.stream.consumer.FileConsumer;
import org.red5.server.stream.message.RTMPMessage;
import org.red5.server.stream.message.ResetMessage;
import org.red5.server.util.ScopeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;

/**
* An implementation for server side stream.
*
* @author The Red5 Project
*/
public class NoSyncServerStream extends AbstractStream implements IServerStream, IFilter, IPushableConsumer, IPipeConnectionListener {
  /**
   * Enumeration for states
   */
  private enum State {
    CLOSED, PLAYING, STOPPED, UNINIT, PAUSED
  }

  /**
   * Logger
   */
  private static final Logger log = LoggerFactory.getLogger(NoSyncServerStream.class);

  /**
   * Actual playlist controller
   */
  private IPlaylistController controller;

  /**
   * Current item
   */
  private IPlayItem currentItem;

  /**
   * Current item index
   */
  private int currentItemIndex;

  /**
   * Default playlist controller
   */
  private IPlaylistController defaultController;

  /**
   * Random flag state
   */
  private boolean isRandom;

  /**
   * Repeat flag state
   */
  private boolean isRepeat;

  /**
   * Rewind flag state
   */
  private boolean isRewind;

  /**
   * List of items in this playlist
   */
  private List<IPlayItem> items;

  /**
   * Live broadcasting scheduled job name
   */
  private String liveJobName;

  /**
   * Message input
   */
  private IMessageInput msgIn;

  /**
   * Message output
   */
  private IMessageOutput msgOut;

  /**
   * Next msg's audio timestamp
   */
  private long nextAudioTS;

  /**
   * Next msg's data timestamp
   */
  private long nextDataTS;

  /**
   * Next RTMP message
   */
  private RTMPMessage nextRTMPMessage;

  /**
   * Next msg's timestamp
   */
  private long nextTS;

  /**
   * Next msg's video timestamp
   */
  private long nextVideoTS;

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

  /**
   * The filename we are recording to.
   */
  private String recordingFilename;

  /**
   * Pipe for recording
   */
  private IPipe recordPipe;

  /**
   * Scheduling service
   */
  private ISchedulingService scheduler;

  /**
   * Server start timestamp
   */
  private long serverStartTS;

  /**
   * Current state
   */
  private State state;

  /**
   * VOD scheduled job name
   */
  private String vodJobName;

  /**
   * VOD start timestamp
   */
  private long vodStartTS;

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

  /** Constructs a new ServerStream. */
  public NoSyncServerStream() {
    defaultController = new SimplePlaylistController();
    items = new CopyOnWriteArrayList<IPlayItem>();
    state = State.UNINIT;
  }

  /** {@inheritDoc} */
  public void addItem(IPlayItem item) {
    items.add(item);
  }

  /** {@inheritDoc} */
  public void addItem(IPlayItem item, int index) {
    items.add(index, item);
  }

  /** {@inheritDoc} */
  public void close() {
    if (state == State.PLAYING || state == State.PAUSED) {
      stop();
    }
    if (msgOut != null) {
      msgOut.unsubscribe(this);
    }
    recordPipe.unsubscribe((IProvider) this);
    state = State.CLOSED;
  }

  /** {@inheritDoc} */
  public IPlayItem getCurrentItem() {
    return currentItem;
  }

  /** {@inheritDoc} */
  public int getCurrentItemIndex() {
    return currentItemIndex;
  }

  /** {@inheritDoc} */
  public IPlayItem getItem(int index) {
    try {
      return items.get(index);
    } catch (IndexOutOfBoundsException e) {
      return null;
    }
  }

  /** {@inheritDoc} */
  public int getItemSize() {
    return items.size();
  }

  /**
   * Getter for next RTMP message.
   *
   * @return Next RTMP message
   */
  private RTMPMessage getNextRTMPMessage() {
    IMessage message;
    do {
      // Pull message from message input object...
      try {
        message = msgIn.pullMessage();
      } catch (IOException err) {
        log.error("Error while pulling message.", err);
        message = null;
      }
      // If message is null then return null
      if (message == null) {
        return null;
      }
    } while (!(message instanceof RTMPMessage));
    // Cast and return
    return (RTMPMessage) message;
  }

  /** {@inheritDoc} */
  public IProvider getProvider() {
    return this;
  }

  /** {@inheritDoc} */
  public String getPublishedName() {
    return publishedName;
  }

  /** {@inheritDoc} */
  public String getSaveFilename() {
    return recordingFilename;
  }

  /** {@inheritDoc} */
  public boolean hasMoreItems() {
    int nextItem = currentItemIndex + 1;
    if (nextItem >= items.size() && !isRepeat) {
      return false;
    } else {
      return true;
    }
  }

  /** {@inheritDoc} */
  public boolean isRandom() {
    return isRandom;
  }

  /** {@inheritDoc} */
  public boolean isRepeat() {
    return isRepeat;
  }

  /** {@inheritDoc} */
  public boolean isRewind() {
    return isRewind;
  }

  /**
   * Move to the next item updating the currentItemIndex. Should be called
   * in context.
   */
  private void moveToNext() {
    if (currentItemIndex >= items.size()) {
      currentItemIndex = items.size() - 1;
    }
    if (controller != null) {
      currentItemIndex = controller.nextItem(this, currentItemIndex);
    } else {
      currentItemIndex = defaultController.nextItem(this, currentItemIndex);
    }
  }

  /**
   * Move to the previous item updating the currentItemIndex. Should be
   * called in context.
   */
  private void moveToPrevious() {
    if (currentItemIndex >= items.size()) {
      currentItemIndex = items.size() - 1;
    }
    if (controller != null) {
      currentItemIndex = controller.previousItem(this, currentItemIndex);
    } else {
      currentItemIndex = defaultController.previousItem(this, currentItemIndex);
    }
  }

  /** {@inheritDoc} */
  public void nextItem() {
    stop();
    moveToNext();
    if (currentItemIndex == -1) {
      return;
    }
    IPlayItem item = items.get(currentItemIndex);
    play(item);
  }

  /**
   * Play next item on item end
   */
  private void onItemEnd() {
    nextItem();
  }

  /** {@inheritDoc} */
  public void onOOBControlMessage(IMessageComponent source, IPipe pipe, OOBControlMessage oobCtrlMsg) {
  }

  /**
   * Pipe connection event handler. There are two types of pipe connection
   * events so far, provider push connection event and provider
   * disconnection event.
   *
   * Pipe events handling is the most common way of working with pipes.
   *
   * @param event Pipe connection event context
   */
  public void onPipeConnectionEvent(PipeConnectionEvent event) {
    switch (event.getType()) {
      case PipeConnectionEvent.PROVIDER_CONNECT_PUSH:
        if (event.getProvider() == this && (event.getParamMap() == null || !event.getParamMap().containsKey("record"))) {
          this.msgOut = (IMessageOutput) event.getSource();
        }
        break;
      case PipeConnectionEvent.PROVIDER_DISCONNECT:
        if (this.msgOut == event.getSource()) {
          this.msgOut = null;
        }
        break;
      default:
        break;
    }
  }

  /**
   * Play a specific IPlayItem. The strategy for now is VOD first, Live
   * second. Should be called in a context.
   *
   * @param item
   *                Item to play
   */
  private void play(IPlayItem item) {
    // Return if already playing
    if (state != State.STOPPED) {
      return;
    }
    // Assume this is not live stream
    boolean isLive = false;
    // Get provider service from Spring bean factory
    IProviderService providerService = (IProviderService) getScope().getContext().getBean(IProviderService.BEAN_NAME);
    msgIn = providerService.getVODProviderInput(getScope(), item.getName());
    if (msgIn == null) {
      msgIn = providerService.getLiveProviderInput(getScope(), item.getName(), true);
      isLive = true;
    }
    if (msgIn == null) {
      log.warn("ABNORMAL Can't get both VOD and Live input from providerService");
      return;
    }
    state = State.PLAYING;
    currentItem = item;
    sendResetMessage();
    msgIn.subscribe(this, null);
    if (isLive) {
      if (item.getLength() >= 0) {
        liveJobName = scheduler.addScheduledOnceJob(item.getLength(), new IScheduledJob() {
          /** {@inheritDoc} */
          public void execute(ISchedulingService service) {
            if (liveJobName == null) {
              return;
            }
            liveJobName = null;
            onItemEnd();
          }
        });
      }
    } else {
      long start = item.getStart();
      if (start < 0) {
        start = 0;
      }
      sendVODInitCM(msgIn, (int) start);
      startBroadcastVOD();
    }
  }

  /** {@inheritDoc} */
  public void previousItem() {
    stop();
    moveToPrevious();
    if (currentItemIndex == -1) {
      return;
    }
    IPlayItem item = items.get(currentItemIndex);
    play(item);
  }

  /**
   * Push message
   *
   * @param message
   *                Message
   */
  private void pushMessage(IMessage message) throws IOException {
    msgOut.pushMessage(message);
    recordPipe.pushMessage(message);

    // Notify listeners about received packet
    if (message instanceof RTMPMessage) {
      final IRTMPEvent rtmpEvent = ((RTMPMessage) message).getBody();
      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);
          }
        }
      }
    }
  }

  /** {@inheritDoc} */
  public void pushMessage(IPipe pipe, IMessage message) throws IOException {
    pushMessage(message);
  }

  /** {@inheritDoc} */
  public void removeAllItems() {
    items.clear();
  }

  /** {@inheritDoc} */
  public void removeItem(int index) {
    if (index < 0 || index >= items.size()) {
      return;
    }
    items.remove(index);
  }

  /** {@inheritDoc} */
  public void saveAs(String name, boolean isAppend) throws IOException, ResourceNotFoundException, ResourceExistException {
    try {
      IScope scope = getScope();
      IStreamFilenameGenerator generator = (IStreamFilenameGenerator) ScopeUtils.getScopeService(scope, IStreamFilenameGenerator.class, DefaultStreamFilenameGenerator.class);

      String filename = generator.generateFilename(scope, name, ".flv", GenerationType.RECORD);
      Resource res = scope.getContext().getResource(filename);
      if (!isAppend) {
        if (res.exists()) {
          // Per livedoc of FCS/FMS:
          // When "live" or "record" is used,
          // any previously recorded stream with the same stream
          // URI is deleted.
          if (!res.getFile().delete())
            throw new IOException("file could not be deleted");
        }
      } else {
        if (!res.exists()) {
          // Per livedoc of FCS/FMS:
          // If a recorded stream at the same URI does not already
          // exist,
          // "append" creates the stream as though "record" was
          // passed.
          isAppend = false;
        }
      }

      if (!res.exists()) {
        // Make sure the destination directory exists
        try {
          String path = res.getFile().getAbsolutePath();
          int slashPos = path.lastIndexOf(File.separator);
          if (slashPos != -1) {
            path = path.substring(0, slashPos);
          }
          File tmp = new File(path);
          if (!tmp.isDirectory()) {
            tmp.mkdirs();
          }
        } catch (IOException err) {
          log.error("Could not create destination directory.", err);
        }
        res = scope.getResource(filename);
      }

      if (!res.exists()) {
        if (!res.getFile().canWrite()) {
          log.warn("File cannot be written to " + res.getFile().getCanonicalPath());
        }
        res.getFile().createNewFile();
      }
      FileConsumer fc = new FileConsumer(scope, res.getFile());
      Map<String, Object> paramMap = new HashMap<String, Object>();
      if (isAppend) {
        paramMap.put("mode", "append");
      } else {
        paramMap.put("mode", "record");
      }
      if (null == recordPipe) {
        recordPipe = new InMemoryPushPushPipe();
      }
      recordPipe.subscribe(fc, paramMap);
      recordingFilename = filename;
    } catch (IOException e) {
      log.warn("Save as exception", e);
    }
  }

  /**
   * Pull the next message from IMessageInput and schedule it for push
   * according to the timestamp.
   */
  private void scheduleNextMessage() {
    boolean first = nextRTMPMessage == null;

    nextRTMPMessage = getNextRTMPMessage();
    if (nextRTMPMessage == null) {
      onItemEnd();
      return;
    }

    IRTMPEvent rtmpEvent = null;

    if (first) {
      rtmpEvent = nextRTMPMessage.getBody();
      // FIXME hack the first Metadata Tag from FLVReader
      // the FLVReader will issue a metadata tag of ts 0
      // even if it is seeked to somewhere in the middle
      // which will cause the stream to wait too long.
      // Is this an FLVReader's bug?
      if (!(rtmpEvent instanceof VideoData) && !(rtmpEvent instanceof AudioData) && rtmpEvent.getTimestamp() == 0) {
        rtmpEvent.release();
        nextRTMPMessage = getNextRTMPMessage();
        if (nextRTMPMessage == null) {
          onItemEnd();
          return;
        }
      }
    }

    rtmpEvent = nextRTMPMessage.getBody();
    if (rtmpEvent instanceof VideoData) {
      nextVideoTS = rtmpEvent.getTimestamp();
      nextTS = nextVideoTS;
    } else if (rtmpEvent instanceof AudioData) {
      nextAudioTS = rtmpEvent.getTimestamp();
      nextTS = nextAudioTS;
    } else {
      nextDataTS = rtmpEvent.getTimestamp();
      nextTS = nextDataTS;
    }
    if (first) {
      vodStartTS = nextTS;
    }
    long delta = nextTS - vodStartTS - (System.currentTimeMillis() - serverStartTS);

    vodJobName = scheduler.addScheduledOnceJob(delta, new IScheduledJob() {
      /** {@inheritDoc} */
      public void execute(ISchedulingService service) {
        if (vodJobName == null) {
          return;
        }
        vodJobName = null;
        try {
          pushMessage(nextRTMPMessage);
        } catch (IOException err) {
          log.error("Error while sending message.", err);
        }
        nextRTMPMessage.getBody().release();
        long start = currentItem.getStart();
        if (start < 0) {
          start = 0;
        }
        if (currentItem.getLength() >= 0 && nextTS - currentItem.getStart() > currentItem.getLength()) {
          onItemEnd();
          return;
        }
        scheduleNextMessage();
      }
    });
  }

  /**
   * Send reset message
   */
  private void sendResetMessage() {
    // Send new reset message
    try {
      pushMessage(new ResetMessage());
    } catch (IOException err) {
      log.error("Error while sending reset message.", err);
    }
  }

  /**
   * Send VOD initialization control message
   *
   * @param msgIn
   *                Message input
   * @param start
   *                Start timestamp
   */
  private void sendVODInitCM(IMessageInput msgIn, int start) {
    // Create new out-of-band control message
    OOBControlMessage oobCtrlMsg = new OOBControlMessage();
    // Set passive type
    oobCtrlMsg.setTarget(IPassive.KEY);
    // Set service name of init
    oobCtrlMsg.setServiceName("init");
    // Create map for parameters
    Map<String, Object> paramMap = new HashMap<String, Object>();
    // Put start timestamp into Map of params
    paramMap.put("startTS", start);
    // Attach to OOB control message and send it
    oobCtrlMsg.setServiceParamMap(paramMap);
    msgIn.sendOOBControlMessage(this, oobCtrlMsg);
  }

  /** {@inheritDoc} */
  public void setItem(int index) {
    if (index < 0 || index >= items.size()) {
      return;
    }
    stop();
    currentItemIndex = index;
    IPlayItem item = items.get(currentItemIndex);
    play(item);
  }

  /** {@inheritDoc} */
  public void setPlaylistController(IPlaylistController controller) {
    this.controller = controller;
  }

  /** {@inheritDoc} */
  public void setPublishedName(String name) {
    publishedName = name;
  }

  /** {@inheritDoc} */
  public void setRandom(boolean random) {
    isRandom = random;
  }

  /** {@inheritDoc} */
  public void setRepeat(boolean repeat) {
    isRepeat = repeat;
  }

  /** {@inheritDoc} */
  public void setRewind(boolean rewind) {
    isRewind = rewind;
  }

  /**
   * Start this server-side stream
   */
  public void start() {
    if (state != State.UNINIT) {
      throw new IllegalStateException("State " + state + " not valid to start");
    }
    if (items.size() == 0) {
      throw new IllegalStateException("At least one item should be specified to start");
    }
    if (publishedName == null) {
      throw new IllegalStateException("A published name is needed to start");
    }
    // publish this server-side stream
    IProviderService providerService = (IProviderService) getScope().getContext().getBean(IProviderService.BEAN_NAME);
    providerService.registerBroadcastStream(getScope(), publishedName, this);
    Map<String, Object> recordParamMap = new HashMap<String, Object>();
    recordPipe = new InMemoryPushPushPipe();
    recordParamMap.put("record", null);
    recordPipe.subscribe((IProvider) this, recordParamMap);
    recordingFilename = null;
    scheduler = (ISchedulingService) getScope().getContext().getBean(ISchedulingService.BEAN_NAME);
    state = State.STOPPED;
    currentItemIndex = -1;
    nextItem();
  }

  /**
   * Begin VOD broadcasting
   */
  private void startBroadcastVOD() {
    nextVideoTS = nextAudioTS = nextDataTS = 0;
    nextRTMPMessage = null;
    vodStartTS = 0;
    serverStartTS = System.currentTimeMillis();
    scheduleNextMessage();
  }

  /**
   * Stop this server-side stream
   */
  public void stop() {
    if (state != State.PLAYING && state != State.PAUSED) {
      return;
    }
    if (liveJobName != null) {
      scheduler.removeScheduledJob(liveJobName);
      liveJobName = null;
    }
    if (vodJobName != null) {
      scheduler.removeScheduledJob(vodJobName);
      vodJobName = null;
    }
    if (msgIn != null) {
      msgIn.unsubscribe(this);
      msgIn = null;
    }
    if (nextRTMPMessage != null) {
      nextRTMPMessage.getBody().release();
    }
    state = State.STOPPED;
  }

  /** {@inheritDoc} */
  public void pause() {
    if (state == State.PLAYING) {
      state = State.PAUSED;
    } else if (state == State.PAUSED) {
      state = State.PLAYING;
      vodStartTS = 0;
      serverStartTS = System.currentTimeMillis();
      scheduleNextMessage();
    }
  }

  /** {@inheritDoc} */
  public void seek(int position) {
    if (state != State.PLAYING && state != State.PAUSED)
      // Can't seek when stopped/closed
      return;

    sendVODSeekCM(msgIn, position);
  }

  /**
   * Send VOD seek control message
   *
   * @param msgIn        Message input
   * @param position      New timestamp to play from
   */
  private void sendVODSeekCM(IMessageInput msgIn, int position) {
    OOBControlMessage oobCtrlMsg = new OOBControlMessage();
    oobCtrlMsg.setTarget(ISeekableProvider.KEY);
    oobCtrlMsg.setServiceName("seek");
    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("position", new Integer(position));
    oobCtrlMsg.setServiceParamMap(paramMap);
    msgIn.sendOOBControlMessage(this, oobCtrlMsg);
    synchronized (this) {
      // Reset properties
      vodStartTS = 0;
      serverStartTS = System.currentTimeMillis();
      if (nextRTMPMessage != null) {
        try {
          pushMessage(nextRTMPMessage);
        } catch (IOException err) {
          log.error("Error while sending message.", err);
        }
        nextRTMPMessage.getBody().release();
        nextRTMPMessage = null;
      }
      ResetMessage reset = new ResetMessage();
      try {
        pushMessage(reset);
      } catch (IOException err) {
        log.error("Error while sending message.", err);
      }
      scheduleNextMessage();
    }
  }

  /** {@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);
  }

}
TOP

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

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.