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 }