org.opencastproject.videosegmenter.ffmpeg.VideoSegmenterServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.videosegmenter.ffmpeg.VideoSegmenterServiceImpl.java

Source

/**
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you 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://opensource.org/licenses/ecl2.txt
 *
 * 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.videosegmenter.ffmpeg;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.opencastproject.job.api.AbstractJobProducer;
import org.opencastproject.job.api.Job;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.metadata.mpeg7.MediaLocator;
import org.opencastproject.metadata.mpeg7.MediaLocatorImpl;
import org.opencastproject.metadata.mpeg7.MediaRelTimeImpl;
import org.opencastproject.metadata.mpeg7.MediaTime;
import org.opencastproject.metadata.mpeg7.Mpeg7Catalog;
import org.opencastproject.metadata.mpeg7.Mpeg7CatalogService;
import org.opencastproject.metadata.mpeg7.Segment;
import org.opencastproject.metadata.mpeg7.Video;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.videosegmenter.api.VideoSegmenterException;
import org.opencastproject.videosegmenter.api.VideoSegmenterService;
import org.opencastproject.workspace.api.Workspace;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.LineReader;

/**
 * Media analysis plugin that takes a video stream and extracts video segments
 * by trying to detect slide and/or scene changes.
 *
 * This plugin runs
 *
 * <pre>
 * ffmpeg -nostats -i in.mp4 -filter:v 'select=gt(scene\,0.04),showinfo' -f null - 2>&1 | grep Parsed_showinfo_1
 * </pre>
 */
