Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.chrome.browser.media.remote; | 5 package org.chromium.chrome.browser.media.remote; |
| 6 | 6 |
| 7 import android.content.Context; | 7 import android.content.Context; |
| 8 import android.net.Uri; | 8 import android.net.Uri; |
| 9 import android.os.AsyncTask; | 9 import android.os.AsyncTask; |
| 10 import android.text.TextUtils; | 10 import android.text.TextUtils; |
| 11 import android.util.Log; | 11 import android.util.Log; |
| 12 | 12 |
| 13 import org.apache.http.Header; | 13 import org.chromium.base.CommandLine; |
| 14 import org.apache.http.message.BasicHeader; | 14 import org.chromium.chrome.ChromeSwitches; |
| 15 import org.chromium.chrome.browser.ChromiumApplication; | 15 import org.chromium.chrome.browser.ChromiumApplication; |
| 16 | 16 |
| 17 import java.io.IOException; | 17 import java.io.IOException; |
| 18 import java.net.HttpURLConnection; | 18 import java.net.HttpURLConnection; |
| 19 import java.net.MalformedURLException; | 19 import java.net.MalformedURLException; |
| 20 import java.net.URI; | 20 import java.net.URI; |
| 21 import java.net.URISyntaxException; | 21 import java.net.URISyntaxException; |
| 22 import java.net.URL; | 22 import java.net.URL; |
| 23 import java.util.Arrays; | 23 import java.util.List; |
| 24 import java.util.Map; | |
| 24 | 25 |
| 25 /** | 26 /** |
| 26 * Resolves the final URL if it's a redirect. Works asynchronously, uses HTTP | 27 * Resolves the final URL if it's a redirect. Works asynchronously, uses HTTP |
| 27 * HEAD request to determine if the URL is redirected. | 28 * HEAD request to determine if the URL is redirected. |
| 28 */ | 29 */ |
| 29 public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res ult> { | 30 public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Res ult> { |
| 30 | 31 |
| 31 /** | 32 /** |
| 32 * The interface to get the initial URI with cookies from and pass the final | 33 * The interface to get the initial URI with cookies from and pass the final |
| 33 * URI to. | 34 * URI to. |
| 34 */ | 35 */ |
| 35 public interface Delegate { | 36 public interface Delegate { |
| 36 /** | 37 /** |
| 37 * @return the original URL to resolve. | 38 * @return the original URL to resolve. |
| 38 */ | 39 */ |
| 39 Uri getUri(); | 40 Uri getUri(); |
| 40 | 41 |
| 41 /** | 42 /** |
| 42 * @return the cookies to fetch the URL with. | 43 * @return the cookies to fetch the URL with. |
| 43 */ | 44 */ |
| 44 String getCookies(); | 45 String getCookies(); |
| 45 | 46 |
| 46 /** | 47 /** |
| 47 * Passes the resolved URL to the delegate. | 48 * Passes the resolved URL to the delegate. |
| 48 * | 49 * |
| 49 * @param uri the resolved URL. | 50 * @param uri the resolved URL. |
| 50 */ | 51 */ |
| 51 void setUri(Uri uri, Header[] headers); | 52 void setUri(Uri uri, boolean palyable); |
| 52 } | 53 } |
| 53 | 54 |
| 54 | 55 |
| 55 protected static final class Result { | 56 protected static final class Result { |
| 56 private final String mUri; | 57 private final String mUri; |
| 57 private final Header[] mRelevantHeaders; | 58 private final boolean mPlayable; |
| 58 | 59 |
| 59 public Result(String uri, Header[] relevantHeaders) { | 60 public Result(String uri, boolean playable) { |
| 60 mUri = uri; | 61 mUri = uri; |
| 61 mRelevantHeaders = | 62 mPlayable = playable; |
| 62 relevantHeaders != null | |
| 63 ? Arrays.copyOf(relevantHeaders, relevantHeaders.length) | |
| 64 : null; | |
| 65 } | 63 } |
| 66 | 64 |
| 67 public String getUri() { | 65 public String getUri() { |
| 68 return mUri; | 66 return mUri; |
| 69 } | 67 } |
| 70 | 68 |
| 71 public Header[] getRelevantHeaders() { | 69 public boolean isPlayable() { |
| 72 return mRelevantHeaders != null | 70 return mPlayable; |
| 73 ? Arrays.copyOf(mRelevantHeaders, mRelevantHeaders.length) | |
| 74 : null; | |
| 75 } | 71 } |
| 76 } | 72 } |
| 77 | 73 |
| 78 private static final String TAG = "MediaUrlResolver"; | 74 private static final String TAG = "MediaUrlResolver"; |
| 79 | 75 |
| 80 private static final String CORS_HEADER_NAME = "Access-Control-Allow-Origin" ; | |
| 81 private static final String COOKIES_HEADER_NAME = "Cookies"; | 76 private static final String COOKIES_HEADER_NAME = "Cookies"; |
| 82 private static final String USER_AGENT_HEADER_NAME = "User-Agent"; | 77 private static final String USER_AGENT_HEADER_NAME = "User-Agent"; |
| 83 private static final String RANGE_HEADER_NAME = "Range"; | 78 private static final String RANGE_HEADER_NAME = "Range"; |
| 79 private static final String CORS_HEADER_NAME = "Access-Control-Allow-Origin" ; | |
| 80 | |
| 81 // Media types supported for cast, see | |
| 82 // media/base/container_names.h for the actual enum where these are defined | |
| 83 private static final int UNKNOWN_MEDIA = 0; | |
| 84 private static final int SMOOTHSTREAM_MEDIA = 39; | |
| 85 private static final int DASH_MEDIA = 38; | |
| 86 private static final int HLS_MEDIA = 22; | |
| 87 private static final int MPEG4_MEDIA = 29; | |
| 84 | 88 |
| 85 // We don't want to necessarily fetch the whole video but we don't want to m iss the CORS header. | 89 // We don't want to necessarily fetch the whole video but we don't want to m iss the CORS header. |
| 86 // Assume that 64k should be more than enough to keep all the headers. | 90 // Assume that 64k should be more than enough to keep all the headers. |
| 87 private static final String RANGE_HEADER_VALUE = "bytes: 0-65536"; | 91 private static final String RANGE_HEADER_VALUE = "bytes: 0-65536"; |
| 88 | 92 |
| 89 private final Context mContext; | |
| 90 private final Delegate mDelegate; | 93 private final Delegate mDelegate; |
| 94 private boolean mDebug; | |
| 91 | 95 |
| 92 /** | 96 /** |
| 93 * The constructor | 97 * The constructor |
| 94 * @param context the context to use to resolve the URL | 98 * @param context the context to use to resolve the URL |
| 95 * @param delegate The customer for this URL resolver. | 99 * @param delegate The customer for this URL resolver. |
| 96 */ | 100 */ |
| 97 public MediaUrlResolver(Context context, Delegate delegate) { | 101 public MediaUrlResolver(Context context, Delegate delegate) { |
| 98 mContext = context; | 102 mDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_ DEBUG_LOGS); |
| 99 mDelegate = delegate; | 103 mDelegate = delegate; |
| 100 } | 104 } |
| 101 | 105 |
| 102 @Override | 106 @Override |
| 103 protected MediaUrlResolver.Result doInBackground(Void... params) { | 107 protected MediaUrlResolver.Result doInBackground(Void... params) { |
| 104 Uri uri = mDelegate.getUri(); | 108 Uri uri = mDelegate.getUri(); |
| 105 String url = uri.toString(); | 109 String url = uri.toString(); |
| 106 Header[] relevantHeaders = null; | |
| 107 String cookies = mDelegate.getCookies(); | 110 String cookies = mDelegate.getCookies(); |
| 108 String userAgent = ChromiumApplication.getBrowserUserAgent(); | 111 String userAgent = ChromiumApplication.getBrowserUserAgent(); |
| 109 // URL may already be partially percent encoded; double percent encoding will break | 112 // URL may already be partially percent encoded; double percent encoding will break |
| 110 // things, so decode it before sanitizing it. | 113 // things, so decode it before sanitizing it. |
| 111 String sanitizedUrl = sanitizeUrl(Uri.decode(url)); | 114 String sanitizedUrl = sanitizeUrl(Uri.decode(url)); |
| 115 Map<String, List<String>> headers = null; | |
| 112 | 116 |
| 113 // If we failed to sanitize the URL (e.g. because the host name contains underscores) then | 117 // If we failed to sanitize the URL (e.g. because the host name contains underscores) then |
| 114 // HttpURLConnection won't work,so we can't follow redirections. Just tr y to use it as is. | 118 // HttpURLConnection won't work,so we can't follow redirections. Just tr y to use it as is. |
| 115 // TODO (aberent): Find out if there is a way of following redirections that is not so | 119 // TODO (aberent): Find out if there is a way of following redirections that is not so |
| 116 // strict on the URL format. | 120 // strict on the URL format. |
| 117 if (!sanitizedUrl.equals("")) { | 121 if (!sanitizedUrl.equals("")) { |
| 118 HttpURLConnection urlConnection = null; | 122 HttpURLConnection urlConnection = null; |
| 119 try { | 123 try { |
| 120 URL requestUrl = new URL(sanitizedUrl); | 124 URL requestUrl = new URL(sanitizedUrl); |
| 121 urlConnection = (HttpURLConnection) requestUrl.openConnection(); | 125 urlConnection = (HttpURLConnection) requestUrl.openConnection(); |
| 122 if (!TextUtils.isEmpty(cookies)) { | 126 if (!TextUtils.isEmpty(cookies)) { |
| 123 urlConnection.setRequestProperty(COOKIES_HEADER_NAME, cookie s); | 127 urlConnection.setRequestProperty(COOKIES_HEADER_NAME, cookie s); |
| 124 } | 128 } |
| 125 urlConnection.setRequestProperty(USER_AGENT_HEADER_NAME, userAge nt); | 129 urlConnection.setRequestProperty(USER_AGENT_HEADER_NAME, userAge nt); |
| 126 urlConnection.setRequestProperty(RANGE_HEADER_NAME, RANGE_HEADER _VALUE); | 130 urlConnection.setRequestProperty(RANGE_HEADER_NAME, RANGE_HEADER _VALUE); |
| 127 | 131 |
| 128 // This triggers resolving the URL and receiving the headers. | 132 // This triggers resolving the URL and receiving the headers. |
| 129 urlConnection.getHeaderFields(); | 133 headers = urlConnection.getHeaderFields(); |
| 130 | 134 |
| 131 url = urlConnection.getURL().toString(); | 135 url = urlConnection.getURL().toString(); |
| 132 String corsHeader = urlConnection.getHeaderField(CORS_HEADER_NAM E); | |
| 133 if (corsHeader != null) { | |
| 134 relevantHeaders = new Header[1]; | |
| 135 relevantHeaders[0] = new BasicHeader(CORS_HEADER_NAME, corsH eader); | |
| 136 } | |
| 137 } catch (IOException e) { | 136 } catch (IOException e) { |
| 138 Log.e(TAG, "Failed to fetch the final URI", e); | 137 Log.e(TAG, "Failed to fetch the final URI", e); |
| 139 url = ""; | 138 url = ""; |
| 140 } | 139 } |
| 141 if (urlConnection != null) urlConnection.disconnect(); | 140 if (urlConnection != null) urlConnection.disconnect(); |
| 142 } | 141 } |
| 143 return new MediaUrlResolver.Result(url, relevantHeaders); | 142 return new MediaUrlResolver.Result(url, canPlayMedia(url, headers)); |
| 144 } | 143 } |
| 145 | 144 |
| 146 @Override | 145 @Override |
| 147 protected void onPostExecute(MediaUrlResolver.Result result) { | 146 protected void onPostExecute(MediaUrlResolver.Result result) { |
| 148 String url = result.getUri(); | 147 String url = result.getUri(); |
| 149 Uri uri = "".equals(url) ? Uri.EMPTY : Uri.parse(url); | 148 Uri uri = "".equals(url) ? Uri.EMPTY : Uri.parse(url); |
| 150 mDelegate.setUri(uri, result.getRelevantHeaders()); | 149 mDelegate.setUri(uri, result.isPlayable()); |
| 151 } | 150 } |
| 152 | 151 |
| 153 private String sanitizeUrl(String unsafeUrl) { | 152 private String sanitizeUrl(String unsafeUrl) { |
| 154 URL url; | 153 URL url; |
| 155 URI uri; | 154 URI uri; |
| 156 try { | 155 try { |
| 157 url = new URL(unsafeUrl); | 156 url = new URL(unsafeUrl); |
| 158 uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), u rl.getPort(), | 157 uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), u rl.getPort(), |
| 159 url.getPath(), url.getQuery(), url.getRef()); | 158 url.getPath(), url.getQuery(), url.getRef()); |
| 160 return uri.toURL().toString(); | 159 return uri.toURL().toString(); |
| 161 } catch (URISyntaxException syntaxException) { | 160 } catch (URISyntaxException syntaxException) { |
| 162 Log.w(TAG, "URISyntaxException " + syntaxException); | 161 Log.w(TAG, "URISyntaxException " + syntaxException); |
| 163 } catch (MalformedURLException malformedUrlException) { | 162 } catch (MalformedURLException malformedUrlException) { |
| 164 Log.w(TAG, "MalformedURLException " + malformedUrlException); | 163 Log.w(TAG, "MalformedURLException " + malformedUrlException); |
| 165 } | 164 } |
| 166 return ""; | 165 return ""; |
| 167 } | 166 } |
| 167 | |
| 168 private boolean canPlayMedia(String url, Map<String, List<String>> headers) { | |
| 169 if (url == "") return false; | |
| 170 | |
| 171 // HLS media requires Cors headers. | |
| 172 if ((headers == null || isEnhancedMedia(url) && !headers.containsKey(COR S_HEADER_NAME))) { | |
| 173 if (mDebug) Log.d(TAG, "HLS stream without CORs header: " + url); | |
| 174 return false; | |
| 175 } | |
| 176 return true; | |
| 177 } | |
| 178 | |
| 179 private boolean isEnhancedMedia(String url) { | |
| 180 int mediaType = getMediaType(url); | |
| 181 return mediaType == HLS_MEDIA || mediaType == DASH_MEDIA || mediaType == SMOOTHSTREAM_MEDIA; | |
| 182 } | |
| 183 | |
| 184 static int getMediaType(String url) { | |
| 185 if (url.contains(".m3u8")) { | |
| 186 return HLS_MEDIA; | |
|
whywhat
2015/03/30 17:34:02
nit: these if-s are probably one liners?
aberent
2015/03/31 09:18:08
Done.
| |
| 187 } | |
| 188 if (url.contains(".mp4")) { | |
| 189 return MPEG4_MEDIA; | |
| 190 } | |
| 191 if (url.contains(".mpd")) { | |
| 192 return DASH_MEDIA; | |
| 193 } | |
| 194 if (url.contains(".ism")) { | |
| 195 return SMOOTHSTREAM_MEDIA; | |
| 196 } | |
| 197 return UNKNOWN_MEDIA; | |
| 198 } | |
| 168 } | 199 } |
| OLD | NEW |