How to Build ffmpeg with NDK r9

This is a updated post for a previous post, where we built ffmpeg 0.8 with Android NDK r5 and r6. This post will give instructions of how to build ffmpeg 2.0.1 with Android NDK r9.

0. Download Android NDK

The latest version of Android NDK can be downloaded at Android NDK website. At the time of writing, the newest version is NDK r9. Note that the website provides both current and legacy toolchains. We only need the current toolchain to compile ffmpeg.

After download NDK, simply decompress the archive. Note that we’ll use $NDK to represent the root path of the decompressed NDK.

1. Download ffmpeg source code

FFMPEG source code can be downloaded from the ffmpeg website. The latest stable release is 2.0.1. Download the source code and decompress it to $NDK/sources folder. We’ll discuss about the reason for doing this later.

2. Update configure file

Open ffmpeg-2.0.1/configure file with a text editor, and locate the following lines.

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'

LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'

SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'

SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

This cause ffmpeg shared libraries to be compiled to libavcodec.so.<version> (e.g. libavcodec.so.55), which is not compatible with Android build system. Therefore we’ll need to replace the above lines with the following lines.

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'

LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'

SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'

SLIB_INSTALL_LINKS='$(SLIBNAME)'

3. Build ffmpeg

Copy the following text to a text editor and save it as build_android.sh.

#!/bin/bash

NDK=$HOME/Desktop/adt/android-ndk-r9

SYSROOT=$NDK/platforms/android-9/arch-arm/

TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64

function build_one

{

./configure 

    --prefix=$PREFIX 

    --enable-shared 

    --disable-static 

    --disable-doc 

    --disable-ffmpeg 

    --disable-ffplay 

    --disable-ffprobe 

    --disable-ffserver 

    --disable-avdevice 

    --disable-doc 

    --disable-symver 

    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- 

    --target-os=linux 

    --arch=arm 

    --enable-cross-compile 

    --sysroot=$SYSROOT 

    --extra-cflags="-Os -fpic $ADDI_CFLAGS" 

    --extra-ldflags="$ADDI_LDFLAGS" 

    $ADDITIONAL_CONFIGURE_FLAG

make clean

make

make install

}

CPU=arm

PREFIX=$(pwd)/android/$CPU 

ADDI_CFLAGS="-marm"

build_one

We disabled static library and enabled shared library. Note that the build script is not optimized for a particular CPU. One should refer to ffmpeg documentation for detailed information about available configure options.

Once the file is saved, make sure the script is executable by the command below,

sudo chmod +x build_android.sh

Then execute the script by the command,

./build_android.sh

4. Build Output

The build can take a while to finish depending on your computer speed. Once it’s done, you should be able to find a folder $NDK/sources/ffmpeg-2.0.1/android, which contains arm/lib and arm/include folders.

The arm/lib folder contains the shared libraries, while arm/include folder contains the header files for libavcodec, libavformat, libavfilter, libavutil, libswscale etc.

Note that the arm/lib folder contains both the library files (e.g.: libavcodec-55.so) and symbolic links (e.g.: libavcodec.so) to them. We can remove the symbolic links to avoid confusion.

5. Make ffmpeg Libraries available for Your Projects

Now we’ve compiled the ffmpeg libraries and ready to use them. Android NDK allows us to reuse a compiled module through the import-module build command.

The reason we built our ffmpeg source code under $NDK/sources folder is that NDK build system will search for directories under this path for external modules automatically. To declare the ffmpeg libraries as reusable modules, we’ll need to add a file named $NDK/sources/ffmpeg-2.0.1/android/arm/Android.mk with the following content,

LOCAL_PATH:= $(call my-dir)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavcodec

LOCAL_SRC_FILES:= lib/libavcodec-55.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavformat

LOCAL_SRC_FILES:= lib/libavformat-55.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libswscale

LOCAL_SRC_FILES:= lib/libswscale-2.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavutil

LOCAL_SRC_FILES:= lib/libavutil-52.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libavfilter

LOCAL_SRC_FILES:= lib/libavfilter-3.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

 

include $(CLEAR_VARS)

LOCAL_MODULE:= libwsresample

