本文共 11526 字,大约阅读时间需要 38 分钟。
上一回说到啊,这千秋月没是佳人离别,时逢枯枝落旧城,却待新兰满长街,战场上还未至瑞雪,各位看官不好意思,今日帝都又雾霾,来听小老二说书的别忘了加个口罩。在中我们基本编写完了所有模块儿代码,但是没有整合在一起,也没有对接层,接下来就是干这事。
我们编写完成了视频编码类、音频编码类、合成视频类,但是他们都没联系到一起,也没有被我们先前定义的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#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 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代码差不多,更多可阅读。
里面配置和 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的同一个根目录下,欢迎下载,如有问题可以直接在上面留言,我会抽时间一个一个的干掉,项目地址,如果你觉得对你有帮助你可以勉为其难的 star。