mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 16:28:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
366
mobile/android/base/java/org/mozilla/gecko/media/Codec.java
Normal file
366
mobile/android/base/java/org/mozilla/gecko/media/Codec.java
Normal 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;
|
||||
}
|
||||
}
|
||||
191
mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
Normal file
191
mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "";
|
||||
}
|
||||
}
|
||||
264
mobile/android/base/java/org/mozilla/gecko/media/Sample.java
Normal file
264
mobile/android/base/java/org/mozilla/gecko/media/Sample.java
Normal 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();
|
||||
}
|
||||
}
|
||||
115
mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java
Normal file
115
mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue