import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaFormat;
import android.os.Handler;
import android.view.Surface;
import java.nio.ByteBuffer;
// A wrapper interface that mimics the new {@link android.media.MediaCodec}
// asynchronous mode API in Lollipop.
public interface AsyncCodec {
public interface Callbacks {
void onInputBufferAvailable(AsyncCodec codec, int index);
void onOutputBufferAvailable(AsyncCodec codec, int index, BufferInfo info);
void onError(AsyncCodec codec, int error);
void onOutputFormatChanged(AsyncCodec codec, MediaFormat format);
}
public abstract void setCallbacks(Callbacks callbacks, Handler handler);
public abstract void configure(MediaFormat format, Surface surface, int flags);
public abstract void start();
public abstract void stop();
public abstract void flush();
public abstract void release();
public abstract ByteBuffer getInputBuffer(int index);
public abstract ByteBuffer getOutputBuffer(int index);
public abstract void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);
public abstract void releaseOutputBuffer(int index, boolean render);
}

View file

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import java.io.IOException;
public final class AsyncCodecFactory {
public static AsyncCodec create(String name) throws IOException {
// TODO: create (to be implemented) LollipopAsyncCodec when running on Lollipop or later devices.
return new JellyBeanAsyncCodec(name);
}
}

View file

@ -0,0 +1,135 @@
package org.mozilla.gecko.media;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.util.Log;
public class AudioFocusAgent {
private static final String LOGTAG = "AudioFocusAgent";
private static Context mContext;
private AudioManager mAudioManager;
private OnAudioFocusChangeListener mAfChangeListener;
public static final String OWN_FOCUS = "own_focus";
public static final String LOST_FOCUS = "lost_focus";
public static final String LOST_FOCUS_TRANSIENT = "lost_focus_transient";
private String mAudioFocusState = LOST_FOCUS;
@WrapForJNI(calledFrom = "gecko")
public static void notifyStartedPlaying() {
if (!isAttachedToContext()) {
return;
}
Log.d(LOGTAG, "NotifyStartedPlaying");
AudioFocusAgent.getInstance().requestAudioFocusIfNeeded();
}
@WrapForJNI(calledFrom = "gecko")
public static void notifyStoppedPlaying() {
if (!isAttachedToContext()) {
return;
}
Log.d(LOGTAG, "NotifyStoppedPlaying");
AudioFocusAgent.getInstance().abandonAudioFocusIfNeeded();
}
public synchronized void attachToContext(Context context) {
if (isAttachedToContext()) {
return;
}
mContext = context;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mAfChangeListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS");
notifyObservers("AudioFocusChanged", "lostAudioFocus");
notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
mAudioFocusState = LOST_FOCUS;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT");
notifyObservers("AudioFocusChanged", "lostAudioFocusTransiently");
notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
mAudioFocusState = LOST_FOCUS_TRANSIENT;
break;
case AudioManager.AUDIOFOCUS_GAIN:
if (!mAudioFocusState.equals(LOST_FOCUS_TRANSIENT)) {
return;
}
Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
notifyObservers("AudioFocusChanged", "gainAudioFocus");
notifyMediaControlService(MediaControlService.ACTION_RESUME_BY_AUDIO_FOCUS);
mAudioFocusState = OWN_FOCUS;
break;
default:
}
}
};
notifyMediaControlService(MediaControlService.ACTION_INIT);
}
@RobocopTarget
public static AudioFocusAgent getInstance() {
return AudioFocusAgent.SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final AudioFocusAgent INSTANCE = new AudioFocusAgent();
}
private static boolean isAttachedToContext() {
return (mContext != null);
}
private void notifyObservers(String topic, String data) {
GeckoAppShell.notifyObservers(topic, data);
}
private AudioFocusAgent() {}
private void requestAudioFocusIfNeeded() {
if (mAudioFocusState.equals(OWN_FOCUS)) {
return;
}
int result = mAudioManager.requestAudioFocus(mAfChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
String focusMsg = (result == AudioManager.AUDIOFOCUS_GAIN) ?
"AudioFocus request granted" : "AudioFoucs request failed";
Log.d(LOGTAG, focusMsg);
if (result == AudioManager.AUDIOFOCUS_GAIN) {
mAudioFocusState = OWN_FOCUS;
}
}
private void abandonAudioFocusIfNeeded() {
if (!mAudioFocusState.equals(OWN_FOCUS)) {
return;
}
Log.d(LOGTAG, "Abandon AudioFocus");
mAudioManager.abandonAudioFocus(mAfChangeListener);
mAudioFocusState = LOST_FOCUS;
}
private void notifyMediaControlService(String action) {
Intent intent = new Intent(mContext, MediaControlService.class);
intent.setAction(action);
mContext.startService(intent);
}
}

View file

@ -0,0 +1,366 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/* package */ final class Codec extends ICodec.Stub implements IBinder.DeathRecipient {
private static final String LOGTAG = "GeckoRemoteCodec";
private static final boolean DEBUG = false;
public enum Error {
DECODE, FATAL
};
private final class Callbacks implements AsyncCodec.Callbacks {
private ICodecCallbacks mRemote;
private boolean mHasInputCapacitySet;
private boolean mHasOutputCapacitySet;
public Callbacks(ICodecCallbacks remote) {
mRemote = remote;
}
@Override
public void onInputBufferAvailable(AsyncCodec codec, int index) {
if (mFlushing) {
// Flush invalidates all buffers.
return;
}
if (!mHasInputCapacitySet) {
int capacity = codec.getInputBuffer(index).capacity();
if (capacity > 0) {
mSamplePool.setInputBufferSize(capacity);
mHasInputCapacitySet = true;
}
}
if (!mInputProcessor.onBuffer(index)) {
reportError(Error.FATAL, new Exception("FAIL: input buffer queue is full"));
}
}
@Override
public void onOutputBufferAvailable(AsyncCodec codec, int index, MediaCodec.BufferInfo info) {
if (mFlushing) {
// Flush invalidates all buffers.
return;
}
ByteBuffer output = codec.getOutputBuffer(index);
if (!mHasOutputCapacitySet) {
int capacity = output.capacity();
if (capacity > 0) {
mSamplePool.setOutputBufferSize(capacity);
mHasOutputCapacitySet = true;
}
}
Sample copy = mSamplePool.obtainOutput(info);
try {
if (info.size > 0) {
copy.buffer.readFromByteBuffer(output, info.offset, info.size);
}
mSentOutputs.add(copy);
mRemote.onOutput(copy);
} catch (IOException e) {
Log.e(LOGTAG, "Fail to read output buffer:" + e.getMessage());
outputDummy(info);
} catch (TransactionTooLargeException ttle) {
Log.e(LOGTAG, "Output is too large:" + ttle.getMessage());
outputDummy(info);
} catch (RemoteException e) {
// Dead recipient.
e.printStackTrace();
}
mCodec.releaseOutputBuffer(index, true);
boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (DEBUG && eos) {
Log.d(LOGTAG, "output EOS");
}
}
private void outputDummy(MediaCodec.BufferInfo info) {
try {
if (DEBUG) Log.d(LOGTAG, "return dummy sample");
mRemote.onOutput(Sample.create(null, info, null));
} catch (RemoteException e) {
// Dead recipient.
e.printStackTrace();
}
}
@Override
public void onError(AsyncCodec codec, int error) {
reportError(Error.FATAL, new Exception("codec error:" + error));
}
@Override
public void onOutputFormatChanged(AsyncCodec codec, MediaFormat format) {
try {
mRemote.onOutputFormatChanged(new FormatParam(format));
} catch (RemoteException re) {
// Dead recipient.
re.printStackTrace();
}
}
}
private final class InputProcessor {
private Queue<Sample> mInputSamples = new LinkedList<>();
private Queue<Integer> mAvailableInputBuffers = new LinkedList<>();
private Queue<Sample> mDequeuedSamples = new LinkedList<>();
private synchronized Sample onAllocate(int size) {
Sample sample = mSamplePool.obtainInput(size);
mDequeuedSamples.add(sample);
return sample;
}
private synchronized boolean onSample(Sample sample) {
if (sample == null) {
return false;
}
if (!sample.isEOS()) {
Sample temp = sample;
sample = mDequeuedSamples.remove();
sample.info = temp.info;
sample.cryptoInfo = temp.cryptoInfo;
temp.dispose();
}
if (!mInputSamples.offer(sample)) {
return false;
}
feedSampleToBuffer();
return true;
}
private synchronized boolean onBuffer(int index) {
if (!mAvailableInputBuffers.offer(index)) {
return false;
}
feedSampleToBuffer();
return true;
}
private void feedSampleToBuffer() {
while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) {
int index = mAvailableInputBuffers.poll();
int len = 0;
Sample sample = mInputSamples.poll();
long pts = sample.info.presentationTimeUs;
int flags = sample.info.flags;
if (!sample.isEOS() && sample.buffer != null) {
len = sample.info.size;
ByteBuffer buf = mCodec.getInputBuffer(index);
try {
sample.writeToByteBuffer(buf);
mCallbacks.onInputExhausted();
} catch (IOException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
mSamplePool.recycleInput(sample);
}
mCodec.queueInputBuffer(index, 0, len, pts, flags);
}
}
private synchronized void reset() {
mInputSamples.clear();
mAvailableInputBuffers.clear();
}
}
private volatile ICodecCallbacks mCallbacks;
private AsyncCodec mCodec;
private InputProcessor mInputProcessor;
private volatile boolean mFlushing = false;
private SamplePool mSamplePool;
private Queue<Sample> mSentOutputs = new ConcurrentLinkedQueue<>();
public synchronized void setCallbacks(ICodecCallbacks callbacks) throws RemoteException {
mCallbacks = callbacks;
callbacks.asBinder().linkToDeath(this, 0);
}
// IBinder.DeathRecipient
@Override
public synchronized void binderDied() {
Log.e(LOGTAG, "Callbacks is dead");
try {
release();
} catch (RemoteException e) {
// Nowhere to report the error.
}
}
@Override
public synchronized boolean configure(FormatParam format, Surface surface, int flags) throws RemoteException {
if (mCallbacks == null) {
Log.e(LOGTAG, "FAIL: callbacks must be set before calling configure()");
return false;
}
if (mCodec != null) {
if (DEBUG) Log.d(LOGTAG, "release existing codec: " + mCodec);
releaseCodec();
}
if (DEBUG) Log.d(LOGTAG, "configure " + this);
MediaFormat fmt = format.asFormat();
String codecName = getDecoderForFormat(fmt);
if (codecName == null) {
Log.e(LOGTAG, "FAIL: cannot find codec");
return false;
}
try {
AsyncCodec codec = AsyncCodecFactory.create(codecName);
codec.setCallbacks(new Callbacks(mCallbacks), null);
codec.configure(fmt, surface, flags);
mCodec = codec;
mInputProcessor = new InputProcessor();
mSamplePool = new SamplePool(codecName);
if (DEBUG) Log.d(LOGTAG, codec.toString() + " created");
return true;
} catch (Exception e) {
if (DEBUG) Log.d(LOGTAG, "FAIL: cannot create codec -- " + codecName);
e.printStackTrace();
return false;
}
}
private void releaseCodec() {
mInputProcessor.reset();
try {
mCodec.release();
} catch (Exception e) {
reportError(Error.FATAL, e);
}
mCodec = null;
}
private String getDecoderForFormat(MediaFormat format) {
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime == null) {
return null;
}
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
if (info.isEncoder()) {
continue;
}
String[] types = info.getSupportedTypes();
for (String t : types) {
if (t.equalsIgnoreCase(mime)) {
return info.getName();
}
}
}
return null;
// TODO: API 21+ is simpler.
//static MediaCodecList sCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
//return sCodecList.findDecoderForFormat(format);
}
@Override
public synchronized void start() throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "start " + this);
mFlushing = false;
try {
mCodec.start();
} catch (Exception e) {
reportError(Error.FATAL, e);
}
}
private void reportError(Error error, Exception e) {
if (e != null) {
e.printStackTrace();
}
try {
mCallbacks.onError(error == Error.FATAL);
} catch (RemoteException re) {
re.printStackTrace();
}
}
@Override
public synchronized void stop() throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "stop " + this);
try {
mCodec.stop();
} catch (Exception e) {
reportError(Error.FATAL, e);
}
}
@Override
public synchronized void flush() throws RemoteException {
mFlushing = true;
if (DEBUG) Log.d(LOGTAG, "flush " + this);
mInputProcessor.reset();
try {
mCodec.flush();
} catch (Exception e) {
reportError(Error.FATAL, e);
}
mFlushing = false;
if (DEBUG) Log.d(LOGTAG, "flushed " + this);
}
@Override
public synchronized Sample dequeueInput(int size) {
return mInputProcessor.onAllocate(size);
}
@Override
public synchronized void queueInput(Sample sample) throws RemoteException {
if (!mInputProcessor.onSample(sample)) {
reportError(Error.FATAL, new Exception("FAIL: input sample queue is full"));
}
}
@Override
public synchronized void releaseOutput(Sample sample) {
try {
mSamplePool.recycleOutput(mSentOutputs.remove());
} catch (Exception e) {
Log.e(LOGTAG, "failed to release output:" + sample);
e.printStackTrace();
}
sample.dispose();
}
@Override
public synchronized void release() throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "release " + this);
releaseCodec();
mSamplePool.reset();
mSamplePool = null;
mCallbacks.asBinder().unlinkToDeath(this, 0);
mCallbacks = null;
}
}

