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.workflow.handler.composer;
23  
24  import org.opencastproject.composer.api.ComposerService;
25  import org.opencastproject.composer.api.EncoderException;
26  import org.opencastproject.composer.api.EncodingProfile;
27  import org.opencastproject.composer.api.EncodingProfile.MediaType;
28  import org.opencastproject.job.api.Job;
29  import org.opencastproject.job.api.JobContext;
30  import org.opencastproject.mediapackage.AdaptivePlaylist;
31  import org.opencastproject.mediapackage.Catalog;
32  import org.opencastproject.mediapackage.MediaPackage;
33  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
34  import org.opencastproject.mediapackage.MediaPackageElementParser;
35  import org.opencastproject.mediapackage.MediaPackageException;
36  import org.opencastproject.mediapackage.Track;
37  import org.opencastproject.mediapackage.selector.AbstractMediaPackageElementSelector;
38  import org.opencastproject.mediapackage.selector.TrackSelector;
39  import org.opencastproject.serviceregistry.api.ServiceRegistry;
40  import org.opencastproject.smil.api.SmilException;
41  import org.opencastproject.smil.api.SmilResponse;
42  import org.opencastproject.smil.api.SmilService;
43  import org.opencastproject.smil.entity.api.Smil;
44  import org.opencastproject.smil.entity.media.param.api.SmilMediaParam;
45  import org.opencastproject.smil.entity.media.param.api.SmilMediaParamGroup;
46  import org.opencastproject.util.NotFoundException;
47  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
48  import org.opencastproject.workflow.api.WorkflowInstance;
49  import org.opencastproject.workflow.api.WorkflowOperationException;
50  import org.opencastproject.workflow.api.WorkflowOperationHandler;
51  import org.opencastproject.workflow.api.WorkflowOperationInstance;
52  import org.opencastproject.workflow.api.WorkflowOperationResult;
53  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
54  import org.opencastproject.workspace.api.Workspace;
55  
56  import org.apache.commons.io.FileUtils;
57  import org.apache.commons.io.FilenameUtils;
58  import org.apache.commons.lang3.StringUtils;
59  import org.osgi.service.component.ComponentContext;
60  import org.osgi.service.component.annotations.Activate;
61  import org.osgi.service.component.annotations.Component;
62  import org.osgi.service.component.annotations.Reference;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  import java.io.File;
67  import java.io.IOException;
68  import java.net.URI;
69  import java.net.URISyntaxException;
70  import java.util.ArrayList;
71  import java.util.Arrays;
72  import java.util.Collection;
73  import java.util.HashMap;
74  import java.util.HashSet;
75  import java.util.Iterator;
76  import java.util.List;
77  import java.util.Map;
78  import java.util.Set;
79  import java.util.function.Predicate;
80  import java.util.stream.Collectors;
81  
82  /**
83   * The workflow definition for handling "compose" operations
84   */
85  @Component(
86      immediate = true,
87      service = WorkflowOperationHandler.class,
88      property = {
89          "service.description=Process Smil Workflow Operation Handler",
90          "workflow.operation=process-smil"
91      }
92  )
93  public class ProcessSmilWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
94    static final String SEPARATOR = ";";
95    /** The logging facility */
96    private static final Logger logger = LoggerFactory.getLogger(ProcessSmilWorkflowOperationHandler.class);
97  
98    /** The composer service */
99    private ComposerService composerService = null;
100   /** The smil service to parse the smil */
101   private SmilService smilService;
102   /** The local workspace */
103   private Workspace workspace = null;
104 
105   private Predicate<EncodingProfile> isManifestEP = p -> p.getOutputType() == EncodingProfile.MediaType.Manifest;
106 
107   /**
108    * A convenience structure to hold info for each paramgroup in the Smil which will produce one trim/concat/encode job
109    */
110   private class TrackSection {
111     private final String paramGroupId;
112     private List<Track> sourceTracks;
113     private List<String> smilTracks;
114     private final String flavor;
115     private String mediaType = ""; // Has both Audio and Video
116 
117     TrackSection(String id, String flavor) {
118       this.flavor = flavor;
119       this.paramGroupId = id;
120     }
121 
122     public List<Track> getSourceTracks() {
123       return sourceTracks;
124     }
125 
126     /**
127      * Set source Tracks for this group, if audio or video is missing in any of the source files, then do not try to
128      * edit with the missing media type, because it will fail
129      *
130      * @param sourceTracks
131      */
132     public void setSourceTracks(List<Track> sourceTracks) {
133       boolean hasVideo = true;
134       boolean hasAudio = true;
135       this.sourceTracks = sourceTracks;
136       for (Track track : sourceTracks) {
137         if (!track.hasVideo())
138           hasVideo = false;
139         if (!track.hasAudio())
140           hasAudio = false;
141       }
142       if (!hasVideo) {
143         mediaType = ComposerService.AUDIO_ONLY;
144       }
145       if (!hasAudio) {
146         mediaType = ComposerService.VIDEO_ONLY;
147       }
148     }
149 
150     public String getFlavor() {
151       return flavor;
152     }
153 
154     @Override
155     public String toString() {
156       return paramGroupId + " " + flavor + " " + sourceTracks.toString();
157     }
158 
159     public void setSmilTrackList(List<String> smilSourceTracks) {
160       smilTracks = smilSourceTracks;
161     }
162 
163     public List<String> getSmilTrackList() {
164       return smilTracks;
165     }
166   };
167 
168   // To return both params from a function that checks all the jobs
169   private class ResultTally {
170     private final MediaPackage mediaPackage;
171     private final long totalTimeInQueue;
172 
173     ResultTally(MediaPackage mediaPackage, long totalTimeInQueue) {
174       super();
175       this.mediaPackage = mediaPackage;
176       this.totalTimeInQueue = totalTimeInQueue;
177     }
178 
179     public MediaPackage getMediaPackage() {
180       return mediaPackage;
181     }
182 
183     public long getTotalTimeInQueue() {
184       return totalTimeInQueue;
185     }
186   }
187 
188   @Activate
189   public void activate(ComponentContext cc) {
190     super.activate(cc);
191   }
192 
193   /**
194    * Callback for the OSGi declarative services configuration.
195    *
196    * @param composerService
197    *          the local composer service
198    */
199   @Reference
200   protected void setComposerService(ComposerService composerService) {
201     this.composerService = composerService;
202   }
203 
204   /**
205    * Callback for the OSGi declarative services configuration.
206    *
207    * @param smilService
208    */
209   @Reference
210   protected void setSmilService(SmilService smilService) {
211     this.smilService = smilService;
212   }
213 
214   /**
215    * Callback for declarative services configuration that will introduce us to the local workspace service.
216    * Implementation assumes that the reference is configured as being static.
217    *
218    * @param workspace
219    *          an instance of the workspace
220    */
221   @Reference
222   public void setWorkspace(Workspace workspace) {
223     this.workspace = workspace;
224   }
225 
226   @Reference
227   @Override
228   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
229     super.setServiceRegistry(serviceRegistry);
230   }
231 
232   /**
233    * {@inheritDoc}
234    *
235    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(org.opencastproject.workflow.api.WorkflowInstance,
236    *      JobContext)
237    */
238   @Override
239   public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
240           throws WorkflowOperationException {
241     try {
242       return processSmil(workflowInstance.getMediaPackage(), workflowInstance.getCurrentOperation());
243     } catch (Exception e) {
244       e.printStackTrace();
245       throw new WorkflowOperationException(e);
246     }
247   }
248 
249   private String[] getConfigAsArray(WorkflowOperationInstance operation, String name) {
250     String sourceOption = StringUtils.trimToNull(operation.getConfiguration(name));
251     String[] options = (sourceOption != null) ? sourceOption.split(SEPARATOR) : null;
252     return (options);
253   }
254 
255   private String[] collapseConfig(WorkflowOperationInstance operation, String name) {
256     String targetOption = StringUtils.trimToNull(operation.getConfiguration(name));
257     return (targetOption != null) ? new String[] { targetOption.replaceAll(SEPARATOR, ",") } : null;
258   }
259 
260   /**
261    * Encode tracks from Smil using profiles stored in properties and updates current MediaPackage. This procedure parses
262    * the workflow definitions and decides how many encoding jobs are needed
263    *
264    * @param src
265    *          The source media package
266    * @param operation
267    *          the current workflow operation
268    * @return the operation result containing the updated media package
269    * @throws EncoderException
270    *           if encoding fails
271    * @throws WorkflowOperationException
272    *           if errors occur during processing
273    * @throws IOException
274    *           if the workspace operations fail
275    * @throws NotFoundException
276    *           if the workspace doesn't contain the requested file
277    */
278   private WorkflowOperationResult processSmil(MediaPackage src, WorkflowOperationInstance operation)
279           throws EncoderException, IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
280     MediaPackage mediaPackage = (MediaPackage) src.clone();
281     // Check which tags have been configured
282     String smilFlavorOption = StringUtils.trimToEmpty(operation.getConfiguration("smil-flavor"));
283     String[] srcFlavors = getConfigAsArray(operation, "source-flavors");
284     String[] targetFlavors = getConfigAsArray(operation, "target-flavors");
285     String[] targetTags = getConfigAsArray(operation, "target-tags");
286     String[] profilesSections = getConfigAsArray(operation, "encoding-profiles");
287     String tagWithProfileConfig = StringUtils.trimToNull(operation.getConfiguration("tag-with-profile"));
288     boolean tagWithProfile = tagWithProfileConfig != null && Boolean.parseBoolean(tagWithProfileConfig);
289 
290     // Make sure there is a smil src
291     if (StringUtils.isBlank(smilFlavorOption)) {
292       logger.info("No smil flavor has been specified, no src to process"); // Must have Smil input
293       return createResult(mediaPackage, Action.CONTINUE);
294     }
295 
296     if (srcFlavors == null) {
297       logger.info("No source flavors have been specified, not matching anything");
298       return createResult(mediaPackage, Action.CONTINUE); // Should be OK
299     }
300     // Make sure at least one encoding profile is provided
301     if (profilesSections == null) {
302       throw new WorkflowOperationException("No encoding profile was specified");
303     }
304 
305     /*
306      * Must have smil file, and encoding profile(s) If source-flavors is used, then target-flavors must be used If
307      * separators ";" are used in source-flavors, then there must be the equivalent number of matching target-flavors
308      * and encoding profiles used, or one for all of them.
309      */
310     if (srcFlavors.length > 1) { // Different processing for each flavor
311       if (targetFlavors != null && srcFlavors.length != targetFlavors.length && targetFlavors.length != 1) {
312         String mesg = "Number of target flavor sections " + targetFlavors + " must either match that of src flavor "
313                 + srcFlavors + " or equal 1 ";
314         throw new WorkflowOperationException(mesg);
315       }
316       if (srcFlavors.length != profilesSections.length) {
317         if (profilesSections.length != 1) {
318           String mesg = "Number of encoding profile sections " + profilesSections
319                   + " must either match that of src flavor " + srcFlavors + " or equal 1 ";
320           throw new WorkflowOperationException(mesg);
321         } else { // we need to duplicate profileSections for each src selector
322           String[] array = new String[srcFlavors.length];
323           Arrays.fill(array, 0, srcFlavors.length, profilesSections[0]);
324           profilesSections = array;
325         }
326       }
327       if (targetTags != null && srcFlavors.length != targetTags.length && targetTags.length != 1) {
328         String mesg = "Number of target Tags sections " + targetTags + " must either match that of src flavor "
329                 + srcFlavors + " or equal 1 ";
330         throw new WorkflowOperationException(mesg);
331       }
332     } else { // Only one srcFlavor - collapse all sections into one
333       targetFlavors = collapseConfig(operation, "target-flavors");
334       targetTags = collapseConfig(operation, "target-tags");
335       profilesSections = collapseConfig(operation, "encoding-profiles");
336       if (profilesSections.length != 1)
337         throw new WorkflowOperationException(
338                 "No matching src flavors " + srcFlavors + " for encoding profiles sections " + profilesSections);
339 
340       logger.debug("Single input flavor: output= " + Arrays.toString(targetFlavors) + " tag: "
341               + Arrays.toString(targetTags) + " profile:" + Arrays.toString(profilesSections));
342     }
343 
344     Map<Job, JobInformation> encodingJobs = new HashMap<Job, JobInformation>();
345     for (int i = 0; i < profilesSections.length; i++) {
346       // Each section is one multiconcatTrim job - set up the jobs
347       processSection(encodingJobs, mediaPackage, (srcFlavors.length > 1) ? srcFlavors[i] : srcFlavors[0],
348               (targetFlavors != null) ? ((targetFlavors.length > 1) ? targetFlavors[i] : targetFlavors[0]) : null,
349               (targetTags != null) ? ((targetTags.length > 1) ? targetTags[i] : targetTags[0]) : null,
350               (profilesSections.length > 0) ? profilesSections[i] : profilesSections[0], smilFlavorOption,
351               tagWithProfile);
352     }
353 
354     if (encodingJobs.isEmpty()) {
355       logger.info("Failed to process any tracks");
356       return createResult(mediaPackage, Action.CONTINUE);
357     }
358 
359     // Wait for the jobs to return
360     if (!waitForStatus(encodingJobs.keySet().toArray(new Job[encodingJobs.size()])).isSuccess()) {
361       throw new WorkflowOperationException("One of the encoding jobs did not complete successfully");
362     }
363     ResultTally allResults = parseResults(encodingJobs, mediaPackage);
364     WorkflowOperationResult result = createResult(allResults.getMediaPackage(), Action.CONTINUE,
365             allResults.getTotalTimeInQueue());
366     logger.debug("ProcessSmil operation completed");
367     return result;
368 
369   }
370 
371   /**
372    * Process one group encode section with one source Flavor declaration(may be wildcard) , sharing one set of shared
373    * optional target tags/flavors and one set of encoding profiles
374    *
375    * @param encodingJobs
376    * @param mediaPackage
377    * @param srcFlavors
378    *          - used to select which param group/tracks to process
379    * @param targetFlavors
380    *          - the resultant track will be tagged with these flavors
381    * @param targetTags
382    *          - the resultant track will be tagged
383    * @param media
384    *          - if video or audio only
385    * @param encodingProfiles
386    *          - profiles to use, if ant of them does not fit the source tracks, they will be omitted
387    * @param smilFlavor
388    *          - the smil flavor for the input smil
389    * @param tagWithProfile - tag target with profile name
390    * @throws WorkflowOperationException
391    *           if flavors/tags/etc are malformed or missing
392    * @throws EncoderException
393    *           if encoding command cannot be constructed
394    * @throws MediaPackageException
395    * @throws IllegalArgumentException
396    * @throws NotFoundException
397    * @throws IOException
398    */
399   private void processSection(Map<Job, JobInformation> encodingJobs, MediaPackage mediaPackage,
400           String srcFlavors, String targetFlavors, String targetTags,
401           String encodingProfiles, String smilFlavor, boolean tagWithProfile) throws WorkflowOperationException,
402           EncoderException, MediaPackageException, IllegalArgumentException, NotFoundException, IOException {
403     // Select the source flavors
404     AbstractMediaPackageElementSelector<Track> elementSelector = new TrackSelector();
405     for (String flavor : asList(srcFlavors)) {
406       try {
407         elementSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(flavor));
408       } catch (IllegalArgumentException e) {
409         throw new WorkflowOperationException("Source flavor '" + flavor + "' is malformed");
410       }
411     }
412     Smil smil = getSmil(mediaPackage, smilFlavor);
413     // Check that the matching source tracks exist in the SMIL
414     List<TrackSection> smilgroups;
415     try {
416       smilgroups = selectTracksFromMP(mediaPackage, smil, srcFlavors);
417     } catch (URISyntaxException e1) {
418       logger.info("Smil contains bad URI", e1);
419       throw new WorkflowOperationException("Smil contains bad URI - cannot process", e1);
420     }
421     if (smilgroups.size() == 0 || smilgroups.get(0).sourceTracks.size() == 0) {
422       logger.info("Smil does not contain any tracks of {} source flavor", srcFlavors);
423       return;
424     }
425 
426     // Check Target flavor
427     MediaPackageElementFlavor targetFlavor = null;
428     if (StringUtils.isNotBlank(targetFlavors)) {
429       try {
430         targetFlavor = MediaPackageElementFlavor.parseFlavor(targetFlavors);
431       } catch (IllegalArgumentException e) {
432         throw new WorkflowOperationException("Target flavor '" + targetFlavors + "' is malformed");
433       }
434     }
435 
436     Set<EncodingProfile> profiles = new HashSet<EncodingProfile>();
437     Set<String> profileNames = new HashSet<String>();
438     // Find all the encoding profiles
439     // Check that the profiles support the media source types
440     for (TrackSection ts : smilgroups)
441       for (Track track : ts.getSourceTracks()) {
442         // Check that the profile is supported
443         for (String profileName : asList(encodingProfiles)) {
444           EncodingProfile profile = composerService.getProfile(profileName);
445           if (profile == null)
446             throw new WorkflowOperationException("Encoding profile '" + profileName + "' was not found");
447           MediaType outputType = profile.getOutputType();
448           // Check if the track supports the output type of the profile MediaType outputType = profile.getOutputType();
449           // Omit if needed
450           if (outputType.equals(MediaType.Audio) && !track.hasAudio()) {
451             logger.info("Skipping encoding of '{}' with " + profileName + ", since the track lacks an audio stream",
452                     track);
453             continue;
454           } else if (outputType.equals(MediaType.Visual) && !track.hasVideo()) {
455             logger.info("Skipping encoding of '{}' " + profileName + ", since the track lacks a video stream", track);
456             continue;
457           } else if (outputType.equals(MediaType.AudioVisual) && !track.hasAudio() && !track.hasVideo()) {
458             logger.info("Skipping encoding of '{}' (audiovisual)" + profileName
459                     + ", since it lacks a audio or video stream", track);
460             continue;
461           }
462           profiles.add(profile); // Include this profiles for encoding
463           profileNames.add(profileName);
464         }
465       }
466     // Make sure there is at least one profile
467     if (profiles.isEmpty())
468       throw new WorkflowOperationException("No encoding profile was specified");
469 
470     List<String> tags = (targetTags != null) ? asList(targetTags) : null;
471     // Encode all tracks found in each param group
472     // Start encoding and wait for the result - usually one for presenter, one for presentation
473     for (TrackSection trackGroup : smilgroups) {
474       encodingJobs.put(
475               composerService.processSmil(smil, trackGroup.paramGroupId, trackGroup.mediaType,
476                       new ArrayList<String>(profileNames)),
477               new JobInformation(trackGroup.paramGroupId, trackGroup.sourceTracks,
478                       new ArrayList<EncodingProfile>(profiles), tags, targetFlavor, tagWithProfile));
479 
480       logger.info("Edit and encode {} target flavors: {} tags: {} profile {}", trackGroup, targetFlavor, tags,
481               profileNames);
482     }
483   }
484 
485   /**
486    * Find the matching encoding profile for this track and tag by name
487    * 
488    * @param track
489    * @param profiles
490    *          - profiles used to encode a track to multiple formats
491    * @return
492    */
493   private void tagByProfile(Track track, List<EncodingProfile> profiles) {
494     String rawfileName = track.getURI().getRawPath();
495     for (EncodingProfile ep : profiles) {
496       // #5687: Add any character at the beginning of the suffix so that it is properly
497       // converted in toSafeName (because the regex used there may treat the first
498       // character differently; the default regex currently does).
499       String suffixToSanitize = "X" + ep.getSuffix();
500       // !! workspace.putInCollection renames the file - need to do the same with suffix
501       String suffix = workspace.toSafeName(suffixToSanitize).substring(1);
502       if (suffix.length() > 0 && rawfileName.endsWith(suffix)) {
503         track.addTag(ep.getIdentifier());
504         return;
505       }
506     }
507   }
508 
509   /**
510    * parse all the encoding jobs to collect all the composed tracks, if any of them fail, just fail the whole thing and
511    * try to clean up
512    *
513    * @param encodingJobs
514    *          - queued jobs to do the encodings, this is parsed for payload
515    * @param mediaPackage
516    *          - to hold the target tracks
517    * @return a structure with time in queue plus a mediaPackage with all the new tracks added if all the encoding jobs
518    *         passed, if any of them fail, just fail the whole thing and try to clean up
519    * @throws IllegalArgumentException
520    * @throws NotFoundException
521    * @throws IOException
522    * @throws MediaPackageException
523    * @throws WorkflowOperationException
524    */
525   @SuppressWarnings("unchecked")
526   private ResultTally parseResults(Map<Job, JobInformation> encodingJobs, MediaPackage mediaPackage)
527           throws IllegalArgumentException, NotFoundException, IOException, MediaPackageException, WorkflowOperationException {
528     // Process the result
529     long totalTimeInQueue = 0;
530     for (Map.Entry<Job, JobInformation> entry : encodingJobs.entrySet()) {
531       Job job = entry.getKey();
532       List<Track> tracks = entry.getValue().getTracks();
533       Track track = tracks.get(0); // Can only reference one track, pick one
534       // add this receipt's queue time to the total
535       totalTimeInQueue += job.getQueueTime();
536       // it is allowed for compose jobs to return an empty payload. See the EncodeEngine interface
537       List<Track> composedTracks = null;
538       if (job.getPayload().length() > 0) {
539         composedTracks = (List<Track>) MediaPackageElementParser.getArrayFromXml(job.getPayload());
540         boolean isHLS = entry.getValue().getProfiles().stream().anyMatch(isManifestEP);
541         if (isHLS) { // check that manifests and segments counts are correct
542           decipherHLSPlaylistResults(track, entry.getValue(), mediaPackage, composedTracks);
543         }
544         // Adjust the target tags
545         for (Track composedTrack : composedTracks) {
546           if (entry.getValue().getTags() != null) {
547             for (String tag : entry.getValue().getTags()) {
548               composedTrack.addTag(tag);
549             }
550           }
551           // Adjust the target flavor. Make sure to account for partial updates
552           MediaPackageElementFlavor targetFlavor = entry.getValue().getFlavor();
553           if (targetFlavor != null) {
554             String flavorType = targetFlavor.getType();
555             String flavorSubtype = targetFlavor.getSubtype();
556             if ("*".equals(flavorType))
557               flavorType = track.getFlavor().getType();
558             if ("*".equals(flavorSubtype))
559               flavorSubtype = track.getFlavor().getSubtype();
560             composedTrack.setFlavor(new MediaPackageElementFlavor(flavorType, flavorSubtype));
561             logger.debug("Composed track has flavor '{}'", composedTrack.getFlavor());
562           }
563           List<EncodingProfile> eps = entry.getValue().getProfiles();
564           String fileName = composedTrack.getURI().getRawPath();
565           // Tag each output with encoding profile name, if configured
566           if (entry.getValue().getTagProfile()) {
567             tagByProfile(composedTrack, eps);
568           }
569 
570           if (!isHLS || composedTrack.isMaster()) {
571             fileName = getFileNameFromElements(track, composedTrack);
572           } else // preserve name from profile - should we do this?
573             fileName = FilenameUtils.getName(composedTrack.getURI().getPath());
574 
575           composedTrack.setURI(workspace.moveTo(composedTrack.getURI(), mediaPackage.getIdentifier().toString(),
576                   composedTrack.getIdentifier(), fileName));
577           synchronized (mediaPackage) {
578             mediaPackage.addDerived(composedTrack, track);
579           }
580         }
581       }
582     }
583     return new ResultTally(mediaPackage, totalTimeInQueue);
584   }
585 
586   private List<Track> getManifest(Collection<Track> tracks) {
587     return tracks.stream().filter(AdaptivePlaylist.isHLSTrackPred).collect(Collectors.toList());
588   }
589 
590   // HLS-VOD
591   private void decipherHLSPlaylistResults(Track track, JobInformation jobInfo, MediaPackage mediaPackage,
592           List<Track> composedTracks)
593           throws WorkflowOperationException, IllegalArgumentException, NotFoundException, IOException {
594     int nprofiles = jobInfo.getProfiles().size();
595     List<Track> manifests = getManifest(composedTracks);
596 
597     if (manifests.size() != nprofiles) {
598       throw new WorkflowOperationException("Number of output playlists does not match number of encoding profiles");
599     }
600     if (composedTracks.size() != manifests.size() * 2 - 1) {
601       throw new WorkflowOperationException("Number of output media does not match number of encoding profiles");
602     }
603   }
604 
605   /**
606    * @param trackFlavor
607    * @param sourceFlavor
608    * @return true if trackFlavor matches sourceFlavor
609    */
610   private boolean trackMatchesFlavor(MediaPackageElementFlavor trackFlavor, MediaPackageElementFlavor sourceFlavor) {
611     return ((trackFlavor.getType().equals(sourceFlavor.getType()) && trackFlavor.getSubtype() // exact match
612             .equals(sourceFlavor.getSubtype()))
613             || ("*".equals(sourceFlavor.getType()) && trackFlavor.getSubtype().equals(sourceFlavor.getSubtype())) // same
614                                                                                                                   // subflavor
615             || (trackFlavor.getType().equals(sourceFlavor.getType()) && "*".equals(sourceFlavor.getSubtype()))); // same
616                                                                                                                  // flavor
617   }
618 
619   /**
620    * @param mediaPackage
621    *          - mp obj contains tracks
622    * @param smil
623    *          - smil obj contains description of clips
624    * @param srcFlavors
625    *          - source flavor string (may contain wild cards)
626    * @return a structure of smil groups, each with a single flavor and mp tracks for that flavor only
627    * @throws WorkflowOperationException
628    * @throws URISyntaxException
629    */
630   private List<TrackSection> selectTracksFromMP(MediaPackage mediaPackage, Smil smil, String srcFlavors)
631           throws WorkflowOperationException, URISyntaxException {
632     List<TrackSection> sourceTrackList = new ArrayList<TrackSection>();
633     Collection<TrackSection> smilFlavors = parseSmil(smil);
634     Iterator<TrackSection> it = smilFlavors.iterator();
635     while (it.hasNext()) {
636       TrackSection ts = it.next();
637 
638       for (String f : StringUtils.split(srcFlavors, ",")) { // Look for all source Flavors
639         String sourceFlavorStr = StringUtils.trimToNull(f);
640         if (sourceFlavorStr == null)
641           continue;
642         MediaPackageElementFlavor sourceFlavor = MediaPackageElementFlavor.parseFlavor(sourceFlavorStr);
643         MediaPackageElementFlavor trackFlavor = MediaPackageElementFlavor.parseFlavor(ts.getFlavor());
644 
645         if (trackMatchesFlavor(trackFlavor, sourceFlavor)) {
646           sourceTrackList.add(ts); // This smil group matches src Flavor, add to list
647           Track[] elements = null;
648           List<Track> sourceTracks = new ArrayList<Track>();
649           elements = mediaPackage.getTracks(sourceFlavor);
650           for (String t : ts.getSmilTrackList()) { // Look thru all the tracks referenced by the smil
651             URI turi = new URI(t);
652             for (Track e : elements)
653               if (e.getURI().equals(turi)) { // find it in the mp
654                 sourceTracks.add(e); // add the track from mp containing inspection info
655               }
656           }
657           if (sourceTracks.isEmpty()) {
658             logger.info("ProcessSmil - No tracks in mediapackage matching the URI in the smil- cannot process");
659             throw new WorkflowOperationException("Smil has no matching tracks in the mediapackage");
660           }
661           ts.setSourceTracks(sourceTracks); // Will also if srcTracks are Video/Audio Only
662         }
663       }
664     }
665     return sourceTrackList;
666   }
667 
668   /**
669    * Get smil from media package
670    *
671    * @param mp
672    * @param smilFlavorOption
673    * @return smil
674    * @throws WorkflowOperationException
675    */
676   private Smil getSmil(MediaPackage mp, String smilFlavorOption) throws WorkflowOperationException {
677     MediaPackageElementFlavor smilFlavor = MediaPackageElementFlavor.parseFlavor(smilFlavorOption);
678     Catalog[] catalogs = mp.getCatalogs(smilFlavor);
679     if (catalogs.length == 0) {
680       throw new WorkflowOperationException("MediaPackage does not contain a SMIL document.");
681     }
682     Smil smil = null;
683     try {
684       File smilFile = workspace.get(catalogs[0].getURI());
685       // break up chained method for junit smil service mockup
686       SmilResponse response = smilService.fromXml(FileUtils.readFileToString(smilFile, "UTF-8"));
687       smil = response.getSmil();
688       return smil;
689     } catch (NotFoundException ex) {
690       throw new WorkflowOperationException("MediaPackage does not contain a smil catalog.");
691     } catch (IOException ex) {
692       throw new WorkflowOperationException("Failed to read smil catalog.", ex);
693     } catch (SmilException ex) {
694       throw new WorkflowOperationException(ex);
695     }
696   }
697 
698   /**
699    * Sort paramGroup by flavor, each one will be a separate job
700    *
701    * @param smil
702    * @return TrackSection
703    */
704   private Collection<TrackSection> parseSmil(Smil smil) {
705     // get all source tracks
706     List<TrackSection> trackGroups = new ArrayList<TrackSection>();
707     // Find the track flavors, and find track groups that matches the flavors
708     for (SmilMediaParamGroup paramGroup : smil.getHead().getParamGroups()) { // For each group look at elements
709       TrackSection ts = null;
710       List<String> src = new ArrayList<String>();
711       for (SmilMediaParam param : paramGroup.getParams()) {
712         if (SmilMediaParam.PARAM_NAME_TRACK_FLAVOR.matches(param.getName())) { // Is a flavor
713           ts = new TrackSection(paramGroup.getId(), param.getValue());
714           trackGroups.add(ts);
715         }
716         if (SmilMediaParam.PARAM_NAME_TRACK_SRC.matches(param.getName())) { // Is a track
717           src.add(param.getValue());
718         }
719       }
720       if (ts != null)
721         ts.setSmilTrackList(src);
722     }
723     return trackGroups;
724   }
725 
726   /**
727    * This class is used to store context information for the jobs.
728    */
729   private static final class JobInformation {
730 
731     private final List<EncodingProfile> profiles;
732     private final List<Track> tracks;
733     private String grp = null;
734     private MediaPackageElementFlavor flavor = null;
735     private List<String> tags = null;
736     private boolean tagProfile;
737 
738     JobInformation(String paramgroup, List<Track> tracks, List<EncodingProfile> profiles, List<String> tags,
739             MediaPackageElementFlavor flavor, boolean tagWithProfile) {
740       this.tracks = tracks;
741       this.grp = paramgroup;
742       this.profiles = profiles;
743       this.tags = tags;
744       this.flavor = flavor;
745       this.tagProfile = tagWithProfile;
746     }
747 
748     public List<Track> getTracks() {
749       return tracks;
750     }
751 
752     public MediaPackageElementFlavor getFlavor() {
753       return flavor;
754     }
755 
756     public List<String> getTags() {
757       return tags;
758     }
759 
760     public boolean getTagProfile() {
761       return this.tagProfile;
762     }
763 
764     @SuppressWarnings("unused")
765     public String getGroups() {
766       return grp;
767     }
768 
769     @SuppressWarnings("unused")
770     public List<EncodingProfile> getProfiles() {
771       return profiles;
772     }
773 
774   }
775 
776 }