Android入门使用FFmepg添加水印

集成FFmepg

本文通过参考这篇博客,然后自己加工而成,原文地址:http://www.jianshu.com/p/e0c32c8b0ebc

本文的demo:https://github.com/reverseAndroid/WaterMark

由于项目中的提出的需求,所以,才有了这篇文章。好,话不多说,直奔主题吧。(此文推荐菜鸟程序猿,如果是大神可以直接略过)

首先我们需要去官网下载FFmepg库。下载地址:http://www.ffmpeg.org/download.html。下载好之后,我们需要来编译一个libffmpeg.so文件。网上说了很多编译的方法。如果自己不想编译,可以用博主项目中的。(如果自己有兴趣想编译的这是地址:http://www.jianshu.com/p/dfd0de17601c

接下来下载好libffmpeg.so文件后,在Android Studio中将这一栏切换成"Project"目录。

image

然后在main目录下新建一个JNI文件夹如下图

image

会弹出一个窗口,不要管直接finish;

接着把下载好的libffmpeg.so文件放到jni文件夹下。

image

此时这个文件全是乱码,不要在意,继续往下走。然后新建一个java类,FFmpegKit.java,写入下面这些代码:


package com.xjx.demo.utils;

import android.os.AsyncTask;

/**

* Created by Dell on 2017/11/17.

*/

public class FFmpegKit {

public interface KitInterface{

void onStart();

void onProgress(int progress);

void onEnd(int result);

}

static{

System.loadLibrary("ffmpeg");

System.loadLibrary("ffmpeginvoke");

}

public static int execute(String[] commands){

return run(commands);

}

public static void execute(String[] commands, final KitInterface kitIntenrface){

new AsyncTask(){

@Override

protected void onPreExecute() {

if(kitIntenrface != null){

kitIntenrface.onStart();

}

}

@Override

protected Integer doInBackground(String[]... params) {

return run(params[0]);

}

@Override

protected void onProgressUpdate(Integer... values) {

if(kitIntenrface != null){

kitIntenrface.onProgress(values[0]);

}

}

@Override

protected void onPostExecute(Integer integer) {

if(kitIntenrface != null){

kitIntenrface.onEnd(integer);

}

}

}.execute(commands);

}

public native static int run(String[] commands);

}

对于FFmpegKit.java这个类就是和FFmpeg这个库进行交互用的,说下个人的看法,这个我个人觉得这个类在某些时候可以理解成这就是个JNI,我们通过调用JNI来和FFmpeg交互,其实就是靠这个类去调用本文中下面说的那些c++的文件(作者比较菜只能这样理解了,大神可以纠正,轻喷)。

写入完代码,我们会发现public native static int run(String[] commands);其中的"run"一直报红,但是却不报错,也没法解决,没事,不用管这东西,他就是这样,继续往下走。

接下来,有两种方法1.打开"cmd";2.在Android Studio中打开Terminal

image

之后切换到项目的"src/main/java"目录下,然后输入"javah com.xjx.demo.utils.FFmpegKit"。(这里的"com.xjx.demo.utils"需要换成你的FFmpegKit这个类的完整包名),输入完后回车,这时会停顿几秒,然后,你的项目中就会自动生成两个c++的文件。

image

这两个的文件名和你刚才输入的包名是一样的。(这里作者有些疑问,别的博客都是只生成一个"com_xjx_demo_utils_FFmpegKit.h"这个文件,但是我不管试多少次都会生成后面那个文件,不是知道你们是不是这样的,然后作者没办法了,只能不管这俩基友,把这俩文件一起移动到刚才建的jni文件夹下)

image

接下来把下载好的FFmpeg源码文件打开,复制ffmpeg.h, ffmpeg.c, ffmpeg_opt.c, ffmpeg_filter.c,cmdutils.c, cmdutils.h, cmdutils_common_opts.h到jni目录下。(这里要注意两个坑,第一源文件中并没有一个"logjam.h"这样的文件夹,但是那个博客却都有这个文件,而且我们如果不加的话,后面集成会有问题,所以我们新建一个,这个文件是一个c++的文件,第二博主照着复制了,最后却编译失败了,最后找到原因,他的项目中的这些文件,和他博客中说的更改的地方貌似少了很多,几乎可以说是他的文件改动了许多地方,但是他只说了改动某几个地方,不知道是博主自己水平问题还是什么,反正死活不行。后来博主自己花费了很长时间把这些文件都改好了,大家可以下载我的项目,直接把这几个文件导入进去)