public class VideoSegmenterServiceImpl extends AbstractJobProducer
        implements VideoSegmenterService, ManagedService {

    /** Resulting collection in the working file repository */
    public static final String COLLECTION_ID = "videosegments";

    /** List of available operations on jobs */
    private enum Operation {
        Segment
    };

    /** Path to the executable */
    protected String binary;

    public static final String FFMPEG_BINARY_CONFIG = "org.opencastproject.composer.ffmpeg.path";
    public static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";

    /** Name of the constant used to retreive the stability threshold */
    public static final String OPT_STABILITY_THRESHOLD = "stabilitythreshold";

    /** The number of seconds that need to resemble until a scene is considered "stable" */
    public static final int DEFAULT_STABILITY_THRESHOLD = 5;

    /** Name of the constant used to retreive the changes threshold */
    public static final String OPT_CHANGES_THRESHOLD = "changesthreshold";

    /** Default value for the number of pixels that may change between two frames without considering them different */
    public static final float DEFAULT_CHANGES_THRESHOLD = 0.05f; // 5% change

    /** The logging facility */
    protected static final Logger logger = LoggerFactory.getLogger(VideoSegmenterServiceImpl.class);

    /** Number of pixels that may change between two frames without considering them different */
    protected float changesThreshold = DEFAULT_CHANGES_THRESHOLD;

    /** The number of seconds that need to resemble until a scene is considered "stable" */
    protected int stabilityThreshold = DEFAULT_STABILITY_THRESHOLD;

    /** Reference to the receipt service */
    protected ServiceRegistry serviceRegistry = null;

    /** The mpeg-7 service */
    protected Mpeg7CatalogService mpeg7CatalogService = null;

    /** The workspace to ue when retrieving remote media files */
    protected Workspace workspace = null;

    /** The security service */
    protected SecurityService securityService = null;

    /** The user directory service */
    protected UserDirectoryService userDirectoryService = null;

    /** The organization directory service */
    protected OrganizationDirectoryService organizationDirectoryService = null;

    /**
     * Creates a new instance of the video segmenter service.
     */
    public VideoSegmenterServiceImpl() {
        super(JOB_TYPE);
        this.binary = FFMPEG_BINARY_DEFAULT;
    }

    public void activate(ComponentContext cc) {
        /* Configure segmenter */
        final String path = cc.getBundleContext().getProperty(FFMPEG_BINARY_CONFIG);
        this.binary = path == null ? FFMPEG_BINARY_DEFAULT : path;
        logger.debug("Configuration {}: {}", FFMPEG_BINARY_CONFIG, FFMPEG_BINARY_DEFAULT);
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary)
     */
    @SuppressWarnings("unchecked")
    @Override
    public void updated(Dictionary properties) throws ConfigurationException {
        if (properties == null) {
            return;
        }
        logger.debug("Configuring the videosegmenter");

        // Stability threshold
        if (properties.get(OPT_STABILITY_THRESHOLD) != null) {
            String threshold = (String) properties.get(OPT_STABILITY_THRESHOLD);
            try {
                stabilityThreshold = Integer.parseInt(threshold);
                logger.info("Stability threshold set to {} consecutive frames", stabilityThreshold);
            } catch (Exception e) {
                logger.warn("Found illegal value '{}' for videosegmenter's stability threshold", threshold);
            }
        }

        // Changes threshold
        if (properties.get(OPT_CHANGES_THRESHOLD) != null) {
            String threshold = (String) properties.get(OPT_CHANGES_THRESHOLD);
            try {
                changesThreshold = Float.parseFloat(threshold);
                logger.info("Changes threshold set to {}", changesThreshold);
            } catch (Exception e) {
                logger.warn("Found illegal value '{}' for videosegmenter's changes threshold", threshold);
            }
        }

    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.videosegmenter.api.VideoSegmenterService#segment(org.opencastproject.mediapackage.Track)
     */
    public Job segment(Track track) throws VideoSegmenterException, MediaPackageException {
        try {
            return serviceRegistry.createJob(JOB_TYPE, Operation.Segment.toString(),
                    Arrays.asList(MediaPackageElementParser.getAsXml(track)));
        } catch (ServiceRegistryException e) {
            throw new VideoSegmenterException("Unable to create a job", e);
        }
    }

    /**
     * Starts segmentation on the video track identified by
     * <code>mediapackageId</code> and <code>elementId</code> and returns a
     * receipt containing the final result in the form of anMpeg7Catalog.
     *
     * @param track
     *            the element to analyze
     * @return a receipt containing the resulting mpeg-7 catalog
     * @throws VideoSegmenterException
     */
    protected Catalog segment(Job job, Track track) throws VideoSegmenterException, MediaPackageException {

        // Make sure the element can be analyzed using this analysis
        // implementation
        if (!track.hasVideo()) {
            logger.warn("Element {} is not a video track", track);
            throw new VideoSegmenterException("Element is not a video track");
        }

        try {
            Mpeg7Catalog mpeg7 = mpeg7CatalogService.newInstance();

            File mediaFile = null;
            URL mediaUrl = null;
            try {
                mediaFile = workspace.get(track.getURI());
                mediaUrl = mediaFile.toURI().toURL();
            } catch (NotFoundException e) {
                throw new VideoSegmenterException("Error finding the video file in the workspace", e);
            } catch (IOException e) {
                throw new VideoSegmenterException("Error reading the video file in the workspace", e);
            }

            if (track.getDuration() == null)
                throw new MediaPackageException("Track " + track + " does not have a duration");
            logger.info("Track {} loaded, duration is {} s", mediaUrl, track.getDuration() / 1000);

            MediaTime contentTime = new MediaRelTimeImpl(0, track.getDuration());
            MediaLocator contentLocator = new MediaLocatorImpl(track.getURI());
            Video videoContent = mpeg7.addVideoContent("videosegment", contentTime, contentLocator);

            logger.info("Starting video segmentation of {}", mediaUrl);
            String[] command = new String[] { binary, "-nostats", "-i",
                    mediaFile.getAbsolutePath().replaceAll(" ", "\\ "), "-filter:v",
                    "select=gt(scene\\," + changesThreshold + "),showinfo", "-f", "null", "-" };
            String commandline = StringUtils.join(command, " ");

            logger.info("Running {}", commandline);

            ProcessBuilder pbuilder = new ProcessBuilder(command);
            List<String> segmentsStrings = new LinkedList<String>();
            Process process = pbuilder.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            try {
                LineReader lr = new LineReader(reader);
                String line = lr.readLine();
                while (null != line) {
                    if (line.startsWith("[Parsed_showinfo")) {
                        segmentsStrings.add(line);
                    }
                    line = lr.readLine();
                }
            } catch (IOException e) {
                logger.error("Error executing ffmpeg: {}", e.getMessage());
            } finally {
                reader.close();
            }

            // [Parsed_showinfo_1 @ 0x157fb40] n:0 pts:12 pts_time:12 pos:227495
            // fmt:rgb24 sar:0/1 s:320x240 i:P iskey:1 type:I checksum:8DF39EA9
            // plane_checksum:[8DF39EA9]

            int segmentcount = 1;
            List<Segment> segments = new LinkedList<Segment>();
            if (segmentsStrings.size() == 0) {
                Segment s = videoContent.getTemporalDecomposition().createSegment("segement-" + segmentcount);
                s.setMediaTime(new MediaRelTimeImpl(0, track.getDuration()));
                segments.add(s);
            } else {
                long starttime = 0;
                long endtime = 0;
                Pattern pattern = Pattern.compile("pts_time\\:\\d+");
                for (String seginfo : segmentsStrings) {
                    Matcher matcher = pattern.matcher(seginfo);
                    String time = "0";
                    while (matcher.find()) {
                        time = matcher.group().substring(9);
                    }
                    endtime = Long.parseLong(time) * 1000;
                    long segmentLength = endtime - starttime;
                    if (1000 * stabilityThreshold < segmentLength) {
                        Segment segement = videoContent.getTemporalDecomposition()
                                .createSegment("segement-" + segmentcount);
                        segement.setMediaTime(new MediaRelTimeImpl(starttime, endtime - starttime));
                        segments.add(segement);
                        segmentcount++;
                        starttime = endtime;
                    }
                }
                // Add last segment
                Segment s = videoContent.getTemporalDecomposition().createSegment("segement-" + segmentcount);
                s.setMediaTime(new MediaRelTimeImpl(endtime, track.getDuration() - endtime));
                segments.add(s);
            }

            logger.info("Segmentation of {} yields {} segments", mediaUrl, segments.size());

            Catalog mpeg7Catalog = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
                    .newElement(Catalog.TYPE, MediaPackageElements.SEGMENTS);
            URI uri;
            try {
                uri = workspace.putInCollection(COLLECTION_ID, job.getId() + ".xml",
                        mpeg7CatalogService.serialize(mpeg7));
            } catch (IOException e) {
                throw new VideoSegmenterException("Unable to put the mpeg7 catalog into the workspace", e);
            }
            mpeg7Catalog.setURI(uri);

            logger.info("Finished video segmentation of {}", mediaUrl);
            return mpeg7Catalog;
        } catch (Exception e) {
            logger.warn("Error segmenting " + track, e);
            if (e instanceof VideoSegmenterException) {
                throw (VideoSegmenterException) e;
            } else {
                throw new VideoSegmenterException(e);
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.job.api.AbstractJobProducer#process(org.opencastproject.job.api.Job)
     */
    @Override
    protected String process(Job job) throws Exception {
        Operation op = null;
        String operation = job.getOperation();
        List<String> arguments = job.getArguments();
        try {
            op = Operation.valueOf(operation);
            switch (op) {
            case Segment:
                Track track = (Track) MediaPackageElementParser.getFromXml(arguments.get(0));
                Catalog catalog = segment(job, track);
                return MediaPackageElementParser.getAsXml(catalog);
            default:
                throw new IllegalStateException("Don't know how to handle operation '" + operation + "'");
            }
        } catch (IllegalArgumentException e) {
            throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'", e);
        } catch (IndexOutOfBoundsException e) {
            throw new ServiceRegistryException(
                    "This argument list for operation '" + op + "' does not meet expectations", e);
        } catch (Exception e) {
            throw new ServiceRegistryException("Error handling operation '" + op + "'", e);
        }
    }

    /**
     * Sets the workspace
     *
     * @param workspace
     *            an instance of the workspace
     */
    protected void setWorkspace(Workspace workspace) {
        this.workspace = workspace;
    }

    /**
     * Sets the mpeg7CatalogService
     *
     * @param mpeg7CatalogService
     *            an instance of the mpeg7 catalog service
     */
    protected void setMpeg7CatalogService(Mpeg7CatalogService mpeg7CatalogService) {
        this.mpeg7CatalogService = mpeg7CatalogService;
    }

    /**
     * Sets the receipt service
     *
     * @param serviceRegistry
     *            the service registry
     */
    protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry()
     */
    @Override
    protected ServiceRegistry getServiceRegistry() {
        return serviceRegistry;
    }

    /**
     * Callback for setting the security service.
     *
     * @param securityService
     *            the securityService to set
     */
    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    /**
     * Callback for setting the user directory service.
     *
     * @param userDirectoryService
     *            the userDirectoryService to set
     */
    public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
        this.userDirectoryService = userDirectoryService;
    }

    /**
     * Sets a reference to the organization directory service.
     *
     * @param organizationDirectory
     *            the organization directory
     */
    public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
        this.organizationDirectoryService = organizationDirectory;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService()
     */
    @Override
    protected SecurityService getSecurityService() {
        return securityService;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService()
     */
    @Override
    protected UserDirectoryService getUserDirectoryService() {
        return userDirectoryService;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService()
     */
    @Override
    protected OrganizationDirectoryService getOrganizationDirectoryService() {
        return organizationDirectoryService;
    }

}