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

Java tutorial

Introduction

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

Source

/*
 * RED5 Open Source Flash Server - http://code.google.com/p/red5/
 * 
 * Copyright 2006-2013 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);
    }

}