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
-
#include <libavcodec/avcodec.h>
-
#include <libavformat/avformat.h>
-
#include <libswscale/swscale.h>
-
#include <libavutil/pixfmt.h>
-
-
#include <stdio.h>
-
#include <wchar.h>
-
-
#include <jni.h>
-
-
/*for android logs*/
-
#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);
}