View file

@ -0,0 +1,191 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CryptoInfo;
import android.media.MediaFormat;
import android.os.DeadObjectException;
import android.os.RemoteException;
import android.util.Log;
import android.view.Surface;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;
import java.io.IOException;
import java.nio.ByteBuffer;
// Proxy class of ICodec binder.
public final class CodecProxy {
private static final String LOGTAG = "GeckoRemoteCodecProxy";
private static final boolean DEBUG = false;
private ICodec mRemote;
private FormatParam mFormat;
private Surface mOutputSurface;
private CallbacksForwarder mCallbacks;
public interface Callbacks {
void onInputExhausted();
void onOutputFormatChanged(MediaFormat format);
void onOutput(Sample output);
void onError(boolean fatal);
}
@WrapForJNI
public static class NativeCallbacks extends JNIObject implements Callbacks {
public native void onInputExhausted();
public native void onOutputFormatChanged(MediaFormat format);
public native void onOutput(Sample output);
public native void onError(boolean fatal);
@Override // JNIObject
protected native void disposeNative();
}
private class CallbacksForwarder extends ICodecCallbacks.Stub {
private final Callbacks mCallbacks;
CallbacksForwarder(Callbacks callbacks) {
mCallbacks = callbacks;
}
@Override
public void onInputExhausted() throws RemoteException {
mCallbacks.onInputExhausted();
}
@Override
public void onOutputFormatChanged(FormatParam format) throws RemoteException {
mCallbacks.onOutputFormatChanged(format.asFormat());
}
@Override
public void onOutput(Sample sample) throws RemoteException {
mCallbacks.onOutput(sample);
mRemote.releaseOutput(sample);
sample.dispose();
}
@Override
public void onError(boolean fatal) throws RemoteException {
reportError(fatal);
}
public void reportError(boolean fatal) {
mCallbacks.onError(fatal);
}
}
@WrapForJNI
public static CodecProxy create(MediaFormat format, Surface surface, Callbacks callbacks) {
return RemoteManager.getInstance().createCodec(format, surface, callbacks);
}
public static CodecProxy createCodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) {
return new CodecProxy(format, surface, callbacks);
}
private CodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) {
mFormat = new FormatParam(format);
mOutputSurface = surface;
mCallbacks = new CallbacksForwarder(callbacks);
}
boolean init(ICodec remote) {
try {
remote.setCallbacks(mCallbacks);
remote.configure(mFormat, mOutputSurface, 0);
remote.start();
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
mRemote = remote;
return true;
}
boolean deinit() {
try {
mRemote.stop();
mRemote.release();
mRemote = null;
return true;
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
}
@WrapForJNI
public synchronized boolean input(ByteBuffer bytes, BufferInfo info, CryptoInfo cryptoInfo) {
if (mRemote == null) {
Log.e(LOGTAG, "cannot send input to an ended codec");
return false;
}
try {
Sample sample = (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) ?
Sample.EOS : mRemote.dequeueInput(info.size).set(bytes, info, cryptoInfo);
mRemote.queueInput(sample);
sample.dispose();
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (DeadObjectException e) {
return false;
} catch (RemoteException e) {
e.printStackTrace();
Log.e(LOGTAG, "fail to input sample: size=" + info.size +
", pts=" + info.presentationTimeUs +
", flags=" + Integer.toHexString(info.flags));
return false;
}
return true;
}
@WrapForJNI
public synchronized boolean flush() {
if (mRemote == null) {
Log.e(LOGTAG, "cannot flush an ended codec");
return false;
}
try {
if (DEBUG) Log.d(LOGTAG, "flush " + this);
mRemote.flush();
} catch (DeadObjectException e) {
return false;
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
return true;
}
@WrapForJNI
public synchronized boolean release() {
if (mRemote == null) {
Log.w(LOGTAG, "codec already ended");
return true;
}
if (DEBUG) Log.d(LOGTAG, "release " + this);
try {
RemoteManager.getInstance().releaseCodec(this);
} catch (DeadObjectException e) {
return false;
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
return true;
}
public synchronized void reportError(boolean fatal) {
mCallbacks.reportError(fatal);
}
}

View file

@ -0,0 +1,133 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import java.nio.ByteBuffer;
/** A wrapper to make {@link MediaFormat} parcelable.
* Supports following keys:
* <ul>
* <li>{@link MediaFormat#KEY_MIME}</li>
* <li>{@link MediaFormat#KEY_WIDTH}</li>
* <li>{@link MediaFormat#KEY_HEIGHT}</li>
* <li>{@link MediaFormat#KEY_CHANNEL_COUNT}</li>
* <li>{@link MediaFormat#KEY_SAMPLE_RATE}</li>
* <li>"csd-0"</li>
* <li>"csd-1"</li>
* </ul>
*/
public final class FormatParam implements Parcelable {
// Keys for codec specific config bits not exposed in {@link MediaFormat}.
private static final String KEY_CONFIG_0 = "csd-0";
private static final String KEY_CONFIG_1 = "csd-1";
private MediaFormat mFormat;
public MediaFormat asFormat() {
return mFormat;
}
public FormatParam(MediaFormat format) {
mFormat = format;
}
protected FormatParam(Parcel in) {
mFormat = new MediaFormat();
readFromParcel(in);
}
public static final Creator<FormatParam> CREATOR = new Creator<FormatParam>() {
@Override
public FormatParam createFromParcel(Parcel in) {
return new FormatParam(in);
}
@Override
public FormatParam[] newArray(int size) {
return new FormatParam[size];
}
};
@Override
public int describeContents() {
return 0;
}
public void readFromParcel(Parcel in) {
Bundle bundle = in.readBundle();
fromBundle(bundle);
}
private void fromBundle(Bundle bundle) {
if (bundle.containsKey(MediaFormat.KEY_MIME)) {
mFormat.setString(MediaFormat.KEY_MIME,
bundle.getString(MediaFormat.KEY_MIME));
}
if (bundle.containsKey(MediaFormat.KEY_WIDTH)) {
mFormat.setInteger(MediaFormat.KEY_WIDTH,
bundle.getInt(MediaFormat.KEY_WIDTH));
}
if (bundle.containsKey(MediaFormat.KEY_HEIGHT)) {
mFormat.setInteger(MediaFormat.KEY_HEIGHT,
bundle.getInt(MediaFormat.KEY_HEIGHT));
}
if (bundle.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
mFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT,
bundle.getInt(MediaFormat.KEY_CHANNEL_COUNT));
}
if (bundle.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
mFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE,
bundle.getInt(MediaFormat.KEY_SAMPLE_RATE));
}
if (bundle.containsKey(KEY_CONFIG_0)) {
mFormat.setByteBuffer(KEY_CONFIG_0,
ByteBuffer.wrap(bundle.getByteArray(KEY_CONFIG_0)));
}
if (bundle.containsKey(KEY_CONFIG_1)) {
mFormat.setByteBuffer(KEY_CONFIG_1,
ByteBuffer.wrap(bundle.getByteArray((KEY_CONFIG_1))));
}
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeBundle(toBundle());
}
private Bundle toBundle() {
Bundle bundle = new Bundle();
if (mFormat.containsKey(MediaFormat.KEY_MIME)) {
bundle.putString(MediaFormat.KEY_MIME, mFormat.getString(MediaFormat.KEY_MIME));
}
if (mFormat.containsKey(MediaFormat.KEY_WIDTH)) {
bundle.putInt(MediaFormat.KEY_WIDTH, mFormat.getInteger(MediaFormat.KEY_WIDTH));
}
if (mFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
bundle.putInt(MediaFormat.KEY_HEIGHT, mFormat.getInteger(MediaFormat.KEY_HEIGHT));
}
if (mFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
bundle.putInt(MediaFormat.KEY_CHANNEL_COUNT, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
}
if (mFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
bundle.putInt(MediaFormat.KEY_SAMPLE_RATE, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
}
if (mFormat.containsKey(KEY_CONFIG_0)) {
ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_0);
bundle.putByteArray(KEY_CONFIG_0,
Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
}
if (mFormat.containsKey(KEY_CONFIG_1)) {
ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_1);
bundle.putByteArray(KEY_CONFIG_1,
Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
}
return bundle;
}
}

View file

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCrypto;
public interface GeckoMediaDrm {
public interface Callbacks {
void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request);
void onSessionUpdated(int promiseId, byte[] sessionId);
void onSessionClosed(int promiseId, byte[] sessionId);
void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request);
void onSessionError(byte[] sessionId, String message);
void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos);
// All failure cases should go through this function.
void onRejectPromise(int promiseId, String message);
}
void setCallbacks(Callbacks callbacks);
void createSession(int createSessionToken,
int promiseId,
String initDataType,
byte[] initData);
void updateSession(int promiseId, String sessionId, byte[] response);
void closeSession(int promiseId, String sessionId);
void release();
MediaCrypto getMediaCrypto();
}

View file

@ -0,0 +1,627 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import java.lang.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.UUID;
import java.util.ArrayDeque;
import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaDrm;
import android.media.MediaDrmException;
import android.util.Log;
public class GeckoMediaDrmBridgeV21 implements GeckoMediaDrm {
private static final String LOGTAG = "GeckoMediaDrmBridgeV21";
private static final String INVALID_SESSION_ID = "Invalid";
private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
private static final boolean DEBUG = false;
private static final UUID WIDEVINE_SCHEME_UUID =
new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
// MediaDrm.KeyStatus information listener is supported on M+, adding a
// dummy key id to report key status.
private static final byte[] DUMMY_KEY_ID = new byte[] {0};
private UUID mSchemeUUID;
private Handler mHandler;
private HandlerThread mHandlerThread;
private ByteBuffer mCryptoSessionId;
// mProvisioningPromiseId is great than 0 only during provisioning.
private int mProvisioningPromiseId;
private HashSet<ByteBuffer> mSessionIds;
private HashMap<ByteBuffer, String> mSessionMIMETypes;
private ArrayDeque<PendingCreateSessionData> mPendingCreateSessionDataQueue;
private GeckoMediaDrm.Callbacks mCallbacks;
private MediaCrypto mCrypto;
protected MediaDrm mDrm;
public static int LICENSE_REQUEST_INITIAL = 0; /*MediaKeyMessageType::License_request*/
public static int LICENSE_REQUEST_RENEWAL = 1; /*MediaKeyMessageType::License_renewal*/
public static int LICENSE_REQUEST_RELEASE = 2; /*MediaKeyMessageType::License_release*/
// Store session data while provisioning
private static class PendingCreateSessionData {
public final int mToken;
public final int mPromiseId;
public final byte[] mInitData;
public final String mMimeType;
private PendingCreateSessionData(int token, int promiseId,
byte[] initData, String mimeType) {
mToken = token;
mPromiseId = promiseId;
mInitData = initData;
mMimeType = mimeType;
}
}
public boolean isSecureDecoderComonentRequired(String mimeType) {
if (mCrypto != null) {
return mCrypto.requiresSecureDecoderComponent(mimeType);
}
return false;
}
private static void assertTrue(boolean condition) {
if (DEBUG && !condition) {
throw new AssertionError("Expected condition to be true");
}
}
@SuppressLint("WrongConstant")
private void configureVendorSpecificProperty() {
assertTrue(mDrm != null);
// Support L3 for now
mDrm.setPropertyString("securityLevel", "L3");
// Refer to chromium, set multi-session mode for Widevine.
if (mSchemeUUID.equals(WIDEVINE_SCHEME_UUID)) {
mDrm.setPropertyString("sessionSharing", "enable");
}
}
GeckoMediaDrmBridgeV21(String keySystem) throws Exception {
if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV21()");
mProvisioningPromiseId = 0;
mSessionIds = new HashSet<ByteBuffer>();
mSessionMIMETypes = new HashMap<ByteBuffer, String>();
mPendingCreateSessionDataQueue = new ArrayDeque<PendingCreateSessionData>();
mSchemeUUID = convertKeySystemToSchemeUUID(keySystem);
mCryptoSessionId = null;
if (DEBUG) Log.d(LOGTAG, "mSchemeUUID : " + mSchemeUUID.toString());
// The caller of GeckoMediaDrmBridgeV21 ctor should handle exceptions
// threw by the following steps.
mDrm = new MediaDrm(mSchemeUUID);
configureVendorSpecificProperty();
mDrm.setOnEventListener(new MediaDrmListener());
}
@Override
public void setCallbacks(GeckoMediaDrm.Callbacks callbacks) {
assertTrue(callbacks != null);
mCallbacks = callbacks;
}
@Override
public void createSession(int createSessionToken,
int promiseId,
String initDataType,
byte[] initData) {
if (DEBUG) Log.d(LOGTAG, "createSession()");
if (mDrm == null) {
onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
return;
}
if (mProvisioningPromiseId > 0 && mCrypto == null) {
if (DEBUG) Log.d(LOGTAG, "Pending createSession because it's provisioning !");
savePendingCreateSessionData(createSessionToken, promiseId,
initData, initDataType);
return;
}
ByteBuffer sessionId = null;
String strSessionId = null;
try {
boolean hasMediaCrypto = ensureMediaCryptoCreated();
if (!hasMediaCrypto) {
onRejectPromise(promiseId, "MediaCrypto intance is not created !");
return;
}
sessionId = openSession();
if (sessionId == null) {
onRejectPromise(promiseId, "Cannot get a session id from MediaDrm !");
return;
}
MediaDrm.KeyRequest request = getKeyRequest(sessionId, initData, initDataType);
if (request == null) {
mDrm.closeSession(sessionId.array());
onRejectPromise(promiseId, "Cannot get a key request from MediaDrm !");
return;
}
onSessionCreated(createSessionToken,
promiseId,
sessionId.array(),
request.getData());
onSessionMessage(sessionId.array(),
LICENSE_REQUEST_INITIAL,
request.getData());
mSessionMIMETypes.put(sessionId, initDataType);
strSessionId = new String(sessionId.array());
mSessionIds.add(sessionId);
if (DEBUG) Log.d(LOGTAG, " StringID : " + strSessionId + " is put into mSessionIds ");
} catch (android.media.NotProvisionedException e) {
if (DEBUG) Log.d(LOGTAG, "Device not provisioned:" + e.getMessage());
if (sessionId != null) {
// The promise of this createSession will be either resolved
// or rejected after provisioning.
mDrm.closeSession(sessionId.array());
}
savePendingCreateSessionData(createSessionToken, promiseId,
initData, initDataType);
startProvisioning(promiseId);
}
}
@Override
public void updateSession(int promiseId,
String sessionId,
byte[] response) {
if (DEBUG) Log.d(LOGTAG, "updateSession(), sessionId = " + sessionId);
if (mDrm == null) {
onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
return;
}
ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes());
if (!sessionExists(session)) {
onRejectPromise(promiseId, "Invalid session during updateSession.");
return;
}
try {
final byte [] keySetId = mDrm.provideKeyResponse(session.array(), response);
if (DEBUG) {
HashMap<String, String> infoMap = mDrm.queryKeyStatus(session.array());
for (String strKey : infoMap.keySet()) {
String strValue = infoMap.get(strKey);
Log.d(LOGTAG, "InfoMap : key(" + strKey + ")/value(" + strValue + ")");
}
}
SessionKeyInfo[] keyInfos = new SessionKeyInfo[1];
keyInfos[0] = new SessionKeyInfo(DUMMY_KEY_ID,
MediaDrm.KeyStatus.STATUS_USABLE);
onSessionBatchedKeyChanged(session.array(), keyInfos);
if (DEBUG) Log.d(LOGTAG, "Key successfully added for session " + sessionId);
onSessionUpdated(promiseId, session.array());
return;
} catch (android.media.NotProvisionedException e) {
if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage());
onSessionError(session.array(), "Got NotProvisionedException.");
onRejectPromise(promiseId, "Not provisioned during updateSession.");
} catch (android.media.DeniedByServerException e) {
if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage());
onSessionError(session.array(), "Got DeniedByServerException.");
onRejectPromise(promiseId, "Denied by server during updateSession.");
} catch (java.lang.IllegalStateException e) {
if (DEBUG) Log.d(LOGTAG, "Exception when calling provideKeyResponse():" + e.getMessage());
onSessionError(session.array(), "Got IllegalStateException.");
onRejectPromise(promiseId, "Rejected during updateSession.");
}
release();
return;
}
@Override
public void closeSession(int promiseId, String sessionId) {
if (DEBUG) Log.d(LOGTAG, "closeSession()");
if (mDrm == null) {
onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
return;
}
ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes());
mSessionIds.remove(session);
mDrm.closeSession(session.array());
onSessionClosed(promiseId, session.array());
}
@Override
public void release() {
if (DEBUG) Log.d(LOGTAG, "release()");
if (mProvisioningPromiseId > 0) {
onRejectPromise(mProvisioningPromiseId, "Releasing ... reject provisioning session.");
mProvisioningPromiseId = 0;
}
while (!mPendingCreateSessionDataQueue.isEmpty()) {
PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
onRejectPromise(pendingData.mPromiseId, "Releasing ... reject all pending sessions.");
}
mPendingCreateSessionDataQueue = null;
if (mDrm != null) {
for (ByteBuffer session : mSessionIds) {
mDrm.closeSession(session.array());
}
mDrm.release();
mDrm = null;
}
mSessionIds.clear();
mSessionIds = null;
mSessionMIMETypes.clear();
mSessionMIMETypes = null;
mCryptoSessionId = null;
if (mCrypto != null) {
mCrypto.release();
mCrypto = null;
}
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
mHandlerThread = null;
}
mHandler = null;
}
@Override
public MediaCrypto getMediaCrypto() {
if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
return mCrypto;
}
protected void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request) {
assertTrue(mCallbacks != null);
mCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
}
protected void onSessionUpdated(int promiseId, byte[] sessionId) {
assertTrue(mCallbacks != null);
mCallbacks.onSessionUpdated(promiseId, sessionId);
}
protected void onSessionClosed(int promiseId, byte[] sessionId) {
assertTrue(mCallbacks != null);
mCallbacks.onSessionClosed(promiseId, sessionId);
}
protected void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request) {
assertTrue(mCallbacks != null);
mCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
}
protected void onSessionError(byte[] sessionId, String message) {
assertTrue(mCallbacks != null);
mCallbacks.onSessionError(sessionId, message);
}
protected void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos) {
assertTrue(mCallbacks != null);
mCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
}
protected void onRejectPromise(int promiseId, String message) {
assertTrue(mCallbacks != null);
mCallbacks.onRejectPromise(promiseId, message);
}
private MediaDrm.KeyRequest getKeyRequest(ByteBuffer aSession,
byte[] data,
String mimeType)
throws android.media.NotProvisionedException {
if (mProvisioningPromiseId > 0) {
// Now provisioning.
return null;
}
try {
HashMap<String, String> optionalParameters = new HashMap<String, String>();
return mDrm.getKeyRequest(aSession.array(),
data,
mimeType,
MediaDrm.KEY_TYPE_STREAMING,
optionalParameters);
} catch (Exception e) {
Log.e(LOGTAG, "Got excpetion during MediaDrm.getKeyRequest", e);
}
return null;
}
private class MediaDrmListener implements MediaDrm.OnEventListener {
@Override
public void onEvent(MediaDrm mediaDrm, byte[] sessionArray, int event,
int extra, byte[] data) {
if (DEBUG) Log.d(LOGTAG, "MediaDrmListener.onEvent()");
if (sessionArray == null) {
if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Null session.");
return;
}
ByteBuffer session = ByteBuffer.wrap(sessionArray);
if (!sessionExists(session)) {
if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Invalid session.");
return;
}
// On L, these events are treated as exceptions and handled correspondingly.
// Leaving this code block for logging message.
String sessionId = new String(session.array());
switch (event) {
case MediaDrm.EVENT_PROVISION_REQUIRED:
if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_PROVISION_REQUIRED");
break;
case MediaDrm.EVENT_KEY_REQUIRED:
if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_REQUIRED");
// No need to handle here if we're not in privacy mode.
break;
case MediaDrm.EVENT_KEY_EXPIRED:
if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_EXPIRED, sessionId=" + sessionId);
break;
case MediaDrm.EVENT_VENDOR_DEFINED:
if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_VENDOR_DEFINED, sessionId=" + sessionId);
break;
default:
if (DEBUG) Log.d(LOGTAG, "Invalid DRM event " + event);
return;
}
}
}
private ByteBuffer openSession() throws android.media.NotProvisionedException {
try {
byte[] sessionId = mDrm.openSession();
// ByteBuffer.wrap() is backed by the byte[]. Make a clone here in
// case the underlying byte[] is modified.
return ByteBuffer.wrap(sessionId.clone());
} catch (android.media.NotProvisionedException e) {
// Throw NotProvisionedException so that we can startProvisioning().
throw e;
} catch (java.lang.RuntimeException e) {
if (DEBUG) Log.d(LOGTAG, "Cannot open a new session:" + e.getMessage());
release();
return null;
} catch (android.media.MediaDrmException e) {
// Other MediaDrmExceptions (e.g. ResourceBusyException) are not
// recoverable.
release();
return null;
}
}
private boolean sessionExists(ByteBuffer session) {
if (mCryptoSessionId == null) {
if (DEBUG) Log.d(LOGTAG, "Session doesn't exist because media crypto session is not created.");
return false;
}
if (session == null) {
if (DEBUG) Log.d(LOGTAG, "Session is null, not in map !");
return false;
}
return !session.equals(mCryptoSessionId) && mSessionIds.contains(session);
}
private class PostRequestTask extends AsyncTask<Void, Void, Void> {
private static final String LOGTAG = "PostRequestTask";
private int mPromiseId;
private String mURL;
private byte[] mDrmRequest;
private byte[] mResponseBody;
PostRequestTask(int promiseId, String url, byte[] drmRequest) {
this.mPromiseId = promiseId;
this.mURL = url;
this.mDrmRequest = drmRequest;
}
@Override
protected Void doInBackground(Void... params) {
try {
URL finalURL = new URL(mURL + "&signedRequest=" + URLEncoder.encode(new String(mDrmRequest), "UTF-8"));
HttpURLConnection urlConnection = (HttpURLConnection) finalURL.openConnection();
urlConnection.setRequestMethod("POST");
if (DEBUG) Log.d(LOGTAG, "Provisioning, posting url =" + finalURL.toString());
// Add data
urlConnection.setRequestProperty("Accept", "*/*");
urlConnection.setRequestProperty("User-Agent", getCDMUserAgent());
urlConnection.setRequestProperty("Content-Type", "application/json");
// Execute HTTP Post Request
urlConnection.connect();
int responseCode = urlConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in =
new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
mResponseBody = String.valueOf(response).getBytes();
if (DEBUG) Log.d(LOGTAG, "Provisioning, response received.");
if (mResponseBody != null) Log.d(LOGTAG, "response length=" + mResponseBody.length);
} else {
Log.d(LOGTAG, "Provisioning, server returned HTTP error code :" + responseCode);
}
} catch (IOException e) {
Log.e(LOGTAG, "Got exception during posting provisioning request ...", e);
}
return null;
}
@Override
protected void onPostExecute(Void v) {
onProvisionResponse(mPromiseId, mResponseBody);
}
}
private boolean provideProvisionResponse(byte[] response) {
if (response == null || response.length == 0) {
if (DEBUG) Log.d(LOGTAG, "Invalid provision response.");
return false;
}
try {
mDrm.provideProvisionResponse(response);
return true;
} catch (android.media.DeniedByServerException e) {
if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
} catch (java.lang.IllegalStateException e) {
if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
}
return false;
}
private void savePendingCreateSessionData(int token,
int promiseId,
byte[] initData,
String mime) {
if (DEBUG) Log.d(LOGTAG, "savePendingCreateSessionData, promiseId : " + promiseId);
mPendingCreateSessionDataQueue.offer(new PendingCreateSessionData(token, promiseId, initData, mime));
}
private void processPendingCreateSessionData() {
if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData ... ");
assertTrue(mProvisioningPromiseId == 0);
try {
while (!mPendingCreateSessionDataQueue.isEmpty()) {
PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData, promiseId : " + pendingData.mPromiseId);
createSession(pendingData.mToken,
pendingData.mPromiseId,
pendingData.mMimeType,
pendingData.mInitData);
}
} catch (Exception e) {
Log.e(LOGTAG, "Got excpetion during processPendingCreateSessionData ...", e);
}
}
private void resumePendingOperations() {
if (mHandlerThread == null) {
mHandlerThread = new HandlerThread("PendingSessionOpsThread");
mHandlerThread.start();
}
if (mHandler == null) {
mHandler = new Handler(mHandlerThread.getLooper());
}
mHandler.post(new Runnable() {
@Override
public void run() {
processPendingCreateSessionData();
}
});
}
// Only triggered when failed on {openSession, getKeyRequest}
private void startProvisioning(int promiseId) {
if (DEBUG) Log.d(LOGTAG, "startProvisioning()");
if (mProvisioningPromiseId > 0) {
// Already in provisioning.
return;
}
try {
mProvisioningPromiseId = promiseId;
MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest();
PostRequestTask postTask =
new PostRequestTask(promiseId, request.getDefaultUrl(), request.getData());
postTask.execute();
} catch (Exception e) {
onRejectPromise(promiseId, "Exception happened in startProvisioning !");
mProvisioningPromiseId = 0;
}
}
private void onProvisionResponse(int promiseId, byte[] response) {
if (DEBUG) Log.d(LOGTAG, "onProvisionResponse()");
mProvisioningPromiseId = 0;
boolean success = provideProvisionResponse(response);
if (success) {
// Promise will either be resovled / rejected in createSession during
// resuming operations.
resumePendingOperations();
} else {
onRejectPromise(promiseId, "Failed to provide provision response.");
}
}
private boolean ensureMediaCryptoCreated() throws android.media.NotProvisionedException {
if (mCrypto != null) {
return true;
}
try {
mCryptoSessionId = openSession();
if (mCryptoSessionId == null) {
if (DEBUG) Log.d(LOGTAG, "Cannot open session for MediaCrypto");
return false;
}
if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
final byte [] cryptoSessionId = mCryptoSessionId.array();
mCrypto = new MediaCrypto(mSchemeUUID, cryptoSessionId);
String strCryptoSessionId = new String(cryptoSessionId);
mSessionIds.add(mCryptoSessionId);
if (DEBUG) Log.d(LOGTAG, "MediaCrypto successfully created! - SId " + INVALID_SESSION_ID + ", " + strCryptoSessionId);
return true;
} else {
if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto for unsupported scheme.");
return false;
}
} catch (android.media.MediaCryptoException e) {
if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto:" + e.getMessage());
release();
return false;
} catch (android.media.NotProvisionedException e) {
if (DEBUG) Log.d(LOGTAG, "ensureMediaCryptoCreated::Device not provisioned:" + e.getMessage());
throw e;
}
}
private UUID convertKeySystemToSchemeUUID(String keySystem) {
if (WIDEVINE_KEY_SYSTEM.equals(keySystem)) {
return WIDEVINE_SCHEME_UUID;
}
if (DEBUG) Log.d(LOGTAG, "Cannot convert unsupported key system : " + keySystem);
return null;
}
private String getCDMUserAgent() {
// This user agent is found and hard-coded in Android(L) source code and
// Chromium project. Not sure if it's gonna change in the future.
String ua = "Widevine CDM v1.0";
return ua;
}
}

View file

@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.annotation.TargetApi;
import static android.os.Build.VERSION_CODES.M;
import android.media.MediaDrm;
import android.util.Log;
import java.util.List;
public class GeckoMediaDrmBridgeV23 extends GeckoMediaDrmBridgeV21 {
private static final String LOGTAG = "GeckoMediaDrmBridgeV23";
private static final boolean DEBUG = false;
GeckoMediaDrmBridgeV23(String keySystem) throws Exception {
super(keySystem);
if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV23 ctor");
mDrm.setOnKeyStatusChangeListener(new KeyStatusChangeListener(), null);
}
@TargetApi(M)
private class KeyStatusChangeListener implements MediaDrm.OnKeyStatusChangeListener {
@Override
public void onKeyStatusChange(MediaDrm mediaDrm,
byte[] sessionId,
List<MediaDrm.KeyStatus> keyInformation,
boolean hasNewUsableKey) {
if (DEBUG) Log.d(LOGTAG, "[onKeyStatusChange] hasNewUsableKey = " + hasNewUsableKey);
if (keyInformation.size() == 0) {
return;
}
SessionKeyInfo[] keyInfos = new SessionKeyInfo[keyInformation.size()];
for (int i = 0; i < keyInformation.size(); i++) {
MediaDrm.KeyStatus keyStatus = keyInformation.get(i);
keyInfos[i] = new SessionKeyInfo(keyStatus.getKeyId(),
keyStatus.getStatusCode());
}
onSessionBatchedKeyChanged(sessionId, keyInfos);
}
}
}

