From ddde6edb925f32cc0f126134ba6e072dfa7048e1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 23 May 2016 02:32:44 -0700 Subject: [PATCH] Add FFmpeg extension. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=122978403 --- .gitignore | 3 + demo/build.gradle | 3 +- .../exoplayer/demo/player/DemoPlayer.java | 19 +- extensions/ffmpeg/README.md | 96 ++++++ extensions/ffmpeg/build.gradle | 44 +++ extensions/ffmpeg/src/main/.classpath | 10 + extensions/ffmpeg/src/main/.cproject | 54 +++ extensions/ffmpeg/src/main/.project | 97 ++++++ .../ffmpeg/src/main/AndroidManifest.xml | 22 ++ .../ext/ffmpeg/FfmpegAudioTrackRenderer.java | 70 ++++ .../exoplayer/ext/ffmpeg/FfmpegDecoder.java | 219 ++++++++++++ .../ext/ffmpeg/FfmpegDecoderException.java | 29 ++ extensions/ffmpeg/src/main/jni/Android.mk | 45 +++ extensions/ffmpeg/src/main/jni/Application.mk | 20 ++ extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc | 324 ++++++++++++++++++ extensions/ffmpeg/src/main/proguard.cfg | 6 + extensions/ffmpeg/src/main/project.properties | 16 + settings.gradle | 3 + 18 files changed, 1076 insertions(+), 4 deletions(-) create mode 100644 extensions/ffmpeg/README.md create mode 100644 extensions/ffmpeg/build.gradle create mode 100644 extensions/ffmpeg/src/main/.classpath create mode 100644 extensions/ffmpeg/src/main/.cproject create mode 100644 extensions/ffmpeg/src/main/.project create mode 100644 extensions/ffmpeg/src/main/AndroidManifest.xml create mode 100644 extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegAudioTrackRenderer.java create mode 100644 extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoder.java create mode 100644 extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoderException.java create mode 100644 extensions/ffmpeg/src/main/jni/Android.mk create mode 100644 extensions/ffmpeg/src/main/jni/Application.mk create mode 100644 extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc create mode 100644 extensions/ffmpeg/src/main/proguard.cfg create mode 100644 extensions/ffmpeg/src/main/project.properties diff --git a/.gitignore b/.gitignore index ea082245c5..4f032b8f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ extensions/opus/src/main/jni/libopus # FLAC extension extensions/flac/src/main/jni/flac + +# FFmpeg extension +extensions/ffmpeg/src/main/jni/ffmpeg diff --git a/demo/build.gradle b/demo/build.gradle index 46f6ed3b21..7dd176bd0a 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -44,7 +44,8 @@ android { dependencies { compile project(':library') - demo_extCompile project(path: ':extension-opus') + demo_extCompile project(path: ':extension-ffmpeg') demo_extCompile project(path: ':extension-flac') + demo_extCompile project(path: ':extension-opus') demo_extCompile project(path: ':extension-vp9') } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 7616e9208e..bbd67c3591 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -495,24 +495,37 @@ public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.Even Constructor constructor = clazz.getConstructor(boolean.class, Handler.class, VideoTrackRendererEventListener.class, int.class); renderersList.add((TrackRenderer) constructor.newInstance(true, mainHandler, this, 50)); + Log.i(TAG, "Loaded LibvpxVideoTrackRenderer."); } catch (Exception e) { - Log.i(TAG, "can't load LibvpxVideoTrackRenderer."); + // Expected if the app was built without the extension. } try { Class clazz = Class.forName("com.google.android.exoplayer.ext.opus.LibopusAudioTrackRenderer"); renderersList.add((TrackRenderer) clazz.newInstance()); + Log.i(TAG, "Loaded LibopusAudioTrackRenderer."); } catch (Exception e) { - Log.i(TAG, "can't load LibopusAudioTrackRenderer."); + // Expected if the app was built without the extension. } try { Class clazz = Class.forName("com.google.android.exoplayer.ext.flac.LibflacAudioTrackRenderer"); renderersList.add((TrackRenderer) clazz.newInstance()); + Log.i(TAG, "Loaded LibflacAudioTrackRenderer."); } catch (Exception e) { - Log.i(TAG, "can't load LibflacAudioTrackRenderer."); + // Expected if the app was built without the extension. + } + + try { + Class clazz = + Class.forName("com.google.android.exoplayer.ext.ffmpeg.FfmpegAudioTrackRenderer"); + renderersList.add((TrackRenderer) clazz.newInstance()); + Log.i(TAG, "Loaded FfmpegAudioTrackRenderer."); + } catch (Exception e) { + // Expected if the app was built without the extension. } } + } diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md new file mode 100644 index 0000000000..57c0b0795e --- /dev/null +++ b/extensions/ffmpeg/README.md @@ -0,0 +1,96 @@ +# FfmpegAudioTrackRenderer # + +## Description ## + +The FFmpeg extension is a [TrackRenderer][] implementation that uses FFmpeg to +decode audio. + +[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)" +FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" +``` + +* Download the [Android NDK][] and set its location in an environment variable: + +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html + +``` +NDK_PATH="" +``` + +* Fetch and build ffmpeg. + +For example, to fetch and build for armv7a: + +``` +cd "${FFMPEG_EXT_PATH}/jni" && \ +git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ +./configure \ + --prefix=../../jniLibs/armeabi-v7a \ + --arch=arm \ + --cpu=armv7-a \ + --cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-" \ + --target-os=android \ + --sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \ + --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ + --extra-ldflags="-Wl,--fix-cortex-a8" \ + --extra-ldexeflags=-pie \ + --disable-static \ + --enable-shared \ + --disable-doc \ + --disable-programs \ + --disable-everything \ + --disable-avdevice \ + --disable-avformat \ + --disable-swscale \ + --disable-postproc \ + --disable-avfilter \ + --disable-symver \ + --enable-avresample \ + --enable-decoder=vorbis \ + --enable-decoder=opus \ + --enable-decoder=flac \ + && \ +make -j4 && \ +make install-libs +``` + +* Build the JNI native libraries. + +``` +cd "${FFMPEG_EXT_PATH}"/jni && \ +${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 +``` + +TODO: Add instructions for other ABIs. + +* In your project, you can add a dependency on the extension by using a rule + like this: + +``` +// in settings.gradle +include ':..:ExoPlayer:library' +include ':..:ExoPlayer:extension-ffmpeg' + +// in build.gradle +dependencies { + compile project(':..:ExoPlayer:library') + compile project(':..:ExoPlayer:extension-ffmpeg') +} +``` + +* Now, when you build your app, the extension will be built and the native + libraries will be packaged along with the APK. diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle new file mode 100644 index 0000000000..6383ab6f10 --- /dev/null +++ b/extensions/ffmpeg/build.gradle @@ -0,0 +1,44 @@ +// 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/ffmpeg/src/main/.classpath b/extensions/ffmpeg/src/main/.classpath new file mode 100644 index 0000000000..b4f30723bc --- /dev/null +++ b/extensions/ffmpeg/src/main/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/ffmpeg/src/main/.cproject b/extensions/ffmpeg/src/main/.cproject new file mode 100644 index 0000000000..85e91eab25 --- /dev/null +++ b/extensions/ffmpeg/src/main/.cproject @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/ffmpeg/src/main/.project b/extensions/ffmpeg/src/main/.project new file mode 100644 index 0000000000..531bcc5618 --- /dev/null +++ b/extensions/ffmpeg/src/main/.project @@ -0,0 +1,97 @@ + + + ExoPlayerExt-FFmpeg + + + + + + 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/ffmpeg/src/main/AndroidManifest.xml b/extensions/ffmpeg/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..136af84b42 --- /dev/null +++ b/extensions/ffmpeg/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegAudioTrackRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegAudioTrackRenderer.java new file mode 100644 index 0000000000..ce8e6c62df --- /dev/null +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegAudioTrackRenderer.java @@ -0,0 +1,70 @@ +/* + * 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.ffmpeg; + +import com.google.android.exoplayer.AudioTrackRendererEventListener; +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.extensions.AudioDecoderTrackRenderer; +import com.google.android.exoplayer.util.MimeTypes; + +import android.os.Handler; + +/** + * Decodes and renders audio using FFmpeg. + */ +public final class FfmpegAudioTrackRenderer extends AudioDecoderTrackRenderer { + + private static final int NUM_BUFFERS = 16; + private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + + private FfmpegDecoder decoder; + + public FfmpegAudioTrackRenderer() { + this(null, null); + } + + public FfmpegAudioTrackRenderer(Handler eventHandler, + AudioTrackRendererEventListener eventListener) { + super(eventHandler, eventListener); + } + + @Override + protected int supportsFormat(Format format) { + if (!FfmpegDecoder.IS_AVAILABLE) { + return FORMAT_UNSUPPORTED_TYPE; + } + String mimeType = format.sampleMimeType; + return FfmpegDecoder.supportsFormat(mimeType) ? FORMAT_HANDLED + : MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + protected FfmpegDecoder createDecoder(Format format) throws FfmpegDecoderException { + decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + format.sampleMimeType, format.initializationData); + return decoder; + } + + @Override + public Format getOutputFormat() { + int channelCount = decoder.getChannelCount(); + int sampleRate = decoder.getSampleRate(); + return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, Format.NO_VALUE, + Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null); + } + +} diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoder.java new file mode 100644 index 0000000000..7a5b2e4223 --- /dev/null +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoder.java @@ -0,0 +1,219 @@ +/* + * 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.ffmpeg; + +import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.extensions.SimpleDecoder; +import com.google.android.exoplayer.extensions.SimpleOutputBuffer; +import com.google.android.exoplayer.util.MimeTypes; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * JNI wrapper for FFmpeg. Only audio decoding is supported. + */ +/* package */ final class FfmpegDecoder extends + SimpleDecoder { + + private static final String TAG = "FfmpegDecoder"; + + /** + * Whether the underlying FFmpeg library is available. + */ + public static final boolean IS_AVAILABLE; + static { + boolean isAvailable; + try { + System.loadLibrary("avutil"); + System.loadLibrary("avresample"); + System.loadLibrary("avcodec"); + System.loadLibrary("ffmpeg"); + isAvailable = true; + } catch (UnsatisfiedLinkError exception) { + isAvailable = false; + } + IS_AVAILABLE = isAvailable; + } + + /** + * Returns whether this decoder can decode samples in the specified MIME type. + */ + public static boolean supportsFormat(String mimeType) { + String codecName = getCodecName(mimeType); + return codecName != null && nativeHasDecoder(codecName); + } + + // Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2; + + private final String codecName; + private final byte[] extraData; + + private long nativeContext; // May be reassigned on resetting the codec. + private boolean hasOutputFormat; + private volatile int channelCount; + private volatile int sampleRate; + + public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, + String mimeType, List initializationData) throws FfmpegDecoderException { + super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); + codecName = getCodecName(mimeType); + extraData = getExtraData(mimeType, initializationData); + nativeContext = nativeInitialize(codecName, extraData); + if (nativeContext == 0) { + throw new FfmpegDecoderException("Initialization failed."); + } + setInitialInputBufferSize(initialInputBufferSize); + } + + @Override + public String getName() { + return "ffmpeg" + nativeGetFfmpegVersion() + "-" + codecName; + } + + @Override + public DecoderInputBuffer createInputBuffer() { + return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + } + + @Override + public SimpleOutputBuffer createOutputBuffer() { + return new SimpleOutputBuffer(this); + } + + @Override + public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer, + SimpleOutputBuffer outputBuffer, boolean reset) { + if (reset) { + nativeContext = nativeReset(nativeContext, extraData); + if (nativeContext == 0) { + return new FfmpegDecoderException("Error resetting (see logcat)."); + } + } + ByteBuffer inputData = inputBuffer.data; + int inputSize = inputData.limit(); + ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE); + int result = nativeDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE); + if (result < 0) { + return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); + } + if (!hasOutputFormat) { + channelCount = nativeGetChannelCount(nativeContext); + sampleRate = nativeGetSampleRate(nativeContext); + hasOutputFormat = true; + } + outputBuffer.data.position(0); + outputBuffer.data.limit(result); + return null; + } + + @Override + public void release() { + super.release(); + nativeRelease(nativeContext); + nativeContext = 0; + } + + /** + * Returns the channel count of output audio. May only be called after {@link #decode}. + */ + public int getChannelCount() { + return channelCount; + } + + /** + * Returns the sample rate of output audio. May only be called after {@link #decode}. + */ + public int getSampleRate() { + return sampleRate; + } + + /** + * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if + * not required. + */ + private static byte[] getExtraData(String mimeType, List initializationData) { + switch (mimeType) { + case MimeTypes.AUDIO_AAC: + case MimeTypes.AUDIO_OPUS: + return initializationData.get(0); + case MimeTypes.AUDIO_VORBIS: + byte[] header0 = initializationData.get(0); + byte[] header1 = initializationData.get(1); + byte[] extraData = new byte[header0.length + header1.length + 6]; + extraData[0] = (byte) (header0.length >> 8); + extraData[1] = (byte) (header0.length & 0xFF); + System.arraycopy(header0, 0, extraData, 2, header0.length); + extraData[header0.length + 2] = 0; + extraData[header0.length + 3] = 0; + extraData[header0.length + 4] = (byte) (header1.length >> 8); + extraData[header0.length + 5] = (byte) (header1.length & 0xFF); + System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); + return extraData; + default: + // Other codecs do not require extra data. + return null; + } + } + + /** + * Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}. The codec + * can only be used if {@link #nativeHasDecoder(String)} returns true for the returned codec name. + */ + private static String getCodecName(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_AAC: + return "aac"; + case MimeTypes.AUDIO_MPEG: + case MimeTypes.AUDIO_MPEG_L1: + case MimeTypes.AUDIO_MPEG_L2: + return "mp3"; + case MimeTypes.AUDIO_AC3: + return "ac3"; + case MimeTypes.AUDIO_E_AC3: + return "eac3"; + case MimeTypes.AUDIO_TRUEHD: + return "truehd"; + case MimeTypes.AUDIO_DTS: + case MimeTypes.AUDIO_DTS_HD: + return "dca"; + case MimeTypes.AUDIO_VORBIS: + return "vorbis"; + case MimeTypes.AUDIO_OPUS: + return "opus"; + case MimeTypes.AUDIO_AMR_NB: + return "amrnb"; + case MimeTypes.AUDIO_AMR_WB: + return "amrwb"; + case MimeTypes.AUDIO_FLAC: + return "flac"; + default: + return null; + } + } + + private static native String nativeGetFfmpegVersion(); + private static native boolean nativeHasDecoder(String codecName); + private native long nativeInitialize(String codecName, byte[] extraData); + private native int nativeDecode(long context, ByteBuffer inputData, int inputSize, + ByteBuffer outputData, int outputSize); + private native int nativeGetChannelCount(long context); + private native int nativeGetSampleRate(long context); + private native long nativeReset(long context, byte[] extraData); + private native void nativeRelease(long context); + +} diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoderException.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoderException.java new file mode 100644 index 0000000000..46199ff75d --- /dev/null +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer/ext/ffmpeg/FfmpegDecoderException.java @@ -0,0 +1,29 @@ +/* + * 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.ffmpeg; + +import com.google.android.exoplayer.extensions.AudioDecoderException; + +/** + * Thrown when an FFmpeg decoder error occurs. + */ +public final class FfmpegDecoderException extends AudioDecoderException { + + /* package */ FfmpegDecoderException(String message) { + super(message); + } + +} diff --git a/extensions/ffmpeg/src/main/jni/Android.mk b/extensions/ffmpeg/src/main/jni/Android.mk new file mode 100644 index 0000000000..d7026576e6 --- /dev/null +++ b/extensions/ffmpeg/src/main/jni/Android.mk @@ -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. +# + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := libavcodec +LOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/lib/$(LOCAL_MODULE).so +include $(PREBUILT_SHARED_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libavutil +LOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/lib/$(LOCAL_MODULE).so +include $(PREBUILT_SHARED_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libavresample +LOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/lib/$(LOCAL_MODULE).so +include $(PREBUILT_SHARED_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libswresample +LOCAL_SRC_FILES := ../jniLibs/$(TARGET_ARCH_ABI)/lib/$(LOCAL_MODULE).so +include $(PREBUILT_SHARED_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := ffmpeg +LOCAL_SRC_FILES := ffmpeg_jni.cc +LOCAL_C_INCLUDES := ffmpeg +LOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil libswresample +LOCAL_LDLIBS := -L../jniLibs/$(TARGET_ARCH_ABI)/lib -llog +include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/ffmpeg/src/main/jni/Application.mk b/extensions/ffmpeg/src/main/jni/Application.mk new file mode 100644 index 0000000000..7dc417cda1 --- /dev/null +++ b/extensions/ffmpeg/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/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc new file mode 100644 index 0000000000..8ebbfd5fb1 --- /dev/null +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -0,0 +1,324 @@ +/* + * 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 + +extern "C" { +#ifdef __cplusplus +#define __STDC_CONSTANT_MACROS +#ifdef _STDINT_H +#undef _STDINT_H +#endif +#include +#endif +#include +#include +#include +#include +} + +#define LOG_TAG "ffmpeg_jni" +#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_ffmpeg_FfmpegDecoder_ ## NAME \ + (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_ffmpeg_FfmpegDecoder_ ## NAME \ + (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ + +#define ERROR_STRING_BUFFER_LENGTH 256 + +// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT. +static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16; + +/** + * Returns the AVCodec with the specified name, or NULL if it is not available. + */ +AVCodec *getCodecByName(JNIEnv* env, jstring codecName); + +/** + * Allocates and opens a new AVCodecContext for the specified codec, passing the + * provided extraData as initialization data for the decoder if it is non-NULL. + * Returns the created context. + */ +AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, + jbyteArray extraData); + +/** + * Decodes the packet into the output buffer, returning the number of bytes + * written, or a negative value in the case of an error. + */ +int decodePacket(AVCodecContext *context, AVPacket *packet, + uint8_t *outputBuffer, int outputSize); + +/** + * Outputs a log message describing the avcodec error number. + */ +void logError(const char *functionName, int errorNumber); + +/** + * Releases the specified context. + */ +void releaseContext(AVCodecContext *context); + +jint JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + avcodec_register_all(); + return JNI_VERSION_1_6; +} + +FUNC(jstring, nativeGetFfmpegVersion) { + return env->NewStringUTF(LIBAVCODEC_IDENT); +} + +FUNC(jboolean, nativeHasDecoder, jstring codecName) { + return getCodecByName(env, codecName) != NULL; +} + +FUNC(jlong, nativeInitialize, jstring codecName, jbyteArray extraData) { + AVCodec *codec = getCodecByName(env, codecName); + if (!codec) { + LOGE("Codec not found."); + return 0L; + } + return (jlong) createContext(env, codec, extraData); +} + +FUNC(jint, nativeDecode, jlong context, jobject inputData, jint inputSize, + jobject outputData, jint outputSize) { + if (!context) { + LOGE("Context must be non-NULL."); + return -1; + } + if (!inputData || !outputData) { + LOGE("Input and output buffers must be non-NULL."); + return -1; + } + if (inputSize < 0) { + LOGE("Invalid input buffer size: %d.", inputSize); + return -1; + } + if (outputSize < 0) { + LOGE("Invalid output buffer length: %d", outputSize); + return -1; + } + uint8_t *inputBuffer = (uint8_t *) env->GetDirectBufferAddress(inputData); + uint8_t *outputBuffer = (uint8_t *) env->GetDirectBufferAddress(outputData); + AVPacket packet; + av_init_packet(&packet); + packet.data = inputBuffer; + packet.size = inputSize; + return decodePacket((AVCodecContext *) context, &packet, outputBuffer, + outputSize); +} + +FUNC(jint, nativeGetChannelCount, jlong context) { + if (!context) { + LOGE("Context must be non-NULL."); + return -1; + } + return ((AVCodecContext *) context)->channels; +} + +FUNC(jint, nativeGetSampleRate, jlong context) { + if (!context) { + LOGE("Context must be non-NULL."); + return -1; + } + return ((AVCodecContext *) context)->sample_rate; +} + +FUNC(jlong, nativeReset, jlong jContext, jbyteArray extraData) { + AVCodecContext *context = (AVCodecContext *) jContext; + if (!context) { + LOGE("Tried to reset without a context."); + return 0L; + } + + AVCodecID codecId = context->codec_id; + if (codecId == AV_CODEC_ID_TRUEHD) { + // Release and recreate the context if the codec is TrueHD. + // TODO: Figure out why flushing doesn't work for this codec. + releaseContext(context); + AVCodec *codec = avcodec_find_decoder(codecId); + if (!codec) { + LOGE("Unexpected error finding codec %d.", codecId); + return 0L; + } + return (jlong) createContext(env, codec, extraData); + } + + avcodec_flush_buffers(context); + return (jlong) context; +} + +FUNC(void, nativeRelease, jlong context) { + if (context) { + releaseContext((AVCodecContext *) context); + } +} + +AVCodec *getCodecByName(JNIEnv* env, jstring codecName) { + if (!codecName) { + return NULL; + } + const char *codecNameChars = env->GetStringUTFChars(codecName, NULL); + AVCodec *codec = avcodec_find_decoder_by_name(codecNameChars); + env->ReleaseStringUTFChars(codecName, codecNameChars); + return codec; +} + +AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, + jbyteArray extraData) { + AVCodecContext *context = avcodec_alloc_context3(codec); + if (!context) { + LOGE("Failed to allocate context."); + return NULL; + } + context->request_sample_fmt = OUTPUT_FORMAT; + if (extraData) { + jsize size = env->GetArrayLength(extraData); + context->extradata_size = size; + context->extradata = + (uint8_t *) av_malloc(size + AV_INPUT_BUFFER_PADDING_SIZE); + if (!context->extradata) { + LOGE("Failed to allocate extradata."); + releaseContext(context); + return NULL; + } + env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata); + } + int result = avcodec_open2(context, codec, NULL); + if (result < 0) { + logError("avcodec_open2", result); + releaseContext(context); + return NULL; + } + return context; +} + +int decodePacket(AVCodecContext *context, AVPacket *packet, + uint8_t *outputBuffer, int outputSize) { + int result = 0; + // Queue input data. + result = avcodec_send_packet(context, packet); + if (result) { + logError("avcodec_send_packet", result); + return result; + } + + // Dequeue output data until it runs out. + int outSize = 0; + while (true) { + AVFrame *frame = av_frame_alloc(); + if (!frame) { + LOGE("Failed to allocate output frame."); + return -1; + } + result = avcodec_receive_frame(context, frame); + if (result) { + av_frame_free(&frame); + if (result == AVERROR(EAGAIN)) { + break; + } + logError("avcodec_receive_frame", result); + return result; + } + + // Resample output. + AVSampleFormat sampleFormat = context->sample_fmt; + int channelCount = context->channels; + int channelLayout = context->channel_layout; + int sampleRate = context->sample_rate; + int sampleCount = frame->nb_samples; + int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount, + sampleFormat, 1); + AVAudioResampleContext *resampleContext; + if (context->opaque) { + resampleContext = (AVAudioResampleContext *)context->opaque; + } else { + resampleContext = avresample_alloc_context(); + av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0); + av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0); + av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0); + av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0); + av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0); + av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0); + result = avresample_open(resampleContext); + if (result < 0) { + logError("avresample_open", result); + av_frame_free(&frame); + return -1; + } + context->opaque = resampleContext; + } + int inSampleSize = av_get_bytes_per_sample(sampleFormat); + int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT); + int outSamples = avresample_get_out_samples(resampleContext, sampleCount); + int bufferOutSize = outSampleSize * channelCount * outSamples; + if (outSize + bufferOutSize > outputSize) { + LOGE("Output buffer size (%d) too small for output data (%d).", + outputSize, outSize + bufferOutSize); + av_frame_free(&frame); + return -1; + } + result = avresample_convert(resampleContext, &outputBuffer, bufferOutSize, + outSamples, frame->data, frame->linesize[0], + sampleCount); + av_frame_free(&frame); + if (result < 0) { + logError("avresample_convert", result); + return result; + } + int available = avresample_available(resampleContext); + if (available != 0) { + LOGE("Expected no samples remaining after resampling, but found %d.", + available); + return -1; + } + outputBuffer += bufferOutSize; + outSize += bufferOutSize; + } + return outSize; +} + +void logError(const char *functionName, int errorNumber) { + char *buffer = (char *) malloc(ERROR_STRING_BUFFER_LENGTH * sizeof(char)); + av_strerror(errorNumber, buffer, ERROR_STRING_BUFFER_LENGTH); + LOGE("Error in %s: %s", functionName, buffer); + free(buffer); +} + +void releaseContext(AVCodecContext *context) { + if (!context) { + return; + } + AVAudioResampleContext *resampleContext; + if (resampleContext = (AVAudioResampleContext *)context->opaque) { + avresample_free(&resampleContext); + context->opaque = NULL; + } + avcodec_free_context(&context); +} + diff --git a/extensions/ffmpeg/src/main/proguard.cfg b/extensions/ffmpeg/src/main/proguard.cfg new file mode 100644 index 0000000000..7f62d2b7e7 --- /dev/null +++ b/extensions/ffmpeg/src/main/proguard.cfg @@ -0,0 +1,6 @@ +# Proguard rules specific to the FFmpeg extension. + +# This prevents the names of native methods from being obfuscated. +-keepclasseswithmembernames class * { + native ; +} diff --git a/extensions/ffmpeg/src/main/project.properties b/extensions/ffmpeg/src/main/project.properties new file mode 100644 index 0000000000..b92a03b7ab --- /dev/null +++ b/extensions/ffmpeg/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/settings.gradle b/settings.gradle index b06950e5f6..79deda6170 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,8 +17,11 @@ include ':extension-opus' include ':extension-vp9' include ':extension-okhttp' include ':extension-flac' +include ':extension-ffmpeg' 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') +project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') +