向使用者錄製音訊

許多瀏覽器現在都能存取使用者的影片與音訊輸入。不過,視瀏覽器而定,這可能會是完整的動態和內嵌體驗,也可能委派給使用者裝置上的其他應用程式。

循序漸進地開始

最簡單的做法是要求使用者提供預錄檔案。方法是建立簡單的檔案輸入元素,並新增 accept 篩選器 (表示我們只接受音訊檔案) 和 capture 屬性表示要從麥克風直接取得。

<input type="file" accept="audio/*" capture />

這個方法適用於所有平台。在電腦上,系統會提示使用者從檔案系統上傳檔案 (忽略 capture 屬性)。在 iOS 版 Safari 中,系統會開啟麥克風應用程式,讓您錄製音訊並傳回網頁;在 Android 上,則在將音訊傳回網頁之前,使用者可選擇要使用哪個應用程式錄製音訊。

使用者完成錄製並回到網站後,您必須透過某種方式保留檔案資料。如要快速存取,您可以將 onchange 事件附加至輸入元素,然後讀取事件物件的 files 屬性。

<input type="file" accept="audio/*" capture id="recorder" />
<audio id="player" controls></audio>
  <script>
    const recorder = document.getElementById('recorder');
    const player = document.getElementById('player');

    recorder.addEventListener('change', function (e) {
      const file = e.target.files[0];
      const url = URL.createObjectURL(file);
      // Do something with the audio file.
      player.src = url;
    });
  </script>
</audio>

取得檔案存取權後,就能對檔案執行任何操作。舉例來說,您可以執行下列操作:

  • 將其直接附加至 <audio> 元素,即可播放
  • 下載到使用者的裝置
  • 將檔案連接至 XMLHttpRequest 即可上傳至伺服器
  • 透過 Web Audio API 傳遞圖像,並對其套用篩選器

雖然使用輸入元素方法取得音訊資料存取權,但這是最具吸引力的選項。我們確實希望可以使用麥克風,並在網頁上直接提供良好的體驗。

透過互動方式存取麥克風

新式瀏覽器可直接連接麥克風,讓我們可以打造與網頁完全整合的使用體驗,使用者也絕對不會離開瀏覽器。

取得麥克風存取權

我們可以藉由在 WebRTC 規格 getUserMedia() 中使用 API 直接存取麥克風。getUserMedia() 會提示使用者授予已連線麥克風和攝影機的存取權。

如果 API 成功,則會傳回 Stream,其中包含相機或麥克風的資料,我們就可以將其附加至 <audio> 元素、附加至 WebRTC 串流、附加至 Web Audio AudioContext,或使用 MediaRecorder API 儲存。

如要從麥克風取得資料,請直接在傳遞至 getUserMedia() API 的限制物件中設定 audio: true

<audio id="player" controls></audio>
<script>
  const player = document.getElementById('player');

  const handleSuccess = function (stream) {
    if (window.URL) {
      player.srcObject = stream;
    } else {
      player.src = stream;
    }
  };

  navigator.mediaDevices
    .getUserMedia({audio: true, video: false})
    .then(handleSuccess);
</script>

如要選擇特定麥克風,可以先列舉可用的麥克風。

navigator.mediaDevices.enumerateDevices().then((devices) => {
  devices = devices.filter((d) => d.kind === 'audioinput');
});

然後,您可以在呼叫 getUserMedia 時傳遞要使用的 deviceId

navigator.mediaDevices.getUserMedia({
  audio: {
    deviceId: devices[0].deviceId,
  },
});

而這本身並不實用。只要取得音訊資料並播放即可。

從麥克風存取原始資料

如要存取麥克風的原始資料,我們必須擷取 getUserMedia() 建立的串流,然後使用 Web Audio API 處理資料。Web Audio API 是一個簡單的 API,能擷取輸入來源並將來源連接至節點,好讓這些節點能夠處理音訊資料 (調整增益等),並最終透過喇叭播放。

