• 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","找不到视频流
    ");
    		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,在手机上测试,没问题
    打印日志
    在这里插入图片描述

  • 相关阅读:
    ssh或scp到远程电脑,不用输密码
    关于浏览器缓存,cookie , session
    js小tips和小笔记
    Promise对象
    terminal命令
    喜大普奔:我的个人博客www.yxmblog.top
    TCP/UDP常用端口号
    以后可能在博客园写的少了!
    << 转载>>Shell一些强大的命令
    Linux下的简单压缩相关操作
  • 原文地址:https://www.cnblogs.com/cj5785/p/10664662.html
Copyright © 2020-2023  润新知