การบันทึกเสียงจากผู้ใช้

ขณะนี้เบราว์เซอร์จำนวนมากสามารถเข้าถึงอินพุตวิดีโอและเสียงจากผู้ใช้ได้แล้ว อย่างไรก็ตาม เบราว์เซอร์อาจเป็นประสบการณ์แบบอินไลน์แบบไดนามิกแบบสมบูรณ์ หรืออาจมีการมอบสิทธิ์ให้กับแอปอื่นในอุปกรณ์ของผู้ใช้ ทั้งนี้ขึ้นอยู่กับเบราว์เซอร์นั้นๆ

เริ่มต้นอย่างง่ายๆ และค่อยๆ เติบโต

วิธีที่ง่ายที่สุดคือขอไฟล์ที่บันทึกไว้ล่วงหน้าจากผู้ใช้ คุณสามารถทำได้โดยการสร้างองค์ประกอบอินพุตไฟล์อย่างง่ายและเพิ่มตัวกรอง accept ที่บ่งชี้ว่าเรารับเฉพาะไฟล์เสียงเท่านั้น และแอตทริบิวต์ capture ที่ระบุว่าเราต้องการรับจากไมโครโฟนโดยตรง

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

วิธีนี้ใช้งานได้ในทุกแพลตฟอร์ม ในเดสก์ท็อป ระบบจะแจ้งให้ผู้ใช้อัปโหลดไฟล์จากระบบไฟล์ (ไม่สนใจแอตทริบิวต์ capture) ใน Safari บน iOS แอปจะเปิดแอปไมโครโฟนซึ่งให้คุณบันทึกเสียงแล้วส่งกลับไปยังหน้าเว็บ ส่วนใน 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 และใช้ตัวกรองกับ Web Audio

แม้ว่าการใช้วิธีการขององค์ประกอบอินพุตเพื่อเข้าถึงข้อมูลเสียงมีแพร่หลาย แต่วิธีนี้ก็เป็นตัวเลือกที่น่าสนใจน้อยที่สุด เราต้องการเข้าถึงไมโครโฟน และมอบประสบการณ์การใช้งานที่ดีในหน้าเว็บโดยตรง

เข้าถึงไมโครโฟนแบบอินเทอร์แอกทีฟ

เบราว์เซอร์ที่ทันสมัยสามารถต่อสายตรงไปยังไมโครโฟนช่วยให้เราสร้างประสบการณ์ที่ผสานรวมเข้ากับหน้าเว็บได้อย่างเต็มที่โดยที่ผู้ใช้จะไม่ต้องออกจากเบราว์เซอร์

รับสิทธิ์เข้าถึงไมโครโฟน

เราเข้าถึงไมโครโฟนได้โดยตรงโดยใช้ API ในข้อกำหนดของ WebRTC ที่ชื่อ getUserMedia() getUserMedia() จะแจ้งให้ผู้ใช้ เข้าถึงไมโครโฟนและกล้องที่เชื่อมต่ออยู่

หาก API เสร็จสมบูรณ์ จะส่งกลับ Stream ที่จะมีข้อมูลจากกล้องหรือไมโครโฟน จากนั้นเราสามารถแนบกับองค์ประกอบ <audio>, แนบไปยังสตรีม WebRTC, แนบกับ Web Audio AudioContext หรือบันทึกโดยใช้ MediaRecorder API

หากต้องการรับข้อมูลจากไมโครโฟน เราเพิ่งตั้งค่า audio: true ในออบเจ็กต์ข้อจำกัดที่ส่งไปยัง getUserMedia() API

<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');
});

จากนั้น คุณจะส่งเงิน deviceId ที่ต้องการใช้เมื่อโทรหา getUserMedia ได้

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

จริงๆ แล้ว การดำเนินการนี้ไม่มีประโยชน์ สิ่งที่เราทำได้มีเพียงการนำข้อมูลเสียงมาเล่น

เข้าถึงข้อมูลดิบจากไมโครโฟน

ในการเข้าถึงข้อมูลดิบจากไมโครโฟน เราจะต้องใช้สตรีมที่สร้างโดย getUserMedia() แล้วใช้ Web Audio API เพื่อประมวลผลข้อมูล Web Audio API เป็น API แบบง่ายที่ใช้แหล่งที่มาของอินพุตและเชื่อมต่อแหล่งที่มาเหล่านั้นกับโหนดที่สามารถประมวลผลข้อมูลเสียง (ปรับค่าเกน ฯลฯ) และกับลำโพงในท้ายที่สุดเพื่อให้ผู้ใช้ได้ยินเสียง

หนึ่งในโหนดที่คุณเชื่อมต่อได้คือ AudioWorkletNode โหนดนี้ช่วยให้คุณมีขีดความสามารถระดับต่ำสำหรับการประมวลผลเสียงที่กำหนดเอง การประมวลผลเสียงจริงจะเกิดขึ้นในเมธอดโค้ดเรียกกลับ process() ใน AudioWorkletProcessor เรียกใช้ฟังก์ชันนี้เพื่อป้อนอินพุตและพารามิเตอร์ รวมทั้งดึงข้อมูลเอาต์พุต

ดูข้อมูลเพิ่มเติมได้ที่ Enter Audio Worklet

<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 API navigator.permission ช่วยให้คุณค้นหาสถานะความสามารถในการเข้าถึง 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 () {};
});

ความคิดเห็น