From 8d5224460eb8b22c7b81d7df5f6e76ee540005b0 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Mon, 23 Apr 2012 08:18:32 -0700 Subject: [PATCH] Added a MediaCoded based video view to the widevine sample player app. Change-Id: I514fee7312c485aa5f7a8c6c0a42ce62e71d2578 --- proprietary/samplePlayer/AndroidManifest.xml | 2 + .../samplePlayer/res/values/strings.xml | 2 + .../src/com/widevine/demo/MediaCodecView.java | 725 ++++++++++++++++++ .../widevine/demo/NonBlockingAudioTrack.java | 207 +++++ .../com/widevine/demo/VideoPlayerView.java | 166 ++-- 5 files changed, 1051 insertions(+), 51 deletions(-) create mode 100644 proprietary/samplePlayer/src/com/widevine/demo/MediaCodecView.java create mode 100644 proprietary/samplePlayer/src/com/widevine/demo/NonBlockingAudioTrack.java diff --git a/proprietary/samplePlayer/AndroidManifest.xml b/proprietary/samplePlayer/AndroidManifest.xml index 1750bf4e..63dd11bd 100644 --- a/proprietary/samplePlayer/AndroidManifest.xml +++ b/proprietary/samplePlayer/AndroidManifest.xml @@ -3,6 +3,8 @@ package="com.widevine.demo" > + + Device Not Provisioned Play Stop + MediaCodec Mode + Normal Mode Constraints Acquire Rights Remove Rights diff --git a/proprietary/samplePlayer/src/com/widevine/demo/MediaCodecView.java b/proprietary/samplePlayer/src/com/widevine/demo/MediaCodecView.java new file mode 100644 index 00000000..cc410647 --- /dev/null +++ b/proprietary/samplePlayer/src/com/widevine/demo/MediaCodecView.java @@ -0,0 +1,725 @@ +/* + * 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 mAvailableInputBufferIndices; + private LinkedList mAvailableOutputBufferIndices; + private LinkedList 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 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 mHeaders; + private boolean mEncrypted; + + private MediaCrypto mCrypto; + private MediaExtractor mExtractor; + + private Map 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 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; + } + } + + 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; + } +} diff --git a/proprietary/samplePlayer/src/com/widevine/demo/NonBlockingAudioTrack.java b/proprietary/samplePlayer/src/com/widevine/demo/NonBlockingAudioTrack.java new file mode 100644 index 00000000..457b1757 --- /dev/null +++ b/proprietary/samplePlayer/src/com/widevine/demo/NonBlockingAudioTrack.java @@ -0,0 +1,207 @@ +/* + * 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.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.LinkedList; + +class NonBlockingAudioTrack { + private static final String TAG = "NonBlockingAudioTrack"; + + private AudioTrack mAudioTrack; + private int mSampleRate; + private int mFrameSize; + private int mBufferSizeInFrames; + private int mNumFramesSubmitted = 0; + + class QueueElem { + byte[] data; + int offset; + int size; + } + private LinkedList mQueue = new LinkedList(); + + private Handler mHandler; + private boolean mWriteMorePending = false; + private static final int EVENT_WRITE_MORE = 1; + + public NonBlockingAudioTrack(int sampleRate, int channelCount) { + int channelConfig; + switch (channelCount) { + case 1: + channelConfig = AudioFormat.CHANNEL_OUT_MONO; + break; + case 2: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + break; + case 6: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + default: + throw new IllegalArgumentException(); + } + + int minBufferSize = + AudioTrack.getMinBufferSize( + sampleRate, + channelConfig, + AudioFormat.ENCODING_PCM_16BIT); + + int bufferSize = 2 * minBufferSize; + + mAudioTrack = new AudioTrack( + AudioManager.STREAM_MUSIC, + sampleRate, + channelConfig, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize, + AudioTrack.MODE_STREAM); + + mSampleRate = sampleRate; + mFrameSize = 2 * channelCount; + mBufferSizeInFrames = bufferSize / mFrameSize; + + mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_WRITE_MORE: + mWriteMorePending = false; + writeMore(); + break; + + default: + break; + } + } + }; + } + + public long getAudioTimeUs() { + int numFramesPlayed = mAudioTrack.getPlaybackHeadPosition(); + + return (numFramesPlayed * 1000000L) / mSampleRate; + } + + public void play() { + mAudioTrack.play(); + } + + public void stop() { + cancelWriteMore(); + + mAudioTrack.stop(); + + mNumFramesSubmitted = 0; + } + + public void pause() { + cancelWriteMore(); + + mAudioTrack.pause(); + } + + public void release() { + cancelWriteMore(); + + mAudioTrack.release(); + mAudioTrack = null; + } + + public int getPlayState() { + return mAudioTrack.getPlayState(); + } + + private void writeMore() { + if (mQueue.isEmpty()) { + return; + } + + int numFramesPlayed = mAudioTrack.getPlaybackHeadPosition(); + int numFramesPending = mNumFramesSubmitted - numFramesPlayed; + int numFramesAvailableToWrite = mBufferSizeInFrames - numFramesPending; + int numBytesAvailableToWrite = numFramesAvailableToWrite * mFrameSize; + + while (numBytesAvailableToWrite > 0) { + QueueElem elem = mQueue.peekFirst(); + + int numBytes = elem.size; + if (numBytes > numBytesAvailableToWrite) { + numBytes = numBytesAvailableToWrite; + } + + int written = mAudioTrack.write(elem.data, elem.offset, numBytes); + assert(written == numBytes); + + mNumFramesSubmitted += written / mFrameSize; + + elem.size -= numBytes; + if (elem.size == 0) { + mQueue.removeFirst(); + + if (mQueue.isEmpty()) { + break; + } + } else { + elem.offset += numBytes; + break; + } + + numBytesAvailableToWrite -= numBytes; + } + + if (!mQueue.isEmpty()) { + scheduleWriteMore(); + } + } + + private void scheduleWriteMore() { + if (mWriteMorePending) { + return; + } + + int numFramesPlayed = mAudioTrack.getPlaybackHeadPosition(); + int numFramesPending = mNumFramesSubmitted - numFramesPlayed; + int pendingDurationMs = 1000 * numFramesPending / mSampleRate; + + mWriteMorePending = true; + mHandler.sendMessageDelayed( + mHandler.obtainMessage(EVENT_WRITE_MORE), + pendingDurationMs / 3); + } + + private void cancelWriteMore() { + mHandler.removeMessages(EVENT_WRITE_MORE); + mWriteMorePending = false; + } + + public void write(byte[] data, int size) { + QueueElem elem = new QueueElem(); + elem.data = data; + elem.offset = 0; + elem.size = size; + + mQueue.add(elem); + + scheduleWriteMore(); + } +} diff --git a/proprietary/samplePlayer/src/com/widevine/demo/VideoPlayerView.java b/proprietary/samplePlayer/src/com/widevine/demo/VideoPlayerView.java index abc90727..60525ff9 100644 --- a/proprietary/samplePlayer/src/com/widevine/demo/VideoPlayerView.java +++ b/proprietary/samplePlayer/src/com/widevine/demo/VideoPlayerView.java @@ -5,6 +5,7 @@ package com.widevine.demo; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -19,12 +20,20 @@ import android.view.Gravity; import android.view.View; import android.content.Context; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.media.MediaCrypto; +import android.media.MediaExtractor; import android.media.MediaPlayer; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnCompletionListener; import android.util.Log; +import java.io.IOException; + public class VideoPlayerView extends Activity { + private final static String TAG = "VideoPlayerView"; + private final static float BUTTON_FONT_SIZE = 10; private final static String EXIT_FULLSCREEN = "Exit Full Screen"; private final static String FULLSCREEN = "Enter Full Screen"; @@ -33,11 +42,13 @@ public class VideoPlayerView extends Activity { private WidevineDrm drm; private FullScreenVideoView videoView; + private MediaCodecView mediaCodecView; private String assetUri; private TextView logs; private ScrollView scrollView; private Context context; private ClipImageView bgImage; + private Button mediaCodecModeButton; private Button playButton; private Button fullScreen; private Handler hRefresh; @@ -45,6 +56,7 @@ public class VideoPlayerView extends Activity { private LinearLayout main; private LinearLayout sidePanel; private boolean enteringFullScreen; + private boolean useMediaCodec; private int width, height; public void onCreate(Bundle savedInstanceState) { @@ -53,6 +65,7 @@ public class VideoPlayerView extends Activity { height = display.getHeight(); width = display.getWidth(); context = this; + useMediaCodec = false; contentView = createView(); if (drm.isProvisionedDevice()) { setContentView(contentView); @@ -68,6 +81,11 @@ public class VideoPlayerView extends Activity { stopPlayback(); } } + if (mediaCodecView != null) { + if (mediaCodecView.isPlaying()) { + stopPlayback(); + } + } super.onStop(); } @@ -91,8 +109,6 @@ public class VideoPlayerView extends Activity { } }; - videoView = new FullScreenVideoView(this); - logs = new TextView(this); drm.setLogListener(drmLogListener); drm.registerPortal(WidevineDrm.Settings.PORTAL_NAME); @@ -130,7 +146,17 @@ public class VideoPlayerView extends Activity { sidePanel.addView(createButtons(), paramsSidePanel); FrameLayout playerFrame = new FrameLayout(this); - playerFrame.addView(videoView, new FrameLayout.LayoutParams( + + View view; + if (useMediaCodec) { + mediaCodecView = new MediaCodecView(this); + view = mediaCodecView; + } else { + videoView = new FullScreenVideoView(this); + view = videoView; + } + + playerFrame.addView(view, new FrameLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT)); @@ -191,70 +217,107 @@ public class VideoPlayerView extends Activity { private void startPlayback() { playButton.setText(R.string.stop); - bgImage.setVisibility(View.GONE); - videoView.setVideoPath(assetUri); - videoView.setMediaController(new MediaController(context)); + if (useMediaCodec) { + mediaCodecView.setDataSource( + this, + Uri.parse(assetUri), + null /* headers */, + true /* encrypted */); - videoView.setOnErrorListener(new OnErrorListener() { - public boolean onError(MediaPlayer mp, int what, int extra) { - String message = "Unknown error"; - switch (what) { - case MediaPlayer.MEDIA_ERROR_UNKNOWN: - message = "Unable to play media"; - break; - case MediaPlayer.MEDIA_ERROR_SERVER_DIED: - message = "Server failed"; - break; - case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: - message = "Invalid media"; - break; - } - drm.logBuffer.append(message + "\n"); - updateLogs(); - bgImage.setVisibility(View.VISIBLE); - return false; - } - }); - videoView.setOnCompletionListener(new OnCompletionListener() { - public void onCompletion(MediaPlayer mp) { - stopPlayback(); - } - }); - videoView.requestFocus(); - - videoView.start(); - - if (videoView.getFullScreen()) { - sidePanel.setVisibility(View.GONE); + mediaCodecView.setMediaController(new MediaController(context)); + mediaCodecView.requestFocus(); + mediaCodecView.start(); } else { - sidePanel.setVisibility(View.VISIBLE); - } + videoView.setVideoPath(assetUri); + videoView.setMediaController(new MediaController(context)); - fullScreen.setVisibility(View.VISIBLE); - videoView.setFullScreenDimensions(contentView.getRight() - contentView.getLeft(), - contentView.getBottom() - contentView.getTop()); + videoView.setOnErrorListener(new OnErrorListener() { + public boolean onError(MediaPlayer mp, int what, int extra) { + String message = "Unknown error"; + switch (what) { + case MediaPlayer.MEDIA_ERROR_UNKNOWN: + message = "Unable to play media"; + break; + case MediaPlayer.MEDIA_ERROR_SERVER_DIED: + message = "Server failed"; + break; + case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: + message = "Invalid media"; + break; + } + drm.logBuffer.append(message + "\n"); + updateLogs(); + bgImage.setVisibility(View.VISIBLE); + return false; + } + }); + videoView.setOnCompletionListener(new OnCompletionListener() { + public void onCompletion(MediaPlayer mp) { + stopPlayback(); + } + }); + videoView.requestFocus(); + + videoView.start(); + + if (videoView.getFullScreen()) { + sidePanel.setVisibility(View.GONE); + } else { + sidePanel.setVisibility(View.VISIBLE); + } + + fullScreen.setVisibility(View.VISIBLE); + videoView.setFullScreenDimensions(contentView.getRight() - contentView.getLeft(), + contentView.getBottom() - contentView.getTop()); + } } private void stopPlayback() { playButton.setText(R.string.play); - videoView.stopPlayback(); - fullScreen.setVisibility(View.INVISIBLE); bgImage.setVisibility(View.VISIBLE); - if (videoView.getFullScreen() && !enteringFullScreen) { - videoView.setVisibility(View.INVISIBLE); - videoView.setFullScreen(false); - videoView.setVisibility(View.VISIBLE); - sidePanel.setVisibility(View.VISIBLE); - fullScreen.setText(FULLSCREEN); + if (useMediaCodec) { + mediaCodecView.reset(); + } else { + videoView.stopPlayback(); + + fullScreen.setVisibility(View.INVISIBLE); + if (videoView.getFullScreen() && !enteringFullScreen) { + videoView.setVisibility(View.INVISIBLE); + videoView.setFullScreen(false); + videoView.setVisibility(View.VISIBLE); + sidePanel.setVisibility(View.VISIBLE); + fullScreen.setText(FULLSCREEN); + } } enteringFullScreen = false; - } private View createButtons() { + mediaCodecModeButton = new Button(this); + if (useMediaCodec) { + mediaCodecModeButton.setText(R.string.normal_mode); + } else { + mediaCodecModeButton.setText(R.string.mediacodec_mode); + } + mediaCodecModeButton.setTextSize(BUTTON_FONT_SIZE); + + mediaCodecModeButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + onStop(); + + useMediaCodec = (useMediaCodec) ? false : true; + contentView = createView(); + if (drm.isProvisionedDevice()) { + setContentView(contentView); + } else { + setContentView(R.layout.notprovisioned); + } + } + }); + playButton = new Button(this); playButton.setText(R.string.play); playButton.setTextSize(BUTTON_FONT_SIZE); @@ -328,6 +391,7 @@ public class VideoPlayerView extends Activity { buttonsLeft.addView(checkConstraints, params); LinearLayout buttonsRight = new LinearLayout(this); + buttonsRight.addView(mediaCodecModeButton, params); buttonsRight.setOrientation(LinearLayout.VERTICAL); buttonsRight.addView(checkButton, params); buttonsRight.addView(removeButton, params);