2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口

上一篇:2019-04-18 Mac OS交叉编译mp4v2生成so文件(https://www.jianshu.com/p/a29831ab90e5)

本篇适合小白阅读,大神的话,内容没有新知识点,有兴趣可以帮忙指正错误

项目源码GitHub地址:https://github.com/HaloMartin/HHMp4v2Test

一,环境

Android Studio 3.3.2
JRE: 1.8.0_152-release-1248-b01 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
操作系统:macOS 10.14.4
CMake: 3.6.4111459
NDK:android-ndk-r15c
视频编码格式:H264
音频编码格式:AAC(Demo中未验证,实际项目中使用过)

二,背景

以前在iOS上有编译过mp4v2库,因为iOS本身就是基于C/C++ runtime底层实现的环境,在使用时相对简单,在我的应用中,我是编译成 .a 文件,而后使用混合编程的方式使用,较方便。
上一篇中,虽然编译了mp4v2库,但是生成的动态库并不能直接使用在Java中,需要使用C/C++语法包含头文件等复杂过程才能调用,实际上还是需要C/C++代码,对于Java上的开发来说,是不合适的。通过这边文章,可以形成我们可以在Java上可以直接使用的动态so文件,不需要关心头文件,在使用时添加 libHHMp4v2.solibMp 4v2.so 即可。

这里使用的NDK是android-ndk-r15c,因为需要兼容armeabi架构,如果使用的NDK版本不能高于16,就不支持armeabi

大概的过程关系如下:


2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第1张图片
过程

三,步骤概览

细节较多,步骤也多,所以先了解一下大概的步骤:

    1. 创建Native C++工程
    1. 添加CMakeList.txt文件内容
    1. 添加源码,封装Java Native接口
    1. 编译工程,生成HHMp4v2.so文件
    1. 获取生成的so文件

四,具体步骤

  • 创建Native C++工程

      1. 创建工程,逐次点击 File->New->New Project,如图 【Fig 4-1 创建Native C++工程(1)】
        2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第2张图片
        Fig 4-1 创建Native C++工程(1)
      1. 选择 Native C++,而后点击 Next,如图【Fig 4-1 创建Native C++工程(2)】
        2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第3张图片
        Fig 4-1 创建Native C++工程(2)
      1. 配置Project,包括工程名以及工程路径,而后点击 Next,如图【Fig 4-1 创建Native C++工程(3)】
        2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第4张图片
        Fig 4-1 创建Native C++工程(3)
      1. Customize C++ Support按照默认即可
  • 调整工程目录

经过 第一步创建Native C++工程 后,Android Studio已经帮我们生成了基本的目录结构,直接编译可以生成一个 libnative-lib.so 的动态库,接下来我们的操作要添加 HHMp4v2,再去掉 native-lib,原始目录结构如图【Fig 4-2 调整工程目录(1)】

2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第5张图片
Fig 4-2 调整工程目录(1)

    1. app->src->main->java->com 下添加一个文件夹 HHMp4v2 ;
    1. 右击 HHMp4v2 添加一个Java类,类名为HHMp4v2 ,按默认生成即可;
    1. 在Java类 HHMp4v2 中添加如下代码:
package com.HHMp4v2;

public class HHMp4v2 {
    /**
     * 初始化MP4文件
     * @param fullPath mp4文件全路径名
     * @param width 视频宽
     * @param height 视频高
     * @param fps 视频帧率
     * @param channel 声道
     * @param samplerate 音频采样率
     * @return 1 if success, 0 if fail
     * */
    public native int initMp4Packer(String fullPath, int width, int height, int fps, int channel, int samplerate);

    /**
     * 封装视频数据帧进Mp4文件
     * @param data 视频帧数据
     * @param dataLen 帧数据data的长度
     * @param duration 帧时长
     * @return -1 if failed, dataLen pack in if success
     * */
    public native int packMp4Video(byte[] data, int dataLen, int duration);

    /**
     * 封装音频数据帧进Mp4文件
     * @param data 音频帧数据
     * @param dataLen 帧数据data的长度
     * @param duration 帧时长
     * @return -1 if failed, dataLen pack in if success
     * */
    public native int packMp4Audio(byte[] data, int dataLen, int duration);

    /**
     * 结束并关闭Mp4文件
     * */
    public native void mp4Close();

    static {
        //加载libHHMp4v2.so动态库,用于测试
        System.loadLibrary("HHMp4v2");
    }
}
    1. 修改NDK,逐次点击 File->Project Structure ,在弹出的对话框中,修改 Android NDK locationandroid-ndk-r15c ,比如我的路径就是 /Users/Martin/Documents/AndroidDev/android-ndk-r15cOK 保存;
    1. 修改 app->build.gradle 文件,在defaultConfig中添加一个ndk配置信息,再加一个sourceSets.main信息:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.vihivision.martin.hhmp4v2test"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
        ndk {
            abiFilters "armeabi"
            moduleName "HHMp4v2"
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
    sourceSets.main {
        jni.srcDirs = []
        jniLibs.srcDirs = ['libs']
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
    1. 右击 app->libs 文件夹,新增目录 armeabi,把 上一篇:2019-04-18 Mac OS交叉编译mp4v2生成so文件(https://www.jianshu.com/p/a29831ab90e5)中编译好的 libMp4v2.so 文件放入;
    1. 右击 app->libs 文件夹,新增目录 include,把mp4v2库中的头文件放入,对应 上一篇Mp4v2 2.0.0 源码文件的 include 目录,如图【Fig 4-2 调整工程目录(2)】,另外还有一个 .h 头文件,这个头文件的生成需要使用到Java类 HHMp4v2,下文会有介绍;
      2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第6张图片
      Fig 4-2 调整工程目录(2)

经过以上 步后,目录结构已经完整了,新的目录结构如图【Fig 4-2 调整工程目录(3)】【Fig 4-2 调整工程目录(3)补充】

2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第7张图片
Fig 4-2 调整工程目录(3)

2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第8张图片
Fig 4-2 调整工程目录(3)补充
  • 添加CMakeList.txt文件内容

按照百度或者Google来的方法,生成动态库so文件的方式:一种是手写一个 Android.mk 文件来并用 ndk-build 命令来构建;另一种是通过Android Studio的来帮你做到,需要做的就是写一个 CMakeList.txt 文件;
相对来说,我还是比较喜欢后者的,起码可以专注于一个IDE工具进行操作,不必进行工具间的切换,但是作为一个程序员,在条件有限的情况下,其实任何方式都应该有所了解。
篇幅有限,我们这边专注于后者,在 CMakeList.txt 文件中配置好我们需要的信息,这里我直接贴上脚本代码及代码注释,有兴趣的朋友可以通过文末附载的连接学习更多。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

#自定义distribution_DIR变量,开发者可以根据自己的需要自行设置
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../libs)
#支持-std=gnu++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
#自定义so文件输出目录,${PROJECT_SOURCE_DIR}是工程目录,即CMakeList.txt文件所在目录
#${ANDROID_ABI}是表示CPU架构,因为我在gradle指定了只有armeabi,所以这里也只会有一个
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/output/${ANDROID_ABI})

#导入Mp4v2动态库,指定SHARED表示动态添加
add_library( Mp4v2
        SHARED
        IMPORTED )

#设置目标动态库的位置,当前目录为CMakeList.txt所在的位置,定位到libMp4v2.so需要先往上三级
set_target_properties( Mp4v2
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/../../../libs/armeabi/libMp4v2.so
        )
#指定动态库头文件路径
include_directories(${PROJECT_SOURCE_DIR}/../../../libs/include)


# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

#添加脚本,编译HHMp4v2.c生成HHMp4v2动态库
add_library(HHMp4v2 SHARED
        HHMp4v2.c
        )
#add_library( # Sets the name of the library.
#        native-lib
#
#        # Sets the library as a shared library.
#        SHARED
#
#        # Provides a relative path to your source file(s).
#        native-lib.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

#关联这两个库
target_link_libraries( HHMp4v2 Mp4v2 ${log-lib} )

#target_link_libraries( # Specifies the target library.
#        native-lib
#
#        # Links the target library to the log library
#        # included in the NDK.
#        ${log-lib})
  • 添加源码,封装Java Native接口

主要分成两个步骤:

    1. 生成 com_HHMp4v2_HHMp4v2.h文件;
    1. 生成与 com_HHMp4v2_HHMp4v2.h 对应的C语言实现文件 HHMp4v2.c
    1. 使用 mp4v2 库实现 com_HHMp4v2_HHMp4v2.h 中定义的方法。

细心的同学会注意到上面的 CMakeList.txt 文件中有一个 HHMp4v2.c 的C语言实现文件我没有解释,第一步: 创建 Native C++工程 中的 第7步 里也涉及到一个 com_HHMp4v2_HHMp4v2.h 头文件。
这两个文件是对应的,com_HHMp4v2_HHMp4v2.h 是通过 javac 命令将 HHMp4v2.java 编译成中间文件 HHMp4v2.class,再通过 javah -jni 命令编译出头文件 com_HHMp4v2_HHMp4v2.h,终端上的命令如下:

Last login: Fri Apr 26 15:04:56 on ttys001
MartinMac:HHMp4v2 Martin$ cd /Users/Martin/GitHub/HHMp4v2Test/app/src/main/java/com/HHMp4v2/
MartinMac:HHMp4v2 Martin$ ls
HHMp4v2.java
MartinMac:HHMp4v2 Martin$ javac HHMp4v2.java 
MartinMac:HHMp4v2 Martin$ cd ..
MartinMac:com Martin$ cd ..
MartinMac:java Martin$ javah -jni com.HHMp4v2.HHMp4v2
MartinMac:java Martin$ 

结果如图 【Fig 4-3 添加源码,封装Java Native接口(1)】

2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第9张图片
Fig 4-3 添加源码,封装Java Native接口(1)

在生成 com_HHMp4v2_HHMp4v2.h 后,需要移动到 include 目录,直接拖动就好。

接下来就是新建 com_HHMp4v2_HHMp4v2.h 的实现文件 HHMp4v2.c
这里使用的是C语言,而不是C++,所以使用扩展名为 .c,右击 app->src->main->cpp,新建一个 C/C++ Source File,命名为HHMp4v2,扩展名.c

接下来就是实现 com_HHMp4v2_HHMp4v2.h 中定义的四个方法,基于需求,这里只需要实现有关MP4文件生成,写入和关闭的方法,共四个。
每次输入的数据必须为一个NAL单元(NALU),负载内容依照次序,应为序列参数集SPS、图像参数集PPS,I帧,P帧,NALU的起始4个字节是NALU的标志 0x00 00 00 01,负载内容的类型则为NALU第5个字节的低5位,可以通过代码 nal[5] & 0x1F 来获取对应的类型值,其中0x07表示该NALU负载的是SPS,0x08则表示负载的是PPS,0x05则表示I帧,0x01表示普通的帧,即P帧。
写入视频的大致步骤如下:

    1. 读取SPS
      从NALU中读取,负载类型为0x07,根据SPS的信息,通过MP4AddH264VideoTrack可以添加到视频Track,返回对应的Track ID,而后通过MP4SetVideoProfileLevel配置Profile Level,再通过MP4AddH264SequenceParameterSet设置序列参数集,具体参数可以参考代码;
    1. 读取PPS
      从NALU中读取,负载类型为0x08,根据PPS的信息,通过MP4AddH264PictureParameterSet设置图像参数集;
    1. 读取I帧
      从NALU中读取,负载类型为0x05,是H264码流中的关键帧,需要把NALU的标志位0x00 00 00 01替换成负载数据长度,而后再通过MP4WriteSample写入文件;
//replace the first 4 bytes with nalu payload's size
naluData[0] = (uint8_t) ((naluSize - 4) >> 24);
naluData[1] = (uint8_t) ((naluSize - 4) >> 16);
naluData[2] = (uint8_t) ((naluSize - 4) >> 8);
naluData[3] = (uint8_t) ((naluSize - 4) & 0xFF);
bool result = MP4WriteSample(recordCtx->m_mp4FHandle, recordCtx->m_vTrackId, naluData, (uint32_t) naluSize, MP4_INVALID_DURATION, 0 , 1);
    1. 读取B/P帧
      从NALU中读取,负载类型为0x01,是一般帧,需要依赖关键帧才能完整显示的视频帧,一个关键帧后会有若干个B/P帧,I帧的间隔不一定,有些为了追求流量的节省,会把I帧间隔放的很大,有些则会把I帧间隔放的很小,B/P帧封装入文件的方式和I帧类似,具体参考代码;

对于 mp4v2 来说,在初始化方法过后,一定要获取到基本的SPS和PPS后才能写入I帧,并且对于录像文件来说,一般都是需要以I帧开头的,如果不使用I帧开头,会导致写入的文件在播放时开头部分会有绿屏卡顿等异常,I帧内容异常也会有此问题。
在调用 mp4v2 的写入方法MP4WriteSample前,需要修改一下NAL数据,把头部四个字节的标识位替换成NAL负载数据的长度,I帧和P帧在写入前都需要进行此步骤,后续的操作就由 mp4v2 来完成,有兴趣的可以学习一下。
在这里因为篇幅的原因,不做展开描述,以代码中的注释为准,欢迎勘误纠正错误。

  • 编译工程,Make Project

如图【Fig 4-4 编译工程,Make Project】

2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第10张图片
Fig 4-4

  • 获取生成的so文件

可以从在 ·CMakeList.txt 文件中指定的输出目录找到动态库so文件,也可以在 intermediates 中找到,如图 【Fig 4-5 获取生成的so文件】

2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口_第11张图片
Fig 4-5 获取生成的so文件

五,总结

第一次编译Android端的mp4v2库,过程中遇到了一些问题,主要还是对Android JNI不熟悉的原因导致的,因为在iOS上有使用过mp4v2库,所以在实现将H264码流解析并存进MP4文件这个过程上比较顺利,知识是慢慢积累的过程,对于自己不懂的东西,还需要不断的学习!

链接

# Where is CMAKE_SOURCE_DIR?
# 使用mp4v2封装mp4
# Android Studio NDK CMake 指定so输出路径以及生成多个so的案例与总结
# Android开发中如何将自己编译的.so文件用到其他的项目中
# 3.3、Android Studio 添加 C 和 C++ 项目
# Java中JNI的使用(上)
# 呕心沥血Android studio使用JNI实例
# 参考源码:Github项目地址:https://github.com/chezi008/Mp4v2Demo

你可能感兴趣的:(2019-04-25 Mac上使用Android Studio封装mp4v2库的Java Native接口)