Skip to content

Commit

Permalink
统一Recorder和Record的set.onProcess回调,废弃RecordApp.OnProcess,RecordApp支持实时…
Browse files Browse the repository at this point in the history
…转码传输特性
  • Loading branch information
xiangyuecn committed Aug 29, 2019
1 parent f2744ec commit 646df8a
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 197 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,12 @@ public void onPermissionRequest(PermissionRequest request) {


# :open_book:语音通话聊天demo:实时编码、传输与播放验证
[线测试Demo](https://xiangyuecn.github.io/Recorder/)中包含了一个语音通话聊天的测试功能,没有服务器支持所以仅支持局域网内一对一语音。用两个设备(浏览器打开两个标签也可以)打开demo,勾选H5版语音通话聊天,按提示交换两个设备的信息即可成功进行P2P连接,然后进行语音。
[线测试Demo](https://xiangyuecn.github.io/Recorder/)中包含了一个语音通话聊天的测试功能,没有服务器支持所以仅支持局域网内一对一语音。用两个设备(浏览器打开两个标签也可以)打开demo,勾选H5版语音通话聊天,按提示交换两个设备的信息即可成功进行P2P连接,然后进行语音。实际使用时数据传输可以用WebSocket,会简单好多。

编写本语音测试的目的在于验证H5录音实时转码、传输的可行性,并验证实时转码mp3格式小片段文件接收后的可播放性。经测试发现:除了移动端可能存在设备性能低下的问题以外,录音后实时转码mp3并传输给对方是可行的,对方接收后播放也能连贯的播放(效果还是要看播放代码写的怎么样,目前没有比较完美的播放代码)。另外(16kbps,16khz)MP3开语音15分钟大概3M的流量,wav 15分钟要37M多流量。

另外除wav外MP3等格式编码出来的音频的播放时间比PCM原始数据要长一些或短一些,如果涉及到解码或拼接时,这个地方需要注意。

![](assets/use_webrtc.png)


Expand Down
42 changes: 14 additions & 28 deletions app-support-sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ IOS其他浏览器||

## 支持功能

- 会自动加载`Recorder`,因此`Recorder`支持的功能,`RecordApp`基本上都能支持,包括语音通话聊天。
- 优先使用`Recorder` H5进行录音,如果浏览器不支持将使用`IOS-Weixin`选项。
- 默认开启`IOS-Weixin`支持,用于支持IOS中微信`H5``小程序WebView`的录音功能,参考[ios-weixin-config.js](ios-weixin-config.js)接入配置。
- 可选手动开启`Native`支持,用于支持IOS、Android上的Hybrid App录音,默认未开启支持,参考[native-config.js](native-config.js)开启`Native`支持配置,实现自己App的`JsBridge`接口调用;本方式优先级最高。


## 限制功能

- `IOS-Weixin`不支持实时回调,因此当在IOS微信上录音时,实时音量反馈、实时波形等功能不会有效果;并且微信素材下载接口下载的amr音频音质勉强能听(总比没有好,自行实现时也许可以使用它的高清接口,不过需要服务器端转码)。
- `IOS-Weixin`不支持实时回调,因此当在IOS微信上录音时,实时音量反馈、实时波形、实时转码等功能不会有效果;并且微信素材下载接口下载的amr音频音质勉强能听(总比没有好,自行实现时也许可以使用它的高清接口,不过需要服务器端转码)。
- `IOS-Weixin`使用的`微信JsSDK`单次调用录音最长为60秒,底层已屏蔽了这个限制,超时后会立即重启接续录音,因此当在IOS微信上录音时,超过60秒还未停止,将重启录音,中间可能会导致短暂的停顿感觉。
- `demo_ios`中swift代码使用的`AVAudioRecorder`来录音,由于录音数据是通过这个对象写入文件来获取的,可能是因为存在文件写入缓存的原因,数据并非实时的flush到文件的,因此实时发送给js的数据存在300ms左右的滞后;`AudioQueue``AudioUnit`之类的更强大的工具文章又少,代码又多,本质上是因为不会用,所以就成这样了。
- `Android WebView`本身是支持录音的(古董版本就算啦),仅需处理网页授权即可,但Android里面使用网页的录音权限问题可能比原生的权限机制要复杂,为了简化js端的复杂性(出问题了好甩锅),不管是Android还是IOS都实现一下可能会简单很多;另外Android和IOS的音频编码并非易事,且不易更新,使用js编码引擎大大简化App的逻辑;因此就有了Android版的Hybrid App Demo。
Expand Down Expand Up @@ -98,7 +99,7 @@ IOS其他浏览器||
RecordApp.RequestPermission(function(){
//dialog&&dialog.Cancel(); 如果开启了弹框,此处需要取消

RecordApp.Start({},function(){//使用默认配置开始录音,mp3格式
RecordApp.Start({type:"mp3",sampleRate:16000},function(){//mp3格式,指定采样率,其他参数使用默认配置;注意:是数字的参数必须提供数字,不要用字符串;需要使用的type类型,需提前把支持文件到Platforms.Default内注册
setTimeout(function(){
RecordApp.Stop(function(blob,duration){//到达指定条件停止录音
console.log((window.URL||webkitURL).createObjectURL(blob),"时长:"+duration+"ms");
Expand All @@ -118,10 +119,6 @@ RecordApp.RequestPermission(function(){
});


RecordApp.OnProcess=function(pcmDatas,powerLevel,duration,sampleRate){
//console.log("PCM实时回调",powerLevel,duration,sampleRate);
};

//我们可以选择性的弹一个对话框:为了防止当移动端浏览器使用Recorder H5录音时存在第三种情况:用户忽略,并且(或者国产系统UC系)浏览器没有任何回调
/*伪代码:
function createDelayDialog(){
Expand Down Expand Up @@ -170,14 +167,16 @@ function createDelayDialog(){
## 【静态方法】RecordApp.Start(set,success,fail)
开始录音,需先调用`RecordApp.RequestPermission`

注:开始录音后如果底层支持实时返还数据,将会回调`RecordApp.OnProcess`事件方法,只需要给他赋一个值
注:开始录音后如果底层支持实时返回PCM数据,将会回调`set.onProcess`事件方法,并非所有平台都支持实时回调,可以通过`RecordApp.Current.CanProcess()`方法来检测

``` javascript
set配置默认值:
{
type:"mp3"//最佳输出格式,如果底层实现能够支持就应当优先返回此格式
sampleRate:16000//最佳采样率hz
bitRate:16//最佳比特率kbps

onProcess:NOOP//如果当前环境支持实时回调(RecordApp.Current.CanProcess()),接收到录音数据时的回调函数:fn(buffers,powerLevel,bufferDuration,bufferSampleRate),此回调和Recorder的回调行为完全一致
}
注意:此对象会被修改,因为平台实现时需要把实际使用的值存入此对象

Expand Down Expand Up @@ -205,38 +204,25 @@ IOS-Weixin底层会把从微信素材下载过来的原始音频信息存储在s
`fail`: `fn(errMsg)` 初始化失败回调


## 【Event】RecordApp.OnProcess(pcmDatas,powerLevel,duration,sampleRate)
录音实时数据回调,如果底层会实时调用`RecordApp.ReceivePCM`返回数据,就一定会触发执行此方法,否则一定不会回调;在需回调的地方绑定一个函数即可,注意:新函数会覆盖旧的函数。这个方法和`Recorder.set.onProcess`基本完全相同。

`pcmDatas`: [[Int16,...]] 当前单声道录音缓冲PCM片段(数组的第一维长度始终为1,是为了和`Recorder`兼容)

`powerLevel`:当前缓冲的音量级别0-100

`bufferDuration`:录音持续总时长

`sampleRate`:缓冲使用的采样率

如果需要绘制波形之类功能,需要实现此方法即可,使用以计算好的`powerLevel`可以实现音量大小的直观展示,使用`pcmDatas`可以达到更高级效果。

注意:pcmDatas数据的采样率`sampleRate`和设置的`set.sampleRate`不一定相同,如需强一致,请在OnProcess中自行连续调用采样率转换函数`Recorder.SampleData()`



## 【静态方法】RecordApp.ReceivePCM(pcmData,powerLevel,duration,sampleRate)
此方法由底层实现来调用,在开始录音后,底层如果能实时返还pcm数据,则需要调用此方法传递数据给js。
## 【全局方法】window.top.NativeRecordReceivePCM(pcmDataBase64,sampleRate)
开启了`Native`支持时,会有这个方法,用于原生App实时返回pcm数据。

`pcmData`: `Int16[]` 当前单声道录音缓冲PCM片段,正常情况下为上次回调本接口开始到现在的录音数据
此方法由Native Platform底层实现来调用,在开始录音后,需调用此方法传递数据给js。

`powerLevel,duration,sampleRate``RecordApp.OnProcess`参数意义相同
`pcmDataBase64`: `Int16[] Base64` 当前单声道录音缓冲PCM片段Base64编码,正常情况下为上次回调本接口开始到现在的录音数据

`sampleRate` 缓冲PCM的采样率

## 【全局方法】window.top.NativeRecordReceivePCM(pcmDataBase64,sampleRate)
开启了`Native`支持时,会有这个方法,用于原生App实时返还pcm数据。里面其实是封装了对`RecordApp.ReceivePCM`的调用。


## 【静态属性】RecordApp.Current
`RecordApp.Install`初始化后识别到的底层平台,取值为`RecordApp.Platforms`之一。

## 【静态方法】RecordApp.Current.CanProcess()
识别的底层平台是否支持实时返回PCM数据,如果返回值为true,`set.onProcess`将可以被实时回调。

## 【静态属性】RecordApp.Platforms
支持的平台列表,目前有三个:
1. `Native`: 原生App平台支持,底层由实际的`JsBridge`提供,此平台默认未开启
Expand Down
101 changes: 80 additions & 21 deletions app-support-sample/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@

<script>
//兼容环境
var PageLM="2019-8-13 13:45:36";
var PageLM="2019-8-29 11:20:24";

function BuildHtml(html,o,notEncode,loop){return o||(o={}),html=(null==html?"":html+"").replace(/\{(fn:)?(:)?(.+?)\}/gi,function(a,b,c,code){try{var v=eval(b?code:"o."+code);return v=void 0===v?"":v+"",c||notEncode?v:v}catch(e){return console.log("BuildHtml Fail",a+"\n"+e.stack),""}}),loop&&/\{(fn:)?(.+?)\}/.test(html)&&(html=BuildHtml(html,o,notEncode,loop)),html};
function RandomKey(){
Expand Down Expand Up @@ -194,6 +194,11 @@
<div style="height:100px;width:300px;border:1px solid #ccc;box-sizing: border-box;display:inline-block" class="recwave"></div>
<input type="checkbox" class="recwaveSet" checked>
</div>
<div class="pd webrtcBox">
<label><input type="checkbox" class="realTimeSendSet">模拟准实时编码传输(H5版语音通话聊天)</label>
,发送间隔<input type="text" class="realTimeSend" value="996" style="width:60px">ms
<div class="webrtcView" style="display:none;"></div>
</div>

<hr>
<audio class="recPlay" style="width:100%"></audio>
Expand Down Expand Up @@ -236,12 +241,12 @@


<script>
function reclog(s){
function reclog(s,e){
var now=new Date();
var t=("0"+now.getHours()).substr(-2)
+":"+("0"+now.getMinutes()).substr(-2)
+":"+("0"+now.getSeconds()).substr(-2);
$(".reclog").prepend('<div>['+t+']'+s+'</div>');
$(".reclog").prepend('<div style="color:'+(e?"red":"")+'">['+t+']'+s+'</div>');
};
window.onerror=function(message, url, lineNo, columnNo, error){
//https://www.cnblogs.com/xianyulaodi/p/6201829.html
Expand Down Expand Up @@ -340,10 +345,29 @@
var bit=+$(".bit").val();
var sample=+$(".sample").val();
var wave,waveSet=$(".recwaveSet")[0].checked;

var realTimeSendSet=$(".realTimeSendSet")[0].checked;
var realTimeSendTime=+$(".realTimeSend").val();
window.realTimeSendTryReset&&realTimeSendTryReset();
if(realTimeSendSet&&!RecordApp.Current.CanProcess()){
reclog("当前环境"+RecordApp.Current.Key+"不支持实时回调,不能模拟实时编码传输",1);
};


var set={
type:type
,bitRate:bit
,sampleRate:sample
,onProcess:function(buffers,level,time,sampleRate){
$(".recpowerx").css("width",level+"%");
$(".recpowert").html(time+"/"+level);

waveSet && wave.input(buffers[buffers.length-1],level,sampleRate);

if(realTimeSendSet&&window.realTimeSendTry){
realTimeSendTry(set,realTimeSendTime,buffers,sampleRate);
};
}
};
curSet=null;
RecordApp.Start(set,function(){
Expand All @@ -354,15 +378,39 @@
},function(err){
reclog(RecordApp.Current.Key+"打开失败:"+err);
});
RecordApp.OnProcess=function(buffers,level,time,sampleRate){
$(".recpowerx").css("width",level+"%");
$(".recpowert").html(time+"/"+level);

waveSet && wave.input(buffers[buffers.length-1],level,sampleRate);
};
};
var recblob={};
function recstop(batCall){
function recstop(){
recstopFn(true,function(){
setTimeout(function(){
window.realTimeSendTryStop&&realTimeSendTryStop(curSet);
});
});
};
function recstopFn(isClick,endCall,rec){
var t1=Date.now();

var add=function(time,tag,blob,set){
var id=RandomKey(16);
recblob[id]={blob:blob,set:$.extend({},set),time:time};
reclog(tag+":"+intp(set.bitRate,3)+"kbps "+intp(set.sampleRate,5)+"hz 花"+intp(Date.now()-t1,4)+"ms编码"+intp(blob.size,6)+"b ["+set.type+"]"+intp(time,6)+'ms <button onclick="recdown(\''+id+'\')">下载</button> <button onclick="recplay(\''+id+'\')">播放</button> <span class="p'+id+'"></span> <span class="d'+id+'"></span>');
};

if(!isClick){
rec.stop(function(blob,time){
var tag=endCall("",blob,time);
if(tag==-1){
return;
};

add(time,tag||"已录制",blob,rec.set);
},function(s){
reclog("失败:"+s);
endCall(s);
});
return;
};

if(!curSet){
reclog("请先开始录音");
return;
Expand All @@ -371,13 +419,8 @@
curSet=null;

reclog(RecordApp.Current.Key+"正在结束"+setData.type+"...");
var t1=Date.now();
RecordApp.Stop(function(blob,time){
var add=function(tag,blob,set){
var id=RandomKey(16);
recblob[id]={blob:blob,set:$.extend({},set),time:time};
reclog(tag+":"+intp(set.bitRate,3)+"kbps "+intp(set.sampleRate,5)+"hz 花"+intp(Date.now()-t1,4)+"ms编码"+intp(blob.size,6)+"b ["+set.type+"]"+intp(time,6)+'ms <button onclick="recdown(\''+id+'\')">下载</button> <button onclick="recplay(\''+id+'\')">播放</button> <span class="p'+id+'"></span> <span class="d'+id+'"></span>');
};
endCall("",blob,time);

var wxData=setData.DownWxMediaData;//微信JsSDK 下载到的音频源文件
if(wxData){
Expand All @@ -389,7 +432,6 @@
reclog("<span style='color:#fb0'>播放需要上面点击换到amr类型,加载amr解码器,目前未加载<span>");
};

var timex=time;
var t1x=t1;
t1=Date.now();
for(var i=0;i<list.length;i++){
Expand All @@ -399,15 +441,14 @@
u8arr[n]=bstr.charCodeAt(n);
};
var blob2=new Blob([u8arr.buffer], {type:obj.mime});
time=obj.duration;
add("<span style='color:#0b1'>微信源片段"+(i+1)+"</span>",blob2,{type:/\/(\w+)/.exec(obj.mime)[1]});
add(obj.duration,"<span style='color:#0b1'>微信源片段"+(i+1)+"</span>",blob2,{type:/\/(\w+)/.exec(obj.mime)[1]});
};
t1=t1x;
time=timex;
};
add("已录制",blob,setData);
add(time,"已录制",blob,setData);
},function(s){
reclog("失败:"+s);
endCall(s);
});
};
var intp=function(s,len){
Expand Down Expand Up @@ -488,6 +529,24 @@

reclog("<span style='color:#f60'>当前页面处在在iframe中,但故意未进行任何处理,"+(isSelf?"当前是同域":"并且已发生跨域,未设置相应策略H5录音权限永远是拒绝的")+"</span>");
};




//实时传输数据模拟开关
$(".realTimeSendSet").bind("change",function(e){
var open=e.target.checked;
$(".webrtcView")[open?"show":"hide"]();
if(open && !window.webrtcCreate){
var file="zdemo.index.webrtc.js";
reclog("正在加载"+file+" ...");

var elem=document.createElement("script");
elem.setAttribute("type","text/javascript");
elem.setAttribute("src","../assets/"+file);
$("head")[0].appendChild(elem);
};
});
</script>


Expand Down
6 changes: 5 additions & 1 deletion app-support-sample/ios-weixin-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,12 @@ var InitJsSDK=function(App,MyWxApi,ajax){
};


if(!/^file:|:\/\/[^\/]*(jiebian.life|github.io)(\/|$)/.test(location.href))
if(!/^file:|:\/\/[^\/]*(jiebian.life|github.io)(\/|$)/.test(location.href)
&& !localStorage["DisableAppSampleAlert"]
&& !window.AppSampleAlert){
window.AppSampleAlert=1;
alert("本网站正在使用RecordApp测试配置例子,正式使用时需要改动哦");
}


if(window.RecordApp){
Expand Down
6 changes: 5 additions & 1 deletion app-support-sample/native-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,12 @@ config.JsBridgeStop=function(success,fail){
};


if(!/^file:|:\/\/[^\/]*(jiebian.life|github.io)(\/|$)/.test(location.href))
if(!/^file:|:\/\/[^\/]*(jiebian.life|github.io)(\/|$)/.test(location.href)
&& !localStorage["DisableAppSampleAlert"])
&& !window.AppSampleAlert){
window.AppSampleAlert=1;
alert("本网站正在使用RecordApp测试配置例子,正式使用时需要改动哦");
}


if(window.RecordApp){
Expand Down
Loading

0 comments on commit 646df8a

Please sign in to comment.