All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.red5.server.stream.ServerStream Maven / Gradle / Ivy

Go to download

Ant Media Server supports RTMP, RTSP, MP4, HLS, WebRTC, Adaptive Streaming, etc.

There is a newer version: 2.10.0
Show newest version
/*
 * 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.IOException;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

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.IContext;
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.IStreamAwareScopeHandler;
import org.red5.server.api.stream.IStreamListener;
import org.red5.server.api.stream.IStreamPacket;
import org.red5.server.api.stream.StreamState;
import org.red5.server.api.stream.support.SimplePlayItem;
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.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.message.RTMPMessage;
import org.red5.server.stream.message.ResetMessage;
import org.red5.server.util.ScopeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An implementation for server side stream.
 * 
 * @author The Red5 Project
 * @author Steven Gong ([email protected])
 * @author Paul Gregoire ([email protected])
 */
public class ServerStream extends AbstractStream implements IServerStream, IFilter, IPushableConsumer, IPipeConnectionListener {

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

    private static final long WAIT_THRESHOLD = 0;

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

    /**
     * Actual playlist controller
     */
    protected IPlaylistController controller;

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

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

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

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

    /**
     * List of items in this playlist
     */
    protected CopyOnWriteArrayList items;

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

    /**
     * Current item
     */
    protected IPlayItem currentItem;

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

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

    /**
     * Provider service
     */
    private IProviderService providerService;

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

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

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

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

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

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

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

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

    /**
     * Recording listener
     */
    private WeakReference recordingListener;

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

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

    /** {@inheritDoc} */
    public void addItem(IPlayItem item, int index) {
        IPlayItem prev = items.get(index);
        if (prev != null && prev instanceof SimplePlayItem) {
            // since it replaces the item in the current spot, reset the items time so the sort will work
            ((SimplePlayItem) item).setCreated(((SimplePlayItem) prev).getCreated() - 1);
        }
        items.add(index, item);
        if (index <= currentItemIndex) {
            // item was added before the currently playing
            currentItemIndex++;
        }
    }

    /** {@inheritDoc} */
    public void removeItem(int index) {
        if (index < 0 || index >= items.size()) {
            return;
        }
        items.remove(index);
        if (index < currentItemIndex) {
            // item was removed before the currently playing
            currentItemIndex--;
        } else if (index == currentItemIndex) {
            // TODO: the currently playing item is removed - this should be handled differently
            currentItemIndex--;
        }
    }

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

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

    public CopyOnWriteArrayList getItems() {
        return items;
    }

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

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

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

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

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

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

