上一篇: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.so
和 libMp 4v2.so
即可。
这里使用的NDK是android-ndk-r15c,因为需要兼容armeabi架构,如果使用的NDK版本不能高于16,就不支持armeabi
大概的过程关系如下:
三,步骤概览
细节较多,步骤也多,所以先了解一下大概的步骤:
- 创建Native C++工程
- 添加CMakeList.txt文件内容
- 添加源码,封装Java Native接口
- 编译工程,生成HHMp4v2.so文件
- 获取生成的so文件
四,具体步骤
-
创建Native C++工程
- 创建工程,逐次点击 File->New->New Project,如图 【Fig 4-1 创建Native C++工程(1)】
- 创建工程,逐次点击 File->New->New Project,如图 【Fig 4-1 创建Native C++工程(1)】
- 选择 Native C++,而后点击 Next,如图【Fig 4-1 创建Native C++工程(2)】
- 选择 Native C++,而后点击 Next,如图【Fig 4-1 创建Native C++工程(2)】
- 配置Project,包括工程名以及工程路径,而后点击 Next,如图【Fig 4-1 创建Native C++工程(3)】
- 配置Project,包括工程名以及工程路径,而后点击 Next,如图【Fig 4-1 创建Native C++工程(3)】
- Customize C++ Support按照默认即可
-
调整工程目录
经过 第一步 : 创建Native C++工程 后,Android Studio已经帮我们生成了基本的目录结构,直接编译可以生成一个 libnative-lib.so
的动态库,接下来我们的操作要添加 HHMp4v2
,再去掉 native-lib
,原始目录结构如图【Fig 4-2 调整工程目录(1)】
- 在 app->src->main->java->com 下添加一个文件夹 HHMp4v2 ;
- 右击 HHMp4v2 添加一个Java类,类名为
HHMp4v2
,按默认生成即可;
- 右击 HHMp4v2 添加一个Java类,类名为
- 在Java类
HHMp4v2
中添加如下代码:
- 在Java类
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");
}
}
- 修改NDK,逐次点击 File->Project Structure ,在弹出的对话框中,修改 Android NDK location 到 android-ndk-r15c ,比如我的路径就是 /Users/Martin/Documents/AndroidDev/android-ndk-r15c,OK 保存;
- 修改 app->build.gradle 文件,在
defaultConfig
中添加一个ndk配置信息,再加一个sourceSets.main
信息:
- 修改 app->build.gradle 文件,在
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'
}
- 右击 app->libs 文件夹,新增目录 armeabi,把 上一篇:2019-04-18 Mac OS交叉编译mp4v2生成so文件(https://www.jianshu.com/p/a29831ab90e5)中编译好的
libMp4v2.so
文件放入;
- 右击 app->libs 文件夹,新增目录 armeabi,把 上一篇:2019-04-18 Mac OS交叉编译mp4v2生成so文件(https://www.jianshu.com/p/a29831ab90e5)中编译好的
- 右击 app->libs 文件夹,新增目录 include,把mp4v2库中的头文件放入,对应 上一篇 中
Mp4v2 2.0.0
源码文件的 include 目录,如图【Fig 4-2 调整工程目录(2)】,另外还有一个.h
头文件,这个头文件的生成需要使用到Java类HHMp4v2
,下文会有介绍;
- 右击 app->libs 文件夹,新增目录 include,把mp4v2库中的头文件放入,对应 上一篇 中
经过以上 七 步后,目录结构已经完整了,新的目录结构如图【Fig 4-2 调整工程目录(3)】 和 【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接口
主要分成两个步骤:
- 生成
com_HHMp4v2_HHMp4v2.h
文件;
- 生成
- 生成与
com_HHMp4v2_HHMp4v2.h
对应的C语言实现文件HHMp4v2.c
;
- 生成与
- 使用
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)】
在生成
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帧。
写入视频的大致步骤如下:
- 读取SPS
从NALU中读取,负载类型为0x07,根据SPS的信息,通过MP4AddH264VideoTrack可以添加到视频Track,返回对应的Track ID,而后通过MP4SetVideoProfileLevel配置Profile Level,再通过MP4AddH264SequenceParameterSet设置序列参数集,具体参数可以参考代码;
- 读取SPS
- 读取PPS
从NALU中读取,负载类型为0x08,根据PPS的信息,通过MP4AddH264PictureParameterSet设置图像参数集;
- 读取PPS
- 读取I帧
从NALU中读取,负载类型为0x05,是H264码流中的关键帧,需要把NALU的标志位0x00 00 00 01
替换成负载数据长度,而后再通过MP4WriteSample写入文件;
- 读取I帧
//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);
- 读取B/P帧
从NALU中读取,负载类型为0x01,是一般帧,需要依赖关键帧才能完整显示的视频帧,一个关键帧后会有若干个B/P帧,I帧的间隔不一定,有些为了追求流量的节省,会把I帧间隔放的很大,有些则会把I帧间隔放的很小,B/P帧封装入文件的方式和I帧类似,具体参考代码;
- 读取B/P帧
对于 mp4v2
来说,在初始化方法过后,一定要获取到基本的SPS和PPS后才能写入I帧,并且对于录像文件来说,一般都是需要以I帧开头的,如果不使用I帧开头,会导致写入的文件在播放时开头部分会有绿屏卡顿等异常,I帧内容异常也会有此问题。
在调用 mp4v2
的写入方法MP4WriteSample前,需要修改一下NAL数据,把头部四个字节的标识位替换成NAL负载数据的长度,I帧和P帧在写入前都需要进行此步骤,后续的操作就由 mp4v2
来完成,有兴趣的可以学习一下。
在这里因为篇幅的原因,不做展开描述,以代码中的注释为准,欢迎勘误纠正错误。
-
编译工程,Make Project
如图【Fig 4-4 编译工程,Make Project】
-
获取生成的so文件
可以从在 ·CMakeList.txt 文件中指定的输出目录找到动态库so文件,也可以在 intermediates 中找到,如图 【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