iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 10
0
Modern Web

Web x Sound - 用 Web 玩轉聲音系列 第 10

Day10 - MediaStream Recording API

昨天我們介紹了音訊串流 MediaStream 與 MediaStreamTrack,以及負責與裝置溝通的 MediaDevices。今天就來介紹與錄音有關的 MediaStream Recording API 吧!

MediaStream Recording API

這支 API 可以取出 MediaStream 的音訊資料,提供給開發者做進一步處理,像是分析波形頻率、調整聲音、儲存並上傳到遠端 server 等。其中最主要的 Interface 就只有一個:MediaRecorder。

MediaRecorder

它會將 MediaStream 內的音訊資料取出來,根據指定的每秒位元率、錄製好的檔案格式 MIME type 塞到內部的 Blobs 中。

const options = {
    mimeType: 'audio/mpeg',
    audioBitsPerSecond: 44100*16,
}
const recorder = new MediaRecorder(stream, options)

我們可以決定錄製的時候,是將整個 MediaStream 錄成一個 Blob,也可以分成每 N 秒一個 Blob。

const timeslice = 1000
recorder.start(timeslice)

有三個時機點可以取得錄製好的 Blob 資料:

  • N 秒一到時,觸發 dataavailable 事件,取得當前錄好的 Blob chunk
  • 錄到底時,先觸發 stop 事件,再觸發最後的 dataavailable 事件,取得當前錄好的 Blob chunk
  • 手動呼叫 requestData() 方法觸發 dataavailable 事件,取得當前錄好的 Blob chunk
let newTrack = [];

recorder.addEventListener('dataavailable', (e) => {
    if (e.data.size > 0) {
        newTrack.push(e.data);
    }
})

recorder.addEventListener('stop', () => console.log('Record Success!'));

Method

name description param return
MediaRecorder() constructor,帶入 MediaStream 並建立一個 MediaRecorder 物件,可以設定 options 決定 MediaRecorder 的 mimeType 與每秒位元率 stream (req.), options (opt.) MediaRecorder 物件
isTypeSupported() 測試是否支援錄製此 MIME type mimeType 字串 boolean
pause() 暫停錄製,將 recording 狀態轉為 pause,Blob 資料保留。若原本非 recording 狀態,會得到 InvalidState Error none undefined
resume() 繼續錄製,將 pause 狀態轉為 recording 並觸發 resume 事件。若原本非 pause 狀態,會得到 InvalidState Error none undefined
start() 開始錄製,將 inactive 狀態轉為 recording 並觸發 start 事件。可指定 timeslice (ms) 每 N 秒分割一個 Blob、或是從頭錄到尾都同一個 Blob。若原本非 inactive 狀態,會得到 InvalidState Error timeslice (opt.) undefined
stop() 停止錄製,將 recording 狀態轉為 inactive 並觸發 stop 與 dataavailable 事件。若原本非 recording 狀態,會得到 InvalidState Error none undefined
requestData() 手動觸發 dataavailable 事件以便取得資料 chunk

Properties

name description value
ignoreMutedMedia 決定當 MediaStreamTrack 是靜音 (muted) 時是否要錄下資料 boolean (default: false)
audioBitsPerSecond Read only. 取得每秒將多少位元的音訊資料存入 Blob 的數值 number
videoBitsPerSecond Read only. 取得每秒將多少位元的影片資料存入 Blob 的數值 number
mimeType Read only. 取得錄製完成後要儲存的 file format mimeType string
state Read only. 取得當前 MediaRecorder 的狀態 inactive (錄製停止), recording (錄製中), paused (錄製暫停)
stream Read only. 取得 MediaStream MediaStream

Events

trigger trigger events event handlers
pause() pause onpause
resume() resume onresume
start() start onstart
stop() stop -> dataavailable onstop, ondataavailable
requestData() dataavailable ondataavailable
exceptions error onerror

接下來介紹不同情境下,實際的錄製方法吧!

錄製本地檔案的聲音

最簡單的使用方式。

當使用者點擊 <input> 時,桌機會請使用者上傳音檔、行動裝置可能會有選項讓使用者用麥克風錄音,錄完後上傳音檔,再進行分析處理。這種情況下可以不用用到 <audio>,甚至也不需要用到 MediaRecorder,只需要從 <input> 取出檔案即可處理。

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

fileUploader.addEventListener('change', function(e) {
  const file = e.target.files[0]; 
  // 針對錄製檔案的處理 ...
});

