Chinaunix首页 | 论坛 | 博客
  • 博客访问: 7775227
  • 博文数量: 701
  • 博客积分: 2150
  • 博客等级: 上尉
  • 技术积分: 13233
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-29 16:28
个人简介

天行健,君子以自强不息!

文章分类

全部博文(701)

文章存档

2019年(2)

2018年(12)

2017年(76)

2016年(120)

2015年(178)

2014年(129)

2013年(123)

2012年(61)

分类: Android平台

2015-12-21 22:56:20

android-ffmpeg-tutorial01的源码分析

1. java的入口函数

android APK的Java入口函数位于:
app->src->java->roman10.tutorial.android_ffmpeg_tutorial01。
在这个文件目录下有两个java程序文件,分别是:
MainActivity.java
Utils.java
其中,MainActivity.java是Java的入口程序源文件。

它的主要函数如下
// MainActivity.java
package roman10.tutorial.android_ffmpeg_tutorial01;
import ...


public class MainActivity extends Activity {
private static final String FRAME_DUMP_FOLDER_PATH = Environment.getExternalStorageDirectory() 
+ File.separator + "android-ffmpeg-tutorial01";
private EditText mEditNumOfFrames;
private ViewPager mViewPagerFrames;

@Override
protected void onCreate(Bundle savedInstanceState) { 
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
            //create directory for the tutorial
            //copy input video file from assets folder to directory
        }

private void displayDumpedFrames(){ ... }

private static class DumpFrameTask extends AsyncTask { ... }

private void saveFrameToPath(Bitmap bitmap, String pPath) { ... }

private static class VideoFrameAdapter extends PagerAdapter { ... }

private static native int naMain(MainActivity pObject, String pVideoFileName, int pNumOfFrames);

        static {
       System.loadLibrary("avutil-52");
            System.loadLibrary("avcodec-55");
            System.loadLibrary("avformat-55");
            System.loadLibrary("swscale-2");
       System.loadLibrary("tutorial01");
    }
}


代码解析:
1.1 onCreate(Bundle)方法
Activity的子类MainActivity实例创建后,onCreate(Bundle)方法将会被调用。
它的主体代码结构如下:
protected void onCreate(Bundle savedInstanceState) { 
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);


            //create directory for the tutorial
            ...

            //copy input video file from assets folder to directory
            ...
        }


activity创建后,它需要获取并管理属于自己的用户界面。
获取activity的用户界面,调用的方法是:
  setContentView(int layoutResID); 

通过传入布局的资源ID参数"R.layout.activity_main",该方法生成指定布局的视图并将其放置在屏幕上。
布局视图生成后,布局文件包含的组件也随之以各自的属性定义完成实例化。

布局是一种资源。
资源是应用中,非代码形式的内容,比如图像文件,音频文件以及XML文件等。
项目的所有资源文件都存放在目录res的子目录下。
通过androidStudio可以看到,布局activity_main.xml资源文件位于res/layout目录下。
包含字符串资源的string文件存放在res/values/目录下。
可以使用资源ID在代码中获取相应的资源,activity_main.xml文件中定义的布局资源ID为R.layout.activity_main。

然后是生成文件的保存路径:
    //create directory for the tutorial
    File dumpFolder = new File(FRAME_DUMP_FOLDER_PATH);
    if (!dumpFolder.exists()) {
        dumpFolder.mkdirs();
    }

最后复制输入文件到工作目录,响应并检测用户的输入:
    //copy input video file from assets folder to directory
    Utils.copyAssets(this, "1.mp4", FRAME_DUMP_FOLDER_PATH);

    mEditNumOfFrames = (EditText) this.findViewById(R.id.editNumOfFrames);
    mViewPagerFrames = (ViewPager) this.findViewById(R.id.viewPagerFrames);

    Button btnStart = (Button) this.findViewById(R.id.buttonStart);
    btnStart.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //get number of frames to grab
            String numText = mEditNumOfFrames.getText().toString();
            int numOfFrames;
            try {
                numOfFrames = Integer.valueOf(numText);
            } catch (Exception e) {
                e.printStackTrace();
                numOfFrames = 5;
            }
            //limit number of frames to grab to 20
            if (numOfFrames > 20) {
                numOfFrames = 20;
            }


            //start processing using asynctask
            DumpFrameTask task = new DumpFrameTask(MainActivity.this, numOfFrames);
            task.execute();
        }
    });
