├── src ├── styles │ └── app.styl ├── assets │ └── logo.png ├── main.js ├── shared │ ├── Utils.js │ ├── encoder-ogg-worker.js │ ├── encoder-wav-worker.js │ ├── wave-encoder.js │ ├── encoder-mp3-worker.js │ ├── WebAudioPeakMeter.js │ └── RecorderService.js ├── views │ ├── Version.vue │ ├── Home.vue │ ├── Test2.vue │ ├── Test4.vue │ ├── Test5.vue │ ├── Test3.vue │ ├── Test6.vue │ ├── Diagnostics.vue │ └── Test1.vue ├── router │ └── index.js └── App.vue ├── .eslintignore ├── dist ├── favicon.ico ├── workers │ └── encoders │ │ └── OggVorbisEncoder.min.js.mem ├── index.html └── js │ └── app.a3ad6502.js ├── public ├── favicon.ico ├── workers │ └── encoders │ │ └── OggVorbisEncoder.min.js.mem └── index.html ├── docs ├── scrshot-test1.png └── scrshot-test1b.png ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── .eslintrc.js ├── package.json └── vue.config.js /src/styles/app.styl: -------------------------------------------------------------------------------- 1 | @import '~vuetify/src/stylus/main' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/dist/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /docs/scrshot-test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/docs/scrshot-test1.png -------------------------------------------------------------------------------- /docs/scrshot-test1b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/docs/scrshot-test1b.png -------------------------------------------------------------------------------- /dist/workers/encoders/OggVorbisEncoder.min.js.mem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/dist/workers/encoders/OggVorbisEncoder.min.js.mem -------------------------------------------------------------------------------- /public/workers/encoders/OggVorbisEncoder.min.js.mem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliatech/web-audio-recording-tests/HEAD/public/workers/encoders/OggVorbisEncoder.min.js.mem -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | /env 21 | /*.iml 22 | /dload 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | import Vuetify from 'vuetify' 4 | import App from './App.vue' 5 | import router from '@/router/index.js' 6 | 7 | // import colors from 'vuetify/es5/util/colors' 8 | 9 | Vue.use(Vuetify, { 10 | theme: { 11 | primary: '#546E7A', 12 | secondary: '#B0BEC5', 13 | accent: '#448AFF', 14 | error: '#EF5350', 15 | warning: '#FFF176', 16 | info: '#2196f3', 17 | success: '#4caf50' 18 | } 19 | }) 20 | 21 | Vue.config.productionTip = false 22 | 23 | new Vue({ 24 | router, 25 | render: h => h(App) 26 | }).$mount('#app') 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Web Audio Recording Tests 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Web Audio Recording Tests
-------------------------------------------------------------------------------- /src/shared/Utils.js: -------------------------------------------------------------------------------- 1 | export class Utils { 2 | // https://stackoverflow.com/a/14919494 3 | humanFileSize (bytes, si) { 4 | var thresh = si ? 1000 : 1024 5 | if (Math.abs(bytes) < thresh) { 6 | return bytes + ' B' 7 | } 8 | var units = si 9 | ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 10 | : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] 11 | var u = -1 12 | do { 13 | bytes /= thresh 14 | ++u 15 | } while (Math.abs(bytes) >= thresh && u < units.length - 1) 16 | return bytes.toFixed(1) + ' ' + units[u] 17 | } 18 | 19 | /** 20 | * Check user agent for Safari on iOS 21 | * @returns {boolean} 22 | */ 23 | isIosSafari () { 24 | return navigator.userAgent.match(/iP(od|hone|ad)/) && 25 | navigator.userAgent.match(/AppleWebKit/) && 26 | !navigator.userAgent.match(/(Cr|Fx|OP)iOS/) 27 | } 28 | } 29 | 30 | export default new Utils() 31 | -------------------------------------------------------------------------------- /src/views/Version.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Josh Sanderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Audio Recording Tests 2 | 3 | ## UPDATE 2021-01!!! 4 | As of 14.3, iOS seems to have finally enabled the native MediaRecorder. All tests in this site use feature detection of MediaRecorder, and so continue to work, but now use MediaRecorder on iOS. This makes many of the notes and code here less relevant unless needing to support older browsers that don't have a MediaRecorder. 5 | 6 | ### More Info 7 | * https://webkit.org/blog/11353/mediarecorder-api/ 8 | * https://caniuse.com/mediarecorder 9 | * https://blog.addpipe.com/safari-technology-preview-73-adds-limited-mediastream-recorder-api-support/ 10 | 11 |
12 | 13 | ---- 14 | 15 | ## Overview 16 | Tests of web audio API recording that work across all browsers, including iOS/Safari 11.2.x and newer. 17 | * https://kaliatech.github.io/web-audio-recording-tests/dist/index.html 18 | 19 | This was built as a single page application using vue and webpack, and it includes a number of advanced components 20 | that are not important for doing basic stable recording. A simpler version, using plain javascript only, is available 21 | here: 22 | * https://github.com/kaliatech/web-audio-recording-tests-simpler 23 | 24 | ### Screenshot
25 | ![Screenshot](docs/scrshot-test1b.png?raw=true) 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | globals: { 12 | "__webpack_public_path__": true, 13 | "WEBPACK_VERSION": true, 14 | "WEBPACK_COMMITHASH": true, 15 | "WEBPACK_BRANCH": true, 16 | "WEBPACK_TIMESTAMP": true, 17 | "BASE_URL": true 18 | }, 19 | extends: [ 20 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 21 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 22 | 'plugin:vue/essential', 23 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 24 | 'standard' 25 | ], 26 | // required to lint *.vue files 27 | plugins: [ 28 | 'vue' 29 | ], 30 | // add your custom rules here 31 | rules: { 32 | // allow paren-less arrow functions 33 | 'arrow-parens': 0, 34 | // brace style 35 | 'brace-style': ["warn", "stroustrup"], 36 | // allow async-await 37 | 'generator-star-spacing': 'off', 38 | // allow debugger during development 39 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-audio-tests", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "babel-polyfill": "^6.26.0", 12 | "lamejs": "^1.2.0", 13 | "vue": "^2.6.12", 14 | "vue-router": "^3.4.9", 15 | "vuetify": "^1.5.24" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli": "^3.12.1", 19 | "@vue/cli-plugin-babel": "^3.12.1", 20 | "@vue/cli-plugin-eslint": "^3.12.1", 21 | "@vue/cli-service": "^3.12.1", 22 | "eslint-config-standard": "^12.0.0", 23 | "eslint-plugin-import": "^2.22.1", 24 | "eslint-plugin-node": "^8.0.1", 25 | "eslint-plugin-promise": "^4.2.1", 26 | "eslint-plugin-standard": "^4.1.0", 27 | "git-revision-webpack-plugin": "^3.0.6", 28 | "stylus": "^0.54.8", 29 | "stylus-loader": "^3.0.2", 30 | "vue-template-compiler": "^2.6.12" 31 | }, 32 | "babel": { 33 | "presets": [ 34 | "@vue/app" 35 | ] 36 | }, 37 | "eslintConfig": { 38 | "root": true, 39 | "extends": [ 40 | "plugin:vue/essential", 41 | "eslint:recommended" 42 | ] 43 | }, 44 | "postcss": { 45 | "plugins": { 46 | "autoprefixer": {} 47 | } 48 | }, 49 | "browserslist": [ 50 | "> 1%", 51 | "last 2 versions", 52 | "not ie <= 8" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import Home from '@/views/Home' 5 | import Diagnostics from '@/views/Diagnostics' 6 | import Test1 from '@/views/Test1' 7 | import Test2 from '@/views/Test2' 8 | import Test3 from '@/views/Test3' 9 | import Test4 from '@/views/Test4' 10 | import Test5 from '@/views/Test5' 11 | import Test6 from '@/views/Test6' 12 | import Version from '@/views/Version' 13 | 14 | Vue.use(VueRouter) 15 | 16 | const router = new VueRouter({ 17 | mode: 'hash', 18 | routes: [ 19 | { 20 | path: '/', 21 | name: 'home', 22 | component: Home 23 | }, 24 | { 25 | path: '/diagnostics', 26 | name: 'diagnostics', 27 | component: Diagnostics 28 | }, 29 | { 30 | path: '/test1', 31 | name: 'test1', 32 | component: Test1 33 | }, 34 | { 35 | path: '/test2', 36 | name: 'test2', 37 | component: Test2 38 | }, 39 | { 40 | path: '/test3', 41 | name: 'test3', 42 | component: Test3 43 | }, 44 | { 45 | path: '/test4', 46 | name: 'test4', 47 | component: Test4 48 | }, 49 | { 50 | path: '/test5', 51 | name: 'test5', 52 | component: Test5 53 | }, 54 | { 55 | path: '/test6', 56 | name: 'test6', 57 | component: Test6 58 | }, 59 | { 60 | path: '/version', 61 | name: 'version', 62 | component: Version 63 | } 64 | ] 65 | }) 66 | 67 | export default router 68 | -------------------------------------------------------------------------------- /src/shared/encoder-ogg-worker.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | let channels = 1 3 | let quality = 0.4 4 | 5 | let oggEncoder = null 6 | 7 | let oggData = [] 8 | 9 | function init (opts) { 10 | /* global OggVorbisEncoder */ 11 | 12 | // Possibly required for loading min version, but couldn't get min version to work. Not sure why. 13 | // self.OggVorbisEncoderConfig = { 14 | // memoryInitializerPrefixURL: opts.baseUrl + '/workers/encoders/' 15 | // } 16 | 17 | // Unable to load min version. Not sure why. Error in firefox is "The URI is malformed". Guessing related to the .mem. 18 | importScripts(opts.baseUrl + '/workers/encoders/OggVorbisEncoder.js'); // eslint-disable-line 19 | oggEncoder = new OggVorbisEncoder(opts.sampleRate, channels, quality) 20 | } 21 | 22 | function encode (arrayBuffer) { 23 | var data = oggEncoder.encode([arrayBuffer]) 24 | oggData.push(data) 25 | } 26 | 27 | function dump () { 28 | let blob = oggEncoder.finish('audio/ogg') 29 | 30 | // this works, but likely results in native memory copy 31 | postMessage(blob) 32 | 33 | // Looking at source of OggVorbisEncoder, I think it would be easy to change to allow transferring of the 34 | // raw buffer instead. 35 | 36 | // this does not work, I presume because blobs aren't transferrable 37 | // postMessage(blob, [blob]) 38 | 39 | oggData = [] 40 | } 41 | 42 | onmessage = function (e) { 43 | if (e.data[0] === 'encode') { 44 | encode(e.data[1]) 45 | } 46 | else if (e.data[0] === 'dump') { 47 | dump(e.data[1]) 48 | } 49 | else if (e.data[0] === 'init') { 50 | init(e.data[1]) 51 | } 52 | else if (e.data[0] === 'close') { 53 | self.close() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 42 | 51 | 59 | -------------------------------------------------------------------------------- /src/shared/encoder-wav-worker.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | // Parts copied from https://github.com/chris-rudmin/Recorderjs 3 | let BYTES_PER_SAMPLE = 2 4 | let recorded = [] 5 | 6 | function encode (buffer) { 7 | let length = buffer.length 8 | let data = new Uint8Array(length * BYTES_PER_SAMPLE) 9 | for (let i = 0; i < length; i++) { 10 | let index = i * BYTES_PER_SAMPLE 11 | let sample = buffer[i] 12 | if (sample > 1) { 13 | sample = 1 14 | } 15 | else if (sample < -1) { 16 | sample = -1 17 | } 18 | sample = sample * 32768 19 | data[index] = sample 20 | data[index + 1] = sample >> 8 21 | } 22 | recorded.push(data) 23 | } 24 | 25 | function dump (sampleRate) { 26 | let bufferLength = recorded.length ? recorded[0].length : 0 27 | let length = recorded.length * bufferLength 28 | let wav = new Uint8Array(44 + length) 29 | 30 | let view = new DataView(wav.buffer) 31 | 32 | // RIFF identifier 'RIFF' 33 | view.setUint32(0, 1380533830, false) 34 | // file length minus RIFF identifier length and file description length 35 | view.setUint32(4, 36 + length, true) 36 | // RIFF type 'WAVE' 37 | view.setUint32(8, 1463899717, false) 38 | // format chunk identifier 'fmt ' 39 | view.setUint32(12, 1718449184, false) 40 | // format chunk length 41 | view.setUint32(16, 16, true) 42 | // sample format (raw) 43 | view.setUint16(20, 1, true) 44 | // channel count 45 | view.setUint16(22, 1, true) 46 | // sample rate 47 | view.setUint32(24, sampleRate, true) 48 | // byte rate (sample rate * block align) 49 | view.setUint32(28, sampleRate * BYTES_PER_SAMPLE, true) 50 | // block align (channel count * bytes per sample) 51 | view.setUint16(32, BYTES_PER_SAMPLE, true) 52 | // bits per sample 53 | view.setUint16(34, 8 * BYTES_PER_SAMPLE, true) 54 | // data chunk identifier 'data' 55 | view.setUint32(36, 1684108385, false) 56 | // data chunk length 57 | view.setUint32(40, length, true) 58 | 59 | for (var i = 0; i < recorded.length; i++) { 60 | wav.set(recorded[i], i * bufferLength + 44) 61 | } 62 | 63 | recorded = [] 64 | let msg = [wav.buffer] 65 | postMessage(msg, [msg[0]]) 66 | } 67 | 68 | self.onmessage = function (e) { 69 | if (e.data[0] === 'encode') { 70 | encode(e.data[1]) 71 | } 72 | else if (e.data[0] === 'dump') { 73 | dump(e.data[1]) 74 | } 75 | else if (e.data[0] === 'close') { 76 | self.close() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/shared/wave-encoder.js: -------------------------------------------------------------------------------- 1 | export const MimeType = 'audio/wave' 2 | 3 | // Parts copied from https://github.com/chris-rudmin/Recorderjs 4 | export default function () { 5 | let BYTES_PER_SAMPLE = 2 6 | let recorded = [] 7 | 8 | function encode (buffer) { 9 | let length = buffer.length 10 | let data = new Uint8Array(length * BYTES_PER_SAMPLE) 11 | for (let i = 0; i < length; i++) { 12 | let index = i * BYTES_PER_SAMPLE 13 | let sample = buffer[i] 14 | if (sample > 1) { 15 | sample = 1 16 | } 17 | else if (sample < -1) { 18 | sample = -1 19 | } 20 | sample = sample * 32768 21 | data[index] = sample 22 | data[index + 1] = sample >> 8 23 | } 24 | recorded.push(data) 25 | } 26 | 27 | function dump (sampleRate) { 28 | let bufferLength = recorded.length ? recorded[0].length : 0 29 | let length = recorded.length * bufferLength 30 | let wav = new Uint8Array(44 + length) 31 | 32 | let view = new DataView(wav.buffer) 33 | 34 | // RIFF identifier 'RIFF' 35 | view.setUint32(0, 1380533830, false) 36 | // file length minus RIFF identifier length and file description length 37 | view.setUint32(4, 36 + length, true) 38 | // RIFF type 'WAVE' 39 | view.setUint32(8, 1463899717, false) 40 | // format chunk identifier 'fmt ' 41 | view.setUint32(12, 1718449184, false) 42 | // format chunk length 43 | view.setUint32(16, 16, true) 44 | // sample format (raw) 45 | view.setUint16(20, 1, true) 46 | // channel count 47 | view.setUint16(22, 1, true) 48 | // sample rate 49 | view.setUint32(24, sampleRate, true) 50 | // byte rate (sample rate * block align) 51 | view.setUint32(28, sampleRate * BYTES_PER_SAMPLE, true) 52 | // block align (channel count * bytes per sample) 53 | view.setUint16(32, BYTES_PER_SAMPLE, true) 54 | // bits per sample 55 | view.setUint16(34, 8 * BYTES_PER_SAMPLE, true) 56 | // data chunk identifier 'data' 57 | view.setUint32(36, 1684108385, false) 58 | // data chunk length 59 | view.setUint32(40, length, true) 60 | 61 | for (var i = 0; i < recorded.length; i++) { 62 | wav.set(recorded[i], i * bufferLength + 44) 63 | } 64 | 65 | recorded = [] 66 | postMessage(wav.buffer, [wav.buffer]) 67 | } 68 | 69 | onmessage = function (e) { 70 | console.log('hereAA') 71 | if (e.data[0] === 'encode') { 72 | encode(e.data[1]) 73 | } 74 | else if (e.data[0] === 'dump') { 75 | dump(e.data[1]) 76 | } 77 | else if (e.data[0] === 'close') { 78 | this.close() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/shared/encoder-mp3-worker.js: -------------------------------------------------------------------------------- 1 | // import 'babel-polyfill' 2 | // import lamejs from 'lamejs' 3 | 4 | export default function () { 5 | // Note that relative paths don't work when loaded as a blob 6 | // https://stackoverflow.com/questions/22172426/using-importsscripts-within-blob-in-a-karma-environment 7 | // importScripts('https://localhost:8443/workers/encoders/lame.js') // eslint-disable-line 8 | 9 | let channels = 1 // 1 for mono or 2 for stereo 10 | // let sampleRate = 44100 // 44.1khz (normal mp3 samplerate) 11 | let kbps = 128 // encode 128kbps mp3 12 | 13 | let mp3encoder = null 14 | 15 | const maxSamples = 1152 16 | 17 | var mp3Data = [] // array of Uint8Array 18 | 19 | function init (opts) { 20 | /* global lamejs */ 21 | importScripts(opts.baseUrl + '/workers/encoders/lame.min.js'); // eslint-disable-line 22 | mp3encoder = new lamejs.Mp3Encoder(channels, opts.sampleRate, kbps) 23 | } 24 | 25 | function floatTo16BitPCM (input, output) { 26 | for (var i = 0; i < input.length; i++) { 27 | var s = Math.max(-1, Math.min(1, input[i])) 28 | output[i] = (s < 0 ? s * 0x8000 : s * 0x7FFF) 29 | } 30 | } 31 | 32 | function convertBuffer (arrayBuffer) { 33 | var data = new Float32Array(arrayBuffer) 34 | var out = new Int16Array(arrayBuffer.length) 35 | floatTo16BitPCM(data, out) 36 | return out 37 | } 38 | 39 | function encode (arrayBuffer) { 40 | let samplesMono = convertBuffer(arrayBuffer) 41 | let remaining = samplesMono.length 42 | for (let i = 0; remaining >= 0; i += maxSamples) { 43 | var left = samplesMono.subarray(i, i + maxSamples) 44 | var data = mp3encoder.encodeBuffer(left) 45 | mp3Data.push(data) 46 | remaining -= maxSamples 47 | } 48 | 49 | // var mp3buf = mp3encoder.encodeBuffer(buffer) 50 | // if (mp3buf.length > 0) { 51 | // mp3Data.push(mp3buf) 52 | // } 53 | } 54 | 55 | function dump () { 56 | var mp3buf = mp3encoder.flush() 57 | if (mp3buf.length > 0) { 58 | mp3Data.push(mp3buf) 59 | } 60 | 61 | // Probably results in native memory copy 62 | postMessage(mp3Data) 63 | 64 | // Would like to do this, but not possible because mp3Data is generic array of Uint8Array, and generic 65 | // arrays are not transferrable types. 66 | // postMessage(mp3Data, [mp3Data]) 67 | 68 | // This might help if/when ever become available again 69 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer 70 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer 71 | 72 | // For now, though, we have not other option except to build a complete copy in javascript. This means 73 | // we temporarily require twice the memory of whatever was recorded. 74 | 75 | mp3Data = [] 76 | } 77 | 78 | onmessage = function (e) { 79 | if (e.data[0] === 'encode') { 80 | encode(e.data[1]) 81 | } 82 | else if (e.data[0] === 'dump') { 83 | dump(e.data[1]) 84 | } 85 | else if (e.data[0] === 'init') { 86 | init(e.data[1]) 87 | } 88 | else if (e.data[0] === 'close') { 89 | self.close() 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | var GitRevisionPlugin = require('git-revision-webpack-plugin') 2 | 3 | let gitRevisionPlugin = new GitRevisionPlugin() 4 | 5 | module.exports = { 6 | lintOnSave: true, 7 | publicPath: '', 8 | devServer: { 9 | host: "0.0.0.0", 10 | https: true, 11 | disableHostCheck: true, 12 | port: 8443, 13 | public: '10.9.22.176:8443', 14 | }, 15 | configureWebpack: config => { 16 | // config.plugins.push(new DefinePlugin({ 17 | // 'VERSION': JSON.stringify(gitRevisionPlugin.version()), 18 | // 'COMMITHASH': JSON.stringify(gitRevisionPlugin.commithash()), 19 | // 'BRANCH': JSON.stringify(gitRevisionPlugin.branch()), 20 | // })) 21 | }, 22 | // configureWebpack: { 23 | // entry: { 24 | // polyfillstest: './src/polyfill-mediarecorder.js' 25 | // } 26 | // }, 27 | // tweak internal webpack configuration. 28 | // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md 29 | chainWebpack: config => { 30 | config.plugin('define').tap(options => { 31 | Object.assign(options[0], { 32 | 'WEBPACK_VERSION': JSON.stringify(gitRevisionPlugin.version()), 33 | 'WEBPACK_COMMITHASH': JSON.stringify(gitRevisionPlugin.commithash()), 34 | 'WEBPACK_BRANCH': JSON.stringify(gitRevisionPlugin.branch()), 35 | 'WEBPACK_TIMESTAMP': JSON.stringify(new Date()) 36 | }) 37 | 38 | // There has got to be a better way... 39 | // Currently this is required for loading the webworkers which have different rules and restrictions. 40 | if (process.env.NODE_ENV === 'production') { 41 | Object.assign(options[0], { 42 | 'BASE_URL': JSON.stringify('https://kaliatech.github.io/web-audio-recording-tests/dist'), 43 | }) 44 | } 45 | else { 46 | Object.assign(options[0], { 47 | //'BASE_URL': JSON.stringify('https://localhost:8443'), 48 | 'BASE_URL': JSON.stringify('https://10.9.22.176:8443') 49 | }) 50 | } 51 | return options 52 | }) 53 | //# worker-loaded webified the workers and allowed use of ES6 imports inside the workers. However, that workers were 54 | // never reloaded from cache. Webpack service had to be stopped, and cache directory deleted, before getting changes. 55 | // Also, the encoders are large, so probably not good to load in to the main bundle anyways. 56 | // config.module 57 | // .rule('worker') 58 | // .test(/-worker\.js$/) 59 | // .use('worker-loader') 60 | // .loader('worker-loader') 61 | // .options({inline: true}) 62 | 63 | 64 | // config.loaders.push(new ) 65 | 66 | // config.entryPoints.delete('app') 67 | // config 68 | // .entry('polyfill-mediarecorder') 69 | // .add('./src/polyfill-mediarecorder.js') 70 | // .end() 71 | // .entry('app') 72 | // .add('./src/main.js') 73 | // .end() 74 | 75 | // config.plugin('html') 76 | // .tap(([options]) => [Object.assign(options, { 77 | // excludeChunks: ['polyfill-mediarecorder'] 78 | // })]) 79 | 80 | // // A, remove the plugin 81 | // config.plugins.delete('prefetch'); 82 | 83 | // or: 84 | // B. Alter settings: 85 | // config.plugin('prefetch').tap(options => { 86 | // options.fileBlackList.push([polyfill(.)+?\.js$/]); 87 | // return options; 88 | // }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 132 | 133 | 136 | -------------------------------------------------------------------------------- /src/views/Test2.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 189 | 202 | -------------------------------------------------------------------------------- /src/views/Test4.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 209 | 222 | -------------------------------------------------------------------------------- /src/views/Test5.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 212 | 225 | -------------------------------------------------------------------------------- /src/shared/WebAudioPeakMeter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied from https://github.com/esonderegger/web-audio-peak-meter 3 | * Modified to class form to allow multiple instances on a page. 4 | */ 5 | export default class WebAudioPeakMeter { 6 | constructor () { 7 | this.options = { 8 | borderSize: 2, 9 | fontSize: 9, 10 | backgroundColor: 'black', 11 | tickColor: '#ddd', 12 | gradient: ['red 1%', '#ff0 16%', 'lime 45%', '#080 100%'], 13 | dbRange: 48, 14 | dbTickSize: 6, 15 | maskTransition: 'height 0.1s' 16 | } 17 | 18 | // this.tickWidth 19 | // this.elementWidth 20 | // this.elementHeight 21 | // this.meterHeight 22 | // this.meterWidth 23 | // this.meterTop 24 | this.vertical = true 25 | this.channelCount = 1 26 | this.channelMasks = [] 27 | this.channelPeaks = [] 28 | this.channelPeakLabels = [] 29 | } 30 | 31 | getBaseLog = function (x, y) { 32 | return Math.log(y) / Math.log(x) 33 | }; 34 | 35 | dbFromFloat = function (floatVal) { 36 | return this.getBaseLog(10, floatVal) * 20 37 | }; 38 | 39 | setOptions = function (userOptions) { 40 | for (var k in userOptions) { 41 | this.options[k] = userOptions[k] 42 | } 43 | this.tickWidth = this.options.fontSize * 2.0 44 | this.meterTop = this.options.fontSize * 1.5 + this.options.borderSize 45 | }; 46 | 47 | createMeterNode = function (sourceNode, audioCtx) { 48 | var c = sourceNode.channelCount 49 | var meterNode = audioCtx.createScriptProcessor(2048, c, c) 50 | sourceNode.connect(meterNode) 51 | meterNode.connect(audioCtx.destination) 52 | return meterNode 53 | }; 54 | 55 | createContainerDiv = function (parent) { 56 | var meterElement = document.createElement('div') 57 | meterElement.style.position = 'relative' 58 | meterElement.style.width = this.elementWidth + 'px' 59 | meterElement.style.height = this.elementHeight + 'px' 60 | meterElement.style.backgroundColor = this.options.backgroundColor 61 | parent.appendChild(meterElement) 62 | return meterElement 63 | }; 64 | 65 | createMeter = function (domElement, meterNode, optionsOverrides) { 66 | this.setOptions(optionsOverrides) 67 | this.elementWidth = domElement.clientWidth 68 | this.elementHeight = domElement.clientHeight 69 | var meterElement = this.createContainerDiv(domElement) 70 | if (this.elementWidth > this.elementHeight) { 71 | this.vertical = false 72 | } 73 | this.meterHeight = this.elementHeight - this.meterTop - this.options.borderSize 74 | this.meterWidth = this.elementWidth - this.tickWidth - this.options.borderSize 75 | this.createTicks(meterElement) 76 | this.createRainbow(meterElement, this.meterWidth, this.meterHeight, 77 | this.meterTop, this.tickWidth) 78 | this.channelCount = meterNode.channelCount 79 | var channelWidth = this.meterWidth / this.channelCount 80 | var channelLeft = this.tickWidth 81 | for (var i = 0; i < this.channelCount; i++) { 82 | this.createChannelMask(meterElement, this.options.borderSize, 83 | this.meterTop, channelLeft, false) 84 | this.channelMasks[i] = this.createChannelMask(meterElement, channelWidth, 85 | this.meterTop, channelLeft, 86 | this.options.maskTransition) 87 | this.channelPeaks[i] = 0.0 88 | this.channelPeakLabels[i] = this.createPeakLabel(meterElement, channelWidth, 89 | channelLeft) 90 | channelLeft += channelWidth 91 | } 92 | meterNode.onaudioprocess = (e) => this.updateMeter(e) 93 | meterElement.addEventListener('click', function () { 94 | for (var i = 0; i < this.channelCount; i++) { 95 | this.channelPeaks[i] = 0.0 96 | this.channelPeakLabels[i].textContent = '-∞' 97 | } 98 | }, false) 99 | }; 100 | 101 | createTicks = function (parent) { 102 | var numTicks = Math.floor(this.options.dbRange / this.options.dbTickSize) 103 | var dbTickLabel = 0 104 | var dbTickTop = this.options.fontSize + this.options.borderSize 105 | for (var i = 0; i < numTicks; i++) { 106 | var dbTick = document.createElement('div') 107 | parent.appendChild(dbTick) 108 | dbTick.style.width = this.tickWidth + 'px' 109 | dbTick.style.textAlign = 'right' 110 | dbTick.style.color = this.options.tickColor 111 | dbTick.style.fontSize = this.options.fontSize + 'px' 112 | dbTick.style.position = 'absolute' 113 | dbTick.style.top = dbTickTop + 'px' 114 | dbTick.textContent = dbTickLabel + '' 115 | dbTickLabel -= this.options.dbTickSize 116 | dbTickTop += this.meterHeight / numTicks 117 | } 118 | }; 119 | 120 | createRainbow = function (parent, width, height, top, left) { 121 | var rainbow = document.createElement('div') 122 | parent.appendChild(rainbow) 123 | rainbow.style.width = width + 'px' 124 | rainbow.style.height = height + 'px' 125 | rainbow.style.position = 'absolute' 126 | rainbow.style.top = top + 'px' 127 | rainbow.style.left = left + 'px' 128 | var gradientStyle = 'linear-gradient(' + this.options.gradient.join(', ') + ')' 129 | rainbow.style.backgroundImage = gradientStyle 130 | return rainbow 131 | }; 132 | 133 | createPeakLabel = function (parent, width, left) { 134 | var label = document.createElement('div') 135 | parent.appendChild(label) 136 | label.style.width = width + 'px' 137 | label.style.textAlign = 'center' 138 | label.style.color = this.options.tickColor 139 | label.style.fontSize = this.options.fontSize + 'px' 140 | label.style.position = 'absolute' 141 | label.style.top = this.options.borderSize + 'px' 142 | label.style.left = left + 'px' 143 | label.textContent = '-∞' 144 | return label 145 | }; 146 | 147 | createChannelMask = function (parent, width, top, left, transition) { 148 | var channelMask = document.createElement('div') 149 | parent.appendChild(channelMask) 150 | channelMask.style.width = width + 'px' 151 | channelMask.style.height = this.meterHeight + 'px' 152 | channelMask.style.position = 'absolute' 153 | channelMask.style.top = top + 'px' 154 | channelMask.style.left = left + 'px' 155 | channelMask.style.backgroundColor = this.options.backgroundColor 156 | if (transition) { 157 | channelMask.style.transition = this.options.maskTransition 158 | } 159 | return channelMask 160 | }; 161 | 162 | maskSize = function (floatVal) { 163 | if (floatVal === 0.0) { 164 | return this.meterHeight 165 | } 166 | else { 167 | var d = this.options.dbRange * -1 168 | var returnVal = Math.floor(this.dbFromFloat(floatVal) * this.meterHeight / d) 169 | if (returnVal > this.meterHeight) { 170 | return this.meterHeight 171 | } 172 | else { 173 | return returnVal 174 | } 175 | } 176 | }; 177 | 178 | updateMeter = function (audioProcessingEvent) { 179 | var inputBuffer = audioProcessingEvent.inputBuffer 180 | var i 181 | var channelData = [] 182 | var channelMaxes = [] 183 | for (i = 0; i < this.channelCount; i++) { 184 | channelData[i] = inputBuffer.getChannelData(i) 185 | channelMaxes[i] = 0.0 186 | } 187 | for (var sample = 0; sample < inputBuffer.length; sample++) { 188 | for (i = 0; i < this.channelCount; i++) { 189 | if (Math.abs(channelData[i][sample]) > channelMaxes[i]) { 190 | channelMaxes[i] = Math.abs(channelData[i][sample]) 191 | } 192 | } 193 | } 194 | for (i = 0; i < this.channelCount; i++) { 195 | var thisMaskSize = this.maskSize(channelMaxes[i], this.meterHeight) 196 | this.channelMasks[i].style.height = thisMaskSize + 'px' 197 | if (channelMaxes[i] > this.channelPeaks[i]) { 198 | this.channelPeaks[i] = channelMaxes[i] 199 | var labelText = this.dbFromFloat(this.channelPeaks[i]).toFixed(1) 200 | this.channelPeakLabels[i].textContent = labelText 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/views/Test3.vue: -------------------------------------------------------------------------------- 1 | 137 | 138 | 215 | 228 | -------------------------------------------------------------------------------- /src/views/Test6.vue: -------------------------------------------------------------------------------- 1 | 162 | 163 | 223 | 236 | -------------------------------------------------------------------------------- /src/views/Diagnostics.vue: -------------------------------------------------------------------------------- 1 | 142 | 143 | 310 | 311 | 326 | -------------------------------------------------------------------------------- /src/views/Test1.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 271 | 287 | -------------------------------------------------------------------------------- /src/shared/RecorderService.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019 Josh Sanderson */ 2 | 3 | import EncoderWav from './encoder-wav-worker.js' 4 | import EncoderMp3 from './encoder-mp3-worker.js' 5 | import EncoderOgg from './encoder-ogg-worker.js' 6 | 7 | export default class RecorderService { 8 | constructor (baseUrl) { 9 | this.baseUrl = baseUrl 10 | 11 | window.AudioContext = window.AudioContext || window.webkitAudioContext 12 | 13 | this.em = document.createDocumentFragment() 14 | 15 | this.state = 'inactive' 16 | 17 | this.chunks = [] 18 | this.chunkType = '' 19 | 20 | this.encoderMimeType = 'audio/wav' 21 | 22 | this.config = { 23 | broadcastAudioProcessEvents: false, 24 | createAnalyserNode: false, 25 | createDynamicsCompressorNode: false, 26 | forceScriptProcessor: false, 27 | manualEncoderId: 'wav', 28 | micGain: 1.0, 29 | processorBufferSize: 2048, 30 | stopTracksAndCloseCtxWhenFinished: true, 31 | usingMediaRecorder: typeof window.MediaRecorder !== 'undefined', 32 | enableEchoCancellation: true 33 | } 34 | } 35 | 36 | createWorker (fn) { 37 | var js = fn 38 | .toString() 39 | .replace(/^function\s*\(\)\s*{/, '') 40 | .replace(/}$/, '') 41 | var blob = new Blob([js]) 42 | return new Worker(URL.createObjectURL(blob)) 43 | } 44 | 45 | startRecording (timeslice) { 46 | if (this.state !== 'inactive') { 47 | return 48 | } 49 | 50 | // This is the case on ios/chrome, when clicking links from within ios/slack (sometimes), etc. 51 | if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { 52 | alert('Missing support for navigator.mediaDevices.getUserMedia') // temp: helps when testing for strange issues on ios/safari 53 | return 54 | } 55 | 56 | this.audioCtx = new AudioContext() 57 | this.micGainNode = this.audioCtx.createGain() 58 | this.outputGainNode = this.audioCtx.createGain() 59 | 60 | if (this.config.createDynamicsCompressorNode) { 61 | this.dynamicsCompressorNode = this.audioCtx.createDynamicsCompressor() 62 | } 63 | 64 | if (this.config.createAnalyserNode) { 65 | this.analyserNode = this.audioCtx.createAnalyser() 66 | } 67 | 68 | // If not using MediaRecorder(i.e. safari and edge), then a script processor is required. It's optional 69 | // on browsers using MediaRecorder and is only useful if wanting to do custom analysis or manipulation of 70 | // recorded audio data. 71 | if (this.config.forceScriptProcessor || this.config.broadcastAudioProcessEvents || !this.config.usingMediaRecorder) { 72 | this.processorNode = this.audioCtx.createScriptProcessor(this.config.processorBufferSize, 1, 1) // TODO: Get the number of channels from mic 73 | } 74 | 75 | // Create stream destination on chrome/firefox because, AFAICT, we have no other way of feeding audio graph output 76 | // in to MediaRecorderSafari/Edge don't have this method as of 2018-04. 77 | if (this.audioCtx.createMediaStreamDestination) { 78 | this.destinationNode = this.audioCtx.createMediaStreamDestination() 79 | } 80 | else { 81 | this.destinationNode = this.audioCtx.destination 82 | } 83 | 84 | // Create web worker for doing the encoding 85 | if (!this.config.usingMediaRecorder) { 86 | if (this.config.manualEncoderId === 'mp3') { 87 | // This also works and avoids weirdness imports with workers 88 | // this.encoderWorker = new Worker(BASE_URL + '/workers/encoder-ogg-worker.js') 89 | this.encoderWorker = this.createWorker(EncoderMp3) 90 | this.encoderWorker.postMessage(['init', { baseUrl: BASE_URL, sampleRate: this.audioCtx.sampleRate }]) 91 | this.encoderMimeType = 'audio/mpeg' 92 | } 93 | else if (this.config.manualEncoderId === 'ogg') { 94 | this.encoderWorker = this.createWorker(EncoderOgg) 95 | this.encoderWorker.postMessage(['init', { baseUrl: BASE_URL, sampleRate: this.audioCtx.sampleRate }]) 96 | this.encoderMimeType = 'audio/ogg' 97 | } 98 | else { 99 | this.encoderWorker = this.createWorker(EncoderWav) 100 | this.encoderMimeType = 'audio/wav' 101 | } 102 | this.encoderWorker.addEventListener('message', (e) => { 103 | let event = new Event('dataavailable') 104 | if (this.config.manualEncoderId === 'ogg') { 105 | event.data = e.data 106 | } 107 | else { 108 | event.data = new Blob(e.data, { type: this.encoderMimeType }) 109 | } 110 | this._onDataAvailable(event) 111 | }) 112 | } 113 | 114 | // Setup media constraints 115 | const userMediaConstraints = { 116 | audio: { 117 | echoCancellation: this.config.enableEchoCancellation 118 | } 119 | } 120 | if (this.config.deviceId) { 121 | userMediaConstraints.audio.deviceId = this.config.deviceId 122 | } 123 | 124 | // This will prompt user for permission if needed 125 | return navigator.mediaDevices.getUserMedia(userMediaConstraints) 126 | .then((stream) => { 127 | this._startRecordingWithStream(stream, timeslice) 128 | }) 129 | .catch((error) => { 130 | alert('Error with getUserMedia: ' + error.message) // temp: helps when testing for strange issues on ios/safari 131 | console.log(error) 132 | }) 133 | } 134 | 135 | setMicGain (newGain) { 136 | this.config.micGain = newGain 137 | if (this.audioCtx && this.micGainNode) { 138 | this.micGainNode.gain.setValueAtTime(newGain, this.audioCtx.currentTime) 139 | } 140 | } 141 | 142 | _startRecordingWithStream (stream, timeslice) { 143 | this.micAudioStream = stream 144 | 145 | this.inputStreamNode = this.audioCtx.createMediaStreamSource(this.micAudioStream) 146 | this.audioCtx = this.inputStreamNode.context 147 | 148 | // Kind-of a hack to allow hooking in to audioGraph inputStreamNode 149 | if (this.onGraphSetupWithInputStream) { 150 | this.onGraphSetupWithInputStream(this.inputStreamNode) 151 | } 152 | 153 | this.inputStreamNode.connect(this.micGainNode) 154 | this.micGainNode.gain.setValueAtTime(this.config.micGain, this.audioCtx.currentTime) 155 | 156 | let nextNode = this.micGainNode 157 | if (this.dynamicsCompressorNode) { 158 | this.micGainNode.connect(this.dynamicsCompressorNode) 159 | nextNode = this.dynamicsCompressorNode 160 | } 161 | 162 | this.state = 'recording' 163 | 164 | if (this.processorNode) { 165 | nextNode.connect(this.processorNode) 166 | this.processorNode.connect(this.outputGainNode) 167 | this.processorNode.onaudioprocess = (e) => this._onAudioProcess(e) 168 | } 169 | else { 170 | nextNode.connect(this.outputGainNode) 171 | } 172 | 173 | if (this.analyserNode) { 174 | // TODO: If we want the analyser node to receive the processorNode's output, this needs to be changed _and_ 175 | // processor node needs to be modified to copy input to output. It currently doesn't because it's not 176 | // needed when doing manual encoding. 177 | // this.processorNode.connect(this.analyserNode) 178 | nextNode.connect(this.analyserNode) 179 | } 180 | 181 | this.outputGainNode.connect(this.destinationNode) 182 | 183 | if (this.config.usingMediaRecorder) { 184 | this.mediaRecorder = new MediaRecorder(this.destinationNode.stream) 185 | this.mediaRecorder.addEventListener('dataavailable', (evt) => this._onDataAvailable(evt)) 186 | this.mediaRecorder.addEventListener('error', (evt) => this._onError(evt)) 187 | 188 | this.mediaRecorder.start(timeslice) 189 | } 190 | else { 191 | // Output gain to zero to prevent feedback. Seems to matter only on Edge, though seems like should matter 192 | // on iOS too. Matters on chrome when connecting graph to directly to audioCtx.destination, but we are 193 | // not able to do that when using MediaRecorder. 194 | this.outputGainNode.gain.setValueAtTime(0, this.audioCtx.currentTime) 195 | // this.outputGainNode.gain.value = 0 196 | 197 | // Todo: Note that time slicing with manual wav encoderWav won't work. To allow it would require rewriting the encoderWav 198 | // to assemble all chunks at end instead of adding header to each chunk. 199 | if (timeslice) { 200 | console.log('Time slicing without MediaRecorder is not yet supported. The resulting recording will not be playable.') 201 | this.slicing = setInterval(function () { 202 | if (this.state === 'recording') { 203 | this.encoderWorker.postMessage(['dump', this.context.sampleRate]) 204 | } 205 | }, timeslice) 206 | } 207 | } 208 | } 209 | 210 | _onAudioProcess (e) { 211 | // console.log('onaudioprocess', e) 212 | // let inputBuffer = e.inputBuffer 213 | // let outputBuffer = e.outputBuffer 214 | // console.log(this.micAudioStream) 215 | // console.log(this.audioCtx) 216 | // console.log(this.micAudioStream.getTracks().forEach((track) => console.log(track))) 217 | 218 | // this.onAudioEm.dispatch(new Event('onaudioprocess', {inputBuffer:inputBuffer,outputBuffer:outputBuffer})) 219 | 220 | if (this.config.broadcastAudioProcessEvents) { 221 | this.em.dispatchEvent(new CustomEvent('onaudioprocess', { 222 | detail: { 223 | inputBuffer: e.inputBuffer, 224 | outputBuffer: e.outputBuffer 225 | } 226 | })) 227 | } 228 | 229 | // // Example handling: 230 | // let inputBuffer = e.inputBuffer 231 | // let outputBuffer = e.outputBuffer 232 | // // Each channel (usually only one) 233 | // for (let channel = 0; channel < outputBuffer.numberOfChannels; channel++) { 234 | // let inputData = inputBuffer.getChannelData(channel) 235 | // let outputData = outputBuffer.getChannelData(channel) 236 | // 237 | // // Each sample 238 | // for (let sample = 0; sample < inputBuffer.length; sample++) { 239 | // // Make output equal to the same as the input (thus processor is doing nothing at this time) 240 | // outputData[sample] = inputData[sample] 241 | // } 242 | // } 243 | 244 | // When manually encoding (safari/edge), there's no reason to copy data to output buffer. We set the output 245 | // gain to 0 anyways (which is required on Edge if we did copy data to output). However, if using a MediaRecorder 246 | // and a processor (all other browsers), then it would be required to copy the data otherwise the graph would 247 | // generate no data for the MediaRecorder to consume. 248 | // if (this.forceScriptProcessor) { 249 | 250 | // // Copy input to output 251 | // let inputBuffer = e.inputBuffer 252 | // let outputBuffer = e.outputBuffer 253 | // // This doesn't work on iOS/Safari. Guessing it doesn't have copyToChannel support, but haven't verified. 254 | // for (let channel = 0; channel < outputBuffer.numberOfChannels; channel++) { 255 | // outputBuffer.copyToChannel(inputBuffer.getChannelData(channel), channel) 256 | // } 257 | 258 | // Safari and Edge require manual encoding via web worker. Single channel only for now. 259 | // Example stereo encoderWav: https://github.com/MicrosoftEdge/Demos/blob/master/microphone/scripts/recorderworker.js 260 | if (!this.config.usingMediaRecorder) { 261 | if (this.state === 'recording') { 262 | if (this.config.broadcastAudioProcessEvents) { 263 | this.encoderWorker.postMessage(['encode', e.outputBuffer.getChannelData(0)]) 264 | } 265 | else { 266 | this.encoderWorker.postMessage(['encode', e.inputBuffer.getChannelData(0)]) 267 | } 268 | } 269 | } 270 | } 271 | 272 | stopRecording () { 273 | if (this.state === 'inactive') { 274 | return 275 | } 276 | if (this.config.usingMediaRecorder) { 277 | this.state = 'inactive' 278 | this.mediaRecorder.stop() 279 | } 280 | else { 281 | this.state = 'inactive' 282 | this.encoderWorker.postMessage(['dump', this.audioCtx.sampleRate]) 283 | clearInterval(this.slicing) 284 | 285 | // TODO: There should be a more robust way to handle this 286 | // Without something like this, I think the last recorded sample could be lost due to timing 287 | // setTimeout(() => { 288 | // this.state = 'inactive' 289 | // this.encoderWorker.postMessage(['dump', this.audioCtx.sampleRate]) 290 | // }, 100) 291 | } 292 | } 293 | 294 | _onDataAvailable (evt) { 295 | // console.log('state', this.mediaRecorder.state) 296 | // console.log('evt.data', evt.data) 297 | 298 | this.chunks.push(evt.data) 299 | this.chunkType = evt.data.type 300 | 301 | if (this.state !== 'inactive') { 302 | return 303 | } 304 | 305 | let blob = new Blob(this.chunks, { 'type': this.chunkType }) 306 | let blobUrl = URL.createObjectURL(blob) 307 | const recording = { 308 | ts: new Date().getTime(), 309 | blobUrl: blobUrl, 310 | mimeType: blob.type, 311 | size: blob.size 312 | } 313 | 314 | this.chunks = [] 315 | this.chunkType = null 316 | 317 | if (this.destinationNode) { 318 | this.destinationNode.disconnect() 319 | this.destinationNode = null 320 | } 321 | if (this.outputGainNode) { 322 | this.outputGainNode.disconnect() 323 | this.outputGainNode = null 324 | } 325 | if (this.analyserNode) { 326 | this.analyserNode.disconnect() 327 | this.analyserNode = null 328 | } 329 | if (this.processorNode) { 330 | this.processorNode.disconnect() 331 | this.processorNode = null 332 | } 333 | if (this.encoderWorker) { 334 | this.encoderWorker.postMessage(['close']) 335 | this.encoderWorker = null 336 | } 337 | if (this.dynamicsCompressorNode) { 338 | this.dynamicsCompressorNode.disconnect() 339 | this.dynamicsCompressorNode = null 340 | } 341 | if (this.micGainNode) { 342 | this.micGainNode.disconnect() 343 | this.micGainNode = null 344 | } 345 | if (this.inputStreamNode) { 346 | this.inputStreamNode.disconnect() 347 | this.inputStreamNode = null 348 | } 349 | 350 | if (this.config.stopTracksAndCloseCtxWhenFinished) { 351 | // This removes the red bar in iOS/Safari 352 | this.micAudioStream.getTracks().forEach((track) => track.stop()) 353 | this.micAudioStream = null 354 | 355 | this.audioCtx.close() 356 | this.audioCtx = null 357 | } 358 | 359 | this.em.dispatchEvent(new CustomEvent('recording', { detail: { recording: recording } })) 360 | } 361 | 362 | _onError (evt) { 363 | console.log('error', evt) 364 | this.em.dispatchEvent(new Event('error')) 365 | alert('error:' + evt) // for debugging purposes 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /dist/js/app.a3ad6502.js: -------------------------------------------------------------------------------- 1 | (function(e){function t(t){for(var s,o,a=t[0],c=t[1],l=t[2],u=0,h=[];u0?i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Recordings")]),e._l(e.recordings,(function(t,s){return i("div",{key:t.ts},[i("v-card",[i("v-card-title",{attrs:{"primary-title":""}},[i("v-layout",{attrs:{column:"",wrap:""}},[i("div",[i("h3",[e._v("Recording #"+e._s(s+1))])]),i("div",{staticClass:"ml-3"},[i("div",[i("audio",{attrs:{src:t.blobUrl,type:t.mimeType,controls:"true"}})]),i("div",[e._v("\n size: "+e._s(e._f("fileSizeToHumanSize")(t.size))+", type: "+e._s(t.mimeType)+"\n ")])])])],1)],1),s!==e.recordings.length-1?i("v-divider"):e._e()],1)}))],2):e._e(),i("v-layout",{staticClass:"mt-5",attrs:{column:"",wrap:""}},[i("h4",[e._v("Notes\n "),i("small",[e._v("(as of iOS 14.3)")])]),i("v-divider"),i("p",[e._v("\n As of 14.3, iOS appears to have a full MediaRecorder implementation. These tests use feature detection, and now\n use MediaRecorder on iOS. I have not done extensive testing, but all of these tests seems to work on latest iOS.\n ")]),i("p",[e._v("\n This makes many of the notes below no longer relevant.\n ")]),i("h4",[e._v("Notes\n "),i("small",[e._v("(as of iOS 11.2.6)")])]),i("v-divider"),i("p",[e._v("\n There are multiple valid ways to do audio recording and playback on every browser/device combination\n "),i("i",[e._v("except")]),e._v(" iOS/Safari. I believe there are four significant issues, and only the first one seems to be well\n known.\n ")]),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("strong",[e._v("Security context of the getUserMedia handler is important")]),e._v(" - "),i("br"),i("p",[e._v("\n The most common and widely known issue issue is that the success handler of the getUserMedia is no longer\n in the context of the user click. As a result, any audio context and processors created in the handler\n won't work. The solution is to create the audio context and any processors before calling getUserMedia,\n while still in the user click handler context. Then once getUserMedia provides the stream, use the\n previously created constructs to setup the graph and start recording.\n ")])]),i("li",[i("strong",[e._v("Multiple recordings require new audio streams")]),e._v(" - "),i("br"),i("p",[e._v("\n There are a number of demos that work for the first recording on iOS/Safari, that then fail with empty\n audio on subsequent recordings until page is reloaded. The reason is that after stopping recording and\n extracting playable blob, the next recording requires a new AudioStream. This AudioStream can come from\n another call to getUserMedia (which won't prompt user after the first time), or potentially, by cloning\n the existing audio stream.\n ")])]),i("li",[i("strong",[e._v("Red recording bar indicator")]),e._v(" - "),i("br"),i("p",[e._v("\n After granting permission to microphone, iOS/Safari will show a red bar notification anytime user switches\n away from the tab where permission was granted. To clear this bar, the recording stream's tracks can be\n stopped after recording is finished. This can be tested with the checkbox above. Stopping the tracks and\n closing the audio context is straightforward and works well, except for this last issue (but see updates):\n ")])]),i("li",[i("strong",[i("del",[e._v("A sleep/lock/switch event can easily break things, is not detectable, and is not easily recoverable\n ")])]),e._v(" - "),i("br"),i("del",[i("p",[e._v("\n To see this, make a recording and verify it plays. Switch to mail app, then back to safari and\n make/verify another recording. As long as red bar/microphone is still visible, it generally works. Then,\n check the option to stop tracks and close audio context. Make another recording and verify. Switch to\n mail app and back and try to make another recording. Most of the time the recording will appear to work,\n but the audio will be silent. As far as I can tell, there is no way to detect this, and there is no way\n to recover without loading a new tab or force quitting Safari.\n ")]),i("p",[i("em",[e._v("If")]),e._v(" the tracks are not stopped and so the red bar/icon remains, then this occurs much less\n frequently. Of course, that means the red bar is constantly visible though. And even then, starting\n another app that uses the microphone will almost always break things again. I have not been able to find\n a way to detect, much less, programmatically fix things, when this occurs. My assumption is that the\n underlying issue is due to low level iOS/Safari bugs, and not in how this code is setting things up.")])])]),i("li",[i("strong",[e._v("Any references not cleaned up after recording complete can affect stability")]),e._v(" - "),i("br"),i("p",[e._v("UPDATED 2018-04-10: After writing the previous section, I rewrote a number of things and removed all\n dependencies. By doing that I was able to ensure that everything gets cleaned up when recording is\n complete. I now no longer have stability issues on iOS, though I don't know exactly what was causing it\n previously. I thought perhaps it was the web worker, which I now close when recording is complete. But\n quick testing shows that even if I don't close the webworker, this new handling seems to be stable after\n sleep/lock/switch events.")])]),i("li",[i("strong",[e._v("Mobile-web-app-capable meta might be causing issues")]),e._v(" - "),i("br"),i("p",[e._v('\n I haven\'t vetted this 100%, but before 11.3 release I thought the "Add to Home Screen" and launching as a\n full screen web app using apple-mobile-web-app-capable tag worked fine. With 11.3 it no longer works. It\n appears as if navigator.mediaDevices is missing completely when launched in this context. Similarly,\n clicking links from apps like slack appears to launch in to a context without mediaDevices. Possibly\n related\n '),i("a",{attrs:{href:"https://stackoverflow.com/questions/46228218/how-to-access-camera-on-ios11-home-screen-web-app/46350136"}},[e._v("\n link on StackOverflow\n ")]),e._v("\n .\n ")])])])])],1),i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Source")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/views/Test1.vue"}},[e._v("\n src/views/Test1.vue\n ")]),i("ul",{staticClass:"ml-3"},[i("li",[e._v("Primarily:\n "),i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/shared/RecorderService.js"}},[e._v("\n src/shared/RecorderService.js\n ")])])])])])])],1),i("v-layout",{attrs:{column:"",wrap:"","hidden-xs-only":""}},[i("h4",{staticClass:"mt-3"},[e._v("Relevant")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/muaz-khan/RecordRTC/issues/324"}},[e._v("\n https://github.com/muaz-khan/RecordRTC/issues/324\n ")])]),i("li",[i("a",{attrs:{href:"https://github.com/ai/audio-recorder-polyfill/issues/4"}},[e._v("\n https://github.com/ai/audio-recorder-polyfill/issues/4\n ")])]),i("li",[i("a",{attrs:{href:"https://github.com/danielstorey/webrtc-audio-recording"}},[e._v("\n https://github.com/danielstorey/webrtc-audio-recording\n ")])])])])],1)],1)},l=[],d=(i("ac6a"),i("6b54"),i("a481"),i("d225")),u=i("b0b4"),h=(i("34ef"),function(){var e=2,t=[];function i(i){for(var s=i.length,r=new Uint8Array(s*e),n=0;n1?a=1:a<-1&&(a=-1),a*=32768,r[o]=a,r[o+1]=a>>8}t.push(r)}function s(i){var s=t.length?t[0].length:0,r=t.length*s,n=new Uint8Array(44+r),o=new DataView(n.buffer);o.setUint32(0,1380533830,!1),o.setUint32(4,36+r,!0),o.setUint32(8,1463899717,!1),o.setUint32(12,1718449184,!1),o.setUint32(16,16,!0),o.setUint16(20,1,!0),o.setUint16(22,1,!0),o.setUint32(24,i,!0),o.setUint32(28,i*e,!0),o.setUint16(32,e,!0),o.setUint16(34,8*e,!0),o.setUint32(36,1684108385,!1),o.setUint32(40,r,!0);for(var a=0;a=0;o+=s){var c=t.subarray(o,o+s),l=i.encodeBuffer(c);r.push(l),n-=s}}function l(){var e=i.flush();e.length>0&&r.push(e),postMessage(r),r=[]}onmessage=function(e){"encode"===e.data[0]?c(e.data[1]):"dump"===e.data[0]?l(e.data[1]):"init"===e.data[0]?n(e.data[1]):"close"===e.data[0]&&self.close()}}),m=function(){var e=1,t=.4,i=null,s=[];function r(s){importScripts(s.baseUrl+"/workers/encoders/OggVorbisEncoder.js"),i=new OggVorbisEncoder(s.sampleRate,e,t)}function n(e){var t=i.encode([e]);s.push(t)}function o(){var e=i.finish("audio/ogg");postMessage(e),s=[]}onmessage=function(e){"encode"===e.data[0]?n(e.data[1]):"dump"===e.data[0]?o(e.data[1]):"init"===e.data[0]?r(e.data[1]):"close"===e.data[0]&&self.close()}},p=function(){function e(t){Object(d["a"])(this,e),this.baseUrl=t,window.AudioContext=window.AudioContext||window.webkitAudioContext,this.em=document.createDocumentFragment(),this.state="inactive",this.chunks=[],this.chunkType="",this.encoderMimeType="audio/wav",this.config={broadcastAudioProcessEvents:!1,createAnalyserNode:!1,createDynamicsCompressorNode:!1,forceScriptProcessor:!1,manualEncoderId:"wav",micGain:1,processorBufferSize:2048,stopTracksAndCloseCtxWhenFinished:!0,usingMediaRecorder:"undefined"!==typeof window.MediaRecorder,enableEchoCancellation:!0}}return Object(u["a"])(e,[{key:"createWorker",value:function(e){var t=e.toString().replace(/^function\s*\(\)\s*{/,"").replace(/}$/,""),i=new Blob([t]);return new Worker(URL.createObjectURL(i))}},{key:"startRecording",value:function(e){var t=this;if("inactive"===this.state){if(navigator&&navigator.mediaDevices&&navigator.mediaDevices.getUserMedia){this.audioCtx=new AudioContext,this.micGainNode=this.audioCtx.createGain(),this.outputGainNode=this.audioCtx.createGain(),this.config.createDynamicsCompressorNode&&(this.dynamicsCompressorNode=this.audioCtx.createDynamicsCompressor()),this.config.createAnalyserNode&&(this.analyserNode=this.audioCtx.createAnalyser()),(this.config.forceScriptProcessor||this.config.broadcastAudioProcessEvents||!this.config.usingMediaRecorder)&&(this.processorNode=this.audioCtx.createScriptProcessor(this.config.processorBufferSize,1,1)),this.audioCtx.createMediaStreamDestination?this.destinationNode=this.audioCtx.createMediaStreamDestination():this.destinationNode=this.audioCtx.destination,this.config.usingMediaRecorder||("mp3"===this.config.manualEncoderId?(this.encoderWorker=this.createWorker(v),this.encoderWorker.postMessage(["init",{baseUrl:"https://kaliatech.github.io/web-audio-recording-tests/dist",sampleRate:this.audioCtx.sampleRate}]),this.encoderMimeType="audio/mpeg"):"ogg"===this.config.manualEncoderId?(this.encoderWorker=this.createWorker(m),this.encoderWorker.postMessage(["init",{baseUrl:"https://kaliatech.github.io/web-audio-recording-tests/dist",sampleRate:this.audioCtx.sampleRate}]),this.encoderMimeType="audio/ogg"):(this.encoderWorker=this.createWorker(h),this.encoderMimeType="audio/wav"),this.encoderWorker.addEventListener("message",(function(e){var i=new Event("dataavailable");"ogg"===t.config.manualEncoderId?i.data=e.data:i.data=new Blob(e.data,{type:t.encoderMimeType}),t._onDataAvailable(i)})));var i={audio:{echoCancellation:this.config.enableEchoCancellation}};return this.config.deviceId&&(i.audio.deviceId=this.config.deviceId),navigator.mediaDevices.getUserMedia(i).then((function(i){t._startRecordingWithStream(i,e)})).catch((function(e){alert("Error with getUserMedia: "+e.message),console.log(e)}))}alert("Missing support for navigator.mediaDevices.getUserMedia")}}},{key:"setMicGain",value:function(e){this.config.micGain=e,this.audioCtx&&this.micGainNode&&this.micGainNode.gain.setValueAtTime(e,this.audioCtx.currentTime)}},{key:"_startRecordingWithStream",value:function(e,t){var i=this;this.micAudioStream=e,this.inputStreamNode=this.audioCtx.createMediaStreamSource(this.micAudioStream),this.audioCtx=this.inputStreamNode.context,this.onGraphSetupWithInputStream&&this.onGraphSetupWithInputStream(this.inputStreamNode),this.inputStreamNode.connect(this.micGainNode),this.micGainNode.gain.setValueAtTime(this.config.micGain,this.audioCtx.currentTime);var s=this.micGainNode;this.dynamicsCompressorNode&&(this.micGainNode.connect(this.dynamicsCompressorNode),s=this.dynamicsCompressorNode),this.state="recording",this.processorNode?(s.connect(this.processorNode),this.processorNode.connect(this.outputGainNode),this.processorNode.onaudioprocess=function(e){return i._onAudioProcess(e)}):s.connect(this.outputGainNode),this.analyserNode&&s.connect(this.analyserNode),this.outputGainNode.connect(this.destinationNode),this.config.usingMediaRecorder?(this.mediaRecorder=new MediaRecorder(this.destinationNode.stream),this.mediaRecorder.addEventListener("dataavailable",(function(e){return i._onDataAvailable(e)})),this.mediaRecorder.addEventListener("error",(function(e){return i._onError(e)})),this.mediaRecorder.start(t)):(this.outputGainNode.gain.setValueAtTime(0,this.audioCtx.currentTime),t&&(console.log("Time slicing without MediaRecorder is not yet supported. The resulting recording will not be playable."),this.slicing=setInterval((function(){"recording"===this.state&&this.encoderWorker.postMessage(["dump",this.context.sampleRate])}),t)))}},{key:"_onAudioProcess",value:function(e){this.config.broadcastAudioProcessEvents&&this.em.dispatchEvent(new CustomEvent("onaudioprocess",{detail:{inputBuffer:e.inputBuffer,outputBuffer:e.outputBuffer}})),this.config.usingMediaRecorder||"recording"===this.state&&(this.config.broadcastAudioProcessEvents?this.encoderWorker.postMessage(["encode",e.outputBuffer.getChannelData(0)]):this.encoderWorker.postMessage(["encode",e.inputBuffer.getChannelData(0)]))}},{key:"stopRecording",value:function(){"inactive"!==this.state&&(this.config.usingMediaRecorder?(this.state="inactive",this.mediaRecorder.stop()):(this.state="inactive",this.encoderWorker.postMessage(["dump",this.audioCtx.sampleRate]),clearInterval(this.slicing)))}},{key:"_onDataAvailable",value:function(e){if(this.chunks.push(e.data),this.chunkType=e.data.type,"inactive"===this.state){var t=new Blob(this.chunks,{type:this.chunkType}),i=URL.createObjectURL(t),s={ts:(new Date).getTime(),blobUrl:i,mimeType:t.type,size:t.size};this.chunks=[],this.chunkType=null,this.destinationNode&&(this.destinationNode.disconnect(),this.destinationNode=null),this.outputGainNode&&(this.outputGainNode.disconnect(),this.outputGainNode=null),this.analyserNode&&(this.analyserNode.disconnect(),this.analyserNode=null),this.processorNode&&(this.processorNode.disconnect(),this.processorNode=null),this.encoderWorker&&(this.encoderWorker.postMessage(["close"]),this.encoderWorker=null),this.dynamicsCompressorNode&&(this.dynamicsCompressorNode.disconnect(),this.dynamicsCompressorNode=null),this.micGainNode&&(this.micGainNode.disconnect(),this.micGainNode=null),this.inputStreamNode&&(this.inputStreamNode.disconnect(),this.inputStreamNode=null),this.config.stopTracksAndCloseCtxWhenFinished&&(this.micAudioStream.getTracks().forEach((function(e){return e.stop()})),this.micAudioStream=null,this.audioCtx.close(),this.audioCtx=null),this.em.dispatchEvent(new CustomEvent("recording",{detail:{recording:s}}))}}},{key:"_onError",value:function(e){console.log("error",e),this.em.dispatchEvent(new Event("error")),alert("error:"+e)}}]),e}(),g=(i("4917"),function(){function e(){Object(d["a"])(this,e)}return Object(u["a"])(e,[{key:"humanFileSize",value:function(e,t){var i=t?1e3:1024;if(Math.abs(e)=i&&r0?i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Recordings")]),e._l(e.recordings,(function(t,s){return i("div",{key:t.ts},[i("v-card",[i("v-card-title",{attrs:{"primary-title":""}},[i("v-layout",{attrs:{column:"",wrap:""}},[i("div",[i("h3",[e._v("Recording #"+e._s(s+1))])]),i("div",{staticClass:"ml-3"},[i("div",[i("audio",{attrs:{src:t.blobUrl,controls:"true"}})]),i("div",[e._v("\n size: "+e._s(e._f("fileSizeToHumanSize")(t.size))+", type: "+e._s(t.mimeType)+"\n ")])])])],1)],1),s!==e.recordings.length-1?i("v-divider"):e._e()],1)}))],2):e._e(),i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Source")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/views/Test2.vue"}},[e._v("\n src/views/Test2.vue\n ")]),i("ul",{staticClass:"ml-3"},[i("li",[e._v("Primarily:\n "),i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/shared/RecorderService.js"}},[e._v("\n src/shared/RecorderService.js\n ")])])])])])])],1),i("v-layout",{attrs:{column:"",wrap:"","hidden-xs-only":""}},[i("h4",{staticClass:"mt-3"},[e._v("Relevant")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/muaz-khan/RecordRTC/issues/324"}},[e._v("\n https://github.com/muaz-khan/RecordRTC/issues/324\n ")])]),i("li",[i("a",{attrs:{href:"https://github.com/ai/audio-recorder-polyfill/issues/4"}},[e._v("\n https://github.com/ai/audio-recorder-polyfill/issues/4\n ")])]),i("li",[i("a",{attrs:{href:"https://github.com/danielstorey/webrtc-audio-recording"}},[e._v("\n https://github.com/danielstorey/webrtc-audio-recording\n ")])]),i("li",[i("a",{attrs:{href:"https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/microphone/"}},[e._v("\n https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/microphone/\n ")])]),i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/MicrosoftEdge/Demos/blob/master/microphone"}},[e._v("\n https://github.com/MicrosoftEdge/Demos/blob/master/microphone\n ")])])])])])],1)],1)},B=[],F={name:"Test1",filters:{fileSizeToHumanSize:function(e){return f.humanFileSize(e,!0)}},data:function(){return{recordingInProgress:!1,supportedMimeTypes:[],recordings:[],cleanupWhenFinished:!0,addNoise:!1,numAudioSamples:0}},created:function(){var e=this;this.recorderSrvc=new p,this.recorderSrvc.em.addEventListener("recording",(function(t){return e.onNewRecording(t)})),this.recorderSrvc.em.addEventListener("onaudioprocess",(function(t){return e.onAudioProcess(t)})),this.recorderSrvc.config.broadcastAudioProcessEvents=!0},mounted:function(){},methods:{startRecording:function(){var e=this;this.numAudioSamples=0,this.recorderSrvc.startRecording().then((function(){e.recordingInProgress=!0})).catch((function(e){console.error("Exception while start recording: "+e),alert("Exception while start recording: "+e.message)}))},stopRecording:function(){this.recorderSrvc.stopRecording(),this.recordingInProgress=!1},onAudioProcess:function(e){this.numAudioSamples++;for(var t=e.detail.inputBuffer,i=e.detail.outputBuffer,s=0;s0?i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Recordings")]),i("v-divider"),e._l(e.recordings,(function(t,s){return i("div",{key:t.ts},[i("v-card",[i("v-card-title",{attrs:{"primary-title":""}},[i("v-layout",{attrs:{column:"",wrap:""}},[i("div",[i("h3",[e._v("Recording #"+e._s(s+1))])]),i("div",{staticClass:"ml-3"},[i("div",[i("audio",{attrs:{src:t.blobUrl,controls:"true"}})]),i("div",[e._v("\n size: "+e._s(e._f("fileSizeToHumanSize")(t.size))+", type: "+e._s(t.mimeType)+"\n ")])])])],1)],1),s!==e.recordings.length-1?i("v-divider"):e._e()],1)}))],2):e._e(),i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Source")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/views/Test3.vue"}},[e._v("\n src/views/Test3.vue\n ")]),i("ul",{staticClass:"ml-3"},[i("li",[e._v("Primarily:\n "),i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/shared/RecorderService.js"}},[e._v("\n src/shared/RecorderService.js\n ")])])])])])])],1),i("v-layout",{attrs:{column:"",wrap:"","hidden-xs-only":""}},[i("h4",{staticClass:"mt-3"},[e._v("Relevant")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/muaz-khan/RecordRTC/issues/324"}},[e._v("\n https://github.com/muaz-khan/RecordRTC/issues/324\n ")])]),i("li",[i("a",{attrs:{href:"https://github.com/ai/audio-recorder-polyfill/issues/4"}},[e._v("\n https://github.com/ai/audio-recorder-polyfill/issues/4\n ")])]),i("li",[i("a",{attrs:{href:"https://github.com/danielstorey/webrtc-audio-recording"}},[e._v("\n https://github.com/danielstorey/webrtc-audio-recording\n ")])]),i("li",[i("a",{attrs:{href:"https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/microphone/"}},[e._v("\n https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/microphone/\n ")])]),i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/MicrosoftEdge/Demos/blob/master/microphone"}},[e._v("\n https://github.com/MicrosoftEdge/Demos/blob/master/microphone\n ")])])]),i("li",[i("a",{attrs:{href:"https://github.com/esonderegger/web-audio-peak-meter"}},[e._v("\n https://github.com/esonderegger/web-audio-peak-meter\n ")])])])])],1)],1)},V=[],Z=i("bd86"),J=function e(){Object(d["a"])(this,e),Object(Z["a"])(this,"getBaseLog",(function(e,t){return Math.log(t)/Math.log(e)})),Object(Z["a"])(this,"dbFromFloat",(function(e){return 20*this.getBaseLog(10,e)})),Object(Z["a"])(this,"setOptions",(function(e){for(var t in e)this.options[t]=e[t];this.tickWidth=2*this.options.fontSize,this.meterTop=1.5*this.options.fontSize+this.options.borderSize})),Object(Z["a"])(this,"createMeterNode",(function(e,t){var i=e.channelCount,s=t.createScriptProcessor(2048,i,i);return e.connect(s),s.connect(t.destination),s})),Object(Z["a"])(this,"createContainerDiv",(function(e){var t=document.createElement("div");return t.style.position="relative",t.style.width=this.elementWidth+"px",t.style.height=this.elementHeight+"px",t.style.backgroundColor=this.options.backgroundColor,e.appendChild(t),t})),Object(Z["a"])(this,"createMeter",(function(e,t,i){var s=this;this.setOptions(i),this.elementWidth=e.clientWidth,this.elementHeight=e.clientHeight;var r=this.createContainerDiv(e);this.elementWidth>this.elementHeight&&(this.vertical=!1),this.meterHeight=this.elementHeight-this.meterTop-this.options.borderSize,this.meterWidth=this.elementWidth-this.tickWidth-this.options.borderSize,this.createTicks(r),this.createRainbow(r,this.meterWidth,this.meterHeight,this.meterTop,this.tickWidth),this.channelCount=t.channelCount;for(var n=this.meterWidth/this.channelCount,o=this.tickWidth,a=0;athis.meterHeight?this.meterHeight:i})),Object(Z["a"])(this,"updateMeter",(function(e){var t,i=e.inputBuffer,s=[],r=[];for(t=0;tr[t]&&(r[t]=Math.abs(s[t][n]));for(t=0;tthis.channelPeaks[t]){this.channelPeaks[t]=r[t];var a=this.dbFromFloat(this.channelPeaks[t]).toFixed(1);this.channelPeakLabels[t].textContent=a}}})),this.options={borderSize:2,fontSize:9,backgroundColor:"black",tickColor:"#ddd",gradient:["red 1%","#ff0 16%","lime 45%","#080 100%"],dbRange:48,dbTickSize:6,maskTransition:"height 0.1s"},this.vertical=!0,this.channelCount=1,this.channelMasks=[],this.channelPeaks=[],this.channelPeakLabels=[]},K=new J,Y=new J,Q={name:"Test3",filters:{fileSizeToHumanSize:function(e){return f.humanFileSize(e,!0)}},data:function(){return{micGainSlider:100,micGain:1,recordingInProgress:!1,supportedMimeTypes:[],recordings:[],cleanupWhenFinished:!0}},watch:{micGainSlider:function(){this.micGain=(.01*this.micGainSlider).toFixed(2),this.recorderSrvc.setMicGain(this.micGain)}},created:function(){var e=this;this.recorderSrvc=new p,this.recorderSrvc.em.addEventListener("recording",(function(t){return e.onNewRecording(t)})),this.recorderSrvc.onGraphSetupWithInputStream=function(t){e.meterNodeRaw=K.createMeterNode(t,e.recorderSrvc.audioCtx),K.createMeter(e.peakMeterRawEl,e.meterNodeRaw,{})}},mounted:function(){this.peakMeterRawEl=document.getElementById("peak-meter-raw"),this.peakMeterPostGainEl=document.getElementById("peak-meter-postgain")},methods:{startRecording:function(){var e=this;this.recorderSrvc.startRecording().then((function(){e.recordingInProgress=!0,e.meterNodePostGain=Y.createMeterNode(e.recorderSrvc.micGainNode,e.recorderSrvc.audioCtx),Y.createMeter(e.peakMeterPostGainEl,e.meterNodePostGain,{})})).catch((function(e){console.error("Exception while start recording: "+e),alert("Exception while start recording: "+e.message)}))},stopRecording:function(){this.recorderSrvc.stopRecording(),this.recordingInProgress=!1,this.meterNodeRaw.disconnect(),this.meterNodeRaw=null,this.peakMeterRawEl.innerHTML="",this.meterNodePostGain.disconnect(),this.meterNodePostGain=null,this.peakMeterPostGainEl.innerHTML=""},onNewRecording:function(e){this.recordings.push(e.detail.recording)}}},X=Q,ee=(i("700e"),Object(_["a"])(X,$,V,!1,null,"3e85acbd",null)),te=ee.exports,ie=function(){var e=this,t=e.$createElement,i=e._self._c||t;return i("v-container",{attrs:{"mb-5":""}},[i("v-layout",{attrs:{row:"",wrap:""}},[i("div",{staticClass:"test2"},[i("h3",[e._v("Test 4 "),e.$vuetify.breakpoint.xsOnly?i("span",[i("br")]):e._e(),e.$vuetify.breakpoint.xsOnly?e._e():i("span",[e._v(" - ")]),e._v(" Microphone Selection\n ")]),i("p",[e._v("Test microphone input selection. See\n "),i("router-link",{attrs:{to:"/diagnostics"}},[e._v("diagnostics")]),e._v("\n for full enumeration and capabilities. Showing microphone labels first requires permissions to an input\n device. Click eye icon to show microphone labels.\n ")],1)])]),i("v-layout",{staticClass:"ml-1 mt-1",attrs:{row:"",wrap:""}},[i("v-flex",{attrs:{xs9:"",md6:""}},[i("v-select",{attrs:{items:e.availableDevices,label:"Select Mic","single-line":"","item-text":"name","item-value":"device.deviceId"},model:{value:e.selectedDeviceId,callback:function(t){e.selectedDeviceId=t},expression:"selectedDeviceId"}})],1),i("v-flex",{attrs:{xs1:""}},[i("div",{staticClass:"input-group"},[i("v-btn",{attrs:{color:"primary",flat:"",small:""},on:{click:e.enumerateDevicesWithPermission}},[i("v-icon",[e._v("remove_red_eye")])],1)],1)])],1),i("v-layout",{staticClass:"ml-1 mt-1",attrs:{row:"",wrap:""}},[i("div",[i("v-btn",{attrs:{disabled:e.recordingInProgress},on:{click:e.startRecording}},[e._v("Start Recording\n ")]),i("v-btn",{attrs:{disabled:!e.recordingInProgress},on:{click:e.stopRecording}},[e._v("Stop Recording")]),i("v-icon",{class:e.recordingInProgress?"live":""},[e._v("mic")])],1)]),e.recordings.length>0?i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Recordings")]),i("v-divider"),e._l(e.recordings,(function(t,s){return i("div",{key:t.ts},[i("v-card",[i("v-card-title",{attrs:{"primary-title":""}},[i("v-layout",{attrs:{column:"",wrap:""}},[i("div",[i("h3",[e._v("Recording #"+e._s(s+1))])]),i("div",{staticClass:"ml-3"},[i("div",[i("audio",{attrs:{src:t.blobUrl,controls:"true"}})]),i("div",[e._v("\n size: "+e._s(e._f("fileSizeToHumanSize")(t.size))+", type: "+e._s(t.mimeType)+"\n ")])])])],1)],1),s!==e.recordings.length-1?i("v-divider"):e._e()],1)}))],2):e._e(),i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Source")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/views/Test4.vue"}},[e._v("\n src/views/Test4.vue\n ")]),i("ul",{staticClass:"ml-3"},[i("li",[e._v("Primarily:\n "),i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/shared/RecorderService.js"}},[e._v("\n src/shared/RecorderService.js\n ")])])])])])])],1),i("v-layout",{attrs:{column:"",wrap:"","hidden-xs-only":""}},[i("h4",{staticClass:"mt-3"},[e._v("Relevant")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://webrtc.github.io/samples/src/content/devices/input-output/"}},[e._v("\n https://webrtc.github.io/samples/src/content/devices/input-output/\n ")])])])])],1)],1)},se=[],re={name:"Test3",filters:{fileSizeToHumanSize:function(e){return f.humanFileSize(e,!0)}},data:function(){return{availableDevices:[],enumeratedDevices:[],selectedDeviceId:null,recordingInProgress:!1,recordings:[],microphones:[]}},watch:{},created:function(){var e=this;this.recorderSrvc=new p,this.recorderSrvc.em.addEventListener("recording",(function(t){return e.onNewRecording(t)})),this.enumerateDevices()},mounted:function(){},methods:{startRecording:function(){var e=this;this.recorderSrvc.config.deviceId=this.selectedDeviceId,this.recorderSrvc.startRecording().then((function(){e.recordingInProgress=!0})).catch((function(e){console.error("Exception while start recording: "+e),alert("Exception while start recording: "+e.message)}))},stopRecording:function(){this.recorderSrvc.stopRecording(),this.recordingInProgress=!1},setupAvailDeviceNames:function(){var e=this,t=[];this.enumeratedDevices.forEach((function(i){"audioinput"===i.kind&&(i.label?t.push({name:i.label,device:i}):(e.enumeratedDevicesPermissionNeeded=!0,t.push({name:"Audio Input "+(t.length+1),device:i})))})),this.availableDevices=t},enumerateDevices:function(){var e=this;navigator.mediaDevices.enumerateDevices().then((function(t){e.enumeratedDevices=t,e.setupAvailDeviceNames(),e.availableDevices&&e.availableDevices.length>1&&e.availableDevices[0].device&&(e.selectedDeviceId=e.availableDevices[0].device.deviceId),e.stream&&(e.stream.getTracks().forEach((function(e){return e.stop()})),e.stream=null)})).catch((function(t){console.log(t),e.enumeratedDevices.push({id:"-",kind:"error",deviceId:"error"})}))},enumerateDevicesWithPermission:function(){var e=this;navigator.mediaDevices.getUserMedia({audio:!0,deviceId:"default"}).then((function(t){e.enumerateDevices(),e.stream=t})).catch((function(e){console.log(e)}))},onNewRecording:function(e){this.recordings.push(e.detail.recording)}}},ne=re,oe=(i("cae3"),Object(_["a"])(ne,ie,se,!1,null,"271d3caa",null)),ae=oe.exports,ce=function(){var e=this,t=e.$createElement,i=e._self._c||t;return i("v-container",{attrs:{"mb-5":""}},[i("v-layout",{attrs:{row:"",wrap:""}},[i("div",{staticClass:"test2"},[i("h3",[e._v("Test 5 "),e.$vuetify.breakpoint.xsOnly?i("span",[i("br")]):e._e(),e.$vuetify.breakpoint.xsOnly?e._e():i("span",[e._v(" - ")]),e._v(" Analyzer Node\n ")]),i("p",[e._v("Test using analyzer node.\n ")])])]),i("v-layout",{staticClass:"ml-1 mt-1",attrs:{row:"",wrap:""}},[i("div",[i("v-btn",{attrs:{disabled:e.recordingInProgress},on:{click:e.startRecording}},[e._v("Start Recording\n ")]),i("v-btn",{attrs:{disabled:!e.recordingInProgress},on:{click:e.stopRecording}},[e._v("Stop Recording")]),i("v-icon",{class:e.recordingInProgress?"live":""},[e._v("mic")])],1)]),i("v-layout",{staticClass:"mt-1",attrs:{row:"",wrap:""}},[i("v-flex",{attrs:{xs12:""}},[i("h4",{staticClass:"mt-3"},[e._v("FFT Visualization")]),i("v-divider")],1),i("v-flex",{attrs:{xs12:""}},[i("canvas",{staticStyle:{border:"1px solid black",width:"100%",height:"20em"}})])],1),e.recordings.length>0?i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Recordings")]),i("v-divider"),e._l(e.recordings,(function(t,s){return i("div",{key:t.ts},[i("v-card",[i("v-card-title",{attrs:{"primary-title":""}},[i("v-layout",{attrs:{column:"",wrap:""}},[i("div",[i("h3",[e._v("Recording #"+e._s(s+1))])]),i("div",{staticClass:"ml-3"},[i("div",[i("audio",{attrs:{src:t.blobUrl,controls:"true"}})]),i("div",[e._v("\n size: "+e._s(e._f("fileSizeToHumanSize")(t.size))+", type: "+e._s(t.mimeType)+"\n ")])])])],1)],1),s!==e.recordings.length-1?i("v-divider"):e._e()],1)}))],2):e._e(),i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Source")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/views/Test5.vue"}},[e._v("\n src/views/Test5.vue\n ")]),i("ul",{staticClass:"ml-3"},[i("li",[e._v("Primarily:\n "),i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/shared/RecorderService.js"}},[e._v("\n src/shared/RecorderService.js\n ")])])])])])])],1),i("v-layout",{attrs:{column:"",wrap:"","hidden-xs-only":""}},[i("h4",{staticClass:"mt-3"},[e._v("Relevant")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode"}},[e._v("\n https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode\n ")])]),i("li",[i("a",{attrs:{href:"http://webaudioapi.com/samples/visualizer/"}},[e._v("http://webaudioapi.com/samples/visualizer/")])])])])],1)],1)},le=[],de=.7,ue=2048,he={name:"Test3",filters:{fileSizeToHumanSize:function(e){return f.humanFileSize(e,!0)}},data:function(){return{recordingInProgress:!1,recordings:[]}},watch:{},created:function(){var e=this;this.recorderSrvc=new p,this.recorderSrvc.config.createAnalyserNode=!0,this.recorderSrvc.em.addEventListener("recording",(function(t){return e.onNewRecording(t)}))},mounted:function(){},methods:{startRecording:function(){var e=this;this.recorderSrvc.startRecording().then((function(){e.recordingInProgress=!0,e.analyser=e.recorderSrvc.analyserNode,e.audioCtx=e.recorderSrvc.audioCtx,e.analyser.minDecibels=-140,e.analyser.maxDecibels=0,e.freqs=new Uint8Array(e.analyser.frequencyBinCount),e.times=new Uint8Array(e.analyser.frequencyBinCount),e.canvas=document.querySelector("canvas"),e.canvasWidth=e.canvas.width,e.canvasHeight=e.canvas.height,window.requestAnimationFrame(e.draw.bind(e))})).catch((function(e){console.error("Exception while start recording: "+e),alert("Exception while start recording: "+e.message)}))},stopRecording:function(){this.recordingInProgress=!1,this.recorderSrvc.stopRecording()},onNewRecording:function(e){this.recordings.push(e.detail.recording)},draw:function(){this.analyser.smoothingTimeConstant=de,this.analyser.fftSize=ue,this.analyser.getByteFrequencyData(this.freqs),this.analyser.getByteTimeDomainData(this.times);var e=this.canvas,t=e.getContext("2d"),i=this.canvasWidth,s=this.canvasHeight;t.clearRect(0,0,i,s);for(var r=0;r0?i("v-layout",{attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Recordings")]),i("v-divider"),e._l(e.recordings,(function(t,s){return i("div",{key:t.ts},[i("v-card",[i("v-card-title",{attrs:{"primary-title":""}},[i("v-layout",{attrs:{column:"",wrap:""}},[i("div",[i("h3",[e._v("Recording #"+e._s(s+1))])]),i("div",{staticClass:"ml-3"},[i("div",[i("audio",{attrs:{src:t.blobUrl,type:t.mimeType,controls:"true"}})]),i("div",[i("a",{attrs:{href:t.blobUrl,download:""}},[e._v("download")])]),i("div",[e._v("\n size: "+e._s(e._f("fileSizeToHumanSize")(t.size))+", type: "+e._s(t.mimeType)+"\n ")])])])],1)],1),s!==e.recordings.length-1?i("v-divider"):e._e()],1)}))],2):e._e(),i("v-layout",{staticClass:"mt-3",attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Notes")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("p",[e._v("\n This is important mostly for safari/edge or any browser without native MediaRecorder support. Without native\n encoding, most encoding implementations encode the incoming audio stream to uncompressed PCM audio/wav.\n However, this significantly limits the duration of the recording due to memory constraints. And if not encoded\n before upload, the upload is significantly larger than it would be if using a compressed encoding. Encoding\n the wav data after recording, before uploading, is relatively straightforward. This test is specifically to\n test encoding "),i("em",[e._v("while")]),e._v(" recording.\n ")]),i("p",[e._v("\n This test uses web workers to do the encoding. Check out\n "),i("a",{attrs:{href:"https://higuma.github.io/ogg-vorbis-encoder-js/"}},[e._v("this demo\n ")]),e._v("\n for testing encoding with and without workers.\n ")]),i("p",[e._v("\n If using ogg encoder, the resulting recordings will not playback on ios/safari because it doesn't support the\n format natively. Also, I was unable to get the minified version (~800k) of the ogg encoder to work. The\n unminified version currently used in this test is ~2.3MB (or ~400k gzipped).\n ")])])],1),i("v-layout",{staticClass:"mt-3",attrs:{column:"",wrap:""}},[i("h4",{staticClass:"mt-3"},[e._v("Source")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/views/Test6.vue"}},[e._v("\n src/views/Test6.vue\n ")]),i("ul",{staticClass:"ml-3"},[i("li",[e._v("Primarily:\n "),i("a",{attrs:{href:"https://github.com/kaliatech/web-audio-recording-tests/blob/master/src/shared/RecorderService.js"}},[e._v("\n src/shared/RecorderService.js\n ")])])])])])])],1),i("v-layout",{attrs:{column:"",wrap:"","hidden-xs-only":""}},[i("h4",{staticClass:"mt-3"},[e._v("Relevant")]),i("v-divider"),i("div",{staticClass:"ml-4"},[i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/zhuker/lamejs"}},[e._v("https://github.com/zhuker/lamejs")])]),i("li",[i("a",{attrs:{href:"https://github.com/zhuker/lamejs/pull/17"}},[e._v("https://github.com/zhuker/lamejs/pull/17")])]),i("li",[i("a",{attrs:{href:"https://higuma.github.io/ogg-vorbis-encoder-js/"}},[e._v("https://higuma.github.io/ogg-vorbis-encoder-js/\n ")]),i("ul",[i("li",[i("a",{attrs:{href:"https://github.com/higuma/ogg-vorbis-encoder-js"}},[e._v("https://github.com/higuma/ogg-vorbis-encoder-js\n ")])])])])])])],1)],1)},fe=[],be=[{id:"wav",name:"audio/wav - custom - mono"},{id:"mp3",name:"audio/mpeg - zhuker/lamejs - mono/128kbps"},{id:"ogg",name:"audio/ogg - higuma/oggjs - mono/~128kps"}],we={name:"Test3",filters:{fileSizeToHumanSize:function(e){return f.humanFileSize(e,!0)}},data:function(){return{encoders:be,hasMediaRecorder:window.MediaRecorder||!1,selectedEncoder:be[0].id,recordingInProgress:!1,recordings:[]}},watch:{},created:function(){var e=this;this.recorderSrvc=new p,this.recorderSrvc.config.usingMediaRecorder=!1,this.hasMediaRecorder=this.recorderSrvc.config.usingMediaRecorder,this.recorderSrvc.em.addEventListener("recording",(function(t){return e.onNewRecording(t)}))},mounted:function(){},methods:{startRecording:function(){var e=this;this.recorderSrvc.config.manualEncoderId=this.selectedEncoder,this.recorderSrvc.startRecording().then((function(){e.recordingInProgress=!0})).catch((function(e){console.error("Exception while start recording: "+e),alert("Exception while start recording: "+e.message)}))},stopRecording:function(){this.recordingInProgress=!1,this.recorderSrvc.stopRecording()},onNewRecording:function(e){this.recordings.push(e.detail.recording)}}},_e=we,ye=(i("40ea"),Object(_["a"])(_e,ge,fe,!1,null,"71cd8b71",null)),ke=ye.exports,Ce=function(){var e=this,t=e.$createElement,i=e._self._c||t;return i("v-container",{attrs:{fluid:""}},[i("v-layout",{attrs:{row:"",wrap:""}},[i("v-flex",{attrs:{xs12:""}},[i("h3",[e._v("Version")]),e._v("\n "+e._s(e.version)+"\n ")]),i("v-flex",{staticClass:"mt-3",attrs:{xs12:""}},[i("h3",[e._v("Commit")]),e._v("\n "+e._s(e.commit)+"\n ")]),i("v-flex",{staticClass:"mt-3",attrs:{xs12:""}},[i("h3",[e._v("Branch")]),e._v("\n "+e._s(e.branch)+"\n ")]),i("v-flex",{staticClass:"mt-3",attrs:{xs12:""}},[i("h3",[e._v("Timestamp")]),e._v("\n "+e._s(e.timestamp)+"\n ")])],1)],1)},Se=[],xe={data:function(){return{version:"8f9f847",commit:"8f9f8473285dd42f6267bb1e30dcb73e5f1b8f74",branch:"master",timestamp:"2021-01-16T15:31:50.292Z"}}},Re=xe,Pe=Object(_["a"])(Re,Ce,Se,!1,null,null,null),Me=Pe.exports;s["default"].use(P["a"]);var Te=new P["a"]({mode:"hash",routes:[{path:"/",name:"home",component:I},{path:"/diagnostics",name:"diagnostics",component:U},{path:"/test1",name:"test1",component:k},{path:"/test2",name:"test2",component:L},{path:"/test3",name:"test3",component:te},{path:"/test4",name:"test4",component:ae},{path:"/test5",name:"test5",component:pe},{path:"/test6",name:"test6",component:ke},{path:"/version",name:"version",component:Me}]}),De=Te;s["default"].use(n.a,{theme:{primary:"#546E7A",secondary:"#B0BEC5",accent:"#448AFF",error:"#EF5350",warning:"#FFF176",info:"#2196f3",success:"#4caf50"}}),s["default"].config.productionTip=!1,new s["default"]({router:De,render:function(e){return e(R)}}).$mount("#app")},6912:function(e,t,i){"use strict";i("5699")},"700e":function(e,t,i){"use strict";i("9f9b")},"86b3":function(e,t,i){},9239:function(e,t,i){"use strict";i("a210")},"9f9b":function(e,t,i){},a0bb:function(e,t,i){"use strict";i("86b3")},a210:function(e,t,i){},b52b:function(e,t,i){"use strict";i("d6db")},c219:function(e,t,i){"use strict";i("e9bb")},cae3:function(e,t,i){"use strict";i("054d")},cff7:function(e,t,i){},d6db:function(e,t,i){},e9bb:function(e,t,i){},eebc:function(e,t,i){}}); 2 | //# sourceMappingURL=app.a3ad6502.js.map --------------------------------------------------------------------------------