建好"logjam.h"文件后,添加代码


#ifndefLOGJAM_H

#defineLOGJAM_H

#include

#define LOGTAG"FFmpegLog"

#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOGTAG, __VA_ARGS__)

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , LOGTAG, __VA_ARGS__)

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO  , LOGTAG, __VA_ARGS__)

#define LOGW(...) __android_log_print(ANDROID_LOG_WARN  , LOGTAG, __VA_ARGS__)

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR  , LOGTAG, __VA_ARGS__)

#endif

然后jni的文件是这样的

image

接下来在jni文件夹下新建一个c++文件,

image
image

文件名要和com_xjx_demo_utils_FFmpegKit.h一样,文件类型选择".c"类型。

然后把


#include

#include"com_xjx_demo_utils_FFmpegKit.h"

#include"ffmpeg.h"

JNIEXPORT jint JNICALL Java_com_xjx_demo_utils_FFmpegKit_run

(JNIEnv *env, jclass obj, jobjectArray commands){

//FFmpeg av_log() callback

intargc = (*env)->GetArrayLength(env, commands);

char*argv[argc];

LOGD("Kit argc %d\n", argc);

inti;

for(i =0; i < argc; i++) {

jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);

argv[i] = (char*) (*env)->GetStringUTFChars(env, js,0);

LOGD("Kit argv %s\n", argv[i]);

}

returnrun(argc, argv);

}

这段代码复制进去。

其中要注意的

image

接下来在jni文件夹下新建一个"Android.mk"文件,这里会让选择用什么样的格式查看,选择Text格式就行,只是为了方便我们编辑。然后


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ffmpeg

LOCAL_SRC_FILES := libffmpeg.so

include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := ffmpeginvoke

LOCAL_SRC_FILES := com_xjx_demo_utils_FFmpegKit.c ffmpeg.c ffmpeg_opt.c cmdutils.c ffmpeg_filter.c

LOCAL_C_INCLUDES := C:\Users\Dell\Desktop\ffmpeg-3.2.4

LOCAL_LDLIBS := -llog -lz -ldl

LOCAL_SHARED_LIBRARIES := ffmpeg

include $(BUILD_SHARED_LIBRARY)

image

同样的,再次在jni文件夹下Application.mk文件,然后给里面编写


APP_ABI := armeabi-v7a

APP_PLATFORM := android-17

APP_BUILD_SCRIPT := Android.mk

其中APP_ABI的值是代表支持的cpu类型。要支持多种cpu的话,可以把类型写一起用空格隔开。就像这样"APP_ABI := armeabi-v7a x86"编辑完成后,修改高深的jni就告一段落了。

接下来我们需要打开"app下的build.gradle"文件,添加


sourceSets {

main {

jniLibs.srcDirs = ['src/main/libs']

jni.srcDirs=[]

}

}

完成后打开mainfest文件添加


<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="27"/>

这里的sdk版本号和你的"build.gradle"文件中的保持一致。(这里是作者踩过的坑)

添加完成后在Terminal或者"cmd"中,把当前目录切换到"src\main\jni"目录下,然后输入"ndk-build"然后等上几秒,当命令行出现这两句时

image

我们来看看文件夹,

image

当出现文件夹生成这样几个文件后,恭喜你,你就成功了(总算完成集成了,接下来就剩下调用牛比哄哄的jni了)。

如果输入"ndk-build"时没有生成相应的文件,而是显示异常了,那就再仔细核对下上述的步骤,看看哪一步没有做对。

如果没什么问题,我们就已经集成好FFmepg了,现在就是使用他的时候了,这里作者只给出演示添加水印的activity:

