首发 http://shudong.wang/article/102
缘起
因公司业务需要在网页录音功能, 因为 h5 的 api 兼容限制不得想出一些解决方案, 以下是总结
调查
百度语音识别
感觉百度够强大, 肯定有完美的解决方案, 最终发现在移动端网页打开百度语音直接跳转让下载 app(说明不支持)
谷歌语音识别
在不支持的机型上面不显示语音识别按钮
兼容性
h5 录音主要使用 AudioContext 和 getUserMedia 兼容性还是很差的, 尤其是在移动端
AudioContext
getUserMedia
真机测试结果: (测试部分, 旁边同事的手机和公司的测试机, 和自己的手机)
可以使用的:(最新版本, 常用)
pc safari 支持
ios Safari 支持
安卓
小米 5 微信里面可以使用
小米 5 Chrome 最新版可以使用
小米 5 uc 可以使用
不可以使用的
ios Chrome 到这出了问题 webkitAudioContext.createScriptProcessor
ios 微信里面不支持
小米 5 自带浏览器不可以
华为自带浏览器不可以
解决方案
考虑到手机端微信里面使用场景比较多
针对微信专门写一套解决方案, 借用微信的 jssdk 的 api, 这样可以解决大部分在微信里面使用的场景
兼容的手机: 正常使用
不兼容的手机: 提示 推荐浏览器
操作过程
先了解一下简介和历史吧
简介
长久以来, 音频 / 视频捕获都是网络开发中的圣杯
多年来, 我们总是依赖于浏览器插件 (Flash 或 Silverlight) 实现这一点快来看看吧!
现在轮到 html5 大显身手了也许看起来不是很显眼, 但是 HTML5 的崛起引发了对设备硬件访问的激增
地理位置 (GPS)Orientation API(加速计)WebGL (GPU) 和 Web Audio API(视频硬件) 都是很好的例子
这些功能非常强大, 展示了基于系统底层硬件功能之上的高级 JavaScript API
本教程介绍了一种新 API:navigator.getUserMedia(), 可让网络应用访问用户的相机和麦克风
了解 getUserMedia() 的历史 (节选 Capturing Audio & Video in HTML5)
如果您还不知道, getUserMedia() 的历史可谓一段有趣的故事
过去几年中出现过好几种 Media Capture API 的变体很多人意识到, 需要能够在网络上访问本地设备, 但这要所有人合力开发出一种新的规范局面一片混乱, 以至于 W3C 最终决定成立一个工作组他们只有一个目的: 理清混乱的局面! 设备 API 政策 (DAP) 工作组负责对过剩的提议进行统一和标准化
我会试着总结一下 2011 所发生的事情...
第 1 轮: HTML 媒体捕获
HTML 媒体捕获是 DAP 在网络媒体捕获标准化上迈出的第一步具体方法是超载 <input type="file"> 并为 accept 参数添加新值
如果您要让用户通过网络摄像头拍摄自己的快照, 就可以使用 capture=camera:
<input type="file" accept="image/*;capture=camera">
录制视频或音频也是类似的:
- <input type="file" accept="video/*;capture=camcorder">
- <input type="file" accept="audio/*;capture=microphone">
第 2 轮: 设备元素
很多人认为 HTML 媒体捕获的局限性太大, 因此一种新的规范应运而生, 可以支持任何类型的 (未来) 设备不出意料地, 该设计需要新的 <device> 元素, 也就是 getUserMedia() 的前身
Opera 是第一批根据 <device> 元素创建视频捕获的初始实施的浏览器之一不久之后 (准确地说是同一天),WhatWG 决定废止 <device> 标记, 以支持称为 navigator.getUserMedia() 的新兴 JavaScript API 一周后, Opera 推出的新版本中加入了对更新的 getUserMedia() 规范的支持当年年底, Microsoft 也加入这一行列, 发布了 IE9 实验室以支持新规范
- <device type="media" onchange="update(this.data)"></device>
- <video autoplay></video>
- <script>
- function update(stream) {
- document.querySelector('video').src = stream.url;
- }
- </script>
很遗憾, 已发布的浏览器中没有任何一款曾经包含 <device> 我猜这是一个不太需要担心的 API 但是 <device> 确实有两大优点: 一是语义方面, 二是可以轻松进行扩展, 而不仅仅是支持音频 / 视频设备
现在深吸一口气这玩意儿速度飞快!
第 3 轮: WebRTC 这就是今天讨论的重点了
<device> 元素最终还是像渡渡鸟一样销声匿迹了
依靠 WebRTC(网络即时通信) 的大力协助, 最近几个月寻找合适捕获 API 的步伐加快了很多该规范由 W3C WebRTC 工作组负责监管 GoogleOperaMozilla 和其他一些公司目前正致力于在自己的浏览器中实施该 API
getUserMedia() 与 WebRTC 相关, 因为它是通向这组 API 的门户它提供了访问用户本地相机 / 麦克风媒体流的手段
支持:
在 Chrome 浏览器 18.0.1008 和更高版本中, 可在 about:flags 下启用 WebRTC
实战 (尽量实现多浏览器兼容)
思路
主要使用 AudioContext 和 getUserMedia 这个 api 来操作
AudioContext
AudioContext 接口表示由音频模块连接而成的音频处理图, 每个模块对应一个 AudioNodeAudioContext 可以控制它所包含的节点的创建, 以及音频处理解码操作的执行做任何事情之前都要先创建 AudioContext 对象, 因为一切都发生在这个环境之中
可以来这里了解
https://developer.mozilla.org...
getUserMedia
MediaDevices.getUserMedia() 会提示用户给予使用媒体输入的许可, 媒体输入会产生一个 MediaStream, 里面包含了请求的媒体类型的轨道此流可以包含一个视频轨道 (来自硬件或者虚拟视频源, 比如相机视频采集设备和屏幕共享服务等等) 一个音频轨道 (同样来自硬件或虚拟音频源, 比如麦克风 A/D 转换器等等), 也可能是其它轨道类型
它返回一个 Promise 对象, 成功后会 resolve 回调一个 MediaStream 对象若用户拒绝了使用权限, 或者需要的媒体源不可用, promise 会 reject 回调一个 PermissionDeniedError 或者 NotFoundError
可以来这里了解
https://developer.mozilla.org...
兼容浏览器
引入 adapter (在旧的浏览器中使用新的 API)
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
这是一个使用 navigator.mediaDevices.getUserMedia() 的例子, 带一个 polyfill 以适应旧的浏览器 要注意的是这个 polyfill 并不能修正一些约束语法上的遗留差异, 这表示约束在某些浏览器上可能不会很好地运行推荐使用处理了约束的 adapter.js polyfill 来替代
https://github.com/webrtc/ada...
/ 老的浏览器可能根本没有实现 mediaDevices, 所以我们可以先设置一个空的对象
- if (navigator.mediaDevices === undefined) {
- navigator.mediaDevices = {};
- }
$ 一些浏览器部分支持 mediaDevices 我们不能直接给对象设置 getUserMedia
- // 因为这样可能会覆盖已有的属性这里我们只会在没有 getUserMedia 属性的时候添加它
- if (navigator.mediaDevices.getUserMedia === undefined) {
- navigator.mediaDevices.getUserMedia = function(constraints) {
- // 首先, 如果有 getUserMedia 的话, 就获得它
- var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
- // 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口
- if (!getUserMedia) {
- return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
- }
- // 否则, 为老的 navigator.getUserMedia 方法包裹一个 Promise
- return new Promise(function(resolve, reject) {
- getUserMedia.call(navigator, constraints, resolve, reject);
- });
- }
- }
- navigator.mediaDevices.getUserMedia({
- audio: true,
- video: true
- }).then(function(stream) {
- var video = document.querySelector('video');
- // 旧的浏览器可能没有 srcObject
- if ("srcObject" in video) {
- video.srcObject = stream;
- } else {
- // 防止再新的浏览器里使用它, 应为它已经不再支持了
- video.src = window.URL.createObjectURL(stream);
- }
- video.onloadedmetadata = function(e) {
- video.play();
- };
- }).
- catch(function(err) {
- console.log(err.name + ":" + err.message);
- });
检测 AudioContext
- function audioContextCheck() {
- if (typeof window.AudioContext !== "undefined") {
- console.log('AudioContext');
- return new window.AudioContext();
- } else if (typeof webkitAudioContext !== "undefined") {
- console.log('webkitAudioContext');
- return new window.webkitAudioContext();
- } else if (typeof window.mozAudioContext !== "undefined") {
- console.log('mozAudioContext');
- return new window.mozAudioContext();
- } else {
- console.log('NONE OF THEM!');
- }
- }
- or
- var context;
- window.addEventListener('load', init, false);
- function init() {
- try {
- // Fix up for prefixing
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- context = new AudioContext();
- }
- catch (e) {
- alert('Web Audio API is not supported in this browser');
- }
- }
检测 getUserMedia
- function hasGetUserMedia() {
- // Note: Opera builds are unprefixed.
- return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia || navigator.msGetUserMedia);
- }
- if (hasGetUserMedia()) {
- // Good to go!
- } else {
- alert('getUserMedia() is not supported in your browser');
- }
兼容写法
var AudioContext = window.AudioContext || window.webkitAudioContext;
开始录音
- Storage.ctx = new AudioContext();
- if (Storage.ctx.createJavaScriptNode) {
- jsAudioNode = Storage.ctx.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels);
- } else if (Storage.ctx.createScriptProcessor) {
- jsAudioNode = Storage.ctx.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels);
- } else {
- alert('WebAudio API has no support on this browser.')
- throw 'WebAudio API has no support on this browser.';
- }
- jsAudioNode.connect(Storage.ctx.destination);
核心
- navigator.mediaDevices.getUserMedia({ audio: true }) // 只处理音频
- .then(onMicrophoneCaptured)
- .catch(onMicrophoneCaptureError);
处理 Safari 兼容
safari 不支持 Buffer 的方式 改为下面这种
- URL.createObjectURL(new Blob([_function.toString(),
- ';this.onmessage = function (e) {' + _function.name + '(e.data);}'
- ], {
- type: 'application/javascript'
- }));
利用 worker 处理异步
- function processInWebWorker(_function) {
- var workerURL = URL.createObjectURL(new Blob([_function.toString(),
- ';this.onmessage = function (e) {' + _function.name + '(e.data);}'
- ], {
- type: 'application/javascript'
- }));
- var worker = new Worker(workerURL);
- worker.workerURL = workerURL;
- return worker;
- }
demo 测试地址
https://wsdo.github.io/recording/
项目地址
https://github.com/wsdo/recording
后续继续完善, 有这个需求的朋友可以继续讨论
来源: https://segmentfault.com/a/1190000014048967