recorder-core
Version:
Recorder库: html5 js 录音 mp3 wav ogg webm amr g711a g711u 格式,支持pc和Android、iOS部分浏览器、Hybrid App(提供Android iOS App源码)、微信,提供ASR语音识别转文字 H5版语音通话聊天示例 DTMF编码解码
450 lines (397 loc) • 14 kB
JavaScript
/*
RecordApp:基于Recorder的跨平台录音,支持在浏览器环境中使用(H5)、各种使用js来构建的程序中使用(App、小程序、UniApp、Electron、NodeJs)
https://github.com/xiangyuecn/Recorder
示例demo请参考根目录内的app-support-sample目录
使用时需先引入recorder-core和需要的编码器,再引入本js,再根据不同平台引入相应的app-xxx-support.js支持文件;如果引入的支持文件需要进行额外的配置,可参考app-support-sample目录内对应的配置文件。
可以仅使用RecordApp作为入口,可以不关心recorder-core中的方法,因为RecordApp已经对他进行了一次封装,并且屏蔽了非通用的功能。
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(win,rec,ni,ni.$T,browser);
//umd returnExports.js
if(typeof(define)=='function' && define.amd){
define(function(){
return win.RecordApp;
});
};
if(typeof(module)=='object' && module.exports){
module.exports=win.RecordApp;
};
}(function(Export,Recorder,i18n,$T,isBrowser){
;
var App={
LM:"2024-04-09 19:22"
//当前使用的平台实现
,Current:0
//已注册的平台实现
,Platforms:{}
};
var Platforms=App.Platforms;
var AppTxt="RecordApp";
var ReqTxt="RequestPermission";
var RegTxt="RegisterPlatform";
var WApp2=Export[AppTxt];//重复加载js
if(WApp2&&WApp2.LM==App.LM){
WApp2.CLog($T("uXtA::重复导入{1}",0,AppTxt),3);
return;
};
Export[AppTxt]=App;
Recorder[AppTxt]=App;
App.__SID_=0;//同步操作,防止同时多次调用
var SID=App.__SID=function(){ return ++App.__SID_; };
var Sync=App.__Sync=function(sid,tag,err){
if(App.__SID_!=sid){
if(tag){
CLog($T("kIBu::注意:因为并发调用了其他录音相关方法,当前 {1} 的调用结果已被丢弃且不会有回调",0,tag)+(err?", error: "+err:""),3);
}
return false;
}
return true;
};
var CLog=function(){
var v=arguments; v[0]="["+(CLog.Tag||AppTxt)+"]["+(App.Current&&App.Current.Key||"?")+"]"+v[0];
Recorder.CLog.apply(null,v);
};
App.CLog=CLog;
/**
注册一个平台的实现,对应的都会有一个app-xxx-support.js支持文件(Default-H5除外),config中提供统一的实现接口:
{
Support: fn( call(canUse) ) 判断此平台是否支持或开启,如果平台可用需回调call(true)选择使用这个平台,并忽略其他平台
CanProcess: fn() 此平台是否支持实时回调,返回true代表支持
Install: fn( success(),fail(errMsg) ) 可选,平台初始化安装,当使用此平台时会执行一次本方法(同一时间只会有一次调用,没有并发调用问题)
Pause: fn() 可选,暂停录音实现,如果返回false将执行默认暂停操作
Resume: fn() 可选,继续录音实现,如果返回false将执行默认继续操作
下面的方法中sid用于同步操作,在异步回调中用App.__Sync判断此sid是否处于同步状态
实现中使用到的Recorder实例需赋值给App.__Rec(Stop结束后会自动清理并赋值为null)
RequestPermission:fn(sid,success,fail) 实现录音权限请求,通过回调函数返回结果
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 没有权限或者不能录音时回调,如果是用户主动拒绝的录音权限,除了有错误消息外,isUserNotAllow=true,方便程序中做不同的提示,提升用户主动授权概率
Start:fn(sid,set,success,fail) 实现开始录音
set:{} 和Recorder的set大部分参数相同
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
Start_Check:fn(set) 可选,调用本实现的Start前执行环境检查,返回检查错误文本,如果返回false将执行默认检查操作
AllStart_Clean:fn(set) 可选,任何实现的Start前执行本配置清理,set里面可能为了兼容不同平台环境会传入额外的参数,可以进行清理,无返回值
Stop:fn(sid,success,fail) 实现结束录音,返回结果,success参数=null时仅清理资源
success:fn(arrayBuffer,duration,mime) 成功完成录音回调,参数可能为null
arrayBuffer:ArrayBuffer 录音数据
duration:123 //录音数据持续时间
mime:"audio/mp3" 录音数据格式
fail:fn(errMsg) 录音出错时回调
}
**/
App[RegTxt]=function(key,config){ //App.RegisterPlatform=function()
config.Key=key;
if(Platforms[key]){
CLog($T("ha2K::重复注册{1}",0,key),3);
}
Platforms[key]=config;
};
App.__StopOnlyClearMsg=function(){
return $T("wpTL::仅清理资源");
};
/****实现默认的H5统一接口*****/
var KeyH5="Default-H5",H5OpenSet=ReqTxt+"_H5OpenSet";
(function(){
var impl={
Support:function(call){
//默认的始终要支持
call(true);
}
,CanProcess:function(){
return true;//支持实时回调
}
};
App[RegTxt](KeyH5,impl);
impl[ReqTxt]=function(sid,success,fail){ //impl.RequestPermission=function()
var old=App.__Rec;
if(old){
old.close();
App.__Rec=null;
};
h5Kill();
//h5会提前打开录音,open时需要的配置只能单独配置
var h5Set=App[H5OpenSet]; App[H5OpenSet]=null;
var rec=Recorder(h5Set||{});
rec.open(function(){
//rec.close(); keep stream Stop时再关,免得Start再次请求权限
success();
},fail);
};
var h5Kill=function(){ //释放检测权限时已打开的录音
if(Recorder.IsOpen()){
CLog("kill open...");
var rec=Recorder();
rec.open();
rec.close();
};
};
impl.Start=function(sid,set,success,fail){
var appRec=App.__Rec;
if(appRec!=null){
appRec.stop();//未stop的stop掉
};
App.__Rec=appRec=Recorder(set);
appRec.appSet=set;
appRec.dataType="arraybuffer";
appRec.open(function(){
if(Sync(sid)){
appRec.start();
};
success();
},fail);
};
impl.Stop=function(sid,success,fail){
var appRec=App.__Rec;
var clearMsg=success?"":App.__StopOnlyClearMsg();
if(!appRec){
h5Kill(); //释放检测权限时已打开的录音
fail($T("bpvP::未开始录音")
+(clearMsg?" ("+clearMsg+")":""));
return;
};
var end=function(){
if(Sync(sid)){
appRec.close();
//把可能变更的配置写回去
for(var k in appRec.set){
appRec.appSet[k]=appRec.set[k];
};
};
};
var stopFail=function(msg){
end();
fail(msg);
};
if(!success){
stopFail(clearMsg);
return;
};
appRec.stop(function(arrBuf,duration,mime){
end();
success(arrBuf,duration,mime);
},stopFail);
};
})();
/***
获取底层平台录音过程中会使用用来处理实时数据的Recorder对象实例rec,如果底层录音过程中不使用Recorder进行数据的实时处理(目前没有),将返回null。Start调用前和Stop调用后均会返回null。
rec中的方法不一定都能使用,主要用来获取内部缓冲用的,比如实时清理缓冲。
***/
App.GetCurrentRecOrNull=function(){
return App.__Rec||null;
};
/**暂停录音**/
App.Pause=function(){
var cur=App.Current,key="Pause";
if(cur&&cur[key]){
if(cur[key]()!==false)return;
}
var rec=App.__Rec;
if(rec && canProc(key)){
rec.pause();
}
};
/**恢复录音**/
App.Resume=function(){
var cur=App.Current,key="Resume";
if(cur&&cur[key]){
if(cur[key]()!==false)return;
}
var rec=App.__Rec;
if(rec && canProc(key)){
rec.resume();
}
};
var canProc=function(tag){
var cur=App.Current;
if(cur&&cur.CanProcess()) return 1;
CLog($T("fLJD::当前环境不支持实时回调,无法进行{1}",0,tag),3);
};
/***
初始化安装,可反复调用
success: fn() 初始化成功
fail: fn(msg) 初始化失败
***/
App.Install=function(success,fail){
var cur=App.Current;
if(cur){ success&&success(); return; }
//因为此操作是异步的,为了避免竞争Current资源,此代码保证得到结果前只会发起一次调用
var reqs=App.__reqs||(App.__reqs=[]);
reqs.push({s:success,f:fail});
success=function(){ call("s",arguments) };
fail=function(){ call("f",arguments) };
var call=function(fn,args){
var arr=[].concat(reqs); reqs.length=0;
for(var i=0;i<arr.length;i++){
var f=arr[i][fn];
f&&f.apply(null,args);
};
};
if(reqs.length>1) return;
var keys=[KeyH5],key;
for(var k in Platforms){
if(k!=KeyH5)keys.push(k);
}
keys.reverse();
var initCur=function(idx){
key=keys[idx];
cur=Platforms[key];
cur.Support(function(canUse){
if(!canUse){
return initCur(idx+1);
};
if(cur.Install){
cur.Install(initOk,fail);
}else{
initOk();
};
});
};
var initOk=function(){
App.Current=cur;
CLog("Install platform: "+key);
success();
};
initCur(0);
};
/***
请求录音权限,如果当前环境不支持录音或用户拒绝将调用错误回调,调用start前需先至少调用一次此方法;请求权限后如果不使用了,不管有没有调用Start,至少要调用一次Stop来清理可能持有的资源。
success:fn() 有权限时回调
fail:fn(errMsg,isUserNotAllow) 没有权限或者不能录音时回调,如果是用户主动拒绝的录音权限,除了有错误消息外,isUserNotAllow=true,方便程序中做不同的提示,提升用户主动授权概率
***/
App[ReqTxt]=function(success,fail){ //App.RequestPermission=function(success,fail){
var sid=SID(),tag=AppTxt+"."+ReqTxt;
var failCall=function(errMsg,isUserNotAllow){
isUserNotAllow=!!isUserNotAllow;
var msg=errMsg+", isUserNotAllow:"+isUserNotAllow;
if(!Sync(sid,tag,msg))return;
CLog($T("YnzX::录音权限请求失败:")+msg,1);
fail&&fail(errMsg,isUserNotAllow);
};
CLog(ReqTxt+"...");
App.Install(function(){
if(!Sync(sid,tag))return;
var checkMsg=CheckH5();
if(checkMsg){ failCall(checkMsg); return; };
App.Current[ReqTxt](sid,function(){ //App.Current.RequestPermission()
if(!Sync(sid,tag))return;
CLog(ReqTxt+" Success");
success&&success();
},failCall);
},failCall);
};
var NeedReqMsg=function(){
return $T("nwKR::需先调用{1}",0,ReqTxt);
};
var CheckH5=function(){
var msg="";
if(App.Current.Key==KeyH5 && !isBrowser){
msg=$T("citA::当前不是浏览器环境,需引入针对此平台的支持文件({1}),或调用{2}自行实现接入",0,"src/app-support/app-xxx-support.js",AppTxt+"."+RegTxt);
};
return msg;
};
/***
开始录音,需先调用RequestPermission获得录音权限
set:设置默认值:{
type:"mp3"//最佳输出格式,如果底层实现能够支持就应当优先返回此格式
sampleRate:16000//最佳采样率hz
bitRate:16//最佳比特率kbps
onProcess:NOOP //如果当前环境支持实时回调(RecordApp.Current.CanProcess()),接收到录音数据时的回调函数:fn(buffers,powerLevel,bufferDuration,bufferSampleRate)
takeoffEncodeChunk:NOOP //fn(chunkBytes)
} 注意:此对象会被修改,因为平台实现时需要把实际使用的值存入此对象
success:fn() 打开录音时回调
fail:fn(errMsg) 开启录音出错时回调
***/
App.Start=function(set,success,fail){
var sid=SID(),tag=AppTxt+".Start";
var failCall=function(msg){
if(!Sync(sid,tag,msg))return;
CLog($T("ecp9::开始录音失败:")+msg,1);
fail&&fail(msg);
};
CLog("Start...");
var cur=App.Current;
if(!cur){
failCall(NeedReqMsg());
return;
};
set||(set={});
var obj={
type:"mp3"
,sampleRate:16000
,bitRate:16
,onProcess:function(){}
};
for(var k in obj){
set[k]||(set[k]=obj[k]);
};
//对配置项进行清理,set里面可能为了兼容不同平台环境会传入额外的参数,可以进行清理
for(var k in Platforms){
var pf=Platforms[k];
if(pf.AllStart_Clean){
pf.AllStart_Clean(set);
}
};
//先执行一下环境配置检查
var checkMsg=false;
if(cur.Start_Check){
checkMsg=cur.Start_Check(set);
};
if(checkMsg===false){
var checkRec=Recorder(set);
checkMsg=checkRec.envCheck({envName:cur.Key,canProcess:cur.CanProcess()});
if(!checkMsg) checkMsg=CheckH5();
}
if(checkMsg){
failCall($T("EKmS::不能录音:")+checkMsg);
return;
};
//重置Stop时的rec
App._SRec=0;
cur.Start(sid,set,function(){
if(!Sync(sid,tag))return;
CLog($T("k7Qo::已开始录音"),set);
App._STime=Date.now();
success&&success();
},failCall);
};
/***
结束录音
success:fn(arrayBuffer,duration,mime) 结束录音时回调
arrayBuffer:ArrayBuffer 录音二进制数据
duration:123 录音时长,单位毫秒
mime:"auido/mp3" 录音格式类型
fail:fn(errMsg) 录音出错时回调
本方法可以用来清理持有的资源,如果不提供success参数=null时,将不会进行音频编码操作,只进行清理完可能持有的资源后走fail回调
***/
App.Stop=function(success,fail){
var sid=SID(),tag=AppTxt+".Stop";
var failCall=function(msg){
if(!Sync(sid,tag,msg))return;
CLog($T("Douz::结束录音失败:")+msg,success?1:0);
try{
fail&&fail(msg);
}finally{ clear() }
};
var clear=function(){
App._SRec=App.__Rec;
App.__Rec=null;
};
CLog("Stop... "+$T("wqSH::和Start时差:{1}ms",0,App._STime?Date.now()-App._STime:-1)+" Recorder.LM:"+Recorder.LM+" "+AppTxt+".LM:"+App.LM);
var t1=Date.now();
var cur=App.Current;
if(!cur){
failCall(NeedReqMsg());
return;
};
cur.Stop(sid, !success?null:function(arrayBuffer,duration,mime){
if(!Sync(sid,tag))return;
CLog($T("g3VX::结束录音 耗时{1}ms 音频时长{2}ms 文件大小{3}b {4}",0,Date.now()-t1,duration,arrayBuffer.byteLength,mime));
try{
success(arrayBuffer,duration,mime);
}finally{ clear() }
},failCall);
};
}));