org.opencastproject.capture.pipeline.GStreamerPipeline.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.capture.pipeline.GStreamerPipeline.java

Source

/**
 *  Copyright 2009, 2010 The Regents of the University of California
 *  Licensed under the Educational Community 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.osedu.org/licenses/ECL-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.opencastproject.capture.pipeline;

import org.opencastproject.capture.CaptureParameters;
import org.opencastproject.capture.impl.CaptureFailureHandler;
import org.opencastproject.capture.impl.RecordingImpl;
import org.opencastproject.capture.impl.UnableToStartCaptureException;
import org.opencastproject.capture.pipeline.bins.CaptureDevice;
import org.opencastproject.capture.pipeline.bins.CaptureDeviceBin;
import org.opencastproject.capture.pipeline.bins.producers.ProducerFactory;
import org.opencastproject.capture.pipeline.bins.producers.ProducerFactory.ProducerType;

import net.luniks.linux.jv4linfo.JV4LInfo;
import net.luniks.linux.jv4linfo.JV4LInfoException;
import net.luniks.linux.jv4linfo.V4LInfo;

import org.apache.commons.lang.StringUtils;
import org.gstreamer.Bus;
import org.gstreamer.Gst;
import org.gstreamer.GstObject;
import org.gstreamer.Pipeline;
import org.gstreamer.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Properties;

/**
 * Given a Properties object describing devices this class will create a suitable pipeline to capture from all those
 * devices simultaneously.
 */
public final class GStreamerPipeline {

    private static final int WAIT_FOR_NULL_SLEEP_TIME = 1000;
    private static final Logger logger = LoggerFactory.getLogger(GStreamerPipeline.class);
    // Capture properties. 
    private static Properties properties;
    // List of captureDeviceBins inside the pipeline so that we can send each an EOS. 
    private ArrayList<CaptureDeviceBin> captureDeviceBins;
    // Pipeline used to capture. 
    private Pipeline pipeline;
    // Used to stop the capture above if something goes wrong at this level. 
    private CaptureFailureHandler captureFailureHandler;
    /** The amount of time to wait until shutting down the pipeline forcefully.**/
    public static final long DEFAULT_PIPELINE_SHUTDOWN_TIMEOUT = 60000L;
    /**
     * The number of nanoseconds in a second. This is a borrowed constant from gStreamer and is used in the pipeline
     * initialisation routines
     */
    public static final long GST_SECOND = 1000000000L;

    public GStreamerPipeline(CaptureFailureHandler captureFailureHandler) {
        captureDeviceBins = new ArrayList<CaptureDeviceBin>();
        this.captureFailureHandler = captureFailureHandler;
    }

    /**
     * Creates the gStreamer pipeline and blocks until it starts successfully
     * 
     * @param newRec
     *          The RecordingImpl of the capture we wish to perform.
     * @return The recording ID (equal to newRec.getID()) or null in the case of an error
     */
    public void start(RecordingImpl newRec) {
        // Create the pipeline
        try {
            pipeline = create(newRec.getProperties(), false);
        } catch (UnsatisfiedLinkError e) {
            throw new UnableToStartCaptureException(
                    e.getMessage() + " : please add libjv4linfo.so to /usr/lib to correct this issue.");
        }

        // Check if the pipeline came up ok
        if (pipeline == null) {
            //logger.error("Capture {} could not start, pipeline was null!", newRec.getID());
            captureFailureHandler.resetOnFailure(newRec.getID());
            throw new UnableToStartCaptureException(
                    "Capture " + newRec.getID() + " could not start, pipeline was null!");
        }

        logger.info("Initializing devices for capture.");

        hookUpBus();

        // Grab time to wait for pipeline to start
        int wait;
        String waitProp = newRec.getProperty(CaptureParameters.CAPTURE_START_WAIT);
        if (waitProp != null) {
            wait = Integer.parseInt(waitProp);
        } else {
            wait = 15; // Default taken from gstreamer docs
        }

        pipeline.debugToDotFile(Pipeline.DEBUG_GRAPH_SHOW_ALL, pipeline.getName());
        // Try and start the pipeline
        pipeline.play();
        if (pipeline.getState(wait * GStreamerPipeline.GST_SECOND) != State.PLAYING) {
            // In case of an error call stop to clean up the pipeline.  
            logger.debug("Pipeline was unable to start after " + wait + " seconds.");
            stop(GStreamerPipeline.DEFAULT_PIPELINE_SHUTDOWN_TIMEOUT);
            throw new UnableToStartCaptureException(
                    "Unable to start pipeline after " + wait + " seconds.  Aborting!");
        }
        logger.info("{} started.", pipeline.getName());
    }

