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