com.github.mrstampy.pprspray.core.streamer.AbstractMediaStreamer.java Source code

Java tutorial

Introduction

Here is the source code for com.github.mrstampy.pprspray.core.streamer.AbstractMediaStreamer.java

Source

/*
 * PepperSpray-core, Encrypted Secure Communications Library
 * 
 * Copyright (C) 2014 Burton Alexander
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * 
 */
package com.github.mrstampy.pprspray.core.streamer;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.util.concurrent.GenericFutureListener;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rx.Scheduler;
import rx.Subscription;
import rx.functions.Action0;
import rx.schedulers.Schedulers;

import com.github.mrstampy.kitchensync.netty.channel.KiSyChannel;
import com.github.mrstampy.kitchensync.stream.ByteArrayStreamer;
import com.github.mrstampy.pprspray.core.handler.NegotiationAckHandler;
import com.github.mrstampy.pprspray.core.handler.NegotiationHandler;
import com.github.mrstampy.pprspray.core.receiver.negotiation.NegotiationAckReceiver;
import com.github.mrstampy.pprspray.core.streamer.chunk.AbstractMediaChunkProcessor;
import com.github.mrstampy.pprspray.core.streamer.chunk.event.ChunkEventBus;
import com.github.mrstampy.pprspray.core.streamer.event.MediaStreamerEvent;
import com.github.mrstampy.pprspray.core.streamer.event.MediaStreamerEventBus;
import com.github.mrstampy.pprspray.core.streamer.event.MediaStreamerEventType;
import com.github.mrstampy.pprspray.core.streamer.footer.MediaFooter;
import com.github.mrstampy.pprspray.core.streamer.footer.MediaFooterChunk;
import com.github.mrstampy.pprspray.core.streamer.negotiation.AbstractNegotiationSubscriber;
import com.github.mrstampy.pprspray.core.streamer.negotiation.AcceptingNegotationSubscriber;
import com.github.mrstampy.pprspray.core.streamer.negotiation.NegotiationAckChunk;
import com.github.mrstampy.pprspray.core.streamer.negotiation.NegotiationChunk;
import com.github.mrstampy.pprspray.core.streamer.negotiation.NegotiationEventBus;
import com.github.mrstampy.pprspray.core.streamer.negotiation.NegotiationMessageUtils;
import com.github.mrstampy.pprspray.core.streamer.util.MediaStreamerUtils;
import com.google.common.eventbus.Subscribe;

/**
 * AbstractMediaStreamer contains common methods and properties for the creation
 * of media streamers. Invocations of connect() will, if
 * {@link #isAutoNegotiate()}, negotiate with the remote site using a unique
 * identifier and will start streaming upon confirmation. Manual negotiations
 * will require a setting of {@link #setNotifyAccepted(boolean)} before
 * streaming can start.
 * 
 * @see MediaStreamType
 */
public abstract class AbstractMediaStreamer {
    private static final Logger log = LoggerFactory.getLogger(AbstractMediaStreamer.class);

    private static final AtomicInteger ID = new AtomicInteger(0);

    private AtomicBoolean streaming = new AtomicBoolean(false);

    /** The notifying. */
    protected AtomicBoolean notifying = new AtomicBoolean(false);

    /** The notify accepted. */
    protected AtomicBoolean notifyAccepted = new AtomicBoolean(false);

    private AtomicBoolean destroyed = new AtomicBoolean(false);

    private Scheduler scheduler = Schedulers.from(Executors.newSingleThreadExecutor());
    private Subscription sub;

    private AbstractMediaChunkProcessor mediaChunkProcessor;
    private MediaFooter mediaFooter;

    private int id = -1;

    private boolean ackRequired;
    private boolean fullThrottle;

    private int throttle = 0;
    private int chunksPerSecond = -1;
    private int streamerPipeSize;
    private int concurrentThreads = 2;

    private String description;

    private KiSyChannel channel;
    private InetSocketAddress destination;

    private ByteArrayStreamer streamer;

    private MediaStreamType type;

    private boolean autoNegotiate = true;

    /**
     * The Constructor.
     *
     * @param defaultPipeSize
     *          the default pipe size
     * @param channel
     *          the channel
     * @param destination
     *          the destination
     * @param type
     *          the type
     */
    protected AbstractMediaStreamer(int defaultPipeSize, KiSyChannel channel, InetSocketAddress destination,
            MediaStreamType type) {
        setStreamerPipeSize(defaultPipeSize);
        this.channel = channel;
        this.destination = destination;
        this.type = type;

        initStreamer();

        ChunkEventBus.register(this);

        addChannelCloseListener();

        setConcurrentThreads(1);
    }

    /**
     * End of message.
     *
     * @param eom
     *          the eom
     * @see ChunkEventBus#register(Object)
     */
    @Subscribe
    public void terminate(MediaFooterChunk eom) {
        if (!eom.isTerminateMessage(getMediaHash()))
            return;

        log.debug("Received receiver termination for type {}, hash {} from {}", getType(), getMediaHash(),
                getDestination());

        try {
            destroyImpl();
        } catch (Exception e) {
            log.error("Unexpected exception", e);
        }
    }

    private void initStreamer() {
        try {
            streamer = createStreamer();
        } catch (Exception e) {
            log.error("Unexpected exception", e);
            throw new IllegalStateException("Cannot initialize streamer", e);
        }
    }

    /**
     * Utility method to return a unique id for a streamer, useful in quickly
     * identifying streamers when receiving {@link MediaStreamerEvent}s.
     *
     * @return the id
     * @see MediaStreamerEventBus
     */
    public int getId() {
        if (id == -1)
            id = ID.incrementAndGet();

        return id;
    }

    /**
     * Checks if is streaming.
     *
     * @return true, if checks if is streaming
     */
    public boolean isStreaming() {
        return streaming.get();
    }

    /**
     * Destroy.
     */
    public void destroy() {
        if (destroyed.get())
            return;
        destroyed.set(true);
        MediaStreamerUtils.sendTerminationEvent(getMediaHash(), getChannel(), getDestination());
        destroyImpl();
    }

    /**
     * Destroy impl.
     */
    protected void destroyImpl() {
        if (isStreaming())
            stop();

        streamer.cancel();
        unregisterForChunks();
        notifyDestroyed();
    }

    /**
     * If the connection has not been negotiated {@link #negotiate()} will be
     * invoked, else {@link #start()}.
     */
    public void connect() {
        if (notifying())
            return;
        if (isStreaming())
            return;

        if (notifyAccepted()) {
            start();
        } else if (isAutoNegotiate()) {
            negotiate();
        } else {
            throw new IllegalStateException("Cannot connect media streamer, notifyAccepted() is false");
        }
    }

    /**
     * Start.
     */
    protected void start() {
        streaming.set(true);
        if (!streamer.isStreaming())
            streamer.stream();
        notifyStart();

        sub = scheduler.createWorker().schedulePeriodically(new Action0() {

            @Override
            public void call() {
                try {
                    if (isStreaming()) {
                        stream();
                    } else {
                        unsubscribe();
                    }
                } catch (Exception e) {
                    log.error("Unexpected exception", e);
                    stop();
                }
            }
        }, 0, 0, TimeUnit.SECONDS);
    }

    /**
     * Returns true if this media streamer is awaiting negotiation confirmation.
     *
     * @return true, if notifying
     * @see NegotiationEventBus
     * @see NegotiationChunk
     * @see NegotiationAckChunk
     * @see NegotiationAckReceiver
     * @see NegotiationHandler
     * @see NegotiationAckHandler
     */
    public boolean notifying() {
        return notifying.get();
    }

    /**
     * Returns true if this media streamer has received affirmative negotiation
     * confirmation.
     *
     * @return true, if notify accepted
     * @see NegotiationEventBus
     * @see NegotiationChunk
     * @see NegotiationAckChunk
     * @see NegotiationAckReceiver
     * @see NegotiationHandler
     * @see NegotiationAckHandler
     */
    public boolean notifyAccepted() {
        return notifyAccepted.get();
    }

    /**
     * To be set manually when {@link #isAutoNegotiate()} is false, prior to
     * calling {@link #connect()}.
     *
     * @param accepted
     *          the notify accepted
     */
    public void setNotifyAccepted(boolean accepted) {
        if (isAutoNegotiate())
            log.warn("Auto negotiation is on.  Setting notify accepted to {}", accepted);

        notifyAccepted.set(accepted);
    }

    /**
     * Sends a
     * {@link NegotiationMessageUtils#getNegotiationMessage(int, MediaStreamType)}
     * to the destinations and awaits acknowledgement. If affirmative
     * {@link #start()} is invoked to commence streaming.
     * 
     * @see NegotiationEventBus
     * @see NegotiationChunk
     * @see NegotiationAckChunk
     * @see NegotiationAckReceiver
     * @see AbstractNegotiationSubscriber
     * @see AcceptingNegotationSubscriber
     */
    protected void negotiate() {
        log.debug("Negotiating with {} for media hash {}", getDestination(), getMediaHash());

        notifying.set(true);

        notifyNegotiating();

        ByteBuf buf = NegotiationMessageUtils.getNegotiationMessage(getMediaHash(), getType());

        ChunkEventBus.register(new AckReceiver(getMediaHash()));

        getChannel().send(buf.array(), getDestination());
    }

    /**
     * Stop.
     */
    public void stop() {
        unsubscribe();
        streamer.pause();
        notifyStop();
    }