可见代码的最后,当用户在输入栏中输入了帧数并点击了"start"按键,
将创建了AsyncTask的子类实例来处理。

1.2 AsyncTask子类
代码如下:
    private static class DumpFrameTask extends AsyncTask {
        int mlNumOfFrames;
        ProgressDialog mlDialog;
        MainActivity mlOuterAct;


        DumpFrameTask(MainActivity pContext, int pNumOfFrames) {
            mlNumOfFrames = pNumOfFrames;
            mlOuterAct = pContext;
        }


        @Override
        protected void onPreExecute() {
            mlDialog = ProgressDialog.show(mlOuterAct, 
                "Dump Frames","Processing..Wait.." , 
                false);
        }


        @Override
        protected Void doInBackground(Void... params) {
            naMain(mlOuterAct, 
                FRAME_DUMP_FOLDER_PATH + File.separator + "1.mp4", 
                mlNumOfFrames);
            return null;
        }


        @Override
        protected void onPostExecute(Void param) {
            if (null != mlDialog && mlDialog.isShowing()) {
                mlDialog.dismiss();
            }
            mlOuterAct.displayDumpedFrames();
        }
    }


AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,
在类中实现异步操作,并提供接口反馈当前异步执行的程度(可以通过接口实现UI进度更新),
最后反馈执行的结果给UI主线程.

使用过AsyncTask 的同学都知道一个异步加载数据最少要重写以下这两个方法:
. doInBackground(Params…) 后台执行,
  比较耗时的操作都可以放在这里。
  注意这里不能直接操作UI。
  此方法在后台线程执行,完成任务的主要工作,通常需要较长的时间。
  在执行过程中可以调用publicProgress(Progress…)来更新任务的进度。


. onPostExecute(Result)  相当于Handler 处理UI的方式,
  在这里面可以使用在doInBackground 得到的结果处理操作UI。 
  此方法在主线程执行,任务执行的结果作为此方法的参数返回


有必要的话你还得重写以下这三个方法,但不是必须的:
. onProgressUpdate(Progress…)   可以使用进度条增加用户体验度。 
  此方法在主线程执行,用于显示任务执行的进度。


. onPreExecute()        这里是最终用户调用Excute时的接口,
  当任务执行之前开始调用此方法,可以在这里显示进度对话框。


. onCancelled()        用户调用取消时,要做的操作


当AsyncTask子类被创建后,会调用它的构造函数实现成员变量的初始化:
        int mlNumOfFrames;
        MainActivity mlOuterAct;


        DumpFrameTask(MainActivity pContext, int pNumOfFrames) {
            mlNumOfFrames = pNumOfFrames;
            mlOuterAct = pContext;
        }
在本例中,重写三个方法:
doInBackground(Params…),  onPostExecute(Result), onPreExecute() ;


其中,
. onPreExecute(),  用于显示后台程序执行前用户对话框的提示;
. doInBackground(Params…), 它是调用后台ffmpeg,执行任务的地方;
  protected Void doInBackground(Void... params) {
      naMain(mlOuterAct, FRAME_DUMP_FOLDER_PATH + File.separator + "1.mp4", mlNumOfFrames);
      return null;
  }
可以看到,它调用了native接口"naMain",实现调用后台的C语言程序;


.  onPostExecute(Result), 它是显示后台任务执行完成后的结果信息,
   它调用了displayDumpedFrames()方法来在用户UI显示结果。


1.3 PagerAdapter的子类VideoFrameAdapter和displayDumpedFrames()方法
它们两者配合,在后台程序执行完成后,实现了一个提取图片的滑动效果。
详细可见《Android的PagerAdapter类简介》


1.4 native接口声明
private static native int naMain(MainActivity pObject, 
  String pVideoFileName, int pNumOfFrames);

它是Java通过native调用C/C++程序的接口。

1.5 外部的ndk编译过的库加载
    static {
    System.loadLibrary("avutil-52");
        System.loadLibrary("avcodec-55");
        System.loadLibrary("avformat-55");
        System.loadLibrary("swscale-2");
    System.loadLibrary("tutorial01");
    }
在本例中,使用了外部的动态库:
libavformat libavcodec libswcale libavutil libswresample
构建了jni库"tutorial01"。
详细可见《Android NDK学习》系列。

