ffmpeg学习笔记-Linux下编译Android动态库

Android平台要使用ffmpeg就需要编译生成动态库,这里采用Ubuntu编译Android动态库

文件准备

要编译生成Android需要以下文件

  • NDK
  • ffmpeg源代码
NDK下载

NDK可以去Google下载,也可以在国内一些Android网站下载
这里推荐两个Android的下载网站
Android Studio 中文组
AndroidDevTools

ffmpeg

ffmpeg在其官网可以直接下载,不需要翻墙
官网下载地址
ffmpeg下载

配置环境

我这里下载的是android-ndk-r10e-linux-x86_64.zipffmpeg-2.6.9.tar.gz

NDK
  • 解压
    下载的NDK,Google下载的话是一个zip压缩包,其他地方下载可能是bin文件,其实都是压缩包
    zip解压缩:unzip android-ndk-r10e-linux-x86_64.zip
    bin解压:./android-ndk-r10e-linux-x86_64.bin

  • 配置环境变量
    vim ~/.bashrc
    在文件末尾加上,NDKROOT为ndk所在路径

export NDKROOT=/usr/ndk/android-ndk-r10e                                                                                                     
export PATH=$NDKROOT:$PATH

使配置的环境变量立即生效
source ~/.bashrc
使用ndk-build -v检查设置是否生效
如果输出类似下列语句,则代表配置成功

GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
ffmpeg

ffmpeg解压
tar -xzvf ffmpeg-2.6.9.tar.gz

编译ffmpeg

编写ffmpeg编译脚本,后缀名为.sh,这里我命名为build_android.sh

#!/bin/bash
make clean
export NDK=/usr/ndk/android-ndk-r10e
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"

./configure
--target-os=linux \
--prefix=$PREFIX \
--arch=arm \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install

如果在linux端不识别,那么可以使用dos2unix转换一下文件
注意,在编译脚本里不可有多余空格,否则会报一堆莫名其妙的错误
使用chmod 755 build_android.sh更改文件权限,使其可以执行
此时便可以使用./build_android.sh编译ffmpeg了

此时编译出来的动态库后缀名不对,那么就需要修改configure文件,使其生成的动态库符合标准
使用./configure --help可以查看如何配置configure文件

修改configure文件
将以下四句做修改

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

改为

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'

此时再编译就可以得到合格的so动态库了

在编译途中会生成一些.h.mak文件

编译完成生成android文件夹,生成的动态库和头文件都在这里

Android app测试(转码功能)

  • 创建Android项目

  • 建立jni文件夹,将include目录拷贝至jni目录下

  • 拷贝so动态库libavcodec-56.so libavdevice-56.so libavfilter-5.so libavformat-56.so libavutil-54.so libpostproc-53.so libswresample-1.so libswscale-3.so拷贝至jni目录

  • 编写Android.mk文件

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := libavcodec-56.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avdevice
LOCAL_SRC_FILES := libavdevice-56.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := libavfilter-5.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := libavformat-56.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := libavutil-54.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := libpostproc-53.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := libswresample-1.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := libswscale-3.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg_player
LOCAL_SRC_FILES := ffmpeg_player.c
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_LDLIBS := -llog
LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale
include $(BUILD_SHARED_LIBRARY)
  • 编写Application.mk文件
APP_ABI := armeabi
APP_PLATFORM := android-8
  • 实现头文件
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>

#include "com_cj5785_ffmpegplayer_VideoUtils.h"

//封装格式
#include "include/libavformat/avformat.h"
//解码
#include "include/libavcodec/avcodec.h"
//像素处理
#include "include/libswscale/swscale.h"

#define LOGI(FORMAT,...) __android_log_print(5,"cj5785",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(6,"cj5785",FORMAT,##__VA_ARGS__);

