recorder-core-rlian
Version:
Recorder库:html5 js 录音 mp3 wav ogg webm amr 格式,支持pc和Android、ios部分浏览器、和Hybrid App(提供Android IOS App源码),微信也是支持的,提供H5版语音通话聊天示例 和DTMF编解码
678 lines (596 loc) • 22.8 kB
JavaScript
/*
录音 RecordApp: app统一录音接口支持,用于兼容原生应用、ios上的微信 等,需要和app-ios-weixin-support.js(支持IOS上的微信)、app-native-support.js(支持原生应用)配合使用,注:这几个support源码文件会自动加载,dist目录内已自动压缩合并到本文件中。
特别注明:使用本功能虽然可以最大限度的兼容Android和IOS,但需ios-weixin需要后端提供支持,native需要app端提供支持,具体情况查看相应的文件。
本功能独立于recorder-core.js,可以仅使用RecordApp作为入口,可以不关心recorder-core中的方法,因为RecordApp已经对他进行了一次封装,并且屏蔽了非通用的功能。
注意:此文件并非拿来就能用的,需要改动【需实现】标注的地方;也可以不改动此文件,使用另外的初始化配置文件来进行配置,可参考app-support-sample目录内的配置文件。
https://github.com/xiangyuecn/Recorder
*/
(function(factory){
factory(window);
//umd returnExports.js
if(typeof(define)=='function' && define.amd){
define(function(){
return RecordApp;
});
};
if(typeof(module)=='object' && module.exports){
module.exports=RecordApp;
};
}(function(window){
;
var IsWx=/MicroMessenger/i.test(navigator.userAgent);
//文件基础目录,此目录内包含recorder-core.js、engine等。实际取值需自行根据自己的网站目录调整,或者加载本js前,设置RecordAppBaseFolder全局变量。
//【需实现】
var BaseFolder=window.RecordAppBaseFolder || /*=:=*/ "https://xiangyuecn.gitee.io/recorder/src/" /*<@ "/Recorder/dist/" @>*/; //编译指令:源码时使用前面的文件夹,压缩时使用后面的文件夹
//提供一个回调fn(),免得BaseFolder要在这个文件之前定义,其他值又要在之后定义的麻烦。
var OnInstalled=window.OnRecordAppInstalled;
/*
可能支持的底层平台列表实现配置,对应的都会有一个app-xxx-support.js文件(Default为使用recorder-core.js除外)
每个实现配置内包含两个值:Support和Config
Support: fn( call(canUse) ) 判断此底层是否支持或开启,如果底层可用需回调call(true)选择使用这个底层平台,并忽略其他平台
Config: 此平台的配置选项,会传入app-xxx-support.js中,具体需要什么配置也一样参考这个js文件里面的说明
*/
var Config_SupportPlatforms=[
{
Key:"Native"
,Support:function(call){
if(!App.AlwaysAppUseJS){
Native.Config.IsApp(call);
return;
};
//不支持app原生录音
call(false);
}
,CanProcess:function(){
return true;//支持实时回调
}
,Config:{
IsApp:function(call){
//如需打开原生App支持,此方法【需实现】,此方法用来判断:1. 判断app是否是在环境中 2. app支持录音
call(false);//默认实现不支持app原生录音,支持就回调call(true)
}
,JsBridgeRequestPermission:function(success,fail){
/*如需打开原生App支持,此方法【需实现】
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 出错回调
*/
fail("JsBridgeRequestPermission未实现");
}
,JsBridgeStart:function(set,success,fail){
/*如需打开原生App支持,此方法【需实现】,app打开录音后原生层定时返回PCM数据时JsBridge js层需要回调set.onProcess。建议JsBridge增加一个Alive接口,为录音时定时心跳请求,如果网页超过10秒未调用此接口,app原生层自动停止录音,防止stop不能调用导致的资源泄露。
set:RecordApp.Start的set参数
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
*/
fail("JsBridgeStart未实现");
}
,JsBridgeStop:function(success,fail){
/*如需打开原生App支持,此方法【需实现】
success:fn() 结束录音时回调
fail:fn(errMsg) 结束录音出错时回调
*/
fail("JsBridgeStop未实现");
}
,paths:[//当在app中使用此实现时,会自动把这些js全部加载
{url:BaseFolder+"app-support/app-native-support.js",check:function(){return !Native.IsInit}}
]
}
,ExtendDefault:true //初始化时自动加载Recorder
}
,{
Key:"IOS-Weixin"
,Support:function(call){
if(!App.AlwaysUseWeixinJS){
if(Recorder.Support()){//浏览器支持录音就不走微信的渣渣接口了
call(false);
return;
};
};
//如果是微信环境 就返回支持
Weixin.Config.Enable(function(enable){
call(enable?IsWx:false);
});
}
,CanProcess:function(){
return false;//不支持实时回调
}
,Config:{
Enable:function(call){
//是否启用微信支持,默认启用,如果要禁用就回调call(false)
call(true);
}
,WxReady:function(call){
//【需实现】
//此方法需要自行实现,需要在微信JsSDK wx.config好后调用call(wx,errMsg)函数,成功只需要提供wx对象,如果失败需要提供errMsg错误原因。微信JsSDK wx.config需使用到后端接口进行签名,文档: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 阅读:通过config接口注入权限验证配置、附录1-JS-SDK使用权限签名算法
call(null,"未实现IOS-Weixin.Config.WxReady");
}
,DownWxMedia:function(param,success,fail){
/*【需实现】
下载微信录音素材,服务器端接口文档: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738727
param:{//接口调用参数
mediaId:"" 录音接口上传得到的微信服务器上的ID,用于下载单个素材(如果录音分了多段,会循环调用DownWxMedia);如果服务器会进行转码,请忽略这个参数
transform_mediaIds:"mediaId,mediaId,mediaId" 1个及以上mediaId,半角逗号分隔,用于服务器端进行转码用的,正常情况下这个参数用不到。如果服务器端会进行转码,需要把这些素材全部下载下来,然后按顺序合并为一个音频文件
transform_type:"mp3" 录音set中的类型,用于转码结果类型,正常情况下这个参数用不到。如果服务器端会进行转码,接口返回的mime必须是:audio/type(如:audio/mp3)。
transform_bitRate:123 建议的比特率,转码用的,同transform_type
transform_sampleRate:123 建议的采样率,转码用的,同transform_type
* 素材下载的amr音质很渣,也许可以通过高清接口获得清晰点的speex音频,那么transform_*参数就有用武之地;直接下载的amr只需用mediaId参数就可以了。
}
success: fn(obj) 下载成功返回结果
obj:{
mime:"audio/amr" //这个值是服务器端请求临时素材接口返回的Content-Type响应头,未转码必须是audio/amr;如果服务器进行了转码,是转码后的类型mime,并且提供duration
,data:"base64文本" //服务器端下载到或转码的文件二进制内容进行base64编码
,duration:0 //音频时长,如果服务器端进行了转码,必须返回这个参数并且>0,否则不要提供或者直接给0
}
fail: fn(msg) 下载出错回调
*/
fail("下载素材接口DownWxMedia未实现");
}
,paths:[//当在微信中使用此实现时,会自动把这些js全部加载
{url:BaseFolder+"app-support/app-ios-weixin-support.js",check:function(){return !Weixin.IsInit}}
//amr解码引擎文件,因为微信临时素材接口返回的音频为amr格式,刚好有amr解码器,省去了服务器端的复杂性。amr解码器只是在Stop时才需要,因此可以在Stop前任何时候进行延迟加载
,{url:BaseFolder+"engine/beta-amr.js",lazyBeforeStop:1,check:function(){return !Recorder.prototype.amr}}
/*=:=*/
,{url:BaseFolder+"engine/beta-amr-engine.js",lazyBeforeStop:1,check:function(){return !Recorder.AMR}}
,{url:BaseFolder+"engine/wav.js",lazyBeforeStop:1,check:function(){return !Recorder.prototype.wav}}
/*<@ @>*/
]
}
,ExtendDefault:true //初始化时自动加载Recorder
}
,{
Key:"Default"
,Support:function(call){
//默认的始终要支持
call(true);
}
,CanProcess:function(){
return true;//支持实时回调
}
,Config:{
paths:[//当使用默认实现时,会自动把这些js全部加载,如果core和编码器已手动加载,可以把此数组清空;另外需要其他编码格式的时候,直接把编码引擎加在后面(不需要mp3格式就删掉),会自动加载
{url:BaseFolder+"recorder-core.js",check:function(){return !window.Recorder}}
//自动加载需要的编码引擎,这里默认提供mp3格式。因为编码引擎只有在Start时才需要,因此可以在Start前任何时候进行延迟加载
,{url:BaseFolder+"engine/mp3.js",lazyBeforeStart:1,check:function(){return !Recorder.prototype.mp3}}
/*=:=*/ ,{url:BaseFolder+"engine/mp3-engine.js",lazyBeforeStart:1,check:function(){return !Recorder.lamejs}} /*<@ @>*/ //编译指令:压缩后mp3-engine已包含在了mp3.js中
]
}
}
];
var Native=Config_SupportPlatforms[0];
var Weixin=Config_SupportPlatforms[1];
var Default=Config_SupportPlatforms[2];
//给Default实现统一接口
Default.RequestPermission=function(success,fail){
var old=App.__Rec;
if(old){
old.close();
App.__Rec=null;
};
var rec=Recorder();
rec.open(function(){
//rec.close(); keep stream Stop时再关,免得Start再次请求权限
success();
},fail);
};
Default.Start=function(set,success,fail){
var appRec=App.__Rec;
if(appRec!=null){
appRec.close();//stream已经被使用过了,close更好
};
App.__Rec=appRec=Recorder(set);
appRec.appSet=set;
appRec.open(function(){
appRec.start();
success();
},function(msg){
fail(msg);
});
};
Default.Stop=function(success,fail){
var appRec=App.__Rec;
if(!appRec){
if(Recorder.IsOpen()){
//释放检测权限时已打开的录音
appRec=Recorder();
appRec.open();
appRec.close();
};
fail("未开始录音");
return;
};
var end=function(){
appRec.close();
//把可能变更的配置写回去
for(var k in appRec.set){
appRec.appSet[k]=appRec.set[k];
};
};
var stopFail=function(msg){
end();
fail(msg);
App.__Rec=null;
};
if(!success){
stopFail("仅清理资源");
return;
};
appRec.stop(function(blob,duration){
end();
App._SRec=appRec;
success(blob,duration);
App.__Rec=null;
},stopFail);
};
//带时间的日志输出,CLog(msg,errOrLogMsg, logMsg...) err为数字时代表日志类型1:error 2:log默认 3:warn,否则当做内容输出,第一个参数不能是对象因为要拼接时间,后面可以接无数个输出参数
var CLog=function(msg,err){
var now=new Date();
var t=("0"+now.getMinutes()).substr(-2)
+":"+("0"+now.getSeconds()).substr(-2)
+"."+("00"+now.getMilliseconds()).substr(-3);
var arr=["["+t+" RecordApp]["+(App.Current&&App.Current.Key||"?")+"]"+msg];
var a=arguments;
var i=2,fn=console.log;
if(typeof(err)=="number"){
fn=err==1?console.error:err==3?console.warn:fn;
}else{
i=1;
};
for(;i<a.length;i++){
arr.push(a[i]);
};
fn.apply(console,arr);
};
var App={
LM:"2020-11-15 21:36:11"
,Current:0
,CLog:CLog
,IsWx:IsWx
,BaseFolder:BaseFolder
,UseLazyLoad:true //默认为true开启部分非核心组件的延迟加载,不会阻塞Install,Install后通过RecordApp.Current.OnLazyReady事件来确定组件是否已全部加载;如果设为false,将忽略组件的延迟加载属性,Install时会将所有组件一次性加载完成后才会Install成功
,AlwaysUseWeixinJS:false //测试用的,微信里面总是使用微信的接口,方便Android上调试
,AlwaysAppUseJS:false //测试用的,App里面总是使用网页版录音,用于测试App里面的网页兼容性
,Platforms:{
Native:Native
,Weixin:Weixin
,Default:Default
}
,Js:function(urls,True,False,win){
win=win||window;
var doc=win.document;
var load=function(idx){
if(idx>=urls.length){
True();
return;
};
var itm=urls[idx];
var url=itm.url;
if(itm.check()===false){
load(idx+1);
return;
};
var elem=doc.createElement("script");
elem.setAttribute("type","text/javascript");
elem.setAttribute("src",url);
elem.onload=function(){
load(idx+1);
};
elem.onerror=function(e){
False("请求失败:"+(e.message||"-")+","+url);
};
doc.body.appendChild(elem);
};
load(0);
}
/*
初始化安装,可反复调用
success: fn() 初始化成功
fail: fn(msg) 初始化失败
*/
,Install:function(success,fail){
//因为此操作是异步的,为了避免竞争Current资源,此代码保证得到结果前只会发起一次调用
var reqs=App.__reqs||(App.__reqs=[]);
reqs.push({s:success,f:fail});
success=function(){
call("s",arguments);
};
fail=function(errMsg,isUserNotAllow){
call("f",arguments);
};
var call=function(fn,args){
for(var i=0;i<reqs.length;i++){
reqs[i][fn].apply(null,args);
};
reqs.length=0;
};
if(reqs.length>1){
return;
};
var readConfigPaths=function(platform,jsLazyStart,jsLazyStop){
var config=platform.Config;
var paths=config.paths,jsArr=[];
for(var i=0,o;i<paths.length;i++){
o=paths[i];
if(App.UseLazyLoad){
if(o.lazyBeforeStart){
jsLazyStart&&jsLazyStart.push(o);
}else if(o.lazyBeforeStop){
jsLazyStop&&jsLazyStop.push(o);
}else{
jsArr.push(o);
};
}else{
jsArr.push(o);
};
};
return jsArr;
};
var idx=0;
var initPlatform=function(platform,end){
//此平台已完成初始化
if(platform.IsInit){
end();
return;
};
var jsArr=readConfigPaths(platform);
CLog("Install "+platform.Key+"...",jsArr);
//加载需要的支持js文件
App.Js(jsArr,function(){
platform.IsInit=true;
end();
},function(msg){
msg="初始化js库失败:"+msg;
CLog(msg,platform);
fail(msg);
});
};
var tryLazyLoad=function(platform,first){
//尝试进行延迟加载,可能已经加载完成、未加载、加载错误
var jsLazyStart=[],jsLazyStop=[];
readConfigPaths(platform,jsLazyStart,jsLazyStop);
if(platform.ExtendDefault){
readConfigPaths(Default,jsLazyStart,jsLazyStop);
};
var data=platform.LazyReady;
if(!data){
data=platform.LazyReady={
fsta:[] //start bind functions
,fsto:[] //stop bind functions
,fall:[] //all ready bind functions
,usta:0 //start status 0未加载 1加载失败 2加载中 3加载成功
,usto:0 //stop status
,msta:"" //start error msg
,msto:"" //stop error msg
};
platform.LazyAtStart=function(fn){
startLoadEnd()?fn(data.msta):data.fsta.push(fn);
};
platform.LazyAtStop=function(fn){
stopLoadEnd()?fn(data.msto):data.fsto.push(fn);
};
platform.OnLazyReady=function(fn){
startLoadEnd()&&stopLoadEnd()?fn(data.msta||data.msto):data.fall.push(fn);
};
};
var startLoadEnd=function(){return data.usta==1||data.usta==3};
var stopLoadEnd=function(){return data.usto==1||data.usto==3};
var statusKey=first?"usta":"usto";
var msgKey=first?"msta":"msto";
var fnsKey=first?"fsta":"fsto";
var loadNext=function(loadEnd){
if(loadEnd){
CLog("Lazy Load:"+statusKey);
};
var callBinds=function(key,err){
var fns=data[key];
data[key]=[];//先清空再说
for(var i=0;i<fns.length;i++){
fns[i](err);
};
};
if(data[statusKey]!=2){
callBinds(fnsKey,data[msgKey]);
};
if(startLoadEnd()&&stopLoadEnd()){
callBinds("fall",data.msta||data.msto);
};
if(first){
tryLazyLoad(platform);
};
};
if(data[statusKey]<2){
data[msgKey]="";
data[statusKey]=2;
var jsArr=first?jsLazyStart:jsLazyStop;
CLog("Lazy Load..."+statusKey,jsArr);
App.Js(jsArr,function(){
data[statusKey]=3;
loadNext(1);
},function(msg){
msg="加载js库失败["+statusKey+"]:"+msg;
CLog(msg,platform);
data[statusKey]=1;
data[msgKey]=msg;
loadNext(1);
});
}else{
loadNext();
};
};
var next=function(cur){
//遍历选择支持的底层平台
if(!cur){
cur=Config_SupportPlatforms[idx];
var initEnd=function(){
cur.Support(function(canUse){
if(canUse){
initPlatform(cur,function(){
next(cur);
});
}else{
idx++;
next();
};
});
};
//需要Default平台支持,先初始化再说
if(cur.ExtendDefault){
initPlatform(Default,initEnd);
}else{
initEnd();
};
return;
};
//先注册延迟加载事件函数,开始进行延迟加载
tryLazyLoad(cur,1);
//已获取支持的底层平台
App.Current=cur;
CLog("Install Success");
success();
};
next(App.Current);
}
/*
获取底层平台录音过程中会使用用来处理实时数据的Recorder对象实例rec,如果底层录音过程中不实用Recorder进行数据的实时处理,将返回null。除了微信平台外,其他平台均会返回rec,但Start调用前和Stop调用后均会返回null,只有Start后和Stop彻底完成前之间才会返回rec。
rec中的方法不一定都能使用,主要用来获取内部缓冲用的,比如实时清理缓冲。
*/
,GetStartUsedRecOrNull:function(){
return App.__Rec||null;
}
/*
获取底层平台录音结束时使用的用来转码音频的Recorder对象实例rec。在Stop成功回调时一定会返回rec对象,Stop回调前和Stop回调后均会返回null。除了微信平台外,其他平台返回的rec和GetStartUsedRecOrNull返回的是同一个对象;(注意如果微信平台的素材下载接口实现了服务器端转码,本方法始终会返回null,这种情况算是比较罕见的功能)。
rec中的方法不一定都能使用,主要用来获取内部缓冲用的,比如额外的格式转码或数据提取。
*/
,GetStopUsedRec:function(){
return App._SRec||null;
}
/*
请求录音权限,如果当前环境不支持录音或用户拒绝将调用错误回调,调用start前需先至少调用一次此方法;请求权限后如果不使用了,不管有没有调用Start,至少要调用一次Stop来清理可能持有的资源。
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 没有权限或者不能录音时回调,如果是用户主动拒绝的录音权限,除了有错误消息外,isUserNotAllow=true,方便程序中做不同的提示,提升用户主动授权概率
*/
,RequestPermission:function(success,fail){
var failCall=function(errMsg,isUserNotAllow){
isUserNotAllow=!!isUserNotAllow;
CLog("录音权限请求失败:"+errMsg+",isUserNotAllow:"+isUserNotAllow,1);
fail&&fail(errMsg,isUserNotAllow);
};
CLog("RequestPermission...");
App.Install(function(){
var cur=App.Current;
CLog("开始请求录音权限");
cur.RequestPermission(function(){
CLog("录音权限请求成功");
success&&success();
},failCall);
},function(err){
failCall("Install失败:"+err);
});
}
/*
开始录音,需先调用RequestPermission
注:如果对应的底层实现可以实时返回PCM数据,Platform应调用set.onProcess方法。注意如果回调速率超过1秒10次,建议限制成一秒10次(可丢弃未回调数据)
set:设置默认值:{
type:"mp3"//最佳输出格式,如果底层实现能够支持就应当优先返回此格式
sampleRate:16000//最佳采样率hz
bitRate:16//最佳比特率kbps
onProcess:NOOP //如果当前环境支持实时回调(RecordApp.Current.CanProcess()),接收到录音数据时的回调函数:fn(buffers,powerLevel,bufferDuration,bufferSampleRate)
} 注意:此对象会被修改,因为平台实现时需要把实际使用的值存入此对象
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
*/
,Start:function(set,success,fail){
var failCall=function(msg){
CLog("开始录音失败:"+msg,1);
fail&&fail(msg);
};
CLog("Start...");
var cur=App.Current;
if(!cur){
failCall("需先调用RequestPermission");
return;
};
set||(set={});
var obj={
type:"mp3"
,sampleRate:16000
,bitRate:16
,onProcess:function(){}
};
for(var k in obj){
set[k]||(set[k]=obj[k]);
};
//先执行一下环境配置检查
var checkRec=Recorder(set);
var checkMsg=checkRec.envCheck({envName:cur.Key,canProcess:cur.CanProcess()});
if(checkMsg){
failCall("不能录音:"+checkMsg);
return;
};
//重置Stop时的rec
App._SRec=0;
var readyWait=0;
cur.LazyAtStart(function(err){
if(readyWait){
CLog("Start Wait Ready "+(Date.now()-readyWait)+"ms",3);
};
readyWait=1;
if(err){
failCall(err);
}else{
cur.Start(set,function(){
CLog("开始录音",set);
success&&success();
},failCall);
};
});
if(!readyWait){
CLog("Start Wait Ready...",3);
};
readyWait=Date.now();
}
/*
结束录音
success:fn(blob,duration) 结束录音时回调
blob:Blob 录音数据audio/mp3|wav...格式
duration:123 //音频持续时间
fail:fn(errMsg) 录音出错时回调
本方法可以用来清理持有的资源,如果不提供success参数=null时,将不会进行音频编码操作,只进行清理完可能持有的资源后走fail回调
*/
,Stop:function(success,fail){
var failCall=function(msg){
CLog("结束录音失败:"+msg,1);
App._SRec=0;//干掉未释放的Stop的rec,并防止fail回调中被读取
fail&&fail(msg);
};
CLog("Stop...");
var t1=Date.now();
var cur=App.Current;
if(!cur){
failCall("需先调用RequestPermission");
return;
};
var readyWait=0;
cur.LazyAtStop(function(err){
if(readyWait){
CLog("Stop Wait Ready "+(Date.now()-readyWait)+"ms",3);
};
readyWait=1;
if(err){
failCall(err);
}else{
cur.Stop(!success?null:function(blob,duration){
CLog("结束录音 耗时"+(Date.now()-t1)+"ms 音频"+duration+"ms/"+blob.size+"b");
success(blob, duration);
App._SRec=0;//清理掉Stop时的rec
},failCall);
};
});
if(!readyWait){
CLog("Stop Wait Ready...",3);
};
readyWait=Date.now();
}
};
window.RecordApp=App;
OnInstalled&&OnInstalled();
}));