在html5網頁中錄音解決方案

緣起

因公司業務須要在網頁錄音功能,由於h5的api兼容限制不得想出一些解決方案,如下是總結。

首發

https://shudong.wang/10585.htmljavascript

demo測試地址

https://wsdo.github.io/recording/html

項目地址

https://github.com/wsdo/recordingjava

Q&A

後續繼續完善,有這個需求的朋友能夠繼續討論

調查

  • 百度語音識別ios

    感受百度夠強大,確定有完美的解決方案,最終發如今移動端網頁打開百度語音直接跳轉讓下載app(說明不支持)
  • 谷歌語音識別git

    在不支持的機型上面不顯示語音識別按鈕

兼容性

h5錄音主要使用AudioContext 和 getUserMedia 兼容性仍是不好的,尤爲是在移動端。

AudioContextgithub

getUserMedia

真機測試結果: (測試部分,旁邊同事的手機和公司的測試機,和本身的手機)

可使用的:(最新版本,經常使用)web

pc safari 支持
ios Safari  支持
安卓
  小米5 微信裏面可使用
  小米5 Chrome 最新版可使用
  小米5 uc可使用

不可使用的
ios Chrome 到這出了問題 webkitAudioContext.createScriptProcessor
ios 微信裏面不支持
小米5 自帶瀏覽器不能夠
華爲自帶瀏覽器不能夠api

解決方案

考慮到手機端微信裏面使用場景比較多
  • 針對微信專門寫一套解決方案,借用微信的jssdk 的api,這樣能夠解決大部分在微信裏面使用的場景
  • 兼容的手機:正常使用
  • 不兼容的手機:提示 推薦瀏覽器

操做過程

先了解一下簡介和歷史吧

簡介

長久以來,音頻/視頻捕獲都是網絡開發中的「聖盃」。
多年來,咱們老是依賴於瀏覽器插件(Flash 或 Silverlight)實現這一點。快來看看吧!promise

如今輪到 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 工做組負責監管。Google、Opera、Mozilla 和其餘一些公司目前正致力於在本身的瀏覽器中實施該 API。

getUserMedia() 與 WebRTC 相關,由於它是通向這組 API 的門戶。它提供了訪問用戶本地相機/麥克風媒體流的手段。

支持:

在 Chrome 瀏覽器 18.0.1008 和更高版本中,可在 about:flags 下啓用 WebRTC。

實戰 (儘可能實現多瀏覽器兼容)

思路

主要使用 AudioContext 和 getUserMedia 這個api 來操做

AudioContext

AudioContext接口表示由音頻模塊鏈接而成的音頻處理圖,每一個模塊對應一個AudioNode。AudioContext能夠控制它所包含的節點的建立,以及音頻處理、解碼操做的執行。作任何事情以前都要先建立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;
}
相關文章
相關標籤/搜索