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