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.os.Build;
12import android.os.ParcelFileDescriptor;
13import android.text.TextUtils;
14import android.util.Base64;
15import android.util.Base64InputStream;
16import android.util.Log;
17import android.view.Surface;
18
19import org.chromium.base.CalledByNative;
20import org.chromium.base.JNINamespace;
21
22import java.io.ByteArrayInputStream;
23import java.io.File;
24import java.io.FileOutputStream;
25import java.io.IOException;
26import java.io.InputStream;
27import java.lang.reflect.InvocationTargetException;
28import java.lang.reflect.Method;
29import 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")
36public 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