JNIEXPORT void JNICALL Java_com_cj5785_ffmpegplayer_VideoUtils_decode
  (JNIEnv *env, jclass jcls, jstring jstr_input, jstring jstr_output)
{
	//将jstring转化为cstr
	const char *input_cstr = (*env)->GetStringUTFChars(env, jstr_input, NULL);
	const char *output_cstr = (*env)->GetStringUTFChars(env, jstr_output, NULL);

	//1.注册组件
	av_register_all();

	//分装格式上下文
	AVFormatContext *pFormatCtx = avformat_alloc_context();
	//2.打开视频文件
	//AVInputFormat和AVDictionary在pFormatContext中已经包含
	if(avformat_open_input(&pFormatCtx, input_cstr, NULL, NULL) != 0)
	{
		LOGE("%s", "打开文件失败!");
		return;
	}

	//3.获取视频相关信息
	if(avformat_find_stream_info(pFormatCtx, NULL) < 0)
	{
		LOGE("%s", "获取视频信息失败!");
		return;
	}

	//视频解码
	int i = 0;
	int video_stream_index = -1;
	for (i = 0; i < pFormatCtx->nb_streams; i++) {
		//判断是否是视频流
		if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			video_stream_index = i;
			break;
		}
	}

	if (video_stream_index == -1)
	{
		LOGE("%s","找不到视频流\n");
		return;
	}
	//4.获取解码器
	AVCodecContext *pCodecCtx = pFormatCtx->streams[video_stream_index]->codec;
	AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
	if(pCodec == NULL)
	{
		LOGE("%s", "无法解码!");
		return;
	}

	//5.打开解码器
	if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
	{
		LOGE("%s", "解码失败!");
		return;
	}
	//输出视频信息
	LOGI("视频的文件格式:%s",pFormatCtx->iformat->name);
	LOGI("视频时长:%d", (pFormatCtx->duration)/1000000);
	LOGI("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
	LOGI("解码器的名称:%s",pCodec->name);

	//6.以帧为单位读取视频文件
	//编码数据 AVPacket初始化
	AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));
	//解码数据(像素数据) AVFrame初始化
	AVFrame *pFrame = av_frame_alloc();
	AVFrame *pYUVFrame = av_frame_alloc();
	//只有指定了AVFrame的像素格式,画面大小才能真正分配内存
	//缓冲区分配内存
	uint8_t *out_buf = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
	//初始化缓冲区
	avpicture_fill((AVPicture *)pYUVFrame, out_buf, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
	//像素格式转换或缩放
	struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
			pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
			SWS_BILINEAR, NULL, NULL, NULL);
	//打开写入文件
	FILE *fp_yuv = fopen(output_cstr, "wb");
	int len, got_frame, frame_count = 0;
	while(av_read_frame(pFormatCtx, packet) >= 0)
	{
		//提取视频压缩数据
		if(packet->stream_index == video_stream_index)
		{
			//AVPacket转化为AVFrame
			len = avcodec_decode_video2(pCodecCtx, pFrame, &got_frame, packet);
			if(len < 0)
			{
				LOGE("%s","解码错误!");
				return;
			}
			//got_frame非零,表示正在解码
			if(got_frame)
			{
				//由frame得到YUV的frame
				//转为指定的YUV420P像素帧
				sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
						pYUVFrame->data, pYUVFrame->linesize);
				//向YUV文件保存解码之后的帧数据
				//一个像素包含一个Y
				//UV都是Y的四分之一
				int y_size = pCodecCtx->width * pCodecCtx->height;
				fwrite(pYUVFrame->data[0], 1, y_size, fp_yuv);
				fwrite(pYUVFrame->data[1], 1, y_size/4, fp_yuv);
				fwrite(pYUVFrame->data[2], 1, y_size/4, fp_yuv);
				LOGI("解码第%d帧", frame_count++);
			}
		}

		//释放AVPacket
		av_free_packet(packet);
	}
	//关闭各种打开的资源
	fclose(fp_yuv);
	av_frame_free(&pFrame);
	avcodec_close(pCodecCtx);
	avformat_free_context(pFormatCtx);

	//释放资源
	(*env)->ReleaseStringUTFChars(env, jstr_input, input_cstr);
	(*env)->ReleaseStringUTFChars(env, jstr_output, output_cstr);
}
  • 创建调用类,注意动态库之间有相互关系,其调用顺序一定要对
public class VideoUtils {
	
	public native static void decode(String input, String output);
	
	static {
		System.loadLibrary("avutil-54");
		System.loadLibrary("swresample-1");
		System.loadLibrary("avcodec-56");
		System.loadLibrary("avformat-56");
		System.loadLibrary("swscale-3");
		System.loadLibrary("postproc-53");
		System.loadLibrary("avfilter-5");
		System.loadLibrary("avdevice-56");
		System.loadLibrary("ffmpeg_player");
	}
}
  • 主活动文件
import java.io.File;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}
	
	public void buttonPush(View view) {
		String input = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "test_in.mp4";
		String output = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "test_out.yuv";
		VideoUtils.decode(input, output);
		Toast.makeText(MainActivity.this, "转码完成", Toast.LENGTH_SHORT).show();
	}
}
  • 布局文件
<LinearLayout 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:orientation="vertical" >

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="开始转码"
		android:onClick="buttonPush"/>

</LinearLayout>
  • 权限添加
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

之后编译,生成apk,在手机上测试,没问题
打印日志
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cj5785

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值