利用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天的業餘時間,其他大部分是在學習和做準備,基本從前到後是如下幾步:
- 對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。
相關推薦
利用FFmpeg玩轉Android視訊錄製與壓縮(三)
前言 上一回說到啊,這千秋月沒是佳人離別,時逢枯枝落舊城,卻待新蘭滿長街,戰場上還未至瑞雪,各位看官不好意思,今日帝都又霧霾,來聽小老二說書的別忘了加個口罩。在利用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 二.服務消費者(2)feign
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取出