View file

@ -0,0 +1,405 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
// Implement async API using MediaCodec sync mode (API v16).
// This class uses internal worker thread/handler (mBufferPoller) to poll
// input and output buffer and notifies the client through callbacks.
final class JellyBeanAsyncCodec implements AsyncCodec {
private static final String LOGTAG = "GeckoAsyncCodecAPIv16";
private static final boolean DEBUG = false;
private static final int ERROR_CODEC = -10000;
private abstract class CancelableHandler extends Handler {
private static final int MSG_CANCELLATION = 0x434E434C; // 'CNCL'
protected CancelableHandler(Looper looper) {
super(looper);
}
protected void cancel() {
removeCallbacksAndMessages(null);
sendEmptyMessage(MSG_CANCELLATION);
// Wait until handleMessageLocked() is done.
synchronized (this) { }
}
protected boolean isCanceled() {
return hasMessages(MSG_CANCELLATION);
}
// Subclass should implement this and return true if it handles msg.
// Warning: Never, ever call super.handleMessage() in this method!
protected abstract boolean handleMessageLocked(Message msg);
public final void handleMessage(Message msg) {
// Block cancel() during handleMessageLocked().
synchronized (this) {
if (isCanceled() || handleMessageLocked(msg)) {
return;
}
}
switch (msg.what) {
case MSG_CANCELLATION:
// Just a marker. Nothing to do here.
if (DEBUG) Log.d(LOGTAG, "handler " + this + " done cancellation, codec=" + JellyBeanAsyncCodec.this);
break;
default:
super.handleMessage(msg);
break;
}
}
}
// A handler to invoke AsyncCodec.Callbacks methods.
private final class CallbackSender extends CancelableHandler {
private static final int MSG_INPUT_BUFFER_AVAILABLE = 1;
private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2;
private static final int MSG_OUTPUT_FORMAT_CHANGE = 3;
private static final int MSG_ERROR = 4;
private Callbacks mCallbacks;
private CallbackSender(Looper looper, Callbacks callbacks) {
super(looper);
mCallbacks = callbacks;
}
public void notifyInputBuffer(int index) {
if (isCanceled()) {
return;
}
Message msg = obtainMessage(MSG_INPUT_BUFFER_AVAILABLE);
msg.arg1 = index;
processMessage(msg);
}
private void processMessage(Message msg) {
if (Looper.myLooper() == getLooper()) {
handleMessage(msg);
} else {
sendMessage(msg);
}
}
public void notifyOutputBuffer(int index, MediaCodec.BufferInfo info) {
if (isCanceled()) {
return;
}
Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, info);
msg.arg1 = index;
processMessage(msg);
}
public void notifyOutputFormat(MediaFormat format) {
if (isCanceled()) {
return;
}
processMessage(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format));
}
public void notifyError(int result) {
Log.e(LOGTAG, "codec error:" + result);
processMessage(obtainMessage(MSG_ERROR, result, 0));
}
protected boolean handleMessageLocked(Message msg) {
switch (msg.what) {
case MSG_INPUT_BUFFER_AVAILABLE: // arg1: buffer index.
mCallbacks.onInputBufferAvailable(JellyBeanAsyncCodec.this,
msg.arg1);
break;
case MSG_OUTPUT_BUFFER_AVAILABLE: // arg1: buffer index, obj: info.
mCallbacks.onOutputBufferAvailable(JellyBeanAsyncCodec.this,
msg.arg1,
(MediaCodec.BufferInfo)msg.obj);
break;
case MSG_OUTPUT_FORMAT_CHANGE: // obj: output format.
mCallbacks.onOutputFormatChanged(JellyBeanAsyncCodec.this,
(MediaFormat)msg.obj);
break;
case MSG_ERROR: // arg1: error code.
mCallbacks.onError(JellyBeanAsyncCodec.this, msg.arg1);
break;
default:
return false;
}
return true;
}
}
// Handler to poll input and output buffers using dequeue(Input|Output)Buffer(),
// with 10ms time-out. Once triggered and successfully gets a buffer, it
// will schedule next polling until EOS or failure. To prevent it from
// automatically polling more buffer, use cancel() it inherits from
// CancelableHandler.
private final class BufferPoller extends CancelableHandler {
private static final int MSG_POLL_INPUT_BUFFERS = 1;
private static final int MSG_POLL_OUTPUT_BUFFERS = 2;
private static final long DEQUEUE_TIMEOUT_US = 10000;
public BufferPoller(Looper looper) {
super(looper);
}
private void schedulePollingIfNotCanceled(int what) {
if (isCanceled()) {
return;
}
schedulePolling(what);
}
private void schedulePolling(int what) {
if (needsBuffer(what)) {
sendEmptyMessage(what);
}
}
private boolean needsBuffer(int what) {
if (mOutputEnded && (what == MSG_POLL_OUTPUT_BUFFERS)) {
return false;
}
if (mInputEnded && (what == MSG_POLL_INPUT_BUFFERS)) {
return false;
}
return true;
}
protected boolean handleMessageLocked(Message msg) {
try {
switch (msg.what) {
case MSG_POLL_INPUT_BUFFERS:
pollInputBuffer();
break;
case MSG_POLL_OUTPUT_BUFFERS:
pollOutputBuffer();
break;
default:
return false;
}
} catch (IllegalStateException e) {
e.printStackTrace();
mCallbackSender.notifyError(ERROR_CODEC);
}
return true;
}
private void pollInputBuffer() {
int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US);
if (result >= 0) {
mCallbackSender.notifyInputBuffer(result);
schedulePollingIfNotCanceled(BufferPoller.MSG_POLL_INPUT_BUFFERS);
} else if (result != MediaCodec.INFO_TRY_AGAIN_LATER) {
mCallbackSender.notifyError(result);
}
}
private void pollOutputBuffer() {
boolean dequeueMoreBuffer = true;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int result = mCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US);
if (result >= 0) {
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mOutputEnded = true;
}
mCallbackSender.notifyOutputBuffer(result, info);
if (!hasMessages(MSG_POLL_INPUT_BUFFERS)) {
schedulePollingIfNotCanceled(MSG_POLL_INPUT_BUFFERS);
}
} else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
mOutputBuffers = mCodec.getOutputBuffers();
} else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mCallbackSender.notifyOutputFormat(mCodec.getOutputFormat());
} else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
// When input ended, keep polling remaining output buffer until EOS.
dequeueMoreBuffer = mInputEnded;
} else {
mCallbackSender.notifyError(result);
dequeueMoreBuffer = false;
}
if (dequeueMoreBuffer) {
schedulePollingIfNotCanceled(MSG_POLL_OUTPUT_BUFFERS);
}
}
}
private MediaCodec mCodec;
private ByteBuffer[] mInputBuffers;
private ByteBuffer[] mOutputBuffers;
private AsyncCodec.Callbacks mCallbacks;
private CallbackSender mCallbackSender;
private BufferPoller mBufferPoller;
private volatile boolean mInputEnded;
private volatile boolean mOutputEnded;
// Must be called on a thread with looper.
/* package */ JellyBeanAsyncCodec(String name) throws IOException {
mCodec = MediaCodec.createByCodecName(name);
initBufferPoller(name + " buffer poller");
}
private void initBufferPoller(String name) {
if (mBufferPoller != null) {
Log.e(LOGTAG, "poller already initialized");
return;
}
HandlerThread thread = new HandlerThread(name);
thread.start();
mBufferPoller = new BufferPoller(thread.getLooper());
if (DEBUG) Log.d(LOGTAG, "start poller for codec:" + this + ", thread=" + thread.getThreadId());
}
@Override
public void setCallbacks(AsyncCodec.Callbacks callbacks, Handler handler) {
if (callbacks == null) {
return;
}
Looper looper = (handler == null) ? null : handler.getLooper();
if (looper == null) {
// Use this thread if no handler supplied.
looper = Looper.myLooper();
}
if (looper == null) {
// This thread has no looper. Use poller thread.
looper = mBufferPoller.getLooper();
}
mCallbackSender = new CallbackSender(looper, callbacks);
if (DEBUG) Log.d(LOGTAG, "setCallbacks(): sender=" + mCallbackSender);
}
@Override
public void configure(MediaFormat format, Surface surface, int flags) {
assertCallbacks();
mCodec.configure(format, surface, null, flags);
}
private void assertCallbacks() {
if (mCallbackSender == null) {
throw new IllegalStateException(LOGTAG + ": callback must be supplied with setCallbacks().");
}
}
@Override
public void start() {
assertCallbacks();
mCodec.start();
mInputEnded = false;
mOutputEnded = false;
mInputBuffers = mCodec.getInputBuffers();
mOutputBuffers = mCodec.getOutputBuffers();
mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
}
@Override
public final void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags) {
assertCallbacks();
mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
} catch (IllegalStateException e) {
e.printStackTrace();
mCallbackSender.notifyError(ERROR_CODEC);
return;
}
mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
}
@Override
public final void releaseOutputBuffer(int index, boolean render) {
assertCallbacks();
mCodec.releaseOutputBuffer(index, render);
}
@Override
public final ByteBuffer getInputBuffer(int index) {
assertCallbacks();
return mInputBuffers[index];
}
@Override
public final ByteBuffer getOutputBuffer(int index) {
assertCallbacks();
return mOutputBuffers[index];
}
@Override
public void flush() {
assertCallbacks();
mInputEnded = false;
mOutputEnded = false;
cancelPendingTasks();
mCodec.flush();
mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
}
private void cancelPendingTasks() {
mBufferPoller.cancel();
mCallbackSender.cancel();
}
@Override
public void stop() {
assertCallbacks();
cancelPendingTasks();
mCodec.stop();
}
@Override
public void release() {
assertCallbacks();
cancelPendingTasks();
mCallbackSender = null;
mCodec.release();
stopBufferPoller();
}
private void stopBufferPoller() {
if (mBufferPoller == null) {
Log.e(LOGTAG, "no initialized poller.");
return;
}
mBufferPoller.getLooper().quit();
mBufferPoller = null;
if (DEBUG) Log.d(LOGTAG, "stop poller " + this);
}
}

