blob: 2543b419d3889b09297fd35621694b960fadcf0e [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.media;
import android.annotation.SuppressLint;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Base64InputStream;
import android.view.Surface;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StreamUtil;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.task.AsyncTask;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* A wrapper around android.media.MediaPlayer that allows the native code to use it.
* See media/base/android/media_player_bridge.cc for the corresponding native code.
*/
@JNINamespace("media")
public class MediaPlayerBridge {
private static final String TAG = "media";
// Local player to forward this to. We don't initialize it here since the subclass might not
// want it.
private LoadDataUriTask mLoadDataUriTask;
private MediaPlayer mPlayer;
private long mNativeMediaPlayerBridge;
@CalledByNative
private static MediaPlayerBridge create(long nativeMediaPlayerBridge) {
return new MediaPlayerBridge(nativeMediaPlayerBridge);
}
protected MediaPlayerBridge(long nativeMediaPlayerBridge) {
mNativeMediaPlayerBridge = nativeMediaPlayerBridge;
}
protected MediaPlayerBridge() {
}
@CalledByNative
protected void destroy() {
cancelLoadDataUriTask();
mNativeMediaPlayerBridge = 0;
}
protected MediaPlayer getLocalPlayer() {
if (mPlayer == null) {
mPlayer = new MediaPlayer();
}
return mPlayer;
}
@CalledByNative
protected void setSurface(Surface surface) {
getLocalPlayer().setSurface(surface);
}
@SuppressLint("NewApi")
@CalledByNative
protected void setPlaybackRate(double speed) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) return;
Log.w(TAG, "Unexpectedly setting playback speed to 0.");
try {
MediaPlayer player = getLocalPlayer();
player.setPlaybackParams(player.getPlaybackParams().setSpeed((float) speed));
} catch (IllegalStateException ise) {
Log.e(TAG, "Unable to set playback rate", ise);
} catch (IllegalArgumentException iae) {
Log.e(TAG, "Unable to set playback rate", iae);
}
}
@CalledByNative
protected boolean prepareAsync() {
try {
getLocalPlayer().prepareAsync();
} catch (IllegalStateException ise) {
Log.e(TAG, "Unable to prepare MediaPlayer.", ise);
return false;
} catch (Exception e) {
// Catch IOException thrown by android MediaPlayer native code.
Log.e(TAG, "Unable to prepare MediaPlayer.", e);
return false;
}
return true;
}
@CalledByNative
protected boolean isPlaying() {
return getLocalPlayer().isPlaying();
}
@CalledByNative
protected int getCurrentPosition() {
return getLocalPlayer().getCurrentPosition();
}
@CalledByNative
protected int getDuration() {
return getLocalPlayer().getDuration();
}
@CalledByNative
protected void release() {
cancelLoadDataUriTask();
getLocalPlayer().release();
}
@CalledByNative
protected void setVolume(double volume) {
getLocalPlayer().setVolume((float) volume, (float) volume);
}
@CalledByNative
protected void start() {
getLocalPlayer().start();
}
@CalledByNative
protected void pause() {
getLocalPlayer().pause();
}
@CalledByNative
protected void seekTo(int msec) throws IllegalStateException {
getLocalPlayer().seekTo(msec);
}
@CalledByNative
protected boolean setDataSource(
String url, String cookies, String userAgent, boolean hideUrlLog) {
Uri uri = Uri.parse(url);
HashMap<String, String> headersMap = new HashMap<String, String>();
if (hideUrlLog) headersMap.put("x-hide-urls-from-log", "true");
if (!TextUtils.isEmpty(cookies)) headersMap.put("Cookie", cookies);
if (!TextUtils.isEmpty(userAgent)) headersMap.put("User-Agent", userAgent);
headersMap.put("android-allow-cross-domain-redirect", "0");
try {
getLocalPlayer().setDataSource(ContextUtils.getApplicationContext(), uri, headersMap);
return true;
} catch (Exception e) {
return false;
}
}
@CalledByNative
protected boolean setDataSourceFromFd(int fd, long offset, long length) {
try {
ParcelFileDescriptor parcelFd = ParcelFileDescriptor.adoptFd(fd);
getLocalPlayer().setDataSource(parcelFd.getFileDescriptor(), offset, length);
parcelFd.close();
return true;
} catch (IOException e) {
Log.e(TAG, "Failed to set data source from file descriptor: " + e);
return false;
}
}
@CalledByNative
protected boolean setDataUriDataSource(final String url) {
cancelLoadDataUriTask();
if (!url.startsWith("data:")) return false;
int headerStop = url.indexOf(',');
if (headerStop == -1) return false;
String header = url.substring(0, headerStop);
final String data = url.substring(headerStop + 1);
String headerContent = header.substring(5);
String headerInfo[] = headerContent.split(";");
if (headerInfo.length != 2) return false;
if (!"base64".equals(headerInfo[1])) return false;
mLoadDataUriTask = new LoadDataUriTask(data);
mLoadDataUriTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return true;
}
private class LoadDataUriTask extends AsyncTask<Boolean> {
private final String mData;
private File mTempFile;
public LoadDataUriTask(String data) {
mData = data;
}
@Override
protected Boolean doInBackground() {
FileOutputStream fos = null;
try {
mTempFile = File.createTempFile("decoded", "mediadata");
fos = new FileOutputStream(mTempFile);
InputStream stream =
new ByteArrayInputStream(ApiCompatibilityUtils.getBytesUtf8(mData));
Base64InputStream decoder = new Base64InputStream(stream, Base64.DEFAULT);
byte[] buffer = new byte[1024];
int len;
while ((len = decoder.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
decoder.close();
return true;
} catch (IOException e) {
return false;
} finally {
StreamUtil.closeQuietly(fos);
}
}
@Override
protected void onPostExecute(Boolean result) {
if (isCancelled()) {
deleteFile();
return;
}
if (result) {
try {
getLocalPlayer().setDataSource(
ContextUtils.getApplicationContext(), Uri.fromFile(mTempFile));
} catch (IOException e) {
result = false;
}
}
deleteFile();
assert (mNativeMediaPlayerBridge != 0);
MediaPlayerBridgeJni.get().onDidSetDataUriDataSource(
mNativeMediaPlayerBridge, MediaPlayerBridge.this, result);
}
private void deleteFile() {
if (mTempFile == null) return;
if (!mTempFile.delete()) {
// File will be deleted when MediaPlayer releases its handler.
Log.e(TAG, "Failed to delete temporary file: " + mTempFile);
assert (false);
}
}
}
protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
getLocalPlayer().setOnCompletionListener(listener);
}
protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
getLocalPlayer().setOnErrorListener(listener);
}
protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
getLocalPlayer().setOnPreparedListener(listener);
}
protected void setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener) {
getLocalPlayer().setOnVideoSizeChangedListener(listener);
}
protected static class AllowedOperations {
private final boolean mCanSeekForward;
private final boolean mCanSeekBackward;
public AllowedOperations(boolean canSeekForward, boolean canSeekBackward) {
mCanSeekForward = canSeekForward;
mCanSeekBackward = canSeekBackward;
}
@CalledByNative("AllowedOperations")
private boolean canSeekForward() {
return mCanSeekForward;
}
@CalledByNative("AllowedOperations")
private boolean canSeekBackward() {
return mCanSeekBackward;
}
}
/**
* Returns an AllowedOperations object to show all the operations that are
* allowed on the media player.
*/
@CalledByNative
protected AllowedOperations getAllowedOperations() {
MediaPlayer player = getLocalPlayer();
boolean canSeekForward = true;
boolean canSeekBackward = true;
try {
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"})
Method getMetadata = player.getClass().getDeclaredMethod(
"getMetadata", boolean.class, boolean.class);
getMetadata.setAccessible(true);
Object data = getMetadata.invoke(player, false, false);
if (data != null) {
Class<?> metadataClass = data.getClass();
Method hasMethod = metadataClass.getDeclaredMethod("has", int.class);
Method getBooleanMethod = metadataClass.getDeclaredMethod("getBoolean", int.class);
int seekForward =
(Integer) metadataClass.getField("SEEK_FORWARD_AVAILABLE").get(null);
int seekBackward =
(Integer) metadataClass.getField("SEEK_BACKWARD_AVAILABLE").get(null);
hasMethod.setAccessible(true);
getBooleanMethod.setAccessible(true);
canSeekForward = !((Boolean) hasMethod.invoke(data, seekForward))
|| ((Boolean) getBooleanMethod.invoke(data, seekForward));
canSeekBackward = !((Boolean) hasMethod.invoke(data, seekBackward))
|| ((Boolean) getBooleanMethod.invoke(data, seekBackward));
}
} catch (NoSuchMethodException e) {
Log.e(TAG, "Cannot find getMetadata() method: " + e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Cannot invoke MediaPlayer.getMetadata() method: " + e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Cannot access metadata: " + e);
} catch (NoSuchFieldException e) {
Log.e(TAG, "Cannot find matching fields in Metadata class: " + e);
}
return new AllowedOperations(canSeekForward, canSeekBackward);
}
private void cancelLoadDataUriTask() {
if (mLoadDataUriTask != null) {
mLoadDataUriTask.cancel(true);
mLoadDataUriTask = null;
}
}
@NativeMethods
interface Natives {
void onDidSetDataUriDataSource(
long nativeMediaPlayerBridge, MediaPlayerBridge caller, boolean success);
}
}