终于折腾好了(使用FlashWavRecorder实现IE11浏览器录音后用科大讯飞转文字,我的是Vue.js项目),对一个前端仔来说,听到要兼容IE,都是不情愿的,但是需求来了,那就折腾起来吧。(主要这需求砍不了,菜刀没带)。但是经过这几天折腾,记录一下,不足之处,还望指教~
首先呢,明确下我们的目标,支持IE11浏览器录音后转文字。我们用逆向思维思考下,音频转文字。我们目前用的是科大讯飞的SDK。前端的话,把我们拿到的音频数据,包装好,调用一下后端给的接口,然后后端调讯飞SDK,转化为文字,发回来给我们。所以我们最主要是拿到音频数据,即录音。那我们难点是IE11上录音,因为IE 不支持 navigator.mediaDevices.getUserMedia这样的API。这里用JS调用flash去实现IE上录音。(应该还可以调用ActiveX,没尝试)。
这里在github找到两个可以运行起来的demo。传送门:recorder.js,FlashWavRecorder
这两个提供的example 都可以正常跑起来。recorder.js音质比较好,第二个杂音大点。但recorder.js不知道怎么取到录音后的音频数据,照理说,上传都可以实现了,拿到音频数据,理应可以。或许是我的能力问题,还请指点。FlashWavRecorder 有getBase64的方法,所以就选用FlashWavRecorder 了。
调用别人的SDK,就要符合别人的游戏规则。如下摘自科大讯飞接入规则
- 接口说明
授权认证,调用接口需要将 Appid,CurTime, Param 和 CheckSum 信息放在 HTTP 请求头中;
接口统一为 UTF-8 编码;
接口支持 http 和 https;
请求方式为POST。 - 授权认证
在调用所有业务接口时,都需要在 Http Request Header 中配置以下参数用于授权认证:
参数 格式 说明 必须
X-Appid string 讯飞开放平台注册申请应用的应用ID(appid) 是
X-CurTime string 当前UTC时间戳,从1970年1月1日0点0 分0 秒开始到现在的秒数 是
X-Param string 相关参数JSON串经Base64编码后的字符串,见各接口详细说明 是
X-CheckSum string 令牌,计算方法:MD5(apiKey + curTime + param),三个值拼接的字符串,进行MD5哈希计算(32位小写),其中apiKey由讯飞提供,调用方管理。 是
讯飞demo中有这个字段 文档没写
body string dataUrl
注:
apiKey:接口密钥,由讯飞开放平台提供,调用方注意保管,如泄露,可联系讯飞技术人员重置;
checkSum 有效期:出于安全性考虑,每个 checkSum 的有效期为 5 分钟(用 curTime 计算),同时 curTime 要与标准时间同步,否则,时间相差太大,服务端会直接认为 curTime 无效;
BASE64 编码采用 MIME 格式,字符包括大小写字母各26个,加上10个数字,和加号 + ,斜杠 / ,一共64个字符。
checkSum 生成示例:
String apiKey="abcd1234";
String curTime="1502607694";
String param="eyAiYXVmIjogImF1ZGlvL0wxNjtyYXR...";
String checkSum=MD5(apiKey+curTime+param);
Copy
- 白名单
在调用所有业务接口时,授权认证通过后,服务端会检查调用方ip是否在讯飞开放平台配置的ip白名单中,对于没有配置到白名单中的IP发来的请求,服务端会拒绝服务。 注:
IP白名单可在控制台应用管理卡片上编辑,五分钟左右生效;
IP白名单最多可设置5个,更多的需求可通过工单联系技术人员;
如果服务器返回结果如下所示,则表示由于未配置IP白名单,服务端拒绝服务:
{
"code":"10105",
"desc":"illegal access|illegal client_ip",
"data":"",
"sid":"xxxxxx"
}
我们请求后端接口类似这样:
that.$http.postObj('restful/voice/recognition', {
'body': sendurl,
'X-Appid': that.appid,
'X-CurTime': time,
'X-Param': xParam,
'X-CheckSum': xChecksum
}).then((res) => {
//成功识别语音后,在这里拿到文字 eg:res.data.data
console.log(res)
}
}).catch((err) => {
console.log(err)
});
//basic.js
$(function () {
//这里的$(function () {}) 原生写法是function DOMContentLoaded(){};
//document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);
var RECORDER_APP_ID = "recorderApp";
var $level = $('.level .progress');
var appWidth = 24;
var appHeight = 24;
var flashvars = {'upload_image': '../images/upload.png'};
var params = {};
var attributes = {'id': RECORDER_APP_ID, 'name': RECORDER_APP_ID};
swfobject.embedSWF("../recorder.swf", "flashcontent", appWidth, appHeight, "11.0.0", "", flashvars, params, attributes);
//注意这句,要确保swfobject.js 已经加载好了 要准备个容器不然报错
//这里使用 document.getElementById(RECORDER_APP_ID).style.top='-3333px'
// 因为把swf对象设置为display:none 或者visibility:hidden 都是使用不了
window.fwr_event_handler = function fwr_event_handler() {//flash插件相关状态反馈
var name, $controls;
switch (arguments[0]) {
case "ready":
FWRecorder.uploadFormId = "#uploadForm";
FWRecorder.uploadFieldName = "upload_file[filename]";
FWRecorder.connect(RECORDER_APP_ID, 0);//如果老是报 recoder is null 这里没有调进来
FWRecorder.recorderOriginalWidth = appWidth;
FWRecorder.recorderOriginalHeight = appHeight;
break;
case "permission_panel_closed":
FWRecorder.recorderOriginalWidth = 0;
FWRecorder.recorderOriginalHeight = 0;
document.getElementById(RECORDER_APP_ID).style.top='-3333px'//隐藏白白一块
FWRecorder.defaultSize();
break;
case "microphone_user_request":
FWRecorder.showPermissionWindow();//这里请求后 会弹出 是否 允许 使用麦克风的请求
break;
....这里省略局部代码
case "microphone_level"://这里用于显示声波。如果成功接入麦克风,录音的时候会一直调用
$level.css({height: arguments[1] * 100 + '%'});
break;
....这里省略局部代码
};
.......这里省略局部代码
});
以上主要是理解swfobject.embedSWF的用法。这里有详细的传送门: swfobject.js使用
//recoder.js
(function(global) {
var Recorder;
var RECORDED_AUDIO_TYPE = "audio/wav";
Recorder = {
recorder: null,
recorderOriginalWidth: 0,
recorderOriginalHeight: 0,
uploadFormId: null,
uploadFieldName: null,
isReady: false,
connect: function(name, attempts) {//连接麦克风 主要是init recorder
if(navigator.appName.indexOf("Microsoft") != -1) {
Recorder.recorder = window[name];
} else {
Recorder.recorder = document[name];//swfObject对象 ,这里的 name='recorderApp'
}
if(attempts >= 40) {
return;
}
// flash app needs time to load and initialize
if(Recorder.recorder && Recorder.recorder.init) {
Recorder.recorderOriginalWidth = Recorder.recorder.width;
Recorder.recorderOriginalHeight = Recorder.recorder.height;
if(Recorder.uploadFormId && $) {
var frm = $(Recorder.uploadFormId);
Recorder.recorder.init(frm.attr('action').toString(), Recorder.uploadFieldName, frm.serializeArray());
}
return;
}
setTimeout(function() {Recorder.connect(name, attempts+1);}, 100);
},
playBack: function(name) {//播放
// TODO: Rename to `playback`
Recorder.recorder.playBack(name);
},
stopRecording: function() {//停止录音
Recorder.recorder.stopRecording();
},
resize: function(width, height) {//设置 麦克风权限请求框大小
Recorder.recorder.width = width + "px";
Recorder.recorder.height = height + "px";
},
defaultSize: function() {
Recorder.resize(Recorder.recorderOriginalWidth, Recorder.recorderOriginalHeight);
},
show: function() {
Recorder.recorder.show();
},
hide: function() {
Recorder.recorder.hide();
},
getBase64: function(name) {//来获取base64数据 如果需要转回来 window.atob("base64")
var data = Recorder.recorder.getBase64(name);
return 'data:' + RECORDED_AUDIO_TYPE + ';base64,' + data;
},
getBlob: function(name) {//这里转化为blod 后面讯飞转文字的时候要用到。当然可以自己转
var base64Data = Recorder.getBase64(name).split(',')[1];
return base64toBlob(base64Data, RECORDED_AUDIO_TYPE);
},
showPermissionWindow: function(options) {
Recorder.resize(240, 160);
// need to wait until app is resized before displaying permissions screen
var permissionCommand = function() {
if (options && options.permanent) {
Recorder.recorder.permitPermanently();
} else {
Recorder.recorder.permit();
}
};
setTimeout(permissionCommand, 1);
},
};
function base64toBlob(b64Data, contentType, sliceSize) {
contentType = contentType || '';
sliceSize = sliceSize || 512;
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, {type: contentType});
}
global.FWRecorder = Recorder;
})(this);
然后写几个button 接入就好了。 大家可以动手试一试。
注意:如果是在vue.js中当成模块的时候,ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
不能使用arguments.callee;不能使用arguments.caller;禁止this指向全局对象;不能使用fn.caller和fn.arguments获取函数调用的堆栈 等等 所以引入swfobject.js 的时候 要把 arguments.callee 替换掉。 传送门:严格模式下arguments.callee代替思路
例如有用arguments.callee的函数里,
function (){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);
把匿名函数 改为具名函数 ? swfobject.js 里面好像有7处需要改
function fn(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(fn,0);