mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add layer of indirection for DRM.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138383979
This commit is contained in:
parent
a6e2770116
commit
4cd8c77053
15 changed files with 445 additions and 36 deletions
|
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.audio.AudioTrack;
|
import com.google.android.exoplayer2.audio.AudioTrack;
|
||||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -71,7 +72,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FfmpegDecoder createDecoder(Format format) throws FfmpegDecoderException {
|
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
|
throws FfmpegDecoderException {
|
||||||
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||||
format.sampleMimeType, format.initializationData);
|
format.sampleMimeType, format.initializationData);
|
||||||
return decoder;
|
return decoder;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.audio.AudioTrack;
|
import com.google.android.exoplayer2.audio.AudioTrack;
|
||||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,7 +64,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FlacDecoder createDecoder(Format format) throws FlacDecoderException {
|
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
|
throws FlacDecoderException {
|
||||||
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
|
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.audio.AudioTrack;
|
import com.google.android.exoplayer2.audio.AudioTrack;
|
||||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,6 +59,21 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
super(eventHandler, eventListener, audioCapabilities, streamType);
|
super(eventHandler, eventListener, audioCapabilities, streamType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
|
* null if delivery of events is not required.
|
||||||
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||||
|
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||||
|
* @param streamType The type of audio stream for the {@link AudioTrack}.
|
||||||
|
*/
|
||||||
|
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||||
|
AudioCapabilities audioCapabilities, int streamType,
|
||||||
|
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
|
||||||
|
super(eventHandler, eventListener, audioCapabilities, streamType, drmSessionManager,
|
||||||
|
playClearSamplesWithoutKeys);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int supportsFormat(Format format) {
|
public int supportsFormat(Format format) {
|
||||||
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
|
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
|
||||||
|
|
@ -64,9 +81,10 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected OpusDecoder createDecoder(Format format) throws OpusDecoderException {
|
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
|
throws OpusDecoderException {
|
||||||
return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||||
format.initializationData);
|
format.initializationData, mediaCrypto);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,12 @@
|
||||||
package com.google.android.exoplayer2.ext.opus;
|
package com.google.android.exoplayer2.ext.opus;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||||
|
import com.google.android.exoplayer2.drm.DecryptionException;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -36,6 +39,12 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
private static final int SAMPLE_RATE = 48000;
|
private static final int SAMPLE_RATE = 48000;
|
||||||
|
|
||||||
|
private static final int NO_ERROR = 0;
|
||||||
|
private static final int DECODE_ERROR = -1;
|
||||||
|
private static final int DRM_ERROR = -2;
|
||||||
|
|
||||||
|
private final ExoMediaCrypto exoMediaCrypto;
|
||||||
|
|
||||||
private final int channelCount;
|
private final int channelCount;
|
||||||
private final int headerSkipSamples;
|
private final int headerSkipSamples;
|
||||||
private final int headerSeekPreRollSamples;
|
private final int headerSeekPreRollSamples;
|
||||||
|
|
@ -52,14 +61,20 @@ import java.util.List;
|
||||||
* @param initializationData Codec-specific initialization data. The first element must contain an
|
* @param initializationData Codec-specific initialization data. The first element must contain an
|
||||||
* opus header. Optionally, the list may contain two additional buffers, which must contain
|
* opus header. Optionally, the list may contain two additional buffers, which must contain
|
||||||
* the encoder delay and seek pre roll values in nanoseconds, encoded as longs.
|
* the encoder delay and seek pre roll values in nanoseconds, encoded as longs.
|
||||||
|
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
||||||
|
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||||
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
|
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||||
List<byte[]> initializationData) throws OpusDecoderException {
|
List<byte[]> initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||||
if (!OpusLibrary.isAvailable()) {
|
if (!OpusLibrary.isAvailable()) {
|
||||||
throw new OpusDecoderException("Failed to load decoder native libraries.");
|
throw new OpusDecoderException("Failed to load decoder native libraries.");
|
||||||
}
|
}
|
||||||
|
this.exoMediaCrypto = exoMediaCrypto;
|
||||||
|
if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) {
|
||||||
|
throw new OpusDecoderException("Opus decoder does not support secure decode.");
|
||||||
|
}
|
||||||
byte[] headerBytes = initializationData.get(0);
|
byte[] headerBytes = initializationData.get(0);
|
||||||
if (headerBytes.length < 19) {
|
if (headerBytes.length < 19) {
|
||||||
throw new OpusDecoderException("Header size is too small.");
|
throw new OpusDecoderException("Header size is too small.");
|
||||||
|
|
@ -139,11 +154,25 @@ import java.util.List;
|
||||||
skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples;
|
skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples;
|
||||||
}
|
}
|
||||||
ByteBuffer inputData = inputBuffer.data;
|
ByteBuffer inputData = inputBuffer.data;
|
||||||
int result = opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
|
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
|
||||||
|
int result = inputBuffer.isEncrypted()
|
||||||
|
? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
|
||||||
|
outputBuffer, SAMPLE_RATE, exoMediaCrypto, cryptoInfo.mode,
|
||||||
|
cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,
|
||||||
|
cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)
|
||||||
|
: opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
|
||||||
outputBuffer, SAMPLE_RATE);
|
outputBuffer, SAMPLE_RATE);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
|
if (result == DRM_ERROR) {
|
||||||
|
String message = "Drm error: " + opusGetErrorMessage(nativeDecoderContext);
|
||||||
|
DecryptionException cause = new DecryptionException(
|
||||||
|
opusGetErrorCode(nativeDecoderContext), message);
|
||||||
|
return new OpusDecoderException(message, cause);
|
||||||
|
} else {
|
||||||
return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result));
|
return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ByteBuffer outputData = outputBuffer.data;
|
ByteBuffer outputData = outputBuffer.data;
|
||||||
outputData.position(0);
|
outputData.position(0);
|
||||||
outputData.limit(result);
|
outputData.limit(result);
|
||||||
|
|
@ -182,8 +211,13 @@ import java.util.List;
|
||||||
int gain, byte[] streamMap);
|
int gain, byte[] streamMap);
|
||||||
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
|
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
|
||||||
SimpleOutputBuffer outputBuffer, int sampleRate);
|
SimpleOutputBuffer outputBuffer, int sampleRate);
|
||||||
|
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
|
||||||
|
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
|
||||||
|
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
|
||||||
|
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
||||||
private native void opusClose(long decoder);
|
private native void opusClose(long decoder);
|
||||||
private native void opusReset(long decoder);
|
private native void opusReset(long decoder);
|
||||||
private native String opusGetErrorMessage(int errorCode);
|
private native int opusGetErrorCode(long decoder);
|
||||||
|
private native String opusGetErrorMessage(long decoder);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,8 @@ public final class OpusDecoderException extends AudioDecoderException {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ OpusDecoderException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,5 +50,5 @@ public final class OpusLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static native String opusGetVersion();
|
public static native String opusGetVersion();
|
||||||
|
public static native boolean opusIsSecureDecodeSupported();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,13 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
|
|
||||||
static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples.
|
static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples.
|
||||||
static int channelCount;
|
static int channelCount;
|
||||||
|
static int errorCode;
|
||||||
|
|
||||||
DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount,
|
DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount,
|
||||||
jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) {
|
jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) {
|
||||||
int status = OPUS_INVALID_STATE;
|
int status = OPUS_INVALID_STATE;
|
||||||
::channelCount = channelCount;
|
::channelCount = channelCount;
|
||||||
|
errorCode = 0;
|
||||||
jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0);
|
jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0);
|
||||||
uint8_t* streamMap = reinterpret_cast<uint8_t*>(streamMapBytes);
|
uint8_t* streamMap = reinterpret_cast<uint8_t*>(streamMapBytes);
|
||||||
OpusMSDecoder* decoder = opus_multistream_decoder_create(
|
OpusMSDecoder* decoder = opus_multistream_decoder_create(
|
||||||
|
|
@ -109,10 +111,24 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs,
|
||||||
env->GetDirectBufferAddress(jOutputBufferData));
|
env->GetDirectBufferAddress(jOutputBufferData));
|
||||||
int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize,
|
int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize,
|
||||||
outputBufferData, outputSize, 0);
|
outputBufferData, outputSize, 0);
|
||||||
|
// record error code
|
||||||
|
errorCode = (sampleCount < 0) ? sampleCount : 0;
|
||||||
return (sampleCount < 0) ? sampleCount
|
return (sampleCount < 0) ? sampleCount
|
||||||
: sampleCount * kBytesPerSample * channelCount;
|
: sampleCount * kBytesPerSample * channelCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jint, opusSecureDecode, jlong jDecoder, jlong jTimeUs,
|
||||||
|
jobject jInputBuffer, jint inputSize, jobject jOutputBuffer,
|
||||||
|
jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key,
|
||||||
|
jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData,
|
||||||
|
jintArray numBytesOfEncryptedData) {
|
||||||
|
// Doesn't support
|
||||||
|
// Java client should have checked vpxSupportSecureDecode
|
||||||
|
// and avoid calling this
|
||||||
|
// return -2 (DRM Error)
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
DECODER_FUNC(void, opusClose, jlong jDecoder) {
|
DECODER_FUNC(void, opusClose, jlong jDecoder) {
|
||||||
OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);
|
OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);
|
||||||
opus_multistream_decoder_destroy(decoder);
|
opus_multistream_decoder_destroy(decoder);
|
||||||
|
|
@ -123,10 +139,19 @@ DECODER_FUNC(void, opusReset, jlong jDecoder) {
|
||||||
opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
|
opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jstring, opusGetErrorMessage, jint errorCode) {
|
DECODER_FUNC(jstring, opusGetErrorMessage, jlong jContext) {
|
||||||
return env->NewStringUTF(opus_strerror(errorCode));
|
return env->NewStringUTF(opus_strerror(errorCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jint, opusGetErrorCode, jlong jContext) {
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
LIBRARY_FUNC(jstring, opusIsSecureDecodeSupported) {
|
||||||
|
// Doesn't support
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
LIBRARY_FUNC(jstring, opusGetVersion) {
|
LIBRARY_FUNC(jstring, opusGetVersion) {
|
||||||
return env->NewStringUTF(opus_get_version_string());
|
return env->NewStringUTF(opus_get_version_string());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.vp9;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import com.google.android.exoplayer2.BaseRenderer;
|
import com.google.android.exoplayer2.BaseRenderer;
|
||||||
|
|
@ -28,8 +29,12 @@ import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.FormatHolder;
|
import com.google.android.exoplayer2.FormatHolder;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSession;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.TraceUtil;
|
import com.google.android.exoplayer2.util.TraceUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
||||||
|
|
||||||
|
|
@ -56,8 +61,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
private final boolean scaleToFit;
|
private final boolean scaleToFit;
|
||||||
private final long allowedJoiningTimeMs;
|
private final long allowedJoiningTimeMs;
|
||||||
private final int maxDroppedFramesToNotify;
|
private final int maxDroppedFramesToNotify;
|
||||||
|
private final boolean playClearSamplesWithoutKeys;
|
||||||
private final EventDispatcher eventDispatcher;
|
private final EventDispatcher eventDispatcher;
|
||||||
private final FormatHolder formatHolder;
|
private final FormatHolder formatHolder;
|
||||||
|
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||||
|
|
||||||
private DecoderCounters decoderCounters;
|
private DecoderCounters decoderCounters;
|
||||||
private Format format;
|
private Format format;
|
||||||
|
|
@ -65,6 +72,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
private DecoderInputBuffer inputBuffer;
|
private DecoderInputBuffer inputBuffer;
|
||||||
private VpxOutputBuffer outputBuffer;
|
private VpxOutputBuffer outputBuffer;
|
||||||
private VpxOutputBuffer nextOutputBuffer;
|
private VpxOutputBuffer nextOutputBuffer;
|
||||||
|
private DrmSession<ExoMediaCrypto> drmSession;
|
||||||
|
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||||
|
|
||||||
private Bitmap bitmap;
|
private Bitmap bitmap;
|
||||||
private boolean renderedFirstFrame;
|
private boolean renderedFirstFrame;
|
||||||
|
|
@ -72,6 +81,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
private Surface surface;
|
private Surface surface;
|
||||||
private VpxOutputBufferRenderer outputBufferRenderer;
|
private VpxOutputBufferRenderer outputBufferRenderer;
|
||||||
private int outputMode;
|
private int outputMode;
|
||||||
|
private boolean waitingForKeys;
|
||||||
|
|
||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
private boolean outputStreamEnded;
|
private boolean outputStreamEnded;
|
||||||
|
|
@ -104,10 +114,37 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
|
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
|
||||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
Handler eventHandler, VideoRendererEventListener eventListener,
|
||||||
int maxDroppedFramesToNotify) {
|
int maxDroppedFramesToNotify) {
|
||||||
|
this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify,
|
||||||
|
null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
|
||||||
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
|
* null if delivery of events is not required.
|
||||||
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||||
|
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||||
|
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
|
||||||
|
* media is not required.
|
||||||
|
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||||
|
* For example a media file may start with a short clear region so as to allow playback to
|
||||||
|
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
|
||||||
|
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||||
|
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||||
|
*/
|
||||||
|
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
|
||||||
|
Handler eventHandler, VideoRendererEventListener eventListener,
|
||||||
|
int maxDroppedFramesToNotify, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||||
|
boolean playClearSamplesWithoutKeys) {
|
||||||
super(C.TRACK_TYPE_VIDEO);
|
super(C.TRACK_TYPE_VIDEO);
|
||||||
this.scaleToFit = scaleToFit;
|
this.scaleToFit = scaleToFit;
|
||||||
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
||||||
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
||||||
|
this.drmSessionManager = drmSessionManager;
|
||||||
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||||
joiningDeadlineMs = -1;
|
joiningDeadlineMs = -1;
|
||||||
previousWidth = -1;
|
previousWidth = -1;
|
||||||
previousHeight = -1;
|
previousHeight = -1;
|
||||||
|
|
@ -135,12 +172,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRendererAvailable()) {
|
if (isRendererAvailable()) {
|
||||||
|
drmSession = pendingDrmSession;
|
||||||
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
|
if (drmSession != null) {
|
||||||
|
int drmSessionState = drmSession.getState();
|
||||||
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||||
|
} else if (drmSessionState == DrmSession.STATE_OPENED
|
||||||
|
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
|
||||||
|
mediaCrypto = drmSession.getMediaCrypto();
|
||||||
|
} else {
|
||||||
|
// The drm session isn't open yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
// If we don't have a decoder yet, we need to instantiate one.
|
// If we don't have a decoder yet, we need to instantiate one.
|
||||||
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
TraceUtil.beginSection("createVpxDecoder");
|
TraceUtil.beginSection("createVpxDecoder");
|
||||||
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE);
|
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||||
|
mediaCrypto);
|
||||||
decoder.setOutputMode(outputMode);
|
decoder.setOutputMode(outputMode);
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||||
|
|
@ -258,7 +310,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
surface.unlockCanvasAndPost(canvas);
|
surface.unlockCanvasAndPost(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean feedInputBuffer() throws VpxDecoderException {
|
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
|
||||||
if (inputStreamEnded) {
|
if (inputStreamEnded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +322,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int result = readSource(formatHolder, inputBuffer);
|
int result;
|
||||||
|
if (waitingForKeys) {
|
||||||
|
// We've already read an encrypted sample into buffer, and are waiting for keys.
|
||||||
|
result = C.RESULT_BUFFER_READ;
|
||||||
|
} else {
|
||||||
|
result = readSource(formatHolder, inputBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
if (result == C.RESULT_NOTHING_READ) {
|
if (result == C.RESULT_NOTHING_READ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -284,6 +343,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
boolean bufferEncrypted = inputBuffer.isEncrypted();
|
||||||
|
waitingForKeys = shouldWaitForKeys(bufferEncrypted);
|
||||||
|
if (waitingForKeys) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
inputBuffer.flip();
|
inputBuffer.flip();
|
||||||
decoder.queueInputBuffer(inputBuffer);
|
decoder.queueInputBuffer(inputBuffer);
|
||||||
decoderCounters.inputBufferCount++;
|
decoderCounters.inputBufferCount++;
|
||||||
|
|
@ -291,8 +355,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
|
if (drmSession == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int drmSessionState = drmSession.getState();
|
||||||
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||||
|
}
|
||||||
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
|
||||||
|
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
|
||||||
|
}
|
||||||
|
|
||||||
private void flushDecoder() {
|
private void flushDecoder() {
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
|
waitingForKeys = false;
|
||||||
if (outputBuffer != null) {
|
if (outputBuffer != null) {
|
||||||
outputBuffer.release();
|
outputBuffer.release();
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
|
|
@ -311,6 +388,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReady() {
|
public boolean isReady() {
|
||||||
|
if (waitingForKeys) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (format != null && (isSourceReady() || outputBuffer != null)
|
if (format != null && (isSourceReady() || outputBuffer != null)
|
||||||
&& (renderedFirstFrame || !isRendererAvailable())) {
|
&& (renderedFirstFrame || !isRendererAvailable())) {
|
||||||
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
||||||
|
|
@ -365,23 +445,46 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
format = null;
|
format = null;
|
||||||
|
waitingForKeys = false;
|
||||||
try {
|
try {
|
||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
} finally {
|
} finally {
|
||||||
|
try {
|
||||||
|
if (drmSession != null) {
|
||||||
|
drmSessionManager.releaseSession(drmSession);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
||||||
|
drmSessionManager.releaseSession(pendingDrmSession);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
drmSession = null;
|
||||||
|
pendingDrmSession = null;
|
||||||
decoderCounters.ensureUpdated();
|
decoderCounters.ensureUpdated();
|
||||||
eventDispatcher.disabled(decoderCounters);
|
eventDispatcher.disabled(decoderCounters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void releaseDecoder() {
|
private void releaseDecoder() {
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
decoder.release();
|
decoder.release();
|
||||||
decoder = null;
|
decoder = null;
|
||||||
decoderCounters.decoderReleaseCount++;
|
decoderCounters.decoderReleaseCount++;
|
||||||
|
waitingForKeys = false;
|
||||||
|
if (drmSession != null && pendingDrmSession != drmSession) {
|
||||||
|
try {
|
||||||
|
drmSessionManager.releaseSession(drmSession);
|
||||||
|
} finally {
|
||||||
|
drmSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean readFormat() {
|
private boolean readFormat() throws ExoPlaybackException {
|
||||||
int result = readSource(formatHolder, null);
|
int result = readSource(formatHolder, null);
|
||||||
if (result == C.RESULT_FORMAT_READ) {
|
if (result == C.RESULT_FORMAT_READ) {
|
||||||
onInputFormatChanged(formatHolder.format);
|
onInputFormatChanged(formatHolder.format);
|
||||||
|
|
@ -390,8 +493,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onInputFormatChanged(Format newFormat) {
|
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||||
|
Format oldFormat = format;
|
||||||
format = newFormat;
|
format = newFormat;
|
||||||
|
|
||||||
|
boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
|
||||||
|
: oldFormat.drmInitData);
|
||||||
|
if (drmInitDataChanged) {
|
||||||
|
if (format.drmInitData != null) {
|
||||||
|
if (drmSessionManager == null) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(
|
||||||
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
|
}
|
||||||
|
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
|
||||||
|
if (pendingDrmSession == drmSession) {
|
||||||
|
drmSessionManager.releaseSession(pendingDrmSession);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingDrmSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventDispatcher.inputFormatChanged(format);
|
eventDispatcher.inputFormatChanged(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@
|
||||||
package com.google.android.exoplayer2.ext.vp9;
|
package com.google.android.exoplayer2.ext.vp9;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
|
import com.google.android.exoplayer2.drm.DecryptionException;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,6 +33,11 @@ import java.nio.ByteBuffer;
|
||||||
public static final int OUTPUT_MODE_YUV = 0;
|
public static final int OUTPUT_MODE_YUV = 0;
|
||||||
public static final int OUTPUT_MODE_RGB = 1;
|
public static final int OUTPUT_MODE_RGB = 1;
|
||||||
|
|
||||||
|
private static final int NO_ERROR = 0;
|
||||||
|
private static final int DECODE_ERROR = 1;
|
||||||
|
private static final int DRM_ERROR = 2;
|
||||||
|
|
||||||
|
private final ExoMediaCrypto exoMediaCrypto;
|
||||||
private final long vpxDecContext;
|
private final long vpxDecContext;
|
||||||
|
|
||||||
private volatile int outputMode;
|
private volatile int outputMode;
|
||||||
|
|
@ -40,14 +48,20 @@ import java.nio.ByteBuffer;
|
||||||
* @param numInputBuffers The number of input buffers.
|
* @param numInputBuffers The number of input buffers.
|
||||||
* @param numOutputBuffers The number of output buffers.
|
* @param numOutputBuffers The number of output buffers.
|
||||||
* @param initialInputBufferSize The initial size of each input buffer.
|
* @param initialInputBufferSize The initial size of each input buffer.
|
||||||
|
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
||||||
|
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||||
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
|
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize)
|
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||||
throws VpxDecoderException {
|
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||||
if (!VpxLibrary.isAvailable()) {
|
if (!VpxLibrary.isAvailable()) {
|
||||||
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
||||||
}
|
}
|
||||||
|
this.exoMediaCrypto = exoMediaCrypto;
|
||||||
|
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
||||||
|
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
||||||
|
}
|
||||||
vpxDecContext = vpxInit();
|
vpxDecContext = vpxInit();
|
||||||
if (vpxDecContext == 0) {
|
if (vpxDecContext == 0) {
|
||||||
throw new VpxDecoderException("Failed to initialize decoder");
|
throw new VpxDecoderException("Failed to initialize decoder");
|
||||||
|
|
@ -90,9 +104,23 @@ import java.nio.ByteBuffer;
|
||||||
boolean reset) {
|
boolean reset) {
|
||||||
ByteBuffer inputData = inputBuffer.data;
|
ByteBuffer inputData = inputBuffer.data;
|
||||||
int inputSize = inputData.limit();
|
int inputSize = inputData.limit();
|
||||||
if (vpxDecode(vpxDecContext, inputData, inputSize) != 0) {
|
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
|
||||||
|
final long result = inputBuffer.isEncrypted()
|
||||||
|
? vpxSecureDecode(vpxDecContext, inputData, inputSize, exoMediaCrypto,
|
||||||
|
cryptoInfo.mode, cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,
|
||||||
|
cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)
|
||||||
|
: vpxDecode(vpxDecContext, inputData, inputSize);
|
||||||
|
if (result != NO_ERROR) {
|
||||||
|
if (result == DRM_ERROR) {
|
||||||
|
String message = "Drm error: " + vpxGetErrorMessage(vpxDecContext);
|
||||||
|
DecryptionException cause = new DecryptionException(
|
||||||
|
vpxGetErrorCode(vpxDecContext), message);
|
||||||
|
return new VpxDecoderException(message, cause);
|
||||||
|
} else {
|
||||||
return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext));
|
return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
outputBuffer.init(inputBuffer.timeUs, outputMode);
|
outputBuffer.init(inputBuffer.timeUs, outputMode);
|
||||||
if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) {
|
if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) {
|
||||||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||||
|
|
@ -109,7 +137,11 @@ import java.nio.ByteBuffer;
|
||||||
private native long vpxInit();
|
private native long vpxInit();
|
||||||
private native long vpxClose(long context);
|
private native long vpxClose(long context);
|
||||||
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
||||||
|
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
|
||||||
|
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
|
||||||
|
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
||||||
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
|
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
|
||||||
|
private native int vpxGetErrorCode(long context);
|
||||||
private native String vpxGetErrorMessage(long context);
|
private native String vpxGetErrorMessage(long context);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,7 @@ public class VpxDecoderException extends Exception {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ VpxDecoderException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,5 +59,5 @@ public final class VpxLibrary {
|
||||||
|
|
||||||
private static native String vpxGetVersion();
|
private static native String vpxGetVersion();
|
||||||
private static native String vpxGetBuildConfig();
|
private static native String vpxGetBuildConfig();
|
||||||
|
public static native boolean vpxIsSecureDecodeSupported();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ static jmethodID initForRgbFrame;
|
||||||
static jmethodID initForYuvFrame;
|
static jmethodID initForYuvFrame;
|
||||||
static jfieldID dataField;
|
static jfieldID dataField;
|
||||||
static jfieldID outputModeField;
|
static jfieldID outputModeField;
|
||||||
|
static int errorCode;
|
||||||
|
|
||||||
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
JNIEnv* env;
|
JNIEnv* env;
|
||||||
|
|
@ -72,6 +73,7 @@ DECODER_FUNC(jlong, vpxInit) {
|
||||||
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
|
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
|
||||||
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
||||||
cfg.threads = android_getCpuCount();
|
cfg.threads = android_getCpuCount();
|
||||||
|
errorCode = 0;
|
||||||
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
|
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
|
||||||
LOGE("ERROR: Fail to initialize libvpx decoder.");
|
LOGE("ERROR: Fail to initialize libvpx decoder.");
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -97,13 +99,26 @@ DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
|
||||||
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
|
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
|
||||||
const vpx_codec_err_t status =
|
const vpx_codec_err_t status =
|
||||||
vpx_codec_decode(context, buffer, len, NULL, 0);
|
vpx_codec_decode(context, buffer, len, NULL, 0);
|
||||||
|
errorCode = 0;
|
||||||
if (status != VPX_CODEC_OK) {
|
if (status != VPX_CODEC_OK) {
|
||||||
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
|
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
|
||||||
|
errorCode = status;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len,
|
||||||
|
jobject mediaCrypto, jint inputMode, jbyteArray&, jbyteArray&,
|
||||||
|
jint inputNumSubSamples, jintArray numBytesOfClearData,
|
||||||
|
jintArray numBytesOfEncryptedData) {
|
||||||
|
// Doesn't support
|
||||||
|
// Java client should have checked vpxSupportSecureDecode
|
||||||
|
// and avoid calling this
|
||||||
|
// return -2 (DRM Error)
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, vpxClose, jlong jContext) {
|
DECODER_FUNC(jlong, vpxClose, jlong jContext) {
|
||||||
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
|
||||||
vpx_codec_destroy(context);
|
vpx_codec_destroy(context);
|
||||||
|
|
@ -181,6 +196,15 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
|
||||||
return env->NewStringUTF(vpx_codec_error(context));
|
return env->NewStringUTF(vpx_codec_error(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) {
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
LIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) {
|
||||||
|
// Doesn't support
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
LIBRARY_FUNC(jstring, vpxGetVersion) {
|
LIBRARY_FUNC(jstring, vpxGetVersion) {
|
||||||
return env->NewStringUTF(vpx_codec_version_str());
|
return env->NewStringUTF(vpx_codec_version_str());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,15 @@ public abstract class AudioDecoderException extends Exception {
|
||||||
super(detailMessage);
|
super(detailMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param detailMessage The detail message for this exception.
|
||||||
|
* @param cause the cause (which is saved for later retrieval by the
|
||||||
|
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||||
|
* permitted, and indicates that the cause is nonexistent or
|
||||||
|
* unknown.)
|
||||||
|
*/
|
||||||
|
public AudioDecoderException(String detailMessage, Throwable cause) {
|
||||||
|
super(detailMessage, cause);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.PlaybackParams;
|
import android.media.PlaybackParams;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import com.google.android.exoplayer2.BaseRenderer;
|
import com.google.android.exoplayer2.BaseRenderer;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
@ -29,17 +30,24 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSession;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.MediaClock;
|
import com.google.android.exoplayer2.util.MediaClock;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.TraceUtil;
|
import com.google.android.exoplayer2.util.TraceUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes and renders audio using a {@link SimpleDecoder}.
|
* Decodes and renders audio using a {@link SimpleDecoder}.
|
||||||
*/
|
*/
|
||||||
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
|
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
|
||||||
|
|
||||||
|
private final boolean playClearSamplesWithoutKeys;
|
||||||
|
|
||||||
private final EventDispatcher eventDispatcher;
|
private final EventDispatcher eventDispatcher;
|
||||||
private final FormatHolder formatHolder;
|
private final FormatHolder formatHolder;
|
||||||
|
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||||
|
|
||||||
private DecoderCounters decoderCounters;
|
private DecoderCounters decoderCounters;
|
||||||
private Format inputFormat;
|
private Format inputFormat;
|
||||||
|
|
@ -47,11 +55,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
? extends AudioDecoderException> decoder;
|
? extends AudioDecoderException> decoder;
|
||||||
private DecoderInputBuffer inputBuffer;
|
private DecoderInputBuffer inputBuffer;
|
||||||
private SimpleOutputBuffer outputBuffer;
|
private SimpleOutputBuffer outputBuffer;
|
||||||
|
private DrmSession<ExoMediaCrypto> drmSession;
|
||||||
|
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||||
|
|
||||||
private long currentPositionUs;
|
private long currentPositionUs;
|
||||||
private boolean allowPositionDiscontinuity;
|
private boolean allowPositionDiscontinuity;
|
||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
private boolean outputStreamEnded;
|
private boolean outputStreamEnded;
|
||||||
|
private boolean waitingForKeys;
|
||||||
|
|
||||||
private final AudioTrack audioTrack;
|
private final AudioTrack audioTrack;
|
||||||
private int audioSessionId;
|
private int audioSessionId;
|
||||||
|
|
@ -84,7 +95,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
||||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
||||||
int streamType) {
|
int streamType) {
|
||||||
|
this(eventHandler, eventListener, audioCapabilities, streamType, null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
|
* null if delivery of events is not required.
|
||||||
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||||
|
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||||
|
* @param streamType The type of audio stream for the {@link AudioTrack}.
|
||||||
|
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
|
||||||
|
* media is not required.
|
||||||
|
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||||
|
* For example a media file may start with a short clear region so as to allow playback to
|
||||||
|
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
|
||||||
|
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||||
|
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||||
|
*/
|
||||||
|
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
||||||
|
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
||||||
|
int streamType, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||||
|
boolean playClearSamplesWithoutKeys) {
|
||||||
super(C.TRACK_TYPE_AUDIO);
|
super(C.TRACK_TYPE_AUDIO);
|
||||||
|
this.drmSessionManager = drmSessionManager;
|
||||||
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||||
audioTrack = new AudioTrack(audioCapabilities, streamType);
|
audioTrack = new AudioTrack(audioCapabilities, streamType);
|
||||||
|
|
@ -108,12 +143,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drmSession = pendingDrmSession;
|
||||||
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
|
if (drmSession != null) {
|
||||||
|
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||||
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||||
|
} else if (drmSessionState == DrmSession.STATE_OPENED
|
||||||
|
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
|
||||||
|
mediaCrypto = drmSession.getMediaCrypto();
|
||||||
|
} else {
|
||||||
|
// The drm session isn't open yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// If we don't have a decoder yet, we need to instantiate one.
|
// If we don't have a decoder yet, we need to instantiate one.
|
||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
try {
|
try {
|
||||||
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
TraceUtil.beginSection("createAudioDecoder");
|
TraceUtil.beginSection("createAudioDecoder");
|
||||||
decoder = createDecoder(inputFormat);
|
decoder = createDecoder(inputFormat, mediaCrypto);
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||||
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
|
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
|
||||||
|
|
@ -141,11 +190,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
* Creates a decoder for the given format.
|
* Creates a decoder for the given format.
|
||||||
*
|
*
|
||||||
* @param format The format for which a decoder is required.
|
* @param format The format for which a decoder is required.
|
||||||
|
* @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content.
|
||||||
|
* Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||||
* @return The decoder.
|
* @return The decoder.
|
||||||
* @throws AudioDecoderException If an error occurred creating a suitable decoder.
|
* @throws AudioDecoderException If an error occurred creating a suitable decoder.
|
||||||
*/
|
*/
|
||||||
protected abstract SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
|
protected abstract SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
|
||||||
? extends AudioDecoderException> createDecoder(Format format) throws AudioDecoderException;
|
? extends AudioDecoderException> createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||||
|
throws AudioDecoderException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the format of audio buffers output by the decoder. Will not be called until the first
|
* Returns the format of audio buffers output by the decoder. Will not be called until the first
|
||||||
|
|
@ -228,7 +280,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean feedInputBuffer() throws AudioDecoderException {
|
private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException {
|
||||||
if (inputStreamEnded) {
|
if (inputStreamEnded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +292,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int result = readSource(formatHolder, inputBuffer);
|
int result;
|
||||||
|
if (waitingForKeys) {
|
||||||
|
// We've already read an encrypted sample into buffer, and are waiting for keys.
|
||||||
|
result = C.RESULT_BUFFER_READ;
|
||||||
|
} else {
|
||||||
|
result = readSource(formatHolder, inputBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
if (result == C.RESULT_NOTHING_READ) {
|
if (result == C.RESULT_NOTHING_READ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -254,6 +313,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
boolean bufferEncrypted = inputBuffer.isEncrypted();
|
||||||
|
waitingForKeys = shouldWaitForKeys(bufferEncrypted);
|
||||||
|
if (waitingForKeys) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
inputBuffer.flip();
|
inputBuffer.flip();
|
||||||
decoder.queueInputBuffer(inputBuffer);
|
decoder.queueInputBuffer(inputBuffer);
|
||||||
decoderCounters.inputBufferCount++;
|
decoderCounters.inputBufferCount++;
|
||||||
|
|
@ -261,8 +325,21 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
|
if (drmSession == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||||
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||||
|
}
|
||||||
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
|
||||||
|
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
|
||||||
|
}
|
||||||
|
|
||||||
private void flushDecoder() {
|
private void flushDecoder() {
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
|
waitingForKeys = false;
|
||||||
if (outputBuffer != null) {
|
if (outputBuffer != null) {
|
||||||
outputBuffer.release();
|
outputBuffer.release();
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
|
|
@ -278,7 +355,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
@Override
|
@Override
|
||||||
public boolean isReady() {
|
public boolean isReady() {
|
||||||
return audioTrack.hasPendingData()
|
return audioTrack.hasPendingData()
|
||||||
|| (inputFormat != null && (isSourceReady() || outputBuffer != null));
|
|| (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -339,6 +416,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
inputFormat = null;
|
inputFormat = null;
|
||||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||||
|
waitingForKeys = false;
|
||||||
try {
|
try {
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
decoder.release();
|
decoder.release();
|
||||||
|
|
@ -347,12 +425,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
}
|
}
|
||||||
audioTrack.release();
|
audioTrack.release();
|
||||||
} finally {
|
} finally {
|
||||||
|
try {
|
||||||
|
if (drmSession != null) {
|
||||||
|
drmSessionManager.releaseSession(drmSession);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
||||||
|
drmSessionManager.releaseSession(pendingDrmSession);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
drmSession = null;
|
||||||
|
pendingDrmSession = null;
|
||||||
decoderCounters.ensureUpdated();
|
decoderCounters.ensureUpdated();
|
||||||
eventDispatcher.disabled(decoderCounters);
|
eventDispatcher.disabled(decoderCounters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean readFormat() {
|
private boolean readFormat() throws ExoPlaybackException {
|
||||||
int result = readSource(formatHolder, null);
|
int result = readSource(formatHolder, null);
|
||||||
if (result == C.RESULT_FORMAT_READ) {
|
if (result == C.RESULT_FORMAT_READ) {
|
||||||
onInputFormatChanged(formatHolder.format);
|
onInputFormatChanged(formatHolder.format);
|
||||||
|
|
@ -361,8 +453,28 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onInputFormatChanged(Format newFormat) {
|
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||||
|
Format oldFormat = inputFormat;
|
||||||
inputFormat = newFormat;
|
inputFormat = newFormat;
|
||||||
|
|
||||||
|
boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null
|
||||||
|
: oldFormat.drmInitData);
|
||||||
|
if (drmInitDataChanged) {
|
||||||
|
if (inputFormat.drmInitData != null) {
|
||||||
|
if (drmSessionManager == null) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(
|
||||||
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
|
}
|
||||||
|
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
|
||||||
|
inputFormat.drmInitData);
|
||||||
|
if (pendingDrmSession == drmSession) {
|
||||||
|
drmSessionManager.releaseSession(pendingDrmSession);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingDrmSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventDispatcher.inputFormatChanged(newFormat);
|
eventDispatcher.inputFormatChanged(newFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception when doing drm decryption using the In-App Drm
|
||||||
|
*/
|
||||||
|
public class DecryptionException extends Exception {
|
||||||
|
private final int errorCode;
|
||||||
|
|
||||||
|
public DecryptionException(int errorCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get error code
|
||||||
|
*/
|
||||||
|
public int getErrorCode() {
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue