Add FFmpeg extension.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=122978403
This commit is contained in:
andrewlewis 2016-05-23 02:32:44 -07:00 committed by Oliver Woodman
parent 9981a83c05
commit ddde6edb92
18 changed files with 1076 additions and 4 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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')
}

View File

@ -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.
}
}
}

View File

@ -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 "<path to exoplayer checkout>"
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="<path to Android NDK>"
```
* 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.

View File

@ -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')
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="java"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?fileVersion 4.0.0?><cproject storage_type_id="org.eclipse.cdt.core.XmlProjectDescriptionStorage">
<storageModule moduleId="org.eclipse.cdt.core.settings">
<cconfiguration id="com.android.toolchain.gcc.423224913">
<storageModule buildSystemId="org.eclipse.cdt.managedbuilder.core.configurationDataProvider" id="com.android.toolchain.gcc.423224913" moduleId="org.eclipse.cdt.core.settings" name="Default">
<externalSettings/>
<extensions>
<extension id="org.eclipse.cdt.core.VCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GmakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.CWDLocator" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.MakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GCCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GASErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GLDErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.ELF" point="org.eclipse.cdt.core.BinaryParser"/>
</extensions>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<configuration artifactName="${ProjName}" buildProperties="" description="" id="com.android.toolchain.gcc.423224913" name="Default" parent="org.eclipse.cdt.build.core.emptycfg">
<folderInfo id="com.android.toolchain.gcc.423224913.1376674556" name="/" resourcePath="">
<toolChain id="com.android.toolchain.gcc.1798416430" name="Android GCC" superClass="com.android.toolchain.gcc">
<targetPlatform binaryParser="org.eclipse.cdt.core.ELF" id="com.android.targetPlatform.1132129264" isAbstract="false" superClass="com.android.targetPlatform"/>
<builder buildPath="${workspace_loc:/ExoPlayerExt-FFmpeg}/jni" id="com.android.builder.532503968" keepEnvironmentInBuildfile="false" managedBuildOn="false" name="Android Builder" superClass="com.android.builder">
<outputEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="outputPath" name="obj"/>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="outputPath" name="libs"/>
</outputEntries>
</builder>
<tool id="com.android.gcc.compiler.906450637" name="Android GCC Compiler" superClass="com.android.gcc.compiler">
<inputType id="com.android.gcc.inputType.835889068" superClass="com.android.gcc.inputType"/>
</tool>
</toolChain>
</folderInfo>
<sourceEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="sourcePath" name="jni"/>
</sourceEntries>
</configuration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>
</cconfiguration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.LanguageSettingsProviders"/>
<storageModule moduleId="scannerConfiguration">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
<scannerConfigBuildInfo instanceId="com.android.toolchain.gcc.423224913;com.android.toolchain.gcc.423224913.1376674556;com.android.gcc.compiler.906450637;com.android.gcc.inputType.835889068">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId="com.android.AndroidPerProjectProfile"/>
</scannerConfigBuildInfo>
</storageModule>
<storageModule moduleId="refreshScope" versionNumber="2">
<configuration configurationName="Default">
<resource resourceType="PROJECT" workspacePath="/ExoPlayerExt-FFmpeg"/>
</configuration>
</storageModule>
</cproject>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerExt-FFmpeg</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.genmakebuilder</name>
<triggers>clean,full,incremental,</triggers>
<arguments>
<dictionary>
<key>?children?</key>
<value>?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\||</value>
</dictionary>
<dictionary>
<key>?name?</key>
<value></value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.append_environment</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.buildArguments</key>
<value></value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.buildCommand</key>
<value>ndk-build</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.cleanBuildTarget</key>
<value>clean</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.contents</key>
<value>org.eclipse.cdt.make.core.activeConfigSettings</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableAutoBuild</key>
<value>false</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableCleanBuild</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.enableFullBuild</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.stopOnError</key>
<value>true</value>
</dictionary>
<dictionary>
<key>org.eclipse.cdt.make.core.useDefaultBuildCmd</key>
<value>true</value>
</dictionary>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder</name>
<triggers>full,incremental,</triggers>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.cdt.core.cnature</nature>
<nature>org.eclipse.cdt.core.ccnature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.managedBuildNature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.ScannerConfigNature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.ext.ffmpeg">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
</manifest>

View File

@ -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);
}
}

View File

@ -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<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
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<byte[]> 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<byte[]> 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);
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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

View File

@ -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 <jni.h>
#include <stdlib.h>
#include <android/log.h>
extern "C" {
#ifdef __cplusplus
#define __STDC_CONSTANT_MACROS
#ifdef _STDINT_H
#undef _STDINT_H
#endif
#include <stdint.h>
#endif
#include <libavcodec/avcodec.h>
#include <libavresample/avresample.h>
#include <libavutil/error.h>
#include <libavutil/opt.h>
}
#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<void**>(&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);
}

View File

@ -0,0 +1,6 @@
# Proguard rules specific to the FFmpeg extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}

View File

@ -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

View File

@ -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')