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.workflow.api.AbstractWorkflowOperationHandler;
36  import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
37  import org.opencastproject.workflow.api.WorkflowInstance;
38  import org.opencastproject.workflow.api.WorkflowOperationException;
39  import org.opencastproject.workflow.api.WorkflowOperationHandler;
40  import org.opencastproject.workflow.api.WorkflowOperationInstance;
41  import org.opencastproject.workflow.api.WorkflowOperationResult;
42  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
43  import org.opencastproject.workspace.api.Workspace;
44  
45  import org.osgi.service.component.annotations.Component;
46  import org.osgi.service.component.annotations.Reference;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import java.io.File;
51  import java.io.FileInputStream;
52  import java.io.IOException;
53  import java.io.InputStream;
54  import java.net.URI;
55  import java.net.URISyntaxException;
56  import java.util.ArrayList;
57  import java.util.Collection;
58  import java.util.List;
59  import java.util.function.BiFunction;
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     ConfiguredTagsAndFlavors.TargetTags targetTrackTags = tagsAndFlavors.getTargetTags();
149     MediaPackageElementFlavor targetFlavor = tagsAndFlavors.getSingleTargetFlavor();
150 
151     // Select those tracks that have matching flavors
152     TrackSelector trackSelector = new TrackSelector();
153     trackSelector.addFlavor(sourceFlavor);
154     Collection<Track> tracks = trackSelector.select(mediaPackage, false);
155     List<Track> tracklist = new ArrayList<>(tracks);
156 
157     // Nothing to sanitize, do not set target tags or flavor on tracks, just return
158     if (!tracklist.stream().filter(AdaptivePlaylist.isHLSTrackPred).findAny().isPresent()) {
159       return createResult(mediaPackage, Action.CONTINUE, 0);
160     }
161     HLSMediaPackageCheck hlstree;
162     try {
163       hlstree = new HLSMediaPackageCheck(tracklist, new Function<URI, File>() {
164         @Override
165         public File apply(URI uri) {
166           try {
167             return workspace.get(uri);
168           } catch (NotFoundException | IOException e1) { // from workspace.get
169             logger.error("Cannot get {} from workspace", uri, e1);
170           }
171           return null;
172         }
173       });
174     } catch (URISyntaxException e1) {
175       throw new MediaPackageException("Cannot process tracks from workspace");
176     }
177     /**
178      * Adds new file to Mediapackage to replace old Track, while retaining all properties. Also sets the target flavor
179      * and target tags
180      */
181     BiFunction<File, Track, Track> replaceHLSPlaylistInWS = (file, track) -> {
182       try (InputStream inputStream = new FileInputStream(file)) {
183         // put file into workspace for mp
184         URI uri = workspace.put(mediaPackage.getIdentifier().toString(), track.getIdentifier(), file.getName(),
185             inputStream);
186         track.setURI(uri); // point track to new URI
187         handleTags(track, targetFlavor, targetTrackTags); // add tags and flavor
188         return track;
189       } catch (Exception e) {
190         logger.error("Cannot add track file to mediapackage in workspace: {} {} ",
191             mediaPackage.getIdentifier().toString(),
192             file);
193         return null;
194       }
195     };
196     // remove old tracks if the entire operation succeeds, or remove new tracks if any of them fails
197     Function<Track, Void> removeFromWS = new Function<Track, Void>() {
198       @Override
199       public Void apply(Track track) {
200         try {
201           workspace.delete(track.getURI());
202         } catch (NotFoundException e) {
203           logger.error("Cannot delete from workspace: File not found {} ", track);
204         } catch (IOException e) {
205           logger.error("Cannot delete from workspace: IO Error {} ", track);
206         }
207         return null;
208       }
209     };
210     if (hlstree.needsRewriting()) {
211       // rewrites the playlists and replaced the old ones in the mp
212       try {
213         hlstree.rewriteHLS(mediaPackage, replaceHLSPlaylistInWS, removeFromWS);
214       } catch (Exception e) {
215         logger.error("Error: cannot rewrite HLS renditions", e);
216         throw new WorkflowOperationException(e);
217       }
218       for (Track track : tracks) { // Update the flavor and tags for all non HLS segments
219         if (!AdaptivePlaylist.isPlaylist(track.getURI().getPath())) {
220           handleTags(track, targetFlavor, targetTrackTags);
221           logger.info("Set flavor {} and tags to {} ", track, targetFlavor);
222         }
223       }
224     } else { // change flavor to mark as sanitized
225       for (Track track : tracks) {
226         handleTags(track, targetFlavor, targetTrackTags);
227         logger.info("Set flavor {} and tags to {} ", track, targetFlavor);
228       }
229     }
230     return createResult(mediaPackage, Action.CONTINUE, 0);
231   }
232 
233   // Add the target tags and flavor
234   private void handleTags(Track track, MediaPackageElementFlavor targetFlavor,
235       ConfiguredTagsAndFlavors.TargetTags targetTags) {
236     if (targetFlavor != null) {
237       String flavorType = targetFlavor.getType();
238       String flavorSubtype = targetFlavor.getSubtype();
239       if ("*".equals(flavorType))
240         flavorType = track.getFlavor().getType();
241       if ("*".equals(flavorSubtype))
242         flavorSubtype = track.getFlavor().getSubtype();
243       track.setFlavor(new MediaPackageElementFlavor(flavorType, flavorSubtype));
244       logger.debug("Composed track has flavor '{}'", track.getFlavor());
245     }
246     applyTargetTagsToElement(targetTags, track);
247   }
248 }