View file

@ -0,0 +1,162 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import org.mozilla.gecko.AppConstants;
import android.media.MediaCrypto;
import android.util.Log;
final class LocalMediaDrmBridge implements GeckoMediaDrm {
private static final String LOGTAG = "GeckoLocalMediaDrmBridge";
private static final boolean DEBUG = false;
private GeckoMediaDrm mBridge = null;
private CallbacksForwarder mCallbacksFwd;
// Forward the callback calls from GeckoMediaDrmBridgeV{21,23}
// to the callback MediaDrmProxy.Callbacks.
private class CallbacksForwarder implements GeckoMediaDrm.Callbacks {
private final GeckoMediaDrm.Callbacks mProxyCallbacks;
CallbacksForwarder(GeckoMediaDrm.Callbacks callbacks) {
assertTrue(callbacks != null);
mProxyCallbacks = callbacks;
}
@Override
public void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request) {
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onSessionCreated(createSessionToken,
promiseId,
sessionId,
request);
}
@Override
public void onSessionUpdated(int promiseId, byte[] sessionId) {
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onSessionUpdated(promiseId, sessionId);
}
@Override
public void onSessionClosed(int promiseId, byte[] sessionId) {
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onSessionClosed(promiseId, sessionId);
}
@Override
public void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request) {
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
}
@Override
public void onSessionError(byte[] sessionId,
String message) {
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onSessionError(sessionId, message);
}
@Override
public void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos) {
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
}
@Override
public void onRejectPromise(int promiseId, String message) {
if (DEBUG) Log.d(LOGTAG, message);
assertTrue(mProxyCallbacks != null);
mProxyCallbacks.onRejectPromise(promiseId, message);
}
} // CallbacksForwarder
private static void assertTrue(boolean condition) {
if (DEBUG && !condition) {
throw new AssertionError("Expected condition to be true");
}
}
LocalMediaDrmBridge(String keySystem) throws Exception {
if (AppConstants.Versions.preLollipop) {
mBridge = null;
} else if (AppConstants.Versions.feature21Plus &&
AppConstants.Versions.preMarshmallow) {
mBridge = new GeckoMediaDrmBridgeV21(keySystem);
} else {
mBridge = new GeckoMediaDrmBridgeV23(keySystem);
}
}
@Override
public synchronized void setCallbacks(Callbacks callbacks) {
if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
mCallbacksFwd = new CallbacksForwarder(callbacks);
assertTrue(mBridge != null);
mBridge.setCallbacks(mCallbacksFwd);
}
@Override
public synchronized void createSession(int createSessionToken,
int promiseId,
String initDataType,
byte[] initData) {
if (DEBUG) Log.d(LOGTAG, "createSession()");
assertTrue(mCallbacksFwd != null);
try {
mBridge.createSession(createSessionToken, promiseId, initDataType, initData);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to createSession.", e);
mCallbacksFwd.onRejectPromise(promiseId, "Failed to createSession.");
}
}
@Override
public synchronized void updateSession(int promiseId, String sessionId, byte[] response) {
if (DEBUG) Log.d(LOGTAG, "updateSession()");
assertTrue(mCallbacksFwd != null);
try {
mBridge.updateSession(promiseId, sessionId, response);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to updateSession.", e);
mCallbacksFwd.onRejectPromise(promiseId, "Failed to updateSession.");
}
}
@Override
public synchronized void closeSession(int promiseId, String sessionId) {
if (DEBUG) Log.d(LOGTAG, "closeSession()");
assertTrue(mCallbacksFwd != null);
try {
mBridge.closeSession(promiseId, sessionId);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to closeSession.", e);
mCallbacksFwd.onRejectPromise(promiseId, "Failed to closeSession.");
}
}
@Override
public synchronized void release() {
if (DEBUG) Log.d(LOGTAG, "release()");
try {
mBridge.release();
mBridge = null;
mCallbacksFwd = null;
} catch (Exception e) {
Log.e(LOGTAG, "Failed to release", e);
}
}
@Override
public synchronized MediaCrypto getMediaCrypto() {
if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
return mBridge != null ? mBridge.getMediaCrypto() : null;
}
}

View file

@ -0,0 +1,431 @@
package org.mozilla.gecko.media;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.util.ThreadUtils;
import java.lang.ref.WeakReference;
public class MediaControlService extends Service implements Tabs.OnTabsChangedListener {
private static final String LOGTAG = "MediaControlService";
public static final String ACTION_INIT = "action_init";
public static final String ACTION_RESUME = "action_resume";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_STOP = "action_stop";
public static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus";
public static final String ACTION_PAUSE_BY_AUDIO_FOCUS = "action_pause_audio_focus";
private static final int MEDIA_CONTROL_ID = 1;
private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
private String mActionState = ACTION_STOP;
private MediaSession mSession;
private MediaController mController;
private PrefsHelper.PrefHandler mPrefsObserver;
private final String[] mPrefs = { MEDIA_CONTROL_PREF };
private boolean mInitialize = false;
private boolean mIsMediaControlPrefOn = true;
private static WeakReference<Tab> mTabReference = new WeakReference<>(null);
private int minCoverSize;
private int coverSize;
@Override
public void onCreate() {
initialize();
}
@Override
public void onDestroy() {
shutdown();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent(intent);
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public boolean onUnbind(Intent intent) {
mSession.release();
return super.onUnbind(intent);
}
@Override
public void onTaskRemoved(Intent rootIntent) {
shutdown();
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
if (!mInitialize) {
return;
}
final Tab playingTab = mTabReference.get();
switch (msg) {
case MEDIA_PLAYING_CHANGE:
// The 'MEDIA_PLAYING_CHANGE' would only be received when the
// media starts or ends.
if (playingTab != tab && tab.isMediaPlaying()) {
mTabReference = new WeakReference<>(tab);
notifyControlInterfaceChanged(ACTION_PAUSE);
} else if (playingTab == tab && !tab.isMediaPlaying()) {
notifyControlInterfaceChanged(ACTION_STOP);
mTabReference = new WeakReference<>(null);
}
break;
case MEDIA_PLAYING_RESUME:
// user resume the paused-by-control media from page so that we
// should make the control interface consistent.
if (playingTab == tab && !isMediaPlaying()) {
notifyControlInterfaceChanged(ACTION_PAUSE);
}
break;
case CLOSED:
if (playingTab == null || playingTab == tab) {
// Remove the controls when the playing tab disappeared or was closed.
notifyControlInterfaceChanged(ACTION_STOP);
}
break;
case FAVICON:
if (playingTab == tab) {
final String actionForPendingIntent = isMediaPlaying() ?
ACTION_PAUSE : ACTION_RESUME;
notifyControlInterfaceChanged(actionForPendingIntent);
}
break;
}
}
private boolean isMediaPlaying() {
return mActionState.equals(ACTION_RESUME);
}
private void initialize() {
if (mInitialize ||
!isAndroidVersionLollopopOrHigher()) {
return;
}
Log.d(LOGTAG, "initialize");
getGeckoPreference();
initMediaSession();
coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
minCoverSize = getResources().getDimensionPixelSize(R.dimen.favicon_bg);
Tabs.registerOnTabsChangedListener(this);
mInitialize = true;
}
private void shutdown() {
if (!mInitialize) {
return;
}
Log.d(LOGTAG, "shutdown");
notifyControlInterfaceChanged(ACTION_STOP);
PrefsHelper.removeObserver(mPrefsObserver);
Tabs.unregisterOnTabsChangedListener(this);
mInitialize = false;
stopSelf();
}
private boolean isAndroidVersionLollopopOrHigher() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
private void handleIntent(Intent intent) {
if (intent == null || intent.getAction() == null || !mInitialize) {
return;
}
Log.d(LOGTAG, "HandleIntent, action = " + intent.getAction() + ", actionState = " + mActionState);
switch (intent.getAction()) {
case ACTION_INIT :
// This action is used to create a service and do the initialization,
// the actual operation would be executed via control interface's
// pending intent.
break;
case ACTION_RESUME :
mController.getTransportControls().play();
break;
case ACTION_PAUSE :
mController.getTransportControls().pause();
break;
case ACTION_STOP :
mController.getTransportControls().stop();
break;
case ACTION_PAUSE_BY_AUDIO_FOCUS :
mController.getTransportControls().sendCustomAction(ACTION_PAUSE_BY_AUDIO_FOCUS, null);
break;
case ACTION_RESUME_BY_AUDIO_FOCUS :
mController.getTransportControls().sendCustomAction(ACTION_RESUME_BY_AUDIO_FOCUS, null);
break;
}
}
private void getGeckoPreference() {
mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
@Override
public void prefValue(String pref, boolean value) {
if (pref.equals(MEDIA_CONTROL_PREF)) {
mIsMediaControlPrefOn = value;
// If media is playing, we just need to create or remove
// the media control interface.
if (mActionState.equals(ACTION_RESUME)) {
notifyControlInterfaceChanged(mIsMediaControlPrefOn ?
ACTION_PAUSE : ACTION_STOP);
}
// If turn off pref during pausing, except removing media
// interface, we also need to stop the service and notify
// gecko about that.
if (mActionState.equals(ACTION_PAUSE) &&
!mIsMediaControlPrefOn) {
Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(ACTION_STOP);
handleIntent(intent);
}
}
}
};
PrefsHelper.addObserver(mPrefs, mPrefsObserver);
}
private void initMediaSession() {
// Android MediaSession is introduced since version L.
mSession = new MediaSession(getApplicationContext(),
"fennec media session");
mController = new MediaController(getApplicationContext(),
mSession.getSessionToken());
mSession.setCallback(new MediaSession.Callback() {
@Override
public void onCustomAction(String action, Bundle extras) {
if (action.equals(ACTION_PAUSE_BY_AUDIO_FOCUS)) {
Log.d(LOGTAG, "Controller, pause by audio focus changed");
notifyControlInterfaceChanged(ACTION_RESUME);
} else if (action.equals(ACTION_RESUME_BY_AUDIO_FOCUS)) {
Log.d(LOGTAG, "Controller, resume by audio focus changed");
notifyControlInterfaceChanged(ACTION_PAUSE);
}
}
@Override
public void onPlay() {
Log.d(LOGTAG, "Controller, onPlay");
super.onPlay();
notifyControlInterfaceChanged(ACTION_PAUSE);
notifyObservers("MediaControl", "resumeMedia");
// To make sure we always own audio focus during playing.
AudioFocusAgent.notifyStartedPlaying();
}
@Override
public void onPause() {
Log.d(LOGTAG, "Controller, onPause");
super.onPause();
notifyControlInterfaceChanged(ACTION_RESUME);
notifyObservers("MediaControl", "mediaControlPaused");
AudioFocusAgent.notifyStoppedPlaying();
}
@Override
public void onStop() {
Log.d(LOGTAG, "Controller, onStop");
super.onStop();
notifyControlInterfaceChanged(ACTION_STOP);
notifyObservers("MediaControl", "mediaControlStopped");
mTabReference = new WeakReference<>(null);
}
});
}
private void notifyObservers(String topic, String data) {
GeckoAppShell.notifyObservers(topic, data);
}
private boolean isNeedToRemoveControlInterface(String action) {
return action.equals(ACTION_STOP);
}
private void notifyControlInterfaceChanged(final String uiAction) {
if (!mInitialize) {
return;
}
Log.d(LOGTAG, "notifyControlInterfaceChanged, action = " + uiAction);
if (isNeedToRemoveControlInterface(uiAction)) {
stopForeground(false);
NotificationManagerCompat.from(this).cancel(MEDIA_CONTROL_ID);
setActionState(uiAction);
return;
}
if (!mIsMediaControlPrefOn) {
return;
}
final Tab tab = mTabReference.get();
if (tab == null) {
return;
}
setActionState(uiAction);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
updateNotification(tab, uiAction);
}
});
}
private void setActionState(final String uiAction) {
switch (uiAction) {
case ACTION_PAUSE:
mActionState = ACTION_RESUME;
break;
case ACTION_RESUME:
mActionState = ACTION_PAUSE;
break;
case ACTION_STOP:
mActionState = ACTION_STOP;
break;
}
}
private void updateNotification(Tab tab, String action) {
ThreadUtils.assertNotOnUiThread();
final Notification.MediaStyle style = new Notification.MediaStyle();
style.setShowActionsInCompactView(0);
final boolean isPlaying = isMediaPlaying();
final int visibility = tab.isPrivate() ?
Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC;
final Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.flat_icon)
.setLargeIcon(generateCoverArt(tab))
.setContentTitle(tab.getTitle())
.setContentText(tab.getURL())
.setContentIntent(createContentIntent(tab.getId()))
.setDeleteIntent(createDeleteIntent())
.setStyle(style)
.addAction(createNotificationAction(action))
.setOngoing(isPlaying)
.setShowWhen(false)
.setWhen(0)
.setVisibility(visibility)
.build();
if (isPlaying) {
startForeground(MEDIA_CONTROL_ID, notification);
} else {
stopForeground(false);
NotificationManagerCompat.from(this)
.notify(MEDIA_CONTROL_ID, notification);
}
}
private Notification.Action createNotificationAction(String action) {
boolean isPlayAction = action.equals(ACTION_RESUME);
int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
String title = getString(isPlayAction ? R.string.media_play : R.string.media_pause);
final Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(action);
final PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
//noinspection deprecation - The new constructor is only for API > 23
return new Notification.Action.Builder(icon, title, pendingIntent).build();
}
private PendingIntent createContentIntent(int tabId) {
Intent intent = new Intent(getApplicationContext(), BrowserApp.class);
intent.setAction(GeckoApp.ACTION_SWITCH_TAB);
intent.putExtra("TabId", tabId);
return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createDeleteIntent() {
Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(ACTION_STOP);
return PendingIntent.getService(getApplicationContext(), 1, intent, 0);
}
private Bitmap generateCoverArt(Tab tab) {
final Bitmap favicon = tab.getFavicon();
// If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
if (favicon == null || favicon.getWidth() < minCoverSize || favicon.getHeight() < minCoverSize) {
// Use the launcher icon as fallback
return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media);
}
// Favicon should at least have half of the size of the cover
int width = Math.max(favicon.getWidth(), coverSize / 2);
int height = Math.max(favicon.getHeight(), coverSize / 2);
final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(coverArt);
canvas.drawColor(0xFF777777);
int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2));
int right = Math.min(coverSize, left + width);
int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2));
int bottom = Math.min(coverSize, top + height);
final Paint paint = new Paint();
paint.setAntiAlias(true);
canvas.drawBitmap(favicon,
new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
new Rect(left, top, right, bottom),
paint);
return coverArt;
}
}

