1. 程式人生 > >利用FFmpeg玩轉Android視訊錄製與壓縮(三)

利用FFmpeg玩轉Android視訊錄製與壓縮(三)

前言

上一回說到啊,這千秋月沒是佳人離別,時逢枯枝落舊城,卻待新蘭滿長街,戰場上還未至瑞雪,各位看官不好意思,今日帝都又霧霾,來聽小老二說書的別忘了加個口罩。在利用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. 然後對FFmpeg的編譯指令碼有一定了解,Android下不可能開啟全部功能的,你需要根據你的專案編譯合適你用的FFmpeg;
  5. 上面都弄完了即可開始編譯自己的FFmpeg,然後匯入專案開始蹂躡它的API。

可能會用的工具:

MediaInfo:一個分析視訊的軟體。

VLC:一個播放器

GLYUVPlay:一個YUV播放器

本工程2.0版本的全部程式碼和1.0放在了github的同一個根目錄下,歡迎下載,如有問題可以直接在上面留言,我會抽時間一個一個的幹掉,專案地址https://github.com/mabeijianxi/small-video-record,如果你覺得對你有幫助你可以勉為其難的 star

相關推薦

利用FFmpegAndroid視訊錄製壓縮

前言 上一回說到啊,這千秋月沒是佳人離別,時逢枯枝落舊城,卻待新蘭滿長街,戰場上還未至瑞雪,各位看官不好意思,今日帝都又霧霾,來聽小老二說書的別忘了加個口罩。在利用FFmpeg玩轉Android視訊錄製與壓縮(二)中我們基本編寫完了所有模組兒程式碼,但是沒有整合在

Android視訊錄製命令screenrecord

0、命令格式 1、基本 screenrecord是一個shell命令 支援Android4.4(API level 19)以上 支援視訊格式: mp4 2、不足 某些裝置可能無法直

Android視訊錄製命令screenrecord

Android5.0的問世可謂誠意十足,拋開設計介面改動等審美元素,在功能上谷歌真正站在使用者的角度開發,包括此前提及的允許使用者刪除預裝應用功能。近日,安卓5.0另一個福利被挖出,系統內建視訊錄製功能,無需Root便可實現,錄製期間不用連線電腦。 其實,安卓

V4L2視訊採集H.264編碼原始碼大放送:Linux視訊採集編碼

這幾天的努力也算沒有白費,現在我將貢獻出我的成果,當然程式碼很大一部分都不是我寫的。 V4L2視訊採集我參考了V4L2官方原始碼,H.264編碼我使用了開源編碼庫x264,並參考了網上的一些例子。 但

十八般武藝GaussDB(DWS)效能調優:好味道表定義

摘要:表結構設計是資料庫建模的一個關鍵環節,表定義好壞直接決定了叢集的有效容量以及業務查詢效能,本文從產品架構、功能實現以及業務特徵的角度闡述在GaussDB(DWS)的中表定義時需要關注的一些關鍵因素。 前言 GaussDB(DWS)是企業級的大規模並行處理關係型資料庫,採用Shared-nothing架構

ffmpeg視訊錄製壓縮

ffmpeg常用命令含義如下: -i 設定輸入流或輸入路徑,例如安卓裝置格式為 /sdcard/……xxx.mp4 -f 設定輸出格式 : -f flv, -f mp3 -s 設定畫面的寬和高,格式為 -s w*h,這裡的w是長邊,h是短邊,安卓設備註

Top團隊大牛帶你Android效能分析優化

第1章 課程導學與學習指南 效能優化是高階工程師必備的技能,本課程將帶你由表及裡學到國內Top團隊對效能問題的體系化解決方案,滿滿的乾貨讓你輕鬆晉級高階工程師。  1-1 課前必讀(不看會錯過一個億)  1-2 課程導學試看

視訊錄製播放豎屏旋轉90度問題

// 設定橫屏顯示// setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);// 選擇支援半透明模式,在有surfaceview的activity中使用。getWindow().setForm

android 視訊錄製進度條分段錄製,回滾刪除,類似美拍錄製

最近因為公司啟動微視訊專案,負責視訊錄製及視訊編輯這塊工作,而首先視訊錄製這塊需要支援分段錄製,和回滾刪除功能。 所以就做了這麼繼承View自定義了這個一個進度條小demo,有待完善及優化。 效果圖先出場 因為比較簡單加上有註釋,就不再多囉嗦,直接上程式碼

用SQL數據挖掘之MADlib——安裝

system wan 商品 ase 關聯規則挖掘 樹模型 ats 調用 ability   一、MADlib簡介    MADlib是Pivotal公司與伯克利大學合作的一個開源機器學習庫,提供了精確的數據並行實現、統計和機器學習方法對結構化和非結構化數據進行分析,主要目的

第二十四篇 數據結構——隊列Queue

stat 基礎 ann move 打印 圖片 data image 線性 1.. 隊列基礎 隊列也是一種線性結構; 相比數組,隊列所對應的操作數是隊列的子集; 隊列只允許從一端(隊尾)添加元素,從另一端(隊首)取出元素;

SpringCloud 二.服務消費者2feign

cover framework version 模式 over 能力 fault ble 路徑 上一篇博客講解了服務消費者的ribbon+restTemplate 模式的搭建,此篇文章將要講解服務消費者feign模式的搭建,這裏是為了普及知識 平時的項目中兩種消費模式選擇其

ArcGIS for Android 100.3的學習應用 實現地圖新增自定義指北針

圖為高德地圖實現指北針的效果,那麼ArcGIS如何實現呢? 實現方式: 新增地圖的旋轉監聽: map.addMapRotationChangedListener(new MapRotationChangedListener() { @Override

ArcGIS for Android 的學習應用 如何移除指定的點和線?

在地圖上新增點和線的時候,我們有時候會遇到要移除或者切換指定的點和線的操作。那麼如何移除指定的點和線呢? ArcGIS的api裡點和線都是由GraphicsOverlay類來進行建立新增的。通過Graphic物件將點或者線的圖形物件(SimpleMarkerSy

Android音訊實時傳輸播放:AMR硬編碼硬解碼

轉載請註明出處! 在Android中我所知道的音訊編解碼有兩種方式: (一)使用AudioRecord採集音訊,用這種方式採集的是未經壓縮的音訊流;用AudioTrack播放實時音訊流。用這兩個類的話,如果需要對音訊進行編解碼,就需要自己移植編解碼庫了,比如可以移植il

手把手教你如何外掛:分頁外掛Pagehelper

情景引入:小白:起床起床,,,快起床!!!我:怎麼怎麼了,小白你到底又怎麼了。。小白:我發現在Web系統中,分頁是一種很常見的功能,可是,我之前寫的方法都比較麻煩,移植性不是很高,有沒有什麼好辦法可以快速實現分頁的呢?我:確實是的,分頁功能幾乎在每個系統中都是存在的,它的實現

利用python進行資料分析】準備例項

我已經分享了本書的ipynb,所以跟著我一起來實驗吧。如果你不懂怎麼開啟ipynb格式的檔案,那也沒關係,anaconda3讓一切變得更簡單(我像是打廣告的)。安裝玩anaconda之後,我們在開始裡就可以找到它的資料夾,裡面有一個Jupyter Notebook,就是它了。

Java高階視訊_IO輸入輸出

四、File類 01、概述 (1)    用來將檔案或者資料夾封裝成物件 (2)    方便對檔案與資料夾進行操作 (3)    File物件可以作為引數傳遞給流的建構函式 02、File類的常見方法 (1)建立。 — Boolean createNweFile();在指定

跟我一起Sencha Touch 移動 WebApp 開發

1.目錄 移動框架簡介,為什麼選擇Sencha Touch?環境搭建建立專案框架,框架檔案簡介建立簡單Tabpanel案例自定義圖示的方式WebApp產品測試和釋出HTML5離線快取釋出成Android/IOS本地app應用 移動框架簡介,為什麼選擇Sencha Touch? 目前市面上,移動應用web框

Android記憶體溢位優化——防止Handler導致的記憶體洩露

在Android中,子執行緒不能直接更新主執行緒的UI,因此提供了Handler來方便我們操作。在子執行緒呼叫handler可以直接傳送Message加入MessageQueue,Looper取出