package com.sun.media.jfxmedia.locator;
import com.sun.media.jfxmedia.MediaError;
import com.sun.media.jfxmediaimpl.MediaUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
final class HLSConnectionHolder extends ConnectionHolder {
private URLConnection urlConnection = null;
private PlaylistThread playlistThread = new PlaylistThread();
private VariantPlaylist variantPlaylist = null;
private Playlist currentPlaylist = null;
private int mediaFileIndex = -1;
private CountDownLatch readySignal = new CountDownLatch(1);
private Semaphore liveSemaphore = new Semaphore(0);
private boolean isPlaylistClosed = false;
private boolean isBitrateAdjustable = false;
private long startTime = -1;
private static final long HLS_VALUE_FLOAT_MULTIPLIER = 1000;
private static final int HLS_PROP_GET_DURATION = 1;
private static final int HLS_PROP_GET_HLS_MODE = 2;
private static final int HLS_PROP_GET_MIMETYPE = 3;
private static final int HLS_VALUE_MIMETYPE_MP2T = 1;
private static final int HLS_VALUE_MIMETYPE_MP3 = 2;
private static final String CHARSET_UTF_8 = "UTF-8";
private static final String CHARSET_US_ASCII = "US-ASCII";
HLSConnectionHolder(URI uri) throws IOException {
playlistThread.setPlaylistURI(uri);
init();
}
private void init() {
playlistThread.putState(PlaylistThread.STATE_INIT);
playlistThread.start();
}
@Override
public int readNextBlock() throws IOException {
if (isBitrateAdjustable && startTime == -1) {
startTime = System.currentTimeMillis();
}
int read = super.readNextBlock();
if (isBitrateAdjustable && read == -1) {
long readTime = System.currentTimeMillis() - startTime;
startTime = -1;
adjustBitrate(readTime);
}
return read;
}
int readBlock(long position, int size) throws IOException {
throw new IOException();
}
boolean needBuffer() {
return true;
}
boolean isSeekable() {
return true;
}
boolean isRandomAccess() {
return false;
}
public long seek(long position) {
try {
readySignal.await();
} catch (Exception e) {
return -1;
}
return (long) (currentPlaylist.seek(position) * HLS_VALUE_FLOAT_MULTIPLIER);
}
@Override
public void closeConnection() {
currentPlaylist.close();
super.closeConnection();
resetConnection();
playlistThread.putState(PlaylistThread.STATE_EXIT);
}
@Override
int property(int prop, int value) {
try {
readySignal.await();
} catch (Exception e) {
return -1;
}
if (prop == HLS_PROP_GET_DURATION) {
return (int) (currentPlaylist.getDuration() * HLS_VALUE_FLOAT_MULTIPLIER);
} else if (prop == HLS_PROP_GET_HLS_MODE) {
return 1;
} else if (prop == HLS_PROP_GET_MIMETYPE) {
return currentPlaylist.getMimeType();
}
return -1;
}
@Override
int getStreamSize() {
try {
readySignal.await();
} catch (Exception e) {
return -1;
}
return loadNextSegment();
}
private void resetConnection() {
super.closeConnection();
Locator.closeConnection(urlConnection);
urlConnection = null;
}
private int loadNextSegment() {
resetConnection();
String mediaFile = currentPlaylist.getNextMediaFile();
if (mediaFile == null) {
return -1;
}
try {
URI uri = new URI(mediaFile);
urlConnection = uri.toURL().openConnection();
channel = openChannel();
} catch (Exception e) {
return -1;
}
if (currentPlaylist.isCurrentMediaFileDiscontinuity()) {
return (-1 * urlConnection.getContentLength());
} else {
return urlConnection.getContentLength();
}
}
private ReadableByteChannel openChannel() throws IOException {
return Channels.newChannel(urlConnection.getInputStream());
}
private void adjustBitrate(long readTime) {
int avgBitrate = (int)(((long) urlConnection.getContentLength() * 8 * 1000) / readTime);
Playlist playlist = variantPlaylist.getPlaylistBasedOnBitrate(avgBitrate);
if (playlist != null && playlist != currentPlaylist) {
if (currentPlaylist.isLive()) {
playlist.update(currentPlaylist.getNextMediaFile());
playlistThread.setReloadPlaylist(playlist);
}
playlist.setForceDiscontinuity(true);
currentPlaylist = playlist;
}
}
private static String stripParameters(String mediaFile) {
int qp = mediaFile.indexOf('?');
if (qp > 0) {
mediaFile = mediaFile.substring(0, qp);
}
return mediaFile;
}
private class PlaylistThread extends Thread {
public static final int STATE_INIT = 0;
public static final int STATE_EXIT = 1;
public static final int STATE_RELOAD_PLAYLIST = 2;
private BlockingQueue<Integer> stateQueue = new LinkedBlockingQueue<Integer>();
private URI playlistURI = null;
private Playlist reloadPlaylist = null;
private final Object reloadLock = new Object();
private volatile boolean stopped = false;
private PlaylistThread() {
setName("JFXMedia HLS Playlist Thread");
setDaemon(true);
}
private void setPlaylistURI(URI playlistURI) {
this.playlistURI = playlistURI;
}
private void setReloadPlaylist(Playlist playlist) {
synchronized(reloadLock) {
reloadPlaylist = playlist;
}
}
@Override
public void run() {
while (!stopped) {
try {
int state = stateQueue.take();
switch (state) {
case STATE_INIT:
stateInit();
break;
case STATE_EXIT:
stopped = true;
break;
case STATE_RELOAD_PLAYLIST:
stateReloadPlaylist();
break;
default:
break;
}
} catch (Exception e) {
}
}
}
private void putState(int state) {
if (stateQueue != null) {
try {
stateQueue.put(state);
} catch (InterruptedException ex) {
}
}
}
private void stateInit() {
if (playlistURI == null) {
return;
}
PlaylistParser parser = new PlaylistParser();
parser.load(playlistURI);
if (parser.isVariantPlaylist()) {
variantPlaylist = new VariantPlaylist(playlistURI);
while (parser.hasNext()) {
variantPlaylist.addPlaylistInfo(parser.getString(), parser.getInteger());
}
} else {
if (currentPlaylist == null) {
currentPlaylist = new Playlist(parser.isLivePlaylist(), parser.getTargetDuration());
currentPlaylist.setPlaylistURI(playlistURI);
}
if (currentPlaylist.setSequenceNumber(parser.getSequenceNumber())) {
while (parser.hasNext()) {
currentPlaylist.addMediaFile(parser.getString(), parser.getDouble(), parser.getBoolean());
}
}
if (variantPlaylist != null) {
variantPlaylist.addPlaylist(currentPlaylist);
}
}
if (variantPlaylist != null) {
while (variantPlaylist.hasNext()) {
try {
currentPlaylist = new Playlist(variantPlaylist.getPlaylistURI());
currentPlaylist.update(null);
variantPlaylist.addPlaylist(currentPlaylist);
} catch (URISyntaxException e) {
} catch (MalformedURLException e) {
}
}
}
if (variantPlaylist != null) {
currentPlaylist = variantPlaylist.getPlaylist(0);
isBitrateAdjustable = true;
}
if (currentPlaylist.isLive()) {
setReloadPlaylist(currentPlaylist);
putState(STATE_RELOAD_PLAYLIST);
}
readySignal.countDown();
}
private void stateReloadPlaylist() {
try {
long timeout;
synchronized(reloadLock) {
timeout = reloadPlaylist.getTargetDuration() / 2;
}
Thread.sleep(timeout);
} catch (InterruptedException ex) {
return;
}
synchronized(reloadLock) {
reloadPlaylist.update(null);
}
putState(STATE_RELOAD_PLAYLIST);
}
}
private static class PlaylistParser {
private boolean isFirstLine = true;
private boolean isLineMediaFileURI = false;
private boolean isEndList = false;
private boolean isLinePlaylistURI = false;
private boolean isVariantPlaylist = false;
private boolean isDiscontinuity = false;
private int targetDuration = 0;
private int sequenceNumber = 0;
private int dataListIndex = -1;
private List<String> dataListString = new ArrayList<String>();
private List<Integer> dataListInteger = new ArrayList<Integer>();
private List<Double> dataListDouble = new ArrayList<Double>();
private List<Boolean> dataListBoolean = new ArrayList<Boolean>();
private void load(URI uri) {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
connection = (HttpURLConnection) uri.toURL().openConnection();
connection.setRequestMethod("GET");
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
MediaUtils.error(this, MediaError.ERROR_LOCATOR_CONNECTION_LOST.code(), "HTTP responce code: " + connection.getResponseCode(), null);
}
Charset charset = getCharset(uri.toURL().toExternalForm(), connection.getContentType());
if (charset != null) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), charset));
}
if (reader != null) {
boolean result;
do {
result = parseLine(reader.readLine());
} while (result);
}
} catch (MalformedURLException e) {
} catch (IOException e) {
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {}
Locator.closeConnection(connection);
}
}
}
private boolean isVariantPlaylist() {
return isVariantPlaylist;
}
private boolean isLivePlaylist() {
return !isEndList;
}
private int getTargetDuration() {
return targetDuration;
}
private int getSequenceNumber() {
return sequenceNumber;
}
private boolean hasNext() {
dataListIndex++;
if (dataListString.size() > dataListIndex || dataListInteger.size() > dataListIndex || dataListDouble.size() > dataListIndex || dataListBoolean.size() > dataListIndex) {
return true;
} else {
return false;
}
}
private String getString() {
return dataListString.get(dataListIndex);
}
private Integer getInteger() {
return dataListInteger.get(dataListIndex);
}
private Double getDouble() {
return dataListDouble.get(dataListIndex);
}
private Boolean getBoolean() {
return dataListBoolean.get(dataListIndex);
}
private boolean parseLine(String line) {
if (line == null) {
return false;
}
if (isFirstLine) {
if (line.compareTo("#EXTM3U") != 0) {
return false;
}
isFirstLine = false;
return true;
}
if (line.isEmpty() || (line.startsWith("#") && !line.startsWith("#EXT"))) {
return true;
}
if (line.startsWith("#EXTINF")) {
String[] s1 = line.split(":");
if (s1.length == 2 && s1[1].length() > 0) {
String[] s2 = s1[1].split(",");
if (s2.length >= 1) {
dataListDouble.add(Double.parseDouble(s2[0]));
}
}
isLineMediaFileURI = true;
} else if (line.startsWith("#EXT-X-TARGETDURATION")) {
String[] s1 = line.split(":");
if (s1.length == 2 && s1[1].length() > 0) {
targetDuration = Integer.parseInt(s1[1]);
}
} else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
String[] s1 = line.split(":");
if (s1.length == 2 && s1[1].length() > 0) {
sequenceNumber = Integer.parseInt(s1[1]);
}
} else if (line.startsWith("#EXT-X-STREAM-INF")) {
isVariantPlaylist = true;
int bitrate = 0;
String[] s1 = line.split(":");
if (s1.length == 2 && s1[1].length() > 0) {
String[] s2 = s1[1].split(",");
if (s2.length > 0) {
for (int i = 0; i < s2.length; i++) {
s2[i] = s2[i].trim();
if (s2[i].startsWith("BANDWIDTH")) {
String[] s3 = s2[i].split("=");
if (s3.length == 2 && s3[1].length() > 0) {
bitrate = Integer.parseInt(s3[1]);
}
}
}
}
}
if (bitrate < 1) {
return false;
}
dataListInteger.add(bitrate);
isLinePlaylistURI = true;
} else if (line.startsWith("#EXT-X-ENDLIST")) {
isEndList = true;
} else if (line.startsWith("#EXT-X-DISCONTINUITY")) {
isDiscontinuity = true;
} else if (isLinePlaylistURI) {
isLinePlaylistURI = false;
dataListString.add(line);
} else if (isLineMediaFileURI) {
isLineMediaFileURI = false;
dataListString.add(line);
dataListBoolean.add(isDiscontinuity);
isDiscontinuity = false;
}
return true;
}
private Charset getCharset(String url, String mimeType) {
if ((url != null && stripParameters(url).endsWith(".m3u8")) || (mimeType != null && mimeType.equals("application/vnd.apple.mpegurl"))) {
if (Charset.isSupported(CHARSET_UTF_8)) {
return Charset.forName(CHARSET_UTF_8);
}
} else if ((url != null && stripParameters(url).endsWith(".m3u")) || (mimeType != null && mimeType.equals("audio/mpegurl"))) {
if (Charset.isSupported(CHARSET_US_ASCII)) {
return Charset.forName(CHARSET_US_ASCII);
}
}
return null;
}
}
private static class VariantPlaylist {
private URI playlistURI = null;
private int infoIndex = -1;
private List<String> playlistsLocations = new ArrayList<String>();
private List<Integer> playlistsBitrates = new ArrayList<Integer>();
private List<Playlist> playlists = new ArrayList<Playlist>();
private String mediaFileExtension = null;
private VariantPlaylist(URI uri) {
playlistURI = uri;
}
private void addPlaylistInfo(String location, int bitrate) {
playlistsLocations.add(location);
playlistsBitrates.add(bitrate);
}
private void addPlaylist(Playlist playlist) {
if (mediaFileExtension == null) {
mediaFileExtension = playlist.getMediaFileExtension();
} else {
if (!mediaFileExtension.equals(playlist.getMediaFileExtension())) {
playlistsLocations.remove(infoIndex);
playlistsBitrates.remove(infoIndex);
infoIndex--;
return;
}
}
playlists.add(playlist);
}
private Playlist getPlaylist(int index) {
if (playlists.size() > index) {
return playlists.get(index);
} else {
return null;
}
}
private boolean hasNext() {
infoIndex++;
if (playlistsLocations.size() > infoIndex && playlistsBitrates.size() > infoIndex) {
return true;
} else {
return false;
}
}
private URI getPlaylistURI() throws URISyntaxException, MalformedURLException {
String location = playlistsLocations.get(infoIndex);
if (location.startsWith("http://") || location.startsWith("https://")) {
return new URI(location);
} else {
return new URI(playlistURI.toURL().toString().substring(0, playlistURI.toURL().toString().lastIndexOf("/") + 1) + location);
}
}
private Playlist getPlaylistBasedOnBitrate(int bitrate) {
int playlistIndex = -1;
int playlistBitrate = 0;
for (int i = 0; i < playlistsBitrates.size(); i++) {
int b = playlistsBitrates.get(i);
if (b < bitrate) {
if (playlistIndex != -1) {
if (b > playlistBitrate) {
playlistBitrate = b;
playlistIndex = i;
}
} else {
playlistIndex = i;
}
}
}
if (playlistIndex == -1) {
for (int i = 0; i < playlistsBitrates.size(); i++) {
int b = playlistsBitrates.get(i);
if (b < playlistBitrate || playlistIndex == -1) {
playlistBitrate = b;
playlistIndex = i;
}
}
}
if (playlistIndex < 0 || playlistIndex >= playlists.size()) {
return null;
} else {
return playlists.get(playlistIndex);
}
}
}
private class Playlist {
private boolean isLive = false;
private volatile boolean isLiveWaiting = false;
private volatile boolean isLiveStop = false;
private long targetDuration = 0;
private URI playlistURI = null;
private final Object lock = new Object();
private List<String> mediaFiles = new ArrayList<String>();
private List<Double> mediaFilesStartTimes = new ArrayList<Double>();
private List<Boolean> mediaFilesDiscontinuities = new ArrayList<Boolean>();
private boolean needBaseURI = true;
private String baseURI = null;
private double duration = 0.0;
private int sequenceNumber = -1;
private int sequenceNumberStart = -1;
private boolean sequenceNumberUpdated = false;
private boolean forceDiscontinuity = false;
private Playlist(boolean isLive, int targetDuration) {
this.isLive = isLive;
this.targetDuration = targetDuration * 1000;
if (isLive) {
duration = -1.0;
}
}
private Playlist(URI uri) {
playlistURI = uri;
}
private void update(String nextMediaFile) {
PlaylistParser parser = new PlaylistParser();
parser.load(playlistURI);
isLive = parser.isLivePlaylist();
targetDuration = parser.getTargetDuration() * 1000;
if (isLive) {
duration = -1.0;
}
if (setSequenceNumber(parser.getSequenceNumber())) {
while (parser.hasNext()) {
addMediaFile(parser.getString(), parser.getDouble(), parser.getBoolean());
}
}
if (nextMediaFile != null) {
synchronized (lock) {
for (int i = 0; i < mediaFiles.size(); i++) {
String mediaFile = mediaFiles.get(i);
if (nextMediaFile.endsWith(mediaFile)) {
mediaFileIndex = i - 1;
break;
}
}
}
}
}
private boolean isLive() {
return isLive;
}
private long getTargetDuration() {
return targetDuration;
}
private void setPlaylistURI(URI uri) {
playlistURI = uri;
}
private void addMediaFile(String URI, double duration, boolean isDiscontinuity) {
synchronized (lock) {
if (needBaseURI) {
setBaseURI(playlistURI.toString(), URI);
}
if (isLive) {
if (sequenceNumberUpdated) {
int index = mediaFiles.indexOf(URI);
if (index != -1) {
for (int i = 0; i < index; i++) {
mediaFiles.remove(0);
mediaFilesDiscontinuities.remove(0);
if (mediaFileIndex == -1) {
forceDiscontinuity = true;
}
if (mediaFileIndex >= 0) {
mediaFileIndex--;
}
}
}
sequenceNumberUpdated = false;
}
if (mediaFiles.contains(URI)) {
return;
}
}
mediaFiles.add(URI);
mediaFilesDiscontinuities.add(isDiscontinuity);
if (isLive) {
if (isLiveWaiting) {
liveSemaphore.release();
}
} else {
mediaFilesStartTimes.add(this.duration);
this.duration += duration;
}
}
}
private String getNextMediaFile() {
if (isLive) {
synchronized (lock) {
isLiveWaiting = ((mediaFileIndex + 1) >= mediaFiles.size());
}
if (isLiveWaiting) {
try {
liveSemaphore.acquire();
isLiveWaiting = false;
if (isLiveStop) {
isLiveStop = false;
return null;
}
} catch (InterruptedException e) {
isLiveWaiting = false;
return null;
}
}
if (isPlaylistClosed) {
return null;
}
}
synchronized (lock) {
mediaFileIndex++;
if ((mediaFileIndex) < mediaFiles.size()) {
if (baseURI != null) {
return baseURI + mediaFiles.get(mediaFileIndex);
} else {
return mediaFiles.get(mediaFileIndex);
}
} else {
return null;
}
}
}
private double getDuration() {
return duration;
}
private void setForceDiscontinuity(boolean value) {
forceDiscontinuity = value;
}
private boolean isCurrentMediaFileDiscontinuity() {
if (forceDiscontinuity) {
forceDiscontinuity = false;
return true;
} else {
return mediaFilesDiscontinuities.get(mediaFileIndex);
}
}
private double seek(long time) {
synchronized (lock) {
if (isLive) {
if (time == 0) {
mediaFileIndex = -1;
if (isLiveWaiting) {
isLiveStop = true;
liveSemaphore.release();
}
return 0;
}
} else {
time += targetDuration / 2000;
int mediaFileStartTimeSize = mediaFilesStartTimes.size();
for (int index = 0; index < mediaFileStartTimeSize; index++) {
if (time >= mediaFilesStartTimes.get(index)) {
if (index + 1 < mediaFileStartTimeSize) {
if (time < mediaFilesStartTimes.get(index + 1)) {
mediaFileIndex = index - 1;
return mediaFilesStartTimes.get(index);
}
} else {
if ((time - targetDuration / 2000) < duration) {
mediaFileIndex = index - 1;
return mediaFilesStartTimes.get(index);
} else if (Double.compare(time - targetDuration / 2000, duration) == 0) {
return duration;
}
}
}
}
}
}
return -1;
}
private int getMimeType() {
synchronized (lock) {
if (mediaFiles.size() > 0) {
if (stripParameters(mediaFiles.get(0)).endsWith(".ts")) {
return HLS_VALUE_MIMETYPE_MP2T;
} else if (stripParameters(mediaFiles.get(0)).endsWith(".mp3")) {
return HLS_VALUE_MIMETYPE_MP3;
}
}
}
return -1;
}
private String getMediaFileExtension() {
synchronized (lock) {
if (mediaFiles.size() > 0) {
String mediaFile = stripParameters(mediaFiles.get(0));
int index = mediaFile.lastIndexOf(".");
if (index != -1) {
return mediaFile.substring(index);
}
}
}
return null;
}
private boolean setSequenceNumber(int value) {
if (sequenceNumberStart == -1) {
sequenceNumberStart = value;
} else if (sequenceNumber != value) {
sequenceNumberUpdated = true;
sequenceNumber = value;
} else {
return false;
}
return true;
}
private void close() {
if (isLive) {
isPlaylistClosed = true;
liveSemaphore.release();
}
}
private void setBaseURI(String playlistURI, String URI) {
if (!URI.startsWith("http://") && !URI.startsWith("https://")) {
baseURI = playlistURI.substring(0, playlistURI.lastIndexOf("/") + 1);
}
needBaseURI = false;
}
}
}