    /** {@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 boolean isRandom() {
        return isRandom;
    }

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

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

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

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

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

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

    /** {@inheritDoc} */
    public void saveAs(String name, boolean isAppend) throws IOException {
        // one recording listener at a time via this entry point
        if (recordingListener == null) {
            IScope scope = getScope();
            // create a recording listener
            IRecordingListener listener = (IRecordingListener) ScopeUtils.getScopeService(scope, IRecordingListener.class, RecordingListener.class);
            // initialize the listener
            if (listener.init(scope, 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<>(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.info("Recording listener already exists for stream: {}", name);
        }
    }

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

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

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

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

    /**
     * Start this server-side stream
     */
    public void start() {
        if (state != StreamState.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");
        }
        try {
            IScope scope = getScope();
            IContext context = scope.getContext();
            providerService = (IProviderService) context.getBean(IProviderService.BEAN_NAME);
            // publish this server-side stream
            providerService.registerBroadcastStream(scope, publishedName, this);
            scheduler = (ISchedulingService) context.getBean(ISchedulingService.BEAN_NAME);
        } catch (NullPointerException npe) {
            log.warn("Context beans were not available; this is ok during unit testing", npe);
        }
        setState(StreamState.STOPPED);
        currentItemIndex = -1;
        nextItem();
    }

    /**
     * Stop this server-side stream
     */
    public void stop() {
        if (state == StreamState.PLAYING || state == StreamState.PAUSED) {
            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();
            }
            stopRecording();
            setState(StreamState.STOPPED);
        }
    }

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

    /** {@inheritDoc} */
    @SuppressWarnings("incomplete-switch")
    public void pause() {
        switch (state) {
            case PLAYING:
                setState(StreamState.PAUSED);
                break;
            case PAUSED:
                setState(StreamState.PLAYING);
                vodStartTS = 0;
                serverStartTS = System.currentTimeMillis();
                scheduleNextMessage();
        }
    }

    /** {@inheritDoc} */
    public void seek(int position) {
        // seek only allowed when playing or paused
        if (state == StreamState.PLAYING || state == StreamState.PAUSED) {
            sendVODSeekCM(msgIn, position);
        }
    }

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

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

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

    /**
     * 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 PROVIDER_CONNECT_PUSH:
                if (event.getProvider() == this && (event.getParamMap() == null || !event.getParamMap().containsKey("record"))) {
                    this.msgOut = (IMessageOutput) event.getSource();
                }
                break;
            case 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.
     *
     * @param item
     *            Item to play
     */
    protected void play(IPlayItem item) {
        // dont play unless we are stopped
        if (state == StreamState.STOPPED) {
            // assume this is not live stream
            boolean isLive = false;
            if (providerService != null) {
                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;
                }
            }
            setState(StreamState.PLAYING);
            currentItem = item;
            sendResetMessage();
            if (msgIn != null) {
                msgIn.subscribe(this, null);
            }
            if (isLive) {
                if (item.getLength() >= 0) {
                    liveJobName = scheduler.addScheduledOnceJob(item.getLength(), new IScheduledJob() {
                        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();
            }
        }
    }

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

    /**
     * Push message
     * 
     * @param message
     *            Message
     */
    private void pushMessage(IMessage message) throws IOException {
        if (msgOut != null) {
            msgOut.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);
                    }
                }
            }
        }
    }

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

    /**
     * Begin VOD broadcasting
     */
    protected void startBroadcastVOD() {
        nextRTMPMessage = null;
        vodStartTS = 0;
        serverStartTS = System.currentTimeMillis();
        IStreamAwareScopeHandler handler = getStreamAwareHandler();
        if (handler != null) {
            if (recordingListener != null && recordingListener.get().isRecording()) {
                // callback for record start
                handler.streamRecordStart(this);
            } else {
                // callback for publish start
                handler.streamPublishStart(this);
            }
        }
        notifyBroadcastStart();
        scheduleNextMessage();
    }