2. 定义在java类中的native方法对应的C语言实现。

定义在MainActivity.java类中naMain对应的C语言实现在
app->src->main->jni->tutorial01.c文件中

// tutorial01.c

  1. #include <libavcodec/avcodec.h>
  2. #include <libavformat/avformat.h>
  3. #include <libswscale/swscale.h>
  4. #include <libavutil/pixfmt.h>

  5. #include <stdio.h>
  6. #include <wchar.h>

  7. #include <jni.h>

  8. /*for android logs*/
  9. #include <android/log.h>



#define LOG_TAG "android-ffmpeg-tutorial01"
#define LOGI(...) __android_log_print(4, LOG_TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(6, LOG_TAG, __VA_ARGS__);

void SaveFrame(JNIEnv *pEnv, 
               jobject pObj, 
               jobject pBitmap, 
               int width, 
               int height, 
               int iFrame) 
{ ... }


jobject createBitmap(JNIEnv *pEnv, int pWidth, int pHeight) 
{ ... }


jint naMain(JNIEnv *pEnv, 
            jobject pObj, 
            jobject pMainAct, 
            jstring pFileName, 
            jint pNumOfFrames)
{ ... }




jint JNI_OnLoad(JavaVM* pVm, void* reserved) {
    JNIEnv* env;
    if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }


    JNINativeMethod nm[1];
    nm[0].name = "naMain";
    nm[0].signature = "(Lroman10/tutorial/android_ffmpeg_tutorial01/MainActivity;
                        Ljava/lang/String;I)I";
    nm[0].fnPtr = (void*)naMain;
    jclass cls = (*env)->FindClass(env, 
        "roman10/tutorial/android_ffmpeg_tutorial01/MainActivity");


    //Register methods with env->RegisterNatives.
    (*env)->RegisterNatives(env, cls, nm, 1);
    return JNI_VERSION_1_6;
}


这个文件主要包括两个部分:
jint JNI_OnLoad(): 当在系统中调用System.loadLibrary函数时,该函数会找到对应的动态库,
                   然后首先试图找到"JNI_OnLoad"函数,如果该函数存在,则调用它。
                   JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换。


jint naMain(): 它使用ffmpeg的SDK,实现了对输入文件,按指定帧开始做视频帧提取。
               void SaveFrame(), createBitmap()都是被它调用的。


2.1 JNI_OnLoad()方法
当Android的VM(Virtual Machine)执行到C组件(即*so档)里的System.loadLibrary()函数时,
首先会去执行C组件里的JNI_OnLoad()函数。
它的用途有二: 
. 告诉VM此C组件使用那一个JNI版本。
  如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。
  由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,
  例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。


. 由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),
  所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。


本例中详细的代码解释如下:
jint JNI_OnLoad(JavaVM* pVm, void* reserved) {
    JNIEnv* env;
    if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }


    JNINativeMethod nm[1];
    nm[0].name = "naMain";
    nm[0].signature = "(Lroman10/tutorial/android_ffmpeg_tutorial01/MainActivity;
                        Ljava/lang/String;I)I";
    nm[0].fnPtr = (void*)naMain;
    /*
     * 进行注册,只需要两步,
     *    首先FindClass,
     *    然后RegisterNatives
     */
    jclass cls = (*env)->FindClass(env, 
        "roman10/tutorial/android_ffmpeg_tutorial01/MainActivity");
    (*env)->RegisterNatives(env, cls, nm, 1);


    // 返回版本号,否则会出错。
    return JNI_VERSION_1_6;
}


NOTE:
nm[0].signature = "(Lroman10/tutorial/android_ffmpeg_tutorial01/MainActivity;
                    Ljava/lang/String;I)I";
是函数的签名,
可以通过以下命令获取:
$ cd app/src/main
$ javah -d jni -classpath ../../src/main/java/ roman10.tutorial.android_ffmpeg_tutorial01.MainActivity
命令执行成功后,会在jni目录下生成头文件:
roman10_tutorial_ffmpeg_tutorial01_MainActivity.h
查看头文件中的内容,可以得到函数的签名:
$cat jni/roman10_tutorial_android_ffmpeg_tutorial01_MainActivity.h 
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class roman10_tutorial_android_ffmpeg_tutorial01_MainActivity */


