上一回说到啊,这千秋月没是佳人离别,时逢枯枝落旧城,却待新兰满长街,战场上还未至瑞雪,各位看官不好意思,今日帝都又雾霾,来听小老二说书的别忘了加个口罩。在利用FFmpeg玩转Android视频录制与压缩(二)中我们基本编写完了所有模块儿代码,但是没有整合在一起,也没有对接Java层,接下来就是干这事。
我们编写完成了视频编码类、音频编码类、合成视频类,但是他们都没联系到一起,也没有被我们先前定义的JNI接口调用,再次看一眼我们的简单流程图以后,就开搞。
它的职责是处理视频编码完成事件、音频编码完成事件、视频合成完成开始控制、视频合成结束回调Java层,老规矩先上菜。
jx_jni_handler.h:
/**
* Created by jianxi on 2017/5/26.
* https://github.com/mabeijianxi
* [email protected]
*/
#ifndef JIANXIFFMPEG_JX_JNI_HANDLER_H
#define JIANXIFFMPEG_JX_JNI_HANDLER_H
#include "jx_user_arguments.h"
class JXJNIHandler{
~JXJNIHandler(){
// delete(arguments);
}
public:
void setup_video_state(int video_state);
void setup_audio_state(int audio_state);
int try_encode_over(UserArguments* arguments);
void end_notify(UserArguments* arguments);
private:
int start_muxer(UserArguments* arguments);
private:
int video_state;
int audio_state;
};
#endif //JIANXIFFMPEG_JX_JNI_HANDLER_H
jx_jni_handler.cpp:
/**
* Created by jianxi on 2017/5/26.
* https://github.com/mabeijianxi
* [email protected]
*/
#include "jx_jni_handler.h"
#include "base_include.h"
#include "jx_media_muxer.h"
#include "jx_log.h"
/**
* 改变视频录制状态
* @param video_state
*/
void JXJNIHandler::setup_video_state(int video_state) {
JXJNIHandler::video_state = video_state;
}
/**
* 改变音频录制状态
* @param audio_state
*/
void JXJNIHandler::setup_audio_state(int audio_state) {
JXJNIHandler::audio_state = audio_state;
}
/**
* 检查是否视音是否都完成,如果完成就开始合成
* @param arguments
* @return
*/
int JXJNIHandler::try_encode_over(UserArguments *arguments) {
if (audio_state == END_STATE && video_state == END_STATE) {
start_muxer(arguments);
return END_STATE;
}
return 0;
}
/**
* 开始视频合成
* @param arguments
* @return
*/
int JXJNIHandler::start_muxer(UserArguments *arguments) {
JXMediaMuxer *muxer = new JXMediaMuxer();
muxer->startMuxer(arguments->video_path, arguments->audio_path, arguments->media_path);
delete (muxer);
end_notify(arguments);
return 0;
}
/**
* 通知java层
* @param arguments
*/
void JXJNIHandler::end_notify(UserArguments *arguments) {
try {
int status;
JNIEnv *env;
status = arguments->javaVM->AttachCurrentThread(&env, NULL);
if (status < 0) {
LOGE(JNI_DEBUG,"callback_handler: failed to attach "
"current thread");
return;
}
jmethodID pID = env->GetStaticMethodID(arguments->java_class, "notifyState", "(IF)V");
if (pID == NULL) {
LOGE(JNI_DEBUG,"callback_handler: failed to get method ID");
arguments->javaVM->DetachCurrentThread();
return;
}
env->CallStaticVoidMethod(arguments->java_class, pID, END_STATE, 0);
env->DeleteGlobalRef(arguments->java_class);
LOGI(JNI_DEBUG,"啦啦啦---succeed");
arguments->javaVM->DetachCurrentThread();
}
catch (exception e) {
LOGI(JNI_DEBUG,"反射回调失败");
}
delete (arguments);
delete(this);
}
这里基本都是API的调用,但是有个地方很关键,可以看到 end_notify函数里面通过反射调用Java的一个方法,这里的写法和一般的不同,因为我们是在 native 的线程里面调用的,直接反射是不行的,我们来看看官方的解释与解决办法
我的这种情况就是用 pthread_create 创建了一个线程,所以我在一开始的时候就把我们要反射的 jclass 对象还有 JavaVM 指针存入了 UserArguments 这个结构体,根据官方提示我们先在当前 JavaVM 上绑定我们的 native线程,然后即可搞事情,这个 env->GetStaticMethodID 函数需要传入个函数ID,这个是有规律的,完全不需要用命令生成。
我们在一开始就定义了众多JNI接口函数,但是都没有实现,现在我们底层关键代码基本编写完成,是时候串联了。
jx_ffmpeg_jni.cpp:
/**
* Created by jianxi on 2017/5/12.
* https://github.com/mabeijianxi
* [email protected]
*/
#include
#include
#include "jx_yuv_encode_h264.h"
#include "jx_pcm_encode_aac.h"
#include "jx_jni_handler.h"
#include "jx_ffmpeg_config.h"
#include "jx_log.h"
using namespace std;
JXYUVEncodeH264 *h264_encoder;
JXPCMEncodeAAC *aac_encoder;
#define VIDEO_FORMAT ".h264"
#define MEDIA_FORMAT ".mp4"
#define AUDIO_FORMAT ".aac"
/**
* 编码准备,写入配置信息
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_prepareJXFFmpegEncoder(JNIEnv *env,
jclass type,
jstring media_base_path_,
jstring media_name_,
jint v_custom_format,
jint in_width,
jint in_height,
jint out_width,
jint out_height,
jint frame_rate,
jlong video_bit_rate) {
jclass global_class = (jclass) env->NewGlobalRef(type);
UserArguments *arguments = (UserArguments *) malloc(sizeof(UserArguments));
const char *media_base_path = env->GetStringUTFChars(media_base_path_, 0);
const char *media_name = env->GetStringUTFChars(media_name_, 0);
JXJNIHandler *jni_handler = new JXJNIHandler();
jni_handler->setup_audio_state(START_STATE);
jni_handler->setup_video_state(START_STATE);
arguments->media_base_path = media_base_path;
arguments->media_name = media_name;
size_t v_path_size = strlen(media_base_path) + strlen(media_name) + strlen(VIDEO_FORMAT) + 1;
arguments->video_path = (char *) malloc(v_path_size + 1);
size_t a_path_size = strlen(media_base_path) + strlen(media_name) + strlen(AUDIO_FORMAT) + 1;
arguments->audio_path = (char *) malloc(a_path_size + 1);
size_t m_path_size = strlen(media_base_path) + strlen(media_name) + strlen(MEDIA_FORMAT) + 1;
arguments->media_path = (char *) malloc(m_path_size + 1);
strcpy(arguments->video_path, media_base_path);
strcat(arguments->video_path, "/");
strcat(arguments->video_path, media_name);
strcat(arguments->video_path, VIDEO_FORMAT);
strcpy(arguments->audio_path, media_base_path);
strcat(arguments->audio_path, "/");
strcat(arguments->audio_path, media_name);
strcat(arguments->audio_path, AUDIO_FORMAT);
strcpy(arguments->media_path, media_base_path);
strcat(arguments->media_path, "/");
strcat(arguments->media_path, media_name);
strcat(arguments->media_path, MEDIA_FORMAT);
arguments->video_bit_rate = video_bit_rate;
arguments->frame_rate = frame_rate;
arguments->audio_bit_rate = 40000;
arguments->audio_sample_rate = 44100;
arguments->in_width = in_width;
arguments->in_height = in_height;
arguments->out_height = out_height;
arguments->out_width = out_width;
arguments->v_custom_format = v_custom_format;
arguments->handler = jni_handler;
arguments->env = env;
arguments->java_class = global_class;
arguments->env->GetJavaVM(&arguments->javaVM);
h264_encoder = new JXYUVEncodeH264(arguments);
aac_encoder = new JXPCMEncodeAAC(arguments);
int v_code = h264_encoder->initVideoEncoder();
int a_code = aac_encoder->initAudioEncoder();
if (v_code == 0 && a_code == 0) {
return 0;
} else {
return -1;
}
}
/**
* 编码一帧视频
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2H264(JNIEnv *env,
jclass type,
jbyteArray data_) {
jbyte *elements = env->GetByteArrayElements(data_, 0);
int i = h264_encoder->startSendOneFrame((uint8_t *) elements);
return 0;
}
/**
* 获取ffmpeg编译信息
*/
extern "C"
JNIEXPORT jstring JNICALL
Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_getFFmpegConfig(JNIEnv *env,
jclass type) {
return getEncoderConfigInfo(env);
}
/**
* 编码一帧音频
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_encodeFrame2AAC(JNIEnv *env,
jclass type,
jbyteArray data_) {
return aac_encoder->sendOneFrame((uint8_t *) env->GetByteArrayElements(data_, 0));
}
/**
*结束
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_recordEnd(JNIEnv *env,
jclass type) {
h264_encoder->user_end();
aac_encoder->user_end();
return 0;
}
JNIEXPORT void JNICALL
Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_nativeRelease(JNIEnv *env,
jclass type) {
// TODO
}
代码很简单在Java_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_prepareJXFFmpegEncoder 函数中我们对传入的地址先是做了个拼接,然后初始化了结构体 UserArguments ,并为其赋值。可以看到 jclass对象与 JavaVM 指针也是在这里赋值的,但是需要调用env->NewGlobalRef 函数来让jclass对象成为全局的。
native代码已经基本完成,接下来就是Java层次调用了,这个不是本文的重点,只记录个大概,2.0的Java代码和1.0的Java代码差不多,更多可阅读利用FFmpeg玩转Android视频录制与压缩(一)。
里面配置和 native需要是对应的,如采样率、通道数、采样格式等。
final int mMinBufferSize = AudioRecord.getMinBufferSize(mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
if (AudioRecord.ERROR_BAD_VALUE == mMinBufferSize) {
mMediaRecorder.onAudioError(MediaRecorderBase.AUDIO_RECORD_ERROR_GET_MIN_BUFFER_SIZE_NOT_SUPPORT, "parameters are not supported by the hardware.");
return;
}
mAudioRecord = new AudioRecord(android.media.MediaRecorder.AudioSource.MIC, mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, mMinBufferSize);
当用户按下录制键的时候我们开始调用 FFmpegBridge.prepareJXFFmpegEncoder 初始化底层,然后在 camera 与
AudioRecorder 的数据回调用把数据再传给底层,如下:
/**
* 数据回调
*/
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (mRecording) {
FFmpegBridge.encodeFrame2H264(data);
mPreviewFrameCallCount++;
}
super.onPreviewFrame(data, camera);
}
/**
* 接收音频数据,传递到底层
*/
@Override
public void receiveAudioData(byte[] sampleBuffer, int len) {
if (mRecording && len > 0) {
FFmpegBridge.encodeFrame2AAC(sampleBuffer);
}
}
最后结束的时候调用 FFmpegBridge.recordEnd() 皆可,只要底层封装好了,一切都会很简单。
本工程2.0搞的时间比较长基本跨越了一个春天,主要是平时工作太忙,只有晚上或者周末有时间搞,tnd春天都过了,女神也已成人妻,真是个悲惨的故事,在这过程中遇到了无数的问题,可以说无数次想放弃,但牛逼已经吹下,就边学边实践的走过来了,期间有很多网友帮助了我,我加了好几个音视频的群,里面同志异常活跃,这对我帮助非常大。本工程中使用的FFmpeg是根据现在的需要编译的,有更多需求的同学可在编译脚本中开启更多功能。
有兴趣从头开始学的同学可以看下我的学习路线,需要有耐心,很关键。本工程撸代码的时间大概是20天的业余时间,其他大部分是在学习和做准备,基本从前到后是如下几步:
可能会用的工具:
MediaInfo:一个分析视频的软件。
VLC:一个播放器
GLYUVPlay:一个YUV播放器
本工程2.0版本的全部代码和1.0放在了github的同一个根目录下,欢迎下载,如有问题可以直接在上面留言,我会抽时间一个一个的干掉,项目地址https://github.com/mabeijianxi/small-video-record,如果你觉得对你有帮助你可以勉为其难的 star。