/*
* Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.media.jfxmedia.locator;
import com.sun.media.jfxmedia.MediaException;
import com.sun.media.jfxmedia.MediaManager;
import com.sun.media.jfxmedia.logging.Logger;
import com.sun.media.jfxmediaimpl.HostUtils;
import com.sun.media.jfxmediaimpl.MediaUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
A
Locator
which refers to a
URI
.
/**
* A
* <code>Locator</code> which refers to a
* <code>URI</code>.
*/
public class Locator {
The content type used if no more specific one may be derived.
/**
* The content type used if no more specific one may be derived.
*/
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
The number of times to attempt to open a URL connection to test the URI.
/**
* The number of times to attempt to open a URL connection to test the URI.
*/
private static final int MAX_CONNECTION_ATTEMPTS = 5;
The number of milliseconds between attempts to open a URL connection.
/**
* The number of milliseconds between attempts to open a URL connection.
*/
private static final long CONNECTION_RETRY_INTERVAL = 1000L;
Timeout in milliseconds to wait for connection (5 min).
/**
* Timeout in milliseconds to wait for connection (5 min).
*/
private static final int CONNECTION_TIMEOUT = 300000;
The content type of the media content.
/**
* The content type of the media content.
*/
protected String contentType = DEFAULT_CONTENT_TYPE;
A hint for the internal player.
/**
* A hint for the internal player.
*/
protected long contentLength = -1; //Used as a hint for the native layer
The URI source.
/**
* The URI source.
*/
protected URI uri;
Properties to be associated with the connection made to the URI. The
significance of the properties depends on the URI protocol and type of
media source.
/**
* Properties to be associated with the connection made to the URI. The
* significance of the properties depends on the URI protocol and type of
* media source.
*/
private Map<String, Object> connectionProperties;
Mutex for connectionProperties;
/**
* Mutex for connectionProperties;
*/
private final Object propertyLock = new Object();
/*
* These variables will be initialized by constructor and used by init()
*/
private String uriString = null;
private String scheme = null;
private String protocol = null;
/*
* if cached, we store a hard reference to keep it alive
*/
private LocatorCache.CacheReference cacheEntry = null;
/*
* True if init(), getContentLength() and getContentType() can block; false
* otherwise.
*/
private boolean canBlock = false;
/*
* Used to block getContentLength() and getContentType().
*/
private CountDownLatch readySignal = new CountDownLatch(1);
iOS only: determines if the given URL points to the iPod library
/**
* iOS only: determines if the given URL points to the iPod library
*/
private boolean isIpod;
Holds connection and response code returned from getConnection()
/**
* Holds connection and response code returned from getConnection()
*/
private static class LocatorConnection {
public HttpURLConnection connection = null;
public int responseCode = HttpURLConnection.HTTP_OK;
}
private LocatorConnection getConnection(URI uri, String requestMethod)
throws MalformedURLException, IOException {
// Check ability to connect.
LocatorConnection locatorConnection = new LocatorConnection();
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
connection.setRequestMethod(requestMethod);
// Set timeouts, otherwise we can wait forever.
connection.setConnectTimeout(CONNECTION_TIMEOUT);
connection.setReadTimeout(CONNECTION_TIMEOUT);
// Set request headers.
synchronized (propertyLock) {
if (connectionProperties != null) {
for (String key : connectionProperties.keySet()) {
Object value = connectionProperties.get(key);
if (value instanceof String) {
connection.setRequestProperty(key, (String) value);
}
}
}
}
// Store response code so we can get more information about
// returning connection.
locatorConnection.responseCode = connection.getResponseCode();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
locatorConnection.connection = connection;
} else {
closeConnection(connection);
locatorConnection.connection = null;
}
return locatorConnection;
}
private static long getContentLengthLong(URLConnection connection) {
Method method = AccessController.doPrivileged((PrivilegedAction<Method>) () -> {
try {
return URLConnection.class.getMethod("getContentLengthLong");
} catch (NoSuchMethodException ex) {
return null;
}
});
try {
if (method != null) {
return (long) method.invoke(connection);
} else {
return connection.getContentLength();
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
return -1;
}
}
Constructs an object representing a media source.
Params: - uri – The URI source.
Throws: - NullPointerException – if
uri
is
null
. - IllegalArgumentException – if the URI's scheme is
null
. - URISyntaxException – if the supplied URI requires some further
manipulation in order to be used and this procedure fails to produce a
usable URI.
- IllegalArgumentException – if the URI is a Jar URL as described in
https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html
, and the scheme of the URL after removing the leading four characters is null
. - UnsupportedOperationException – if the URI's protocol is
unsupported.
/**
* Constructs an object representing a media source.
*
* @param uri The URI source.
* @throws NullPointerException if
* <code>uri</code> is
* <code>null</code>.
* @throws IllegalArgumentException if the URI's scheme is
* <code>null</code>.
* @throws URISyntaxException if the supplied URI requires some further
* manipulation in order to be used and this procedure fails to produce a
* usable URI.
* @throws IllegalArgumentException if the URI is a Jar URL as described in
* {@link JarURLConnection https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html},
* and the scheme of the URL after removing the leading four characters is
* <code>null</code>.
* @throws UnsupportedOperationException if the URI's protocol is
* unsupported.
*/
public Locator(URI uri) throws URISyntaxException {
// Check for NULL parameter.
if (uri == null) {
throw new NullPointerException("uri == null!");
}
// Get the scheme part.
uriString = uri.toASCIIString();
scheme = uri.getScheme();
if (scheme == null) {
throw new IllegalArgumentException("uri.getScheme() == null! uri == '" + uri + "'");
}
scheme = scheme.toLowerCase();
// Get the protocol.
if (scheme.equals("jar")) {
URI subURI = new URI(uriString.substring(4));
protocol = subURI.getScheme();
if (protocol == null) {
throw new IllegalArgumentException("uri.getScheme() == null! subURI == '" + subURI + "'");
}
protocol = protocol.toLowerCase();
} else {
protocol = scheme; // scheme is already lower case.
}
if (HostUtils.isIOS() && protocol.equals("ipod-library")) {
isIpod = true;
}
// Verify the protocol is supported.
if (!isIpod && !MediaManager.canPlayProtocol(protocol)) {
throw new UnsupportedOperationException("Unsupported protocol \"" + protocol + "\"");
}
// Check if we can block
if (protocol.equals("http") || protocol.equals("https")) {
canBlock = true;
}
// Set instance variable.
this.uri = uri;
}
private InputStream getInputStream(URI uri)
throws MalformedURLException, IOException {
URL url = uri.toURL();
URLConnection connection = url.openConnection();
// Set request headers.
synchronized (propertyLock) {
if (connectionProperties != null) {
for (String key : connectionProperties.keySet()) {
Object value = connectionProperties.get(key);
if (value instanceof String) {
connection.setRequestProperty(key, (String) value);
}
}
}
}
InputStream inputStream = url.openStream();
contentLength = getContentLengthLong(connection);
return inputStream;
}
Tell this Locator to preload the media into memory, if it hasn't been
already.
/**
* Tell this Locator to preload the media into memory, if it hasn't been
* already.
*/
public void cacheMedia() {
LocatorCache.CacheReference ref = LocatorCache.locatorCache().fetchURICache(uri);
if (null == ref) {
ByteBuffer cacheBuffer;
// not cached, load it
InputStream is;
try {
is = getInputStream(uri);
} catch (Throwable t) {
return; // just bail
}
// contentLength is set now, so we can go ahead and allocate
cacheBuffer = ByteBuffer.allocateDirect((int) contentLength);
byte[] readBuf = new byte[8192];
int total = 0;
int count;
while (total < contentLength) {
try {
count = is.read(readBuf);
} catch (IOException ioe) {
try {
is.close();
} catch (Throwable t) {
}
if (Logger.canLog(Logger.DEBUG)) {
Logger.logMsg(Logger.DEBUG, "IOException trying to preload media: " + ioe);
}
return;
}
if (count == -1) {
break; // EOS
}
cacheBuffer.put(readBuf, 0, count);
}
try {
is.close();
} catch (Throwable t) {
}
cacheEntry = LocatorCache.locatorCache().registerURICache(uri, cacheBuffer, contentType);
canBlock = false;
}
}
/*
* True if init() can block; false otherwise.
*/
public boolean canBlock() {
return canBlock;
}
/*
* Initialize locator. Use canBlock() to determine if init() can block.
*
* @throws URISyntaxException if the supplied URI requires some further
* manipulation in order to be used and this procedure fails to produce a
* usable URI. @throws IOExceptions if a stream cannot be opened over a
* connection of the corresponding URL. @throws MediaException if the
* content type of the media is not supported. @throws FileNotFoundException
* if the media is not available.
*/
public void init() throws URISyntaxException, IOException, FileNotFoundException {
try {
// Ensure the correct number of '/'s follows the ':'.
int firstSlash = uriString.indexOf("/");
if (firstSlash != -1 && uriString.charAt(firstSlash + 1) != '/') {
// Only one '/' after the ':'.
if (protocol.equals("file")) {
// Map file:/somepath to file:///somepath
uriString = uriString.replaceFirst("/", "///");
} else if (protocol.equals("http") || protocol.equals("https")) {
// Map http:/somepath to http://somepath
uriString = uriString.replaceFirst("/", "//");
}
}
// On non-Windows systems, replace "/~/" with home directory path + "/".
if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1
&& protocol.equals("file")) {
int index = uriString.indexOf("/~/");
if (index != -1) {
uriString = uriString.substring(0, index)
+ System.getProperty("user.home")
+ uriString.substring(index + 2);
}
}
// Recreate the URI if needed
uri = new URI(uriString);
// First check if this URI is cached, if it is then we're done
cacheEntry = LocatorCache.locatorCache().fetchURICache(uri);
if (null != cacheEntry) {
// Cache hit! Grab contentType and contentLength and be done
contentType = cacheEntry.getMIMEType();
contentLength = cacheEntry.getBuffer().capacity();
if (Logger.canLog(Logger.DEBUG)) {
Logger.logMsg(Logger.DEBUG, "Locator init cache hit:"
+ "\n uri " + uri
+ "\n type " + contentType
+ "\n length " + contentLength);
}
return;
}
// Try to open a connection on the corresponding URL.
boolean isConnected = false;
boolean isMediaUnAvailable = false;
boolean isMediaSupported = true;
if (!isIpod) {
for (int numConnectionAttempts = 0; numConnectionAttempts < MAX_CONNECTION_ATTEMPTS; numConnectionAttempts++) {
try {
// Verify existence.
if (scheme.equals("http") || scheme.equals("https")) {
// Check ability to connect, trying HEAD before GET.
LocatorConnection locatorConnection = getConnection(uri, "HEAD");
if (locatorConnection == null || locatorConnection.connection == null) {
locatorConnection = getConnection(uri, "GET");
}
if (locatorConnection != null && locatorConnection.connection != null) {
isConnected = true;
// Get content type.
contentType = locatorConnection.connection.getContentType();
contentLength = getContentLengthLong(locatorConnection.connection);
// Disconnect.
closeConnection(locatorConnection.connection);
locatorConnection.connection = null;
} else if (locatorConnection != null) {
if (locatorConnection.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
isMediaUnAvailable = true;
}
}
// FIXME: get cache settings from server, honor them
} else if (scheme.equals("file") || scheme.equals("jar") || scheme.equals("jrt")) {
InputStream stream = getInputStream(uri);
stream.close();
isConnected = true;
contentType = MediaUtils.filenameToContentType(uriString); // We need to provide at least something
}
if (isConnected) {
// Check whether content may be played.
// For WAV use file signature, since it can detect audio format
// and we can fail sooner, then doing it at runtime.
// This is important for AudioClip.
if (MediaUtils.CONTENT_TYPE_WAV.equals(contentType)) {
contentType = getContentTypeFromFileSignature(uri);
if (!MediaManager.canPlayContentType(contentType)) {
isMediaSupported = false;
}
} else {
if (contentType == null || !MediaManager.canPlayContentType(contentType)) {
// Try content based on file name.
contentType = MediaUtils.filenameToContentType(uriString);
if (Locator.DEFAULT_CONTENT_TYPE.equals(contentType)) {
// Try content based on file signature.
contentType = getContentTypeFromFileSignature(uri);
}
if (!MediaManager.canPlayContentType(contentType)) {
isMediaSupported = false;
}
}
}
// Break as connection has been made and media type checked.
break;
}
} catch (IOException ioe) {
if (numConnectionAttempts + 1 >= MAX_CONNECTION_ATTEMPTS) {
throw ioe;
}
}
try {
Thread.sleep(CONNECTION_RETRY_INTERVAL);
} catch (InterruptedException ie) {
// Ignore it.
}
}
}
else {
// in case of iPod files we can be sure all files are supported
contentType = MediaUtils.filenameToContentType(uriString);
}
if (Logger.canLog(Logger.WARNING)) {
if (contentType.equals(MediaUtils.CONTENT_TYPE_FLV)) {
Logger.logMsg(Logger.WARNING, "Support for FLV container and VP6 video is removed.");
throw new MediaException("media type not supported (" + uri.toString() + ")");
} else if (contentType.equals(MediaUtils.CONTENT_TYPE_JFX)) {
Logger.logMsg(Logger.WARNING, "Support for FXM container and VP6 video is removed.");
throw new MediaException("media type not supported (" + uri.toString() + ")");
}
}
// Check URI validity.
if (!isIpod && !isConnected) {
if (isMediaUnAvailable) {
throw new FileNotFoundException("media is unavailable (" + uri.toString() + ")");
} else {
throw new IOException("could not connect to media (" + uri.toString() + ")");
}
} else if (!isMediaSupported) {
throw new MediaException("media type not supported (" + uri.toString() + ")");
}
} catch (FileNotFoundException e) {
throw e; // Just re-throw exception
} catch (IOException e) {
throw e; // Just re-throw exception
} catch (MediaException e) {
throw e; // Just re-throw exception
} finally {
readySignal.countDown();
}
}
Retrieves the content type describing the media content or
"application/octet-stream"
if no more specific content type
may be detected.
/**
* Retrieves the content type describing the media content or
* <code>"application/octet-stream"</code> if no more specific content type
* may be detected.
*/
public String getContentType() {
try {
readySignal.await();
} catch (Exception e) {
}
return contentType;
}
Retrieves the protocol of the media URL
/**
* Retrieves the protocol of the media URL
*/
public String getProtocol() {
return protocol;
}
Retrieves the media size.
Returns: size of the media file in bytes. -1 indicates unknown, which may
happen with network streams.
/**
* Retrieves the media size.
*
* @return size of the media file in bytes. -1 indicates unknown, which may
* happen with network streams.
*/
public long getContentLength() {
try {
readySignal.await();
} catch (Exception e) {
}
return contentLength;
}
Blocks until locator is ready (connection is established or failed).
/**
* Blocks until locator is ready (connection is established or failed).
*/
public void waitForReadySignal() {
try {
readySignal.await();
} catch (Exception e) {
}
}
Retrieves the associated
URI
.
Returns: The URI source.
/**
* Retrieves the associated
* <code>URI</code>.
*
* @return The URI source.
*/
public URI getURI() {
return this.uri;
}
Retrieves a string representation of the
Locator
Returns: The
LocatorURI
as a
String
.
/**
* Retrieves a string representation of the
* <code>Locator</code>
*
* @return The
* <code>LocatorURI</code> as a
* <code>String</code>.
*/
@Override
public String toString() {
if (LocatorCache.locatorCache().isCached(uri)) {
return "{LocatorURI uri: " + uri.toString() + " (media cached)}";
}
return "{LocatorURI uri: " + uri.toString() + "}";
}
public String getStringLocation() {
return uri.toString();
}
Sets a property to be used by the connection to the media specified by
the URI. The meaning of the property is a function of the URI protocol
and type of media source. This method should be invoked before calling createConnectionHolder()
or it will have no effect. Params: - property – The name of the property.
- value – The value of the property.
/**
* Sets a property to be used by the connection to the media specified by
* the URI. The meaning of the property is a function of the URI protocol
* and type of media source. This method should be invoked <i>before</i>
* calling {@link #createConnectionHolder()} or it will have no effect.
*
* @param property The name of the property.
* @param value The value of the property.
*/
public void setConnectionProperty(String property, Object value) {
synchronized (propertyLock) {
if (connectionProperties == null) {
connectionProperties = new TreeMap<String, Object>();
}
connectionProperties.put(property, value);
}
}
public ConnectionHolder createConnectionHolder() throws IOException {
// first check if it's cached
if (null != cacheEntry) {
if (Logger.canLog(Logger.DEBUG)) {
Logger.logMsg(Logger.DEBUG, "Locator.createConnectionHolder: media cached, creating memory connection holder");
}
return ConnectionHolder.createMemoryConnectionHolder(cacheEntry.getBuffer());
}
// then fall back on other methods
ConnectionHolder holder;
if ("file".equals(scheme)) {
holder = ConnectionHolder.createFileConnectionHolder(uri);
} else if (uri.toString().endsWith(".m3u8") || uri.toString().endsWith(".m3u")) {
holder = ConnectionHolder.createHLSConnectionHolder(uri);
} else {
synchronized (propertyLock) {
holder = ConnectionHolder.createURIConnectionHolder(uri, connectionProperties);
}
}
return holder;
}
private String getContentTypeFromFileSignature(URI uri) throws MalformedURLException, IOException {
InputStream stream = getInputStream(uri);
byte[] signature = new byte[MediaUtils.MAX_FILE_SIGNATURE_LENGTH];
int size = stream.read(signature);
stream.close();
return MediaUtils.fileSignatureToContentType(signature, size);
}
static void closeConnection(URLConnection connection) {
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection)connection;
try {
if (httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST &&
httpConnection.getInputStream() != null) {
httpConnection.getInputStream().close();
}
} catch (IOException ex) {
try {
if (httpConnection.getErrorStream() != null) {
httpConnection.getErrorStream().close();
}
} catch (IOException e) {}
}
}
}
}