Home | History | Annotate | Download | only in media
      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