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 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章