1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
65
66
67
68
69
70
71 public interface AdaptivePlaylist extends Track {
72
73
74
75
76
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
82
83
84 List<String> extVariant = Arrays.asList("#EXT-X-MAP:", "#EXT-X-TARGETDURATION:", "EXTINF", "#EXT-X-BYTERANGE:");
85
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
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
123
124
125
126
127
128
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
140
141
142
143
144
145
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
157
158
159
160
161
162
163
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
185
186
187
188
189
190
191
192 static Set<String> getReferencedFiles(File file, boolean segmentsOnly) throws IOException {
193 Set<String> allFiles = new HashSet<String>();
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);
200 allFiles.addAll(segments);
201
202 for (String f : variants) {
203 try {
204 new URL(f);
205 } catch (MalformedURLException e) {
206
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
216
217
218
219
220 static void setLogicalName(Track track) {
221 track.setLogicalName(FilenameUtils.getName(track.getURI().getPath()));
222 }
223
224
225
226
227
228
229
230
231
232
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()));
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);
247
248 seg.forEach(s -> map.put(s, t));
249 }
250 segments.forEach(t -> {
251 t.referTo(map.get(t.getLogicalName()));
252 });
253 }
254
255
256
257
258
259
260
261
262
263
264
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()) {
269 logger.debug("Move file from " + entry.getKey() + " to " + entry.getValue());
270 if (entry.getValue().exists())
271 FileUtils.forceDelete(entry.getValue());
272 FileUtils.moveFile(entry.getKey(), entry.getValue());
273 }
274 }
275
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);
281 }
282 return new ArrayList<File>(map.values());
283 }
284
285
286
287
288
289
290
291
292
293
294
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 {
299 FileUtils.moveFile(srcFile, tmpFile);
300 hlsRewriteFileReference(tmpFile, srcFile, mapNames);
301 } catch (IOException e) {
302 throw new IOException("Cannot rewrite " + srcFile + " " + e.getMessage());
303 } finally {
304 FileUtils.deleteQuietly(tmpFile);
305 tmpFile = null;
306 }
307 }
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322 static void hlsRewriteFileReference(File srcFile, File destFile, Map<String, String> mapNames) throws IOException {
323
324
325
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
331
332 if (!line.trim().isEmpty()) {
333 if (line.startsWith("#")) {
334
335 if (line.startsWith("#EXT-X-MAP:") || line.startsWith("#EXT-X-MEDIA:")) {
336 String tmpLine = line;
337 Matcher matcher = uriPatt.matcher(line);
338
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());
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
371
372
373
374
375
376
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
395
396
397
398
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())
405 others.forEach(track -> {
406 nameMap.put(track.getLogicalName(), track.getURI().relativize(master.get().getURI()).toString());
407 });
408 return nameMap;
409 }
410
411
412 class Rep {
413 private Track track;
414 private String 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
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());
430 }
431
432
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
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
478
479
480
481
482
483
484
485
486
487
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
493 FileUtils.moveFile(file, newFile);
494
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);
501 newFile = null;
502 }
503 return file;
504
505 }
506
507
508
509
510
511
512
513
514
515
516
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();
524 }
525
526
527 static File relativizeF(String s1, String s2) throws URISyntaxException {
528 String fp = new File(s1).getParent();
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
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
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
573 for (Track track : tracks) {
574 Rep rep = new Rep(track, mpDir);
575 nameMap.put(track.getLogicalName(), rep);
576 if (track.isMaster())
577 master = rep;
578 if (!rep.isPlaylist)
579 segment = rep;
580 }
581 if (segment == null) {
582 throw new MediaPackageException("No playable media segment in mediapackage");
583
584 }
585
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()) {
594 Rep rep = nameMap.get(logName);
595
596 String relPath;
597 if (!segment.origMpuri.equals(rep.origMpuri)) {
598 relPath = relativize(segment.origMpuri, rep.origMpuri);
599 } else {
600 relPath = relativize(master.origMpuri, rep.origMpuri);
601 }
602 newNames.put(logName, relPath);
603 }
604
605 for (String logName : nameMap.keySet()) {
606 Rep rep = nameMap.get(logName);
607 if (rep == master)
608 continue;
609 if (!rep.isPlaylist) {
610 newTracks.add(rep.track);
611 continue;
612 }
613 replaceTrackFileInPlace(rep.origMpfile, newNames);
614 rep.newMpuri = rep.origMpuri;
615 newTracks.add(rep.track);
616 }
617
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
626 replaceTrackFileInPlace(master.origMpfile, newNames);
627 master.newMpuri = master.track.getURI();
628 newTracks.add(master.track);
629
630 for (Track track : newTracks) {
631 String newpath = newNames.get(track.getLogicalName());
632 if (newpath != null && track != master)
633 track.setLogicalName(newpath);
634 }
635 newNames = null;
636 return newTracks;
637 }
638
639
640
641
642
643
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
655
656
657
658
659
660
661
662
663
664
665
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);
677 }
678 mapTracks(trackRep);
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
686 public boolean needsRewriting() {
687 if (this.playlists.size() == 0)
688 return false;
689 for (String s : fileMap.keySet()) {
690 if (!s.equals(fileMap.get(s)))
691 return true;
692 }
693 return false;
694 }
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712 public List<Track> rewriteHLS(MediaPackage mp, BiFunction<File, Track, Track> replaceTrackFileInWS,
713 Function<Track, Void> removeFromWS) throws MediaPackageException {
714
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);
720
721
722
723 Function<Rep, Boolean> rewriteTrack = (trackRep) -> {
724 File srcFile = trackRep.origMpfile;
725
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();
737 mp.add(copyTrack);
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 {
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));
752 return true;
753 };
754
755
756 try {
757
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
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 });
773
774 } catch (IOException e) {
775
776 logger.error("Cannot rewrite HLS tracks files:", e);
777 newTracks.forEach(t -> {
778 mp.remove(t);
779 removeFromWS.apply(t);
780 });
781 throw new MediaPackageException("Cannot rewrite HLS tracks files", e);
782
783 } finally {
784 newFiles.forEach(f -> f.delete());
785 }
786 return oldTracks;
787 }
788
789
790
791
792
793
794
795
796
797
798
799
800 private void mapTracks(Rep trackRep) throws IOException, URISyntaxException {
801 Set<String> paths = getVariants(trackRep.origMpfile);
802 for (String path : paths) {
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;
810 } else {
811 logger.warn("Adaptive Playlist referenced track not found in mediapackage");
812 }
813 }
814 }
815 }
816 }