錄製遠端檔案的聲音

有兩種方式可以取得遠端音檔:

  • 使用 HTMLMediaElement (也就是 <audio><video>)
  • 使用 Web Audio API

因為還沒介紹到 Web Audio API,先講第一種方式該怎麼錄製。步驟如下:

  1. 先為 <audio> 設定 src 屬性讀取遠端檔案
  2. <audio> 內的音源轉成 MediaStream
  3. 建立一個 MediaRecorder 開始錄製
  4. 自由發揮 XD
const remoteUrl = '.....';
const timeslice = 10000;
let chunks = [];
const myAudio = new Audio(remoteUrl);
const recorder = new MediaRecorder(myAudio.captureStream(), { mimeType: 'audio/mpeg' });

recorder.ondataavailable(e => {
    if (e.data.size > 0) chunks.push(e.data);
    // 針對每個 chunk 的處理 ...
});

recorder.onstop(() => {
    // 針對錄製檔案的處理 ...
});

// 播放的同時一起錄製
myAudio.addEventListener('play', () => {
    if (recorder.state === 'inactive') {
        recorder.start(timeslice);
    } else if (recorder.state === 'pause') {
        recorder.resume();
    }
});
myAudio.addEventListener('pause', () => recorder.pause());
myAudio.addEventListener('stop', () => recorder.stop());

myAudio.play();

當然,也可以不用播放 <audio>,讀取完畢後直接背景錄製也行。

錄製麥克風的聲音

  1. 使用 MediaDevices 判斷可取用的裝置,當然不限定是麥克風,也可以是錄音介面
  2. getUserMedia() 取得裝置傳輸的 MediaStream
  3. 建立一個 MediaRecorder 開始錄製
  4. 自由發揮 XD
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    // 取得麥克風裝置
    const deviceId = navigator.mediaDevices.enumerateDevices()
        .then(devices => devices.find(deviceInfo => deviceInfo.kind === 'audioinput'))
        .catch(e => console.error(e))
    
    // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
    const contraints = {
        audio: {
            deviceId,
            sampleRate: 44100,
            sampleSize: 16,
        }
    }

    if (!deviceId) return;
    
    // 取得麥克風資料
    navigator.mediaDevices.getUserMedia(contraints)
        .then(stream => {
            const options = { mimeType: 'audio/mpeg' };
            const recordedChunks = [];
            const recorder = new MediaRecorder(stream, options);  

            recorder.ondataavailable = e => {
              if (e.data.size > 0) recordedChunks.push(e.data);
              // 針對每個 chunk 的處理 ...
            });

            recorder.onstop = () => {
              // 針對錄製檔案的處理 ...
            });

            recorder.start();
        });
}

順道一提,有一個進行中的 W3C 標準:Audio Output Devices API,它希望擴充 HTMLMediaElemet 功能,可以透過 audio.setSinkId(deviceId) 選擇音檔播放的硬體裝置,看起來 Chrome、Edge 有實作、Firefox 還沒,再讓子彈飛一會吧~

處理裝置的存取權限

在 Google 的這篇教學文章有提到:getUserMedia() 會在使用者第一次訪問網站時,跳出提示請使用者授予麥克風的存取權限。往往這種授權提示一般人第一時間會傾向不允許,但是一旦拿不到麥克風的存取權限,我們也沒辦法從 getUserMedia() 知道這件事。這時候可以透過 Permission API 處理這件事。

要注意的是,Permission API 目前還只是 W3C 的 Working Draft,可以在 Chrome 和 Firefox 上運行,但其他瀏覽器尚未支援,請參考 caniuse

navigator.permissions.query({name:'microphone'}).then(function(result) {
  if (result.state == 'granted') {
      // 已授予對麥克風的訪問權
  } else if (result.state == 'prompt') {
      // 尚未授予訪問權,調用 getUserMedia 時將會收到提示
  } else if (result.state == 'denied') {
      // 系統或用戶已拒絕對麥克風的訪問權
  }
  result.onchange = function() {
      // 授權有變化時的處理 ...
  };
});

今天就到這邊啦!
明天開始會進入另一個實作,會應用到這段時間的所學,敬請期待吧~

Reference


上一篇
Day09 - Media Capture and Streams API
下一篇
Day11 - 自製簡易 DAW (0)
系列文
Web x Sound - 用 Web 玩轉聲音13

尚未有邦友留言

立即登入留言