    private void hookUpBus() {
        logger.debug("Starting to hookup GStreamer Pipeline bus. ");
        // Hook up the shutdown handlers
        Bus bus = pipeline.getBus();
        bus.connect(new Bus.EOS() {
            /**
             * {@inheritDoc}
             * 
             * @see org.gstreamer.Bus.EOS#endOfStream(org.gstreamer.GstObject)
             */
            public void endOfStream(GstObject arg0) {
                logger.debug("Pipeline received EOS.");
                pipeline.setState(State.NULL);
                pipeline = null;
            }
        });
        bus.connect(new Bus.ERROR() {
            /**
             * {@inheritDoc}
             * 
             * @see org.gstreamer.Bus.ERROR#errorMessage(org.gstreamer.GstObject, int, java.lang.String)
             */
            public void errorMessage(GstObject obj, int retCode, String msg) {
                logger.warn("{}: {}", obj.getName(), msg);
            }
        });
        bus.connect(new Bus.WARNING() {
            /**
             * {@inheritDoc}
             * 
             * @see org.gstreamer.Bus.WARNING#warningMessage(org.gstreamer.GstObject, int, java.lang.String)
             */
            public void warningMessage(GstObject obj, int retCode, String msg) {
                logger.warn("{}: {}", obj.getName(), msg);
            }
        });
        logger.debug("Successfully hooked up GStreamer Pipeline bus to Log4J.");
    }

    /**
     * This method waits until the pipeline has had an opportunity to shutdown and if it surpasses the maximum timeout
     * value it will be manually stopped.
     */
    public void stop(long timeout) {
        // We must stop the capture as soon as possible, then check whatever needed
        for (CaptureDeviceBin captureDeviceBin : captureDeviceBins) {
            captureDeviceBin.shutdown();
        }

        long startWait = System.currentTimeMillis();

        while (pipeline != null && (pipeline.getState() != State.PAUSED || pipeline.getState() != State.NULL)) {
            try {
                Thread.sleep(WAIT_FOR_NULL_SLEEP_TIME);
            } catch (InterruptedException e) {
            }
            // If we've timed out then force kill the pipeline
            if (System.currentTimeMillis() - startWait >= timeout) {
                if (pipeline != null) {
                    logger.debug("The pipeline took too long to shut down, now sending State.NULL.");
                    pipeline.setState(State.NULL);
                }
                pipeline = null;
            }
        }

        if (pipeline != null) {
            pipeline.setState(State.NULL);
        }
        pipeline = null;
    }

    public boolean isPipelineNull() {
        return pipeline == null;
    }

    /**
     * Create a bin that contains multiple pipelines using each source in the properties object as the gstreamer source
     * 
     * @param props
     *          {@code Properties} object defining sources
     * @return The {@code Pipeline} to control the pipelines
     * @throws Exception
     * @throws UnsupportedDeviceException
     */
    public Pipeline create(Properties props, boolean confidence) {
        properties = props;
        ArrayList<CaptureDevice> devices = new ArrayList<CaptureDevice>();

        String[] friendlyNames;
        try {
            friendlyNames = getDeviceNames(props);
        } catch (InvalidCaptureDevicesSpecifiedException e) {
            logger.error(e.getMessage());
            return null;
        }

        String outputDirectory = properties.getProperty(CaptureParameters.RECORDING_ROOT_URL);

        devices = initDevices(friendlyNames, outputDirectory, confidence);
        if (devices == null) {
            // This odd case will be logged why in initDevices.
            return null;
        }

        return startPipeline(devices, confidence);
    }

