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.EncoderException;
25  import org.opencastproject.job.api.JobContext;
26  import org.opencastproject.mediapackage.AdaptivePlaylist;
27  import org.opencastproject.mediapackage.AdaptivePlaylist.HLSMediaPackageCheck;
28  import org.opencastproject.mediapackage.MediaPackage;
29  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
30  import org.opencastproject.mediapackage.MediaPackageException;
31  import org.opencastproject.mediapackage.Track;
32  import org.opencastproject.mediapackage.selector.TrackSelector;
33  import org.opencastproject.serviceregistry.api.ServiceRegistry;
34  import org.opencastproject.util.NotFoundException;
35  import org.opencastproject.util.data.Function2;
36  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
37  import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
38  import org.opencastproject.workflow.api.WorkflowInstance;
39  import org.opencastproject.workflow.api.WorkflowOperationException;
40  import org.opencastproject.workflow.api.WorkflowOperationHandler;
41  import org.opencastproject.workflow.api.WorkflowOperationInstance;
42  import org.opencastproject.workflow.api.WorkflowOperationResult;
43  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
44  import org.opencastproject.workspace.api.Workspace;
45  
46  import org.osgi.service.component.annotations.Component;
47  import org.osgi.service.component.annotations.Reference;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import java.io.File;
52  import java.io.FileInputStream;
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.net.URI;
56  import java.net.URISyntaxException;
57  import java.util.ArrayList;
58  import java.util.Collection;
59  import java.util.List;
60  import java.util.function.Function;
61  
62  /**
63   * The <code></code> operation will make sure that media where hls playlists and video track come in separate files
64   * will have appropriately references prior to further processing such as inspection.
65   */
66  @Component(
67      immediate = true,
68      service = WorkflowOperationHandler.class,
69      property = {
70          "service.description=Sanitize Adaptive Workflow Operation Handler",
71          "workflow.operation=sanitize-adaptive"
72      }
73  )
74  public class SanitizeAdaptiveWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
75  
76    /** The logging facility */
77    private static final Logger logger = LoggerFactory.getLogger(SanitizeAdaptiveWorkflowOperationHandler.class);
78    private static final String PLUS = "+";
79    private static final String MINUS = "-";
80  
81    /** The local workspace */
82    private Workspace workspace = null;
83  
84    /**
85     * Callback for declarative services configuration that will introduce us to the local workspace service.
86     * Implementation assumes that the reference is configured as being static.
87     *
88     * @param workspace
89     *          an instance of the workspace
90     */
91    @Reference
92    public void setWorkspace(Workspace workspace) {
93      this.workspace = workspace;
94    }
95  
96    @Reference
97    @Override
98    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
99      super.setServiceRegistry(serviceRegistry);
100   }
101 
102   /**
103    * {@inheritDoc}
104    *
105    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(org.opencastproject.workflow.api.WorkflowInstance,
106    *      JobContext)
107    */
108   @Override
109   public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
110           throws WorkflowOperationException {
111     logger.debug("Running HLS Check workflow operation on workflow {}", workflowInstance.getId());
112     try {
113       return sanitizeHLS(workflowInstance);
114     } catch (Exception e) {
115       throw new WorkflowOperationException(e);
116     }
117   }
118 
119   /**
120    * Checks the references in the playists and make sure that the playlists can pass though an ffmpeg inspection. If the
121    * file references are off, they will be rewritten. The problem is mainly the media package elementID.
122    *
123    * @param wi
124    *          the sanitizeHLS workflow instance
125    * @return the operation result containing the updated mediapackage
126    * @throws EncoderException
127    *           if encoding fails
128    * @throws IOException
129    *           if read/write operations from and to the workspace fail
130    * @throws NotFoundException
131    *           if the workspace does not contain the requested element
132    * @throws URISyntaxException
133    */
134   private WorkflowOperationResult sanitizeHLS(WorkflowInstance wi)
135           throws EncoderException,
136           WorkflowOperationException, NotFoundException, MediaPackageException, IOException, URISyntaxException {
137     MediaPackage src = wi.getMediaPackage();
138     MediaPackage mediaPackage = (MediaPackage) src.clone();
139 
140     WorkflowOperationInstance operation = wi.getCurrentOperation();
141 
142     // Check which tags have been configured
143     ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(wi,
144         Configuration.none, Configuration.one, Configuration.many, Configuration.one);
145 
146     // Read the configuration properties
147     MediaPackageElementFlavor sourceFlavor = tagsAndFlavors.getSingleSrcFlavor();
148     List<String> targetTrackTags = tagsAndFlavors.getTargetTags();
149     MediaPackageElementFlavor targetFlavor = tagsAndFlavors.getSingleTargetFlavor();
150 
151     List<String> removeTags = new ArrayList<String>();
152     List<String> addTags = new ArrayList<String>();
153     List<String> overrideTags = new ArrayList<String>();
154 
155     if (!targetTrackTags.isEmpty()) {
156       for (String tag : targetTrackTags) {
157         if (tag.startsWith(MINUS)) {
158           removeTags.add(tag);
159         } else if (tag.startsWith(PLUS)) {
160           addTags.add(tag);
161         } else {
162           overrideTags.add(tag);
163         }
164       }
165     }
166 
167     // Select those tracks that have matching flavors
168     TrackSelector trackSelector = new TrackSelector();
169     trackSelector.addFlavor(sourceFlavor);
170     Collection<Track> tracks = trackSelector.select(mediaPackage, false);
171     List<Track> tracklist = new ArrayList<>(tracks);
172 
173     // Nothing to sanitize, do not set target tags or flavor on tracks, just return
174     if (!tracklist.stream().filter(AdaptivePlaylist.isHLSTrackPred).findAny().isPresent()) {
175       return createResult(mediaPackage, Action.CONTINUE, 0);
176     }
177     HLSMediaPackageCheck hlstree;
178     try {
179       hlstree = new HLSMediaPackageCheck(tracklist, new Function<URI, File>() {
180         @Override
181         public File apply(URI uri) {
182           try {
183             return workspace.get(uri);
184           } catch (NotFoundException | IOException e1) { // from workspace.get
185             logger.error("Cannot get {} from workspace", uri, e1);
186           }
187           return null;
188         }
189       });
190     } catch (URISyntaxException e1) {
191       throw new MediaPackageException("Cannot process tracks from workspace");
192     }
193     /**
194      * Adds new file to Mediapackage to replace old Track, while retaining all properties. Also sets the target flavor
195      * and target tags
196      */
197     Function2<File, Track, Track> replaceHLSPlaylistInWS = new Function2<File, Track, Track>() {
198       @Override
199       public Track apply(File file, Track track) {
200         try {
201           InputStream inputStream = new FileInputStream(file);
202           // put file into workspace for mp
203           URI uri = workspace.put(mediaPackage.getIdentifier().toString(), track.getIdentifier(), file.getName(),
204                   inputStream);
205           track.setURI(uri); // point track to new URI
206           handleTags(track, targetFlavor, overrideTags, removeTags, addTags); // add tags and flavor
207           return track;
208         } catch (Exception e) {
209           logger.error("Cannot add track file to mediapackage in workspace: {} {} ",
210                   mediaPackage.getIdentifier().toString(),
211                   file);
212           return null;
213         }
214       }
215     };
216     // remove old tracks if the entire operation succeeds, or remove new tracks if any of them fails
217     Function<Track, Void> removeFromWS = new Function<Track, Void>() {
218       @Override
219       public Void apply(Track track) {
220         try {
221           workspace.delete(track.getURI());
222         } catch (NotFoundException e) {
223           logger.error("Cannot delete from workspace: File not found {} ", track);
224         } catch (IOException e) {
225           logger.error("Cannot delete from workspace: IO Error {} ", track);
226         }
227         return null;
228       }
229     };
230     if (hlstree.needsRewriting()) {
231       // rewrites the playlists and replaced the old ones in the mp
232       try {
233         hlstree.rewriteHLS(mediaPackage, replaceHLSPlaylistInWS, removeFromWS);
234       } catch (Exception e) {
235         logger.error("Error: cannot rewrite HLS renditions", e);
236         throw new WorkflowOperationException(e);
237       }
238       for (Track track : tracks) { // Update the flavor and tags for all non HLS segments
239         if (!AdaptivePlaylist.isPlaylist(track.getURI().getPath())) {
240           handleTags(track, targetFlavor, overrideTags, removeTags, addTags);
241           logger.info("Set flavor {} and tags to {} ", track, targetFlavor);
242         }
243       }
244     } else { // change flavor to mark as sanitized
245       for (Track track : tracks) {
246         handleTags(track, targetFlavor, overrideTags, removeTags, addTags);
247         logger.info("Set flavor {} and tags to {} ", track, targetFlavor);
248       }
249     }
250     return createResult(mediaPackage, Action.CONTINUE, 0);
251   }
252 
253   // Add the target tags and flavor
254   private void handleTags(Track track, MediaPackageElementFlavor targetFlavor, List<String> overrideTags,
255           List<String> removeTags, List<String> addTags) {
256     if (targetFlavor != null) {
257       String flavorType = targetFlavor.getType();
258       String flavorSubtype = targetFlavor.getSubtype();
259       if ("*".equals(flavorType))
260         flavorType = track.getFlavor().getType();
261       if ("*".equals(flavorSubtype))
262         flavorSubtype = track.getFlavor().getSubtype();
263       track.setFlavor(new MediaPackageElementFlavor(flavorType, flavorSubtype));
264       logger.debug("Composed track has flavor '{}'", track.getFlavor());
265     }
266     if (overrideTags.size() > 0) {
267       track.clearTags();
268       for (String tag : overrideTags) {
269         logger.trace("Tagging composed track with '{}'", tag);
270         track.addTag(tag);
271       }
272     } else {
273       for (String tag : removeTags) {
274         logger.trace("Remove tagging '{}' from composed track", tag);
275         track.removeTag(tag.substring(MINUS.length()));
276       }
277       for (String tag : addTags) {
278         logger.trace("Add tagging '{}' to composed track", tag);
279         track.addTag(tag.substring(PLUS.length()));
280       }
281     }
282   }
283 }