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
JavaScript
/*
录音 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");
}
});
};
})();