UNPKG

recorder-core-rlian

Version:

Recorder库:html5 js 录音 mp3 wav ogg webm amr 格式,支持pc和Android、ios部分浏览器、和Hybrid App(提供Android IOS App源码),微信也是支持的,提供H5版语音通话聊天示例 和DTMF编解码

529 lines (480 loc) 14.8 kB
/* 录音 RecordApp:ios上的微信支持文件。 特别注明:本文件涉及的功能需要后端的支持,如果你不能提供微信JsSDK签名、素材下载api,并且坚决要使用本文件,那将会很困难。 本文件诞生的原因是因为IOS端WKWebView(UIWebView)中未开放getUserMedia功能来录音,对应的微信内也不能用H5录音,只能寻求其他解决方案。Android端没有此问题。当以后IOS任何地方的网页都能录音时,本文件就可以删除了。 本文件源码可以不用改动,因为需要改动的地方已放到了app.js的IOS-Weixin.Config中了;最终实际实现可参考app-support-sample目录内的配置文件。 https://github.com/xiangyuecn/Recorder */ (function(){ "use strict"; var App=RecordApp; var CLog=App.CLog; var platform=App.Platforms.Weixin; var config=platform.Config; platform.IsInit=true; var WXRecordData={}; /*******给IOS-Weixin实现统一接口*******/ platform.RequestPermission=function(success,fail){ console.log('app-ios-weixin-support requestpermission开始 permissionState值:',permissionState ) // alert('permissionState值:'+permissionState) if(permissionState==5){//授权过,能管很久 console.log('受全国,成功') success(permissionState); return; }; permissionCalls.push({t:success,f:fail}); if(permissionState==1){ CLog("检测到连续的RequestPermission调用,wx环境已加入队列,这种操作是不建议的,如果是用户触发,应当开启遮罩层避免重复点击",3); return; }; permissionState=1; var end=function(err,isUser){ permissionState=err?0:5; var arr=permissionCalls; permissionCalls=[]; for(var i=0;i<arr.length;i++){ if(err){ arr[i].f(err,isUser); }else{ arr[i].t(permissionState); }; }; }; config.WxReady(function(wx,err){ WXRecordData.wx=null; if(err){ end("微信JsSDK准备失败:"+err); return; }; WXRecordData.wx=wx; //可能已经在录音了,暴力直接关掉一次再说,不然没法start killStart(function(){ //微信不能提前发起授权请求,需要开始录音时才会调起授权 ,并且授权一次后管很久,因此开始录音然后关闭就能检测出权限 wx.startRecord({ success:function(){ //只许关闭成功,不许失败 var kill=function(wait,Final){ setTimeout(function(){ stopNow(function(err){ if(err){ CLog("停止wx录音"+(Final?'[Final]':'')+":"+err,3); }; if(!err || Final){ end(); }else{ kill(1000,1);//加大延迟重试一次,再不行就忽略 }; }); },wait); }; kill(500); } ,fail:function(o){ end("无法微信录音:"+o.errMsg); } ,cancel:function(o){ end("用户不允许微信录音:"+o.errMsg,true); } }); }); }); }; var permissionState;//0 无动作 1请求中 5已有权限 var permissionCalls=[]; var isWaitStart,isStart,startID=0; var stopNow=function(call,keep){ if(!keep){ isStart=0; isWaitStart=0; }; WXRecordData.wx.stopRecord({ success:function(){ call&&call(); },fail:function(o){ call&&call("无法结束录音:"+o.errMsg); } }); }; var killStart=function(call,keep){ if(!keep && isStart){ CLog("录音中,正在kill重试",3); }; stopNow(function(){ setTimeout(call,300); },keep); }; platform.Start=function(set,success,fail){ var wx=WXRecordData.wx; if(!wx){ fail("请先调用RequestPermission"); return; }; if(isStart){ CLog("wx正在录音,但又开始新录音,kill掉老的",3); killStart(function(){ platform.Start(set,success,fail); }); return; }; if(isWaitStart){ CLog("wx上次Start还未完成,等待重试...",3); setTimeout(function(){ platform.Start(set,success,fail); },600); return; }; var id=++startID;//自增id阻止掉非本次的老的异步回调 var startFail=function(o){ isWaitStart=0; fail("开始微信录音失败:"+o.errMsg); stopNow(); }; //开始微信录音 var run=function(Final){ isStart=0; isWaitStart=1; wx.startRecord({ success:function(){ isStart=1; isWaitStart=0; WXRecordData.startTime=Date.now(); WXRecordData.start=set; CLog("wx已开始录音"); success(); } ,fail:function(e){ if(Final){ startFail(e); }else{ killStart(function(){//重试一次 run(1); }); }; } ,cancel:startFail }); }; run(); //监听超时自动停止后接续录音 WXRecordData.chunks=[]; WXRecordData.chunkErr=""; stopJoinEnd=null; stopWaitChunk=null; wx.onVoiceRecordEnd({ complete:function(res){ var t1=Date.now(); if(stopJoinEnd){ //正在进行stop调用时发生了onVoiceRecordEnd,此时不会触发stop回调,需要手动触发 stopJoinEnd(res,"chunk"); }else{ if(res.localId && WXRecordData.chunks){ WXRecordData.chunks.push({res:res,duration:t1-WXRecordData.startTime,time:t1,from:"chunk"}); }else{ //已彻底停止录音了,就不要塞数据了,丢弃 CLog("已忽略wx chunk数据",3,res); }; }; if(stopWaitChunk){//已停止,正在等待满一分钟的一块 stopWaitChunk(); return; }; CLog("wx录音超时,正在接续..."); var rerun=function(tryCount){ if(!isStart || id!=startID){ CLog("已停止wx录音,拒绝接续",3); return; }; wx.startRecord({ success:function(){ if(id==startID){//应该是相同的 WXRecordData.startTime=Date.now(); CLog("已接续wx录音,中断"+(Date.now()-t1)+"ms"); } } ,fail:function(o){ if(!isStart || id!=startID){ return; }; var msg="无法接续微信录音:"+o.errMsg; CLog(msg,1,o); if(tryCount>2){ WXRecordData.chunkErr=msg; }else{ tryCount++; CLog("尝试重启..."+tryCount); killStart(function(){//救一下,缺个几百毫秒已不重要了 rerun(tryCount); },1); }; } }); }; rerun(0); } }); }; var stopJoinEnd,stopWaitChunk; platform.Stop=function(successx,failx){ var wx=WXRecordData.wx; isStart=0; var hasWaitChunk=!!stopWaitChunk;//已收到59秒以上最后一块录音,无需停止录音了 stopWaitChunk=null; CLog("开始停止录音"); var failCall=function(msg){ failx("录音失败[wx]:"+(msg.errMsg||msg)); }; var set=WXRecordData.start; if(!set){ failCall("未开始录音"); return; }; //开始或接续录音后距离1分钟最大时间间隔太短,等待接续信号 var curDuration=Date.now()-WXRecordData.startTime; if(curDuration < 1000){ failx("录音太短!"); killStart(function(){ console.log('kill 成功') }) return; } if(!hasWaitChunk && curDuration>59100){ CLog("wx录音即将满1分钟,等待它录满,不然stop不可控...",3); stopWaitChunk=function(){ platform.Stop(successx,failx); }; return; }; WXRecordData.start=null; var dwxData={list:[]}; set.DownWxMediaData=dwxData; //格式转换 音质差是跟微信服务器返回的amr本来就音质差,转其他格式几乎无损音质,和微信本地播放音质有区别 var transform=function(){ var list=dwxData.list; var list0=list[0]; if(list0.duration){ //服务器端已经转码了,就直接返回 var bstr=atob(list0.data),n=bstr.length,u8arr=new Uint8Array(n); while(n--){ u8arr[n]=bstr.charCodeAt(n); }; var blob=new Blob([u8arr.buffer], {type:list0.mime}); App._SRec=null; CLog("微信素材服务器端已转码,不支持RecordApp.GetStopUsedRec方法",3); successx(blob,list0.duration); return; }; var pcms=[]; var enTime=0; var encode=function(){ enTime||(enTime=Date.now()); var pcm=[]; //采样率提升处理,copy自Recorder.SampleRaise(pcms,8000,set.sampleRate) var step=set.sampleRate/8000; if(step<=1){ step=1; }else{ CLog("微信arm素材采样率为8000hz(语音音质勉强能听),已自动提升成设置的采样率"+set.sampleRate+"hz,但音质不可能会变好",3); }; var posFloat=0,prev=0; for(var i=0;i<pcms.length;i++){ var o=pcms[i]; for(var i2=0;i2<o.length;i2++){ var cur=o[i2]; var pos=Math.floor(posFloat); posFloat+=step; var end=Math.floor(posFloat); //简单的从prev平滑填充到cur,有效减少转换引入的杂音 var n=(cur-prev)/(end-pos); for(var j=1;pos<end;pos++,j++){ pcm.push( Math.floor(prev+(j*n)) ); }; prev=cur; }; }; var rec=Recorder(set).mock(pcm,set.sampleRate); rec.stop(function(blob,duration){ dwxData.encodeTime=Date.now()-enTime; //把可能变更的配置写回去 for(var k in rec.set){ set[k]=rec.set[k]; }; App._SRec=rec; successx(blob,duration); },failCall); }; var deidx=0; var deTime=0; var decode=function(){ deTime||(deTime=Date.now()); if(deidx>=list.length){ dwxData.decodeTime=Date.now()-deTime; encode(); return; }; var data=list[deidx]; data.duration=chunkList[deidx].duration; data.isAmr=true; var bstr=atob(data.data),n=bstr.length,u8arr=new Uint8Array(n); while(n--){ u8arr[n]=bstr.charCodeAt(n); }; Recorder.AMR.decode(u8arr,function(pcm){ pcms.push(pcm); deidx++; decode(); },function(msg){ failCall("AMR音频"+(deidx+1)+"无法解码:"+msg); }); }; if(Recorder.AMR){ decode(); }else{ failCall("未加载AMR转换引擎"); }; }; //上传微信本地录音,然后通过微信素材接口下载录音数据 var mediaIds=[]; var stopFn=function(){ if(!successx){//仅清理资源的直接返回,避免进行上传和下载操作 failCall("仅清理资源"); return; }; var upIds=[]; for(var i=0;i<chunkList.length;i++){ upIds.push(chunkList[i].res.localId); }; CLog("结束录音共"+upIds.length+"段,开始上传下载",upIds,chunkList); if(!upIds.length){ failCall("未获得任何录音"); return; }; //下载所有的片段 var downidx=0; var downStart=0; var downEnd=function(){ dwxData.downTime=Date.now()-downStart; //上传下载都完成了,进行转码 CLog("开始转码..."); transform(); }; var down=function(tryCount){ downStart||(downStart=Date.now()); if(downidx>=mediaIds.length){ downEnd(); return; }; var serverId=mediaIds[downidx]; config.DownWxMedia({ mediaId:serverId ,transform_mediaIds:mediaIds.join(",") ,transform_type:set.type ,transform_bitRate:set.bitRate ,transform_sampleRate:set.sampleRate },function(data){ dwxData.list.push(data); //转码结果,已全部转换合并好了 if(data.duration){ downEnd(); return; }; if(/amr/i.test(data.mime)){ downidx++; down(); }else{ failCall("微信服务器返回了未知音频类型:"+data.mime); }; },function(msg){ tryCount=tryCount||0; if(tryCount>2){ failCall("下载微信音频失败:"+msg); }else{ tryCount++; CLog("DownWxMedia失败,重试..."+tryCount,1,msg); down(tryCount); }; }); }; //微信上传所有片段 var upidx=0; var up=function(tryCount){ if(upidx>=upIds.length){ dwxData.uploadTime=Date.now()-upStart; CLog("开始下载微信素材..."); down(); return; }; var localId=upIds[upidx]; CLog("wx上传本地录音["+upidx+"] wx.playVoice({localId:'"+localId+"'})"); wx.uploadVoice({ localId:localId ,isShowProgressTips:0 ,fail:function(e){ tryCount=tryCount||0; if(tryCount>2){ failCall("微信uploadVoice失败["+upidx+"]:"+e.errMsg); }else{ tryCount++; CLog("uploadVoice失败,重试..."+tryCount,1,e); up(tryCount); }; } ,success:function(res){ var serverId=res.serverId; CLog("上传OK serverId:"+serverId); mediaIds.push(serverId); upidx++; up(); } }); }; var upStart=Date.now(); up(); }; var chunkList=WXRecordData.chunks; if(WXRecordData.chunkErr){//接续错误重试也没用的 就返回错误 CLog(WXRecordData.chunkErr,1,chunkList); failCall("录制失败,已录制"+chunkList.length+"分钟,但后面出错:"+WXRecordData.chunkErr); return; }; if(hasWaitChunk){//已收到59秒以上最后一块录音,无需停止录音了 stopFn(); return; }; if(chunkList.length){ if(Date.now()-chunkList[chunkList.length-1].time<900){ CLog("丢弃结尾未停止太短录音",3); stopNow(); stopFn(); return; }; }; //等待停止回调,或者onVoiceRecordEnd回调,如果停止过程中发生了onVoiceRecordEnd可能不会触发stop回调,虽然开头已经处理了快满1分钟的情况,但还是要补刀一下 stopJoinEnd=function(res,from){ stopJoinEnd=null; var t1=Date.now(); if(res.localId){ chunkList.push({res:res,duration:t1-WXRecordData.startTime,time:t1,from:from}); }else{ //定时n分钟录音时,当刚刚接续录音,然后立即出发停止时,返回数据没有localId CLog("已忽略"+from+"数据",3,res); }; WXRecordData.chunks=null;//不要继续塞数据了,就算有也丢弃 stopFn(); }; wx.stopRecord({ fail:function(e){ stopJoinEnd=null; //出错了,如果录音太短,尽力将有效的部分返回 if(chunkList.length && curDuration<3000){ CLog("停止录音出错,但后续录音太短,已忽略此错误:"+e.errMsg,3); stopFn(); }else{ failCall(e); }; } ,success:function(res){ stopJoinEnd&&stopJoinEnd(res,"stop"); } }); }; })();