一、目标
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