    /**
     * Notifies handler on stream broadcast stop
     */
    protected void notifyBroadcastClose() {
        IStreamAwareScopeHandler handler = getStreamAwareHandler();
        if (handler != null) {
            try {
                handler.streamBroadcastClose(this);
            } catch (Throwable t) {
                log.error("error notify streamBroadcastStop", 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
     */
    protected void notifyBroadcastStart() {
        IStreamAwareScopeHandler handler = getStreamAwareHandler();
        if (handler != null) {
            try {
                handler.streamBroadcastStart(this);
            } catch (Throwable t) {
                log.error("error notify streamBroadcastStart", t);
            }
        }
    }

    /**
     * Pull the next message from IMessageInput and schedule it for push according to the timestamp.
     */
    protected void scheduleNextMessage() {
        boolean first = (nextRTMPMessage == null);
        long delta = 0L;
        do {
            nextRTMPMessage = getNextRTMPMessage();
            if (nextRTMPMessage != null) {
                IRTMPEvent rtmpEvent = nextRTMPMessage.getBody();
                // filter all non-AV messages
                if (rtmpEvent instanceof VideoData || rtmpEvent instanceof AudioData) {
                    rtmpEvent = nextRTMPMessage.getBody();
                    nextTS = rtmpEvent.getTimestamp();
                    if (first) {
                        vodStartTS = nextTS;
                        first = false;
                    }
                    delta = nextTS - vodStartTS - (System.currentTimeMillis() - serverStartTS);
                    if (delta < WAIT_THRESHOLD) {
                        if (doPushMessage()) {
                            if (state != StreamState.PLAYING) {
                                // Stream is not playing, don't load more messages
                                nextRTMPMessage = null;
                            }
                        } else {
                            nextRTMPMessage = null;
                        }
                    }
                }
            } else {
                onItemEnd();
            }
        } while (nextRTMPMessage != null || delta < WAIT_THRESHOLD);
        // start the job all over again
        vodJobName = scheduler.addScheduledOnceJob(delta, new IScheduledJob() {
            public void execute(ISchedulingService service) {
                if (vodJobName != null) {
                    vodJobName = null;
                    if (doPushMessage()) {
                        if (state == StreamState.PLAYING) {
                            scheduleNextMessage();
                        } else {
                            // Stream is paused, don't load more messages
                            nextRTMPMessage = null;
                        }
                    }
                }
            }
        });
    }

    private boolean doPushMessage() {
        boolean sent = false;
        long start = currentItem.getStart();
        if (start < 0) {
            start = 0;
        }
        if (currentItem.getLength() >= 0 && nextTS - start > currentItem.getLength()) {
            onItemEnd();
            return sent;
        }
        if (nextRTMPMessage != null) {
            sent = true;
            try {
                pushMessage(nextRTMPMessage);
            } catch (IOException err) {
                log.error("Error while sending message.", err);
            }
            nextRTMPMessage.getBody().release();
        }
        return sent;
    }

    /**
     * Getter for next RTMP message.
     *
     * @return Next RTMP message
     */
    protected RTMPMessage getNextRTMPMessage() {
        IMessage message;
        do {
            // Pull message from message input object...
            try {
                message = msgIn.pullMessage();
            } catch (Exception 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;
    }

    /**
     * Send VOD initialization control message
     * 
     * @param msgIn
     *            Message input
     * @param start
     *            Start timestamp
     */
    private void sendVODInitCM(IMessageInput msgIn, int start) {
        if (msgIn != null) {
            // 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 paramMap = new HashMap<>(1);
            // 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);
        }
    }

    /**
     * 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 paramMap = new HashMap<>(1);
        paramMap.put("position", Integer.valueOf(position));
        oobCtrlMsg.setServiceParamMap(paramMap);
        msgIn.sendOOBControlMessage(this, oobCtrlMsg);
        // 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();
    }

    /**
     * Move to the next item updating the currentItemIndex.
     */
    protected 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.
     */
    protected void moveToPrevious() {
        if (currentItemIndex >= items.size()) {
            currentItemIndex = items.size() - 1;
        }
        if (controller != null) {
            currentItemIndex = controller.previousItem(this, currentItemIndex);
        } else {
            currentItemIndex = defaultController.previousItem(this, currentItemIndex);
        }
    }

    public void addStreamListener(IStreamListener listener) {
        listeners.add(listener);
    }

    public Collection getStreamListeners() {
        return listeners;
    }

    public void removeStreamListener(IStreamListener listener) {
        listeners.remove(listener);
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "ServerStream [publishedName=" + publishedName + ", controller=" + controller + ", defaultController=" + defaultController + ", isRewind=" + isRewind + ", isRandom=" + isRandom + ", isRepeat=" + isRepeat + ", items=" + items + ", currentItemIndex=" + currentItemIndex + ", currentItem=" + currentItem + ", providerService=" + providerService + ", scheduler=" + scheduler + ", liveJobName=" + liveJobName
                + ", vodJobName=" + vodJobName + ", vodStartTS=" + vodStartTS + ", serverStartTS=" + serverStartTS + ", nextTS=" + nextTS + "]";
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy