利用FFmpeg玩转Android视频录制与压缩(三)

版权声明:本文为“剑西”原创文章,转载需注明出处! https://blog.csdn.net/mabeijianxi/article/details/73011313


请尊重原创,转载请注明出处http://blog.csdn.net/mabeijianxi/article/details/73011313

前言

上一回说到啊,这千秋月没是佳人离别,时逢枯枝落旧城,却待新兰满长街,战场上还未至瑞雪,各位看官不好意思,今日帝都又雾霾,来听小老二说书的别忘了加个口罩。在利用FFmpeg玩转Android视频录制与压缩(二)中我们基本编写完了所有模块儿代码,但是没有整合在一起,也没有对接Java层,接下来就是干这事。

native代码对接

我们编写完成了视频编码类、音频编码类、合成视频类,但是他们都没联系到一起,也没有被我们先前定义的JNI接口调用,再次看一眼我们的简单流程图以后,就开搞。


1. 准备一个处理全局数据的Handler

它的职责是处理视频编码完成事件、音频编码完成事件、视频合成完成开始控制、视频合成结束回调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,这个是有规律的,完全不需要用命令生成。

2. JNI接口实现

我们在一开始就定义了众多JNI接口函数,但是都没有实现,现在我们底层关键代码基本编写完成,是时候串联了。

jx_ffmpeg_jni.cpp:

/**
 * Created by jianxi on 2017/5/12.
 * https://github.com/mabeijianxi
 * [email protected]
 */
#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 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对象成为全局的。

Java代码对接

native代码已经基本完成,接下来就是Java层次调用了,这个不是本文的重点,只记录个大概,2.0的Java代码和1.0的Java代码差不多,更多可阅读利用FFmpeg玩转Android视频录制与压缩(一)

1、相机关键参数配置

  • 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。

2、音频配置:

里面配置和 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);

3、调用JNI函数

当用户按下录制键的时候我们开始调用 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天的业余时间,其他大部分是在学习和做准备,基本从前到后是如下几步:

  1. 对c/c++能基本会利用,语言就是个工具,一开始可以不太深入,可以到工程中实践;
  2. 对jni有个全面的了解,网上很多博客,别光看,多实际操作;
  3. 这个时候就可以看一些音视频编解码基础性质的东西,雷神写了好多入门教程,这里贴一个入口视音频编解码技术零基础学习方法
  4. 视频压缩编码和音频压缩编码的基本原理,看些原理性的东西,不限于这篇博客。
  5. 然后对FFmpeg的编译脚本有一定了解,Android下不可能开启全部功能的,你需要根据你的项目编译合适你用的FFmpeg;
  6. 上面都弄完了即可开始编译自己的FFmpeg,然后导入项目开始蹂蹑它的API。

可能会用的工具:

MediaInfo:一个分析视频的软件。

VLC:一个播放器

GLYUVPlay:一个YUV播放器

本工程2.0版本的全部代码和1.0放在了github的同一个根目录下,欢迎下载,如有问题可以直接在上面留言,我会抽时间一个一个的干掉,项目地址https://github.com/mabeijianxi/small-video-record,如果你觉得对你有帮助你可以勉为其难的 star

猜你喜欢

转载自blog.csdn.net/mabeijianxi/article/details/73011313
今日推荐