网上搜集了一些浏览器录音的项目,大多半路腰斩,本文整理一套比较完善了解决方案,希望能解决常规浏览器的录音需求。

支持情况

Html5 录音使用MediaStream 查看支持情况:
20190921221122-image.png
由图可知 Firefox Chrome Edge支持度良好,对于IE全版本 ~~GG~~
20190921230026-image.png
再根据上图Edge不需要Https, 而Firefox、Chrome的常规版本碍于安全策略均需要Https环境。所以非Https环境浏览器不能获取到Navigator.getUserMedia()

小结

  • Edge、Firefox、Chrome浏览器可用Html5的MediaStream实现屏幕录音 可根据MDN文档实现
  • IE 使用需要调用flash实现录音功能 根据 FlashWavRecorder

实现代码

Html5 实现中数据处理部分来源于网络

//HTML5加载方法
function h5RecordLoader() {
    //兼容
    window.URL = window.URL || window.webkitURL;
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.mediaDevices;
    if (navigator.mediaDevices) {
        navigator.getUserMedia = navigator.mediaDevices.getUserMedia
    }

    var HZRecorder = function (stream, config) {
        config = config || {};
        config.sampleBits = config.sampleBits || 16;      //采样数位 8, 16
        config.sampleRate = config.sampleRate || (16000); 
        var context = new (window.webkitAudioContext || window.AudioContext)();
        var audioInput = context.createMediaStreamSource(stream);
        var createScript = context.createScriptProcessor || context.createJavaScriptNode;
        var recorder = createScript.apply(context, [4096, 1, 1]);

        var audioData = {
            size: 0          //录音文件长度
            , buffer: []     //录音缓存
            , inputSampleRate: context.sampleRate    //输入采样率
            , inputSampleBits: 16       //输入采样数位 8, 16
            , outputSampleRate: config.sampleRate    //输出采样率
            , oututSampleBits: config.sampleBits       //输出采样数位 8, 16
            , input: function (data) {
                this.buffer.push(new Float32Array(data));
                this.size += data.length;
            }
            , compress: function () { //合并压缩
                //合并
                var data = new Float32Array(this.size);
                var offset = 0;
                for (var i = 0; i < this.buffer.length; i++) {
                    data.set(this.buffer[i], offset);
                    offset += this.buffer[i].length;
                }
                //压缩
                var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
                var length = data.length / compression;
                var result = new Float32Array(length);
                var index = 0, j = 0;
                while (index < length) {
                    result[index] = data[j];
                    j += compression;
                    index++;
                }
                return result;
            }
            , encodeWAV: function () {
                var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
                var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
                var bytes = this.compress();
                var dataLength = bytes.length * (sampleBits / 8);
                var buffer = new ArrayBuffer(44 + dataLength);
                var data = new DataView(buffer);

                var channelCount = 1;//单声道
                var offset = 0;

                var writeString = function (str) {
                    for (var i = 0; i < str.length; i++) {
                        data.setUint8(offset + i, str.charCodeAt(i));
                    }
                }

                // 资源交换文件标识符
                writeString('RIFF'); offset += 4;
                // 下个地址开始到文件尾总字节数,即文件大小-8
                data.setUint32(offset, 36 + dataLength, true); offset += 4;
                // WAV文件标志
                writeString('WAVE'); offset += 4;
                // 波形格式标志
                writeString('fmt '); offset += 4;
                // 过滤字节,一般为 0x10 = 16
                data.setUint32(offset, 16, true); offset += 4;
                // 格式类别 (PCM形式采样数据)
                data.setUint16(offset, 1, true); offset += 2;
                // 通道数
                data.setUint16(offset, channelCount, true); offset += 2;
                // 采样率,每秒样本数,表示每个通道的播放速度
                data.setUint32(offset, sampleRate, true); offset += 4;
                // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
                data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
                // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
                data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
                // 每样本数据位数
                data.setUint16(offset, sampleBits, true); offset += 2;
                // 数据标识符
                writeString('data'); offset += 4;
                // 采样数据总数,即数据总大小-44
                data.setUint32(offset, dataLength, true); offset += 4;
                // 写入采样数据
                if (sampleBits === 8) {
                    for (var i = 0; i < bytes.length; i++, offset++) {
                        var s = Math.max(-1, Math.min(1, bytes[i]));
                        var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
                        val = parseInt(255 / (65535 / (val + 32768)));
                        data.setInt8(offset, val, true);
                    }
                } else {
                    for (var i = 0; i < bytes.length; i++, offset += 2) {
                        var s = Math.max(-1, Math.min(1, bytes[i]));
                        data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                    }
                }

                return new Blob([data], { type: 'audio/wav' });
            }
        };

        //开始录音
        this.start = function () {
            audioInput.connect(recorder);
            recorder.connect(context.destination);
        }

        //停止
        this.stop = function () {
            recorder.disconnect();
            audioInput.disconnect();
            stream.getTracks()[0].stop();
        }

        //获取音频文件
        this.getBlob = function () {
            this.stop();
            return audioData.encodeWAV();
        }

        //回放
        this.play = function (audio) {
            audio.src = window.URL.createObjectURL(this.getBlob());
        }
        //上传
        this.upload = function (callback) {

            console.log(this.getBlob())
            callback(this.getBlob())

        }

        this.onProgress=function (cb){
            recorder.onaudioprocess = function (e) {
                var data= e.inputBuffer.getChannelData(0);
                var l = Math.floor(data.length / 10);
                var vol = 0;
                for(var i = 0; i < l ; i++){
                    vol += Math.abs(data[i*10]);
                }
                audioData.input(e.inputBuffer.getChannelData(0));
                cb(vol)
            }
        }

    };
    //抛出异常
    HZRecorder.throwError = function (message) {
        alert(message);
        throw new function () { this.toString = function () { return message; } }
    }
    //是否支持录音
    HZRecorder.canRecording = (navigator.getUserMedia != null);
    //获取录音机
    HZRecorder.get = function (callback, config) {
        if (callback) {
            if (navigator.getUserMedia) {
                navigator.getUserMedia({ audio: true }).then(function (stream) {
                   var rec = new HZRecorder(stream, config);
                   callback(rec);
                }).catch(function (err) {
                   console.log(err);
                });
            }
        }
    }
    window.HZRecorder = HZRecorder;

}
//flash加载方法

