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
3 // found in the LICENSE file.
5 package org.chromium.media;
7 import android.content.Context;
8 import android.media.MediaPlayer;
9 import android.net.Uri;
10 import android.os.AsyncTask;
11 import android.os.Build;
12 import android.os.ParcelFileDescriptor;
13 import android.text.TextUtils;
14 import android.util.Base64;
15 import android.util.Base64InputStream;
16 import android.util.Log;
17 import android.view.Surface;
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.JNINamespace;
22 import java.io.ByteArrayInputStream;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.lang.reflect.InvocationTargetException;
28 import java.lang.reflect.Method;
29 import java.util.HashMap;
32 * A wrapper around android.media.MediaPlayer that allows the native code to use it.
33 * See media/base/android/media_player_bridge.cc for the corresponding native code.
35 @JNINamespace("media")
36 public class MediaPlayerBridge {
38 public static class ResourceLoadingFilter {
39 public boolean shouldOverrideResourceLoading(
40 MediaPlayer mediaPlayer, Context context, Uri uri) {
45 private static ResourceLoadingFilter sResourceLoadFilter = null;
47 public static void setResourceLoadingFilter(ResourceLoadingFilter filter) {
48 sResourceLoadFilter = filter;
51 private static final String TAG = "MediaPlayerBridge";
53 // Local player to forward this to. We don't initialize it here since the subclass might not
55 private LoadDataUriTask mLoadDataUriTask;
56 private MediaPlayer mPlayer;
57 private long mNativeMediaPlayerBridge;
60 private static MediaPlayerBridge create(long nativeMediaPlayerBridge) {
61 return new MediaPlayerBridge(nativeMediaPlayerBridge);
64 protected MediaPlayerBridge(long nativeMediaPlayerBridge) {
65 mNativeMediaPlayerBridge = nativeMediaPlayerBridge;
68 protected MediaPlayerBridge() {
72 protected void destroy() {
73 if (mLoadDataUriTask != null) {
74 mLoadDataUriTask.cancel(true);
75 mLoadDataUriTask = null;
77 mNativeMediaPlayerBridge = 0;
80 protected MediaPlayer getLocalPlayer() {
81 if (mPlayer == null) {
82 mPlayer = new MediaPlayer();
88 protected void setSurface(Surface surface) {
89 getLocalPlayer().setSurface(surface);
93 protected boolean prepareAsync() {
95 getLocalPlayer().prepareAsync();
96 } catch (IllegalStateException e) {
97 Log.e(TAG, "Unable to prepare MediaPlayer.", e);
104 protected boolean isPlaying() {
105 return getLocalPlayer().isPlaying();
109 protected int getVideoWidth() {
110 return getLocalPlayer().getVideoWidth();
114 protected int getVideoHeight() {
115 return getLocalPlayer().getVideoHeight();
119 protected int getCurrentPosition() {
120 return getLocalPlayer().getCurrentPosition();
124 protected int getDuration() {
125 return getLocalPlayer().getDuration();
129 protected void release() {
130 getLocalPlayer().release();
134 protected void setVolume(double volume) {
135 getLocalPlayer().setVolume((float) volume, (float) volume);
139 protected void start() {
140 getLocalPlayer().start();
144 protected void pause() {
145 getLocalPlayer().pause();
149 protected void seekTo(int msec) throws IllegalStateException {
150 getLocalPlayer().seekTo(msec);
154 protected boolean setDataSource(
155 Context context, String url, String cookies, String userAgent, boolean hideUrlLog) {
156 Uri uri = Uri.parse(url);
157 HashMap<String, String> headersMap = new HashMap<String, String>();
158 if (hideUrlLog) headersMap.put("x-hide-urls-from-log", "true");
159 if (!TextUtils.isEmpty(cookies)) headersMap.put("Cookie", cookies);
160 if (!TextUtils.isEmpty(userAgent)) headersMap.put("User-Agent", userAgent);
161 // The security origin check is enforced for devices above K. For devices below K,
162 // only anonymous media HTTP request (no cookies) may be considered same-origin.
163 // Note that if the server rejects the request we must not consider it same-origin.
164 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
165 headersMap.put("allow-cross-domain-redirect", "false");
168 if (sResourceLoadFilter != null &&
169 sResourceLoadFilter.shouldOverrideResourceLoading(
170 getLocalPlayer(), context, uri)) {
173 getLocalPlayer().setDataSource(context, uri, headersMap);
175 } catch (Exception e) {
181 protected boolean setDataSourceFromFd(int fd, long offset, long length) {
183 ParcelFileDescriptor parcelFd = ParcelFileDescriptor.adoptFd(fd);
184 getLocalPlayer().setDataSource(parcelFd.getFileDescriptor(), offset, length);
187 } catch (IOException e) {
188 Log.e(TAG, "Failed to set data source from file descriptor: " + e);
194 protected boolean setDataUriDataSource(final Context context, final String url) {
195 if (mLoadDataUriTask != null) {
196 mLoadDataUriTask.cancel(true);
197 mLoadDataUriTask = null;
200 if (!url.startsWith("data:")) return false;
201 int headerStop = url.indexOf(',');
202 if (headerStop == -1) return false;
203 String header = url.substring(0, headerStop);
204 final String data = url.substring(headerStop + 1);
206 String headerContent = header.substring(5);
207 String headerInfo[] = headerContent.split(";");
208 if (headerInfo.length != 2) return false;
209 if (!"base64".equals(headerInfo[1])) return false;
211 mLoadDataUriTask = new LoadDataUriTask(context, data);
212 mLoadDataUriTask.execute();
216 private class LoadDataUriTask extends AsyncTask <Void, Void, Boolean> {
217 private final String mData;
218 private final Context mContext;
219 private File mTempFile;
221 public LoadDataUriTask(Context context, String data) {
227 protected Boolean doInBackground(Void... params) {
228 FileOutputStream fos = null;
230 mTempFile = File.createTempFile("decoded", "mediadata");
231 fos = new FileOutputStream(mTempFile);
232 InputStream stream = new ByteArrayInputStream(mData.getBytes());
233 Base64InputStream decoder = new Base64InputStream(stream, Base64.DEFAULT);
234 byte[] buffer = new byte[1024];
236 while ((len = decoder.read(buffer)) != -1) {
237 fos.write(buffer, 0, len);
241 } catch (IOException e) {
245 if (fos != null) fos.close();
246 } catch (IOException e) {
247 // Can't do anything.
253 protected void onPostExecute(Boolean result) {
260 getLocalPlayer().setDataSource(mContext, Uri.fromFile(mTempFile));
261 } catch (IOException e) {
266 assert (mNativeMediaPlayerBridge != 0);
267 nativeOnDidSetDataUriDataSource(mNativeMediaPlayerBridge, result);
270 private void deleteFile() {
271 if (mTempFile == null) return;
272 if (!mTempFile.delete()) {
273 // File will be deleted when MediaPlayer releases its handler.
274 Log.e(TAG, "Failed to delete temporary file: " + mTempFile);
280 protected void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener) {
281 getLocalPlayer().setOnBufferingUpdateListener(listener);
284 protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
285 getLocalPlayer().setOnCompletionListener(listener);
288 protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
289 getLocalPlayer().setOnErrorListener(listener);
292 protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
293 getLocalPlayer().setOnPreparedListener(listener);
296 protected void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener) {
297 getLocalPlayer().setOnSeekCompleteListener(listener);
300 protected void setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener) {
301 getLocalPlayer().setOnVideoSizeChangedListener(listener);
304 protected static class AllowedOperations {
305 private final boolean mCanPause;
306 private final boolean mCanSeekForward;
307 private final boolean mCanSeekBackward;
309 public AllowedOperations(boolean canPause, boolean canSeekForward,
310 boolean canSeekBackward) {
311 mCanPause = canPause;
312 mCanSeekForward = canSeekForward;
313 mCanSeekBackward = canSeekBackward;
316 @CalledByNative("AllowedOperations")
317 private boolean canPause() { return mCanPause; }
319 @CalledByNative("AllowedOperations")
320 private boolean canSeekForward() { return mCanSeekForward; }
322 @CalledByNative("AllowedOperations")
323 private boolean canSeekBackward() { return mCanSeekBackward; }
327 * Returns an AllowedOperations object to show all the operations that are
328 * allowed on the media player.
331 protected AllowedOperations getAllowedOperations() {
332 MediaPlayer player = getLocalPlayer();
333 boolean canPause = true;
334 boolean canSeekForward = true;
335 boolean canSeekBackward = true;
337 Method getMetadata = player.getClass().getDeclaredMethod(
338 "getMetadata", boolean.class, boolean.class);
339 getMetadata.setAccessible(true);
340 Object data = getMetadata.invoke(player, false, false);
342 Class<?> metadataClass = data.getClass();
343 Method hasMethod = metadataClass.getDeclaredMethod("has", int.class);
344 Method getBooleanMethod = metadataClass.getDeclaredMethod("getBoolean", int.class);
346 int pause = (Integer) metadataClass.getField("PAUSE_AVAILABLE").get(null);
348 (Integer) metadataClass.getField("SEEK_FORWARD_AVAILABLE").get(null);
350 (Integer) metadataClass.getField("SEEK_BACKWARD_AVAILABLE").get(null);
351 hasMethod.setAccessible(true);
352 getBooleanMethod.setAccessible(true);
353 canPause = !((Boolean) hasMethod.invoke(data, pause))
354 || ((Boolean) getBooleanMethod.invoke(data, pause));
355 canSeekForward = !((Boolean) hasMethod.invoke(data, seekForward))
356 || ((Boolean) getBooleanMethod.invoke(data, seekForward));
357 canSeekBackward = !((Boolean) hasMethod.invoke(data, seekBackward))
358 || ((Boolean) getBooleanMethod.invoke(data, seekBackward));
360 } catch (NoSuchMethodException e) {
361 Log.e(TAG, "Cannot find getMetadata() method: " + e);
362 } catch (InvocationTargetException e) {
363 Log.e(TAG, "Cannot invoke MediaPlayer.getMetadata() method: " + e);
364 } catch (IllegalAccessException e) {
365 Log.e(TAG, "Cannot access metadata: " + e);
366 } catch (NoSuchFieldException e) {
367 Log.e(TAG, "Cannot find matching fields in Metadata class: " + e);
369 return new AllowedOperations(canPause, canSeekForward, canSeekBackward);
372 private native void nativeOnDidSetDataUriDataSource(long nativeMediaPlayerBridge,