前端设置

前端界面参考了ns2250225/audioRecord: 📢 仿微信的语音段传输 (github.com), 使用 html+css+js 编写网页,其中html控制页面中有哪些组成部分,css控制各组成部分的外观,JS控制页面的执行逻辑。

前端代码中,较为重要的两个步骤在于音频录制和音频传输。以下分辨介绍这两个部分。

1. 音频录制

音频录制代码如下,主要基于 MediaRecorder 服务。

if (navigator.mediaDevices.getUserMedia) {
  console.log('getUserMedia supported.');

  var constraints = { audio: true };
  var chunks = [];

  var onSuccess = function (stream) {
    var mediaRecorder = new MediaRecorder(stream);

    record.onclick = function () {
      mediaRecorder.start();
      console.log(mediaRecorder.state);
      console.log("recorder started");

      stop.disabled = false;
      record.disabled = true;
    }

    stop.onclick = function () {
      mediaRecorder.stop();
      console.log(mediaRecorder.state);
      console.log("recorder stopped");

      stop.disabled = true;
      record.disabled = false;
    }

    mediaRecorder.onstop = function (e) {
      console.log("data available after MediaRecorder.stop() called.");

      // 保存录音
      var blob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' });

      // 生成录音框
      createAudioBox(blob, msg_content)

      // 发送录音
      ws.send(blob)

      // 重置录音数据
      chunks = [];

      console.log("recorder stopped");
    }

    // 录音逻辑
    mediaRecorder.ondataavailable = function (e) {
      chunks.push(e.data);
    }
  }

  var onError = function (err) {
    console.log('The following error occured: ' + err);
  }

  // 开始获取音频流
  navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);

} else {
  console.log('getUserMedia not supported on your browser!');
}

以上代码中的录音框函数createAudioBox()如下所示。注意这里的一个坑是,正常设置录音框时,audio.duration 属性,这说明计算机不清楚音频的具体时长。这种情况下,需要将audio.currentTime 赋一个非常大的值,相当于直接将进度条拉到终点,以检测音频具体的时间。

/* 创建音频框 */
function createAudioBox(blob_obj, msg_content) {
  var tmp_div = document.createElement('div');
  var audio = document.createElement('audio');
  var tmp_span = document.createElement('span');
  var tmp_btn = document.createElement('img');

  tmp_div.setAttribute('class', 'myAudio');
  tmp_span.setAttribute('class', 'audio_time');
  tmp_btn.setAttribute('class', 'play_btn');
  tmp_btn.src = "/static/images/audio-high.png"
  audio.setAttribute('id', 'myAudio');
  audio.src = window.URL.createObjectURL(blob_obj);
  audio.addEventListener('loadedmetadata', () => {
    if (audio.duration === Infinity || isNaN(Number(audio.duration))) {
      audio.currentTime = 1e101   // 相当于快进
      audio.addEventListener('timeupdate', getDuration)
    }
  })
function getDuration(event) {
	event.target.currentTime = 0
	event.target.removeEventListener('timeupdate', getDuration)
    tmp_span.innerHTML = event.target.duration + '"'
}

2. 音频传输

音频传输服务代码如下, 核心是利用 WebSocket()与服务器之间建立套接字。

// 注册PWA服务
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/static/js/sw.js')
    .then(function () {
      console.log('SW registered');
    });
}

// 设置websocket服务器地址
const wsUrl = 'ws://localhost:8000/';
const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";

// Websocket钩子方法
ws.onopen = function (evt) {
  console.log('ws open()');
}

ws.onerror = function (err) {
  console.error('ws onerror() ERR:', err);
}

ws.onmessage = function (evt) {
  console.log('ws onmessage() data:', typeof (evt.data));
  // 判断 typeof (evt.data) 是否为文本类型
  if (typeof (evt.data) === 'string') {
    // 添加文本显示框
    var tmp_div = document.createElement('div');
    tmp_div.setAttribute('class', 'myText');
    tmp_div.innerHTML = evt.data;
    msg_content.appendChild(tmp_div);
  } else if (typeof (evt.data) === 'object') {
    // TODO: 未来添加TTS服务时,可在此增加接受语音的逻辑
  } else {
    console.error('Unexpected data type:', typeof (evt.data));
  }
}