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. 4 5 package org.chromium.media; 6 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; 18 19 import org.chromium.base.CalledByNative; 20 import org.chromium.base.JNINamespace; 21 22 import java.io.ByteArrayInputStream; 23 import java.io.File; 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; 30 31 /** 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. 34 */ 35 @JNINamespace("media") 36 public class MediaPlayerBridge { 37 38 private static final String TAG = "MediaPlayerBridge"; 39 40 // Local player to forward this to. We don't initialize it here since the subclass might not 41 // want it. 42 private LoadDataUriTask mLoadDataUriTask; 43 private MediaPlayer mPlayer; 44 private long mNativeMediaPlayerBridge; 45 46 @CalledByNative 47 private static MediaPlayerBridge create(long nativeMediaPlayerBridge) { 48 return new MediaPlayerBridge(nativeMediaPlayerBridge); 49 } 50 51 protected MediaPlayerBridge(long nativeMediaPlayerBridge) { 52 mNativeMediaPlayerBridge = nativeMediaPlayerBridge; 53 } 54 55 protected MediaPlayerBridge() { 56 } 57 58 @CalledByNative 59 protected void destroy() { 60 if (mLoadDataUriTask != null) { 61 mLoadDataUriTask.cancel(true); 62 mLoadDataUriTask = null; 63 } 64 mNativeMediaPlayerBridge = 0; 65 } 66 67 protected MediaPlayer getLocalPlayer() { 68 if (mPlayer == null) { 69 mPlayer = new MediaPlayer(); 70 } 71 return mPlayer; 72 } 73 74 @CalledByNative 75 protected void setSurface(Surface surface) { 76 getLocalPlayer().setSurface(surface); 77 } 78 79 @CalledByNative 80 protected boolean prepareAsync() { 81 try { 82 getLocalPlayer().prepareAsync(); 83 } catch (IllegalStateException e) { 84 Log.e(TAG, "Unable to prepare MediaPlayer.", e); 85 return false; 86 } 87 return true; 88 } 89 90 @CalledByNative 91 protected boolean isPlaying() { 92 return getLocalPlayer().isPlaying(); 93 } 94 95 @CalledByNative 96 protected int getVideoWidth() { 97 return getLocalPlayer().getVideoWidth(); 98 } 99 100 @CalledByNative 101 protected int getVideoHeight() { 102 return getLocalPlayer().getVideoHeight(); 103 } 104 105 @CalledByNative 106 protected int getCurrentPosition() { 107 return getLocalPlayer().getCurrentPosition(); 108 } 109 110 @CalledByNative 111 protected int getDuration() { 112 return getLocalPlayer().getDuration(); 113 } 114 115 @CalledByNative 116 protected void release() { 117 getLocalPlayer().release(); 118 } 119 120 @CalledByNative 121 protected void setVolume(double volume) { 122 getLocalPlayer().setVolume((float) volume, (float) volume); 123 } 124 125 @CalledByNative 126 protected void start() { 127 getLocalPlayer().start(); 128 } 129 130 @CalledByNative 131 protected void pause() { 132 getLocalPlayer().pause(); 133 } 134 135 @CalledByNative 136 protected void seekTo(int msec) throws IllegalStateException { 137 getLocalPlayer().seekTo(msec); 138 } 139 140 @CalledByNative 141 protected boolean setDataSource( 142 Context context, String url, String cookies, String userAgent, boolean hideUrlLog) { 143 Uri uri = Uri.parse(url); 144 HashMap<String, String> headersMap = new HashMap<String, String>(); 145 if (hideUrlLog) headersMap.put("x-hide-urls-from-log", "true"); 146 if (!TextUtils.isEmpty(cookies)) headersMap.put("Cookie", cookies); 147 if (!TextUtils.isEmpty(userAgent)) headersMap.put("User-Agent", userAgent); 148 // The security origin check is enforced for devices above K. For devices below K, 149 // only anonymous media HTTP request (no cookies) may be considered same-origin. 150 // Note that if the server rejects the request we must not consider it same-origin. 151 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { 152 headersMap.put("allow-cross-domain-redirect", "false"); 153 } 154 try { 155 getLocalPlayer().setDataSource(context, uri, headersMap); 156 return true; 157 } catch (Exception e) { 158 return false; 159 } 160 } 161 162 @CalledByNative 163 protected boolean setDataSourceFromFd(int fd, long offset, long length) { 164 try { 165 ParcelFileDescriptor parcelFd = ParcelFileDescriptor.adoptFd(fd); 166 getLocalPlayer().setDataSource(parcelFd.getFileDescriptor(), offset, length); 167 parcelFd.close(); 168 return true; 169 } catch (IOException e) { 170 Log.e(TAG, "Failed to set data source from file descriptor: " + e); 171 return false; 172 } 173 } 174 175 @CalledByNative 176 protected boolean setDataUriDataSource(final Context context, final String url) { 177 if (mLoadDataUriTask != null) { 178 mLoadDataUriTask.cancel(true); 179 mLoadDataUriTask = null; 180 } 181 182 if (!url.startsWith("data:")) return false; 183 int headerStop = url.indexOf(','); 184 if (headerStop == -1) return false; 185 String header = url.substring(0, headerStop); 186 final String data = url.substring(headerStop + 1); 187 188 String headerContent = header.substring(5); 189 String headerInfo[] = headerContent.split(";"); 190 if (headerInfo.length != 2) return false; 191 if (!"base64".equals(headerInfo[1])) return false; 192 193 mLoadDataUriTask = new LoadDataUriTask(context, data); 194 mLoadDataUriTask.execute(); 195 return true; 196 } 197 198 private class LoadDataUriTask extends AsyncTask <Void, Void, Boolean> { 199 private final String mData; 200 private final Context mContext; 201 private File mTempFile; 202 203 public LoadDataUriTask(Context context, String data) { 204 mData = data; 205 mContext = context; 206 } 207 208 @Override 209 protected Boolean doInBackground(Void... params) { 210 FileOutputStream fos = null; 211 try { 212 mTempFile = File.createTempFile("decoded", "mediadata"); 213 fos = new FileOutputStream(mTempFile); 214 InputStream stream = new ByteArrayInputStream(mData.getBytes()); 215 Base64InputStream decoder = new Base64InputStream(stream, Base64.DEFAULT); 216 byte[] buffer = new byte[1024]; 217 int len; 218 while ((len = decoder.read(buffer)) != -1) { 219 fos.write(buffer, 0, len); 220 } 221 decoder.close(); 222 return true; 223 } catch (IOException e) { 224 return false; 225 } finally { 226 try { 227 if (fos != null) fos.close(); 228 } catch (IOException e) { 229 // Can't do anything. 230 } 231 } 232 } 233 234 @Override 235 protected void onPostExecute(Boolean result) { 236 if (isCancelled()) { 237 deleteFile(); 238 return; 239 } 240 241 try { 242 getLocalPlayer().setDataSource(mContext, Uri.fromFile(mTempFile)); 243 } catch (IOException e) { 244 result = false; 245 } 246 247 deleteFile(); 248 assert (mNativeMediaPlayerBridge != 0); 249 nativeOnDidSetDataUriDataSource(mNativeMediaPlayerBridge, result); 250 } 251 252 private void deleteFile() { 253 if (mTempFile == null) return; 254 if (!mTempFile.delete()) { 255 // File will be deleted when MediaPlayer releases its handler. 256 Log.e(TAG, "Failed to delete temporary file: " + mTempFile); 257 assert (false); 258 } 259 } 260 } 261 262 protected void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener) { 263 getLocalPlayer().setOnBufferingUpdateListener(listener); 264 } 265 266 protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) { 267 getLocalPlayer().setOnCompletionListener(listener); 268 } 269 270 protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) { 271 getLocalPlayer().setOnErrorListener(listener); 272 } 273 274 protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) { 275 getLocalPlayer().setOnPreparedListener(listener); 276 } 277 278 protected void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener) { 279 getLocalPlayer().setOnSeekCompleteListener(listener); 280 } 281 282 protected void setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener) { 283 getLocalPlayer().setOnVideoSizeChangedListener(listener); 284 } 285 286 protected static class AllowedOperations { 287 private final boolean mCanPause; 288 private final boolean mCanSeekForward; 289 private final boolean mCanSeekBackward; 290 291 public AllowedOperations(boolean canPause, boolean canSeekForward, 292 boolean canSeekBackward) { 293 mCanPause = canPause; 294 mCanSeekForward = canSeekForward; 295 mCanSeekBackward = canSeekBackward; 296 } 297 298 @CalledByNative("AllowedOperations") 299 private boolean canPause() { return mCanPause; } 300 301 @CalledByNative("AllowedOperations") 302 private boolean canSeekForward() { return mCanSeekForward; } 303 304 @CalledByNative("AllowedOperations") 305 private boolean canSeekBackward() { return mCanSeekBackward; } 306 } 307 308 /** 309 * Returns an AllowedOperations object to show all the operations that are 310 * allowed on the media player. 311 */ 312 @CalledByNative 313 protected AllowedOperations getAllowedOperations() { 314 MediaPlayer player = getLocalPlayer(); 315 boolean canPause = true; 316 boolean canSeekForward = true; 317 boolean canSeekBackward = true; 318 try { 319 Method getMetadata = player.getClass().getDeclaredMethod( 320 "getMetadata", boolean.class, boolean.class); 321 getMetadata.setAccessible(true); 322 Object data = getMetadata.invoke(player, false, false); 323 if (data != null) { 324 Class<?> metadataClass = data.getClass(); 325 Method hasMethod = metadataClass.getDeclaredMethod("has", int.class); 326 Method getBooleanMethod = metadataClass.getDeclaredMethod("getBoolean", int.class); 327 328 int pause = (Integer) metadataClass.getField("PAUSE_AVAILABLE").get(null); 329 int seekForward = 330 (Integer) metadataClass.getField("SEEK_FORWARD_AVAILABLE").get(null); 331 int seekBackward = 332 (Integer) metadataClass.getField("SEEK_BACKWARD_AVAILABLE").get(null); 333 hasMethod.setAccessible(true); 334 getBooleanMethod.setAccessible(true); 335 canPause = !((Boolean) hasMethod.invoke(data, pause)) 336 || ((Boolean) getBooleanMethod.invoke(data, pause)); 337 canSeekForward = !((Boolean) hasMethod.invoke(data, seekForward)) 338 || ((Boolean) getBooleanMethod.invoke(data, seekForward)); 339 canSeekBackward = !((Boolean) hasMethod.invoke(data, seekBackward)) 340 || ((Boolean) getBooleanMethod.invoke(data, seekBackward)); 341 } 342 } catch (NoSuchMethodException e) { 343 Log.e(TAG, "Cannot find getMetadata() method: " + e); 344 } catch (InvocationTargetException e) { 345 Log.e(TAG, "Cannot invoke MediaPlayer.getMetadata() method: " + e); 346 } catch (IllegalAccessException e) { 347 Log.e(TAG, "Cannot access metadata: " + e); 348 } catch (NoSuchFieldException e) { 349 Log.e(TAG, "Cannot find matching fields in Metadata class: " + e); 350 } 351 return new AllowedOperations(canPause, canSeekForward, canSeekBackward); 352 } 353 354 private native void nativeOnDidSetDataUriDataSource(long nativeMediaPlayerBridge, 355 boolean success); 356 } 357