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