diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md new file mode 100644 index 0000000000..28252fdfda --- /dev/null +++ b/extensions/vp9/README.md @@ -0,0 +1,81 @@ +# ExoPlayer VP9 Extension # + +## Description ## + +The VP9 Extension is a [TrackRenderer][] implementation that helps you bundle +libvpx (the VP9 decoding library) into your app and use it along with ExoPlayer +to play VP9 video on Android devices. + +[TrackRenderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/TrackRenderer.html + +## Build Instructions ## + +* Checkout ExoPlayer along with Extensions: + +``` +git clone https://github.com/google/ExoPlayer.git +``` + +* Set the following environment variables: + +``` +cd "" +EXOPLAYER_ROOT="$(pwd)" +VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" +``` + +* Download the [Android NDK][] and set its location in an environment variable: + +``` +NDK_PATH="" +``` + +* Fetch libvpx and libyuv: + +``` +cd "${VP9_EXT_PATH}/jni" && \ +git clone https://chromium.googlesource.com/webm/libvpx libvpx && \ +git clone https://chromium.googlesource.com/libyuv/libyuv libyuv +``` + +* Run a script that generates necessary configuration files for libvpx: + +``` +cd ${VP9_EXT_PATH}/jni && \ +./generate_libvpx_android_configs.sh "${NDK_PATH}" +``` + +* Build the JNI native libraries from the command line: + +``` +cd "${VP9_EXT_PATH}"/jni && \ +${NDK_PATH}/ndk-build APP_ABI=all -j4 +``` + +* In your project, you can add a dependency to the VP9 Extension by using a the + following rule: + +``` +// in settings.gradle +include ':..:ExoPlayer:library' +include ':..:ExoPlayer:extension-vp9' + +// in build.gradle +dependencies { + compile project(':..:ExoPlayer:library') + compile project(':..:ExoPlayer:extension-vp9') +} +``` + +* Now, when you build your app, the VP9 extension will be built and the native + libraries will be packaged along with the APK. + +## Notes ## + +* Every time there is a change to the libvpx checkout: + * Android config scripts should be re-generated by running + `generate_libvpx_android_configs.sh` + * Clean and re-build the project. +* If you want to use your own version of libvpx or libyuv, place it in + `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. + diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle new file mode 100644 index 0000000000..9ccc1862aa --- /dev/null +++ b/extensions/vp9/build.gradle @@ -0,0 +1,45 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 23 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + lintOptions { + abortOnError false + } + + sourceSets.main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } +} + +dependencies { + compile project(':library') +} + diff --git a/extensions/vp9/src/androidTest/.classpath b/extensions/vp9/src/androidTest/.classpath new file mode 100644 index 0000000000..5b30f3b49b --- /dev/null +++ b/extensions/vp9/src/androidTest/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/vp9/src/androidTest/.project b/extensions/vp9/src/androidTest/.project new file mode 100644 index 0000000000..8e0bfa4eb9 --- /dev/null +++ b/extensions/vp9/src/androidTest/.project @@ -0,0 +1,45 @@ + + + ExoPlayerExt-VP9Tests + + + ExoPlayerLib + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + + + 0 + + 14 + + org.eclipse.ui.ide.multiFilter + 1.0-name-matches-true-false-BUILD + + + + diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..a2859b3c3c --- /dev/null +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/extensions/vp9/src/androidTest/assets/bear-vp9-odd-dimensions.webm b/extensions/vp9/src/androidTest/assets/bear-vp9-odd-dimensions.webm new file mode 100644 index 0000000000..4d65a906fa Binary files /dev/null and b/extensions/vp9/src/androidTest/assets/bear-vp9-odd-dimensions.webm differ diff --git a/extensions/vp9/src/androidTest/assets/bear-vp9.webm b/extensions/vp9/src/androidTest/assets/bear-vp9.webm new file mode 100644 index 0000000000..4f497aeb34 Binary files /dev/null and b/extensions/vp9/src/androidTest/assets/bear-vp9.webm differ diff --git a/extensions/vp9/src/androidTest/assets/invalid-bitstream.webm b/extensions/vp9/src/androidTest/assets/invalid-bitstream.webm new file mode 100644 index 0000000000..7763cb452f Binary files /dev/null and b/extensions/vp9/src/androidTest/assets/invalid-bitstream.webm differ diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer/ext/vp9/VpxPlaybackTest.java new file mode 100644 index 0000000000..92aee04f2a --- /dev/null +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer/ext/vp9/VpxPlaybackTest.java @@ -0,0 +1,135 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +import com.google.android.exoplayer.DefaultTrackSelector; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultDataSource; +import com.google.android.exoplayer.util.Util; + +import android.content.Context; +import android.net.Uri; +import android.os.Looper; +import android.test.InstrumentationTestCase; + +/** + * Playback tests using {@link LibvpxVideoTrackRenderer}. + */ +public class VpxPlaybackTest extends InstrumentationTestCase { + + private static final String BEAR_URI = "asset:///bear-vp9.webm"; + private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm"; + private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm"; + + public void testBasicPlayback() throws ExoPlaybackException { + playUri(BEAR_URI); + } + + public void testOddDimensionsPlayback() throws ExoPlaybackException { + playUri(BEAR_ODD_DIMENSIONS_URI); + } + + public void testInvalidBitstream() { + try { + playUri(INVALID_BITSTREAM_URI); + fail(); + } catch (Exception e) { + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof VpxDecoderException); + } + } + + private void playUri(String uri) throws ExoPlaybackException { + TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri), + getInstrumentation().getContext()); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + fail(); // Should never happen. + } + if (thread.playbackException != null) { + throw thread.playbackException; + } + } + + private static class TestPlaybackThread extends Thread implements ExoPlayer.Listener { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int BUFFER_SEGMENT_COUNT = 16; + + private final Context context; + private final Uri uri; + + private ExoPlayer player; + private ExoPlaybackException playbackException; + + public TestPlaybackThread(Uri uri, Context context) { + this.uri = uri; + this.context = context; + } + + @Override + public void run() { + Looper.prepare(); + LibvpxVideoTrackRenderer videoRenderer = new LibvpxVideoTrackRenderer(true); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(null, null); + player = ExoPlayer.Factory.newInstance(new TrackRenderer[] {videoRenderer}, trackSelector); + player.addListener(this); + ExtractorSampleSource sampleSource = new ExtractorSampleSource( + uri, + new DefaultDataSource(context, null, Util.getUserAgent(context, "ExoPlayerExtVP9Test"), + false), + new DefaultAllocator(BUFFER_SEGMENT_SIZE), BUFFER_SEGMENT_SIZE * BUFFER_SEGMENT_COUNT, + new MatroskaExtractor()); + player.sendMessage(videoRenderer, LibvpxVideoTrackRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, + new VpxVideoSurfaceView(context)); + player.prepare(sampleSource); + player.setPlayWhenReady(true); + Looper.loop(); + } + + @Override + public void onPlayWhenReadyCommitted () { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + playbackException = error; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == ExoPlayer.STATE_ENDED + || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + releasePlayerAndQuitLooper(); + } + } + + private void releasePlayerAndQuitLooper() { + player.release(); + Looper.myLooper().quit(); + } + + } + +} diff --git a/extensions/vp9/src/androidTest/project.properties b/extensions/vp9/src/androidTest/project.properties new file mode 100644 index 0000000000..916037e334 --- /dev/null +++ b/extensions/vp9/src/androidTest/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-23 diff --git a/extensions/vp9/src/androidTest/res/.README.txt b/extensions/vp9/src/androidTest/res/.README.txt new file mode 100644 index 0000000000..c27147ce56 --- /dev/null +++ b/extensions/vp9/src/androidTest/res/.README.txt @@ -0,0 +1,2 @@ +This file is needed to make sure the res directory is present. +The file is ignored by the Android toolchain because its name starts with a dot. diff --git a/extensions/vp9/src/main/.classpath b/extensions/vp9/src/main/.classpath new file mode 100644 index 0000000000..fd895b0917 --- /dev/null +++ b/extensions/vp9/src/main/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/vp9/src/main/.cproject b/extensions/vp9/src/main/.cproject new file mode 100644 index 0000000000..8548e48eb9 --- /dev/null +++ b/extensions/vp9/src/main/.cproject @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/vp9/src/main/.project b/extensions/vp9/src/main/.project new file mode 100644 index 0000000000..3811626cb3 --- /dev/null +++ b/extensions/vp9/src/main/.project @@ -0,0 +1,97 @@ + + + ExoPlayerExt-VP9 + + + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + ?children? + ?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\|| + + + ?name? + + + + org.eclipse.cdt.make.core.append_environment + true + + + org.eclipse.cdt.make.core.buildArguments + + + + org.eclipse.cdt.make.core.buildCommand + ndk-build + + + org.eclipse.cdt.make.core.cleanBuildTarget + clean + + + org.eclipse.cdt.make.core.contents + org.eclipse.cdt.make.core.activeConfigSettings + + + org.eclipse.cdt.make.core.enableAutoBuild + false + + + org.eclipse.cdt.make.core.enableCleanBuild + true + + + org.eclipse.cdt.make.core.enableFullBuild + true + + + org.eclipse.cdt.make.core.stopOnError + true + + + org.eclipse.cdt.make.core.useDefaultBuildCmd + true + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs b/extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..d17b6724d1 --- /dev/null +++ b/extensions/vp9/src/main/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/extensions/vp9/src/main/AndroidManifest.xml b/extensions/vp9/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2869352679 --- /dev/null +++ b/extensions/vp9/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java new file mode 100644 index 0000000000..c256d44541 --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/LibvpxVideoTrackRenderer.java @@ -0,0 +1,520 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.TrackStream; +import com.google.android.exoplayer.util.MimeTypes; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Handler; +import android.os.SystemClock; +import android.view.Surface; + +/** + * Decodes and renders video using the native VP9 decoder. + */ +public final class LibvpxVideoTrackRenderer extends TrackRenderer { + + /** + * Interface definition for a callback to be notified of {@link LibvpxVideoTrackRenderer} events. + */ + public interface EventListener { + + /** + * Invoked to report the number of frames dropped by the renderer. Dropped frames are reported + * whenever the renderer is stopped having dropped frames, and optionally, whenever the count + * reaches a specified threshold whilst the renderer is started. + * + * @param count The number of dropped frames. + * @param elapsed The duration in milliseconds over which the frames were dropped. This + * duration is timed from when the renderer was started or from when dropped frames were + * last reported (whichever was more recent), and not from when the first of the reported + * drops occurred. + */ + void onDroppedFrames(int count, long elapsed); + + /** + * Invoked each time there's a change in the size of the video being rendered. + * + * @param width The video width in pixels. + * @param height The video height in pixels. + */ + void onVideoSizeChanged(int width, int height); + + /** + * Invoked when a frame is rendered to a surface for the first time following that surface + * having been set as the target for the renderer. + * + * @param surface The surface to which a first frame has been rendered. + */ + void onDrawnToSurface(Surface surface); + + /** + * Invoked when one of the following happens: libvpx initialization failure, decoder error, + * renderer error. + * + * @param e The corresponding exception. + */ + void onDecoderError(VpxDecoderException e); + + /** + * Invoked when a decoder is successfully created. + * + * @param decoderName The decoder that was configured and created. + * @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the initialization + * finished. + * @param initializationDurationMs Amount of time taken to initialize the decoder. + */ + void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, + long initializationDurationMs); + + } + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be the target {@link Surface}, or null. + */ + public static final int MSG_SET_SURFACE = 1; + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be the target {@link VpxOutputBufferRenderer}, or null. + */ + public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = 2; + + /** + * The number of input buffers and the number of output buffers. The track renderer may limit the + * minimum possible value due to requiring multiple output buffers to be dequeued at a time for it + * to make progress. + */ + private static final int NUM_BUFFERS = 16; + private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. + + public final CodecCounters codecCounters = new CodecCounters(); + + private final boolean scaleToFit; + private final Handler eventHandler; + private final EventListener eventListener; + private final int maxDroppedFrameCountToNotify; + private final FormatHolder formatHolder; + + private Format format; + private VpxDecoder decoder; + private VpxInputBuffer inputBuffer; + private VpxOutputBuffer outputBuffer; + private VpxOutputBuffer nextOutputBuffer; + + private Bitmap bitmap; + private boolean drawnToSurface; + private boolean renderedFirstFrame; + private Surface surface; + private VpxOutputBufferRenderer outputBufferRenderer; + private int outputMode; + + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private int previousWidth; + private int previousHeight; + + private int droppedFrameCount; + private long droppedFrameAccumulationStartTimeMs; + + /** + * @param scaleToFit Boolean that indicates if video frames should be scaled to fit when + * rendering. + */ + public LibvpxVideoTrackRenderer(boolean scaleToFit) { + this(scaleToFit, null, null, 0); + } + + /** + * @param scaleToFit Boolean that indicates if video frames should be scaled to fit when + * rendering. + * @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 maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between + * invocations of {@link EventListener#onDroppedFrames(int, long)}. + */ + public LibvpxVideoTrackRenderer(boolean scaleToFit, Handler eventHandler, + EventListener eventListener, int maxDroppedFrameCountToNotify) { + this.scaleToFit = scaleToFit; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify; + previousWidth = -1; + previousHeight = -1; + formatHolder = new FormatHolder(); + outputMode = VpxDecoder.OUTPUT_MODE_UNKNOWN; + } + + /** + * Returns whether the underlying libvpx library is available. + */ + public static boolean isLibvpxAvailable() { + return VpxDecoder.IS_AVAILABLE; + } + + /** + * Returns the version of the underlying libvpx library if available, otherwise {@code null}. + */ + public static String getLibvpxVersion() { + return isLibvpxAvailable() ? VpxDecoder.getLibvpxVersion() : null; + } + + @Override + protected int supportsFormat(Format format) { + return MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType) + ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + protected void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + + // Try and read a format if we don't have one already. + if (format == null && !readFormat()) { + // We can't make progress without one. + return; + } + + try { + if (decoder == null) { + // If we don't have a decoder yet, we need to instantiate one. + long startElapsedRealtimeMs = SystemClock.elapsedRealtime(); + decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE); + decoder.setOutputMode(outputMode); + decoder.start(); + notifyDecoderInitialized(startElapsedRealtimeMs, SystemClock.elapsedRealtime()); + codecCounters.codecInitCount++; + } + while (processOutputBuffer(positionUs)) {} + while (feedInputBuffer()) {} + } catch (VpxDecoderException e) { + notifyDecoderError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + codecCounters.ensureUpdated(); + } + + private boolean processOutputBuffer(long positionUs) + throws VpxDecoderException { + if (outputStreamEnded) { + return false; + } + + // Acquire outputBuffer either from nextOutputBuffer or from the decoder. + if (outputBuffer == null) { + if (nextOutputBuffer != null) { + outputBuffer = nextOutputBuffer; + nextOutputBuffer = null; + } else { + outputBuffer = decoder.dequeueOutputBuffer(); + } + if (outputBuffer == null) { + return false; + } + } + + if (nextOutputBuffer == null) { + nextOutputBuffer = decoder.dequeueOutputBuffer(); + } + + if (outputBuffer.isEndOfStream()) { + outputStreamEnded = true; + outputBuffer.release(); + outputBuffer = null; + return false; + } + + // Drop frame only if we have the next frame and that's also late, otherwise render whatever we + // have. + if (nextOutputBuffer != null && nextOutputBuffer.timestampUs < positionUs) { + // Drop frame if we are too late. + codecCounters.droppedOutputBufferCount++; + droppedFrameCount++; + if (droppedFrameCount == maxDroppedFrameCountToNotify) { + notifyAndResetDroppedFrameCount(); + } + outputBuffer.release(); + outputBuffer = null; + return true; + } + + // If we have not rendered any frame so far (either initially or immediately following a seek), + // render one frame irrespective of the state or current position. + if (!renderedFirstFrame) { + renderBuffer(); + renderedFirstFrame = true; + return false; + } + + if (getState() == TrackRenderer.STATE_STARTED + && outputBuffer.timestampUs <= positionUs + 30000) { + renderBuffer(); + } + return false; + } + + private void renderBuffer() { + codecCounters.renderedOutputBufferCount++; + notifyIfVideoSizeChanged(outputBuffer); + if (outputBuffer.mode == VpxDecoder.OUTPUT_MODE_RGB && surface != null) { + renderRgbFrame(outputBuffer, scaleToFit); + if (!drawnToSurface) { + drawnToSurface = true; + notifyDrawnToSurface(surface); + } + outputBuffer.release(); + } else if (outputBuffer.mode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null) { + // The renderer will release the buffer. + outputBufferRenderer.setOutputBuffer(outputBuffer); + } else { + outputBuffer.release(); + } + outputBuffer = null; + } + + private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { + if (bitmap == null || bitmap.getWidth() != outputBuffer.width + || bitmap.getHeight() != outputBuffer.height) { + bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); + } + bitmap.copyPixelsFromBuffer(outputBuffer.data); + Canvas canvas = surface.lockCanvas(null); + if (scale) { + canvas.scale(((float) canvas.getWidth()) / outputBuffer.width, + ((float) canvas.getHeight()) / outputBuffer.height); + } + canvas.drawBitmap(bitmap, 0, 0, null); + surface.unlockCanvasAndPost(canvas); + } + + private boolean feedInputBuffer() throws VpxDecoderException { + if (inputStreamEnded) { + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + int result = readSource(formatHolder, inputBuffer); + if (result == TrackStream.NOTHING_READ) { + return false; + } + if (result == TrackStream.FORMAT_READ) { + format = formatHolder.format; + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + } else { + inputBuffer.width = format.width; + inputBuffer.height = format.height; + } + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return true; + } + + private void flushDecoder() { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + if (nextOutputBuffer != null) { + nextOutputBuffer.release(); + nextOutputBuffer = null; + } + decoder.flush(); + } + + @Override + protected boolean isEnded() { + return outputStreamEnded; + } + + @Override + protected boolean isReady() { + return format != null && (isSourceReady() || outputBuffer != null) && renderedFirstFrame; + } + + @Override + protected void reset(long positionUs) { + inputStreamEnded = false; + outputStreamEnded = false; + renderedFirstFrame = false; + if (decoder != null) { + flushDecoder(); + } + } + + @Override + protected void onStarted() { + droppedFrameCount = 0; + droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + } + + @Override + protected void onStopped() { + notifyAndResetDroppedFrameCount(); + } + + @Override + protected void onDisabled() { + inputBuffer = null; + outputBuffer = null; + format = null; + try { + if (decoder != null) { + decoder.release(); + decoder = null; + codecCounters.codecReleaseCount++; + } + } finally { + super.onDisabled(); + } + } + + private boolean readFormat() { + int result = readSource(formatHolder, null); + if (result == TrackStream.FORMAT_READ) { + format = formatHolder.format; + return true; + } + return false; + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_SURFACE) { + setSurface((Surface) message); + } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) { + setOutputBufferRenderer((VpxOutputBufferRenderer) message); + } else { + super.handleMessage(messageType, message); + } + } + + private void setSurface(Surface surface) { + if (this.surface == surface) { + return; + } + this.surface = surface; + outputBufferRenderer = null; + outputMode = (surface != null) ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_UNKNOWN; + if (decoder != null) { + decoder.setOutputMode(outputMode); + } + drawnToSurface = false; + } + + private void setOutputBufferRenderer(VpxOutputBufferRenderer outputBufferRenderer) { + if (this.outputBufferRenderer == outputBufferRenderer) { + return; + } + this.outputBufferRenderer = outputBufferRenderer; + surface = null; + outputMode = (outputBufferRenderer != null) + ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_UNKNOWN; + if (decoder != null) { + decoder.setOutputMode(outputMode); + } + } + + private void notifyIfVideoSizeChanged(final VpxOutputBuffer outputBuffer) { + if (previousWidth == -1 || previousHeight == -1 + || previousWidth != outputBuffer.width || previousHeight != outputBuffer.height) { + previousWidth = outputBuffer.width; + previousHeight = outputBuffer.height; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onVideoSizeChanged(outputBuffer.width, outputBuffer.height); + } + }); + } + } + } + + private void notifyAndResetDroppedFrameCount() { + if (eventHandler != null && eventListener != null && droppedFrameCount > 0) { + long now = SystemClock.elapsedRealtime(); + final int countToNotify = droppedFrameCount; + final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; + droppedFrameCount = 0; + droppedFrameAccumulationStartTimeMs = now; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDroppedFrames(countToNotify, elapsedToNotify); + } + }); + } + } + + private void notifyDrawnToSurface(final Surface surface) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrawnToSurface(surface); + } + }); + } + } + + private void notifyDecoderError(final VpxDecoderException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderError(e); + } + }); + } + } + + private void notifyDecoderInitialized( + final long startElapsedRealtimeMs, final long finishElapsedRealtimeMs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderInitialized("libvpx" + getLibvpxVersion(), + finishElapsedRealtimeMs, finishElapsedRealtimeMs - startElapsedRealtimeMs); + } + }); + } + } + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java new file mode 100644 index 0000000000..5ade128a25 --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoder.java @@ -0,0 +1,128 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.util.extensions.SimpleDecoder; + +import java.nio.ByteBuffer; + +/** + * JNI wrapper for the libvpx VP9 decoder. + */ +/* package */ final class VpxDecoder extends + SimpleDecoder { + + public static final int OUTPUT_MODE_UNKNOWN = -1; + public static final int OUTPUT_MODE_YUV = 0; + public static final int OUTPUT_MODE_RGB = 1; + + /** + * Whether the underlying libvpx library is available. + */ + public static final boolean IS_AVAILABLE; + static { + boolean isAvailable; + try { + System.loadLibrary("vpx"); + System.loadLibrary("vpxJNI"); + isAvailable = true; + } catch (UnsatisfiedLinkError exception) { + isAvailable = false; + } + IS_AVAILABLE = isAvailable; + } + + /** + * Returns the version string of the underlying libvpx decoder. + */ + public static native String getLibvpxVersion(); + + private final long vpxDecContext; + + private volatile int outputMode; + + /** + * Creates a VP9 decoder. + * + * @param numInputBuffers The number of input buffers. + * @param numOutputBuffers The number of output buffers. + * @param initialInputBufferSize The initial size of each input buffer. + * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. + */ + public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize) + throws VpxDecoderException { + super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); + vpxDecContext = vpxInit(); + if (vpxDecContext == 0) { + throw new VpxDecoderException("Failed to initialize decoder"); + } + setInitialInputBufferSize(initialInputBufferSize); + } + + /** + * Sets the output mode for frames rendered by the decoder. + * + * @param outputMode The output mode to use, which must be one of the {@code OUTPUT_MODE_*} + * constants in {@link VpxDecoder}. + */ + public void setOutputMode(int outputMode) { + this.outputMode = outputMode; + } + + @Override + protected VpxInputBuffer createInputBuffer() { + return new VpxInputBuffer(); + } + + @Override + protected VpxOutputBuffer createOutputBuffer() { + return new VpxOutputBuffer(this); + } + + @Override + protected void releaseOutputBuffer(VpxOutputBuffer buffer) { + super.releaseOutputBuffer(buffer); + } + + @Override + protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, + boolean reset) { + outputBuffer.timestampUs = inputBuffer.timeUs; + inputBuffer.data.position(inputBuffer.data.position() - inputBuffer.size); + if (vpxDecode(vpxDecContext, inputBuffer.data, inputBuffer.size) != 0) { + return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext)); + } + outputBuffer.mode = outputMode; + if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) { + outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + return null; + } + + @Override + public void release() { + super.release(); + vpxClose(vpxDecContext); + } + + private native long vpxInit(); + private native long vpxClose(long context); + private native long vpxDecode(long context, ByteBuffer encoded, int length); + private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); + private native String vpxGetErrorMessage(long context); + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java new file mode 100644 index 0000000000..6d08f1b0d1 --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxDecoderException.java @@ -0,0 +1,27 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +/** + * Thrown when a libvpx decoder error occurs. + */ +public class VpxDecoderException extends Exception { + + /* package */ VpxDecoderException(String message) { + super(message); + } + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxInputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxInputBuffer.java new file mode 100644 index 0000000000..44680169df --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxInputBuffer.java @@ -0,0 +1,31 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +import com.google.android.exoplayer.DecoderInputBuffer; + +/** + * Input buffer to a {@link VpxDecoder}. + */ +/* package */ final class VpxInputBuffer extends DecoderInputBuffer { + + public int width; + public int height; + + public VpxInputBuffer() { + super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + } +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java new file mode 100644 index 0000000000..709e504139 --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2015 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.google.android.exoplayer.ext.vp9; + +import com.google.android.exoplayer.util.extensions.OutputBuffer; + +import java.nio.ByteBuffer; + +/** + * Output buffer containing video frame data, populated by {@link VpxDecoder}. + */ +public final class VpxOutputBuffer extends OutputBuffer { + + public static final int COLORSPACE_UNKNOWN = 0; + public static final int COLORSPACE_BT601 = 1; + public static final int COLORSPACE_BT709 = 2; + + private final VpxDecoder owner; + + public int mode; + /** + * RGB buffer for RGB mode. + */ + public ByteBuffer data; + public int width; + public int height; + /** + * YUV planes for YUV mode. + */ + public ByteBuffer[] yuvPlanes; + public int[] yuvStrides; + public int colorspace; + + /* package */ VpxOutputBuffer(VpxDecoder owner) { + this.owner = owner; + } + + @Override + public void release() { + owner.releaseOutputBuffer(this); + } + + /** + * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes. + */ + /* package */ void initForRgbFrame(int width, int height) { + this.width = width; + this.height = height; + int minimumRgbSize = width * height * 2; + if (data == null || data.capacity() < minimumRgbSize) { + data = ByteBuffer.allocateDirect(minimumRgbSize); + yuvPlanes = null; + } + data.position(0); + data.limit(minimumRgbSize); + } + + /** + * Resizes the buffer based on the given stride. Called via JNI after decoding completes. + */ + /* package */ void initForYuvFrame(int width, int height, int yStride, int uvStride, + int colorspace) { + this.width = width; + this.height = height; + this.colorspace = colorspace; + int yLength = yStride * height; + int uvLength = uvStride * ((height + 1) / 2); + int minimumYuvSize = yLength + (uvLength * 2); + if (data == null || data.capacity() < minimumYuvSize) { + data = ByteBuffer.allocateDirect(minimumYuvSize); + } + data.limit(minimumYuvSize); + if (yuvPlanes == null) { + yuvPlanes = new ByteBuffer[3]; + } + // Rewrapping has to be done on every frame since the stride might have changed. + data.position(0); + yuvPlanes[0] = data.slice(); + yuvPlanes[0].limit(yLength); + data.position(yLength); + yuvPlanes[1] = data.slice(); + yuvPlanes[1].limit(uvLength); + data.position(yLength + uvLength); + yuvPlanes[2] = data.slice(); + yuvPlanes[2].limit(uvLength); + if (yuvStrides == null) { + yuvStrides = new int[3]; + } + yuvStrides[0] = yStride; + yuvStrides[1] = uvStride; + yuvStrides[2] = uvStride; + } + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java new file mode 100644 index 0000000000..0ab1fc475e --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBufferRenderer.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2015 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.google.android.exoplayer.ext.vp9; + +/** + * Renders the {@link VpxOutputBuffer}. + */ +public interface VpxOutputBufferRenderer { + + /** + * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. + */ + void setOutputBuffer(VpxOutputBuffer outputBuffer); + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java new file mode 100644 index 0000000000..508e77deac --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxRenderer.java @@ -0,0 +1,244 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.concurrent.atomic.AtomicReference; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * GLSurfaceView.Renderer implementation that can render YUV Frames returned by libvpx after + * decoding. It does the YUV to RGB color conversion in the Fragment Shader. + */ +/* package */ class VpxRenderer implements GLSurfaceView.Renderer { + + private static final float[] kColorConversion601 = { + 1.164f, 1.164f, 1.164f, + 0.0f, -0.392f, 2.017f, + 1.596f, -0.813f, 0.0f, + }; + + private static final float[] kColorConversion709 = { + 1.164f, 1.164f, 1.164f, + 0.0f, -0.213f, 2.112f, + 1.793f, -0.533f, 0.0f, + }; + + private static final String VERTEX_SHADER = + "varying vec2 interp_tc;\n" + + "attribute vec4 in_pos;\n" + + "attribute vec2 in_tc;\n" + + "void main() {\n" + + " gl_Position = in_pos;\n" + + " interp_tc = in_tc;\n" + + "}\n"; + private static final String[] TEXTURE_UNIFORMS = {"y_tex", "u_tex", "v_tex"}; + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "varying vec2 interp_tc;\n" + + "uniform sampler2D y_tex;\n" + + "uniform sampler2D u_tex;\n" + + "uniform sampler2D v_tex;\n" + + "uniform mat3 mColorConversion;\n" + + "void main() {\n" + + " vec3 yuv;" + + " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n" + + " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n" + + " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n" + + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);" + + "}\n"; + private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer( + -1.0f, 1.0f, + -1.0f, -1.0f, + 1.0f, 1.0f, + 1.0f, -1.0f); + private final int[] yuvTextures = new int[3]; + private final AtomicReference pendingOutputBufferReference; + + private int program; + private int texLocation; + private int colorMatrixLocation; + private FloatBuffer textureCoords; + private int previousWidth; + private int previousStride; + + private VpxOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. + + public VpxRenderer() { + previousWidth = -1; + previousStride = -1; + pendingOutputBufferReference = new AtomicReference<>(); + } + + /** + * Set a frame to be rendered. This should be followed by a call to + * VpxVideoSurfaceView.requestRender() to actually render the frame. + * + * @param outputBuffer OutputBuffer containing the YUV Frame to be rendered + */ + public void setFrame(VpxOutputBuffer outputBuffer) { + VpxOutputBuffer oldPendingOutputBuffer = pendingOutputBufferReference.getAndSet(outputBuffer); + if (oldPendingOutputBuffer != null) { + // The old pending output buffer will never be used for rendering, so release it now. + oldPendingOutputBuffer.release(); + } + } + + @Override + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + // Create the GL program. + program = GLES20.glCreateProgram(); + + // Add the vertex and fragment shaders. + addShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER, program); + addShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER, program); + + // Link the GL program. + GLES20.glLinkProgram(program); + int[] result = new int[] { + GLES20.GL_FALSE + }; + result[0] = GLES20.GL_FALSE; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, result, 0); + abortUnless(result[0] == GLES20.GL_TRUE, GLES20.glGetProgramInfoLog(program)); + GLES20.glUseProgram(program); + int posLocation = GLES20.glGetAttribLocation(program, "in_pos"); + GLES20.glEnableVertexAttribArray(posLocation); + GLES20.glVertexAttribPointer( + posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES); + texLocation = GLES20.glGetAttribLocation(program, "in_tc"); + GLES20.glEnableVertexAttribArray(texLocation); + checkNoGLES2Error(); + colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion"); + checkNoGLES2Error(); + setupTextures(); + checkNoGLES2Error(); + } + + @Override + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES20.glViewport(0, 0, width, height); + } + + @Override + public void onDrawFrame(GL10 unused) { + VpxOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); + if (pendingOutputBuffer == null && renderedOutputBuffer == null) { + // There is no output buffer to render at the moment. + return; + } + if (pendingOutputBuffer != null) { + if (renderedOutputBuffer != null) { + renderedOutputBuffer.release(); + } + renderedOutputBuffer = pendingOutputBuffer; + } + VpxOutputBuffer outputBuffer = renderedOutputBuffer; + // Set color matrix. Assume BT709 if the color space is unknown. + float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601 + ? kColorConversion601 : kColorConversion709; + GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); + + for (int i = 0; i < 3; i++) { + int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2; + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); + GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, + outputBuffer.yuvStrides[i], h, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, + outputBuffer.yuvPlanes[i]); + } + // Set cropping of stride if either width or stride has changed. + if (previousWidth != outputBuffer.width || previousStride != outputBuffer.yuvStrides[0]) { + float crop = (float) outputBuffer.width / outputBuffer.yuvStrides[0]; + textureCoords = nativeFloatBuffer( + 0.0f, 0.0f, + 0.0f, 1.0f, + crop, 0.0f, + crop, 1.0f); + GLES20.glVertexAttribPointer( + texLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords); + previousWidth = outputBuffer.width; + previousStride = outputBuffer.yuvStrides[0]; + } + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkNoGLES2Error(); + } + + private void addShader(int type, String source, int program) { + int[] result = new int[] { + GLES20.GL_FALSE + }; + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0); + abortUnless(result[0] == GLES20.GL_TRUE, + GLES20.glGetShaderInfoLog(shader) + ", source: " + source); + GLES20.glAttachShader(program, shader); + GLES20.glDeleteShader(shader); + + checkNoGLES2Error(); + } + + private void setupTextures() { + GLES20.glGenTextures(3, yuvTextures, 0); + for (int i = 0; i < 3; i++) { + GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + } + checkNoGLES2Error(); + } + + private void abortUnless(boolean condition, String msg) { + if (!condition) { + throw new RuntimeException(msg); + } + } + + private void checkNoGLES2Error() { + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + throw new RuntimeException("GLES20 error: " + error); + } + } + + private static FloatBuffer nativeFloatBuffer(float... array) { + FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * 4).order( + ByteOrder.nativeOrder()).asFloatBuffer(); + buffer.put(array); + buffer.flip(); + return buffer; + } + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java new file mode 100644 index 0000000000..47d0770abe --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxVideoSurfaceView.java @@ -0,0 +1,50 @@ +/* + * 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 com.google.android.exoplayer.ext.vp9; + +import android.annotation.TargetApi; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; + +/** + * A GLSurfaceView extension that scales itself to the given aspect ratio. + */ +@TargetApi(11) +public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBufferRenderer { + + private final VpxRenderer renderer; + + public VpxVideoSurfaceView(Context context) { + this(context, null); + } + + public VpxVideoSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + renderer = new VpxRenderer(); + setPreserveEGLContextOnPause(true); + setEGLContextClientVersion(2); + setRenderer(renderer); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + + @Override + public void setOutputBuffer(VpxOutputBuffer outputBuffer) { + renderer.setFrame(outputBuffer); + requestRender(); + } + +} diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk new file mode 100644 index 0000000000..f2d8a99a90 --- /dev/null +++ b/extensions/vp9/src/main/jni/Android.mk @@ -0,0 +1,42 @@ +# +# 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. +# + +WORKING_DIR := $(call my-dir) +include $(CLEAR_VARS) +LIBVPX_ROOT := $(WORKING_DIR)/libvpx +LIBYUV_ROOT := $(WORKING_DIR)/libyuv + +# build libyuv_static.a +LOCAL_PATH := $(WORKING_DIR) +include $(LIBYUV_ROOT)/Android.mk + +# build libvpx.so +LOCAL_PATH := $(WORKING_DIR) +include libvpx.mk + +# build libvpxJNI.so +include $(CLEAR_VARS) +LOCAL_PATH := $(WORKING_DIR) +LOCAL_MODULE := libvpxJNI +LOCAL_ARM_MODE := arm +LOCAL_CPP_EXTENSION := .cc +LOCAL_SRC_FILES := vpx_jni.cc +LOCAL_LDLIBS := -llog -lz -lm +LOCAL_SHARED_LIBRARIES := libvpx +LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,android/cpufeatures) diff --git a/extensions/vp9/src/main/jni/Application.mk b/extensions/vp9/src/main/jni/Application.mk new file mode 100644 index 0000000000..7dc417cda1 --- /dev/null +++ b/extensions/vp9/src/main/jni/Application.mk @@ -0,0 +1,20 @@ +# +# 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. +# + +APP_OPTIM := release +APP_STL := gnustl_static +APP_CPPFLAGS := -frtti +APP_PLATFORM := android-9 diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh new file mode 100755 index 0000000000..951dcc0dfe --- /dev/null +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# +# 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. +# + +# a bash script that generates the necessary config files for libvpx android ndk +# builds. + +set -e + +if [ $# -ne 1 ]; then + echo "Usage: ${0} " + exit +fi + +ndk="${1}" +shift 1 + +# configuration parameters common to all architectures +common_params="--disable-examples --disable-docs --enable-realtime-only" +common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io" +common_params+=" --disable-vp10 --disable-libyuv --disable-runtime-cpu-detect" + +# configuration parameters for various architectures +arch[0]="armeabi-v7a" +config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon" +config[0]+=" --enable-neon-asm" + +arch[1]="armeabi" +config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" +config[1]+=" --disable-neon-asm --disable-media" + +arch[2]="mips" +config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" + +arch[3]="x86" +config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2" +config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" +config[3]+=" --disable-avx2 --enable-pic" + +arch[4]="arm64-v8a" +config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon" +config[4]+=" --disable-neon-asm" + +arch[5]="x86_64" +config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" +config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" +config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm" + +arch[6]="mips64" +config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk" + +limit=$((${#arch[@]} - 1)) + +# list of files allowed after running configure in each arch directory. +# everything else will be removed. +allowed_files="libvpx_srcs.txt vpx_config.c vpx_config.h vpx_scale_rtcd.h" +allowed_files+=" vp8_rtcd.h vp9_rtcd.h vpx_version.h vpx_config.asm" +allowed_files+=" vpx_dsp_rtcd.h" + +remove_trailing_whitespace() { + perl -pi -e 's/\s\+$//' "$@" +} + +convert_asm() { + for i in $(seq 0 ${limit}); do + while read file; do + case "${file}" in + *.asm.s) + # Some files may already have been processed (there are duplicated + # .asm.s files for vp8 in the armeabi/armeabi-v7a configurations). + file="libvpx/${file}" + if [[ ! -e "${file}" ]]; then + asm_file="${file%.s}" + cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}" + remove_trailing_whitespace "${file}" + rm "${asm_file}" + fi + ;; + esac + done < libvpx_android_configs/${arch[${i}]}/libvpx_srcs.txt + done +} + +extglob_status="$(shopt extglob | cut -f2)" +shopt -s extglob +for i in $(seq 0 ${limit}); do + mkdir -p "libvpx_android_configs/${arch[${i}]}" + pushd "libvpx_android_configs/${arch[${i}]}" + + # configure and make + echo "build_android_configs: " + echo "configure ${config[${i}]} ${common_params}" + ../../libvpx/configure ${config[${i}]} ${common_params} + rm -f libvpx_srcs.txt + make libvpx_srcs.txt + + # remove files that aren't needed + rm -rf !(${allowed_files// /|}) + remove_trailing_whitespace * + + popd +done + +# restore extglob status as it was before +if [[ "${extglob_status}" == "off" ]]; then + shopt -u extglob +fi + +convert_asm + +echo "Generated android config files." diff --git a/extensions/vp9/src/main/jni/libvpx.mk b/extensions/vp9/src/main/jni/libvpx.mk new file mode 100644 index 0000000000..369b3b7f94 --- /dev/null +++ b/extensions/vp9/src/main/jni/libvpx.mk @@ -0,0 +1,51 @@ +# +# 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. +# + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) +CONFIG_DIR := $(LOCAL_PATH)/libvpx_android_configs/$(TARGET_ARCH_ABI) +libvpx_source_dir := $(LOCAL_PATH)/libvpx + +LOCAL_MODULE := libvpx +LOCAL_MODULE_CLASS := STATIC_LIBRARIES +LOCAL_CFLAGS := -DHAVE_CONFIG_H=vpx_config.h +LOCAL_ARM_MODE := arm +LOCAL_CFLAGS += -O3 + +# config specific include should go first to pick up the config specific rtcd. +LOCAL_C_INCLUDES := $(CONFIG_DIR) $(libvpx_source_dir) + +# generate source file list +libvpx_codec_srcs := $(sort $(shell cat $(CONFIG_DIR)/libvpx_srcs.txt)) +LOCAL_SRC_FILES := libvpx_android_configs/$(TARGET_ARCH_ABI)/vpx_config.c +LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \ + $(filter %.c, $(libvpx_codec_srcs)))) + +# include assembly files if they exist +# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly +LOCAL_SRC_FILES += $(addprefix libvpx/, \ + $(filter %.asm.s %.asm, $(libvpx_codec_srcs))) + +ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) +# append .neon to *_neon.c and *.s +LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES)) +LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES)) +endif + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \ + $(LOCAL_PATH)/libvpx/vpx + +include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc new file mode 100644 index 0000000000..a3abe24399 --- /dev/null +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -0,0 +1,176 @@ +/* + * 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. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "libyuv.h" // NOLINT + +#define VPX_CODEC_DISABLE_COMPAT 1 +#include "vpx/vpx_decoder.h" +#include "vpx/vp8dx.h" + +#define LOG_TAG "LIBVPX_DEC" +#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \ + __VA_ARGS__)) + +#define FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_vp9_VpxDecoder_ ## NAME \ + (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_vp9_VpxDecoder_ ## NAME \ + (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ + +// JNI references for VpxOutputBuffer class. +static jmethodID initForRgbFrame; +static jmethodID initForYuvFrame; +static jfieldID dataField; +static jfieldID outputModeField; + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + return JNI_VERSION_1_6; +} + +FUNC(jlong, vpxInit) { + vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); + vpx_codec_dec_cfg_t cfg = {0}; + cfg.threads = android_getCpuCount(); + if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { + LOGE("ERROR: Fail to initialize libvpx decoder."); + return 0; + } + + // Populate JNI References. + const jclass outputBufferClass = env->FindClass( + "com/google/android/exoplayer/ext/vp9/VpxOutputBuffer"); + initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", + "(IIIII)V"); + initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame", + "(II)V"); + dataField = env->GetFieldID(outputBufferClass, "data", + "Ljava/nio/ByteBuffer;"); + outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); + + return reinterpret_cast(context); +} + +FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { + vpx_codec_ctx_t* const context = reinterpret_cast(jContext); + const uint8_t* const buffer = + reinterpret_cast(env->GetDirectBufferAddress(encoded)); + const vpx_codec_err_t status = + vpx_codec_decode(context, buffer, len, NULL, 0); + if (status != VPX_CODEC_OK) { + LOGE("ERROR: vpx_codec_decode() failed, status= %d", status); + return -1; + } + return 0; +} + +FUNC(jlong, vpxClose, jlong jContext) { + vpx_codec_ctx_t* const context = reinterpret_cast(jContext); + vpx_codec_destroy(context); + delete context; + return 0; +} + +FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { + vpx_codec_ctx_t* const context = reinterpret_cast(jContext); + vpx_codec_iter_t iter = NULL; + const vpx_image_t* const img = vpx_codec_get_frame(context, &iter); + + if (img == NULL) { + return 1; + } + + const int kOutputModeYuv = 0; + const int kOutputModeRgb = 1; + + int outputMode = env->GetIntField(jOutputBuffer, outputModeField); + if (outputMode == kOutputModeRgb) { + // resize buffer if required. + env->CallVoidMethod(jOutputBuffer, initForRgbFrame, img->d_w, img->d_h); + + // get pointer to the data buffer. + const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); + uint8_t* const dst = + reinterpret_cast(env->GetDirectBufferAddress(dataObject)); + + libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y], + img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U], + img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V], + dst, img->d_w * 2, img->d_w, img->d_h); + } else if (outputMode == kOutputModeYuv) { + const int kColorspaceUnknown = 0; + const int kColorspaceBT601 = 1; + const int kColorspaceBT709 = 2; + + int colorspace = kColorspaceUnknown; + switch (img->cs) { + case VPX_CS_BT_601: + colorspace = kColorspaceBT601; + break; + case VPX_CS_BT_709: + colorspace = kColorspaceBT709; + break; + default: + break; + } + + // resize buffer if required. + env->CallVoidMethod(jOutputBuffer, initForYuvFrame, img->d_w, img->d_h, + img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], + colorspace); + + // get pointer to the data buffer. + const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); + jbyte* const data = + reinterpret_cast(env->GetDirectBufferAddress(dataObject)); + + // TODO: This copy can be eliminated by using external frame buffers. NOLINT + // This is insignificant for smaller videos but takes ~1.5ms for 1080p + // clips. So this should eventually be gotten rid of. + const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h; + const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2); + memcpy(data, img->planes[VPX_PLANE_Y], y_length); + memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length); + memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length); + } + return 0; +} + +FUNC(jstring, getLibvpxVersion) { + return env->NewStringUTF(vpx_codec_version_str()); +} + +FUNC(jstring, vpxGetErrorMessage, jlong jContext) { + vpx_codec_ctx_t* const context = reinterpret_cast(jContext); + return env->NewStringUTF(vpx_codec_error(context)); +} diff --git a/extensions/vp9/src/main/proguard.cfg b/extensions/vp9/src/main/proguard.cfg new file mode 100644 index 0000000000..41a8b85a7e --- /dev/null +++ b/extensions/vp9/src/main/proguard.cfg @@ -0,0 +1,11 @@ +# Proguard rules specific to the VP9 extension. + +# This prevents the names of native methods from being obfuscated. +-keepclasseswithmembernames class * { + native ; +} + +# Some members of this class are being accessed from native methods. Keep them unobfuscated. +-keep class com.google.android.exoplayer.ext.vp9.VpxOutputBuffer { + *; +} diff --git a/extensions/vp9/src/main/project.properties b/extensions/vp9/src/main/project.properties new file mode 100644 index 0000000000..b92a03b7ab --- /dev/null +++ b/extensions/vp9/src/main/project.properties @@ -0,0 +1,16 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-23 +android.library=true +android.library.reference.1=../../../../library/src/main diff --git a/extensions/vp9/src/main/res/.README.txt b/extensions/vp9/src/main/res/.README.txt new file mode 100644 index 0000000000..c27147ce56 --- /dev/null +++ b/extensions/vp9/src/main/res/.README.txt @@ -0,0 +1,2 @@ +This file is needed to make sure the res directory is present. +The file is ignored by the Android toolchain because its name starts with a dot. diff --git a/settings.gradle b/settings.gradle index 58e6868d79..b06950e5f6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,9 +14,11 @@ include ':library' include ':demo' include ':extension-opus' +include ':extension-vp9' include ':extension-okhttp' include ':extension-flac' project(':extension-opus').projectDir = new File(settingsDir, 'extensions/opus') +project(':extension-vp9').projectDir = new File(settingsDir, 'extensions/vp9') project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac')