View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.videoeditor.impl;
23  
24  import static org.opencastproject.videoeditor.impl.VideoEditorProperties.SUBTITLE_GRACE_PERIOD;
25  
26  import org.opencastproject.inspection.api.MediaInspectionException;
27  import org.opencastproject.inspection.api.MediaInspectionService;
28  import org.opencastproject.job.api.AbstractJobProducer;
29  import org.opencastproject.job.api.Job;
30  import org.opencastproject.job.api.JobBarrier;
31  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
32  import org.opencastproject.mediapackage.MediaPackageElementParser;
33  import org.opencastproject.mediapackage.MediaPackageException;
34  import org.opencastproject.mediapackage.MediaPackageReference;
35  import org.opencastproject.mediapackage.MediaPackageReferenceImpl;
36  import org.opencastproject.mediapackage.Track;
37  import org.opencastproject.mediapackage.identifier.IdImpl;
38  import org.opencastproject.security.api.OrganizationDirectoryService;
39  import org.opencastproject.security.api.SecurityService;
40  import org.opencastproject.security.api.UserDirectoryService;
41  import org.opencastproject.serviceregistry.api.ServiceRegistry;
42  import org.opencastproject.serviceregistry.api.ServiceRegistryException;
43  import org.opencastproject.smil.api.SmilException;
44  import org.opencastproject.smil.api.SmilService;
45  import org.opencastproject.smil.entity.api.Smil;
46  import org.opencastproject.smil.entity.media.api.SmilMediaObject;
47  import org.opencastproject.smil.entity.media.container.api.SmilMediaContainer;
48  import org.opencastproject.smil.entity.media.element.api.SmilMediaElement;
49  import org.opencastproject.smil.entity.media.param.api.SmilMediaParam;
50  import org.opencastproject.smil.entity.media.param.api.SmilMediaParamGroup;
51  import org.opencastproject.subtitleparser.webvttparser.WebVTTParser;
52  import org.opencastproject.subtitleparser.webvttparser.WebVTTSubtitle;
53  import org.opencastproject.subtitleparser.webvttparser.WebVTTSubtitleCue;
54  import org.opencastproject.subtitleparser.webvttparser.WebVTTWriter;
55  import org.opencastproject.util.LoadUtil;
56  import org.opencastproject.util.NotFoundException;
57  import org.opencastproject.videoeditor.api.ProcessFailedException;
58  import org.opencastproject.videoeditor.api.VideoEditorService;
59  import org.opencastproject.videoeditor.ffmpeg.FFmpegEdit;
60  import org.opencastproject.workspace.api.Workspace;
61  
62  import org.apache.commons.io.FileUtils;
63  import org.apache.commons.io.FilenameUtils;
64  import org.apache.commons.io.IOUtils;
65  import org.osgi.service.cm.ConfigurationException;
66  import org.osgi.service.cm.ManagedService;
67  import org.osgi.service.component.ComponentContext;
68  import org.osgi.service.component.annotations.Activate;
69  import org.osgi.service.component.annotations.Component;
70  import org.osgi.service.component.annotations.Deactivate;
71  import org.osgi.service.component.annotations.Reference;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  import java.io.File;
76  import java.io.FileInputStream;
77  import java.io.FileOutputStream;
78  import java.io.IOException;
79  import java.io.InputStream;
80  import java.net.URI;
81  import java.net.URISyntaxException;
82  import java.util.ArrayList;
83  import java.util.Arrays;
84  import java.util.Dictionary;
85  import java.util.Enumeration;
86  import java.util.Iterator;
87  import java.util.LinkedList;
88  import java.util.List;
89  import java.util.Objects;
90  import java.util.Properties;
91  import java.util.stream.Collectors;
92  
93  import javax.xml.bind.JAXBException;
94  
95  /**
96   * Implementation of VideoeditorService using FFMPEG
97   */
98  @Component(
99      immediate = true,
100     service = { VideoEditorService.class,ManagedService.class },
101     property = {
102         "service.description=Video Editor Service"
103     }
104 )
105 public class VideoEditorServiceImpl extends AbstractJobProducer implements VideoEditorService, ManagedService {
106 
107   public static final String JOB_LOAD_KEY = "job.load.videoeditor";
108 
109   private static final float DEFAULT_JOB_LOAD = 0.8f;
110 
111   private float jobload = DEFAULT_JOB_LOAD;
112 
113   public static final String SEGMENTS_MIN_DURATION_KEY = "segments.min.duration";
114 
115   private static final int DEFAULT_SEGMENTS_MIN_DURATION = 2000;
116 
117   private int segmentsMinDuration = DEFAULT_SEGMENTS_MIN_DURATION;
118 
119   public static final String SEGMENTS_MIN_CUT_DURATION_KEY = "segments.min.cut.duration";
120 
121   private static final int DEFAULT_SEGMENTS_MIN_CUT_DURATION = 2000;
122 
123   private int segmentsMinCutDuration = DEFAULT_SEGMENTS_MIN_CUT_DURATION;
124 
125   private static final String VTT_SHORTEN_FLAVOR_TYPES = "vtt.shorten.flavor.types";
126   private static final String DEFAULT_VTT_SHORTEN_FLAVOR_TYPES = "chapters";
127   private List<String> shortenFlavorTypes = new ArrayList<>();
128 
129   /**
130    * The logging instance
131    */
132   private static final Logger logger = LoggerFactory.getLogger(VideoEditorServiceImpl.class);
133   private static final String JOB_TYPE = "org.opencastproject.videoeditor";
134   private static final String COLLECTION_ID = "videoeditor";
135   private static final String SINK_FLAVOR_SUBTYPE = "trimmed";
136 
137   private enum Operation {
138     PROCESS_SMIL
139   }
140 
141   /**
142    * Reference to the media inspection service
143    */
144   private MediaInspectionService inspectionService = null;
145   /**
146    * Reference to the workspace service
147    */
148   private Workspace workspace = null;
149   /**
150    * Reference to the receipt service
151    */
152   private ServiceRegistry serviceRegistry;
153   /**
154    * The organization directory service
155    */
156   protected OrganizationDirectoryService organizationDirectoryService = null;
157   /**
158    * The security service
159    */
160   protected SecurityService securityService = null;
161   /**
162    * The user directory service
163    */
164   protected UserDirectoryService userDirectoryService = null;
165   /**
166    * The smil service.
167    */
168   protected SmilService smilService = null;
169   /**
170    * Bundle properties
171    */
172   private Properties properties = new Properties();
173 
174   public VideoEditorServiceImpl() {
175     super(JOB_TYPE);
176   }
177 
178   /**
179    * Splice segments given by smil document for the given track to the new one.
180    *
181    * @param job
182    *          processing job
183    * @param smil
184    *          smil document with media segments description
185    * @param trackParamGroupId
186    * @return processed track
187    * @throws ProcessFailedException
188    *           if an error occured
189    */
190   protected Track processSmil(Job job, Smil smil, String trackParamGroupId) throws ProcessFailedException {
191 
192     SmilMediaParamGroup trackParamGroup;
193     ArrayList<String> inputfile = new ArrayList<>();
194     ArrayList<VideoClip> videoclips = new ArrayList<>();
195     ArrayList<VideoClip> refElements = new ArrayList<>();
196     try {
197       trackParamGroup = (SmilMediaParamGroup) smil.get(trackParamGroupId);
198     } catch (SmilException ex) {
199       // can't be thrown, because we found the Id in processSmil(Smil)
200       throw new ProcessFailedException("Smil does not contain a paramGroup element with Id " + trackParamGroupId);
201     }
202     MediaPackageElementFlavor sourceTrackFlavor = null;
203     String sourceTrackUri = null;
204     MediaPackageReference ref = null;
205     // get source track metadata
206     for (SmilMediaParam param : trackParamGroup.getParams()) {
207       if (SmilMediaParam.PARAM_NAME_TRACK_SRC.equals(param.getName())) {
208         sourceTrackUri = param.getValue();
209       } else if (SmilMediaParam.PARAM_NAME_TRACK_FLAVOR.equals(param.getName())) {
210         sourceTrackFlavor = MediaPackageElementFlavor.parseFlavor(param.getValue());
211       } else if (SmilMediaParam.PARAM_NAME_TRACK_ID.equals(param.getName())) {
212         ref = new MediaPackageReferenceImpl("track", param.getValue());
213       }
214     }
215     File sourceFile;
216     try {
217       sourceFile = workspace.get(new URI(sourceTrackUri));
218     } catch (IOException ex) {
219       throw new ProcessFailedException("Can't read " + sourceTrackUri);
220     } catch (NotFoundException ex) {
221       throw new ProcessFailedException("Workspace does not contain a track " + sourceTrackUri);
222     } catch (URISyntaxException ex) {
223       throw new ProcessFailedException("Source URI " + sourceTrackUri + " is not valid.");
224     }
225     // inspect input file to retrieve media information
226     Job inspectionJob;
227     Track sourceTrack;
228     try {
229       inspectionJob = inspect(job, new URI(sourceTrackUri));
230       sourceTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload());
231     } catch (URISyntaxException e) {
232       throw new ProcessFailedException("Source URI " + sourceTrackUri + " is not valid.");
233     } catch (MediaInspectionException e) {
234       throw new ProcessFailedException("Media inspection of " + sourceTrackUri + " failed", e);
235     } catch (MediaPackageException e) {
236       throw new ProcessFailedException("Deserialization of source track " + sourceTrackUri + " failed", e);
237     }
238 
239     // create working directory
240     File tempDirectory = new File(new File(workspace.rootDirectory()), "editor");
241     tempDirectory = new File(tempDirectory, Long.toString(job.getId()));
242 
243     URI newTrackURI;
244     inputfile.add(sourceFile.getAbsolutePath()); // default source - add to source table as 0
245     int srcIndex = inputfile.indexOf(sourceFile.getAbsolutePath()); // index = 0
246     logger.info("Start processing srcfile {}", sourceFile.getAbsolutePath());
247     try {
248       // parse body elements
249       for (SmilMediaObject element : smil.getBody().getMediaElements()) {
250         // body should contain par elements
251         if (element.isContainer()) {
252           SmilMediaContainer container = (SmilMediaContainer) element;
253           if (SmilMediaContainer.ContainerType.PAR == container.getContainerType()) {
254             // par element should contain media elements
255             for (SmilMediaObject elementChild : container.getElements()) {
256               if (!elementChild.isContainer()) {
257                 SmilMediaElement media = (SmilMediaElement) elementChild;
258                 if (trackParamGroupId.equals(media.getParamGroup())) {
259                   long begin = media.getClipBeginMS();
260                   long end = media.getClipEndMS();
261                   URI clipTrackURI = media.getSrc();
262                   File clipSourceFile = null;
263                   if (clipTrackURI != null) {
264                     try {
265                       clipSourceFile = workspace.get(clipTrackURI);
266                     } catch (IOException ex) {
267                       throw new ProcessFailedException("Can't read " + clipTrackURI);
268                     } catch (NotFoundException ex) {
269                       throw new ProcessFailedException("Workspace does not contain a track " + clipTrackURI);
270                     }
271                   }
272                   int index;
273 
274                   if (clipSourceFile != null) {      // clip has different source
275                     index = inputfile.indexOf(clipSourceFile.getAbsolutePath()); // Look for known tracks
276                     if (index == -1) {
277                       inputfile.add(clipSourceFile.getAbsolutePath()); // add new track
278                      //TODO: inspect each new video file, bad input will throw exc
279                     }
280                     index = inputfile.indexOf(clipSourceFile.getAbsolutePath());
281                   } else {
282                     index = srcIndex; // default src
283                   }
284 
285                   // Sort out ref elements
286                   if (media.getMediaType() == SmilMediaElement.MediaType.REF) {
287                     refElements.add(new VideoClip(index, begin, end));
288                   } else {
289                     videoclips.add(new VideoClip(index, begin, end));
290                   }
291                 }
292               } else {
293                 throw new ProcessFailedException("Smil container '"
294                         + ((SmilMediaContainer) elementChild).getContainerType().toString()
295                         + "'is not supported yet");
296               }
297             }
298           } else {
299             throw new ProcessFailedException("Smil container '"
300                     + container.getContainerType().toString() + "'is not supported yet");
301           }
302         }
303       }
304 
305       // Don't mix video/audio and subtitles
306       if (videoclips.size() > 0 && refElements.size() > 0) {
307         throw new ProcessFailedException("Can not process media elements together with ref elements. "
308                 + "There likely is an error in the SMIL file");
309       }
310 
311       // get output file extension
312       String outputFileExtension = null;
313       if (videoclips.size() > 0) {
314         outputFileExtension = properties.getProperty(VideoEditorProperties.DEFAULT_EXTENSION, ".mp4");
315       }
316       if (refElements.size() > 0) {
317         String extension = FilenameUtils.getExtension(sourceTrackUri);
318         if (VideoEditorProperties.WEBVTT_EXTENSION.equals(extension)) {
319           outputFileExtension = properties.getProperty(VideoEditorProperties.WEBVTT_EXTENSION, ".vtt");
320         }
321       }
322       outputFileExtension = properties.getProperty(VideoEditorProperties.OUTPUT_FILE_EXTENSION, outputFileExtension);
323 
324       if (!outputFileExtension.startsWith(".")) {
325         outputFileExtension = '.' + outputFileExtension;
326       }
327 
328       String filename = String.format("%s-%s%s", sourceTrackFlavor,
329               FilenameUtils.removeExtension(sourceFile.getName()), outputFileExtension);
330       File outputPath = new File(tempDirectory, filename);
331 
332       if (!outputPath.getParentFile().exists()) {
333         outputPath.getParentFile().mkdirs();
334       }
335 
336       // If we are cutting video/audio, use ffmpeg
337       if (videoclips.size() > 0) {
338         // remove very short cuts that will look bad
339         List<VideoClip> cleanclips = sortSegments(videoclips, segmentsMinDuration, segmentsMinCutDuration);
340         String error = null;
341         String outputResolution = "";    //TODO: fetch the largest output resolution from SMIL.head.layout.root-layout
342         // When outputResolution is set to WxH, all clips are scaled to that size in the output video.
343         // TODO: Each clips could have a region id, relative to the root-layout
344         // Then each clip is zoomed/panned/padded to WxH before concatenation
345         FFmpegEdit ffmpeg = new FFmpegEdit(properties);
346         error = ffmpeg.processEdits(inputfile, outputPath.getAbsolutePath(), outputResolution, cleanclips,
347                 sourceTrack.hasAudio(), sourceTrack.hasVideo());
348 
349         if (error != null) {
350           FileUtils.deleteQuietly(tempDirectory);
351           throw new ProcessFailedException("Editing pipeline exited abnormally! Error: " + error);
352         }
353       }
354 
355       // If we are cutting ref elements, check if they are subtitle files
356       // Or give up
357       // TODO: It might be better if subtitle tracks were assigned the mediatype "texttrack" in the first place
358       if (refElements.size() > 0) {
359         // remove very short cuts that will look bad
360         List<VideoClip> cleanclips = sortSegments(refElements, segmentsMinDuration, segmentsMinCutDuration);
361         String extension = FilenameUtils.getExtension(sourceTrackUri);
362         if (VideoEditorProperties.WEBVTT_EXTENSION.equals(extension)) {
363           // Parse
364           WebVTTParser parser = new WebVTTParser();
365           WebVTTSubtitle subtitle;
366           try (FileInputStream fin = new FileInputStream(sourceFile)) {
367             subtitle = parser.parse(fin);
368           }
369 
370           if (shortenFlavorTypes.contains(sourceTrackFlavor.getType())) {
371             // Edit - Shorten
372             List<WebVTTSubtitleCue> result = new ArrayList<>();
373             for (WebVTTSubtitleCue ch :  subtitle.getCues()) {
374               long newStart = totalKeptBefore(ch.getStartTime(), cleanclips);
375               long newEnd   = totalKeptBefore(ch.getEndTime(), cleanclips);
376               if (newEnd > newStart) {
377                 ch.setStartTime(newStart);
378                 ch.setEndTime(newEnd);
379                 result.add(ch);
380               }
381             }
382             subtitle.setCues(result);
383           } else {
384             // Edit - Remove (Default)
385             List<WebVTTSubtitleCue> cutCues = new ArrayList<>();
386             double removedTime = 0;
387             for (int i = 0; i < cleanclips.size(); i++) {
388               if (i == 0) {
389                 removedTime = removedTime
390                     + cleanclips.get(i).getStartInMilliseconds();
391               } else {
392                 removedTime = removedTime
393                     + cleanclips.get(i).getStartInMilliseconds()
394                     - cleanclips.get(i - 1).getEndInMilliseconds();
395               }
396               for (WebVTTSubtitleCue cue : subtitle.getCues()) {
397                 if ((cleanclips.get(i).getStartInMilliseconds() - SUBTITLE_GRACE_PERIOD) <= cue.getStartTime()
398                     && (cleanclips.get(i).getEndInMilliseconds() + SUBTITLE_GRACE_PERIOD) >= cue.getEndTime()) {
399                   cue.setStartTime((long) (cue.getStartTime() - removedTime));
400                   cue.setEndTime((long) (cue.getEndTime() - removedTime));
401                   cutCues.add(cue);
402                 }
403               }
404             }
405             subtitle.setCues(cutCues);
406           }
407 
408           // Write
409           try (FileOutputStream fos = new FileOutputStream(outputPath)) {
410             WebVTTWriter writer = new WebVTTWriter();
411             writer.write(subtitle, fos);
412           }
413         } else {
414           throw new ProcessFailedException("The video editor does not support the following file: " + sourceTrackUri);
415         }
416       }
417 
418       // create Track for edited file
419       String newTrackId = IdImpl.fromUUID().toString();
420       InputStream in = new FileInputStream(outputPath);
421       try {
422         newTrackURI = workspace.putInCollection(COLLECTION_ID,
423                 String.format("%s-%s%s", sourceTrackFlavor.getType(), newTrackId, outputFileExtension), in);
424       } catch (IllegalArgumentException ex) {
425         throw new ProcessFailedException("Copy track into workspace failed! " + ex.getMessage());
426       } finally {
427         IOUtils.closeQuietly(in);
428         FileUtils.deleteQuietly(tempDirectory);
429       }
430 
431       // inspect new Track
432       try {
433         inspectionJob = inspect(job,newTrackURI);
434       } catch (MediaInspectionException e) {
435         throw new ProcessFailedException("Media inspection of " + newTrackURI + " failed", e);
436       }
437       Track editedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload());
438       logger.info("Finished editing track {}", editedTrack);
439       editedTrack.setIdentifier(newTrackId);
440       editedTrack.setReference(ref);
441       if (videoclips.size() > 0) {
442         editedTrack.setFlavor(new MediaPackageElementFlavor(sourceTrackFlavor.getType(), SINK_FLAVOR_SUBTYPE));
443       }
444       if (refElements.size() > 0) {
445         String extension = FilenameUtils.getExtension(sourceTrackUri);
446         if (VideoEditorProperties.WEBVTT_EXTENSION.equals(extension)) {
447           editedTrack.setFlavor(new MediaPackageElementFlavor(sourceTrackFlavor.getType(),
448               sourceTrackFlavor.getSubtype() + "+" + SINK_FLAVOR_SUBTYPE));
449         }
450       }
451 
452       return editedTrack;
453 
454     } catch (MediaInspectionException ex) {
455       throw new ProcessFailedException("Inspecting encoded Track failed with: " + ex.getMessage());
456     } catch (MediaPackageException ex) {
457       throw new ProcessFailedException("Unable to serialize edited Track! " + ex.getMessage());
458     } catch (Exception ex) {
459       throw new ProcessFailedException("Unable to process SMIL: " + ex.getMessage(), ex);
460     } finally {
461       FileUtils.deleteQuietly(tempDirectory);
462     }
463   }
464 
465   /*
466    * Inspect the output file
467    */
468   protected Job inspect(Job job, URI workspaceURI) throws MediaInspectionException, ProcessFailedException {
469     Job inspectionJob;
470     try {
471       inspectionJob = inspectionService.inspect(workspaceURI);
472     } catch (MediaInspectionException e) {
473       incident().recordJobCreationIncident(job, e);
474       throw new MediaInspectionException("Media inspection of " + workspaceURI + " failed", e);
475     }
476     JobBarrier barrier = new JobBarrier(job, serviceRegistry, inspectionJob);
477     if (!barrier.waitForJobs().isSuccess()) {
478       throw new ProcessFailedException("Media inspection of " + workspaceURI + " failed");
479     }
480     return inspectionJob;
481   }
482 
483   /* Clean up the edit points, make sure they are at least 2 seconds apart (default fade duration)
484    * Otherwise it can be very slow to run and output will be ugly because of the cross fades
485    */
486   private static List<VideoClip> sortSegments(List<VideoClip> edits, int segmentsMinDuration,
487       int segmentsMinCutDuration) {
488     LinkedList<VideoClip> ll = new LinkedList<>();
489     List<VideoClip> clips = new ArrayList<>();
490     Iterator<VideoClip> it = edits.iterator();
491     VideoClip clip;
492     VideoClip nextclip;
493     while (it.hasNext()) {     // Check for legal durations
494       clip = it.next();
495       if (clip.getDurationInMilliseconds() > segmentsMinDuration) { // Keep segments longer than segmentsMinDuration
496         ll.add(clip);
497       }
498     }
499     clip = ll.pop();        // initialize
500     while (!ll.isEmpty()) { // Check that 2 consecutive segments from same src are at least segmentsMinCutDuration apart
501       if (ll.peek() != null) {
502         nextclip = ll.pop();  // check next consecutive segment
503         // collapse two segments into one
504         if (nextclip.getSrc() == clip.getSrc()
505             && nextclip.getStartInMilliseconds() - clip.getEndInMilliseconds() < segmentsMinCutDuration) {
506           clip.setEnd(nextclip.getEndInMilliseconds());   // by using input of seg 1 and outpoint of seg 2
507         } else {
508           clips.add(clip);   // keep last segment
509           clip = nextclip;   // check next segment
510         }
511       }
512     }
513     clips.add(clip); // add last segment
514     return clips;
515   }
516 
517   /*
518    * Compute removed time from segments for cue time
519    */
520   private long totalKeptBefore(long t, List<VideoClip> keepSegments) {
521     long kept = 0;
522     for (VideoClip s : keepSegments) {
523       if (s.getEndInMilliseconds() <= 0) {
524         continue;
525       }
526       if (s.getStartInMilliseconds() >= t) {
527         break; // this and all further segments are after t
528       }
529       long overlapStart = Math.max(s.getStartInMilliseconds(), 0);
530       long overlapEnd = Math.min(s.getEndInMilliseconds(), t);
531       if (overlapEnd > overlapStart) {
532         kept += (overlapEnd - overlapStart);
533       }
534     }
535     return kept;
536   }
537 
538   /**
539    * {@inheritDoc}
540    *
541    * @see
542    * org.opencastproject.videoeditor.api.VideoEditorService#processSmil(org.opencastproject.smil.entity.api.Smil)
543    */
544   @Override
545   public List<Job> processSmil(Smil smil) throws ProcessFailedException {
546     if (smil == null) {
547       throw new ProcessFailedException("Smil document is null!");
548     }
549 
550     List<Job> jobs = new LinkedList<Job>();
551     try {
552       for (SmilMediaParamGroup paramGroup : smil.getHead().getParamGroups()) {
553         for (SmilMediaParam param : paramGroup.getParams()) {
554           if (SmilMediaParam.PARAM_NAME_TRACK_ID.equals(param.getName())) {
555             jobs.add(serviceRegistry.createJob(getJobType(), Operation.PROCESS_SMIL.toString(),
556                     Arrays.asList(smil.toXML(), paramGroup.getId()), jobload));
557           }
558         }
559       }
560       return jobs;
561     } catch (JAXBException ex) {
562       throw new ProcessFailedException("Failed to serialize smil " + smil.getId());
563     } catch (ServiceRegistryException ex) {
564       throw new ProcessFailedException("Failed to create job: " + ex.getMessage());
565     } catch (Exception ex) {
566       throw new ProcessFailedException(ex.getMessage());
567     }
568   }
569 
570   @Override
571   protected String process(Job job) throws Exception {
572     if (Operation.PROCESS_SMIL.toString().equals(job.getOperation())) {
573       Smil smil = smilService.fromXml(job.getArguments().get(0)).getSmil();
574       if (smil == null) {
575         throw new ProcessFailedException("Smil document is null!");
576       }
577 
578       Track editedTrack = processSmil(job, smil, job.getArguments().get(1));
579       return MediaPackageElementParser.getAsXml(editedTrack);
580     }
581 
582     throw new ProcessFailedException("Can't handle this operation: " + job.getOperation());
583   }
584 
585   @Override
586   protected ServiceRegistry getServiceRegistry() {
587     return serviceRegistry;
588   }
589 
590   @Override
591   protected SecurityService getSecurityService() {
592     return securityService;
593   }
594 
595   @Override
596   protected UserDirectoryService getUserDirectoryService() {
597     return userDirectoryService;
598   }
599 
600   @Override
601   protected OrganizationDirectoryService getOrganizationDirectoryService() {
602     return organizationDirectoryService;
603   }
604 
605   @Override
606   @Activate
607   public void activate(ComponentContext context) {
608     logger.debug("activating...");
609     super.activate(context);
610     FFmpegEdit.init(context.getBundleContext());
611   }
612 
613   @Deactivate
614   protected void deactivate(ComponentContext context) {
615     logger.debug("deactivating...");
616   }
617 
618   @Override
619   public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
620     this.properties = new Properties();
621     if (properties == null) {
622       logger.info("No configuration available, using defaults");
623       return;
624     }
625 
626     Enumeration<String> keys = properties.keys();
627     while (keys.hasMoreElements()) {
628       String key = keys.nextElement();
629       this.properties.put(key, properties.get(key));
630     }
631     logger.debug("Properties updated!");
632 
633     jobload = LoadUtil.getConfiguredLoadValue(properties, JOB_LOAD_KEY, DEFAULT_JOB_LOAD, serviceRegistry);
634     segmentsMinDuration = Integer.parseInt(this.properties.getProperty(SEGMENTS_MIN_DURATION_KEY,
635         String.valueOf(DEFAULT_SEGMENTS_MIN_DURATION)));
636     segmentsMinCutDuration = Integer.parseInt(this.properties.getProperty(SEGMENTS_MIN_CUT_DURATION_KEY,
637         String.valueOf(DEFAULT_SEGMENTS_MIN_CUT_DURATION)));
638     String tmp = Objects.toString(properties.get(VTT_SHORTEN_FLAVOR_TYPES),
639         DEFAULT_VTT_SHORTEN_FLAVOR_TYPES);
640     shortenFlavorTypes = Arrays.stream(tmp.split(","))
641         .map(String::trim)
642         .collect(Collectors.toList());
643   }
644 
645   @Reference
646   public void setMediaInspectionService(MediaInspectionService inspectionService) {
647     this.inspectionService = inspectionService;
648   }
649 
650   @Reference
651   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
652     this.serviceRegistry = serviceRegistry;
653   }
654 
655   @Reference
656   public void setWorkspace(Workspace workspace) {
657     this.workspace = workspace;
658   }
659 
660   @Reference
661   public void setSecurityService(SecurityService securityService) {
662     this.securityService = securityService;
663   }
664 
665   @Reference
666   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
667     this.userDirectoryService = userDirectoryService;
668   }
669 
670   @Reference
671   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
672     this.organizationDirectoryService = organizationDirectoryService;
673   }
674 
675   @Reference
676   public void setSmilService(SmilService smilService) {
677     this.smilService = smilService;
678   }
679 }