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.mediapackage;
23  
24  
25  import org.opencastproject.util.NotFoundException;
26  
27  import org.apache.commons.io.FileUtils;
28  import org.apache.commons.io.FilenameUtils;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import java.io.BufferedReader;
33  import java.io.File;
34  import java.io.FileReader;
35  import java.io.FileWriter;
36  import java.io.IOException;
37  import java.net.MalformedURLException;
38  import java.net.URI;
39  import java.net.URISyntaxException;
40  import java.net.URL;
41  import java.nio.file.Files;
42  import java.nio.file.Path;
43  import java.nio.file.Paths;
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.Collection;
47  import java.util.Comparator;
48  import java.util.HashMap;
49  import java.util.HashSet;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Optional;
53  import java.util.Set;
54  import java.util.UUID;
55  import java.util.function.BiFunction;
56  import java.util.function.Function;
57  import java.util.function.Predicate;
58  import java.util.regex.Matcher;
59  import java.util.regex.Pattern;
60  import java.util.stream.Collectors;
61  import java.util.stream.Stream;
62  
63  /**
64   * HLS-VOD
65   *
66   * This interface describes methods and fields for an adaptive manifest playlist. as defined in
67   * https://tools.ietf.org/html/draft-pantos-http-live-streaming-20 This is text file which references media tracks or
68   * playlists in the same mediapackage using relative path names (usual) or absolute URI. Master Playlist tags MUST NOT
69   * appear in a Media Playlist; Media Segment tag MUST NOT appear in a Master Playlist.
70   */
71  public interface AdaptivePlaylist extends Track {
72  
73    /**
74     * Media package element type.
75     */
76    // String COLLECTION = "AdaptivePlaylist";
77    Logger logger = LoggerFactory.getLogger(AdaptivePlaylist.class);
78  
79    Pattern uriPatt = Pattern.compile("URI=\"([^\"]+)\"");
80    Pattern filePatt = Pattern.compile("([a-zA-Z0-9_.\\-\\/]+)\\.(\\w+)$");
81    // Known tags that references other files include the following - but we only use EXT-X-MAP here
82    // "#EXT-X-MAP:", "#EXT-X-MEDIA:", "#EXT-X-I-FRAME-STREAM-INF:", "#EXT-X-SESSION-DATA:",
83    // Variant tags: see Section 4.4.2 in draft
84    List<String> extVariant = Arrays.asList("#EXT-X-MAP:", "#EXT-X-TARGETDURATION:", "EXTINF", "#EXT-X-BYTERANGE:");
85    // Master tags: see Section 4.4.4
86    List<String> extMaster = Arrays.asList("#EXT-X-MEDIA:", "#EXT-X-STREAM-INF:", "#EXT-X-I-FRAME-STREAM-INF:",
87            "#EXT-X-SESSION-DATA:");
88    Pattern masterPatt = Pattern.compile(String.join("|", extMaster), Pattern.CASE_INSENSITIVE);
89    Pattern variantPatt = Pattern.compile(String.join("|", extVariant), Pattern.CASE_INSENSITIVE);
90    Predicate<File> isHLSFilePred = f -> "m3u8".equalsIgnoreCase(FilenameUtils.getExtension(f.getName()));
91    Predicate<String> isPlaylistPred = f -> "m3u8".equalsIgnoreCase(FilenameUtils.getExtension(f));
92    Predicate<Track> isHLSTrackPred = f -> "m3u8".equalsIgnoreCase(FilenameUtils.getExtension(f.getURI().getPath()));
93  
94    static boolean isPlaylist(String filename) {
95      return filename != null && isPlaylistPred.test(filename);
96    }
97  
98    static boolean isPlaylist(File file) {
99      return file != null && isHLSFilePred.test(file);
100   }
101 
102   static boolean isPlaylist(Track track) {
103     return track != null && isHLSTrackPred.test(track);
104   }
105 
106   // Return true if any elements in a collection is a m3u8 playlist
107   static boolean hasHLSPlaylist(Collection<MediaPackageElement> elements) {
108     return elements.stream().filter(e -> e.getElementType() == MediaPackageElement.Type.Track)
109             .anyMatch(t -> isHLSTrackPred.test((Track) t));
110   }
111 
112   static List<Track> getSortedTracks(List<Track> files, boolean segmentsOnly) {
113     List<Track> fmp4 = files;
114     if (segmentsOnly) {
115       fmp4 = files.stream().filter(isHLSTrackPred.negate()).collect(Collectors.toList());
116     }
117     fmp4.sort(Comparator.comparing(track -> FilenameUtils.getBaseName(track.getURI().getPath())));
118     return fmp4;
119   }
120 
121   /**
122    * Return true if this is a master manifest (contains variants manifest and no media segments)
123    *
124    * @param file
125    *          - media file
126    * @return true if is a master manifest
127    * @throws IOException
128    *           if bad file
129    */
130   static boolean checkForMaster(File file) throws IOException {
131     if (!isPlaylist(file))
132       return false;
133     try (Stream<String> lines = Files.lines(file.toPath())) {
134       return lines.map(masterPatt::matcher).anyMatch(Matcher::find);
135     }
136   }
137 
138   /**
139    * Return true if this is a variant manifest (contains media segments only)
140    *
141    * @param file
142    *          - media file
143    * @return true if is a HLS playlist but not master
144    * @throws IOException
145    *           if bad file - can't access or otherwise
146    */
147   static boolean checkForVariant(File file) throws IOException {
148     if (!isPlaylist(file))
149       return false;
150     try (Stream<String> lines = Files.lines(file.toPath())) {
151       return lines.map(variantPatt::matcher).anyMatch(Matcher::find);
152     }
153   }
154 
155   /**
156    * Given a master or variant playlist/manifest - get referenced files. This does not deal with files referenced by
157    * tags yet.
158    *
159    * @param file
160    *          to parse
161    * @return Set of names referenced
162    * @throws IOException
163    *           if can't access file
164    */
165   static Set<String> getVariants(File file) throws IOException {
166     Set<String> files = new HashSet<String>();
167     try (BufferedReader br = Files.newBufferedReader(file.toPath())) {
168       files = (br.lines().map(l -> {
169         if (!l.startsWith("#")) {
170           Matcher m = filePatt.matcher(l);
171           if (m != null && m.matches())
172             return m.group(0);
173         }
174         return null;
175       }).collect(Collectors.toSet()));
176     } catch (IOException e) {
177       throw new IOException("Cannot read file " + file + e.getMessage());
178     }
179     files.remove(null);
180     return files;
181   }
182 
183   /**
184    * Given a playlist - recursively get all referenced files in the same filesystem with relative links
185    *
186    * @param file
187    *          media file
188    * @return Set of names referenced
189    * @throws IOException
190    *           if can't access file
191    */
192   static Set<String> getReferencedFiles(File file, boolean segmentsOnly) throws IOException {
193     Set<String> allFiles = new HashSet<String>(); // don't include playlist variants
194     Set<String> segments = getVariants(file).stream().filter(isPlaylistPred.negate())
195             .collect(Collectors.toSet());
196     Set<String> variants = getVariants(file).stream().filter(isPlaylistPred).collect(Collectors.toSet());
197 
198     if (!segmentsOnly)
199       allFiles.addAll(variants); // include the playlist
200     allFiles.addAll(segments);
201 
202     for (String f : variants) {
203       try {
204         new URL(f); // ignore external paths
205       } catch (MalformedURLException e) {
206         // is relative path - read the variant playlist
207         String name = FilenameUtils.concat(FilenameUtils.getFullPath(file.getAbsolutePath()), f);
208         allFiles.addAll(getReferencedFiles(new File(name), true));
209       }
210     }
211     return allFiles;
212   }
213 
214   /***
215    * Set the path of the url as the logical name
216    *
217    * @param track
218    *          - tag with name
219    */
220   static void setLogicalName(Track track) {
221     track.setLogicalName(FilenameUtils.getName(track.getURI().getPath()));
222   }
223 
224   /**
225    * Set HLS Tracks references to point to immediate parent, post inspection
226    *
227    * @param tracks
228    *          - all tracks in an HLS adaptive playlist
229    * @param getFileFromURI
230    *          - a way to map uri to file
231    * @throws IOException
232    *           if failed to read files
233    */
234   static void hlsSetReferences(List<Track> tracks, Function<URI, File> getFileFromURI) throws IOException {
235     final Optional<Track> master = tracks.stream().filter(t -> t.isMaster()).findAny();
236     final List<Track> variants = tracks.stream().filter(t -> t.getElementType() == MediaPackageElement.Type.Manifest)
237             .collect(Collectors.toList());
238     final List<Track> segments = tracks.stream().filter(t -> t.getElementType() != MediaPackageElement.Type.Manifest)
239             .collect(Collectors.toList());
240     tracks.forEach(track -> setLogicalName(track));
241     if (master.isPresent())
242       variants.forEach(t -> t.referTo(master.get())); // variants refer to master
243     HashMap<String, Track> map = new HashMap<String, Track>();
244     for (Track t : variants) {
245       File f = getFileFromURI.apply(t.getURI());
246       Set<String> seg = getReferencedFiles(f, true); // Find segment
247       // Should be one only
248       seg.forEach(s -> map.put(s, t));
249     }
250     segments.forEach(t -> { // segments refer to variants
251       t.referTo(map.get(t.getLogicalName()));
252     });
253   }
254 
255   /**
256    * Fix all the playlists locations and references based on a file map from old name to new name.
257    *
258    * @param hlsFiles
259    *          - List of all files in a playlist including playlists
260    * @param map
261    *          - the mapping of the references to the actual file location
262    * @return the fixed files
263    * @throws IOException
264    *           if failed to read files
265    */
266   static List<File> hlsRenameAllFiles(List<File> hlsFiles, Map<File, File> map) throws IOException {
267     for (Map.Entry<File, File> entry : map.entrySet()) {
268       if (entry.getKey().toPath() != entry.getValue().toPath()) { // if different
269         logger.debug("Move file from " + entry.getKey() + " to " + entry.getValue());
270         if (entry.getValue().exists())
271           FileUtils.forceDelete(entry.getValue()); // can redo this
272         FileUtils.moveFile(entry.getKey(), entry.getValue());
273       }
274     }
275     // rename all files to new names if needed
276     HashMap<String, String> nameMap = new HashMap<String, String>();
277     map.forEach((k, v) -> nameMap.put(k.getName(), v.getName()));
278     for (File f : map.values()) {
279       if (isPlaylist(f))
280         hlsRewriteFileReference(f, nameMap); // fix references
281     }
282     return new ArrayList<File>(map.values());
283   }
284 
285 
286   /**
287    * Fix all the HLS file references in a manifest when a referenced file is renamed
288    *
289    * @param srcFile
290    *          - srcFile to be rewritten
291    * @param mapNames
292    *          - mapped from old name to new name
293    * @throws IOException
294    *           if failed
295    */
296   static void hlsRewriteFileReference(File srcFile, Map<String, String> mapNames) throws IOException {
297     File tmpFile = new File(srcFile.getAbsolutePath() + UUID.randomUUID() + ".tmp");
298     try { // rewrite src file
299       FileUtils.moveFile(srcFile, tmpFile);
300       hlsRewriteFileReference(tmpFile, srcFile, mapNames); // fix references
301     } catch (IOException e) {
302       throw new IOException("Cannot rewrite " + srcFile + " " + e.getMessage());
303     } finally {
304       FileUtils.deleteQuietly(tmpFile); // delete temp file
305       tmpFile = null;
306     }
307   }
308 
309   /**
310    * Fix all the HLS file references in a manifest when a referenced file is renamed All the mapped files should be in
311    * the same directory, make sure they are not workspace files (no md5)
312    *
313    * @param srcFile
314    *          - source file to change
315    * @param destFile
316    *          - dest file to hold results
317    * @param mapNames
318    *          - mapping from oldName to newName
319    * @throws IOException
320    *           if failed
321    */
322   static void hlsRewriteFileReference(File srcFile, File destFile, Map<String, String> mapNames) throws IOException {
323     // Many tags reference URIs - not all are dealt with in this code, eg:
324     // "#EXT-X-MAP:", "#EXT-X-MEDIA:", "#EXT-X-I-FRAME-STREAM-INF:", "#EXT-X-SESSION-DATA:", "#EXT-X-KEY:",
325     // "#EXT-X-SESSION-DATA:"
326     try (FileWriter hlsReWriter = new FileWriter(destFile.getAbsoluteFile(), false);
327          BufferedReader br = new BufferedReader(new FileReader(srcFile))) {
328       String line;
329       while ((line = br.readLine()) != null) {
330         // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20
331         // Each line is a URI, blank, or starts with the character #
332         if (!line.trim().isEmpty()) {
333           if (line.startsWith("#")) {
334             // eg: #EXT-X-MAP:URI="39003_segment_0.mp4",BYTERANGE="1325@0"
335             if (line.startsWith("#EXT-X-MAP:") || line.startsWith("#EXT-X-MEDIA:")) {
336               String tmpLine = line;
337               Matcher matcher = uriPatt.matcher(line);
338               // replace iff file is mapped
339               if (matcher.find() && mapNames.containsKey(matcher.group(1))) {
340                 tmpLine = line.replaceFirst(matcher.group(1), mapNames.get(matcher.group(1)));
341               }
342               hlsReWriter.write(tmpLine);
343             } else
344               hlsReWriter.write(line);
345           } else {
346             line = line.trim();
347             String filename = FilenameUtils.getName(line);
348             if (mapNames.containsKey(line)) {
349               hlsReWriter.write(mapNames.get(line));
350             } else if (mapNames.containsKey(filename)) {
351               String newFileName = mapNames.get(FilenameUtils.getName(filename));
352               String newPath = FilenameUtils.getPath(line);
353               if (newPath.isEmpty())
354                 hlsReWriter.write(newFileName);
355               else
356                 hlsReWriter.write(FilenameUtils.concat(newPath, newFileName));
357             } else
358               hlsReWriter.write(line);
359           }
360         }
361         hlsReWriter.write(System.lineSeparator()); // new line
362       }
363     } catch (Exception e) {
364       logger.error("Failed to rewrite hls references " + e.getMessage());
365       throw new IOException(e);
366     }
367   }
368 
369   /**
370    * Return logical name mapped to file
371    *
372    * @param tracks
373    *          from a HLS manifest
374    * @param getFileFromURI
375    *          is a function to get file from an URI
376    * @return names mapped to file
377    */
378   static Map<String, File> logicalNameFileMap(List<Track> tracks, Function<URI, File> getFileFromURI) {
379     Map<String, File> nameMap = tracks.stream().collect(Collectors.<Track, String, File> toMap(
380             track -> track.getLogicalName(), track -> getFileFromURI.apply(track.getURI())));
381     return nameMap;
382   }
383 
384   static Map<String, URI> logicalNameURLMap(List<Track> tracks) {
385     HashMap<String, URI> nameMap = new HashMap<String, URI>();
386     for (Track track : tracks) {
387       nameMap.put(track.getLogicalName(), track.getURI());
388     }
389     return nameMap;
390   }
391 
392   /**
393    *
394    * Return track urls as relative to the master playlist (only one in the list)
395    *
396    * @param tracks
397    *          from an HLS playlist
398    * @return track urls as relative to the master playlist
399    */
400   static HashMap<String, String> urlRelativeToMasterMap(List<Track> tracks) {
401     HashMap<String, String> nameMap = new HashMap<String, String>();
402     Optional<Track> master = tracks.stream().filter(t -> t.isMaster()).findAny();
403     List<Track> others = tracks.stream().filter(t -> !t.isMaster()).collect(Collectors.toList());
404     if (master.isPresent()) // Relativize all the files from the master playlist
405       others.forEach(track -> {
406         nameMap.put(track.getLogicalName(), track.getURI().relativize(master.get().getURI()).toString());
407       });
408     return nameMap;
409   }
410 
411   // Representation of a track/playlist for internal use only
412   class Rep {
413     private Track track;
414     private String name; // reference name
415     private boolean isPlaylist = false;
416     private boolean isMaster = false;
417     private File origMpfile;
418     private String newfileName;
419     private URI origMpuri;
420     private URI newMpuri = null;
421     private String relPath;
422 
423     // Get file relative to the mediapackage directory
424     Rep(Track track, File mpdir) throws NotFoundException, IOException {
425       this.track = track;
426       origMpuri = track.getURI();
427       origMpfile = getFilePath(origMpuri, mpdir);
428       name = FilenameUtils.getName(origMpuri.getPath()).trim();
429       isPlaylist = AdaptivePlaylist.isPlaylist(track.getURI().getPath()); // check suffix
430     }
431 
432     // Get file based on a look up, eg: workspace.get()
433     Rep(Track track, Function<URI, File> getFileFromURI) {
434       this.track = track;
435       origMpuri = track.getURI();
436       origMpfile = getFileFromURI.apply(origMpuri);
437       name = FilenameUtils.getName(origMpfile.getPath());
438       isPlaylist = AdaptivePlaylist.isPlaylist(track.getURI().getPath());
439     }
440 
441     private File getFilePath(URI uri, File mpDir) {
442       String mpid = mpDir.getName();
443       String path = uri.getPath();
444       final Matcher matcher = Pattern.compile(mpid).matcher(path);
445       if (matcher.find()) {
446         return new File(mpDir, path.substring(matcher.end()).trim());
447       }
448       // If there is no mpDir, it may be a relative path
449       return new File(mpDir, path);
450     }
451 
452     public boolean isMaster() {
453       return this.track.isMaster();
454     }
455 
456     public boolean parseForMaster() {
457       try {
458         setMaster(checkForMaster(origMpfile));
459       } catch (IOException e) {
460         logger.error("Cannot open file for check for master:{}", origMpfile);
461       }
462       return isMaster;
463     }
464 
465     public void setMaster(boolean isMaster) {
466       this.isMaster = isMaster;
467       this.track.setMaster(isMaster);
468     }
469 
470     @Override
471     public String toString() {
472       return track.toString();
473     }
474   }
475 
476   /**
477    * Replace the content of a playlist file in place, use in composer only - not in workspace
478    *
479    * @param file
480    *          as playlist
481    * @param map
482    *          - mapping from reference/logical name to new path
483    * @return playlist with changed file names based on the map
484    * @throws IOException
485    *           if can't access file
486    * @throws NotFoundException
487    *           if file not found
488    */
489   static File replaceTrackFileInPlace(File file, Map<String, String> map) throws IOException, NotFoundException {
490     File newFile = new File(file.getAbsolutePath() + UUID.randomUUID() + ".tmp");
491     try {
492       // move old file to tmp
493       FileUtils.moveFile(file, newFile);
494       // Write over old file with fixed references
495       hlsRewriteFileReference(newFile, file, map);
496     } catch (IOException e) {
497       logger.error("Cannot rewrite " + file + ": " + e.getMessage());
498       throw (e);
499     } finally {
500       FileUtils.deleteQuietly(newFile); // not needed anymore
501       newFile = null;
502     }
503     return file;
504 
505   }
506 
507   /**
508    * Find relative path to referee URL if a link is in the referer page
509    *
510    * @param referer
511    *          - pointer to file
512    * @param referee
513    *          - pointee
514    * @return referee path as a relative path from referer URL
515    * @throws URISyntaxException
516    *           if bad URI
517    */
518 
519   static String relativize(URI referer, URI referee) throws URISyntaxException {
520     URI u1 = referer.normalize();
521     URI u2 = referee.normalize();
522     File f = relativizeF(u1.getPath(), u2.getPath());
523     return f.getPath(); // relative name to use in manifest
524   }
525 
526   // They should all be relative paths at this point in the working file repo
527   static File relativizeF(String s1, String s2) throws URISyntaxException {
528     String fp = new File(s1).getParent(); // get s1 folder
529     Path p2 = Paths.get(s2);
530     if (fp != null) {
531       Path p1 = Paths.get(fp);
532       try {
533         Path rp = p1.relativize(p2);
534         return rp.toFile();
535       } catch (IllegalArgumentException e) {
536         logger.info("Not a relative path " + p1 + " to " + p2);
537         return p2.toFile();
538       }
539     } else
540       return p2.toFile();
541   }
542 
543   /**
544    * Fix the playlist references in a publication. The playlist files are replaced in place using relative link instead
545    * of the filename
546    *
547    * @param tracks
548    *          - tracks that represent a HLS playlist
549    * @param mpDir
550    *          - distribution media package file directory which represents the file storage of the URI used in the
551    *          tracks
552    * @return the tracks with the files updated
553    * @throws MediaPackageException
554    *           if files do not conform to HLS spec.
555    * @throws NotFoundException
556    *           if files are missing
557    * @throws IOException
558    *           if can't read
559    * @throws URISyntaxException
560    *           if bad URI
561    */
562   static List<Track> fixReferences(List<Track> tracks, File mpDir)
563           throws MediaPackageException, NotFoundException, IOException, URISyntaxException {
564     HashMap<String, Rep> nameMap = new HashMap<String, Rep>();
565     Rep master = null;
566     Rep segment = null;
567     List<Track> newTracks = new ArrayList<Track>();
568     if (tracks.size() < 2) {
569       logger.debug("At least 2 files in an HLS distribution");
570       throw new MediaPackageException("Not enough files in a playlist");
571     }
572     // map logical name to track representation
573     for (Track track : tracks) {
574       Rep rep = new Rep(track, mpDir);
575       nameMap.put(track.getLogicalName(), rep); // add all to nameMap
576       if (track.isMaster())
577         master = rep; // track.getLogicalname();
578       if (!rep.isPlaylist)
579         segment = rep; // find any segment
580     }
581     if (segment == null) { // must have at least one segment
582       throw new MediaPackageException("No playable media segment in mediapackage");
583 
584     }
585     // Try to find master or use any playlist, if not found, throw exception
586     Optional<Rep> oprep = nameMap.values().stream().filter(r -> r.parseForMaster()).findFirst();
587     if (!oprep.isPresent())
588       oprep = nameMap.values().parallelStream().filter(r -> r.isPlaylist).findFirst();
589     oprep.orElseThrow(() -> new MediaPackageException("No playlist found, not HLS distribution"));
590     master = oprep.get();
591 
592     HashMap<String, String> newNames = new HashMap<String, String>();
593     for (String logName : nameMap.keySet()) { // map original name
594       Rep rep = nameMap.get(logName);
595       // segments are fixed, fix variant references to segments based on segments
596       String relPath;
597       if (!segment.origMpuri.equals(rep.origMpuri)) { // not itself
598         relPath = relativize(segment.origMpuri, rep.origMpuri);
599       } else { // only element id is different
600         relPath = relativize(master.origMpuri, rep.origMpuri);
601       }
602       newNames.put(logName, relPath);
603     }
604     // on variant playlists, rewrite references to segments
605     for (String logName : nameMap.keySet()) {
606       Rep rep = nameMap.get(logName);
607       if (rep == master) // deal with master later
608         continue;
609       if (!rep.isPlaylist) {
610         newTracks.add(rep.track); // segments are unchanged
611         continue;
612       }
613       replaceTrackFileInPlace(rep.origMpfile, newNames);
614       rep.newMpuri = rep.origMpuri;
615       newTracks.add(rep.track); // add changed variants
616     }
617     // remap logical name to the new id for the variant files from above
618     for (String logName : nameMap.keySet()) {
619       Rep rep = nameMap.get(logName);
620       if (!rep.isPlaylist || rep == master)
621         continue;
622       String relPath = relativize(segment.origMpuri, rep.newMpuri);
623       newNames.put(logName, relPath);
624     }
625     // on master, fix references to variant playlists
626     replaceTrackFileInPlace(master.origMpfile, newNames);
627     master.newMpuri = master.track.getURI();
628     newTracks.add(master.track);
629     // Update the logical names to keep referential integrity
630     for (Track track : newTracks) {
631       String newpath = newNames.get(track.getLogicalName());
632       if (newpath != null && track != master) // no file refers to master
633         track.setLogicalName(newpath);
634     }
635     newNames = null;
636     return newTracks;
637   }
638 
639   /**
640    * Fix HLS playlists/media already in the workspace as the result of an ingest This builds the hierarchies of a HLS
641    * playlist with masters as the roots. This is useful if mixed files are ingested into a mediapackage. HLS files with
642    * relative links will fail in an inspection unless the relative paths are fixed. Logical names should be preserved if
643    * they exists.
644    */
645   class HLSMediaPackageCheck {
646     private HashMap<String, String> fileMap = new HashMap<String, String>();
647     private HashMap<String, Rep> repMap = new HashMap<String, Rep>();;
648     private List<Rep> reps;
649     private List<Rep> playlists;
650     private List<Rep> segments;
651     private List<Rep> masters = new ArrayList<Rep>();
652 
653     /**
654      * Builds a map of files in the mediapackage so that it can be analyzed and fixed if needed
655      *
656      * @param tracks
657      *          - list of tracks from a media package
658      * @param getFileFromURI
659      *          - a function to get files from the media package by URI
660      * @throws IOException
661      *           if can't read file
662      * @throws URISyntaxException
663      *           if bad URI
664      * @throws MediaPackageException
665      *           - if mediapackage is incomplete and missing segments
666      */
667     public HLSMediaPackageCheck(List<Track> tracks, Function<URI, File> getFileFromURI)
668             throws IOException, MediaPackageException, URISyntaxException {
669       this.reps = tracks.stream().map(t -> new Rep(t, getFileFromURI)).collect(Collectors.toList());
670       for (Rep rep : reps)
671         repMap.put(rep.name, rep);
672       this.playlists = reps.stream().filter(r -> r.isPlaylist).collect(Collectors.toList());
673       for (Rep trackRep : playlists) {
674         if (checkForMaster(trackRep.origMpfile)) {
675           this.masters.add(trackRep);
676           trackRep.setMaster(true); // Track.master is set by inspection
677         }
678         mapTracks(trackRep); // find relationships of playlist segments
679       }
680       this.segments = reps.stream().filter(r -> !r.isPlaylist).collect(Collectors.toList());
681       if (this.segments.size() < 1)
682         throw new MediaPackageException("No media segments");
683     }
684 
685     // File references need to be fixed
686     public boolean needsRewriting() {
687       if (this.playlists.size() == 0) // not HLS
688         return false;
689       for (String s : fileMap.keySet()) { // paths are already corrected
690         if (!s.equals(fileMap.get(s)))
691           return true;
692       }
693       return false;
694     }
695 
696     /**
697      * Rewrite the playlist file from master on down, this has to be done in multiple steps because the act of putting a
698      * file into a collection changes the path and new path is not known in advance. The two functions are passed in to
699      * this function to manage the tracks in its storage
700      *
701      * @param mp
702      *          to be rewrittem
703      * @param replaceTrackFileInWS
704      *          A function that creates a new track with the file using the metadata in the track, returning a new track
705      *          in the media package.
706      * @param removeFromWS
707      *          A function that removes() the track from the media package in the workspace
708      * @return old tracks that are removed from the media package
709      * @throws MediaPackageException
710      *           if bad mp
711      */
712     public List<Track> rewriteHLS(MediaPackage mp, BiFunction<File, Track, Track> replaceTrackFileInWS,
713             Function<Track, Void> removeFromWS) throws MediaPackageException {
714       /* rewrite variants first, * segments are unchanged */
715       List<Rep> variants = playlists.stream().filter(i -> !masters.contains(i)).collect(Collectors.toList());
716       List<File> newFiles = new ArrayList<File>();
717       List<Track> oldTracks = new ArrayList<Track>();
718       List<Track> newTracks = new ArrayList<Track>();
719       Rep rep = segments.get(0); // use segment dir as temp space
720 
721 
722       // Lambda to rewrite a track using the passed in functions, using closure
723       Function<Rep, Boolean> rewriteTrack = (trackRep) -> {
724         File srcFile = trackRep.origMpfile;
725         // Use first segment's folder as temp space
726         File destFile = new File(rep.origMpfile.getAbsoluteFile().getParent(),
727                 FilenameUtils.getName(srcFile.getName()));
728         try {
729           hlsRewriteFileReference(srcFile, destFile, fileMap);
730         } catch (IOException e) {
731           logger.error("HLS Rewrite {} to {} failed", srcFile, destFile);
732           return false;
733         }
734         newFiles.add(destFile);
735         oldTracks.add(trackRep.track);
736         Track copyTrack = (Track) trackRep.track.clone(); // get all the properties, id, etc
737         mp.add(copyTrack); // add to mp and get new elementID
738         Track newTrack = replaceTrackFileInWS.apply(destFile, copyTrack);
739         if (newTrack == null) {
740           logger.error("Cannot add HLS track tp MP: {}", trackRep.track);
741           return false;
742         }
743         newTracks.add(newTrack);
744 
745         try { // Keep track of the new file's relative URI
746           fileMap.put(trackRep.relPath, relativize(rep.origMpuri, newTrack.getURI()));
747         } catch (URISyntaxException e) {
748           logger.error("Cannot rewrite relativize track name: {}", trackRep.track);
749           return false;
750         }
751         newTrack.setLogicalName(fileMap.get(trackRep.name)); // set logical name for publication
752         return true;
753       };
754 
755 
756       try {
757         // Rewrite the variants and masters tracks in order and throw exception if there are any failures
758         if (!(variants.stream().map(t -> rewriteTrack.apply(t)).allMatch(Boolean::valueOf)
759                 && masters.stream().map(t -> rewriteTrack.apply(t)).allMatch(Boolean::valueOf)))
760           throw new IOException("Cannot rewrite track");
761 
762         // if segments are referenced by variant - set the logical name used
763         for (Rep segment : segments) {
764           if (fileMap.containsValue(segment.newfileName)) {
765             segment.track.setLogicalName(segment.newfileName);
766           }
767         }
768 
769         oldTracks.forEach(t -> {
770           mp.remove(t);
771           removeFromWS.apply(t);
772         }); // remove old tracks if successful
773 
774       } catch (IOException /* | URISyntaxException */ e) {
775 
776         logger.error("Cannot rewrite HLS tracks files:", e);
777         newTracks.forEach(t -> {
778           mp.remove(t);
779           removeFromWS.apply(t);
780         }); // remove new Tracks if any of them failed
781         throw new MediaPackageException("Cannot rewrite HLS tracks files", e);
782 
783       } finally {
784         newFiles.forEach(f -> f.delete()); // temp files not needed anymore
785       }
786       return oldTracks;
787     }
788 
789     /**
790      * Look for track by filename, assuming that all the variant playlists and segments are uniquely named. It is
791      * possible that someone ingests a set of published playlists so the paths are nested. Then referenced names are
792      * mapped to tracks.
793      *
794      * @param trackRep
795      *          - playlist to examine
796      * @throws IOException
797      *           - bad files
798      * @throws URISyntaxException
799      */
800     private void mapTracks(Rep trackRep) throws IOException, URISyntaxException {
801       Set<String> paths = getVariants(trackRep.origMpfile); // Get all tracks it points to by path
802       for (String path : paths) { // Check each file name
803         String name = FilenameUtils.getName(path);
804         if (repMap.containsKey(name)) {
805           Rep rep = repMap.get(name);
806           rep.newMpuri = trackRep.track.getURI().relativize(rep.origMpuri);
807           rep.newfileName = relativize(trackRep.origMpuri, rep.origMpuri);
808           fileMap.put(path, rep.newfileName);
809           rep.relPath = path; // for reverse lookup
810         } else {
811           logger.warn("Adaptive Playlist referenced track not found in mediapackage");
812         }
813       }
814     }
815   }
816 }