View file

@ -0,0 +1,307 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import java.util.ArrayList;
import java.util.UUID;
import org.mozilla.gecko.mozglue.JNIObject;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.AppConstants;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCrypto;
import android.media.MediaDrm;
import android.util.Log;
import android.os.Build;
public final class MediaDrmProxy {
private static final String LOGTAG = "GeckoMediaDrmProxy";
private static final boolean DEBUG = false;
private static final UUID WIDEVINE_SCHEME_UUID =
new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
@WrapForJNI
private static final String AAC = "audio/mp4a-latm";
@WrapForJNI
private static final String AVC = "video/avc";
@WrapForJNI
private static final String VORBIS = "audio/vorbis";
@WrapForJNI
private static final String VP8 = "video/x-vnd.on2.vp8";
@WrapForJNI
private static final String VP9 = "video/x-vnd.on2.vp9";
@WrapForJNI
private static final String OPUS = "audio/opus";
// A flag to avoid using the native object that has been destroyed.
private boolean mDestroyed;
private GeckoMediaDrm mImpl;
public static ArrayList<MediaDrmProxy> mProxyList = new ArrayList<MediaDrmProxy>();
private static boolean isSystemSupported() {
// Support versions >= LOLLIPOP
if (AppConstants.Versions.preLollipop) {
if (DEBUG) Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT);
return false;
}
return true;
}
@WrapForJNI
public static boolean isSchemeSupported(String keySystem) {
if (!isSystemSupported()) {
return false;
}
if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID)
&& MediaCrypto.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID);
}
if (DEBUG) Log.d(LOGTAG, "isSchemeSupported key sytem = " + keySystem);
return false;
}
@WrapForJNI
public static boolean IsCryptoSchemeSupported(String keySystem,
String container) {
if (!isSystemSupported()) {
return false;
}
if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID, container);
}
if (DEBUG) Log.d(LOGTAG, "cannot decrypt key sytem = " + keySystem + ", container = " + container);
return false;
}
@WrapForJNI
public static boolean CanDecode(String mimeType) {
for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
if (info.isEncoder()) {
continue;
}
for (String m : info.getSupportedTypes()) {
if (m.equals(mimeType)) {
return true;
}
}
}
if (DEBUG) Log.d(LOGTAG, "cannot decode mimetype = " + mimeType);
return false;
}
// Interface for callback to native.
public interface Callbacks {
void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request);
void onSessionUpdated(int promiseId, byte[] sessionId);
void onSessionClosed(int promiseId, byte[] sessionId);
void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request);
void onSessionError(byte[] sessionId,
String message);
// MediaDrm.KeyStatus is available in API level 23(M)
// https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html
// For compatibility between L and M above, we'll unwrap the KeyStatus structure
// and store the keyid and status into SessionKeyInfo and pass to native(MediaDrmCDMProxy).
void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos);
void onRejectPromise(int promiseId,
String message);
} // Callbacks
public static class NativeMediaDrmProxyCallbacks extends JNIObject implements Callbacks {
@WrapForJNI(calledFrom = "gecko")
NativeMediaDrmProxyCallbacks() {}
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request);
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onSessionUpdated(int promiseId, byte[] sessionId);
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onSessionClosed(int promiseId, byte[] sessionId);
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request);
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onSessionError(byte[] sessionId,
String message);
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos);
@Override
@WrapForJNI(dispatchTo = "gecko")
public native void onRejectPromise(int promiseId,
String message);
@Override // JNIObject
protected void disposeNative() {
throw new UnsupportedOperationException();
}
} // NativeMediaDrmProxyCallbacks
// A proxy to callback from LocalMediaDrmBridge to native instance.
public static class MediaDrmProxyCallbacks implements GeckoMediaDrm.Callbacks {
private final Callbacks mNativeCallbacks;
private final MediaDrmProxy mProxy;
public MediaDrmProxyCallbacks(MediaDrmProxy proxy, Callbacks callbacks) {
mNativeCallbacks = callbacks;
mProxy = proxy;
}
@Override
public void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onSessionCreated(createSessionToken,
promiseId,
sessionId,
request);
}
}
@Override
public void onSessionUpdated(int promiseId, byte[] sessionId) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onSessionUpdated(promiseId, sessionId);
}
}
@Override
public void onSessionClosed(int promiseId, byte[] sessionId) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onSessionClosed(promiseId, sessionId);
}
}
@Override
public void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
}
}
@Override
public void onSessionError(byte[] sessionId,
String message) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onSessionError(sessionId, message);
}
}
@Override
public void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
}
}
@Override
public void onRejectPromise(int promiseId,
String message) {
if (!mProxy.isDestroyed()) {
mNativeCallbacks.onRejectPromise(promiseId, message);
}
}
} // MediaDrmProxyCallbacks
public boolean isDestroyed() {
return mDestroyed;
}
@WrapForJNI(calledFrom = "gecko")
public static MediaDrmProxy create(String keySystem,
Callbacks nativeCallbacks,
boolean isRemote) {
// TODO: Will implement {Local,Remote}MediaDrmBridge instantiation by
// '''isRemote''' flag in Bug 1307818.
MediaDrmProxy proxy = new MediaDrmProxy(keySystem, nativeCallbacks);
return proxy;
}
MediaDrmProxy(String keySystem, Callbacks nativeCallbacks) {
if (DEBUG) Log.d(LOGTAG, "Constructing MediaDrmProxy");
// TODO: Bug 1306185 will implement the LocalMediaDrmBridge as an impl
// of GeckoMediaDrm for in-process decoding mode.
//mImpl = new LocalMediaDrmBridge(keySystem);
mImpl.setCallbacks(new MediaDrmProxyCallbacks(this, nativeCallbacks));
mProxyList.add(this);
}
@WrapForJNI
private void createSession(int createSessionToken,
int promiseId,
String initDataType,
byte[] initData) {
if (DEBUG) Log.d(LOGTAG, "createSession, promiseId = " + promiseId);
mImpl.createSession(createSessionToken,
promiseId,
initDataType,
initData);
}
@WrapForJNI
private void updateSession(int promiseId, String sessionId, byte[] response) {
if (DEBUG) Log.d(LOGTAG, "updateSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")");
mImpl.updateSession(promiseId, sessionId, response);
}
@WrapForJNI
private void closeSession(int promiseId, String sessionId) {
if (DEBUG) Log.d(LOGTAG, "closeSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")");
mImpl.closeSession(promiseId, sessionId);
}
@WrapForJNI // Called when natvie object is destroyed.
private void destroy() {
if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed.");
if (mDestroyed) {
return;
}
mDestroyed = true;
release();
}
private void release() {
if (DEBUG) Log.d(LOGTAG, "release");
mProxyList.remove(this);
mImpl.release();
}
}

View file

