Files
android/proprietary/samplePlayer/src/com/widevine/demo/MediaCodecView.java
Edwin Wong e9f5431e78 Revert Widevine 6.0.0 -> 4.5.0 libraries
Includes Widevine libraries Version 4.5.0.7809

Also fixed samplePlayer's MediaCodec mode not running and
WVDrmInfoRequestStatusKey returning incorrect value.

Change-Id: Ibcc6d313790670a908ada93be80d6bf55a67b4ed
related-to-bug: 6929628
related-to-bug: 6833718
related-to-bug: 6889322
2012-08-03 13:33:58 -07:00

739 lines
21 KiB
Java

/*
* Copyright (C) 2012 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 com.widevine.demo;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.MediaController;
import java.io.IOException;
import java.lang.IllegalStateException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.UUID;
class CodecState {
private static final String TAG = "CodecState";
private MediaCodecView mView;
private MediaExtractor mExtractor;
private int mTrackIndex;
private MediaFormat mFormat;
private boolean mSawInputEOS, mSawOutputEOS;
private MediaCodec mCodec;
private MediaFormat mOutputFormat;
private ByteBuffer[] mCodecInputBuffers;
private ByteBuffer[] mCodecOutputBuffers;
private LinkedList<Integer> mAvailableInputBufferIndices;
private LinkedList<Integer> mAvailableOutputBufferIndices;
private LinkedList<MediaCodec.BufferInfo> mAvailableOutputBufferInfos;
private NonBlockingAudioTrack mAudioTrack;
private long mLastMediaTimeUs;
public CodecState(
MediaCodecView view,
MediaExtractor extractor,
int trackIndex,
MediaFormat format,
MediaCodec codec) {
mView = view;
mExtractor = extractor;
mTrackIndex = trackIndex;
mFormat = format;
mSawInputEOS = mSawOutputEOS = false;
mCodec = codec;
mCodec.start();
mCodecInputBuffers = mCodec.getInputBuffers();
mCodecOutputBuffers = mCodec.getOutputBuffers();
mAvailableInputBufferIndices = new LinkedList();
mAvailableOutputBufferIndices = new LinkedList();
mAvailableOutputBufferInfos = new LinkedList();
mLastMediaTimeUs = 0;
}
public void release() {
mCodec.stop();
mCodecInputBuffers = null;
mCodecOutputBuffers = null;
mOutputFormat = null;
mAvailableOutputBufferInfos = null;
mAvailableOutputBufferIndices = null;
mAvailableInputBufferIndices = null;
mCodec.release();
mCodec = null;
if (mAudioTrack != null) {
mAudioTrack.release();
mAudioTrack = null;
}
}
public void start() {
if (mAudioTrack != null) {
mAudioTrack.play();
}
}
public void pause() {
if (mAudioTrack != null) {
mAudioTrack.pause();
}
}
public long getCurrentPositionUs() {
return mLastMediaTimeUs;
}
public void flush() {
mAvailableInputBufferIndices.clear();
mAvailableOutputBufferIndices.clear();
mAvailableOutputBufferInfos.clear();
mSawInputEOS = false;
mSawOutputEOS = false;
if (mAudioTrack != null
&& mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_STOPPED) {
mAudioTrack.play();
}
mCodec.flush();
}
public void doSomeWork() {
int index = mCodec.dequeueInputBuffer(0 /* timeoutUs */);
if (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
mAvailableInputBufferIndices.add(new Integer(index));
}
while (feedInputBuffer()) {}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
index = mCodec.dequeueOutputBuffer(info, 0 /* timeoutUs */);
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mOutputFormat = mCodec.getOutputFormat();
onOutputFormatChanged();
} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
mCodecOutputBuffers = mCodec.getOutputBuffers();
} else if (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
mAvailableOutputBufferIndices.add(new Integer(index));
mAvailableOutputBufferInfos.add(info);
}
while (drainOutputBuffer()) {}
}
/** returns true if more input data could be fed */
private boolean feedInputBuffer() {
if (mSawInputEOS || mAvailableInputBufferIndices.isEmpty()) {
return false;
}
int index = mAvailableInputBufferIndices.peekFirst().intValue();
ByteBuffer codecData = mCodecInputBuffers[index];
int trackIndex = mExtractor.getSampleTrackIndex();
if (trackIndex == mTrackIndex) {
int sampleSize =
mExtractor.readSampleData(codecData, 0 /* offset */);
long sampleTime = mExtractor.getSampleTime();
int sampleFlags = mExtractor.getSampleFlags();
try {
if ((sampleFlags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
MediaCodec.CryptoInfo info = new MediaCodec.CryptoInfo();
mExtractor.getSampleCryptoInfo(info);
mCodec.queueSecureInputBuffer(
index, 0 /* offset */, info, sampleTime, 0 /* flags */);
} else {
mCodec.queueInputBuffer(
index, 0 /* offset */, sampleSize, sampleTime,
0 /* flags */);
}
mAvailableInputBufferIndices.removeFirst();
mExtractor.advance();
} catch (MediaCodec.CryptoException e) {
Log.d(TAG, "CryptoException w/ errorCode "
+ e.getErrorCode() + ", '" + e.getMessage() + "'");
return false;
}
return true;
} else if (trackIndex < 0) {
Log.d(TAG, "saw input EOS on track " + mTrackIndex);
mSawInputEOS = true;
try {
mCodec.queueInputBuffer(
index, 0 /* offset */, 0 /* sampleSize */,
0 /* sampleTime */, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
mAvailableInputBufferIndices.removeFirst();
} catch (MediaCodec.CryptoException e) {
Log.d(TAG, "CryptoException w/ errorCode "
+ e.getErrorCode() + ", '" + e.getMessage() + "'");
}
}
return false;
}
private void onOutputFormatChanged() {
String mime = mOutputFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
int sampleRate =
mOutputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelCount =
mOutputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mAudioTrack = new NonBlockingAudioTrack(sampleRate, channelCount);
mAudioTrack.play();
}
}
/** returns true if more output data could be drained */
private boolean drainOutputBuffer() {
if (mSawOutputEOS || mAvailableOutputBufferIndices.isEmpty()) {
return false;
}
int index = mAvailableOutputBufferIndices.peekFirst().intValue();
MediaCodec.BufferInfo info = mAvailableOutputBufferInfos.peekFirst();
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "saw output EOS on track " + mTrackIndex);
mSawOutputEOS = true;
if (mAudioTrack != null) {
mAudioTrack.stop();
}
return false;
}
long realTimeUs =
mView.getRealTimeUsForMediaTime(info.presentationTimeUs);
long nowUs = mView.getNowUs();
long lateUs = nowUs - realTimeUs;
if (mAudioTrack != null) {
ByteBuffer buffer = mCodecOutputBuffers[index];
buffer.clear();
buffer.position(0 /* offset */);
byte[] audioCopy = new byte[info.size];
buffer.get(audioCopy, 0, info.size);
mAudioTrack.write(audioCopy, info.size);
mCodec.releaseOutputBuffer(index, false /* render */);
mLastMediaTimeUs = info.presentationTimeUs;
mAvailableOutputBufferIndices.removeFirst();
mAvailableOutputBufferInfos.removeFirst();
return true;
} else {
// video
boolean render;
if (lateUs < -10000) {
// too early;
return false;
} else if (lateUs > 30000) {
Log.d(TAG, "video late by " + lateUs + " us.");
render = false;
} else {
render = true;
mLastMediaTimeUs = info.presentationTimeUs;
}
mCodec.releaseOutputBuffer(index, render);
mAvailableOutputBufferIndices.removeFirst();
mAvailableOutputBufferInfos.removeFirst();
return true;
}
}
public long getAudioTimeUs() {
if (mAudioTrack == null) {
return 0;
}
return mAudioTrack.getAudioTimeUs();
}
}
class MediaCodecView extends SurfaceView
implements MediaController.MediaPlayerControl {
private static final String TAG = "MediaCodecView";
private Context mContext;
private Uri mUri;
private Map<String, String> mHeaders;
private boolean mEncrypted;
private MediaCrypto mCrypto;
private MediaExtractor mExtractor;
private Map<Integer, CodecState> mCodecStates;
CodecState mAudioTrackState;
private int mState;
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 Handler mHandler;
private static final int EVENT_PREPARE = 1;
private static final int EVENT_DO_SOME_WORK = 2;
private long mDeltaTimeUs;
private long mDurationUs;
private MediaController mMediaController;
public MediaCodecView(Context context) {
super(context);
initMediaCodecView();
}
public MediaCodecView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
initMediaCodecView();
}
public MediaCodecView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initMediaCodecView();
}
private void initMediaCodecView() {
mState = STATE_IDLE;
mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_PREPARE:
{
try {
prepare();
start();
} catch (IOException e) {
Log.d(TAG, "prepare failed.");
} catch (MediaCryptoException e) {
Log.d(TAG, "failed to initialize crypto.");
}
break;
}
case EVENT_DO_SOME_WORK:
{
doSomeWork();
mHandler.sendMessageDelayed(
mHandler.obtainMessage(EVENT_DO_SOME_WORK), 5);
break;
}
default:
break;
}
}
};
}
public void setDataSource(
Context context, Uri uri, Map<String, String> headers,
boolean encrypted) {
reset();
mContext = context;
mUri = uri;
mHeaders = headers;
mEncrypted = encrypted;
}
private void prepare() throws IOException, MediaCryptoException {
if (mEncrypted) {
UUID uuid = new UUID(
(long)0xedef8ba979d64aceL, (long)0xa3c827dcd51d21edL);
try {
mCrypto = new MediaCrypto(uuid, null);
} catch (MediaCryptoException e) {
reset();
throw e;
}
}
try {
mExtractor = new MediaExtractor();
mExtractor.setDataSource(mContext, mUri, mHeaders);
} catch (IOException e) {
reset();
throw e;
}
mCodecStates = new HashMap();
boolean haveAudio = false;
boolean haveVideo = false;
for (int i = mExtractor.getTrackCount(); i-- > 0;) {
MediaFormat format = mExtractor.getTrackFormat(i);
Log.d(TAG, "track format #" + i + " is " + format);
String mime = format.getString(MediaFormat.KEY_MIME);
boolean isVideo = mime.startsWith("video/");
boolean isAudio = mime.startsWith("audio/");
if (!haveAudio && isAudio || !haveVideo && isVideo) {
mExtractor.selectTrack(i);
addTrack(i, format, mEncrypted);
if (isAudio) {
haveAudio = true;
} else {
haveVideo = true;
}
if (format.containsKey(MediaFormat.KEY_DURATION)) {
long durationUs = format.getLong(MediaFormat.KEY_DURATION);
if (durationUs > mDurationUs) {
mDurationUs = durationUs;
}
}
if (haveAudio && haveVideo) {
break;
}
}
}
mState = STATE_PAUSED;
}
private String getSecureDecoderNameForMime(String mime) {
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)) {
return info.getName() + ".secure";
}
}
}
return null;
}
private void addTrack(
int trackIndex, MediaFormat format, boolean encrypted) {
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);
}
codec.configure(
format,
isVideo ? getHolder().getSurface() : null,
mCrypto,
0);
CodecState state =
new CodecState(this, mExtractor, trackIndex, format, codec);
mCodecStates.put(new Integer(trackIndex), state);
if (isAudio) {
mAudioTrackState = state;
}
// set video view size
invalidate();
}
public void start() {
Log.d(TAG, "start");
if (mState == STATE_PLAYING || mState == STATE_PREPARING) {
return;
} else if (mState == STATE_IDLE) {
mState = STATE_PREPARING;
mHandler.sendMessage(mHandler.obtainMessage(EVENT_PREPARE));
return;
} else if (mState != STATE_PAUSED) {
throw new IllegalStateException();
}
for (CodecState state : mCodecStates.values()) {
state.start();
}
mHandler.sendMessage(mHandler.obtainMessage(EVENT_DO_SOME_WORK));
mDeltaTimeUs = -1;
mState = STATE_PLAYING;
if (mMediaController != null) {
mMediaController.show();
}
}
public void pause() {
Log.d(TAG, "pause");
if (mState == STATE_PAUSED) {
return;
} else if (mState != STATE_PLAYING) {
throw new IllegalStateException();
}
mHandler.removeMessages(EVENT_DO_SOME_WORK);
for (CodecState state : mCodecStates.values()) {
state.pause();
}
mState = STATE_PAUSED;
}
public void reset() {
if (mState == STATE_PLAYING) {
pause();
}
if (mCodecStates != null) {
for (CodecState state : mCodecStates.values()) {
state.release();
}
mCodecStates = null;
}
if (mExtractor != null) {
mExtractor.release();
mExtractor = null;
}
if (mCrypto != null) {
mCrypto.release();
mCrypto = null;
}
mDurationUs = -1;
mState = STATE_IDLE;
}
public void setMediaController(MediaController ctrl) {
mMediaController = ctrl;
attachMediaController();
}
private void attachMediaController() {
View anchorView =
this.getParent() instanceof View ? (View)this.getParent() : this;
mMediaController.setMediaPlayer(this);
mMediaController.setAnchorView(anchorView);
mMediaController.setEnabled(true);
}
private void doSomeWork() {
for (CodecState state : mCodecStates.values()) {
state.doSomeWork();
}
}
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() {
long positionUs = 0;
for (CodecState state : mCodecStates.values()) {
long trackPositionUs = state.getCurrentPositionUs();
if (trackPositionUs > positionUs) {
positionUs = trackPositionUs;
}
}
return (int)((positionUs + 500) / 1000);
}
public void seekTo(int timeMs) {
if (mState != STATE_PLAYING && mState != STATE_PAUSED) {
return;
}
mExtractor.seekTo(timeMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
for (CodecState state : mCodecStates.values()) {
state.flush();
}
Log.d(TAG, "seek to " + timeMs * 1000);
mDeltaTimeUs = -1;
}
public boolean isPlaying() {
return mState == STATE_PLAYING;
}
public int getBufferPercentage() {
long cachedDurationUs = mExtractor.getCachedDuration();
if (cachedDurationUs < 0 || mDurationUs < 0) {
return 0;
}
int nowMs = getCurrentPosition();
int percentage =
100 * (nowMs + (int)(cachedDurationUs / 1000))
/ (int)(mDurationUs / 1000);
if (percentage > 100) {
percentage = 100;
}
return percentage;
}
public boolean canPause() {
return true;
}
public boolean canSeekBackward() {
return true;
}
public boolean canSeekForward() {
return true;
}
private void toggleMediaControlsVisiblity() {
if (mMediaController.isShowing()) {
mMediaController.hide();
} else {
mMediaController.show();
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mState != STATE_IDLE && mMediaController != null) {
toggleMediaControlsVisiblity();
}
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 720;
int height = 480;
Log.d(TAG, "setting size: " + width + 'x' + height);
setMeasuredDimension(width, height);
}
}