    /**
     * Splits the device names from the pipeline's properties.
     * 
     * @return The device names to capture from.
     * @throws InvalidCaptureDevicesSpecifiedException
     *           - If there are no capture devices specified in the configuration file we throw an exception.
     */
    public static String[] getDeviceNames(Properties props) throws InvalidCaptureDevicesSpecifiedException {
        // Setup pipeline for all the devices specified
        String deviceNames = props.getProperty(CaptureParameters.CAPTURE_DEVICE_NAMES);
        if (deviceNames == null) {
            throw new InvalidCaptureDevicesSpecifiedException(
                    "No capture devices specified in " + CaptureParameters.CAPTURE_DEVICE_NAMES);
        }

        // Sanity checks for the device list
        String[] friendlyNames = deviceNames.split(",");
        if (friendlyNames.length < 1) {
            throw new InvalidCaptureDevicesSpecifiedException(
                    "Insufficient number of capture devices listed.  Aborting!");
        } else if (friendlyNames.length == 1) {
            // Java gives us an array even if the string being split is blank...
            if (StringUtils.isBlank(friendlyNames[0])) {
                throw new InvalidCaptureDevicesSpecifiedException(
                        "Invalid capture device listed in \"" + deviceNames + "\".  Aborting!");
            }
        }

        return friendlyNames;
    }

    /**
     * Returns an {@code ArrayList} of {@code CaptureDevice}s which contain everything the rest of this class needs to
     * start the pipeline
     * 
     * @param friendlyNames
     *          The list of friendly names we will be capturing from.
     * @param outputDirectory
     *          The destination directory of the captures.
     * @param confidence
     *          True to enable confidence monitoring, false otherwise.
     * @return A list of {@code CaptureDevice}s which can be captured from.
     * @throws InvalidDeviceNameException
     *           The device name specified in the list of devices could not be found in the properties list.
     * @throws UnrecognizedDeviceException
     *           JV4L could fail top recognise the device after the type was not specified.
     * @throws UnableToCreateSampleOutputFileException
     *           Failure while trying to create a test capture file.
     */
    protected static ArrayList<CaptureDevice> initDevices(String[] friendlyNames, String outputDirectory,
            boolean confidence) {
        ArrayList<CaptureDevice> devices = new ArrayList<CaptureDevice>();
        for (String name : friendlyNames) {
            String deviceName = null;
            try {
                deviceName = createDevice(outputDirectory, confidence, devices, name);
            } catch (CannotFindSourceFileOrDeviceException e) {
                logger.error("Device " + name + " can't find source file or device: ", e);
            } catch (InvalidDeviceNameException e) {
                logger.error("Device " + name + " has invalid device name: " + deviceName, e);
            } catch (UnableToCreateSampleOutputFileException e) {
                logger.error("Device " + name + " unable to create sample output file " + outputDirectory, e);
            } catch (UnrecognizedDeviceException e) {
                logger.error("Device " + name + " is an unrecognized device ", e);
            }
        }

        return devices;
    }

    private static String createDevice(String outputDirectory, boolean confidence, ArrayList<CaptureDevice> devices,
            String name) throws InvalidDeviceNameException, UnableToCreateSampleOutputFileException,
            UnrecognizedDeviceException, CannotFindSourceFileOrDeviceException {
        name = name.trim();
        ProducerType devName;

        // Get properties from the configuration
        String srcProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_SOURCE;
        String outputProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_DEST;
        String typeProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_TYPE;

        if (outputDirectory == null && !confidence) {
            logger.warn(
                    "Output directory is null, this may not work because we may not be able to write to the current "
                            + "output dir!");
        }
        if (!properties.containsKey(outputProperty)) {
            throw new InvalidDeviceNameException("Invalid device name: " + name + ".  No keys named "
                    + CaptureParameters.CAPTURE_DEVICE_PREFIX + name + " exist in the properties!");
        }
        String srcLoc = properties.getProperty(srcProperty);
        File outputFile = new File(outputDirectory, properties.getProperty(outputProperty));

        logger.debug("Device {} has source at {}.", name, srcLoc);
        logger.debug("Device {} has output at {}.", name, outputFile);

        String type = properties.getProperty(typeProperty);
        logger.debug("Device {} has type {}.", name, type);

        // Only try and create an output file if this pipeline will *not* be used for confidence monitoring
        if (!confidence) {
            try {
                if (!outputFile.createNewFile()) {
                    throw new UnableToCreateSampleOutputFileException(
                            "Could not create ouput file for " + outputFile.getAbsolutePath()
                                    + " because we cannot create a new file possibly because it already exists.");
                }
            } catch (IOException e) {
                throw new UnableToCreateSampleOutputFileException(
                        "An error occured while creating output file for " + name + ". " + e.getMessage());
            }
        }
        String outputLoc = outputFile.getAbsolutePath();

        if (type != null) {
            devName = ProducerType.valueOf(type);
            logger.debug("Device {} has been confirmed to be type {}", name, devName.toString());
            /** For certain devices we need to check to make sure that the src is specified, others are exempt. **/
            if (ProducerFactory.getInstance().requiresSrc(devName)) {
                checkSrcLocationExists(name, srcLoc);
            }
        } else {
            logger.debug("Device {} has no type so we will determine it's type.", name);
            checkSrcLocationExists(name, srcLoc);
            if (new File(srcLoc).isFile()) {
                // Non-V4L file. If it exists, assume it is ingestable
                // TODO: Fix security risk. Any file on CaptureAgent filesytem could be ingested
                devName = ProducerType.FILE;
                logger.debug("Device {} is a File device.", name);
            } else {
                devName = determineSourceFromJ4VLInfo(srcLoc);
            }
        }
        // devices will store the CaptureDevice list arbitrary order
        CaptureDevice capdev = createCaptureDevice(srcLoc, devName, name, outputLoc);
        if (!devices.add(capdev)) {
            logger.error("Unable to add device: {}.", capdev);
        }
        return name;
    }

    /**
     * Double checks that the source location for capture is set, otherwise throws an exception. 
     * @param name Friendly name of the capture device. 
     * @param srcLoc The source location that should be set to something. 
     * @throws CannotFindSourceFileOrDeviceException Thrown if the source location is null or the empty string. 
     */
    static void checkSrcLocationExists(String name, String srcLoc) throws CannotFindSourceFileOrDeviceException {
        if (srcLoc == null || ("").equals(srcLoc)) {
            throw new CannotFindSourceFileOrDeviceException(
                    "Unable to create pipeline for " + name + " because its source file/device does not exist!");
        }
    }

    /** This is legacy code that determines a Producer from the JV4L library. It really only supports the reference
     * capture devices. **/
    static ProducerType determineSourceFromJ4VLInfo(String srcLoc) throws UnrecognizedDeviceException {
        // ALSA source
        if (srcLoc.contains("hw:")) {
            return ProducerType.ALSASRC;
        } else if ("dv1394".equals(srcLoc)) {
            return ProducerType.DV_1394;
        } else { // V4L devices
            // Attempt to determine what the device is using the JV4LInfo library
            try {
                V4LInfo v4linfo = JV4LInfo.getV4LInfo(srcLoc);
                String deviceString = v4linfo.toString();
                if (deviceString.contains("Hauppauge") || deviceString.contains("WinTV")) {
                    return ProducerType.HAUPPAUGE_WINTV;
                } else if (deviceString.contains("BT878")) {
                    return ProducerType.BLUECHERRY_PROVIDEO;
                } else {
                    throw new UnrecognizedDeviceException("Do not recognized device: " + srcLoc);

                }
            } catch (JV4LInfoException e) {
                // The v4l device caused an exception
                throw new UnrecognizedDeviceException(
                        "Unexpected jv4linfo exception: " + e.getMessage() + " for " + srcLoc);
            }
        }
    }

    /**
     * Initializes the pipeline itself, but does not start capturing
     * 
     * @param devices
     *          The list of devices to capture from.
     * @param confidence
     *          True to enable confidence monitoring.
     * @return The created {@code Pipeline}, or null in the case of an error.
     */
    private Pipeline startPipeline(ArrayList<CaptureDevice> devices, boolean confidence) {
        logger.info("Successfully initialised {} devices.", devices.size());
        for (int i = 0; i < devices.size(); i++)
            logger.debug("Device #{}: {}.", i, devices.get(i));

        // setup gstreamer pipeline using capture devices
        Gst.init(); // cannot using gst library without first initialising it

        Pipeline pipeline = new Pipeline();
        for (CaptureDevice c : devices) {
            if (!addCaptureDeviceBinsToPipeline(c, pipeline))
                logger.error("Failed to create pipeline for {}.", c);
        }

        pipeline.debugToDotFile(Pipeline.DEBUG_GRAPH_SHOW_ALL, pipeline.getName());
        return pipeline;
    }

    /** Creates a CaptureDevice used mostly for testing purposes from its important components. 
     * @param srcLoc Where the capture device will capture from e.g. /dev/video0.
     * @param devName The ProducerType of the capture device such as V4L2SRC.
     * @param name The unique name of the capture device.
     * @param outputLoc The output name of the capture media. **/
    public static CaptureDevice createCaptureDevice(String srcLoc, ProducerType devName, String name,
            String outputLoc) {
        CaptureDevice capdev = new CaptureDevice(srcLoc, devName, name, outputLoc);
        String codecProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_CODEC;
        String containerProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_CONTAINER;
        String bitrateProperty = codecProperty + CaptureParameters.CAPTURE_DEVICE_BITRATE;
        String quantizerProperty = codecProperty + CaptureParameters.CAPTURE_DEVICE_QUANTIZER;
        String bufferProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_BUFFER;
        String bufferCountProperty = bufferProperty + CaptureParameters.CAPTURE_DEVICE_BUFFER_MAX_BUFFERS;
        String bufferByteProperty = bufferProperty + CaptureParameters.CAPTURE_DEVICE_BUFFER_MAX_BYTES;
        String bufferTimeProperty = bufferProperty + CaptureParameters.CAPTURE_DEVICE_BUFFER_MAX_TIME;
        String framerateProperty = CaptureParameters.CAPTURE_DEVICE_PREFIX + name
                + CaptureParameters.CAPTURE_DEVICE_FRAMERATE;
        String codec = properties.getProperty(codecProperty);
        String container = properties.getProperty(containerProperty);
        String bitrate = properties.getProperty(bitrateProperty);
        String quantizer = properties.getProperty(quantizerProperty);
        String bufferCount = properties.getProperty(bufferCountProperty);
        String bufferBytes = properties.getProperty(bufferByteProperty);
        String bufferTime = properties.getProperty(bufferTimeProperty);
        String framerate = properties.getProperty(framerateProperty);

        if (codec != null)
            capdev.getProperties().setProperty("codec", codec);
        if (bitrate != null)
            capdev.getProperties().setProperty("bitrate", bitrate);
        if (quantizer != null)
            capdev.getProperties().setProperty("quantizer", quantizer);
        if (container != null)
            capdev.getProperties().setProperty("container", container);
        if (bufferCount != null)
            capdev.getProperties().setProperty("bufferCount", bufferCount);
        if (bufferBytes != null)
            capdev.getProperties().setProperty("bufferBytes", bufferBytes);
        if (bufferTime != null)
            capdev.getProperties().setProperty("bufferTime", bufferTime);
        if (framerate != null)
            capdev.getProperties().setProperty("framerate", framerate);

        return capdev;
    }

    /**
     * addPipeline will add a pipeline for the specified capture device to the bin.
     * 
     * @param captureDevice
     *          {@code CaptureDevice} to create pipeline around
     * @param pipeline
     *          The {@code Pipeline} bin to add it to
     * @return True, if successful
     */
    protected boolean addCaptureDeviceBinsToPipeline(CaptureDevice captureDevice, Pipeline pipeline) {

        CaptureDeviceBin captureDeviceBin = null;
        try {
            captureDeviceBin = new CaptureDeviceBin(captureDevice, properties);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        pipeline.add(captureDeviceBin.getBin());
        // Add them to a list so that we can send EOS's to their source Elements.
        captureDeviceBins.add(captureDeviceBin);
        return true;
    }
}