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 }
134 try (Stream<String> lines = Files.lines(file.toPath())) {
135 return lines.map(masterPatt::matcher).anyMatch(Matcher::find);
136 }
137 }
138
139
140
141
142
143
144
145
146
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
159
160
161
162
163
164
165
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
188
189
190
191
192
193
194
195 static Set<String> getReferencedFiles(File file, boolean segmentsOnly) throws IOException {
196 Set<String> allFiles = new HashSet<String>();
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);
203 }
204 allFiles.addAll(segments);
205
206 for (String f : variants) {
207 try {
208 new URL(f);
209 } catch (MalformedURLException e) {
210
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
220
221
222
223
224 static void setLogicalName(Track track) {
225 track.setLogicalName(FilenameUtils.getName(track.getURI().getPath()));
226 }
227
228
229
230
231
232
233
234
235
236
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()));
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);
252
253 seg.forEach(s -> map.put(s, t));
254 }
255 segments.forEach(t -> {
256 t.referTo(map.get(t.getLogicalName()));
257 });
258 }
259
260
261
262
263
264
265
266
267
268
269
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()) {
274 logger.debug("Move file from " + entry.getKey() + " to " + entry.getValue());
275 if (entry.getValue().exists()) {
276 FileUtils.forceDelete(entry.getValue());
277 }
278 FileUtils.moveFile(entry.getKey(), entry.getValue());
279 }
280 }
281
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);
287 }
288 }
289 return new ArrayList<File>(map.values());
290 }
291
292
293
294
295
296
297
298
299
300
301
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 {
306 FileUtils.moveFile(srcFile, tmpFile);
307 hlsRewriteFileReference(tmpFile, srcFile, mapNames);
308 } catch (IOException e) {
309 throw new IOException("Cannot rewrite " + srcFile + " " + e.getMessage());
310 } finally {
311 FileUtils.deleteQuietly(tmpFile);
312 tmpFile = null;
313 }
314 }
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329 static void hlsRewriteFileReference(File srcFile, File destFile, Map<String, String> mapNames) throws IOException {
330
331
332
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
338
339 if (!line.trim().isEmpty()) {
340 if (line.startsWith("#")) {
341
342 if (line.startsWith("#EXT-X-MAP:") || line.startsWith("#EXT-X-MEDIA:")) {
343 String tmpLine = line;
344 Matcher matcher = uriPatt.matcher(line);
345
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());
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
381
382
383
384
385
386
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
405
406
407
408
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()) {
415 others.forEach(track -> {
416 nameMap.put(track.getLogicalName(), track.getURI().relativize(master.get().getURI()).toString());
417 });
418 }
419 return nameMap;
420 }
421
422
423 class Rep {
424 private Track track;
425 private String 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
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());
441 }
442
443
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
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
489
490
491
492
493
494
495
496
497
498
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
504 FileUtils.moveFile(file, newFile);
505
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);
512 newFile = null;
513 }
514 return file;
515
516 }
517
518
519
520
521
522
523
524
525
526
527
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();
535 }
536
537
538 static File relativizeF(String s1, String s2) throws URISyntaxException {
539 String fp = new File(s1).getParent();
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
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
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
585 for (Track track : tracks) {
586 Rep rep = new Rep(track, mpDir);
587 nameMap.put(track.getLogicalName(), rep);
588 if (track.isMaster()) {
589 master = rep;
590 }
591 if (!rep.isPlaylist) {
592 segment = rep;
593 }
594 }
595 if (segment == null) {
596 throw new MediaPackageException("No playable media segment in mediapackage");
597 }
598
599
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()) {
609 Rep rep = nameMap.get(logName);
610
611 String relPath;
612 if (!segment.origMpuri.equals(rep.origMpuri)) {
613 relPath = relativize(segment.origMpuri, rep.origMpuri);
614 } else {
615 relPath = relativize(master.origMpuri, rep.origMpuri);
616 }
617 newNames.put(logName, relPath);
618 }
619
620 for (String logName : nameMap.keySet()) {
621 Rep rep = nameMap.get(logName);
622 if (rep == master) {
623 continue;
624 }
625 if (!rep.isPlaylist) {
626 newTracks.add(rep.track);
627 continue;
628 }
629 replaceTrackFileInPlace(rep.origMpfile, newNames);
630 rep.newMpuri = rep.origMpuri;
631 newTracks.add(rep.track);
632 }
633
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
643 replaceTrackFileInPlace(master.origMpfile, newNames);
644 master.newMpuri = master.track.getURI();
645 newTracks.add(master.track);
646
647 for (Track track : newTracks) {
648 String newpath = newNames.get(track.getLogicalName());
649 if (newpath != null && track != master) {
650 track.setLogicalName(newpath);
651 }
652 }
653 newNames = null;
654 return newTracks;
655 }
656
657
658
659
660
661
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
673
674
675
676
677
678
679
680
681
682
683
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);
696 }
697 mapTracks(trackRep);
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
706 public boolean needsRewriting() {
707 if (this.playlists.size() == 0) {
708 return false;
709 }
710 for (String s : fileMap.keySet()) {
711 if (!s.equals(fileMap.get(s))) {
712 return true;
713 }
714 }
715 return false;
716 }
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734 public List<Track> rewriteHLS(MediaPackage mp, BiFunction<File, Track, Track> replaceTrackFileInWS,
735 Function<Track, Void> removeFromWS) throws MediaPackageException {
736
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);
742
743
744
745 Function<Rep, Boolean> rewriteTrack = (trackRep) -> {
746 File srcFile = trackRep.origMpfile;
747
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();
759 mp.add(copyTrack);
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 {
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));
774 return true;
775 };
776
777
778 try {
779
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
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 });
796
797 } catch (IOException e) {
798
799 logger.error("Cannot rewrite HLS tracks files:", e);
800 newTracks.forEach(t -> {
801 mp.remove(t);
802 removeFromWS.apply(t);
803 });
804 throw new MediaPackageException("Cannot rewrite HLS tracks files", e);
805
806 } finally {
807 newFiles.forEach(f -> f.delete());
808 }
809 return oldTracks;
810 }
811
812
813
814
815
816
817
818
819
820
821
822
823 private void mapTracks(Rep trackRep) throws IOException, URISyntaxException {
824 Set<String> paths = getVariants(trackRep.origMpfile);
825 for (String path : paths) {
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;
833 } else {
834 logger.warn("Adaptive Playlist referenced track not found in mediapackage");
835 }
836 }
837 }
838 }
839 }