一、目標
1、把使用原生H5的audio錄音功能組件移植到Android平臺中;
2、儘量少改動代碼。
二、思路
錄音之所以放在移植的最後一個章節講,主要是因爲需要修改原生H5 錄音的JS,並在JS中調用Android,Android處理完成後,還要調用js,過程比較複雜。
三、步驟
1、在Android中先實現錄音功能,VoiceMgr.java代碼如下(詳細工程代碼見github):
package com.justinsoft.voice;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.justinsoft.util.LogUtil;
import android.media.MediaRecorder;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
/**
* 音量管理
*/
public final class VoiceMgr
{
private static final String TAG = LogUtil.getClassTag(VoiceMgr.class);
// 最大錄音時長30秒;
private static final int MAX_LENGTH = 1000 * 30;
// 單例鎖
private static final Lock LOCK = new ReentrantLock();
// 是否錄音中
private static final AtomicBoolean STARTING = new AtomicBoolean(false);
// 分貝管理實例(單例)
private static VoiceMgr instance;
// Android內置錄音對象
private MediaRecorder recorder;
// 錄音的存儲文件
private String filePath;
// 錄音開始時間
private long startTime;
// 最大振幅
private double max;
/**
* 私有化構造方法,避免在外部被實例化
*/
private VoiceMgr()
{
filePath = Environment.getExternalStorageDirectory() + "/bt_voice_temp.amr";
Log.i(TAG, "bt voice path:" + filePath);
}
/**
* 獲取單例
*
* @return
*/
public static VoiceMgr getInstance()
{
if (null == instance)
{
LOCK.tryLock();
try
{
if (null == instance)
{
instance = new VoiceMgr();
}
return instance;
}
catch (Exception e)
{
Log.e(TAG, "failed to get voice instance.", e);
}
finally
{
LOCK.unlock();
}
}
return instance;
}
/**
* 開始測試
*/
public void startVoice()
{
if (STARTING.compareAndSet(false, true))
{
if (null == recorder)
{
recorder = new MediaRecorder();
File file = new File(filePath);
if(file.exists()){
file.deleteOnExit();
}
}
try
{
// 設置麥克風
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 設置音頻文件的編碼:AAC/AMR_NB/AMR_MB/Default 聲音的(波形)的採樣
recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
/*
* 設置輸出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
* ,H263視頻/ARM音頻編碼)、MPEG-4、RAW_AMR(只支持音頻且音頻編碼要求爲AMR_NB)
*/
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile(filePath);
recorder.setMaxDuration(MAX_LENGTH);
recorder.prepare();
/* ④開始 */
recorder.start();
// AudioRecord audioRecord.
/* 獲取開始時間* */
startTime = System.currentTimeMillis();
updateMicStatus();
Log.i(TAG, "record start time:" + startTime);
}
catch (IllegalStateException e)
{
Log.i(TAG, "start voice failed!" + e.getMessage());
}
catch (IOException e)
{
Log.i(TAG, "start voice failed!" + e.getMessage());
}
}
else
{
Log.i(TAG, "It's recording now.");
}
}
/**
* 停止測試
*/
public int stopVoice()
{
recorder.stop();
recorder.reset();
recorder.release();
recorder = null;
long endTime = System.currentTimeMillis();
Log.i(TAG, "Time cost:" + (endTime - startTime) / 1000 + " sec.");
int maxDB = new Double(20 * Math.log10(max)).intValue();
Log.d(TAG, "maxAmplitude:" + max + ",max dB:" + maxDB);
STARTING.compareAndSet(true, false);
return maxDB;
}
/**
* 更新最大振幅
*/
private void updateMicStatus()
{
if (recorder != null)
{
double maxAmplitude = recorder.getMaxAmplitude();
// double ratio = maxAmplitude / BASE;
// 分貝
// double db = 0;
if (max < maxAmplitude)
{
max = maxAmplitude;
}
// 不需要使用平均振幅
// if (ratio > 1)
// {
// db = 20 * Math.log10(ratio);
// }
// Log.d(TAG, "分貝值:" + db + ",最大振幅:" + maxAmplitude + ",平均振幅:" + ratio);
if (startTime + MAX_LENGTH > System.currentTimeMillis())
{
handler.postDelayed(dBRunnable, SPACE);
}
else
{
Log.i(TAG, "reach the max record time.");
stopVoice();
}
}
}
/**
* 更新分貝的線程
*/
private Runnable dBRunnable = new Runnable()
{
public void run()
{
updateMicStatus();
}
};
// // 取樣振幅的基數
// private static final int BASE = 1;
// 間隔取樣時間
private static final int SPACE = 10;
private final Handler handler = new Handler();
}
2、拍照代碼寫好以後,需要新增一個JsCallback.java用於js和java相互調用:
package com.justinsoft.webview;
import java.util.ArrayList;
import java.util.List;
import com.justinsoft.util.LogUtil;
import com.justinsoft.voice.VoiceMgr;
import android.app.Activity;
import android.os.Build;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebView;
/**
* 提供給JS的回調
*/
public class JsCallback
{
private static final String TAG = LogUtil.getClassTag(JsCallback.class);
private WebView webView;
private Activity activity;
public JsCallback(Activity activity, WebView webView)
{
this.activity = activity;
this.webView = webView;
}
/**
* 開始測試音量
*/
@JavascriptInterface
public void startVoice()
{
Log.i(TAG, "start voice.");
VoiceMgr voiceMgr = VoiceMgr.getInstance();
voiceMgr.startVoice();
callbackJs("startVoice", null);
}
/**
* 停止測試音量
*/
@JavascriptInterface
public void stopVoice()
{
Log.i(TAG, "stop voice.");
VoiceMgr voiceMgr = VoiceMgr.getInstance();
int maxDB = voiceMgr.stopVoice();
List<String> values = new ArrayList<String>();
values.add(maxDB + "");
callbackJs("stopVoice", values);
}
/**
* 拍照
*/
@JavascriptInterface
public void takePicture()
{
//TODO
}
/**
* 回調JS
*
* @param name JS方法名
* @param values JS參數
*/
private void callbackJs(String name, List<String> values)
{
final StringBuilder callbackJs = new StringBuilder();
callbackJs.append("javascript:");
callbackJs.append(name);
callbackJs.append("(");
if (values != null && values.size() > 0)
{
int size = values.size();
for (int i = 0; i < size; i++)
{
callbackJs.append("'");
callbackJs.append(values.get(i));
callbackJs.append("'");
if (i != size - 1)
{
callbackJs.append(",");
}
}
}
callbackJs.append(")");
Log.i(TAG, "callback js:" + callbackJs);
activity.runOnUiThread(new Runnable()
{
@Override
public void run()
{
final int version = Build.VERSION.SDK_INT;
// 因爲該方法在 Android 4.4 版本纔可使用,所以使用時需進行版本判斷
if (version < 18)
{
webView.loadUrl(callbackJs.toString());
}
else
{
webView.evaluateJavascript(callbackJs.toString(), new ValueCallback<String>()
{
@Override
public void onReceiveValue(String value)
{
Log.i(TAG, "js result:" + value);
}
});
}
}
});
}
}
3、在WebView中初始化這個JsCallback,相當於注入到Android App中html根dom下:
/**
* 綁定activity
*
* @param activity
*/
public void bind(Activity activity)
{
// this.activity = activity;
this.webChromeClient.bind(activity);
// 添加給js回調的接口
jsCallback = new JsCallback(activity, this);
addJavascriptInterface(jsCallback, "jsCallback");
}
4、在前端的record-sdk.js代碼中,需要在window下注冊接收錄音結果的方法,同時調用Java側注入的全局jsCallback對象,代碼如下:
export default class Record {
startRecord(param) {
try {
console.log("Recording start now.");
window.startVoice = () => {
console.log("start voice now.");
param.success();
window.startVoice = undefined;
};
jsCallback.startVoice();
} catch (e) {
param.error("Error:" + e);
}
}
stopRecord(param) {
try {
console.log("Recording stop now.");
window.stopVoice = (maxDB) => {
console.log("stop voice now:" + maxDB);
param.success(maxDB);
window.stopVoice = undefined;
};
jsCallback.stopVoice();
} catch (e) {
param.error("Error:" + e);
}
}
}
其中,window.startVoice和window.stopVoice是H5註冊給Android測調用的,詳見JsCallback中的代碼。
5、修改Record.vue代碼如下:
<template>
<div class="record">
<div class="record-title">Click following button to record voice:</div>
<input @click="startRecord" type="button" value="錄音">
<input @click="stopRecord" type="button" value="停止">
</div>
</template>
<script>
import Record from "../commons/record-sdk";
export default {
name: "Record",
data() {
return {
recorder: new Record()
};
},
methods: {
startRecord: function() {
console.log("start to record now.");
let self = this;
self.recorder.startRecord({
success: res => {
console.log("start record successfully.");
},
error: res => {
console.log("start record failed.");
}
});
},
stopRecord: function() {
console.log("stop record now.");
let self = this;
self.recorder.stopRecord({
success: res => {
console.log("stop record successfully.");
},
error: res => {
console.log("stop record failed.");
}
});
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.record {
width: 100%;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
font-weight: bold;
}
.record-title {
width: 100%;
}
#input {
width: 50px;
height: 20px;
/* margin-left: 10%; */
}
.record-play {
width: 100%;
height: 100px;
}
</style>
四、總結
1、彙總下前面移植H5 App至Android App時,處理各個組件不同的策略(見下表):
組件 | H5 App | H5移至Android App | 移植難度 |
彈框(Alert) | 原生支持 | 1、需要覆寫WebView Java方法即可,H5代碼不需要做任何改動 | ☆ |
地理經緯度(Location) | 原生支持 | 1、需要設置獲取經緯度的權限; 2、需要覆寫WebView Java方法,H5代碼不需要做任何改動 |
☆☆☆ |
拍照 | 原生支持 | 1、需要設置獲取拍照的權限 2、需要覆寫較多的WebView Java方法,主要是適配不同Android版本的拍照方法,H5代碼不需要做任何改動 |
☆☆☆☆ |
錄音 | 原生支持 | 1、沒有找到原生的方法,但是Android側有非常好的實現方式; 2、Android側和H5 js存在相互調用的交互邏輯(錄音時,H5 js需要調用Android的錄音Java代碼,同時告訴Android錄音成功後,回調js的哪個方法;Android錄音成功後,把錄音的處理結果傳給H5 js用於頁面展示) |
☆☆☆☆☆ |
2、錄音之所以考慮使用原生Android來實現,一是因爲業務訴求是測試分貝,直接使用Android的話,錄完馬上就可以測完,不需要借鑑其它後臺代碼(在原生H5中還用到了專門解析amr音頻和wav音頻的go語言+C語言代碼,涉及產品隱私,恕不提供);二是存在音頻轉換的問題,在原生H5中,一般是錄製wav音頻,而Android音頻格式不限制,可以錄製各種音頻,比如考慮爲了統一微信(錄音文件爲amr格式)和Android的音頻文件格式,那就採用amr比較好,所以說原生Android錄音更靈活更強大。
四、參考資料
[1]https://blog.csdn.net/bzhou0125/article/details/46444201
[2]https://www.cnblogs.com/blqw/p/3782420.html
上一篇:Vue.js實戰——H5拍照遷移至Android App_14 下一篇:Vue.js實戰——單獨封裝echarts時間軸高級篇_16