LOCAL_SRC_FILES:= lib/libswresample-0.so

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)

Below is an example of how we can use the libraries in a Android project’s jni/Android.mk file,

LOCAL_PATH := $(call my-dir)

 

include $(CLEAR_VARS)

 

LOCAL_MODULE    := tutorial03

LOCAL_SRC_FILES := tutorial03.c

LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid

LOCAL_SHARED_LIBRARIES := libavformat libavcodec libswscale libavutil

 

include $(BUILD_SHARED_LIBRARY)

$(call import-module,ffmpeg-2.0.1/android/arm)

Note that we called import-module with the relative path to $NDK/sources for the build system to locate the reusable ffmpeg libraries.

For real examples to how to use the ffmpeg libraries in Android app, please refer to my github repo of android-ffmpeg-tutorial.

Android Animation Playback: Inline Animation with TextView

TextView allows us to attach and detach markup objects to a range of text using Spannable. We can use this to implement inline animation within TextView.

0. AnimatedImageSpan

We implement a AnimatedImageSpan, which extends DynamicDrawableSpan. The object will accept a set of image frames and keep track of the frame to display. A handler and a runnable are implemented to update the frame to display.

Note that AnimatedImageSpan doesn’t update the screen by itself. It calls AnimatedImageUpdateHandler.updateFrame to refresh the entire TextView, which subsequently redraw the AnimatedImageSpan with new image.

public class AnimatedImageSpan extends DynamicDrawableSpan {

    private AnimationAssetsSet mGifAssets;

    private int mCurrentFrameIdx;

    private Context mContext;

    

    private SimpleImageMemCache mImageCache;

    private AnimatedImageUpdateHandler mImageUpdater;

    

    private final Handler handler = new Handler();

    

    public AnimatedImageSpan(Context context) {

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

    

    public void setAnimationAssets(AnimationAssetsSet pGifAssets) {

        mGifAssets = pGifAssets;

    }

    

    private Runnable mRunnable;

    private int mPlaybackTimes;

    private boolean mPlaying;

    public void playGif(final AnimationSettings pGifSettings, AnimatedImageUpdateHandler pListener) {

        mPlaying = true;

        mImageUpdater = pListener;

        mPlaybackTimes = 0;

        mRunnable = new Runnable() {

            public void run() {

                mCurrentFrameIdx = (mCurrentFrameIdx + 1)%mGifAssets.getNumOfFrames();

//                Logger.d(this, "current frame " + mCurrentFrameIdx);

                handler.postDelayed(this, pGifSettings.mDelay);

                if (null != mImageUpdater) {

//                    Logger.d(this, "update frame using listener " + mImageUpdater.getId());

                    mImageUpdater.updateFrame();

                }

                if (mCurrentFrameIdx == mGifAssets.getNumOfFrames() - 1) {

                    if (pGifSettings.mPlaybackTimes == 0) {

                        //repeat forever

                    } else {

                        mPlaybackTimes++;

                        if (mPlaybackTimes == pGifSettings.mPlaybackTimes) {

                            stopRendering();

                        }

                    }

                }

            }

        };

        handler.post(mRunnable);

    }

    

    public boolean isPlaying() {

        return mPlaying;

    }

    

    public void stopRendering() {

        handler.removeCallbacks(mRunnable);

        mPlaying = false;

    }

    

    @Override

    public Drawable getDrawable() {

        Bitmap bitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(mCurrentFrameIdx));

        BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);

        int width = drawable.getIntrinsicWidth();

        int height = drawable.getIntrinsicHeight();

        drawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0);

        return drawable;

    }

 

    @Override

    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {

//        Logger.d(this, "draw " + mCurrentFrameIdx);

        Drawable b = getDrawable();

        canvas.save();

 

        int transY = bottom - b.getBounds().bottom;

        if (mVerticalAlignment == ALIGN_BASELINE) {

            transY -= paint.getFontMetricsInt().descent;

        }

 

        canvas.translate(x, transY);

        b.draw(canvas);

        canvas.restore();

    }

}

1. Detect Click Events

We want the animation to start playing when the area is clicked, therefore detecting clicking is necessary. We’ll extend ClickableSpan as below.