您可連接的其中一個節點為 AudioWorkletNode。這個節點可提供低階自訂音訊處理功能。實際音訊處理作業會在 AudioWorkletProcessor 中的 process() 回呼方法進行。呼叫此函式來動態饋給輸入和參數,並擷取輸出內容。

如要瞭解詳情,請參閱輸入音訊小程式

<script>
  const handleSuccess = async function(stream) {
    const context = new AudioContext();
    const source = context.createMediaStreamSource(stream);

    await context.audioWorklet.addModule("processor.js");
    const worklet = new AudioWorkletNode(context, "worklet-processor");

    source.connect(worklet);
    worklet.connect(context.destination);
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess);
</script>
// processor.js
class WorkletProcessor extends AudioWorkletProcessor {
  process(inputs, outputs, parameters) {
    // Do something with the data, e.g. convert it to WAV
    console.log(inputs);
    return true;
  }
}

registerProcessor("worklet-processor", WorkletProcessor);

緩衝區中保存的資料是麥克風的原始資料,您可以採取多種做法來處理這些資料:

  • 直接將檔案上傳到伺服器
  • 儲存在本機
  • 將其轉換為專屬檔案格式 (例如 WAV),然後儲存至伺服器或本機

儲存麥克風的資料

如要儲存麥克風中的資料,最簡單的方法就是使用 MediaRecorder API。

MediaRecorder API 會擷取 getUserMedia 建立的串流,然後逐步將串流中的資料儲存至偏好的目的地。

<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
  const downloadLink = document.getElementById('download');
  const stopButton = document.getElementById('stop');


  const handleSuccess = function(stream) {
    const options = {mimeType: 'audio/webm'};
    const recordedChunks = [];
    const mediaRecorder = new MediaRecorder(stream, options);

    mediaRecorder.addEventListener('dataavailable', function(e) {
      if (e.data.size > 0) recordedChunks.push(e.data);
    });

    mediaRecorder.addEventListener('stop', function() {
      downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
      downloadLink.download = 'acetest.wav';
    });

    stopButton.addEventListener('click', function() {
      mediaRecorder.stop();
    });

    mediaRecorder.start();
  };

  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(handleSuccess);
</script>

在本範例中,我們會將資料直接儲存為陣列,並在稍後轉換為 Blob 來將資料儲存至網路伺服器,或直接將資料儲存到使用者裝置的儲存空間。

以負責任的方式要求使用麥克風

如果使用者先前尚未授予網站麥克風存取權,當您呼叫 getUserMedia 時,瀏覽器會提示使用者授予網站麥克風權限。

使用者因不希望收到提示,要求存取電腦上的強大裝置,而經常封鎖要求;或者,如果使用者不瞭解建立提示的情境,就會忽略要求。最佳做法是只在首次需要時才要求存取麥克風。使用者授予存取權後,系統就不會再詢問他們,不過如果他們拒絕存取權,您就無法再次要求使用者授予權限。

使用權限 API 檢查您是否具備存取權

如果已經擁有麥克風存取權,getUserMedia API 就不會知道。這表示您遇到了問題,為了提供良好的 UI 來讓使用者授予您麥克風存取權,您必須要求麥克風存取權。

在某些瀏覽器中使用 Permission API 即可解決這個問題。您可以利用 navigator.permission API 查詢特定 API 的存取權狀態,而不必再次提示。

如要查詢您是否有使用者的麥克風存取權,可以將 {name: 'microphone'} 傳入查詢方法,然後它會傳回以下其中一項:

  • granted:使用者先前已授予您麥克風存取權;
  • prompt:使用者尚未授予您存取權,且會在您呼叫 getUserMedia 時收到提示;
  • denied:系統或使用者已明確封鎖對麥克風的存取權,而您將無法存取該麥克風。

您現在可以快速查看是否需要修改使用者介面,以符合使用者需要的動作。

navigator.permissions.query({name: 'microphone'}).then(function (result) {
  if (result.state == 'granted') {
  } else if (result.state == 'prompt') {
  } else if (result.state == 'denied') {
  }
  result.onchange = function () {};
});

意見回饋: