Vue.js实战——封装Android H5 App的录音组件_15

一、目标

    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 AppH5移至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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值