@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import org.mozilla.gecko.mozglue.GeckoLoader;
public final class MediaManager extends Service {
private static boolean sNativeLibLoaded;
private Binder mBinder = new IMediaManager.Stub() {
@Override
public ICodec createCodec() throws RemoteException {
return new Codec();
}
@Override
public IMediaDrmBridge createRemoteMediaDrmBridge(String keySystem,
String stubId)
throws RemoteException {
return new RemoteMediaDrmBridgeStub(keySystem, stubId);
}
};
@Override
public synchronized void onCreate() {
if (!sNativeLibLoaded) {
GeckoLoader.doLoadLibrary(this, "mozglue");
sNativeLibLoaded = true;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

View file

@ -0,0 +1,224 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.Telemetry;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.MediaFormat;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.Surface;
import android.util.Log;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.LinkedList;
import java.util.List;
public final class RemoteManager implements IBinder.DeathRecipient {
private static final String LOGTAG = "GeckoRemoteManager";
private static final boolean DEBUG = false;
private static RemoteManager sRemoteManager = null;
public synchronized static RemoteManager getInstance() {
if (sRemoteManager == null) {
sRemoteManager = new RemoteManager();
}
sRemoteManager.init();
return sRemoteManager;
}
private List<CodecProxy> mProxies = new LinkedList<CodecProxy>();
private volatile IMediaManager mRemote;
private volatile CountDownLatch mConnectionLatch;
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (DEBUG) Log.d(LOGTAG, "service connected");
try {
service.linkToDeath(RemoteManager.this, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
mRemote = IMediaManager.Stub.asInterface(service);
if (mConnectionLatch != null) {
mConnectionLatch.countDown();
}
}
/**
* Called when a connection to the Service has been lost. This typically
* happens when the process hosting the service has crashed or been killed.
* This does <em>not</em> remove the ServiceConnection itself -- this
* binding to the service will remain active, and you will receive a call
* to {@link #onServiceConnected} when the Service is next running.
*
* @param name The concrete component name of the service whose
* connection has been lost.
*/
@Override
public void onServiceDisconnected(ComponentName name) {
if (DEBUG) Log.d(LOGTAG, "service disconnected");
mRemote.asBinder().unlinkToDeath(RemoteManager.this, 0);
mRemote = null;
if (mConnectionLatch != null) {
mConnectionLatch.countDown();
}
}
};
private synchronized boolean init() {
if (mRemote != null) {
return true;
}
if (DEBUG) Log.d(LOGTAG, "init remote manager " + this);
Context appCtxt = GeckoAppShell.getApplicationContext();
if (DEBUG) Log.d(LOGTAG, "ctxt=" + appCtxt);
appCtxt.bindService(new Intent(appCtxt, MediaManager.class),
mConnection, Context.BIND_AUTO_CREATE);
if (!waitConnection()) {
appCtxt.unbindService(mConnection);
return false;
}
return true;
}
private boolean waitConnection() {
boolean ok = false;
mConnectionLatch = new CountDownLatch(1);
try {
int retryCount = 0;
while (retryCount < 5) {
if (DEBUG) Log.d(LOGTAG, "waiting for connection latch:" + mConnectionLatch);
mConnectionLatch.await(1, TimeUnit.SECONDS);
if (mConnectionLatch.getCount() == 0) {
break;
}
Log.w(LOGTAG, "Creator not connected in 1s. Try again.");
retryCount++;
}
ok = true;
} catch (InterruptedException e) {
Log.e(LOGTAG, "service not connected in 5 seconds. Stop waiting.");
e.printStackTrace();
}
mConnectionLatch = null;
return ok;
}
public synchronized CodecProxy createCodec(MediaFormat format,
Surface surface,
CodecProxy.Callbacks callbacks) {
if (mRemote == null) {
if (DEBUG) Log.d(LOGTAG, "createCodec failed due to not initialize");
return null;
}
try {
ICodec remote = mRemote.createCodec();
CodecProxy proxy = CodecProxy.createCodecProxy(format, surface, callbacks);
if (proxy.init(remote)) {
mProxies.add(proxy);
return proxy;
} else {
return null;
}
} catch (RemoteException e) {
e.printStackTrace();
return null;
}
}
private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
private void reportDecodingProcessCrash() {
Telemetry.addToHistogram(MEDIA_DECODING_PROCESS_CRASH, 1);
}
public synchronized IMediaDrmBridge createRemoteMediaDrmBridge(String keySystem,
String stubId) {
if (mRemote == null) {
if (DEBUG) Log.d(LOGTAG, "createRemoteMediaDrmBridge failed due to not initialize");
return null;
}
try {
IMediaDrmBridge remoteBridge =
mRemote.createRemoteMediaDrmBridge(keySystem, stubId);
return remoteBridge;
} catch (RemoteException e) {
Log.e(LOGTAG, "Got exception during createRemoteMediaDrmBridge().", e);
return null;
}
}
@Override
public void binderDied() {
Log.e(LOGTAG, "remote codec is dead");
reportDecodingProcessCrash();
handleRemoteDeath();
}
private synchronized void handleRemoteDeath() {
// Wait for onServiceDisconnected()
if (!waitConnection()) {
notifyError(true);
return;
}
// Restart
if (init() && recoverRemoteCodec()) {
notifyError(false);
} else {
notifyError(true);
}
}
private synchronized void notifyError(boolean fatal) {
for (CodecProxy proxy : mProxies) {
proxy.reportError(fatal);
}
}
private synchronized boolean recoverRemoteCodec() {
if (DEBUG) Log.d(LOGTAG, "recover codec");
boolean ok = true;
try {
for (CodecProxy proxy : mProxies) {
ok &= proxy.init(mRemote.createCodec());
}
return ok;
} catch (RemoteException e) {
return false;
}
}
public void releaseCodec(CodecProxy proxy) throws DeadObjectException, RemoteException {
if (mRemote == null) {
if (DEBUG) Log.d(LOGTAG, "releaseCodec called but not initialized yet");
return;
}
proxy.deinit();
synchronized (this) {
if (mProxies.remove(proxy) && mProxies.isEmpty()) {
release();
}
}
}
private void release() {
if (DEBUG) Log.d(LOGTAG, "release remote manager " + this);
Context appCtxt = GeckoAppShell.getApplicationContext();
mRemote.asBinder().unlinkToDeath(this, 0);
mRemote = null;
appCtxt.unbindService(mConnection);
}
} // RemoteManager

View file

@ -0,0 +1,152 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCrypto;
import android.util.Log;
final class RemoteMediaDrmBridge implements GeckoMediaDrm {
private static final String LOGTAG = "GeckoRemoteMediaDrmBridge";
private static final boolean DEBUG = false;
private CallbacksForwarder mCallbacksFwd;
private IMediaDrmBridge mRemote;
// Forward callbacks from remote bridge stub to MediaDrmProxy.
private static class CallbacksForwarder extends IMediaDrmBridgeCallbacks.Stub {
private final GeckoMediaDrm.Callbacks mProxyCallbacks;
CallbacksForwarder(Callbacks callbacks) {
assertTrue(callbacks != null);
mProxyCallbacks = callbacks;
}
@Override
public void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request) {
mProxyCallbacks.onSessionCreated(createSessionToken,
promiseId,
sessionId,
request);
}
@Override
public void onSessionUpdated(int promiseId, byte[] sessionId) {
mProxyCallbacks.onSessionUpdated(promiseId, sessionId);
}
@Override
public void onSessionClosed(int promiseId, byte[] sessionId) {
mProxyCallbacks.onSessionClosed(promiseId, sessionId);
}
@Override
public void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request) {
mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
}
@Override
public void onSessionError(byte[] sessionId, String message) {
mProxyCallbacks.onSessionError(sessionId, message);
}
@Override
public void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos) {
mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
}
@Override
public void onRejectPromise(int promiseId, String message) {
mProxyCallbacks.onRejectPromise(promiseId, message);
}
} // CallbacksForwarder
/* package-private */ static void assertTrue(boolean condition) {
if (DEBUG && !condition) {
throw new AssertionError("Expected condition to be true");
}
}
public RemoteMediaDrmBridge(IMediaDrmBridge remoteBridge) {
assertTrue(remoteBridge != null);
mRemote = remoteBridge;
}
@Override
public synchronized void setCallbacks(Callbacks callbacks) {
if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
assertTrue(callbacks != null);
assertTrue(mRemote != null);
mCallbacksFwd = new CallbacksForwarder(callbacks);
try {
mRemote.setCallbacks(mCallbacksFwd);
} catch (Exception e) {
Log.e(LOGTAG, "Got exception during setCallbacks", e);
}
}
@Override
public synchronized void createSession(int createSessionToken,
int promiseId,
String initDataType,
byte[] initData) {
if (DEBUG) Log.d(LOGTAG, "createSession()");
try {
mRemote.createSession(createSessionToken, promiseId, initDataType, initData);
} catch (Exception e) {
Log.e(LOGTAG, "Got exception while creating remote session.", e);
mCallbacksFwd.onRejectPromise(promiseId, "Failed to create session.");
}
}
@Override
public synchronized void updateSession(int promiseId, String sessionId, byte[] response) {
if (DEBUG) Log.d(LOGTAG, "updateSession()");
try {
mRemote.updateSession(promiseId, sessionId, response);
} catch (Exception e) {
Log.e(LOGTAG, "Got exception while updating remote session.", e);
mCallbacksFwd.onRejectPromise(promiseId, "Failed to update session.");
}
}
@Override
public synchronized void closeSession(int promiseId, String sessionId) {
if (DEBUG) Log.d(LOGTAG, "closeSession()");
try {
mRemote.closeSession(promiseId, sessionId);
} catch (Exception e) {
Log.e(LOGTAG, "Got exception while closing remote session.", e);
mCallbacksFwd.onRejectPromise(promiseId, "Failed to close session.");
}
}
@Override
public synchronized void release() {
if (DEBUG) Log.d(LOGTAG, "release()");
try {
mRemote.release();
} catch (Exception e) {
Log.e(LOGTAG, "Got exception while releasing RemoteDrmBridge.", e);
}
mRemote = null;
mCallbacksFwd = null;
}
@Override
public synchronized MediaCrypto getMediaCrypto() {
if (DEBUG) Log.d(LOGTAG, "getMediaCrypto(), should not enter here!");
assertTrue(false);
return null;
}
}

View file

