1 /* 2 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package com.sun.media.jfxmedia.locator; 26 27 import com.sun.media.jfxmedia.MediaError; 28 import com.sun.media.jfxmediaimpl.MediaUtils; 29 import java.io.BufferedReader; 30 import java.io.IOException; 31 import java.io.InputStreamReader; 32 import java.net.*; 33 import java.nio.channels.Channels; 34 import java.nio.channels.ReadableByteChannel; 35 import java.nio.charset.Charset; 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.concurrent.BlockingQueue; 39 import java.util.concurrent.CountDownLatch; 40 import java.util.concurrent.LinkedBlockingQueue; 41 import java.util.concurrent.Semaphore; 42 43 final class HLSConnectionHolder extends ConnectionHolder { 44 45 private URLConnection urlConnection = null; 46 private PlaylistThread playlistThread = new PlaylistThread(); 47 private VariantPlaylist variantPlaylist = null; 48 private Playlist currentPlaylist = null; 49 private int mediaFileIndex = -1; 50 private CountDownLatch readySignal = new CountDownLatch(1); 51 private Semaphore liveSemaphore = new Semaphore(0); 52 private boolean isPlaylistClosed = false; 53 private boolean isBitrateAdjustable = false; 54 private long startTime = -1; 55 private static final long HLS_VALUE_FLOAT_MULTIPLIER = 1000; 56 private static final int HLS_PROP_GET_DURATION = 1; 57 private static final int HLS_PROP_GET_HLS_MODE = 2; 58 private static final int HLS_PROP_GET_MIMETYPE = 3; 59 private static final int HLS_VALUE_MIMETYPE_MP2T = 1; 60 private static final int HLS_VALUE_MIMETYPE_MP3 = 2; 61 private static final String CHARSET_UTF_8 = "UTF-8"; 62 private static final String CHARSET_US_ASCII = "US-ASCII"; 63 64 HLSConnectionHolder(URI uri) throws IOException { 65 playlistThread.setPlaylistURI(uri); 66 init(); 67 } 68 69 private void init() { 70 playlistThread.putState(PlaylistThread.STATE_INIT); 71 playlistThread.start(); 72 } 73 74 @Override 75 public int readNextBlock() throws IOException { 76 if (isBitrateAdjustable && startTime == -1) { 77 startTime = System.currentTimeMillis(); 78 } 79 80 int read = super.readNextBlock(); 81 if (isBitrateAdjustable && read == -1) { 82 long readTime = System.currentTimeMillis() - startTime; 83 startTime = -1; 84 adjustBitrate(readTime); 85 } 86 87 return read; 88 } 89 90 int readBlock(long position, int size) throws IOException { 91 throw new IOException(); 92 } 93 94 boolean needBuffer() { 95 return true; 96 } 97 98 boolean isSeekable() { 99 return true; 100 } 101 102 boolean isRandomAccess() { 103 return false; // Only by segments 104 } 105 106 public long seek(long position) { 107 try { 108 readySignal.await(); 109 } catch (Exception e) { 110 return -1; 111 } 112 113 return (long) (currentPlaylist.seek(position) * HLS_VALUE_FLOAT_MULTIPLIER); 114 } 115 116 @Override 117 public void closeConnection() { 118 currentPlaylist.close(); 119 super.closeConnection(); 120 resetConnection(); 121 playlistThread.putState(PlaylistThread.STATE_EXIT); 122 } 123 124 @Override 125 int property(int prop, int value) { 126 try { 127 readySignal.await(); 128 } catch (Exception e) { 129 return -1; 130 } 131 132 if (prop == HLS_PROP_GET_DURATION) { 133 return (int) (currentPlaylist.getDuration() * HLS_VALUE_FLOAT_MULTIPLIER); 134 } else if (prop == HLS_PROP_GET_HLS_MODE) { 135 return 1; 136 } else if (prop == HLS_PROP_GET_MIMETYPE) { 137 return currentPlaylist.getMimeType(); 138 } 139 140 return -1; 141 } 142 143 @Override 144 int getStreamSize() { 145 try { 146 readySignal.await(); 147 } catch (Exception e) { 148 return -1; 149 } 150 151 return loadNextSegment(); 152 } 153 154 private void resetConnection() { 155 super.closeConnection(); 156 157 Locator.closeConnection(urlConnection); 158 urlConnection = null; 159 } 160 161 // Returns -1 EOS or critical error 162 // Returns positive size of segment if no isssues. 163 // Returns negative size of segment if discontinuity. 164 private int loadNextSegment() { 165 resetConnection(); 166 167 String mediaFile = currentPlaylist.getNextMediaFile(); 168 if (mediaFile == null) { 169 return -1; 170 } 171 172 try { 173 URI uri = new URI(mediaFile); 174 urlConnection = uri.toURL().openConnection(); 175 channel = openChannel(); 176 } catch (Exception e) { 177 return -1; 178 } 179 180 if (currentPlaylist.isCurrentMediaFileDiscontinuity()) { 181 return (-1 * urlConnection.getContentLength()); 182 } else { 183 return urlConnection.getContentLength(); 184 } 185 } 186 187 private ReadableByteChannel openChannel() throws IOException { 188 return Channels.newChannel(urlConnection.getInputStream()); 189 } 190 191 private void adjustBitrate(long readTime) { 192 int avgBitrate = (int)(((long) urlConnection.getContentLength() * 8 * 1000) / readTime); 193 194 Playlist playlist = variantPlaylist.getPlaylistBasedOnBitrate(avgBitrate); 195 if (playlist != null && playlist != currentPlaylist) { 196 if (currentPlaylist.isLive()) { 197 playlist.update(currentPlaylist.getNextMediaFile()); 198 playlistThread.setReloadPlaylist(playlist); 199 } 200 201 playlist.setForceDiscontinuity(true); 202 currentPlaylist = playlist; 203 } 204 } 205 206 private static String stripParameters(String mediaFile) { 207 int qp = mediaFile.indexOf('?'); 208 if (qp > 0) { 209 mediaFile = mediaFile.substring(0, qp); // Strip all possible http parameters. 210 } 211 return mediaFile; 212 } 213 214 private class PlaylistThread extends Thread { 215 216 public static final int STATE_INIT = 0; 217 public static final int STATE_EXIT = 1; 218 public static final int STATE_RELOAD_PLAYLIST = 2; 219 private BlockingQueue<Integer> stateQueue = new LinkedBlockingQueue<Integer>(); 220 private URI playlistURI = null; 221 private Playlist reloadPlaylist = null; 222 private final Object reloadLock = new Object(); 223 private volatile boolean stopped = false; 224 225 private PlaylistThread() { 226 setName("JFXMedia HLS Playlist Thread"); 227 setDaemon(true); 228 } 229 230 private void setPlaylistURI(URI playlistURI) { 231 this.playlistURI = playlistURI; 232 } 233 234 private void setReloadPlaylist(Playlist playlist) { 235 synchronized(reloadLock) { 236 reloadPlaylist = playlist; 237 } 238 } 239 240 @Override 241 public void run() { 242 while (!stopped) { 243 try { 244 int state = stateQueue.take(); 245 switch (state) { 246 case STATE_INIT: 247 stateInit(); 248 break; 249 case STATE_EXIT: 250 stopped = true; 251 break; 252 case STATE_RELOAD_PLAYLIST: 253 stateReloadPlaylist(); 254 break; 255 default: 256 break; 257 } 258 } catch (Exception e) { 259 } 260 } 261 } 262 263 private void putState(int state) { 264 if (stateQueue != null) { 265 try { 266 stateQueue.put(state); 267 } catch (InterruptedException ex) { 268 } 269 } 270 } 271 272 private void stateInit() { 273 if (playlistURI == null) { 274 return; 275 } 276 277 PlaylistParser parser = new PlaylistParser(); 278 parser.load(playlistURI); 279 280 if (parser.isVariantPlaylist()) { 281 variantPlaylist = new VariantPlaylist(playlistURI); 282 283 while (parser.hasNext()) { 284 variantPlaylist.addPlaylistInfo(parser.getString(), parser.getInteger()); 285 } 286 } else { 287 if (currentPlaylist == null) { 288 currentPlaylist = new Playlist(parser.isLivePlaylist(), parser.getTargetDuration()); 289 currentPlaylist.setPlaylistURI(playlistURI); 290 } 291 292 if (currentPlaylist.setSequenceNumber(parser.getSequenceNumber())) { 293 while (parser.hasNext()) { 294 currentPlaylist.addMediaFile(parser.getString(), parser.getDouble(), parser.getBoolean()); 295 } 296 } 297 298 if (variantPlaylist != null) { 299 variantPlaylist.addPlaylist(currentPlaylist); 300 } 301 } 302 303 // Update variant playlists 304 if (variantPlaylist != null) { 305 while (variantPlaylist.hasNext()) { 306 try { 307 currentPlaylist = new Playlist(variantPlaylist.getPlaylistURI()); 308 currentPlaylist.update(null); 309 variantPlaylist.addPlaylist(currentPlaylist); 310 } catch (URISyntaxException e) { 311 } catch (MalformedURLException e) { 312 } 313 } 314 } 315 316 // Always start with first data playlist 317 if (variantPlaylist != null) { 318 currentPlaylist = variantPlaylist.getPlaylist(0); 319 isBitrateAdjustable = true; 320 } 321 322 // Start reloading live playlist 323 if (currentPlaylist.isLive()) { 324 setReloadPlaylist(currentPlaylist); 325 putState(STATE_RELOAD_PLAYLIST); 326 } 327 328 readySignal.countDown(); 329 } 330 331 private void stateReloadPlaylist() { 332 try { 333 long timeout; 334 synchronized(reloadLock) { 335 timeout = reloadPlaylist.getTargetDuration() / 2; 336 } 337 Thread.sleep(timeout); 338 } catch (InterruptedException ex) { 339 return; 340 } 341 342 synchronized(reloadLock) { 343 reloadPlaylist.update(null); 344 } 345 346 putState(STATE_RELOAD_PLAYLIST); 347 } 348 } 349 350 private static class PlaylistParser { 351 352 private boolean isFirstLine = true; 353 private boolean isLineMediaFileURI = false; 354 private boolean isEndList = false; 355 private boolean isLinePlaylistURI = false; 356 private boolean isVariantPlaylist = false; 357 private boolean isDiscontinuity = false; 358 private int targetDuration = 0; 359 private int sequenceNumber = 0; 360 private int dataListIndex = -1; 361 private List<String> dataListString = new ArrayList<String>(); 362 private List<Integer> dataListInteger = new ArrayList<Integer>(); 363 private List<Double> dataListDouble = new ArrayList<Double>(); 364 private List<Boolean> dataListBoolean = new ArrayList<Boolean>(); 365 366 private void load(URI uri) { 367 HttpURLConnection connection = null; 368 BufferedReader reader = null; 369 try { 370 connection = (HttpURLConnection) uri.toURL().openConnection(); 371 connection.setRequestMethod("GET"); 372 373 if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { 374 MediaUtils.error(this, MediaError.ERROR_LOCATOR_CONNECTION_LOST.code(), "HTTP responce code: " + connection.getResponseCode(), null); 375 } 376 377 Charset charset = getCharset(uri.toURL().toExternalForm(), connection.getContentType()); 378 if (charset != null) { 379 reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), charset)); 380 } 381 382 if (reader != null) { 383 boolean result; 384 do { 385 result = parseLine(reader.readLine()); 386 } while (result); 387 } 388 } catch (MalformedURLException e) { 389 } catch (IOException e) { 390 } finally { 391 if (reader != null) { 392 try { 393 reader.close(); 394 } catch (IOException e) {} 395 396 Locator.closeConnection(connection); 397 } 398 } 399 } 400 401 private boolean isVariantPlaylist() { 402 return isVariantPlaylist; 403 } 404 405 private boolean isLivePlaylist() { 406 return !isEndList; 407 } 408 409 private int getTargetDuration() { 410 return targetDuration; 411 } 412 413 private int getSequenceNumber() { 414 return sequenceNumber; 415 } 416 417 private boolean hasNext() { 418 dataListIndex++; 419 if (dataListString.size() > dataListIndex || dataListInteger.size() > dataListIndex || dataListDouble.size() > dataListIndex || dataListBoolean.size() > dataListIndex) { 420 return true; 421 } else { 422 return false; 423 } 424 } 425 426 private String getString() { 427 return dataListString.get(dataListIndex); 428 } 429 430 private Integer getInteger() { 431 return dataListInteger.get(dataListIndex); 432 } 433 434 private Double getDouble() { 435 return dataListDouble.get(dataListIndex); 436 } 437 438 private Boolean getBoolean() { 439 return dataListBoolean.get(dataListIndex); 440 } 441 442 private boolean parseLine(String line) { 443 if (line == null) { 444 return false; 445 } 446 447 // First line of playlist must be "#EXTM3U" 448 if (isFirstLine) { 449 if (line.compareTo("#EXTM3U") != 0) { 450 return false; 451 } 452 453 isFirstLine = false; 454 return true; 455 } 456 457 // Ignore blank lines and comments 458 if (line.isEmpty() || (line.startsWith("#") && !line.startsWith("#EXT"))) { 459 return true; 460 } 461 462 if (line.startsWith("#EXTINF")) { // #EXTINF 463 //#EXTINF:<duration>,<title> 464 String[] s1 = line.split(":"); 465 if (s1.length == 2 && s1[1].length() > 0) { 466 String[] s2 = s1[1].split(","); 467 if (s2.length >= 1) { // We have duration 468 dataListDouble.add(Double.parseDouble(s2[0])); 469 } 470 } 471 472 isLineMediaFileURI = true; 473 } else if (line.startsWith("#EXT-X-TARGETDURATION")) { 474 // #EXT-X-TARGETDURATION:<s> 475 String[] s1 = line.split(":"); 476 if (s1.length == 2 && s1[1].length() > 0) { 477 targetDuration = Integer.parseInt(s1[1]); 478 } 479 } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) { 480 // #EXT-X-MEDIA-SEQUENCE:<number> 481 String[] s1 = line.split(":"); 482 if (s1.length == 2 && s1[1].length() > 0) { 483 sequenceNumber = Integer.parseInt(s1[1]); 484 } 485 } else if (line.startsWith("#EXT-X-STREAM-INF")) { 486 // #EXT-X-STREAM-INF:<attribute-list> 487 isVariantPlaylist = true; 488 489 int bitrate = 0; 490 String[] s1 = line.split(":"); 491 if (s1.length == 2 && s1[1].length() > 0) { 492 String[] s2 = s1[1].split(","); 493 if (s2.length > 0) { 494 for (int i = 0; i < s2.length; i++) { 495 s2[i] = s2[i].trim(); 496 if (s2[i].startsWith("BANDWIDTH")) { 497 String[] s3 = s2[i].split("="); 498 if (s3.length == 2 && s3[1].length() > 0) { 499 bitrate = Integer.parseInt(s3[1]); 500 } 501 } 502 } 503 } 504 } 505 506 if (bitrate < 1) { 507 return false; 508 } 509 510 dataListInteger.add(bitrate); 511 512 isLinePlaylistURI = true; // Next line will be URI to playlist 513 } else if (line.startsWith("#EXT-X-ENDLIST")) { // #EXT-X-ENDLIST 514 isEndList = true; 515 } else if (line.startsWith("#EXT-X-DISCONTINUITY")) { // #EXT-X-DISCONTINUITY 516 isDiscontinuity = true; 517 } else if (isLinePlaylistURI) { 518 isLinePlaylistURI = false; 519 dataListString.add(line); 520 } else if (isLineMediaFileURI) { 521 isLineMediaFileURI = false; 522 dataListString.add(line); 523 dataListBoolean.add(isDiscontinuity); 524 isDiscontinuity = false; 525 } 526 527 return true; 528 } 529 530 private Charset getCharset(String url, String mimeType) { 531 if ((url != null && stripParameters(url).endsWith(".m3u8")) || (mimeType != null && mimeType.equals("application/vnd.apple.mpegurl"))) { 532 if (Charset.isSupported(CHARSET_UTF_8)) { 533 return Charset.forName(CHARSET_UTF_8); 534 } 535 } else if ((url != null && stripParameters(url).endsWith(".m3u")) || (mimeType != null && mimeType.equals("audio/mpegurl"))) { 536 if (Charset.isSupported(CHARSET_US_ASCII)) { 537 return Charset.forName(CHARSET_US_ASCII); 538 } 539 } 540 541 return null; 542 } 543 } 544 545 private static class VariantPlaylist { 546 547 private URI playlistURI = null; 548 private int infoIndex = -1; 549 private List<String> playlistsLocations = new ArrayList<String>(); 550 private List<Integer> playlistsBitrates = new ArrayList<Integer>(); 551 private List<Playlist> playlists = new ArrayList<Playlist>(); 552 private String mediaFileExtension = null; // Will be set to media file extension of first playlist 553 554 private VariantPlaylist(URI uri) { 555 playlistURI = uri; 556 } 557 558 private void addPlaylistInfo(String location, int bitrate) { 559 playlistsLocations.add(location); 560 playlistsBitrates.add(bitrate); 561 } 562 563 private void addPlaylist(Playlist playlist) { 564 if (mediaFileExtension == null) { 565 mediaFileExtension = playlist.getMediaFileExtension(); 566 } else { 567 if (!mediaFileExtension.equals(playlist.getMediaFileExtension())) { 568 playlistsLocations.remove(infoIndex); 569 playlistsBitrates.remove(infoIndex); 570 infoIndex--; 571 return; // Ignore playlist with different media type 572 } 573 } 574 playlists.add(playlist); 575 } 576 577 private Playlist getPlaylist(int index) { 578 if (playlists.size() > index) { 579 return playlists.get(index); 580 } else { 581 return null; 582 } 583 } 584 585 private boolean hasNext() { 586 infoIndex++; 587 if (playlistsLocations.size() > infoIndex && playlistsBitrates.size() > infoIndex) { 588 return true; 589 } else { 590 return false; 591 } 592 } 593 594 private URI getPlaylistURI() throws URISyntaxException, MalformedURLException { 595 String location = playlistsLocations.get(infoIndex); 596 if (location.startsWith("http://") || location.startsWith("https://")) { 597 return new URI(location); 598 } else { 599 return new URI(playlistURI.toURL().toString().substring(0, playlistURI.toURL().toString().lastIndexOf("/") + 1) + location); 600 } 601 } 602 603 private Playlist getPlaylistBasedOnBitrate(int bitrate) { 604 int playlistIndex = -1; 605 int playlistBitrate = 0; 606 607 // Get bitrate that less then requested bitrate, but most closed to it 608 for (int i = 0; i < playlistsBitrates.size(); i++) { 609 int b = playlistsBitrates.get(i); 610 if (b < bitrate) { 611 if (playlistIndex != -1) { 612 if (b > playlistBitrate) { 613 playlistBitrate = b; 614 playlistIndex = i; 615 } 616 } else { 617 playlistIndex = i; 618 } 619 } 620 } 621 622 // If we did not find one (stall), then get the lowest bitrate possible 623 if (playlistIndex == -1) { 624 for (int i = 0; i < playlistsBitrates.size(); i++) { 625 int b = playlistsBitrates.get(i); 626 if (b < playlistBitrate || playlistIndex == -1) { 627 playlistBitrate = b; 628 playlistIndex = i; 629 } 630 } 631 } 632 633 // Just in case 634 if (playlistIndex < 0 || playlistIndex >= playlists.size()) { 635 return null; 636 } else { 637 return playlists.get(playlistIndex); 638 } 639 } 640 } 641 642 private class Playlist { 643 644 private boolean isLive = false; 645 private volatile boolean isLiveWaiting = false; 646 private volatile boolean isLiveStop = false; 647 private long targetDuration = 0; 648 private URI playlistURI = null; 649 private final Object lock = new Object(); 650 private List<String> mediaFiles = new ArrayList<String>(); 651 private List<Double> mediaFilesStartTimes = new ArrayList<Double>(); 652 private List<Boolean> mediaFilesDiscontinuities = new ArrayList<Boolean>(); 653 private boolean needBaseURI = true; 654 private String baseURI = null; 655 private double duration = 0.0; 656 private int sequenceNumber = -1; 657 private int sequenceNumberStart = -1; 658 private boolean sequenceNumberUpdated = false; 659 private boolean forceDiscontinuity = false; 660 661 private Playlist(boolean isLive, int targetDuration) { 662 this.isLive = isLive; 663 this.targetDuration = targetDuration * 1000; 664 665 if (isLive) { 666 duration = -1.0; 667 } 668 } 669 670 private Playlist(URI uri) { 671 playlistURI = uri; 672 } 673 674 private void update(String nextMediaFile) { 675 PlaylistParser parser = new PlaylistParser(); 676 parser.load(playlistURI); 677 678 isLive = parser.isLivePlaylist(); 679 targetDuration = parser.getTargetDuration() * 1000; 680 681 if (isLive) { 682 duration = -1.0; 683 } 684 685 if (setSequenceNumber(parser.getSequenceNumber())) { 686 while (parser.hasNext()) { 687 addMediaFile(parser.getString(), parser.getDouble(), parser.getBoolean()); 688 } 689 } 690 691 if (nextMediaFile != null) { 692 synchronized (lock) { 693 for (int i = 0; i < mediaFiles.size(); i++) { 694 String mediaFile = mediaFiles.get(i); 695 if (nextMediaFile.endsWith(mediaFile)) { 696 mediaFileIndex = i - 1; 697 break; 698 } 699 } 700 } 701 } 702 } 703 704 private boolean isLive() { 705 return isLive; 706 } 707 708 private long getTargetDuration() { 709 return targetDuration; 710 } 711 712 private void setPlaylistURI(URI uri) { 713 playlistURI = uri; 714 } 715 716 private void addMediaFile(String URI, double duration, boolean isDiscontinuity) { 717 synchronized (lock) { 718 719 if (needBaseURI) { 720 setBaseURI(playlistURI.toString(), URI); 721 } 722 723 if (isLive) { 724 if (sequenceNumberUpdated) { 725 int index = mediaFiles.indexOf(URI); 726 if (index != -1) { 727 for (int i = 0; i < index; i++) { 728 mediaFiles.remove(0); 729 mediaFilesDiscontinuities.remove(0); 730 if (mediaFileIndex == -1) { 731 forceDiscontinuity = true; 732 } 733 if (mediaFileIndex >= 0) { 734 mediaFileIndex--; 735 } 736 } 737 } 738 sequenceNumberUpdated = false; 739 } 740 741 if (mediaFiles.contains(URI)) { 742 return; // Nothing to add 743 } 744 } 745 746 mediaFiles.add(URI); 747 mediaFilesDiscontinuities.add(isDiscontinuity); 748 749 if (isLive) { 750 if (isLiveWaiting) { 751 liveSemaphore.release(); 752 } 753 } else { 754 mediaFilesStartTimes.add(this.duration); 755 this.duration += duration; 756 } 757 } 758 } 759 760 private String getNextMediaFile() { 761 if (isLive) { 762 synchronized (lock) { 763 isLiveWaiting = ((mediaFileIndex + 1) >= mediaFiles.size()); 764 } 765 if (isLiveWaiting) { 766 try { 767 liveSemaphore.acquire(); 768 isLiveWaiting = false; 769 if (isLiveStop) { 770 isLiveStop = false; 771 return null; 772 } 773 } catch (InterruptedException e) { 774 isLiveWaiting = false; 775 return null; 776 } 777 } 778 if (isPlaylistClosed) { 779 return null; 780 } 781 } 782 783 synchronized (lock) { 784 mediaFileIndex++; 785 if ((mediaFileIndex) < mediaFiles.size()) { 786 if (baseURI != null) { 787 return baseURI + mediaFiles.get(mediaFileIndex); 788 } else { 789 return mediaFiles.get(mediaFileIndex); 790 } 791 } else { 792 return null; 793 } 794 } 795 } 796 797 private double getDuration() { 798 return duration; 799 } 800 801 private void setForceDiscontinuity(boolean value) { 802 forceDiscontinuity = value; 803 } 804 805 private boolean isCurrentMediaFileDiscontinuity() { 806 if (forceDiscontinuity) { 807 forceDiscontinuity = false; 808 return true; 809 } else { 810 return mediaFilesDiscontinuities.get(mediaFileIndex); 811 } 812 } 813 814 private double seek(long time) { 815 synchronized (lock) { 816 if (isLive) { 817 if (time == 0) { 818 mediaFileIndex = -1; 819 if (isLiveWaiting) { 820 isLiveStop = true; 821 liveSemaphore.release(); 822 } 823 return 0; 824 } 825 } else { 826 time += targetDuration / 2000; 827 828 int mediaFileStartTimeSize = mediaFilesStartTimes.size(); 829 830 for (int index = 0; index < mediaFileStartTimeSize; index++) { 831 if (time >= mediaFilesStartTimes.get(index)) { 832 if (index + 1 < mediaFileStartTimeSize) { 833 if (time < mediaFilesStartTimes.get(index + 1)) { 834 mediaFileIndex = index - 1; // Seek will load segment and increment mediaFileIndex 835 return mediaFilesStartTimes.get(index); 836 } 837 } else { 838 if ((time - targetDuration / 2000) < duration) { 839 mediaFileIndex = index - 1; // Seek will load segment and increment mediaFileIndex 840 return mediaFilesStartTimes.get(index); 841 } else if (Double.compare(time - targetDuration / 2000, duration) == 0) { 842 return duration; 843 } 844 } 845 } 846 } 847 } 848 } 849 850 return -1; 851 } 852 853 private int getMimeType() { 854 synchronized (lock) { 855 if (mediaFiles.size() > 0) { 856 if (stripParameters(mediaFiles.get(0)).endsWith(".ts")) { 857 return HLS_VALUE_MIMETYPE_MP2T; 858 } else if (stripParameters(mediaFiles.get(0)).endsWith(".mp3")) { 859 return HLS_VALUE_MIMETYPE_MP3; 860 } 861 } 862 } 863 864 return -1; 865 } 866 867 private String getMediaFileExtension() { 868 synchronized (lock) { 869 if (mediaFiles.size() > 0) { 870 String mediaFile = stripParameters(mediaFiles.get(0)); 871 int index = mediaFile.lastIndexOf("."); 872 if (index != -1) { 873 return mediaFile.substring(index); 874 } 875 } 876 } 877 878 return null; 879 } 880 881 private boolean setSequenceNumber(int value) { 882 if (sequenceNumberStart == -1) { 883 sequenceNumberStart = value; 884 } else if (sequenceNumber != value) { 885 sequenceNumberUpdated = true; 886 sequenceNumber = value; 887 } else { 888 return false; 889 } 890 891 return true; 892 } 893 894 private void close() { 895 if (isLive) { 896 isPlaylistClosed = true; 897 liveSemaphore.release(); 898 } 899 } 900 901 private void setBaseURI(String playlistURI, String URI) { 902 if (!URI.startsWith("http://") || !URI.startsWith("https://")) { 903 baseURI = playlistURI.substring(0, playlistURI.lastIndexOf("/") + 1); 904 } 905 needBaseURI = false; 906 } 907 } 908 }