最近在微信上实现一个长按录音并识别语音功能,即在用户按下时调用微信 JS-SDK 录音接口,然后在用户松开时停止录音并识别录下了的语音。
流程如下:
touchstart -> wx.startRecord()
touchend -> wx.stopRecord()-> wx.translateVoice()
嗯,看起来流程很简单,实现起来也很快,但是测试的时候马上发现了问题:
1.短按会出现无法结束录音
如果点击了一下录音按钮, 相当于快速地startRecord然后stopRecord,那么stopRecord是极有可能是无效的,不会执行任何callback。因为微信JSSDK的调用是异步的。你调用startRecord的时间,和startRecord的success的callback被执行的时间可能间隔了若干毫秒甚至秒。这意味着,用户点击按钮可能会造成:虽然是先调用startRecord再调用stopRecord,但是可能stopRecord先于startRecord调用成功。这样就造成无法结束录音的情况。
解决的方法就是记录touchend 的时间点A,然后在startRecord调用成功时判断A是否大于此时的时间点,如果小于,则再次调用结束录音方法。
2.部分安卓机在调用startRecord后无法触发touchend
通过不断的测试,得出以下的异常表现(目前测试到的有vivo手机):
当 touchstart 时调用了 startRecord,触发的事件为: touchstart -> touchcancel(此时还未松开手指)
当 touchstart 时不调用 startRecord,触发的事件为: touchstart -> touchend(松开手指后)
可以看到:通过 touchstart 调用了 startRecord 后会立即触发元素的 touchcancel 事件,继而造成 touchend 事件就不触发了。
目前还没找到好的解决方法,只能针对安卓更好交互方式:点击录音->再次点击结束录音并识别。
相关实现代码:
<div class="voice-remote">
<span class="cover"></span>
<span class="icon"></span>
</div>
.voice-remote{border-radius:100%;width:1.8rem;height:1.8rem;position:absolute;background:#f6f6f6;bottom:1.5rem;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);-webkit-transition:all .2s;transition:all .2s}
.voice-remote.active{width:2.8rem;height:2.8rem;bottom:1rem;border:1px solid #e7e7e7}
.voice-remote:before{content:"";width:100%;height:100%;position:absolute;z-index:2;top:0;left:0;border-radius:100%;background-image:-webkit-linear-gradient(-90deg,transparent 50%,#f6720e 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%)}
.voice-remote:after{content:"";width:100%;height:100%;position:absolute;z-index:3;bottom:0;left:0;border-radius:100%;-webkit-background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%)}
.voice-remote .cover{position:absolute;border-radius:100%;width:100%;height:100%;z-index:4;top:0;left:0;-webkit-background-image:linear-gradient(-90deg,transparent 50%,#f6f6f6 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6f6f6 50%)}
.voice-remote .icon{position:absolute;width:100%;height:100%;top:0;left:0;background:#f6f6f6 url(/public/wap/app/images/voice.png) no-repeat center center;background-size:100%;border-radius:100%;z-index:5}
.voice-remote .icon:before{content:'长按说话';position:absolute;top:-.65rem;left:0;color:#f6720e;width:100%;text-align:center;font-size:.32rem}
.voice-remote.android .icon:before{content:"点击说话"}
.voice-remote.active .icon:before{content:'松开完成';top:-.85rem}
.voice-remote.android.active .icon:before{content:"点击完成"}
.voice-remote.active .icon{width:90%;height:90%;top:5%;left:5%;background-size:60%}
.voice-remote.active:before{-webkit-animation:scoll linear 2.5s;animation:scoll linear 2.5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
.voice-remote.active:after{-webkit-animation:xscoll linear 5s;animation:xscoll linear 5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
.voice-remote.active .cover{-webkit-animation:hide linear 5s;animation:hide linear 5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
@-webkit-keyframes scoll{0%{-webkit-transform:rotate(0)}
100%{-webkit-transform:rotate(180deg)}
}
@keyframes scoll{0%{transform:rotate(0)}
100%{transform:rotate(180deg)}
}
@-webkit-keyframes xscoll{0%{-webkit-transform:rotate(0)}
100%{-webkit-transform:rotate(360deg)}
}
@keyframes xscoll{0%{transform:rotate(0)}
100%{transform:rotate(360deg)}
}
@-webkit-keyframes hide{0%{opacity:1}
49.9%{opacity:1}
50%{opacity:0}
100%{opacity:0}
}
@keyframes hide{0%{opacity:1}
49.9%{opacity:1}
50%{opacity:0}
100%{opacity:0}
}
var VR = {
options: {
point: 0,
tpoint: 0,
epoint: 0,
timer: 0
},
keyword: [
{
data: '声音大、音乐大、音乐调大、音乐调高、声音调大',
type: 1,
cmdid: '203',
reply: '小主,已为你调了音乐声音~',
error: '音乐没调成功,再试一次喔~'
},
{
data: '声音小、音乐小、音乐调小、音乐调低、声音调小',
type: 1,
cmdid: '204',
reply: '小主,已为你调了音乐声音~',
error: '音乐没调成功,再试一次喔~'
},
{
data: '评分开、开评分、开启评分、评分开启',
type: 1,
cmdid: '541',
reply: '小主,已为你开启评分~',
error: '评分开启失败,再试一次喔~'
},
{
data: '评分关、关评分、关掉评分、评分关掉',
type: 1,
cmdid: '541',
reply: '小主,已为你关闭评分~',
error: '评分关闭失败,再试一次喔~'
}
],
recode: function(){//定时最长5s后结束录音
VR.options.timer = setInterval(function(){
var time = +new Date() - VR.options.point;
if(time >= 5000){
if(VR.getPlatformType() == 2){
$('.voice-remote').removeClass('active');
}
clearInterval(VR.options.timer);
setTimeout(function(){
VR.translate();
},100);
}
},1000);
},
cmdMatch: function(words){//根据语音识别处理的内容匹配关键词
for(i in VR.keyword){
var key = VR.keyword[i]['data'].split('、');
for(n in key){
//console.log(key[n]);
if(words.indexOf(key[n]) > -1){
VR.runCmd(i);//匹配到关键词执行相应方法
return true;
}else {
if(i == VR.keyword.length-1 && n == key.length - 1){
window.location.href = '/Wap/app?searchby=voice&searchkey='+words+'#pageSearchList';
}
}
}
}
},
runCmd: function(index){
var cmd = VR.keyword[index];
//执行方法...
},
translate: function(){//结束录音并识别语音
wx.stopRecord({
success: function(res) {
localId = res.localId;
wx.translateVoice({
localId: localId,
complete: function(res) {
if (res.hasOwnProperty('translateResult')) {
//alert('识别结果:' + res.translateResult);
VR.cmdMatch(res.translateResult.split('。')[0]);
} else {
//alert('无法识别');
}
}
});
},
fail: function(res) {
alert(JSON.stringify(res));
}
});
},
getPlatformType: function(){ //1-IOS微信 2-安卓微信 0-未知
var ua = navigator.userAgent.toLowerCase();
if(/micromessenger/.test(ua)){
return /android/.test(ua)?'2':'1';
}
return '0';
},
init: function(){
if(VR.getPlatformType() == 2){//android采用点击方式录音,ios默认采用长按方式录音
$('.voice-remote').addClass('android');
}
wx.config({
debug: false,
appId: sign_pkg.appId,
timestamp: sign_pkg.timestamp,
nonceStr: sign_pkg.nonceStr,
signature: sign_pkg.signature,
jsApiList: [
'checkJsApi',
'startRecord',
'stopRecord',
'translateVoice'
]
});
wx.ready(function(){
$('.voice-remote').on('touchstart',function(){
VR.options.tpoint = +new Date();//记录touchstart时间点
if(VR.getPlatformType() == 2){//如果是android端,touchstart后判断当前是否处于录音状态,如果是则结束录音并识别语音
if($('.voice-remote').hasClass('active')){
$('.voice-remote').removeClass('active');//切换录音按钮状态
if(time < 5000){
clearInterval(VR.options.timer);//清除定时结束录音定时器
setTimeout(function(){//如果录音按钮有动画效果,需延迟结束录音,否则调用结束录音接口时,会导致动画卡顿,延时时长为动画效果时长
VR.translate();
},200);
}
return false;
}
}
wx.startRecord({
success: function(){
$('.voice-remote').addClass('active');
VR.options.point = +new Date();//记录开始录音成功时间点
VR.recode();//启用定时结束录音定时器
if(VR.options.point > VR.options.epoint && VR.options.epoint > VR.options.tpoint){//处理因为短按,startRecord还未初始成功,导致无法正常停止录音
clearInterval(VR.options.timer);
$('.voice-remote').removeClass('active');
setTimeout(function(){
VR.translate();
},200);
}
},
fail: function(res) {
alert(JSON.stringify(res));
},
cancel: function() {
alert('您拒绝了授权录音');
}
});
});
if(VR.getPlatformType() == 1){//ios端才监听touchend事件
$('.voice-remote').on('touchend',function(){
VR.options.epoint = +new Date();//记录touchend时间点
$(this).removeClass('active');
var time = +new Date() - VR.options.point;
if(time < 5000){//当录音间隔时间小于5s,touchend后清除定时结束录音定时器,并调用结束录音方法
clearInterval(VR.options.timer);
setTimeout(function(){
VR.translate();
},200);
}
});
}
});
}
}
VR.init();
PS:语音识别操作按钮,请看demo