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