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.isEmpty()) 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")) return HLS_MEDIA; |
| 186 if (url.contains(".mp4")) return MPEG4_MEDIA; |
| 187 if (url.contains(".mpd")) return DASH_MEDIA; |
| 188 if (url.contains(".ism")) return SMOOTHSTREAM_MEDIA; |
| 189 return UNKNOWN_MEDIA; |
| 190 } |
168 } | 191 } |
OLD | NEW |