blob: 888cf2355606c7754462c5a7c7c478b6023dceb6 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.cts;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.media.DrmInitData;
import android.media.MediaCas;
import android.media.MediaCasException;
import android.media.MediaCasException.UnsupportedCasException;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaDescrambler;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* JB(API 16) introduces {@link MediaCodec} API. It allows apps have more control over
* media playback, pushes individual frames to decoder and supports decryption via
* {@link MediaCrypto} API.
*
* {@link MediaDrm} can be used to obtain keys for decrypting protected media streams,
* in conjunction with MediaCrypto.
*/
public class MediaCodecClearKeyPlayer implements MediaTimeProvider {
private static final String TAG = MediaCodecClearKeyPlayer.class.getSimpleName();
private static final String FILE_SCHEME = "file://";
private static final int STATE_IDLE = 1;
private static final int STATE_PREPARING = 2;
private static final int STATE_PLAYING = 3;
private static final int STATE_PAUSED = 4;
private static final UUID CLEARKEY_SCHEME_UUID =
new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
private boolean mEncryptedAudio;
private boolean mEncryptedVideo;
private volatile boolean mThreadStarted = false;
private byte[] mSessionId;
private boolean mScrambled;
private CodecState mAudioTrackState;
private int mMediaFormatHeight;
private int mMediaFormatWidth;
private int mState;
private long mDeltaTimeUs;
private long mDurationUs;
private Map<Integer, CodecState> mAudioCodecStates;
private Map<Integer, CodecState> mVideoCodecStates;
private Map<String, String> mAudioHeaders;
private Map<String, String> mVideoHeaders;
private Map<UUID, byte[]> mPsshInitData;
private MediaCrypto mCrypto;
private MediaCas mMediaCas;
private MediaDescrambler mAudioDescrambler;
private MediaDescrambler mVideoDescrambler;
private MediaExtractor mAudioExtractor;
private MediaExtractor mVideoExtractor;
private Deque<Surface> mSurfaces;
private Thread mThread;
private Uri mAudioUri;
private Uri mVideoUri;
private Context mContext;
private Resources mResources;
private Error mErrorFromThread;
private static final byte[] PSSH = hexStringToByteArray(
// BMFF box header (4 bytes size + 'pssh')
"0000003470737368" +
// Full box header (version = 1 flags = 0
"01000000" +
// SystemID
"1077efecc0b24d02ace33c1e52e2fb4b" +
// Number of key ids
"00000001" +
// Key id
"30303030303030303030303030303030" +
// size of data, must be zero
"00000000");
// ClearKey CAS/Descrambler test provision string
private static final String sProvisionStr =
"{ " +
" \"id\": 21140844, " +
" \"name\": \"Test Title\", " +
" \"lowercase_organization_name\": \"Android\", " +
" \"asset_key\": { " +
" \"encryption_key\": \"nezAr3CHFrmBR9R8Tedotw==\" " +
" }, " +
" \"cas_type\": 1, " +
" \"track_types\": [ ] " +
"} " ;
// ClearKey private data (0-bytes of length 4)
private static final byte[] sCasPrivateInfo = hexStringToByteArray("00000000");
/**
* Convert a hex string into byte array.
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/*
* Media player class to stream CENC content using MediaCodec class.
*/
public MediaCodecClearKeyPlayer(
List<Surface> surfaces, byte[] sessionId, boolean scrambled, Context context) {
mSessionId = sessionId;
mScrambled = scrambled;
mSurfaces = new ArrayDeque<>(surfaces);
mContext = context;
mResources = context.getResources();
mState = STATE_IDLE;
mThread = new Thread(new Runnable() {
@Override
public void run() {
int n = 0;
while (mThreadStarted == true) {
doSomeWork();
if (mAudioTrackState != null) {
mAudioTrackState.processAudioTrack();
}
try {
Thread.sleep(5);
} catch (InterruptedException ex) {
Log.d(TAG, "Thread interrupted");
}
if(++n % 1000 == 0) {
cycleSurfaces();
}
}
if (mAudioTrackState != null) {
mAudioTrackState.stopAudioTrack();
}
}
});
}
public void setAudioDataSource(Uri uri, Map<String, String> headers, boolean encrypted) {
mAudioUri = uri;
mAudioHeaders = headers;
mEncryptedAudio = encrypted;
}
public void setVideoDataSource(Uri uri, Map<String, String> headers, boolean encrypted) {
mVideoUri = uri;
mVideoHeaders = headers;
mEncryptedVideo = encrypted;
}
public final int getMediaFormatHeight() {
return mMediaFormatHeight;
}
public final int getMediaFormatWidth() {
return mMediaFormatWidth;
}
public final byte[] getDrmInitData() {
for (MediaExtractor ex: new MediaExtractor[] {mVideoExtractor, mAudioExtractor}) {
DrmInitData drmInitData = ex.getDrmInitData();
if (drmInitData != null) {
DrmInitData.SchemeInitData schemeInitData = drmInitData.get(CLEARKEY_SCHEME_UUID);
if (schemeInitData != null && schemeInitData.data != null) {
// llama content still does not contain pssh data, return hard coded PSSH
return (schemeInitData.data.length > 1)? schemeInitData.data : PSSH;
}
}
}
// Should not happen after we get content that has the clear key system id.
return PSSH;
}
private void prepareAudio() throws IOException, MediaCasException {
boolean hasAudio = false;
for (int i = mAudioExtractor.getTrackCount(); i-- > 0;) {
MediaFormat format = mAudioExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (!mime.startsWith("audio/")) {
continue;
}
Log.d(TAG, "audio track #" + i + " " + format + " " + mime +
" Is ADTS:" + getMediaFormatInteger(format, MediaFormat.KEY_IS_ADTS) +
" Sample rate:" + getMediaFormatInteger(format, MediaFormat.KEY_SAMPLE_RATE) +
" Channel count:" +
getMediaFormatInteger(format, MediaFormat.KEY_CHANNEL_COUNT));
if (mScrambled) {
MediaExtractor.CasInfo casInfo = mAudioExtractor.getCasInfo(i);
if (casInfo != null && casInfo.getSession() != null) {
mAudioDescrambler = new MediaDescrambler(casInfo.getSystemId());
mAudioDescrambler.setMediaCasSession(casInfo.getSession());
}
}
if (!hasAudio) {
mAudioExtractor.selectTrack(i);
addTrack(i, format, mEncryptedAudio);
hasAudio = true;
if (format.containsKey(MediaFormat.KEY_DURATION)) {
long durationUs = format.getLong(MediaFormat.KEY_DURATION);
if (durationUs > mDurationUs) {
mDurationUs = durationUs;
}
Log.d(TAG, "audio track format #" + i +
" Duration:" + mDurationUs + " microseconds");
}
if (hasAudio) {
break;
}
}
}
}
private void prepareVideo() throws IOException, MediaCasException {
boolean hasVideo = false;
for (int i = mVideoExtractor.getTrackCount(); i-- > 0;) {
MediaFormat format = mVideoExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (!mime.startsWith("video/")) {
continue;
}
mMediaFormatHeight = getMediaFormatInteger(format, MediaFormat.KEY_HEIGHT);
mMediaFormatWidth = getMediaFormatInteger(format, MediaFormat.KEY_WIDTH);
Log.d(TAG, "video track #" + i + " " + format + " " + mime +
" Width:" + mMediaFormatWidth + ", Height:" + mMediaFormatHeight);
if (mScrambled) {
MediaExtractor.CasInfo casInfo = mVideoExtractor.getCasInfo(i);
if (casInfo != null && casInfo.getSession() != null) {
mVideoDescrambler = new MediaDescrambler(casInfo.getSystemId());
mVideoDescrambler.setMediaCasSession(casInfo.getSession());
}
}
if (!hasVideo) {
mVideoExtractor.selectTrack(i);
addTrack(i, format, mEncryptedVideo);
hasVideo = true;
if (format.containsKey(MediaFormat.KEY_DURATION)) {
long durationUs = format.getLong(MediaFormat.KEY_DURATION);
if (durationUs > mDurationUs) {
mDurationUs = durationUs;
}
Log.d(TAG, "track format #" + i + " Duration:" +
mDurationUs + " microseconds");
}
if (hasVideo) {
break;
}
}
}
}
private void initCasAndDescrambler(MediaExtractor extractor) throws MediaCasException {
int trackCount = extractor.getTrackCount();
for (int trackId = 0; trackId < trackCount; trackId++) {
android.media.MediaFormat format = extractor.getTrackFormat(trackId);
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
Log.d(TAG, "track "+ trackId + ": " + mime);
if (MediaFormat.MIMETYPE_VIDEO_SCRAMBLED.equals(mime) ||
MediaFormat.MIMETYPE_AUDIO_SCRAMBLED.equals(mime)) {
MediaExtractor.CasInfo casInfo = extractor.getCasInfo(trackId);
if (casInfo != null) {
if (!Arrays.equals(sCasPrivateInfo, casInfo.getPrivateData())) {
throw new Error("Cas private data mismatch");
}
// Need MANAGE_USERS or CREATE_USERS permission to access
// ActivityManager#getCurrentUse in MediaCas, then adopt it from shell.
InstrumentationRegistry
.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
try {
mMediaCas = new MediaCas(casInfo.getSystemId());
} finally {
InstrumentationRegistry
.getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
mMediaCas.provision(sProvisionStr);
extractor.setMediaCas(mMediaCas);
break;
}
}
}
}
public void prepare() throws IOException, MediaCryptoException, MediaCasException {
if (null == mCrypto && (mEncryptedVideo || mEncryptedAudio)) {
try {
byte[] initData = new byte[0];
mCrypto = new MediaCrypto(CLEARKEY_SCHEME_UUID, initData);
} catch (MediaCryptoException e) {
reset();
Log.e(TAG, "Failed to create MediaCrypto instance.");
throw e;
}
mCrypto.setMediaDrmSession(mSessionId);
} else {
reset();
}
if (null == mAudioExtractor) {
mAudioExtractor = new MediaExtractor();
if (null == mAudioExtractor) {
Log.e(TAG, "Cannot create Audio extractor.");
return;
}
}
mAudioExtractor.setDataSource(mContext, mAudioUri, mAudioHeaders);
if (mScrambled) {
initCasAndDescrambler(mAudioExtractor);
mVideoExtractor = mAudioExtractor;
} else {
if (null == mVideoExtractor){
mVideoExtractor = new MediaExtractor();
if (null == mVideoExtractor) {
Log.e(TAG, "Cannot create Video extractor.");
return;
}
}
mVideoExtractor.setDataSource(mContext, mVideoUri, mVideoHeaders);
}
if (null == mVideoCodecStates) {
mVideoCodecStates = new HashMap<Integer, CodecState>();
} else {
mVideoCodecStates.clear();
}
if (null == mAudioCodecStates) {
mAudioCodecStates = new HashMap<Integer, CodecState>();
} else {
mAudioCodecStates.clear();
}
prepareVideo();
prepareAudio();
mState = STATE_PAUSED;
}
private void addTrack(int trackIndex, MediaFormat format,
boolean encrypted) throws IOException {
String mime = format.getString(MediaFormat.KEY_MIME);
boolean isVideo = mime.startsWith("video/");
boolean isAudio = mime.startsWith("audio/");
MediaCodec codec;
if (encrypted && mCrypto.requiresSecureDecoderComponent(mime)) {
codec = MediaCodec.createByCodecName(
getSecureDecoderNameForMime(mime));
} else {
codec = MediaCodec.createDecoderByType(mime);
}
if (!mScrambled) {
codec.configure(
format,
isVideo ? mSurfaces.getFirst() : null,
mCrypto,
0);
} else {
codec.configure(
format,
isVideo ? mSurfaces.getFirst() : null,
0,
isVideo ? mVideoDescrambler : mAudioDescrambler);
}
CodecState state;
if (isVideo) {
state = new CodecState((MediaTimeProvider)this, mVideoExtractor,
trackIndex, format, codec, true, false,
AudioManager.AUDIO_SESSION_ID_GENERATE);
mVideoCodecStates.put(Integer.valueOf(trackIndex), state);
} else {
state = new CodecState((MediaTimeProvider)this, mAudioExtractor,
trackIndex, format, codec, true, false,
AudioManager.AUDIO_SESSION_ID_GENERATE);
mAudioCodecStates.put(Integer.valueOf(trackIndex), state);
}
if (isAudio) {
mAudioTrackState = state;
}
}
protected int getMediaFormatInteger(MediaFormat format, String key) {
return format.containsKey(key) ? format.getInteger(key) : 0;
}
// Find first secure decoder for media type. If none found, return
// the name of the first regular codec with ".secure" suffix added.
// If all else fails, return null.
protected String getSecureDecoderNameForMime(String mime) {
String firstDecoderName = null;
int n = MediaCodecList.getCodecCount();
for (int i = 0; i < n; ++i) {
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
if (info.isEncoder()) {
continue;
}
String[] supportedTypes = info.getSupportedTypes();
for (int j = 0; j < supportedTypes.length; ++j) {
if (supportedTypes[j].equalsIgnoreCase(mime)) {
if (info.getCapabilitiesForType(mime).isFeatureSupported(
MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback)) {
return info.getName();
} else if (firstDecoderName == null) {
firstDecoderName = info.getName();
}
}
}
}
if (firstDecoderName != null) {
return firstDecoderName + ".secure";
}
return null;
}
public void start() {
Log.d(TAG, "start");
if (mState == STATE_PLAYING || mState == STATE_PREPARING) {
return;
} else if (mState == STATE_IDLE) {
mState = STATE_PREPARING;
return;
} else if (mState != STATE_PAUSED) {
throw new IllegalStateException();
}
for (CodecState state : mVideoCodecStates.values()) {
state.startCodec();
state.play();
}
for (CodecState state : mAudioCodecStates.values()) {
state.startCodec();
state.play();
}
mDeltaTimeUs = -1;
mState = STATE_PLAYING;
}
public void startWork() throws IOException, MediaCryptoException, Exception {
try {
// Just change state from STATE_IDLE to STATE_PREPARING.
start();
// Extract media information from uri asset, and change state to STATE_PAUSED.
prepare();
// Start CodecState, and change from STATE_PAUSED to STATE_PLAYING.
start();
} catch (IOException e) {
throw e;
} catch (MediaCryptoException e) {
throw e;
}
mThreadStarted = true;
mThread.start();
}
public void startThread() {
start();
mThreadStarted = true;
mThread.start();
}
public void pause() {
Log.d(TAG, "pause");
if (mState == STATE_PAUSED) {
return;
} else if (mState != STATE_PLAYING) {
throw new IllegalStateException();
}
for (CodecState state : mVideoCodecStates.values()) {
state.pause();
}
for (CodecState state : mAudioCodecStates.values()) {
state.pause();
}
mState = STATE_PAUSED;
}
public void reset() {
if (mState == STATE_PLAYING) {
mThreadStarted = false;
try {
mThread.join();
} catch (InterruptedException ex) {
Log.d(TAG, "mThread.join " + ex);
}
pause();
}
if (mVideoCodecStates != null) {
for (CodecState state : mVideoCodecStates.values()) {
state.release();
}
mVideoCodecStates = null;
}
if (mAudioCodecStates != null) {
for (CodecState state : mAudioCodecStates.values()) {
state.release();
}
mAudioCodecStates = null;
}
if (mAudioExtractor != null) {
mAudioExtractor.release();
mAudioExtractor = null;
}
if (mVideoExtractor != null) {
mVideoExtractor.release();
mVideoExtractor = null;
}
if (mCrypto != null) {
mCrypto.release();
mCrypto = null;
}
if (mMediaCas != null) {
mMediaCas.close();
mMediaCas = null;
}
if (mAudioDescrambler != null) {
mAudioDescrambler.close();
mAudioDescrambler = null;
}
if (mVideoDescrambler != null) {
mVideoDescrambler.close();
mVideoDescrambler = null;
}
mDurationUs = -1;
mState = STATE_IDLE;
}
public boolean isEnded() {
if (mErrorFromThread != null) {
throw mErrorFromThread;
}
for (CodecState state : mVideoCodecStates.values()) {
if (!state.isEnded()) {
return false;
}
}
for (CodecState state : mAudioCodecStates.values()) {
if (!state.isEnded()) {
return false;
}
}
return true;
}
private void doSomeWork() {
try {
for (CodecState state : mVideoCodecStates.values()) {
state.doSomeWork();
}
} catch (MediaCodec.CryptoException e) {
mErrorFromThread = new Error("Video CryptoException w/ errorCode "
+ e.getErrorCode() + ", '" + e.getMessage() + "'");
return;
} catch (IllegalStateException e) {
mErrorFromThread =
new Error("Video CodecState.feedInputBuffer IllegalStateException " + e);
return;
}
try {
for (CodecState state : mAudioCodecStates.values()) {
state.doSomeWork();
}
} catch (MediaCodec.CryptoException e) {
mErrorFromThread = new Error("Audio CryptoException w/ errorCode "
+ e.getErrorCode() + ", '" + e.getMessage() + "'");
return;
} catch (IllegalStateException e) {
mErrorFromThread =
new Error("Audio CodecState.feedInputBuffer IllegalStateException " + e);
return;
}
}
private void cycleSurfaces() {
if (mSurfaces.size() > 1) {
final Surface s = mSurfaces.removeFirst();
mSurfaces.addLast(s);
for (CodecState c : mVideoCodecStates.values()) {
c.setOutputSurface(mSurfaces.getFirst());
/*
* Calling InputSurface.clearSurface on an old `output` surface because after
* MediaCodec has rendered to the old output surface, we need `edit`
* (i.e. draw black on) the old output surface.
*/
InputSurface.clearSurface(s);
break;
}
}
}
public long getNowUs() {
if (mAudioTrackState == null) {
return System.currentTimeMillis() * 1000;
}
return mAudioTrackState.getAudioTimeUs();
}
public long getRealTimeUsForMediaTime(long mediaTimeUs) {
if (mDeltaTimeUs == -1) {
long nowUs = getNowUs();
mDeltaTimeUs = nowUs - mediaTimeUs;
}
return mDeltaTimeUs + mediaTimeUs;
}
public int getDuration() {
return (int)((mDurationUs + 500) / 1000);
}
public int getCurrentPosition() {
if (mVideoCodecStates == null) {
return 0;
}
long positionUs = 0;
for (CodecState state : mVideoCodecStates.values()) {
long trackPositionUs = state.getCurrentPositionUs();
if (trackPositionUs > positionUs) {
positionUs = trackPositionUs;
}
}
return (int)((positionUs + 500) / 1000);
}
}