发表评论
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。
上一回说到啊,这千秋月没是佳人离别,时逢枯枝落旧城,却待新兰满长街,战场上还未至瑞雪,各位看官不好意思,今日帝都又雾霾,来听小老二说书的别忘了加个口罩。在利用FFmpeg玩转Android视频录制与压缩(二)中我们基本编写完了所有模块儿代码,但是没有整合在一起,也没有对接Java层,接下来就是干这事。
我们编写完成了视频编码类、音频编码类、合成视频类,但是他们都没联系到一起,也没有被我们先前定义的JNI接口调用,再次看一眼我们的简单流程图以后,就开搞。
它的职责是处理视频编码完成事件、音频编码完成事件、视频合成完成开始控制、视频合成结束回调Java层,老规矩先上菜。
jx_jni_handler.h:
/**
* Created by jianxi on 2017/5/26.
* https://github.com/mabeijianxi
* mabeijianxi@gmail.com
*/#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
* mabeijianxi@gmail.com
*/#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
* mabeijianxi@gmail.com
*/#include <jni.h>#include <string>#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 JNICALLJava_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 JNICALLJava_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 JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_getFFmpegConfig(JNIEnv *env,
jclass type) { return getEncoderConfigInfo(env);
}/**
* 编码一帧音频
*/extern "C"JNIEXPORT jint JNICALLJava_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 JNICALLJava_com_mabeijianxi_smallvideorecord2_jniinterface_FFmpegBridge_recordEnd(JNIEnv *env,
jclass type) {
h264_encoder->user_end();
aac_encoder->user_end(); return 0;
}JNIEXPORT void JNICALLJava_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视频录制与压缩(一)。
mParameters.setPreviewFormat(ImageFormat.YV12) :很关键,因为我们在底层是按照YV12的数据结构操作的。
camera.addCallbackBuffer(new byte[buffSize]) 我们需要add 三个buffer,也很关键,我试过用一个buffer,结果就是丢帧,这buffSize大小是width*height*3/2,这个和YV12是对应的,width*height 个Y,(1/4)*width*height个V,(1/4)*width*height个U。
里面配置和 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天的业余时间,其他大部分是在学习和做准备,基本从前到后是如下几步:
对c/c++能基本会利用,语言就是个工具,一开始可以不太深入,可以到工程中实践;
对jni有个全面的了解,网上很多博客,别光看,多实际操作;
这个时候就可以看一些音视频编解码基础性质的东西,雷神写了好多入门教程,这里贴一个入口视音频编解码技术零基础学习方法
视频压缩编码和音频压缩编码的基本原理,看些原理性的东西,不限于这篇博客。
然后对FFmpeg的编译脚本有一定了解,Android下不可能开启全部功能的,你需要根据你的项目编译合适你用的FFmpeg;
上面都弄完了即可开始编译自己的FFmpeg,然后导入项目开始蹂蹑它的API。
可能会用的工具:
MediaInfo:一个分析视频的软件。
VLC:一个播放器
GLYUVPlay:一个YUV播放器
本工程2.0版本的全部代码和1.0放在了github的同一个根目录下,欢迎下载,如有问题可以直接在上面留言,我会抽时间一个一个的干掉,项目地址https://github.com/mabeijianxi/small-video-record,如果你觉得对你有帮助你可以勉为其难的 star。
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。