private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

When the click event is detected, we start the animation playback, if it’s clicked while the animation is playing, we stop the playback.

2. MyTextView

We glue everything together in the MyTextView as below.

public class MyTextView extends TextView {

    private SpannableStringBuilder mSb = new SpannableStringBuilder();

    private String dummyText = "dummy " + System.currentTimeMillis();

    private Context mContext;

    private SimpleImageMemCache mImageCache;

    private ArrayList<AnimatedImageSpan> mAnimatedImages = new ArrayList<AnimatedImageSpan>();

 

    public MyTextView(Context context) {

        super(context);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs) {

        super(context, attrs);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs, int defStyle) {

        super(context, attrs, defStyle);

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

 

    @Override

    protected void onDetachedFromWindow() {

        super.onDetachedFromWindow();

        Log.d(this.getClass().getName(), "onDetachedFromWindow ");

        for (AnimatedImageSpan ais : mAnimatedImages) {

            Log.d(this.getClass().getName(), "animation playing " + ais.isPlaying());

            if (ais.isPlaying()) {

                ais.stopRendering();

            }

        }

        mAnimatedImages.clear();

        mSb.clearSpans();

        mSb.clear();

    }

    

    public void appendText(String pStr) {

        mSb.append(pStr);

    }

    

    public void appendAnimation(AnimationAssetsSet pAsset, AnimationSettings pSettings) {

        mSb.append(dummyText);

        AnimatedImageSpan ais = new AnimatedImageSpan(mContext);

        ais.setImageCache(mImageCache);

        ais.setAnimationAssets(pAsset);

        mSb.setSpan(ais, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        AnimationClickableSpan clickSpan = new AnimationClickableSpan(this, ais, pSettings);

        mSb.setSpan(clickSpan, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        mAnimatedImages.add(ais);

    }

    

    public void finishAddingContent() {

        this.setText(mSb);

        this.setMovementMethod(LinkMovementMethod.getInstance());

    }

    

    private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

}

3. The Complete Source Code

The complete source code can found at github repo.

Android Animation Playback: Frame Animation

Android provides AnimationDrawable object to create frame animation as a series of Drawable objects. The details can be found at Drawable Animation guideline at http://developer.android.com/guide/topics/graphics/drawable-animation.html.

This post discusses a different approach where we extends an ImageView and update the image displayed on the ImageView to render an animation.

0. Decode Animation Frame

Decoding frames is relatively computation intensive and therefore it’s better to do it in a separate thread. This can be implemented in code below.

class MyThread extends Thread {

    boolean mIsPlayingGif;

    AnimationSettings mGifSettings;

    MyThread(AnimationSettings pGifSettings) {

        mIsPlayingGif = true;

        mGifSettings = AnimationSettings.newCopy(pGifSettings);

    }

    @Override

    public void run() {

        int repetitionCounter = 0;

        do {

            for (int i = 0; i < mGifAssets.getNumOfFrames(); ++i) {

                if (!mIsPlayingGif) {

                    break;

                }

                Log.d(this.getName(), FrameAnimationView.this.getWidth()

                        + ":" + FrameAnimationView.this.getHeight());

                switch (mLoadMethod) {

                case ASSETS:

                    mTmpBitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(i));

                    break;

                case FILES:

                    mTmpBitmap = mImageCache.loadBitmap(mGifFrames.get(i));

                    break;

                case RESOURCES:

                    //TODO

                    break;

                }

                mHandler.post(mUpdateResults);

                try {

                    Thread.sleep(mGifSettings.mDelay);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

            if(0 != mGifSettings.mPlaybackTimes) {

                repetitionCounter++;

            }

        } while (mIsPlayingGif && repetitionCounter <= mGifSettings.mPlaybackTimes);

    }

}

1. Update Frame

Update the ImageView is required to be done in UI thread. Therefore we need to post a Runnable to a Handler object. The runnable is as below.

final Runnable mUpdateResults = new Runnable() {

    public void run() {

        if (mTmpBitmap != null && !mTmpBitmap.isRecycled()) {

            FrameAnimationView.this.setImageBitmap(mTmpBitmap);

        }

    }

};

2. Test the Animation

To test the animation, we can declare FrameAnimationView in an XML layout as below.

<;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".FrameAnimationActivity" >;

    <roman10.tutorial.frameanimation.FrameAnimationView

        android:id="@+id/gifPreviewOne"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_marginTop="100dp" />;

</RelativeLayout>

Suppose we have all frames of an animation under assets/1/ folder, to test the animation, we can code an activity as below.

public class FrameAnimationActivity extends Activity {

    private FrameAnimationView mFrameAniView;

    private SimpleImageMemCache mImageCache;

    private Context mContext;

    

    public int convertDpToPixel(int dp) {

        Resources r = mContext.getResources();

        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));

    }

    

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        mContext = this;

        setContentView(R.layout.activity_frame_animation);

        mFrameAniView = (FrameAnimationView) this.findViewById(R.id.gifPreviewOne);

        mImageCache = new SimpleImageMemCache(0.20f, convertDpToPixel(320), convertDpToPixel(200));

        mFrameAniView.setImageCache(mImageCache);

        mFrameAniView.setAnimationAssets(new AnimationAssetsSet(this, "1"));

    }

 

    @Override

    public void onResume() {

        super.onResume();

        mFrameAniView.playGif(new AnimationSettings());

    }

 

    @Override

    public void onPause() {

        super.onPause();

        mFrameAniView.stopRendering();

    }

    

    @Override

    public void onDestroy() {

        super.onDestroy();

        mFrameAniView.clearBitmap();

        mImageCache.clearCache();

    }

}

3. The Complete Source Code

The complete source code can be found at github repo. The code includes a simple image memory cache and a few bitmap utilities to optimize image decoding.

Android Animation Playback: Display GIF Animation in WebView

Android doesn’t support GIF natively. However, there’re a few different approaches to display GIF-like animation. In this and subsequently three blogs, we’ll cover two different approaches to display animation, including WebView and Frame Animation. We’ll start with GIF Animation playback in WebView.

The idea of using WebView to display GIF is simply embed the GIF into a few lines of HTML code as below.

<html>

