• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
create(long nativeMediaPlayerBridge)47     private static MediaPlayerBridge create(long nativeMediaPlayerBridge) {
48         return new MediaPlayerBridge(nativeMediaPlayerBridge);
49     }
50 
MediaPlayerBridge(long nativeMediaPlayerBridge)51     protected MediaPlayerBridge(long nativeMediaPlayerBridge) {
52         mNativeMediaPlayerBridge = nativeMediaPlayerBridge;
53     }
54 
MediaPlayerBridge()55     protected MediaPlayerBridge() {
56     }
57 
58     @CalledByNative
destroy()59     protected void destroy() {
60         if (mLoadDataUriTask != null) {
61             mLoadDataUriTask.cancel(true);
62             mLoadDataUriTask = null;
63         }
64         mNativeMediaPlayerBridge = 0;
65     }
66 
getLocalPlayer()67     protected MediaPlayer getLocalPlayer() {
68         if (mPlayer == null) {
69             mPlayer = new MediaPlayer();
70         }
71         return mPlayer;
72     }
73 
74     @CalledByNative
setSurface(Surface surface)75     protected void setSurface(Surface surface) {
76         getLocalPlayer().setSurface(surface);
77     }
78 
79     @CalledByNative
prepareAsync()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
isPlaying()91     protected boolean isPlaying() {
92         return getLocalPlayer().isPlaying();
93     }
94 
95     @CalledByNative
getVideoWidth()96     protected int getVideoWidth() {
97         return getLocalPlayer().getVideoWidth();
98     }
99 
100     @CalledByNative
getVideoHeight()101     protected int getVideoHeight() {
102         return getLocalPlayer().getVideoHeight();
103     }
104 
105     @CalledByNative
getCurrentPosition()106     protected int getCurrentPosition() {
107         return getLocalPlayer().getCurrentPosition();
108     }
109 
110     @CalledByNative
getDuration()111     protected int getDuration() {
112         return getLocalPlayer().getDuration();
113     }
114 
115     @CalledByNative
release()116     protected void release() {
117         getLocalPlayer().release();
118     }
119 
120     @CalledByNative
setVolume(double volume)121     protected void setVolume(double volume) {
122         getLocalPlayer().setVolume((float) volume, (float) volume);
123     }
124 
125     @CalledByNative
start()126     protected void start() {
127         getLocalPlayer().start();
128     }
129 
130     @CalledByNative
pause()131     protected void pause() {
132         getLocalPlayer().pause();
133     }
134 
135     @CalledByNative
seekTo(int msec)136     protected void seekTo(int msec) throws IllegalStateException {
137         getLocalPlayer().seekTo(msec);
138     }
139 
140     @CalledByNative
setDataSource( Context context, String url, String cookies, String userAgent, boolean hideUrlLog)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
setDataSourceFromFd(int fd, long offset, long length)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
setDataUriDataSource(final Context context, final String url)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 
LoadDataUriTask(Context context, String data)203         public LoadDataUriTask(Context context, String data) {
204             mData = data;
205             mContext = context;
206         }
207 
208         @Override
doInBackground(Void... params)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
onPostExecute(Boolean result)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 
deleteFile()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 
setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener)262     protected void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener) {
263         getLocalPlayer().setOnBufferingUpdateListener(listener);
264     }
265 
setOnCompletionListener(MediaPlayer.OnCompletionListener listener)266     protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
267         getLocalPlayer().setOnCompletionListener(listener);
268     }
269 
setOnErrorListener(MediaPlayer.OnErrorListener listener)270     protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
271         getLocalPlayer().setOnErrorListener(listener);
272     }
273 
setOnPreparedListener(MediaPlayer.OnPreparedListener listener)274     protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
275         getLocalPlayer().setOnPreparedListener(listener);
276     }
277 
setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener)278     protected void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener) {
279         getLocalPlayer().setOnSeekCompleteListener(listener);
280     }
281 
setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener)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 
AllowedOperations(boolean canPause, boolean canSeekForward, boolean canSeekBackward)291         public AllowedOperations(boolean canPause, boolean canSeekForward,
292                 boolean canSeekBackward) {
293             mCanPause = canPause;
294             mCanSeekForward = canSeekForward;
295             mCanSeekBackward = canSeekBackward;
296         }
297 
298         @CalledByNative("AllowedOperations")
canPause()299         private boolean canPause() { return mCanPause; }
300 
301         @CalledByNative("AllowedOperations")
canSeekForward()302         private boolean canSeekForward() { return mCanSeekForward; }
303 
304         @CalledByNative("AllowedOperations")
canSeekBackward()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
getAllowedOperations()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 
nativeOnDidSetDataUriDataSource(long nativeMediaPlayerBridge, boolean success)354     private native void nativeOnDidSetDataUriDataSource(long nativeMediaPlayerBridge,
355                                                         boolean success);
356 }
357