#ifndef _Included_roman10_tutorial_android_ffmpeg_tutorial01_MainActivity
#define _Included_roman10_tutorial_android_ffmpeg_tutorial01_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     roman10_tutorial_android_ffmpeg_tutorial01_MainActivity
 * Method:    naMain
 * Signature: (Lroman10/tutorial/android_ffmpeg_tutorial01/MainActivity;Ljava/lang/String;I)I
 */
JNIEXPORT jint JNICALL Java_roman10_tutorial_android_1ffmpeg_1tutorial01_MainActivity_naMain
  (JNIEnv *, jclass, jobject, jstring, jint);


#ifdef __cplusplus
}


如果要给入口函数添加更多的输入参数,需要对这个函数签名做修改,
如,想把入口函数修改成这样:
jint naMain(JNIEnv *pEnv, 
            jobject pObj, 
            jobject pMainAct, 
            jstring pFileName, 
            jint pNumOfFrames,
            jint pWidth,
            jint pHeight)
{ ... }


则函数签名要修改这样:
Signature: (Lroman10/tutorial/android_ffmpeg_tutorial01/MainActivity;Ljava/lang/String;III)I






2.2 naMain()方法
它使用ffmpeg的SDK,实现了对输入文件,按指定帧开始做视频帧提取。
void SaveFrame(), createBitmap()都是被它调用的。


至此,可以看到这个应用程序从用户界面获得了输入的帧参数,
当点击了"start"按键后,就触发了按键的响应事件,
通过JNI调用了基于ffmpeg SDK的C程序,在后台中对输入文件进行帧抽取。
最后,将调用的结果显示在了用户界面上。




3. 输入文件的读与输出文件的写

3.1 从assets中读入输入文件
相关的源码如下:
在AndroidMainifest.xml中添加外部存储读和写的权限:
/*
 * \File
 *    android-ffmpeg-tutorial-master\android-ffmpeg-tutorial01\AndroidManifest.xml
 */

   
    ...





/*
 * \File
 *   android-ffmpeg-tutorial-master\android-ffmpeg-tutorial01\src\roman10\tutorial\android_ffmpeg_tutorial01\MainActivity.java
 */
package roman10.tutorial.android_ffmpeg_tutorial01;


import ...
import android.os.Environment;


public class MainActivity extends Activity {
    // 使用Enviroment类,获得外部存储的路径,并拼接出存储目录的路径;
    private static final String FRAME_DUMP_FOLDER_PATH = Environment.getExternalStorageDirectory() 
                        + File.separator + "android-ffmpeg-tutorial01";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        
        // 生成存储目录
        File dumpFolder = new File(FRAME_DUMP_FOLDER_PATH);
        if (!dumpFolder.exists()) {
            dumpFolder.mkdirs();
        }
        
        // 复制本机项目中assets目录下的输入视频文件到android的指定外部存储目录
        Utils.copyAssets(this, "1.mp4", FRAME_DUMP_FOLDER_PATH);
        ...
    }
}


/*
 * \File
 *   android-ffmpeg-tutorial-master\android-ffmpeg-tutorial01\src\roman10\tutorial\android_ffmpeg_tutorial01\Utils.java
 */
package roman10.tutorial.android_ffmpeg_tutorial01;


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;


import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;


public class Utils {
    public static void copyAssets(Context pContext, String pAssetFilePath, String pDestDirPath) {
        /* 
         * 使用AssetManager类来对应用程序的原始资源文件进行访问
         * pContext.getAssets() 是用来获取AssetManager引用
         */
        AssetManager assetManager = pContext.getAssets();


        InputStream  in = null;
        OutputStream out = null;


        try {
            /* 
             * 以指定的访问模式打开文件并得到输入流
             */ 
            in = assetManager.open(pAssetFilePath);


            File outFile = new File(pDestDirPath, pAssetFilePath);
            out = new FileOutputStream(outFile);


            /* 把应用程序assets文件夹下的输入文件复制到Android系统外部存储的指定目录*/
            copyFile(in, out);
            
            /* 释放输入和输出资源 */
            in.close();
            in = null;


            out.flush();
            out.close();
            out = null;
        } catch(IOException e) {
            Log.e("tag", "Failed to copy asset file: " + pAssetFilePath, e);
        }       
    } // end of copyAssets
    
    /* 以字节流的方式进行资源文件的复制 */
    private static void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024*16];
        int read;
        while((read = in.read(buffer)) != -1){
            out.write(buffer, 0, read);
        }
    }
}