package com.demo.watermark;

import android.content.Intent;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;

import com.demo.watermark.utils.FFmpegKit;
import com.demo.watermark.utils.ThreadPoolUtils;
import com.demo.watermark.view.WaitDialog;

import java.io.File;
import java.util.Date;

public class PlayActivity extends AppCompatActivity {

    private static final String TAG = "PlayActivity";
    private VideoView mVideoView;

    private WaitDialog mWaitDialog;
    private Handler mHandler;
    private String videoAbsolutePath;
    private String textAbsolutePath;
    private String outputUrl;
    private File videoFile;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_play);
        initData();
        initView();
    }

    private void initData() {
        Intent intent = getIntent();
        videoAbsolutePath = intent.getStringExtra("path");
        textAbsolutePath = intent.getStringExtra("imagePath");
        //获取路径
        String path = Environment.getExternalStorageDirectory() + File.separator + "video1";
        //定义文件名
        String fileName = new Date().getTime() + ".mp4";
        videoFile = new File(path, fileName);
        //文件夹不存在
        if (!videoFile.getParentFile().exists()) {
            videoFile.getParentFile().mkdirs();
        }
        outputUrl = videoFile.getAbsolutePath();
    }

    private void initView() {
        mVideoView = (VideoView) findViewById(R.id.view);
        //将视频和图片水印合成
        makeVideo();
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 0) {
                    //视频播放的控制
                    playVideo();
                }
            }
        };
    }

    private void makeVideo() {
        mWaitDialog = new WaitDialog(PlayActivity.this, R.style.waitDialog);
        mWaitDialog.setWaitText("正在生成视频...请不要退出(大概一分钟左右)");
        mWaitDialog.show();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String[] commands = new String[12];
                commands[0] = "ffmpeg";
                commands[1] = "-i";
                commands[2] = videoAbsolutePath;
                commands[3] = "-i";
                commands[4] = textAbsolutePath;
                commands[5] = "-filter_complex";
                //水印位置:(x,y)=(10,10)<=(left,top)距离左侧、底部各多少像素
                commands[6]="overlay=main_w-overlay_w+75:main_h-overlay_h-1";
                commands[7] = "-b";        //这个是为了使视频质量不掉的太严重
                commands[8] = "2048k";     //这个是设置生成视频的大小
                commands[9] = "-codec:a";
                commands[10] = "copy";
                commands[11] = outputUrl;
                FFmpegKit.execute(commands, new FFmpegKit.KitInterface() {
                    @Override
                    public void onStart() {
                        Log.e("FFmpegLog LOGCAT", "FFmpeg 命令行开始执行了...");
                    }

                    @Override
                    public void onProgress(int progress) {
                        Log.e("FFmpegLog LOGCAT", "done com" + "FFmpeg 命令行执行进度..." + progress);
                    }

                    @Override
                    public void onEnd(int result) {
                        Log.e("FFmpegLog LOGCAT", "FFmpeg 命令行执行完成...");
                        Message msg = new Message();
                        msg.what = 0;
                        mHandler.sendMessage(msg);
                        mWaitDialog.dismiss();
                    }
                });
            }
        };
        ThreadPoolUtils.execute(runnable);
    }

    private void playVideo() {
        mVideoView.setVideoPath(outputUrl);
        mVideoView.setMediaController(new MediaController(PlayActivity.this));
        if (!mVideoView.isPlaying()) {
            mVideoView.start();
        } else {
            mVideoView.pause();
        }
    }
}

上面代码中,makeVideo()这个方法就是使用FFmepg的方法,由于FFmepg库是需要使用命令行操作的,所以,我们的方法里加了一个数组,用来编写命令行,具体的命令行怎么操作和输入,大家只能百度去找了。这里推荐一篇博客,作者是参考这个改的:http://blog.csdn.net/weiyuefei/article/details/52808890(这里要提示一点,如果命令行输错的话,程序会崩溃,但是不会打印日志。)
好了,这篇文章,终于完成了,由于作者比较菜,所以文章写的烂,求大佬忽略就好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容