function flashRecordLoader () {
  var Recorder;

  var RECORDED_AUDIO_TYPE = "audio/wav";

  Recorder = {
    recorder: null,
    recorderOriginalWidth: 0,
    recorderOriginalHeight: 0,
    uploadFormId: null,
    uploadFieldName: null,
    isReady: false,

    connect: function (name, attempts) {
      if (navigator.appName.indexOf("Microsoft") != -1) {
        Recorder.recorder = window[name];
      } else {
        Recorder.recorder = document[name];
      }

      if (attempts >= 40) {
        return;
      }

      // flash app needs time to load and initialize
      if (Recorder.recorder && Recorder.recorder.init) {
        Recorder.recorderOriginalWidth = Recorder.recorder.width;
        Recorder.recorderOriginalHeight = Recorder.recorder.height;
        if (Recorder.uploadFormId && $) {
          var frm = $(Recorder.uploadFormId);
          Recorder.recorder.init(frm.attr('action').toString(), Recorder.uploadFieldName, frm.serializeArray());
        }
        return;
      }

      setTimeout(function () { Recorder.connect(name, attempts + 1); }, 100);
    },

    playBack: function (name) {
      // TODO: Rename to `playback`
      Recorder.recorder.playBack(name);
    },

    pausePlayBack: function (name) {
      // TODO: Rename to `pausePlayback`
      Recorder.recorder.pausePlayBack(name);
    },

    playBackFrom: function (name, time) {
      // TODO: Rename to `playbackFrom`
      Recorder.recorder.playBackFrom(name, time);
    },

    record: function (name, filename) {
      Recorder.recorder.record(name, filename);
    },

    stopRecording: function () {
      Recorder.recorder.stopRecording();
    },

    stopPlayBack: function () {
      // TODO: Rename to `stopPlayback`
      Recorder.recorder.stopPlayBack();
    },

    observeLevel: function () {
      Recorder.recorder.observeLevel();
    },

    stopObservingLevel: function () {
      Recorder.recorder.stopObservingLevel();
    },

    observeSamples: function () {
      Recorder.recorder.observeSamples();
    },

    stopObservingSamples: function () {
      Recorder.recorder.stopObservingSamples();
    },

    resize: function (width, height) {
      Recorder.recorder.width = width + "px";
      Recorder.recorder.height = height + "px";
    },

    defaultSize: function () {
      Recorder.resize(Recorder.recorderOriginalWidth, Recorder.recorderOriginalHeight);
    },

    show: function () {
      Recorder.recorder.show();
    },

    hide: function () {
      Recorder.recorder.hide();
    },

    duration: function (name) {
      // TODO: rename to `getDuration`
      return Recorder.recorder.duration(name || Recorder.uploadFieldName);
    },

    getBase64: function (name) {
      var data = Recorder.recorder.getBase64(name);
      return 'data:' + RECORDED_AUDIO_TYPE + ';base64,' + data;
    },

    getBlob: function (name) {
      var base64Data = Recorder.getBase64(name).split(',')[1];
      return base64toBlob(base64Data, RECORDED_AUDIO_TYPE);
    },

    getCurrentTime: function (name) {
      return Recorder.recorder.getCurrentTime(name);
    },

    isMicrophoneAccessible: function () {
      return Recorder.recorder.isMicrophoneAccessible();
    },

    updateForm: function () {
      var frm = $(Recorder.uploadFormId);
      Recorder.recorder.update(frm.serializeArray());
    },

    showPermissionWindow: function (options) {
      Recorder.resize(240, 160);
      // need to wait until app is resized before displaying permissions screen
      var permissionCommand = function () {
        if (options && options.permanent) {
          Recorder.recorder.permitPermanently();
        } else {
          Recorder.recorder.permit();
        }
      };
      setTimeout(permissionCommand, 1);
    },

    configure: function (rate, gain, silenceLevel, silenceTimeout) {
      rate = parseInt(rate || 22);
      gain = parseInt(gain || 100);
      silenceLevel = parseInt(silenceLevel || 0);
      silenceTimeout = parseInt(silenceTimeout || 4000);
      switch (rate) {
        case 44:
        case 22:
        case 11:
        case 8:
        case 5:
          break;
        default:
          throw ("invalid rate " + rate);
      }

      if (gain < 0 || gain > 100) {
        throw ("invalid gain " + gain);
      }

      if (silenceLevel < 0 || silenceLevel > 100) {
        throw ("invalid silenceLevel " + silenceLevel);
      }

      if (silenceTimeout < -1) {
        throw ("invalid silenceTimeout " + silenceTimeout);
      }

      Recorder.recorder.configure(rate, gain, silenceLevel, silenceTimeout);
    },

    setUseEchoSuppression: function (val) {
      if (typeof (val) != 'boolean') {
        throw ("invalid value for setting echo suppression, val: " + val);
      }

      Recorder.recorder.setUseEchoSuppression(val);
    },

    setLoopBack: function (val) {
      if (typeof (val) != 'boolean') {
        throw ("invalid value for setting loop back, val: " + val);
      }

      Recorder.recorder.setLoopBack(val);
    }
  };

  function base64toBlob(b64Data, contentType, sliceSize) {
    contentType = contentType || '';
    sliceSize = sliceSize || 512;

    var byteCharacters = atob(b64Data);
    var byteArrays = [];

    for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
      var slice = byteCharacters.slice(offset, offset + sliceSize);

      var byteNumbers = new Array(slice.length);
      for (var i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }

      var byteArray = new Uint8Array(byteNumbers);
      byteArrays.push(byteArray);
    }

    return new Blob(byteArrays, { type: contentType });
  }
  window.FWRecorder = Recorder;
}