通过上面的代码,就将应用程序assets文件夹下的输入资源文件复制到了android外部存储上的指定目录;




3.2 将数据写到输出文件
/*
 * \File
 *   android-ffmpeg-tutorial-master\android-ffmpeg-tutorial01\src\roman10\tutorial\android_ffmpeg_tutorial01\jni\tutorial01.c
 */
jint naMain(JNIEnv *pEnv, 
            jobject pObj, 
            jobject pMainAct, 
            jstring pFileName, 
            jint pNumOfFrames) {


    jobject  bitmap;
    void*    buffer;


    //create a bitmap as the buffer for pFrameRGBA
    bitmap = createBitmap(pEnv, pCodecCtx->width, pCodecCtx->height);


    // lock the bitmap
    if (AndroidBitmap_lockPixels(pEnv, bitmap, &buffer) < 0)
        return -1;
    ...


    // Assign appropriate parts of bitmap to image planes in pFrameRGBA
    // Note that pFrameRGBA is an AVFrame, but AVFrame is a superset of AVPicture
    avpicture_fill((AVPicture *)pFrameRGBA, 
                   buffer, 
                   AV_PIX_FMT_RGBA,
  pCodecCtx->width, 
                   pCodecCtx->height);


    SaveFrame(pEnv, pMainAct, bitmap, pCodecCtx->width, pCodecCtx->height, i);
    ...


    //unlock the bitmap
    AndroidBitmap_unlockPixels(pEnv, bitmap);
}




/*
 * 使用 Android图像处理的Bitmap类,实现对图像文件的保存
 */
jobject createBitmap(JNIEnv *pEnv, int pWidth, int pHeight) {
    int i;


    //get Bitmap class and createBitmap method ID
    jclass javaBitmapClass = (jclass)(*pEnv)->FindClass(pEnv, "android/graphics/Bitmap");
    jmethodID mid = (*pEnv)->GetStaticMethodID(pEnv, javaBitmapClass, "createBitmap", 
                             "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");


    //create Bitmap.Config
    //reference:
    const wchar_t* configName = L"ARGB_8888";
    int len = wcslen(configName);
    jstring jConfigName;
    if (sizeof(wchar_t) != sizeof(jchar)) {
        //wchar_t is defined as different length than jchar(2 bytes)
        jchar* str = (jchar*)malloc((len+1)*sizeof(jchar));
        for (i = 0; i < len; ++i) {
            str[i] = (jchar)configName[i];
        }


        str[len] = 0;
        jConfigName = (*pEnv)->NewString(pEnv, (const jchar*)str, len);
    } else {
        //wchar_t is defined same length as jchar(2 bytes)
        jConfigName = (*pEnv)->NewString(pEnv, (const jchar*)configName, len);
    }


    jclass bitmapConfigClass = (*pEnv)->FindClass(pEnv, "android/graphics/Bitmap$Config");
    jobject javaBitmapConfig = (*pEnv)->CallStaticObjectMethod(pEnv, bitmapConfigClass,
            *pEnv)->GetStaticMethodID(pEnv, bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;"), jConfigName);
    
    //create the bitmap
    return (*pEnv)->CallStaticObjectMethod(pEnv, javaBitmapClass, mid, pWidth, pHeight, javaBitmapConfig);
}




void SaveFrame(JNIEnv *pEnv, jobject pObj, jobject pBitmap, int width, int height, int iFrame) {
    char szFilename[200];
    jmethodID sSaveFrameMID;
    jclass mainActCls;
    
    sprintf(szFilename, "/sdcard/android-ffmpeg-tutorial01/frame%d.jpg", iFrame);
    mainActCls = (*pEnv)->GetObjectClass(pEnv, pObj);
    sSaveFrameMID = (*pEnv)->GetMethodID(pEnv, mainActCls, "saveFrameToPath", "(Landroid/graphics/Bitmap;Ljava/lang/String;)V");

    LOGI("call java method to save frame %d", iFrame);
    jstring filePath = (*pEnv)->NewStringUTF(pEnv, szFilename);
    (*pEnv)->CallVoidMethod(pEnv, pObj, sSaveFrameMID, pBitmap, filePath);
    LOGI("call java method to save frame %d done", iFrame);
}
阅读(2339) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~