org.opencastproject.workflow.handler.videoeditor.VideoEditorWorkflowOperationHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.workflow.handler.videoeditor.VideoEditorWorkflowOperationHandler.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.workflow.handler.videoeditor;

import org.opencastproject.job.api.Job;
import org.opencastproject.job.api.JobContext;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementBuilder;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.mediapackage.selector.SimpleElementSelector;
import org.opencastproject.mediapackage.selector.TrackSelector;
import org.opencastproject.smil.api.SmilException;
import org.opencastproject.smil.api.SmilResponse;
import org.opencastproject.smil.api.SmilService;
import org.opencastproject.smil.entity.api.Smil;
import org.opencastproject.smil.entity.media.api.SmilMediaObject;
import org.opencastproject.smil.entity.media.container.api.SmilMediaContainer;
import org.opencastproject.smil.entity.media.element.api.SmilMediaElement;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.videoeditor.api.ProcessFailedException;
import org.opencastproject.videoeditor.api.VideoEditorService;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowOperationResult;
import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
import org.opencastproject.workflow.handler.workflow.ResumableWorkflowOperationHandlerBase;
import org.opencastproject.workspace.api.Workspace;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

public class VideoEditorWorkflowOperationHandler extends ResumableWorkflowOperationHandlerBase {

    private static final Logger logger = LoggerFactory.getLogger(VideoEditorWorkflowOperationHandler.class);

    /** Path to the hold ui resources */
    private static final String HOLD_UI_PATH = "/ui/operation/editor/index.html";

    /** Name of the configuration option that provides the source flavors we use for processing. */
    private static final String SOURCE_FLAVORS_PROPERTY = "source-flavors";

    /** Name of the configuration option that provides the preview flavors we use as preview. */
    private static final String PREVIEW_FLAVORS_PROPERTY = "preview-flavors";

    /** Name of the configuration option that provides the source flavors on skipped videoeditor operation. */
    private static final String SKIPPED_FLAVORS_PROPERTY = "skipped-flavors";

    /** Name of the configuration option that provides the smil flavor as input. */
    private static final String SMIL_FLAVORS_PROPERTY = "smil-flavors";

    /** Name of the configuration option that provides the smil flavor as input. */
    private static final String TARGET_SMIL_FLAVOR_PROPERTY = "target-smil-flavor";

    /** Name of the configuration that provides the target flavor subtype for encoded media tracks. */
    private static final String TARGET_FLAVOR_SUBTYPE_PROPERTY = "target-flavor-subtype";

    /** Name of the configuration that provides the interactive flag */
    private static final String INTERACTIVE_PROPERTY = "interactive";

    /** Name of the configuration that provides the smil file name */
    private static final String SMIL_FILE_NAME = "smil.smil";

    /**
     * The configuration options for this handler
     */
    private static final SortedMap<String, String> CONFIG_OPTIONS;

    static {
        CONFIG_OPTIONS = new TreeMap<String, String>();
        CONFIG_OPTIONS.put(SOURCE_FLAVORS_PROPERTY, "The flavor for working files (tracks to edit).");
        CONFIG_OPTIONS.put(PREVIEW_FLAVORS_PROPERTY, "The flavor for preview files (tracks to show in edit UI).");
        CONFIG_OPTIONS.put(SKIPPED_FLAVORS_PROPERTY,
                "The flavor for working files if videoeditor operation is disabled."
                        + " This is an optional option." + " Default value is given by \"" + SOURCE_FLAVORS_PROPERTY
                        + "\".");
        CONFIG_OPTIONS.put(SMIL_FLAVORS_PROPERTY, "The flavor for input smil files.");
        CONFIG_OPTIONS.put(TARGET_SMIL_FLAVOR_PROPERTY, "The flavor for target smil file.");
        CONFIG_OPTIONS.put(TARGET_FLAVOR_SUBTYPE_PROPERTY, "The flavor subtype for target media files.");
        CONFIG_OPTIONS.put(INTERACTIVE_PROPERTY, "Whether the operation is interactive or not");
    }

    /**
     * The Smil service to modify smil files.
     */
    private SmilService smilService;
    /**
     * The VideoEditor service to edit files.
     */
    private VideoEditorService videoEditorService;
    /**
     * The workspace.
     */
    private Workspace workspace;