*注:swfobject.js 2.2版本可从网络轻松获取。

  <!-- 一个简单的调用例子,仅供参考: -->
<html lang="zh-CN">
<html>
<head>
    <title>h5录音</title>
    <meta charset="UTF-8">
    <script src="./js/jquery.js"></script>
    <!-- H5录音 -->
    <script type="text/javascript" src="./js/h5Recorder.js"></script>
    <!-- Flash录音 -->
    <script type="text/javascript" src="./js/swfobject.js"></script>
    <script type="text/javascript" src="./js/flashRecorder.js"></script>
</head>

<body>
    <!-- Recorder control buttons -->
    <h1>浏览器录音</h1>
    <div class="recorder">
        <button id="start" class="start-recording" onclick="flashStart()">开始录音</button>
        <div class="level">
            <div class="progress"></div>
        </div>
        <button id="stop" class="stop-recording" disabled="disabled" onclick="flashStop()">停止录音</button>
        <br>
        <br>
        <br>
        <button id="play" class="start-playing" onclick="flashPlay()" title="Play">播放录音</button>


        <audio src="#" autoplay controls id="reply" class="audioEl"></audio>
        <div id="flashcontent">
        </div>
        <br>
        <br>
        <br>
        <button id="upload" onclick="uploadFlashPlayer()">录音转文字</button>
        <p id="baiduApi" ></p>
    </div>
    <script>
        var userAgent = navigator.userAgent;
        if (!!window.ActiveXObject || "ActiveXObject" in window) {
            //IE浏览器 启用flash
            flashRecordLoader();

            var RECORDER_APP_ID = "recorderApp";
            var $level = $('.level .progress');
            var appWidth = 24,appHeight = 24,flashvars = {},params = {},attributes = { 'id': RECORDER_APP_ID, 'name': RECORDER_APP_ID };
            swfobject.embedSWF("./js/recorder.swf", "flashcontent", appWidth, appHeight, "11.0.0", "", flashvars, params, attributes);

            window.fwr_event_handler = function fwr_event_handler() {
                var name, $controls;
                switch (arguments[0]) {
                    case "ready":
                        FWRecorder.connect(RECORDER_APP_ID, 0);
                        FWRecorder.recorderOriginalWidth = appWidth;
                        FWRecorder.recorderOriginalHeight = appHeight;
                        break;

                    case "microphone_user_request":
                        FWRecorder.showPermissionWindow();
                        break;

                    case "permission_panel_closed":
                        FWRecorder.defaultSize();
                        break;

                    case "recording":
                        FWRecorder.hide();
                        FWRecorder.observeLevel();//监控声音
                        $("#stop").attr("disabled",false);
                        $("#start").attr("disabled","disabled");
                        break;

                    case "recording_stopped":
                        FWRecorder.show();
                        FWRecorder.stopObservingLevel();//停止监控声音
                        $level.css({ height: 0 });
                        $("#recorderApp").css("display","none");
                        break;

                    case "microphone_level":
                        $level.css({ height: arguments[1] * 100 + '%' });//渲染监控声音
                        break;

                    case "save_pressed":
                        FWRecorder.updateForm();
                        break;

                    case "saving":
                        name = arguments[1];
                        console.info('saving started', name);
                        break;

                    case "saved":
                        name = arguments[1];
                        var response = arguments[2];
                        console.info('saving success', name, response);
                        break;

                    case "save_failed":
                        name = arguments[1];
                        var errorMessage = arguments[2];
                        console.info('saving failed', name, errorMessage);
                        break;

                    case "save_progress":
                        name = arguments[1];
                        var bytesLoaded = arguments[2];
                        var bytesTotal = arguments[3];
                        console.info('saving progress', name, bytesLoaded, '/', bytesTotal);
                        break;
                }
            };
        } else {
            h5RecordLoader();
            //清除onclick;
            $("#start").attr("onclick","");
            $("#stop").attr("onclick","");
            $("#play").attr("onclick","");
            $("#upload").attr("onclick","");

            //
            var recorder;
            var audio = $("#reply")[0];
            var $level = $('.level .progress');
            $("#start").click(function () {
                $("#stop").attr("disabled",false);
                $("#start").attr("disabled","disabled");
                HZRecorder.get(function (rec) {
                    recorder = rec;
                    recorder.start();
                    //开始录音后就可以实时监视声音录制过程
                    recorder.onProgress(function (vol) {
                        console.log(vol);
                    })
                });
                //开始录音,其中replay是一个音频对象
            })
            $("#stop").click(function () {
                //停止录音
                recorder.stop();
                $("#start").attr("disabled",false);
                $("#stop").attr("disabled","disabled");
            })
            $("#play").click(function () {
                //播放录音
                $(".audioEl").css("display","inline-block");
                recorder.play(audio);
            })
            $("#upload").click(function () {
                //上传音频数据,经过优化压缩过的
                recorder.upload(function (data) {
                    var reader = new FileReader();
                    reader.readAsDataURL(recorder.getBlob());
                    reader.onloadend = function () {
                        var base64data = reader.result;
                        var fd = new FormData();
                        fd.append("audioData", base64data);
                        var xhr = new XMLHttpRequest();
                        xhr.open("POST", "https://10.10.10.10:8080/server_api");
                        xhr.onreadystatechange = function () {
                            if (xhr.readyState == 4 && (xhr.status == 200)) {
                                console.log(xhr.responseText);
                                result = JSON.parse(xhr.responseText);
                                if(result.err_no==0){
                                    $("#baiduApi").html(result.result.toString());
                                }else{
                                    $("#baiduApi").html("解析失败,错误码:"+Number(result.err_no)+","+result.err_msg);
                                }
                            }
                        }
                        xhr.send(base64data);
                    }

                });
            })
        }

        function flashStart() {
            FWRecorder.record('audio', 'audio.wav');

        }
        function flashStop() {
            FWRecorder.stopRecording('audio');
            $("#start").attr("disabled",false);
            $("#stop").attr("disabled","disabled");
        }
        function flashPlay() {
            FWRecorder.playBack('audio');
        }

        function uploadFlashPlayer() {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "https://10.10.10.10:8080/server_api");
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && (xhr.status == 200)) {
                    console.log(xhr.responseText);
                    result = JSON.parse(xhr.responseText);
                    if(result.err_no==0){
                        $("#baiduApi").html(result.result.toString());
                    }else{
                        $("#baiduApi").html("解析失败,错误码:"+Number(result.err_no)+","+result.err_msg);
                    }
                }
            }
            xhr.send(FWRecorder.getBase64());
        }
    </script>
