| Index: chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java
|
| index 0c9e7b37bbce9b6225cc710787747102640abb72..c4746c5b64ff8469f890043092a65293b89e5d0d 100644
|
| --- a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java
|
| @@ -7,21 +7,20 @@ package org.chromium.chrome.browser.media.remote;
|
| import android.net.Uri;
|
| import android.os.AsyncTask;
|
| import android.text.TextUtils;
|
| -import android.util.Log;
|
|
|
| import org.chromium.base.CommandLine;
|
| +import org.chromium.base.Log;
|
| import org.chromium.base.VisibleForTesting;
|
| import org.chromium.base.metrics.RecordHistogram;
|
| import org.chromium.chrome.browser.ChromeSwitches;
|
|
|
| import java.io.IOException;
|
| import java.net.HttpURLConnection;
|
| -import java.net.MalformedURLException;
|
| -import java.net.URI;
|
| -import java.net.URISyntaxException;
|
| import java.net.URL;
|
| import java.net.URLStreamHandler;
|
| +import java.util.Arrays;
|
| import java.util.List;
|
| +import java.util.Locale;
|
| import java.util.Map;
|
|
|
| /**
|
| @@ -33,12 +32,27 @@ public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res
|
| // Cast.Sender.UrlResolveResult UMA histogram values; must match values of
|
| // RemotePlaybackUrlResolveResult in histograms.xml. Do not change these values, as they are
|
| // being used in UMA.
|
| - private static final int RESOLVE_SUCCESSFUL = 0;
|
| - private static final int MALFORMED_URL = 1;
|
| - private static final int NO_CORS = 2;
|
| - private static final int INCOMPATIBLE_CORS = 3;
|
| + private static final int RESOLVE_RESULT_SUCCESS = 0;
|
| + private static final int RESOLVE_RESULT_MALFORMED_URL = 1;
|
| + private static final int RESOLVE_RESULT_NO_CORS = 2;
|
| + private static final int RESOLVE_RESULT_INCOMPATIBLE_CORS = 3;
|
| + private static final int RESOLVE_RESULT_SERVER_ERROR = 4;
|
| + private static final int RESOLVE_RESULT_NETWORK_ERROR = 5;
|
| + private static final int RESOLVE_RESULT_UNSUPPORTED_MEDIA = 6;
|
| +
|
| // Range of histogram.
|
| - private static final int HISTOGRAM_RESULT_COUNT = 4;
|
| + private static final int HISTOGRAM_RESULT_COUNT = 7;
|
| +
|
| + // Acceptal response codes for URL resolving request.
|
| + private static final Integer[] SUCCESS_RESPONSE_CODES = {
|
| + // Request succeeded.
|
| + HttpURLConnection.HTTP_OK,
|
| + HttpURLConnection.HTTP_PARTIAL,
|
| +
|
| + // HttpURLConnection only follows up to 5 redirects, this response is unlikely but possible.
|
| + HttpURLConnection.HTTP_MOVED_PERM,
|
| + HttpURLConnection.HTTP_MOVED_TEMP,
|
| + };
|
|
|
| /**
|
| * The interface to get the initial URI with cookies from and pass the final
|
| @@ -65,19 +79,19 @@ public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res
|
|
|
|
|
| protected static final class Result {
|
| - private final String mUri;
|
| + private final Uri mUri;
|
| private final boolean mPlayable;
|
|
|
| - public Result(String uri, boolean playable) {
|
| + public Result(Uri uri, boolean playable) {
|
| mUri = uri;
|
| mPlayable = playable;
|
| }
|
|
|
| - public String getUri() {
|
| + public Uri getUri() {
|
| return mUri;
|
| }
|
|
|
| - public boolean isPlayable() {
|
| + public boolean isPlayable() {
|
| return mPlayable;
|
| }
|
| }
|
| @@ -93,12 +107,19 @@ public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res
|
| private static final String CHROMECAST_ORIGIN = "https://www.gstatic.com";
|
|
|
| // Media types supported for cast, see
|
| - // media/base/container_names.h for the actual enum where these are defined
|
| - private static final int UNKNOWN_MEDIA = 0;
|
| - private static final int SMOOTHSTREAM_MEDIA = 39;
|
| - private static final int DASH_MEDIA = 38;
|
| - private static final int HLS_MEDIA = 22;
|
| - private static final int MPEG4_MEDIA = 29;
|
| + // media/base/container_names.h for the actual enum where these are defined.
|
| + // See https://developers.google.com/cast/docs/media#media-container-formats for the formats
|
| + // supported by Cast devices.
|
| + private static final int MEDIA_TYPE_UNKNOWN = 0;
|
| + private static final int MEDIA_TYPE_AAC = 1;
|
| + private static final int MEDIA_TYPE_HLS = 22;
|
| + private static final int MEDIA_TYPE_MP3 = 26;
|
| + private static final int MEDIA_TYPE_MPEG4 = 29;
|
| + private static final int MEDIA_TYPE_OGG = 30;
|
| + private static final int MEDIA_TYPE_WAV = 35;
|
| + private static final int MEDIA_TYPE_WEBM = 36;
|
| + private static final int MEDIA_TYPE_DASH = 38;
|
| + private static final int MEDIA_TYPE_SMOOTHSTREAM = 39;
|
|
|
| // We don't want to necessarily fetch the whole video but we don't want to miss the CORS header.
|
| // Assume that 64k should be more than enough to keep all the headers.
|
| @@ -130,72 +151,57 @@ public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res
|
| @Override
|
| protected MediaUrlResolver.Result doInBackground(Void... params) {
|
| Uri uri = mDelegate.getUri();
|
| - if (uri == null) {
|
| - return new MediaUrlResolver.Result("", false);
|
| + if (uri == null || uri.equals(Uri.EMPTY)) {
|
| + return new MediaUrlResolver.Result(Uri.EMPTY, false);
|
| }
|
| - String url = uri.toString();
|
| String cookies = mDelegate.getCookies();
|
| - // URL may already be partially percent encoded; double percent encoding will break
|
| - // things, so decode it before sanitizing it.
|
| - String sanitizedUrl = sanitizeUrl(Uri.decode(url));
|
| +
|
| Map<String, List<String>> headers = null;
|
| + HttpURLConnection urlConnection = null;
|
| + try {
|
| + URL requestUrl = new URL(null, uri.toString(), mStreamHandler);
|
| + urlConnection = (HttpURLConnection) requestUrl.openConnection();
|
| + if (!TextUtils.isEmpty(cookies)) {
|
| + urlConnection.setRequestProperty(COOKIES_HEADER_NAME, cookies);
|
| + }
|
|
|
| - // If we failed to sanitize the URL (e.g. because the host name contains underscores) then
|
| - // HttpURLConnection won't work,so we can't follow redirections. Just try to use it as is.
|
| - // TODO (aberent): Find out if there is a way of following redirections that is not so
|
| - // strict on the URL format.
|
| - if (!sanitizedUrl.equals("")) {
|
| - HttpURLConnection urlConnection = null;
|
| - try {
|
| - URL requestUrl = new URL(null, sanitizedUrl, mStreamHandler);
|
| - urlConnection = (HttpURLConnection) requestUrl.openConnection();
|
| - if (!TextUtils.isEmpty(cookies)) {
|
| - urlConnection.setRequestProperty(COOKIES_HEADER_NAME, cookies);
|
| - }
|
| - // Pretend that this is coming from the Chromecast.
|
| - urlConnection.setRequestProperty(ORIGIN_HEADER_NAME, CHROMECAST_ORIGIN);
|
| - urlConnection.setRequestProperty(USER_AGENT_HEADER_NAME, mUserAgent);
|
| + // Pretend that this is coming from the Chromecast.
|
| + urlConnection.setRequestProperty(ORIGIN_HEADER_NAME, CHROMECAST_ORIGIN);
|
| + urlConnection.setRequestProperty(USER_AGENT_HEADER_NAME, mUserAgent);
|
| + if (!isEnhancedMedia(uri)) {
|
| + // Manifest files are typically smaller than 64K so range request can fail.
|
| urlConnection.setRequestProperty(RANGE_HEADER_NAME, RANGE_HEADER_VALUE);
|
| + }
|
| +
|
| + // This triggers resolving the URL and receiving the headers.
|
| + headers = urlConnection.getHeaderFields();
|
|
|
| - // This triggers resolving the URL and receiving the headers.
|
| - headers = urlConnection.getHeaderFields();
|
| + uri = Uri.parse(urlConnection.getURL().toString());
|
|
|
| - url = urlConnection.getURL().toString();
|
| - } catch (IOException e) {
|
| - Log.e(TAG, "Failed to fetch the final URI", e);
|
| - url = "";
|
| + // If server's response is not valid, don't try to fling the video.
|
| + int responseCode = urlConnection.getResponseCode();
|
| + if (!Arrays.asList(SUCCESS_RESPONSE_CODES).contains(responseCode)) {
|
| + recordResultHistogram(RESOLVE_RESULT_SERVER_ERROR);
|
| + Log.e(TAG, "Server response is not valid: %d", responseCode);
|
| + uri = Uri.EMPTY;
|
| }
|
| - if (urlConnection != null) urlConnection.disconnect();
|
| + } catch (IOException e) {
|
| + recordResultHistogram(RESOLVE_RESULT_NETWORK_ERROR);
|
| + Log.e(TAG, "Failed to fetch the final url", e);
|
| + uri = Uri.EMPTY;
|
| }
|
| - return new MediaUrlResolver.Result(url, canPlayMedia(url, headers));
|
| + if (urlConnection != null) urlConnection.disconnect();
|
| + return new MediaUrlResolver.Result(uri, canPlayMedia(uri, headers));
|
| }
|
|
|
| @Override
|
| protected void onPostExecute(MediaUrlResolver.Result result) {
|
| - String url = result.getUri();
|
| - Uri uri = "".equals(url) ? Uri.EMPTY : Uri.parse(url);
|
| - mDelegate.deliverResult(uri, result.isPlayable());
|
| + mDelegate.deliverResult(result.getUri(), result.isPlayable());
|
| }
|
|
|
| - private String sanitizeUrl(String unsafeUrl) {
|
| - URL url;
|
| - URI uri;
|
| - try {
|
| - url = new URL(unsafeUrl);
|
| - uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(),
|
| - url.getPath(), url.getQuery(), url.getRef());
|
| - return uri.toURL().toString();
|
| - } catch (URISyntaxException syntaxException) {
|
| - Log.w(TAG, "URISyntaxException " + syntaxException);
|
| - } catch (MalformedURLException malformedUrlException) {
|
| - Log.w(TAG, "MalformedURLException " + malformedUrlException);
|
| - }
|
| - return "";
|
| - }
|
| -
|
| - private boolean canPlayMedia(String url, Map<String, List<String>> headers) {
|
| - if (url.isEmpty()) {
|
| - recordResultHistogram(MALFORMED_URL);
|
| + private boolean canPlayMedia(Uri uri, Map<String, List<String>> headers) {
|
| + if (uri == null || uri.equals(Uri.EMPTY)) {
|
| + recordResultHistogram(RESOLVE_RESULT_MALFORMED_URL);
|
| return false;
|
| }
|
|
|
| @@ -204,24 +210,34 @@ public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res
|
| List<String> corsData = headers.get(CORS_HEADER_NAME);
|
| if (corsData.isEmpty() || (!corsData.get(0).equals("*")
|
| && !corsData.get(0).equals(CHROMECAST_ORIGIN))) {
|
| - recordResultHistogram(INCOMPATIBLE_CORS);
|
| + recordResultHistogram(RESOLVE_RESULT_INCOMPATIBLE_CORS);
|
| return false;
|
| }
|
| - } else if (isEnhancedMedia(url)) {
|
| - // HLS media requires Cors headers.
|
| - if (mDebug) Log.d(TAG, "HLS stream without CORS header: " + url);
|
| - recordResultHistogram(NO_CORS);
|
| + } else if (isEnhancedMedia(uri)) {
|
| + // HLS media requires CORS headers.
|
| + // TODO(avayvod): it actually requires CORS on the final video URLs vs the manifest.
|
| + // Clank assumes that if CORS is set for the manifest it's set for everything but
|
| + // it not necessary always true. See b/19138712
|
| + Log.d(TAG, "HLS stream without CORS header: %s", uri);
|
| + recordResultHistogram(RESOLVE_RESULT_NO_CORS);
|
| return false;
|
| }
|
| - // TODO(aberent) Return false for media types that are not playable on Chromecast
|
| - // (getMediaType would need to know about more types to implement this).
|
| - recordResultHistogram(RESOLVE_SUCCESSFUL);
|
| +
|
| + if (getMediaType(uri) == MEDIA_TYPE_UNKNOWN) {
|
| + Log.d(TAG, "Unsupported media container format: %s", uri);
|
| + recordResultHistogram(RESOLVE_RESULT_UNSUPPORTED_MEDIA);
|
| + return false;
|
| + }
|
| +
|
| + recordResultHistogram(RESOLVE_RESULT_SUCCESS);
|
| return true;
|
| }
|
|
|
| - private boolean isEnhancedMedia(String url) {
|
| - int mediaType = getMediaType(url);
|
| - return mediaType == HLS_MEDIA || mediaType == DASH_MEDIA || mediaType == SMOOTHSTREAM_MEDIA;
|
| + private boolean isEnhancedMedia(Uri uri) {
|
| + int mediaType = getMediaType(uri);
|
| + return mediaType == MEDIA_TYPE_HLS
|
| + || mediaType == MEDIA_TYPE_DASH
|
| + || mediaType == MEDIA_TYPE_SMOOTHSTREAM;
|
| }
|
|
|
| @VisibleForTesting
|
| @@ -230,11 +246,17 @@ public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res
|
| HISTOGRAM_RESULT_COUNT);
|
| }
|
|
|
| - static int getMediaType(String url) {
|
| - if (url.contains(".m3u8")) return HLS_MEDIA;
|
| - if (url.contains(".mp4")) return MPEG4_MEDIA;
|
| - if (url.contains(".mpd")) return DASH_MEDIA;
|
| - if (url.contains(".ism")) return SMOOTHSTREAM_MEDIA;
|
| - return UNKNOWN_MEDIA;
|
| + static int getMediaType(Uri uri) {
|
| + String path = uri.getPath().toLowerCase(Locale.US);
|
| + if (path.endsWith(".m3u8")) return MEDIA_TYPE_HLS;
|
| + if (path.endsWith(".mp4")) return MEDIA_TYPE_MPEG4;
|
| + if (path.endsWith(".mpd")) return MEDIA_TYPE_DASH;
|
| + if (path.endsWith(".ism")) return MEDIA_TYPE_SMOOTHSTREAM;
|
| + if (path.endsWith(".m4a") || path.endsWith(".aac")) return MEDIA_TYPE_AAC;
|
| + if (path.endsWith(".mp3")) return MEDIA_TYPE_MP3;
|
| + if (path.endsWith(".wav")) return MEDIA_TYPE_WAV;
|
| + if (path.endsWith(".webm")) return MEDIA_TYPE_WEBM;
|
| + if (path.endsWith(".ogg")) return MEDIA_TYPE_OGG;
|
| + return MEDIA_TYPE_UNKNOWN;
|
| }
|
| -}
|
| +}
|
|
|