    /**
     * Gets the sequence.
     *
     * @return the sequence
     */
    public long getSequence() {
        return streamer.getSequence();
    }

    private ByteArrayStreamer createStreamer() throws Exception {
        ByteArrayStreamer bas = new ByteArrayStreamer(getChannel(), getDestination(), getStreamerPipeSize());

        bas.setEomOnFinish(true);
        bas.setProcessChunk(true);
        bas.setChunkProcessor(getMediaChunkProcessor());
        bas.setFooter(getMediaFooter());

        if (isAckRequired())
            bas.ackRequired();
        if (getChunksPerSecond() > 0)
            bas.setChunksPerSecond(getChunksPerSecond());
        if (isFullThrottle())
            bas.fullThrottle();

        bas.setThrottle(getThrottle());
        bas.setConcurrentThreads(getConcurrentThreads());

        notifyAdd();

        return bas;
    }

    /**
     * Checks if is ack required.
     *
     * @return true, if checks if is ack required
     */
    public boolean isAckRequired() {
        return ackRequired;
    }

    private void notifyAdd() {
        log.debug("Adding Media Streamer for channel {} and destination {}", getChannel().localAddress(),
                getDestination());
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.DESTINATION_ADDED));
    }

    private void notifyStart() {
        log.debug("Started streamer, type {}, hash {} for {}", getType(), getMediaHash(), getDestination());
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.STARTED));
    }

    private void notifyStop() {
        log.debug("Stopped streamer, type {}, hash {} for {}", getType(), getMediaHash(), getDestination());
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.STOPPED));
    }

    private void notifyDestroyed() {
        log.debug("Destroyed streamer, type {}, hash {} for {}", getType(), getMediaHash(), getDestination());
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.DESTROYED));
    }

    private void notifyNegotiationFailed() {
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.NEGOTIATION_FAILED));
    }

    private void notifyNegotiationSuccessful() {
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.NEGOTIATION_SUCCESSFUL));
    }

    private void notifyNegotiating() {
        MediaStreamerEventBus.post(new MediaStreamerEvent(this, MediaStreamerEventType.NEGOTIATING));
    }

    /**
     * Stream.
     */
    protected void stream() {
        try {
            byte[] data = getBytes();

            if (data == null || data.length == 0)
                return;

            setMessageHash(data);

            sendData(data);
        } catch (Exception e) {
            log.error("Unexpected exception streaming from {} to {}", streamer.getChannel().localAddress(),
                    streamer.getDestination(), e);
        }
    }

    private void setMessageHash(byte[] data) {
        int messageHash = MediaStreamerUtils.createMessageHash();

        log.trace("Setting message hash {} for data length {}", messageHash, data.length);

        getMediaChunkProcessor().setMessageHash(messageHash);
        getMediaFooter().setMessageHash(messageHash);
    }

    /**
     * Send data.
     *
     * @param data
     *          the data
     * @throws Exception
     *           the exception
     */
    protected void sendData(byte[] data) throws Exception {
        ChannelFuture cf = streamer.stream(data);
        cf.await();
    }

    /**
     * Checks if is streamable.
     *
     * @return true, if checks if is streamable
     */
    protected abstract boolean isStreamable();

    /**
     * Gets the bytes.
     *
     * @return the bytes
     */
    protected abstract byte[] getBytes();

    private void unsubscribe() {
        if (sub != null)
            sub.unsubscribe();
    }

    /**
     * Gets the media chunk processor.
     *
     * @return the media chunk processor
     */
    public AbstractMediaChunkProcessor getMediaChunkProcessor() {
        return mediaChunkProcessor;
    }

    /**
     * Sets the media chunk processor.
     *
     * @param mediaChunkProcessor
     *          the media chunk processor
     */
    public void setMediaChunkProcessor(AbstractMediaChunkProcessor mediaChunkProcessor) {
        this.mediaChunkProcessor = mediaChunkProcessor;
        streamer.setChunkProcessor(mediaChunkProcessor);
    }

    /**
     * Gets the media hash.
     *
     * @return the media hash
     */
    public int getMediaHash() {
        return getMediaChunkProcessor().getMediaHash();
    }

    /**
     * Gets the media footer.
     *
     * @return the media footer
     */
    public MediaFooter getMediaFooter() {
        return mediaFooter;
    }

    /**
     * Sets the media footer.
     *
     * @param mediaFooter
     *          the media footer
     */
    public void setMediaFooter(MediaFooter mediaFooter) {
        this.mediaFooter = mediaFooter;
        streamer.setFooter(mediaFooter);
    }

    /**
     * Sets the ack required.
     *
     * @param isAckRequired
     *          the ack required
     */
    public void setAckRequired(boolean isAckRequired) {
        this.ackRequired = isAckRequired;
        if (isAckRequired)
            streamer.ackRequired();
    }

    /**
     * Gets the throttle.
     *
     * @return the throttle
     */
    public int getThrottle() {
        return throttle;
    }

    /**
     * Sets the throttle.
     *
     * @param throttle
     *          the throttle
     */
    public void setThrottle(int throttle) {
        this.throttle = throttle;
        streamer.setThrottle(throttle);
    }

    /**
     * Gets the chunks per second.
     *
     * @return the chunks per second
     */
    public int getChunksPerSecond() {
        return chunksPerSecond;
    }

    /**
     * Sets the chunks per second.
     *
     * @param chunksPerSecond
     *          the chunks per second
     */
    public void setChunksPerSecond(int chunksPerSecond) {
        this.chunksPerSecond = chunksPerSecond;
        streamer.setChunksPerSecond(chunksPerSecond);
    }

    /**
     * Sets the streamer pipe size.
     *
     * @param streamerPipeSize
     *          the streamer pipe size
     */
    public void setStreamerPipeSize(int streamerPipeSize) {
        this.streamerPipeSize = streamerPipeSize;
    }

    /**
     * Gets the streamer pipe size.
     *
     * @return the streamer pipe size
     */
    public int getStreamerPipeSize() {
        return streamerPipeSize;
    }

    /**
     * Gets the description.
     *
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * Sets the description.
     *
     * @param description
     *          the description
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * Gets the concurrent threads.
     *
     * @return the concurrent threads
     */
    public int getConcurrentThreads() {
        return concurrentThreads;
    }

    /**
     * Sets the concurrent threads.
     *
     * @param concurrentThreads
     *          the concurrent threads
     */
    public void setConcurrentThreads(int concurrentThreads) {
        this.concurrentThreads = concurrentThreads;
        streamer.setConcurrentThreads(concurrentThreads);
    }

    /**
     * Checks if is full throttle.
     *
     * @return true, if checks if is full throttle
     */
    public boolean isFullThrottle() {
        return fullThrottle;
    }

    /**
     * Sets the full throttle.
     *
     * @param fullThrottle
     *          the full throttle
     */
    public void setFullThrottle(boolean fullThrottle) {
        this.fullThrottle = fullThrottle;
        if (!fullThrottle)
            return;

        streamer.fullThrottle();
    }

    /**
     * Gets the channel.
     *
     * @return the channel
     */
    public KiSyChannel getChannel() {
        return channel;
    }

    /**
     * Gets the destination.
     *
     * @return the destination
     */
    public InetSocketAddress getDestination() {
        return destination;
    }

    /**
     * Gets the type.
     *
     * @return the type
     */
    public MediaStreamType getType() {
        return type;
    }

    /**
     * If true then any invocation of {@link #connect()} when
     * {@link #notifyAccepted()} is false will send a
     * {@link NegotiationMessageUtils#getNegotiationMessage(int, MediaStreamType)}
     * to the {@link #getDestination()} and await a successful
     * {@link NegotiationAckChunk} response to begin streaming.<br>
     * <br>
     * 
     * If false then the encapsulating application must negotiate the
     * {@link #getMediaHash()} with the {@link #getDestination()} and
     * {@link #setNotifyAccepted(boolean)} appropriately prior to calling
     * {@link #connect()}.
     *
     * @return true, if checks if is auto negotiate
     */
    public boolean isAutoNegotiate() {
        return autoNegotiate;
    }

    /**
     * Sets the auto negotiate.
     *
     * @param autoNegotiate
     *          the auto negotiate
     */
    public void setAutoNegotiate(boolean autoNegotiate) {
        this.autoNegotiate = autoNegotiate;
    }

    private void addChannelCloseListener() {
        getChannel().getChannel().closeFuture().addListener(new GenericFutureListener<ChannelFuture>() {

            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                destroyImpl();
            }
        });
    }

    private void unregisterForChunks() {
        try {
            ChunkEventBus.unregister(this);
        } catch (Exception e) {
            log.trace("Could not unregister", e);
        }
    }

    private class AckReceiver extends NegotiationAckReceiver {

        public AckReceiver(int mediaHash) {
            super(mediaHash);
        }

        @Override
        protected void ackReceived(NegotiationAckChunk chunk) {
            notifying.set(false);

            notifyAccepted.set(chunk.isAccepted());

            if (notifyAccepted()) {
                log.debug("Negotiations with {} for type {}, media hash {} successful", getDestination(),
                        AbstractMediaStreamer.this.getType(), getMediaHash());

                notifyNegotiationSuccessful();
                start();
            } else {
                log.debug("Negotiations with {} for type {}, media hash {} unsuccessful", getDestination(),
                        getType(), getMediaHash());

                notifyNegotiationFailed();
                AbstractMediaStreamer.this.destroy();
            }
        }

    }

}