@ -0,0 +1,247 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import org.mozilla.gecko.AppConstants;
import java.util.ArrayList;
import android.media.MediaCrypto;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
final class RemoteMediaDrmBridgeStub extends IMediaDrmBridge.Stub implements IBinder.DeathRecipient {
private static final String LOGTAG = "GeckoRemoteMediaDrmBridgeStub";
private static final boolean DEBUG = false;
private volatile IMediaDrmBridgeCallbacks mCallbacks = null;
// Underlying bridge implmenetaion, i.e. GeckoMediaDrmBrdigeV21.
private GeckoMediaDrm mBridge = null;
// mStubId is initialized during stub construction. It should be a unique
// string which is generated in MediaDrmProxy in Fennec App process and is
// used for Codec to obtain corresponding MediaCrypto as input to achieve
// decryption.
// The generated stubId will be delivered to Codec via a code path starting
// from MediaDrmProxy -> MediaDrmCDMProxy -> RemoteDataDecoder => IPC => Codec.
private String mStubId = "";
public static ArrayList<RemoteMediaDrmBridgeStub> mBridgeStubs =
new ArrayList<RemoteMediaDrmBridgeStub>();
private String getId() {
return mStubId;
}
private MediaCrypto getMediaCryptoFromBridge() {
return mBridge != null ? mBridge.getMediaCrypto() : null;
}
public static synchronized MediaCrypto getMediaCrypto(String stubId) {
if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
for (int i = 0; i < mBridgeStubs.size(); i++) {
if (mBridgeStubs.get(i) != null &&
mBridgeStubs.get(i).getId().equals(stubId)) {
return mBridgeStubs.get(i).getMediaCryptoFromBridge();
}
}
return null;
}
// Callback to RemoteMediaDrmBridge.
private final class Callbacks implements GeckoMediaDrm.Callbacks {
private IMediaDrmBridgeCallbacks mRemoteCallbacks;
public Callbacks(IMediaDrmBridgeCallbacks remote) {
mRemoteCallbacks = remote;
}
@Override
public void onSessionCreated(int createSessionToken,
int promiseId,
byte[] sessionId,
byte[] request) {
if (DEBUG) Log.d(LOGTAG, "onSessionCreated()");
try {
mRemoteCallbacks.onSessionCreated(createSessionToken,
promiseId,
sessionId,
request);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public void onSessionUpdated(int promiseId, byte[] sessionId) {
if (DEBUG) Log.d(LOGTAG, "onSessionUpdated()");
try {
mRemoteCallbacks.onSessionUpdated(promiseId, sessionId);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public void onSessionClosed(int promiseId, byte[] sessionId) {
if (DEBUG) Log.d(LOGTAG, "onSessionClosed()");
try {
mRemoteCallbacks.onSessionClosed(promiseId, sessionId);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public void onSessionMessage(byte[] sessionId,
int sessionMessageType,
byte[] request) {
if (DEBUG) Log.d(LOGTAG, "onSessionMessage()");
try {
mRemoteCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public void onSessionError(byte[] sessionId, String message) {
if (DEBUG) Log.d(LOGTAG, "onSessionError()");
try {
mRemoteCallbacks.onSessionError(sessionId, message);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public void onSessionBatchedKeyChanged(byte[] sessionId,
SessionKeyInfo[] keyInfos) {
if (DEBUG) Log.d(LOGTAG, "onSessionBatchedKeyChanged()");
try {
mRemoteCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public void onRejectPromise(int promiseId, String message) {
if (DEBUG) Log.d(LOGTAG, "onRejectPromise()");
try {
mRemoteCallbacks.onRejectPromise(promiseId, message);
} catch (RemoteException e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
}
/* package-private */ void assertTrue(boolean condition) {
if (DEBUG && !condition) {
throw new AssertionError("Expected condition to be true");
}
}
RemoteMediaDrmBridgeStub(String keySystem, String stubId) throws RemoteException {
if (AppConstants.Versions.preLollipop) {
Log.e(LOGTAG, "Pre-Lollipop should never enter here!!");
throw new RemoteException("Error, unsupported version!");
}
try {
if (AppConstants.Versions.feature21Plus &&
AppConstants.Versions.preMarshmallow) {
mBridge = new GeckoMediaDrmBridgeV21(keySystem);
} else {
mBridge = new GeckoMediaDrmBridgeV23(keySystem);
}
mStubId = stubId;
mBridgeStubs.add(this);
} catch (Exception e) {
throw new RemoteException("RemoteMediaDrmBridgeStub cannot create bridge implementation.");
}
}
@Override
public synchronized void setCallbacks(IMediaDrmBridgeCallbacks callbacks) throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
assertTrue(mBridge != null);
assertTrue(callbacks != null);
mCallbacks = callbacks;
callbacks.asBinder().linkToDeath(this, 0);
mBridge.setCallbacks(new Callbacks(mCallbacks));
}
@Override
public synchronized void createSession(int createSessionToken,
int promiseId,
String initDataType,
byte[] initData) throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "createSession()");
try {
assertTrue(mCallbacks != null);
assertTrue(mBridge != null);
mBridge.createSession(createSessionToken,
promiseId,
initDataType,
initData);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to createSession.", e);
mCallbacks.onRejectPromise(promiseId, "Failed to createSession.");
}
}
@Override
public synchronized void updateSession(int promiseId,
String sessionId,
byte[] response) throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "updateSession()");
try {
assertTrue(mCallbacks != null);
assertTrue(mBridge != null);
mBridge.updateSession(promiseId, sessionId, response);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to updateSession.", e);
mCallbacks.onRejectPromise(promiseId, "Failed to updateSession.");
}
}
@Override
public synchronized void closeSession(int promiseId, String sessionId) throws RemoteException {
if (DEBUG) Log.d(LOGTAG, "closeSession()");
try {
assertTrue(mCallbacks != null);
assertTrue(mBridge != null);
mBridge.closeSession(promiseId, sessionId);
} catch (Exception e) {
Log.e(LOGTAG, "Failed to closeSession.", e);
mCallbacks.onRejectPromise(promiseId, "Failed to closeSession.");
}
}
// IBinder.DeathRecipient
@Override
public synchronized void binderDied() {
Log.e(LOGTAG, "Binder died !!");
try {
release();
} catch (Exception e) {
Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
}
}
@Override
public synchronized void release() {
if (DEBUG) Log.d(LOGTAG, "release()");
mBridgeStubs.remove(this);
if (mBridge != null) {
mBridge.release();
mBridge = null;
}
mCallbacks.asBinder().unlinkToDeath(this, 0);
mCallbacks = null;
mStubId = "";
}
}

View file

@ -0,0 +1,264 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CryptoInfo;
import android.os.Parcel;
import android.os.Parcelable;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.SharedMemBuffer;
import org.mozilla.gecko.mozglue.SharedMemory;
import java.io.IOException;
import java.nio.ByteBuffer;
// Parcelable carrying input/output sample data and info cross process.
public final class Sample implements Parcelable {
public static final Sample EOS;
static {
BufferInfo eosInfo = new BufferInfo();
eosInfo.set(0, 0, Long.MIN_VALUE, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
EOS = new Sample(null, eosInfo, null);
}
public interface Buffer extends Parcelable {
int capacity();
void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException;
void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException;
void dispose();
}
private static final class ArrayBuffer implements Buffer {
private byte[] mArray;
public static final Creator<ArrayBuffer> CREATOR = new Creator<ArrayBuffer>() {
@Override
public ArrayBuffer createFromParcel(Parcel in) {
return new ArrayBuffer(in);
}
@Override
public ArrayBuffer[] newArray(int size) {
return new ArrayBuffer[size];
}
};
private ArrayBuffer(Parcel in) {
mArray = in.createByteArray();
}
private ArrayBuffer(byte[] bytes) { mArray = bytes; }
@Override
public int describeContents() { return 0; }
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(mArray);
}
@Override
public int capacity() {
return mArray != null ? mArray.length : 0;
}
@Override
public void readFromByteBuffer(ByteBuffer src, int offset, int size) throws IOException {
src.position(offset);
if (mArray == null || mArray.length != size) {
mArray = new byte[size];
}
src.get(mArray, 0, size);
}
@Override
public void writeToByteBuffer(ByteBuffer dest, int offset, int size) throws IOException {
dest.put(mArray, offset, size);
}
@Override
public void dispose() {
mArray = null;
}
}
public Buffer buffer;
@WrapForJNI
public BufferInfo info;
public CryptoInfo cryptoInfo;
public static Sample create() { return create(null, new BufferInfo(), null); }
public static Sample create(ByteBuffer src, BufferInfo info, CryptoInfo cryptoInfo) {
ArrayBuffer buffer = new ArrayBuffer(byteArrayFromBuffer(src, info.offset, info.size));
BufferInfo bufferInfo = new BufferInfo();
bufferInfo.set(0, info.size, info.presentationTimeUs, info.flags);
return new Sample(buffer, bufferInfo, cryptoInfo);
}
public static Sample create(SharedMemory sharedMem) {
return new Sample(new SharedMemBuffer(sharedMem), new BufferInfo(), null);
}
private Sample(Buffer bytes, BufferInfo info, CryptoInfo cryptoInfo) {
buffer = bytes;
this.info = info;
this.cryptoInfo = cryptoInfo;
}
private Sample(Parcel in) {
readInfo(in);
readCrypto(in);
buffer = in.readParcelable(Sample.class.getClassLoader());
}
private void readInfo(Parcel in) {
int offset = in.readInt();
int size = in.readInt();
long pts = in.readLong();
int flags = in.readInt();
info = new BufferInfo();
info.set(offset, size, pts, flags);
}
private void readCrypto(Parcel in) {
int hasCryptoInfo = in.readInt();
if (hasCryptoInfo == 0) {
return;
}
byte[] iv = in.createByteArray();
byte[] key = in.createByteArray();
int mode = in.readInt();
int[] numBytesOfClearData = in.createIntArray();
int[] numBytesOfEncryptedData = in.createIntArray();
int numSubSamples = in.readInt();
cryptoInfo = new CryptoInfo();
cryptoInfo.set(numSubSamples,
numBytesOfClearData,
numBytesOfEncryptedData,
key,
iv,
mode);
}
public Sample set(ByteBuffer bytes, BufferInfo info, CryptoInfo cryptoInfo) throws IOException {
if (bytes != null && info.size > 0) {
buffer.readFromByteBuffer(bytes, info.offset, info.size);
}
this.info.set(0, info.size, info.presentationTimeUs, info.flags);
this.cryptoInfo = cryptoInfo;
return this;
}
public void dispose() {
if (isEOS()) {
return;
}
if (buffer != null) {
buffer.dispose();
buffer = null;
}
info = null;
cryptoInfo = null;
}
public boolean isEOS() {
return (this == EOS) ||
((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
}
public static final Creator<Sample> CREATOR = new Creator<Sample>() {
@Override
public Sample createFromParcel(Parcel in) {
return new Sample(in);
}
@Override
public Sample[] newArray(int size) {
return new Sample[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int parcelableFlags) {
writeInfo(dest);
writeCrypto(dest);
dest.writeParcelable(buffer, parcelableFlags);
}
private void writeInfo(Parcel dest) {
dest.writeInt(info.offset);
dest.writeInt(info.size);
dest.writeLong(info.presentationTimeUs);
dest.writeInt(info.flags);
}
private void writeCrypto(Parcel dest) {
if (cryptoInfo != null) {
dest.writeInt(1);
dest.writeByteArray(cryptoInfo.iv);
dest.writeByteArray(cryptoInfo.key);
dest.writeInt(cryptoInfo.mode);
dest.writeIntArray(cryptoInfo.numBytesOfClearData);
dest.writeIntArray(cryptoInfo.numBytesOfEncryptedData);
dest.writeInt(cryptoInfo.numSubSamples);
} else {
dest.writeInt(0);
}
}
public static byte[] byteArrayFromBuffer(ByteBuffer buffer, int offset, int size) {
if (buffer == null || buffer.capacity() == 0 || size == 0) {
return null;
}
if (buffer.hasArray() && offset == 0 && buffer.array().length == size) {
return buffer.array();
}
int length = Math.min(offset + size, buffer.capacity()) - offset;
byte[] bytes = new byte[length];
buffer.position(offset);
buffer.get(bytes);
return bytes;
}
@WrapForJNI
public void writeToByteBuffer(ByteBuffer dest) throws IOException {
if (buffer != null && dest != null && info.size > 0) {
buffer.writeToByteBuffer(dest, info.offset, info.size);
}
}
@Override
public String toString() {
if (isEOS()) {
return "EOS sample";
}
StringBuilder str = new StringBuilder();
str.append("{ buffer=").append(buffer).
append(", info=").
append("{ offset=").append(info.offset).
append(", size=").append(info.size).
append(", pts=").append(info.presentationTimeUs).
append(", flags=").append(Integer.toHexString(info.flags)).append(" }").
append(" }");
return str.toString();
}
}

View file

@ -0,0 +1,115 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.media.MediaCodec;
import org.mozilla.gecko.mozglue.SharedMemory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
final class SamplePool {
private final class Impl {
private final String mName;
private int mNextId = 0;
private int mDefaultBufferSize = 4096;
private final List<Sample> mRecycledSamples = new ArrayList<>();
private Impl(String name) {
mName = name;
}
private void setDefaultBufferSize(int size) {
mDefaultBufferSize = size;
}
private synchronized Sample allocate(int size) {
Sample sample;
if (!mRecycledSamples.isEmpty()) {
sample = mRecycledSamples.remove(0);
sample.info.set(0, 0, 0, 0);
} else {
SharedMemory shm = null;
try {
shm = new SharedMemory(mNextId++, Math.max(size, mDefaultBufferSize));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
if (shm != null) {
sample = Sample.create(shm);
} else {
sample = Sample.create();
}
}
return sample;
}
private synchronized void recycle(Sample recycled) {
if (recycled.buffer.capacity() >= mDefaultBufferSize) {
mRecycledSamples.add(recycled);
} else {
recycled.dispose();
}
}
private synchronized void clear() {
for (Sample s : mRecycledSamples) {
s.dispose();
}
mRecycledSamples.clear();
}
@Override
protected void finalize() {
clear();
}
}
private final Impl mInputs;
private final Impl mOutputs;
/* package */ SamplePool(String name) {
mInputs = new Impl(name + " input buffer pool");
mOutputs = new Impl(name + " output buffer pool");
}
/* package */ void setInputBufferSize(int size) {
mInputs.setDefaultBufferSize(size);
}
/* package */ void setOutputBufferSize(int size) {
mOutputs.setDefaultBufferSize(size);
}
/* package */ Sample obtainInput(int size) {
return mInputs.allocate(size);
}
/* package */ Sample obtainOutput(MediaCodec.BufferInfo info) {
Sample output = mOutputs.allocate(info.size);
output.info.set(0, info.size, info.presentationTimeUs, info.flags);
return output;
}
/* package */ void recycleInput(Sample sample) {
sample.cryptoInfo = null;
mInputs.recycle(sample);
}
/* package */ void recycleOutput(Sample sample) {
mOutputs.recycle(sample);
}
/* package */ void reset() {
mInputs.clear();
mOutputs.clear();
}
}

View file

@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.os.Parcel;
import android.os.Parcelable;
import org.mozilla.gecko.annotation.WrapForJNI;
public final class SessionKeyInfo implements Parcelable {
@WrapForJNI
public byte[] keyId;
@WrapForJNI
public int status;
@WrapForJNI
public SessionKeyInfo(byte[] keyId, int status) {
this.keyId = keyId;
this.status = status;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int parcelableFlags) {
dest.writeByteArray(keyId);
dest.writeInt(status);
}
public static final Creator<SessionKeyInfo> CREATOR = new Creator<SessionKeyInfo>() {
@Override
public SessionKeyInfo createFromParcel(Parcel in) {
return new SessionKeyInfo(in);
}
@Override
public SessionKeyInfo[] newArray(int size) {
return new SessionKeyInfo[size];
}
};
private SessionKeyInfo(Parcel src) {
keyId = src.createByteArray();
status = src.readInt();
}
}

View file

@ -0,0 +1,204 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.media;
import android.content.Context;
import android.graphics.Color;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.MediaController;
import android.widget.VideoView;
import org.mozilla.gecko.R;
public class VideoPlayer extends FrameLayout {
private VideoView video;
private FullScreenMediaController controller;
private FullScreenListener fullScreenListener;
private boolean isFullScreen;
public VideoPlayer(Context ctx) {
this(ctx, null);
}
public VideoPlayer(Context ctx, AttributeSet attrs) {
this(ctx, attrs, 0);
}
public VideoPlayer(Context ctx, AttributeSet attrs, int defStyle) {
super(ctx, attrs, defStyle);
setFullScreen(false);
setVisibility(View.GONE);
}
public void start(Uri uri) {
stop();
video = new VideoView(getContext());
controller = new FullScreenMediaController(getContext());
video.setMediaController(controller);
controller.setAnchorView(video);
video.setVideoURI(uri);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER);
addView(video, layoutParams);
setVisibility(View.VISIBLE);
video.setZOrderOnTop(true);
video.start();
}
public boolean isPlaying() {
return video != null;
}
public void stop() {
if (video == null) {
return;
}
removeAllViews();
setVisibility(View.GONE);
video.stopPlayback();
video = null;
controller = null;
}
public void setFullScreenListener(FullScreenListener listener) {
fullScreenListener = listener;
}
public boolean isFullScreen() {
return isFullScreen;
}
public void setFullScreen(boolean fullScreen) {
isFullScreen = fullScreen;
if (fullScreen) {
setBackgroundColor(Color.BLACK);
} else {
setBackgroundResource(R.color.dark_transparent_overlay);
}
if (controller != null) {
controller.setFullScreen(fullScreen);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.isSystem()) {
return super.onKeyDown(keyCode, event);
}
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (event.isSystem()) {
return super.onKeyUp(keyCode, event);
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
return true;
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
super.onTrackballEvent(event);
return true;
}
public interface FullScreenListener {
void onFullScreenChanged(boolean fullScreen);
}
private class FullScreenMediaController extends MediaController {
private ImageButton mButton;
public FullScreenMediaController(Context ctx) {
super(ctx);
mButton = new ImageButton(getContext());
mButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
mButton.setBackgroundColor(Color.TRANSPARENT);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FullScreenMediaController.this.onFullScreenClicked();
}
});
updateFullScreenButton(false);
}
public void setFullScreen(boolean fullScreen) {
updateFullScreenButton(fullScreen);
}
private void updateFullScreenButton(boolean fullScreen) {
mButton.setImageResource(fullScreen ? R.drawable.exit_fullscreen : R.drawable.fullscreen);
}
private void onFullScreenClicked() {
if (VideoPlayer.this.fullScreenListener != null) {
boolean fullScreen = !VideoPlayer.this.isFullScreen();
VideoPlayer.this.fullScreenListener.onFullScreenChanged(fullScreen);
}
}
@Override
public void setAnchorView(final View view) {
super.setAnchorView(view);
// Add the fullscreen button here because this is where the parent class actually creates
// the media buttons and their layout.
//
// http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/MediaController.java#239
//
// The media buttons are in a horizontal linear layout which is itself packed into
// a vertical layout. The vertical layout is the only child of the FrameLayout which
// MediaController inherits from.
LinearLayout child = (LinearLayout) getChildAt(0);
LinearLayout buttons = (LinearLayout) child.getChildAt(0);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.FILL_PARENT);
params.gravity = Gravity.CENTER_VERTICAL;
if (mButton.getParent() != null) {
((ViewGroup)mButton.getParent()).removeView(mButton);
}
buttons.addView(mButton, params);
}
}
}