    <head>

    <style type='text/css'>body{margin:auto auto;text-align:center;} img{width:100%25;} </style>

    </head>

    <body>

    <img src="1.gif" width="100%" />

    </body>

</html>

We can create a custom view which extends the WebView to display GIF animation as below.

public class GifWebView extends WebView {

    

    public GifWebView(Context context) {

        super(context);

    }

    

    public GifWebView(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

        

    public void setGifAssetPath(String pPath) {

        String baseUrl = pPath.substring(0, pPath.lastIndexOf("/") + 1);

        String fileName = pPath.substring(pPath.lastIndexOf("/")+1);

        StringBuilder strBuilder = new StringBuilder();

        strBuilder.append("<html><head><style type='text/css'>body{margin:auto auto;text-align:center;} img{width:100%25;} </style>");

        strBuilder.append("</head><body>");

        strBuilder.append("<img src="" + fileName + "" width="100%" /></body></html>");

        String data = strBuilder.toString();

        Log.d(this.getClass().getName(), "data: " + data);

        Log.d(this.getClass().getName(), "base url: " + baseUrl);

        Log.d(this.getClass().getName(), "file name: " + fileName);

        loadDataWithBaseURL(baseUrl, data, "text/html", "utf-8", null);

    }

}

We can declare a XML layout with the custom view.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".GifWebviewDisplayActivity" >

 

    <roman10.tutorial.gifinwebview.GifWebView

        android:id="@+id/gif_view"

        android:layout_height="match_parent"

        android:layout_width="match_parent">

    </roman10.tutorial.gifinwebview.GifWebView>

 

</RelativeLayout>

Suppose we have an gif animation file 1.gif under assets folder, we can display the gif file in an activity as below.

public class GifWebviewDisplayActivity extends Activity {

    private GifWebView gifView;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_gif_webview_display);

        gifView = (GifWebView) findViewById(R.id.gif_view);

        gifView.setGifAssetPath("file:///android_asset/1.gif");

    }

}

The complete source code can be found at github repo.

References:

1. WebView Android doc: http://developer.android.com/reference/android/webkit/WebView.html