    @Override
    public void activate(ComponentContext cc) {
        super.activate(cc);
        setHoldActionTitle("Review / VideoEdit");
        registerHoldStateUserInterface(HOLD_UI_PATH);
        logger.info("Registering videoEditor hold state ui from classpath {}", HOLD_UI_PATH);
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.workflow.api.WorkflowOperationHandler#getConfigurationOptions()
     */
    @Override
    public SortedMap<String, String> getConfigurationOptions() {
        return CONFIG_OPTIONS;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(org.opencastproject.workflow.api.WorkflowInstance,
     *      JobContext)
     */
    @Override
    public WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context)
            throws WorkflowOperationException {

        MediaPackage mp = workflowInstance.getMediaPackage();
        logger.info("Start editor workflow for mediapackage {}", mp.getIdentifier().compact());

        // get configuration
        WorkflowOperationInstance worflowOperationInstance = workflowInstance.getCurrentOperation();
        String smilFlavorsProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(SMIL_FLAVORS_PROPERTY));
        if (smilFlavorsProperty == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set", SMIL_FLAVORS_PROPERTY));
        }
        String targetSmilFlavorProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(TARGET_SMIL_FLAVOR_PROPERTY));
        if (targetSmilFlavorProperty == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set", TARGET_SMIL_FLAVOR_PROPERTY));
        }
        String previewTrackFlavorsProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(PREVIEW_FLAVORS_PROPERTY));
        if (previewTrackFlavorsProperty == null) {
            logger.info("Configuration property '{}' not set, use preview tracks from smil catalog",
                    PREVIEW_FLAVORS_PROPERTY);
        }

        if (StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(TARGET_FLAVOR_SUBTYPE_PROPERTY)) == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set", TARGET_FLAVOR_SUBTYPE_PROPERTY));
        }

        final boolean interactive = BooleanUtils
                .toBoolean(worflowOperationInstance.getConfiguration(INTERACTIVE_PROPERTY));

        // check at least one smil catalog exists
        SimpleElementSelector elementSelector = new SimpleElementSelector();
        for (String flavor : asList(smilFlavorsProperty)) {
            elementSelector.addFlavor(flavor);
        }
        Collection<MediaPackageElement> smilCatalogs = elementSelector.select(mp, false);
        MediaPackageElementBuilder mpeBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();

        if (smilCatalogs.isEmpty()) {

            // There is nothing to do, skip the operation
            if (!interactive) {
                logger.info("Skipping cutting opertion since no edit decision list is available");
                return skip(workflowInstance, context);
            }

            // Without SMIL catalogs and without preview tracks, there is nothing we can do
            if (previewTrackFlavorsProperty == null) {
                throw new WorkflowOperationException(String.format(
                        "No smil catalogs with flavor %s nor preview files with flavor %s found in mediapackage %s",
                        smilFlavorsProperty, previewTrackFlavorsProperty, mp.getIdentifier().compact()));
            }

            // Basd on the preview trcks, create new and empty SMIL catalog
            TrackSelector trackSelector = new TrackSelector();
            for (String flavor : asList(previewTrackFlavorsProperty)) {
                trackSelector.addFlavor(flavor);
            }
            Collection<Track> previewTracks = trackSelector.select(mp, false);
            if (previewTracks.isEmpty()) {
                throw new WorkflowOperationException(
                        String.format("No preview tracks found in mediapackage %s with flavor %s",
                                mp.getIdentifier().compact(), previewTrackFlavorsProperty));
            }
            Track[] previewTracksArr = previewTracks.toArray(new Track[previewTracks.size()]);
            MediaPackageElementFlavor smilFlavor = MediaPackageElementFlavor.parseFlavor(smilFlavorsProperty);

            for (Track previewTrack : previewTracks) {
                try {
                    SmilResponse smilResponse = smilService.createNewSmil(mp);
                    smilResponse = smilService.addParallel(smilResponse.getSmil());
                    smilResponse = smilService.addClips(smilResponse.getSmil(), smilResponse.getEntity().getId(),
                            previewTracksArr, 0L, previewTracksArr[0].getDuration());
                    Smil smil = smilResponse.getSmil();

                    InputStream is = null;
                    try {
                        // put new smil into workspace
                        is = IOUtils.toInputStream(smil.toXML(), "UTF-8");
                        URI smilURI = workspace.put(mp.getIdentifier().compact(), smil.getId(), SMIL_FILE_NAME, is);
                        MediaPackageElementFlavor trackSmilFlavor = previewTrack.getFlavor();
                        if (!"*".equals(smilFlavor.getType())) {
                            trackSmilFlavor = new MediaPackageElementFlavor(smilFlavor.getType(),
                                    trackSmilFlavor.getSubtype());
                        }
                        if (!"*".equals(smilFlavor.getSubtype())) {
                            trackSmilFlavor = new MediaPackageElementFlavor(trackSmilFlavor.getType(),
                                    smilFlavor.getSubtype());
                        }
                        Catalog catalog = (Catalog) mpeBuilder.elementFromURI(smilURI,
                                MediaPackageElement.Type.Catalog, trackSmilFlavor);
                        catalog.setIdentifier(smil.getId());
                        mp.add(catalog);
                    } finally {
                        IOUtils.closeQuietly(is);
                    }
                } catch (Exception ex) {
                    throw new WorkflowOperationException(String.format(
                            "Failed to create smil catalog for mediapackage %s", mp.getIdentifier().compact()), ex);
                }
            }
        }

        // check target smil catalog exists
        MediaPackageElementFlavor targetSmilFlavor = MediaPackageElementFlavor
                .parseFlavor(targetSmilFlavorProperty);
        Catalog[] targetSmilCatalogs = mp.getCatalogs(targetSmilFlavor);
        if (targetSmilCatalogs == null || targetSmilCatalogs.length == 0) {

            if (!interactive)
                return skip(workflowInstance, context);

            // create new empty smil to fill it from editor UI
            try {
                SmilResponse smilResponse = smilService.createNewSmil(mp);
                Smil smil = smilResponse.getSmil();

                InputStream is = null;
                try {
                    // put new smil into workspace
                    is = IOUtils.toInputStream(smil.toXML(), "UTF-8");
                    URI smilURI = workspace.put(mp.getIdentifier().compact(), smil.getId(), SMIL_FILE_NAME, is);
                    Catalog catalog = (Catalog) mpeBuilder.elementFromURI(smilURI, MediaPackageElement.Type.Catalog,
                            targetSmilFlavor);
                    catalog.setIdentifier(smil.getId());
                    mp.add(catalog);
                } finally {
                    IOUtils.closeQuietly(is);
                }
            } catch (Exception ex) {
                throw new WorkflowOperationException(
                        String.format("Failed to create an initial empty smil catalog for mediapackage %s",
                                mp.getIdentifier().compact()),
                        ex);
            }

            logger.info("Holding for video edit...");
            return createResult(mp, Action.PAUSE);
        } else {
            logger.debug("Move on, SMIL catalog ({}) already exists for media package '{}'", targetSmilFlavor, mp);
            return resume(workflowInstance, context, Collections.<String, String>emptyMap());
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.workflow.api.AbstractWorkflowOperationHandler#skip(org.opencastproject.workflow.api.WorkflowInstance,
     *      JobContext)
     */
    @Override
    public WorkflowOperationResult skip(WorkflowInstance workflowInstance, JobContext context)
            throws WorkflowOperationException {
        // If we do not hold for trim, we still need to put tracks in the mediapackage with the target flavor
        MediaPackage mp = workflowInstance.getMediaPackage();
        logger.info("Skip video editor operation for mediapackage {}", mp.getIdentifier().compact());

        // get configuration
        WorkflowOperationInstance worflowOperationInstance = workflowInstance.getCurrentOperation();
        String sourceTrackFlavorsProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(SKIPPED_FLAVORS_PROPERTY));
        if (sourceTrackFlavorsProperty == null || sourceTrackFlavorsProperty.isEmpty()) {
            logger.info("\"{}\" option not set, use value of \"{}\"", SKIPPED_FLAVORS_PROPERTY,
                    SOURCE_FLAVORS_PROPERTY);
            sourceTrackFlavorsProperty = StringUtils
                    .trimToNull(worflowOperationInstance.getConfiguration(SOURCE_FLAVORS_PROPERTY));
            if (sourceTrackFlavorsProperty == null) {
                throw new WorkflowOperationException(
                        String.format("Required configuration property %s not set.", SOURCE_FLAVORS_PROPERTY));
            }
        }
        String targetFlavorSubTypeProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(TARGET_FLAVOR_SUBTYPE_PROPERTY));
        if (targetFlavorSubTypeProperty == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set.", TARGET_FLAVOR_SUBTYPE_PROPERTY));
        }

        // get source tracks
        TrackSelector trackSelector = new TrackSelector();
        for (String flavor : asList(sourceTrackFlavorsProperty)) {
            trackSelector.addFlavor(flavor);
        }
        Collection<Track> sourceTracks = trackSelector.select(mp, false);

        for (Track sourceTrack : sourceTracks) {
            // set target track flavor
            Track clonedTrack = (Track) sourceTrack.clone();
            clonedTrack.setIdentifier(null);
            clonedTrack.setURI(sourceTrack.getURI()); // use the same URI as the original
            clonedTrack.setFlavor(
                    new MediaPackageElementFlavor(sourceTrack.getFlavor().getType(), targetFlavorSubTypeProperty));
            mp.addDerived(clonedTrack, sourceTrack);
        }

        return createResult(mp, Action.SKIP);
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.workflow.api.ResumableWorkflowOperationHandler#resume(org.opencastproject.workflow.api.WorkflowInstance,
     *      JobContext, java.util.Map)
     */
    @Override
    public WorkflowOperationResult resume(WorkflowInstance workflowInstance, JobContext context,
            Map<String, String> properties) throws WorkflowOperationException {

        MediaPackage mp = workflowInstance.getMediaPackage();
        logger.info("Resume video editor operation for mediapackage {}", mp.getIdentifier().compact());

        // get configuration
        WorkflowOperationInstance worflowOperationInstance = workflowInstance.getCurrentOperation();
        String sourceTrackFlavorsProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(SOURCE_FLAVORS_PROPERTY));
        if (sourceTrackFlavorsProperty == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set.", SOURCE_FLAVORS_PROPERTY));
        }
        String targetSmilFlavorProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(TARGET_SMIL_FLAVOR_PROPERTY));
        if (targetSmilFlavorProperty == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set.", TARGET_SMIL_FLAVOR_PROPERTY));
        }
        String targetFlavorSybTypeProperty = StringUtils
                .trimToNull(worflowOperationInstance.getConfiguration(TARGET_FLAVOR_SUBTYPE_PROPERTY));
        if (targetFlavorSybTypeProperty == null) {
            throw new WorkflowOperationException(
                    String.format("Required configuration property %s not set.", TARGET_FLAVOR_SUBTYPE_PROPERTY));
        }

        // get source tracks
        TrackSelector trackSelector = new TrackSelector();
        for (String flavor : asList(sourceTrackFlavorsProperty)) {
            trackSelector.addFlavor(flavor);
        }
        Collection<Track> sourceTracks = trackSelector.select(mp, false);
        if (sourceTracks.isEmpty()) {
            throw new WorkflowOperationException(
                    String.format("No source tracks found in mediapacksge %s with flavors %s.",
                            mp.getIdentifier().compact(), sourceTrackFlavorsProperty));
        }

        // get smil file
        MediaPackageElementFlavor smilTargetFlavor = MediaPackageElementFlavor
                .parseFlavor(targetSmilFlavorProperty);
        Catalog[] smilCatalogs = mp.getCatalogs(smilTargetFlavor);
        if (smilCatalogs == null || smilCatalogs.length == 0) {
            throw new WorkflowOperationException(
                    String.format("No smil catalog found in mediapackage %s with flavor %s.",
                            mp.getIdentifier().compact(), targetSmilFlavorProperty));
        }

        File smilFile = null;
        Smil smil = null;
        try {
            smilFile = workspace.get(smilCatalogs[0].getURI());
            smil = smilService.fromXml(smilFile).getSmil();
            smil = replaceAllTracksWith(smil, sourceTracks.toArray(new Track[sourceTracks.size()]));

            InputStream is = null;
            try {
                is = IOUtils.toInputStream(smil.toXML());
                // remove old smil
                workspace.delete(mp.getIdentifier().compact(), smilCatalogs[0].getIdentifier());
                mp.remove(smilCatalogs[0]);
                // put modified smil into workspace
                URI newSmilUri = workspace.put(mp.getIdentifier().compact(), smil.getId(), SMIL_FILE_NAME, is);
                Catalog catalog = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
                        .elementFromURI(newSmilUri, MediaPackageElement.Type.Catalog, smilCatalogs[0].getFlavor());
                catalog.setIdentifier(smil.getId());
                mp.add(catalog);
            } catch (Exception ex) {
                throw new WorkflowOperationException(ex);
            } finally {
                IOUtils.closeQuietly(is);
            }

        } catch (NotFoundException ex) {
            throw new WorkflowOperationException(
                    String.format("Failed to get smil catalog %s from mediapackage %s.",
                            smilCatalogs[0].getIdentifier(), mp.getIdentifier().compact()),
                    ex);
        } catch (IOException ex) {
            throw new WorkflowOperationException(String.format("Can't open smil catalog %s from mediapackage %s.",
                    smilCatalogs[0].getIdentifier(), mp.getIdentifier().compact()), ex);
        } catch (SmilException ex) {
            throw new WorkflowOperationException(ex);
        }

        // create video edit jobs and run them
        List<Job> jobs = null;
        try {
            logger.info("Create processing jobs for smil {}.", smilCatalogs[0].getIdentifier());
            jobs = videoEditorService.processSmil(smil);
            if (!waitForStatus(jobs.toArray(new Job[jobs.size()])).isSuccess()) {
                throw new WorkflowOperationException("Smil processing jobs for smil "
                        + smilCatalogs[0].getIdentifier() + " are ended unsuccessfull.");
            }
            logger.info("Smil " + smilCatalogs[0].getIdentifier() + " processing finished.");
        } catch (ProcessFailedException ex) {
            throw new WorkflowOperationException("Processing smil " + smilCatalogs[0].getIdentifier() + " failed",
                    ex);
        }

        // move edited tracks to work location and set target flavor
        Track editedTrack = null;
        boolean mpAdded = false;
        for (Job job : jobs) {
            try {
                editedTrack = (Track) MediaPackageElementParser.getFromXml(job.getPayload());
                MediaPackageElementFlavor editedTrackFlavor = editedTrack.getFlavor();
                editedTrack.setFlavor(
                        new MediaPackageElementFlavor(editedTrackFlavor.getType(), targetFlavorSybTypeProperty));
                URI editedTrackNewUri = workspace.moveTo(editedTrack.getURI(), mp.getIdentifier().compact(),
                        editedTrack.getIdentifier(), FilenameUtils.getName(editedTrack.getURI().toString()));
                editedTrack.setURI(editedTrackNewUri);
                for (Track track : sourceTracks) {
                    if (track.getFlavor().getType().equals(editedTrackFlavor.getType())) {
                        mp.addDerived(editedTrack, track);
                        mpAdded = true;
                        break;
                    }
                }

                if (!mpAdded) {
                    mp.add(editedTrack);
                }

            } catch (MediaPackageException ex) {
                throw new WorkflowOperationException("Failed to get edited track information.", ex);
            } catch (Exception ex) {
                if (ex instanceof NotFoundException || ex instanceof IOException
                        || ex instanceof IllegalArgumentException) {
                    throw new WorkflowOperationException("Moving edited track to work location failed.", ex);
                } else {
                    throw new WorkflowOperationException(ex);
                }
            }
        }

        logger.info("VideoEdit workflow {} finished", workflowInstance.getId());

        return createResult(mp, Action.CONTINUE);

    }

    protected Smil replaceAllTracksWith(Smil smil, Track[] otherTracks) throws SmilException {
        SmilResponse smilResponse;
        try {
            // copy smil to work with
            smilResponse = smilService.fromXml(smil.toXML());
        } catch (Exception ex) {
            throw new SmilException("Can't parse smil.");
        }

        long start;
        long end;
        // iterate over all elements inside smil body
        for (SmilMediaObject elem : smil.getBody().getMediaElements()) {
            start = -1L;
            end = -1L;
            // body should contain par elements (container)
            if (elem.isContainer()) {
                // iterate over all elements in container
                for (SmilMediaObject child : ((SmilMediaContainer) elem).getElements()) {
                    // second depth should contain media elements like audio or video
                    if (!child.isContainer() && child instanceof SmilMediaElement) {
                        SmilMediaElement media = (SmilMediaElement) child;
                        start = media.getClipBeginMS();
                        end = media.getClipEndMS();
                        // remove it
                        smilResponse = smilService.removeSmilElement(smilResponse.getSmil(), media.getId());
                    }
                }
                if (start != -1L && end != -1L) {
                    // add the new tracks inside
                    smilResponse = smilService.addClips(smilResponse.getSmil(), elem.getId(), otherTracks, start,
                            end - start);
                }
            } else if (elem instanceof SmilMediaElement) {
                throw new SmilException("Media elements inside smil body are not supported yet.");
            }
        }
        return smilResponse.getSmil();
    }

    public void setSmilService(SmilService smilService) {
        this.smilService = smilService;
    }

    public void setVideoEditorService(VideoEditorService editor) {
        this.videoEditorService = editor;
    }

    public void setWorkspace(Workspace workspace) {
        this.workspace = workspace;
    }
}