</body>

</html>

百度语音解析这里用Nodejs 的API,顺便描述一下Nodejs的https本地配置

  • 生成证书
    openssl genrsa -out domain.key 2048
    openssl req -new -x509 -key domain.key -out domain.cert -days 3650 -subj /CN=domain
let AipSpeech = require("baidu-aip-sdk").speech;
let fs = require('fs');
const http = require('http');
const url = require('url');
var querystring = require('querystring');
// 务必替换百度云控制台中新建百度语音应用的 Api Key 和 Secret Key
let client = new AipSpeech("**************", '************', '************');

let voice = fs.readFileSync('百度语音解析官方样例path:16k_test.pcm');

let voiceBase64 = new Buffer(voice);

// 识别本地语音文件
client.recognize(voiceBase64, 'pcm', 16000).then(function (result) {
    console.log('语音识别本地音频文件结果: ' + JSON.stringify(result));
}, function (err) {
    console.log(err);
});
const hostname = '10.10.10.10';
const port = 8080;
var express = require('express');
var app = express();
var key = fs.readFileSync("C:\\Users\\Documents\\domain.key");
var cert = fs.readFileSync('C:\\Users\\Documents\\domain.crt');

var options = {
    key: key,
    cert: cert
};

app.use(express.static('html'))  //设置静态资源文件
app.post('/server_api', function (req, res) {
    str = "";
    resultData = "";
    req.on('data', data => {
        str += data
    })
    //数据全部到达触发(一次)
    req.on('end', () => {
        var dataBuffer = new Buffer(str.replace(/^data:audio\/\w+;base64,/, ""), 'base64');

        fs.writeFile("C:\\Users\\Desktop\\FinalRecord\\NodeServer\\speech\\node\\sdasdaas.wav", dataBuffer, (err) => {
            if (err) throw err;
            console.log('文件已保存');
        })

        client.recognize(dataBuffer, 'wav', 16000).then(function (result) {
            resultData = result;
            console.log('语音识别本地音频文件结果: ' + JSON.stringify(result));
            res.write(JSON.stringify(result));
            res.end();
        }, function (err) {
            resultData = "解析失败" + err;
            console.log(err);
        });
    }) 

})
var https = require('https');
https.createServer(options, app).listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

只要有树叶飞舞的地方,火就会燃烧。