mirror of
https://github.com/kevin-DL/vue-audio-recorder.git
synced 2026-01-11 10:54:26 +00:00
Add existing code
This commit is contained in:
6
.babelrc
Normal file
6
.babelrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["env", { "modules": false }],
|
||||||
|
"stage-3"
|
||||||
|
]
|
||||||
|
}
|
||||||
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
73
README.md
73
README.md
@@ -1,2 +1,71 @@
|
|||||||
# vue-a-recorder
|
# vue-audio-recorder
|
||||||
A simple audio recorder for VueJS applications
|
|
||||||
|
> Audio recorder for Vue.js. It allows to create, play, download and store records on a server
|
||||||
|
|
||||||
|
#### [Live demo](https://jsfiddle.net/grishkovelli/rb1anxyj/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Beautiful clean UI
|
||||||
|
- Download/upload/play record
|
||||||
|
- Time limit
|
||||||
|
- Records limit
|
||||||
|
- A lot of callbacks
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
npm i vue-audio-recorder --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| --------------------- | -------- | ---------------------------------------------------------------- |
|
||||||
|
| attempts | Number | Number of recording attempts |
|
||||||
|
| time | Number | Time limit for record in minutes |
|
||||||
|
| upload-url | String | URL for uploading |
|
||||||
|
| start-record | Function | It fires after click the record button |
|
||||||
|
| stop-record | Function | It fires after click the stop button or exceeding the time limit |
|
||||||
|
| start-upload | Function | It fires after start uploading |
|
||||||
|
| attempts-limit | Function | It fires after exceeding the attempts |
|
||||||
|
| failed-upload | Function | Is fires after failure uploading |
|
||||||
|
| mic-failed | Function | It fires if your microphone doesn't work |
|
||||||
|
| successful-upload | Function | It fires after successful uploading |
|
||||||
|
| successful-upload-msg | String | Display the message after successful uploading |
|
||||||
|
| failed-upload-msg | String | Display the message after failure uploading |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
<audio-recorder
|
||||||
|
upload-url="YOUR_API_URL"
|
||||||
|
:attempts="3"
|
||||||
|
:time="2"
|
||||||
|
:start-record="callback"
|
||||||
|
:stop-record="callback"
|
||||||
|
:start-upload="callback"
|
||||||
|
:successful-upload="callback"
|
||||||
|
:failed-upload="callback"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Setup
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# serve with hot reload at localhost:8080
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# build for production with minification
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
[Gennady Grishkovtsov](https://www.linkedin.com/in/grishkovtsov/) - Developer
|
||||||
|
|
||||||
|
[Olga Zimina](https://www.behance.net/zimin4ik) - UIX Designer
|
||||||
|
|||||||
21
demo/index.html
Normal file
21
demo/index.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>vue-audio-recorder | demo</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<audio-recorder
|
||||||
|
upload-url="some url"
|
||||||
|
:attempts="3"
|
||||||
|
:time="2"
|
||||||
|
:start-record="callback"
|
||||||
|
:stop-record="callback"
|
||||||
|
:start-upload="callback"
|
||||||
|
:successful-upload="callback"
|
||||||
|
:failed-upload="callback"/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
demo/index.js
Normal file
16
demo/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import AudioRecorder from '../src/audio-recorder'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.prototype.$http = axios
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
components: {AudioRecorder},
|
||||||
|
methods: {
|
||||||
|
callback (msg) {
|
||||||
|
console.debug('Event: ', msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
2
dist/vue-audio-recorder.min.js
vendored
Normal file
2
dist/vue-audio-recorder.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/vue-audio-recorder.min.js.map
vendored
Normal file
1
dist/vue-audio-recorder.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
9104
package-lock.json
generated
Normal file
9104
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
package.json
Normal file
51
package.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-audio-recorder",
|
||||||
|
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie <= 8"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-preset-env": "^1.6.0",
|
||||||
|
"babel-preset-stage-3": "^6.24.1",
|
||||||
|
"cross-env": "^5.0.5",
|
||||||
|
"css-loader": "^0.28.7",
|
||||||
|
"file-loader": "^1.1.4",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"node-sass": "^4.5.3",
|
||||||
|
"sass-loader": "^6.0.6",
|
||||||
|
"vue": "^2.5.16",
|
||||||
|
"vue-loader": "^13.0.5",
|
||||||
|
"vue-template-compiler": "^2.4.4",
|
||||||
|
"webpack": "^3.6.0",
|
||||||
|
"webpack-dev-server": "^2.9.1",
|
||||||
|
"webpack-merge": "^4.1.3"
|
||||||
|
},
|
||||||
|
"main": "dist/vue-audio-recorder.min.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/grishkovelli/vue-audio-recorder.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"vue",
|
||||||
|
"audio",
|
||||||
|
"record",
|
||||||
|
"microphone"
|
||||||
|
],
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/grishkovelli/vue-audio-recorder/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/grishkovelli/vue-audio-recorder#readme"
|
||||||
|
}
|
||||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
321
src/audio-recorder.vue
Normal file
321
src/audio-recorder.vue
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
<style lang="scss">
|
||||||
|
.ar {
|
||||||
|
width: 420px;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #FAFAFA;
|
||||||
|
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.17);
|
||||||
|
position: relative;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-records {
|
||||||
|
height: 138px;
|
||||||
|
padding-top: 1px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__record {
|
||||||
|
width: 320px;
|
||||||
|
padding: 0 10px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 45px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #E8E8E8;
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
border: 1px solid #E8E8E8;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
margin-top: -1px;
|
||||||
|
padding: 0 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-recorder {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__duration {
|
||||||
|
color: #AEAEAE;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stop {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: -52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-limit {
|
||||||
|
position: absolute;
|
||||||
|
color: #AEAEAE;
|
||||||
|
font-size: 12px;
|
||||||
|
top: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__records-limit {
|
||||||
|
position: absolute;
|
||||||
|
color: #AEAEAE;
|
||||||
|
font-size: 12px;
|
||||||
|
top: 78px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-spinner {
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 144px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
display: block;
|
||||||
|
margin: 0 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background: #05CBCD;
|
||||||
|
animation-name: blink;
|
||||||
|
animation-duration: 1.4s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
|
||||||
|
&:nth-child(2) { animation-delay: .2s; }
|
||||||
|
|
||||||
|
&:nth-child(3) { animation-delay: .4s; }
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0% { opacity: .2; }
|
||||||
|
20% { opacity: 1; }
|
||||||
|
100% { opacity: .2; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: rgba(84,84,84,0.5);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__blur {
|
||||||
|
filter: blur(2px);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upload-status {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--fail {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ar">
|
||||||
|
<div class="ar__overlay" v-if="isUploading"></div>
|
||||||
|
|
||||||
|
<div class="ar-spinner" v-if="isUploading">
|
||||||
|
<div class="ar-spinner__dot"></div>
|
||||||
|
<div class="ar-spinner__dot"></div>
|
||||||
|
<div class="ar-spinner__dot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ar-content" :class="{'ar__blur': isUploading}">
|
||||||
|
<div class="ar-recorder">
|
||||||
|
<icon-button
|
||||||
|
size="lg"
|
||||||
|
:name="iconButtonType"
|
||||||
|
:class="{
|
||||||
|
'ar-icon-button--rec': isRecording,
|
||||||
|
'ar-icon-button--pulse': isRecording && volume > 0.02
|
||||||
|
}"
|
||||||
|
@click.native="toggleRecorder"/>
|
||||||
|
<icon-button
|
||||||
|
class="ar-recorder__stop"
|
||||||
|
name="stop"
|
||||||
|
@click.native="stopRecorder"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ar-recorder__records-limit" v-if="attempts">Attempts: {{attemptsLeft}}/{{attempts}}</div>
|
||||||
|
<div class="ar-recorder__duration">{{recordedTime}}</div>
|
||||||
|
<div class="ar-recorder__time-limit" v-if="time">Record duration is limited: {{time}}m</div>
|
||||||
|
|
||||||
|
<div class="ar-records">
|
||||||
|
<div
|
||||||
|
class="ar-records__record"
|
||||||
|
:class="{'ar-records__record--selected': idx === selectedRecord.idx}"
|
||||||
|
v-for="(record, idx) in recordList"
|
||||||
|
@click="selectRecord(idx, record)">
|
||||||
|
<div class="ar__text">Record {{idx + 1}}</div>
|
||||||
|
<div class="ar__text">{{record.duration}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<record-player
|
||||||
|
:record="selectedRecord"
|
||||||
|
:upload-url="uploadUrl"
|
||||||
|
:start-upload="startUpload"
|
||||||
|
:successful-upload="successfulUpload"
|
||||||
|
:failed-upload="failedUpload"
|
||||||
|
@on-start-upload="onStartUpload"
|
||||||
|
@on-end-upload="onEndUpload"/>
|
||||||
|
|
||||||
|
<div :class="uploadStatusClasses" v-if="uploadStatus">{{message}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import IconButton from './components/icon-button.vue'
|
||||||
|
import Recorder from './lib/recorder.js'
|
||||||
|
import RecordPlayer from './components/record-player.vue'
|
||||||
|
import { convertTimeMMSS } from './lib/utils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
attempts: { type: Number },
|
||||||
|
time: { type: Number },
|
||||||
|
uploadUrl: { type: String },
|
||||||
|
|
||||||
|
attemptsLimit: { type: Function },
|
||||||
|
failedUpload: { type: Function },
|
||||||
|
micFailed: { type: Function },
|
||||||
|
startRecord: { type: Function },
|
||||||
|
startUpload: { type: Function },
|
||||||
|
stopRecord: { type: Function },
|
||||||
|
successfulUpload: { type: Function },
|
||||||
|
|
||||||
|
successfulUploadMsg: { type: String, default: 'Upload successful' },
|
||||||
|
failedUploadMsg: { type: String, default: 'Upload fail' }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isUploading: false,
|
||||||
|
recorder: new Recorder({
|
||||||
|
afterStop: () => {
|
||||||
|
this.recordList = this.recorder.recordList()
|
||||||
|
|
||||||
|
if (this.stopRecord) {
|
||||||
|
this.stopRecord('stop record')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attempts: this.attempts,
|
||||||
|
time: this.time
|
||||||
|
}),
|
||||||
|
recordList: [],
|
||||||
|
selectedRecord: {},
|
||||||
|
uploadStatus: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
RecordPlayer
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleRecorder () {
|
||||||
|
if (this.attempts && this.recorder.records.length >= this.attempts) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isRecording || (this.isRecording && this.isPause)) {
|
||||||
|
this.recorder.start()
|
||||||
|
if (this.startRecord) {
|
||||||
|
this.startRecord('start record')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.recorder.pause()
|
||||||
|
if (this.startRecord) {
|
||||||
|
this.startRecord('pause record')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopRecorder () {
|
||||||
|
if (!this.isRecording) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recorder.stop()
|
||||||
|
},
|
||||||
|
selectRecord (idx, record) {
|
||||||
|
this.selectedRecord = { idx: idx, url: record.url, blob: record.blob }
|
||||||
|
},
|
||||||
|
onStartUpload () {
|
||||||
|
this.isUploading = true
|
||||||
|
},
|
||||||
|
onEndUpload (status) {
|
||||||
|
this.isUploading = false
|
||||||
|
this.uploadStatus = status
|
||||||
|
setTimeout(() => {this.uploadStatus = null}, 1500)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
attemptsLeft () {
|
||||||
|
return this.attempts - this.recorder.records.length
|
||||||
|
},
|
||||||
|
iconButtonType () {
|
||||||
|
return this.isRecording && this.isPause ? 'mic' : this.isRecording ? 'pause' : 'mic'
|
||||||
|
},
|
||||||
|
isPause () {
|
||||||
|
return this.recorder.isPause
|
||||||
|
},
|
||||||
|
isRecording () {
|
||||||
|
return this.recorder.isRecording
|
||||||
|
},
|
||||||
|
message () {
|
||||||
|
return this.uploadStatus === 'success' ? this.successfulUploadMsg : this.failedUploadMsg
|
||||||
|
},
|
||||||
|
recordedTime () {
|
||||||
|
if (this.time && this.recorder.duration >= this.time * 60) {
|
||||||
|
this.stopRecorder()
|
||||||
|
}
|
||||||
|
return convertTimeMMSS(this.recorder.duration)
|
||||||
|
},
|
||||||
|
uploadStatusClasses () {
|
||||||
|
let classes = ['ar__upload-status']
|
||||||
|
classes.push(this.uploadStatus === 'success' ? 'ar__upload-status--success' : 'ar__upload-status--fail')
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
volume () {
|
||||||
|
return parseFloat(this.recorder.volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
84
src/components/icon-button.vue
Normal file
84
src/components/icon-button.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<style lang="scss">
|
||||||
|
.ar-icon-button {
|
||||||
|
fill: #747474;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #05CBCD;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .2s;
|
||||||
|
|
||||||
|
&--rec {
|
||||||
|
fill: white;
|
||||||
|
background-color: #FF6B64;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pulse {
|
||||||
|
animation: ripple .5s linear infinite;
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
0% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 0 rgba(red, 0.1),
|
||||||
|
0 0 0 1px rgba(red, 0.1),
|
||||||
|
0 0 0 5px rgba(red, 0.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 0 rgba(red, 0.1),
|
||||||
|
0 0 0 10px rgba(red, 0.1),
|
||||||
|
0 0 0 20px rgba(red, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--clicked {
|
||||||
|
fill: white;
|
||||||
|
background-color: #05CBCD;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sm {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__lg {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="iconClasses" v-html="icons[name]"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: { type: String },
|
||||||
|
size: { type: String, default: 'sm' }
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
icons: {
|
||||||
|
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/><path fill="none" d="M0 0h24v24H0z"/></svg>',
|
||||||
|
mic: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||||
|
pause: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||||
|
play: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||||
|
save: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>',
|
||||||
|
stop: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 6h12v12H6z"/></svg>',
|
||||||
|
volume: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconClasses () {
|
||||||
|
return ['ar-icon-button', `ar-icon-button__${this.size}`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
54
src/components/line-control.vue
Normal file
54
src/components/line-control.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<style lang="scss">
|
||||||
|
.ar-line-control {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
position: absolute;
|
||||||
|
height: inherit;
|
||||||
|
background-color: #616161;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :ref="refId" class="ar-line-control" @mousedown="onMouseDown">
|
||||||
|
<div class="ar-line-control__head" :style="{width: percentageWidth}"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { calculateLineHeadPosition } from '../lib/utils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
refId: { type: String },
|
||||||
|
eventName: { type: String },
|
||||||
|
percentage: { type: Number, default: 0 }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onMouseDown (ev) {
|
||||||
|
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||||
|
this.$emit('changeLineHead', seekPos)
|
||||||
|
document.addEventListener('mousemove', this.onMouseMove)
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
},
|
||||||
|
onMouseUp (ev) {
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
|
document.removeEventListener('mousemove', this.onMouseMove)
|
||||||
|
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||||
|
this.$emit('changeLineHead', seekPos)
|
||||||
|
},
|
||||||
|
onMouseMove (ev) {
|
||||||
|
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||||
|
this.$emit('changeLineHead', seekPos)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
percentageWidth () {
|
||||||
|
let width = this.percentage < 1 ? this.percentage * 100 : this.percentage
|
||||||
|
return `${width}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
227
src/components/record-player.vue
Normal file
227
src/components/record-player.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<style lang="scss">
|
||||||
|
.ar-player {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
border: 1px solid #E8E8E8;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #FAFAFA;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&-actions {
|
||||||
|
width: 55%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-volume {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 10px;
|
||||||
|
|
||||||
|
&-bar {
|
||||||
|
width: 50px;
|
||||||
|
height: 8px;
|
||||||
|
background: #E6E6E6;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
fill: #747474;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: unset;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
width: 160px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #E6E6E6;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
color: rgba(84,84,84,0.5);
|
||||||
|
font-size: 16px;
|
||||||
|
width: 41px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ar-player" :class="{'ar-player--active': record.url}">
|
||||||
|
<div class="ar-player-bar">
|
||||||
|
|
||||||
|
<div class="ar-player__time">{{playedTime}}</div>
|
||||||
|
|
||||||
|
<line-control
|
||||||
|
class="ar-player__progress"
|
||||||
|
ref-id="progress"
|
||||||
|
:percentage="progress"
|
||||||
|
@changeLineHead="_onUpdateProgress"/>
|
||||||
|
|
||||||
|
<div class="ar-player__time">{{duration}}</div>
|
||||||
|
|
||||||
|
<div class="ar-player-volume">
|
||||||
|
<icon-button class="ar-player-volume__icon" name="volume"/>
|
||||||
|
<line-control
|
||||||
|
class="ar-player-volume-bar"
|
||||||
|
ref-id="volume"
|
||||||
|
:percentage="volume"
|
||||||
|
@changeLineHead="_onUpdateVolume"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ar-player-actions">
|
||||||
|
<icon-button name="download" @click.native="download"/>
|
||||||
|
<icon-button
|
||||||
|
size="lg"
|
||||||
|
:name="playBtnIcon"
|
||||||
|
:class="{'ar-icon-button--clicked': isPlaying}"
|
||||||
|
@click.native="playback"/>
|
||||||
|
<icon-button name="save" @click.native="upload"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio id="audio-recorder-player" :src="record.url"></audio>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import IconButton from './icon-button.vue'
|
||||||
|
import LineControl from './line-control.vue'
|
||||||
|
import { convertTimeMMSS } from '../lib/utils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
uploadUrl: { type: String },
|
||||||
|
record: { type: Object },
|
||||||
|
startUpload: { type: Function },
|
||||||
|
successfulUpload: { type: Function },
|
||||||
|
failedUpload: { type: Function }
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isPlaying: false,
|
||||||
|
duration: convertTimeMMSS(0),
|
||||||
|
playedTime: convertTimeMMSS(0),
|
||||||
|
progress: 0,
|
||||||
|
volume: 0.8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
LineControl
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
this.player = document.getElementById('audio-recorder-player')
|
||||||
|
|
||||||
|
this.player.addEventListener('ended', () => {
|
||||||
|
this.isPlaying = false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.player.addEventListener('loadeddata', (ev) => {
|
||||||
|
this._resetProgress()
|
||||||
|
this.duration = convertTimeMMSS(this.player.duration)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.player.addEventListener('timeupdate', this._onTimeUpdate)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
playBtnIcon () {
|
||||||
|
return this.isPlaying ? 'pause' : 'play'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
playback () {
|
||||||
|
if (!this.record.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPlaying) {
|
||||||
|
this.player.pause()
|
||||||
|
} else {
|
||||||
|
setTimeout(() => { this.player.play() }, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isPlaying = !this.isPlaying
|
||||||
|
},
|
||||||
|
upload () {
|
||||||
|
if (!this.record.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.startUpload) {
|
||||||
|
this.startUpload()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('on-start-upload')
|
||||||
|
|
||||||
|
let data = new FormData()
|
||||||
|
data.append('audio', this.record.blob, 'my-record')
|
||||||
|
|
||||||
|
this.$http.post(this.uploadUrl, data, {
|
||||||
|
headers: {'Content-Type': `multipart/form-data; boundary=${data._boundary}`}
|
||||||
|
}).then(resp => {
|
||||||
|
this.$emit('on-end-upload', 'success')
|
||||||
|
if (this.successfulUpload) {
|
||||||
|
this.successfulUpload(resp)
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.$emit('on-end-upload', 'fail')
|
||||||
|
if (this.failedUpload) {
|
||||||
|
this.failedUpload(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
download () {
|
||||||
|
if (!this.record.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let link = document.createElement('a')
|
||||||
|
link.href = this.record.url
|
||||||
|
link.download = 'record.wav'
|
||||||
|
link.click()
|
||||||
|
},
|
||||||
|
_resetProgress () {
|
||||||
|
this.isPlaying = false
|
||||||
|
this.progress = 0
|
||||||
|
},
|
||||||
|
_onTimeUpdate () {
|
||||||
|
this.playedTime = convertTimeMMSS(this.player.currentTime)
|
||||||
|
this.progress = (this.player.currentTime / this.player.duration) * 100
|
||||||
|
},
|
||||||
|
_onUpdateProgress (pos) {
|
||||||
|
if (pos) {
|
||||||
|
this.player.currentTime = pos * this.player.duration
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_onUpdateVolume (val) {
|
||||||
|
if (val) {
|
||||||
|
this.player.volume = val
|
||||||
|
this.volume = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
13
src/index.js
Normal file
13
src/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import AudioRecorder from './audio-recorder.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: function (Vue) {
|
||||||
|
if (this.installed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.installed = true
|
||||||
|
|
||||||
|
Vue.component('audio-recorder', AudioRecorder)
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/lib/recorder.js
Normal file
110
src/lib/recorder.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import WavEncoder from './wav-encoder'
|
||||||
|
import { convertTimeMMSS } from './utils'
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
constructor (options = {}) {
|
||||||
|
this.afterStop = options.afterStop
|
||||||
|
this.micFailed = options.micFailed
|
||||||
|
|
||||||
|
this.bufferSize = 4096
|
||||||
|
this.records = []
|
||||||
|
this.samples = []
|
||||||
|
|
||||||
|
this.isPause = false
|
||||||
|
this.isRecording = false
|
||||||
|
|
||||||
|
this.duration = 0
|
||||||
|
this.volume = 0
|
||||||
|
|
||||||
|
this._duration = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
start () {
|
||||||
|
navigator.mediaDevices.getUserMedia({audio: true})
|
||||||
|
.then(this._micCaptured.bind(this))
|
||||||
|
.catch(this._micError.bind(this))
|
||||||
|
this.isPause = false
|
||||||
|
this.isRecording = true
|
||||||
|
}
|
||||||
|
|
||||||
|
stop () {
|
||||||
|
this.stream.getTracks().forEach((track) => track.stop())
|
||||||
|
this.input.disconnect()
|
||||||
|
this.processor.disconnect()
|
||||||
|
this.context.close()
|
||||||
|
|
||||||
|
let encoder = new WavEncoder({
|
||||||
|
bufferSize: this.bufferSize,
|
||||||
|
sampleRate: this.context.sampleRate,
|
||||||
|
samples: this.samples
|
||||||
|
})
|
||||||
|
|
||||||
|
let audioBlob = encoder.getData()
|
||||||
|
let audioUrl = URL.createObjectURL(audioBlob)
|
||||||
|
|
||||||
|
this.samples = []
|
||||||
|
|
||||||
|
this.records.push({
|
||||||
|
blob: audioBlob,
|
||||||
|
url: audioUrl,
|
||||||
|
duration: convertTimeMMSS(this.duration)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isPause = false
|
||||||
|
this.isRecording = false
|
||||||
|
this._duration = 0
|
||||||
|
this.duration = 0
|
||||||
|
|
||||||
|
if (this.afterStop) {
|
||||||
|
this.afterStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause () {
|
||||||
|
this.stream.getTracks().forEach((track) => track.stop())
|
||||||
|
this.input.disconnect()
|
||||||
|
this.processor.disconnect()
|
||||||
|
this.context.close()
|
||||||
|
|
||||||
|
this._duration = this.duration
|
||||||
|
this.isPause = true
|
||||||
|
}
|
||||||
|
|
||||||
|
recordList () {
|
||||||
|
return this.records
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRecord () {
|
||||||
|
return this.records.slice(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_micCaptured (stream) {
|
||||||
|
this.context = new AudioContext()
|
||||||
|
this.input = this.context.createMediaStreamSource(stream)
|
||||||
|
this.processor = this.context.createScriptProcessor(this.bufferSize, 1, 1)
|
||||||
|
this.duration = this._duration
|
||||||
|
this.stream = stream
|
||||||
|
|
||||||
|
this.processor.onaudioprocess = (ev) => {
|
||||||
|
let sample = ev.inputBuffer.getChannelData(0)
|
||||||
|
let sum = 0.0
|
||||||
|
|
||||||
|
for (let i = 0; i < sample.length; ++i) {
|
||||||
|
sum += sample[i] * sample[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.duration = parseFloat(this._duration) + parseFloat(this.context.currentTime.toFixed(2))
|
||||||
|
this.volume = Math.sqrt(sum / sample.length).toFixed(2)
|
||||||
|
this.samples.push(new Float32Array(sample))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.input.connect(this.processor)
|
||||||
|
this.processor.connect(this.context.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
_micError (error) {
|
||||||
|
if (this.micFailed) {
|
||||||
|
this.micFailed(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/lib/utils.js
Normal file
22
src/lib/utils.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export function calculateLineHeadPosition (ev, element) {
|
||||||
|
let progressWidth = element.getBoundingClientRect().width
|
||||||
|
let leftPosition = ev.target.getBoundingClientRect().left
|
||||||
|
let pos = (ev.clientX - leftPosition) / progressWidth
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!ev.target.className.match(/^ar\-line\-control/)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = pos < 0 ? 0 : pos
|
||||||
|
pos = pos > 1 ? 1 : pos
|
||||||
|
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertTimeMMSS (seconds) {
|
||||||
|
return new Date(seconds * 1000).toISOString().substr(14, 5)
|
||||||
|
}
|
||||||
59
src/lib/wav-encoder.js
Normal file
59
src/lib/wav-encoder.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
export default class {
|
||||||
|
constructor (options) {
|
||||||
|
this.bufferSize = options.bufferSize || 4096
|
||||||
|
this.sampleRate = options.sampleRate
|
||||||
|
this.samples = options.samples
|
||||||
|
}
|
||||||
|
|
||||||
|
getData () {
|
||||||
|
this._joinSamples()
|
||||||
|
|
||||||
|
let buffer = new ArrayBuffer(44 + this.samples.length * 2)
|
||||||
|
let view = new DataView(buffer)
|
||||||
|
|
||||||
|
this._writeString(view, 0, 'RIFF') // RIFF identifier
|
||||||
|
view.setUint32(4, 36 + this.samples.length * 2, true) // RIFF chunk length
|
||||||
|
this._writeString(view, 8, 'WAVE') // RIFF type
|
||||||
|
this._writeString(view, 12, 'fmt ') // format chunk identifier
|
||||||
|
view.setUint32(16, 16, true) // format chunk length
|
||||||
|
view.setUint16(20, 1, true) // sample format (raw)
|
||||||
|
view.setUint16(22, 1, true) // channel count
|
||||||
|
view.setUint32(24, this.sampleRate, true) // sample rate
|
||||||
|
view.setUint32(28, this.sampleRate * 4, true) // byte rate (sample rate * block align)
|
||||||
|
view.setUint16(32, 4, true) // block align (channel count * bytes per sample)
|
||||||
|
view.setUint16(34, 16, true) // bits per sample
|
||||||
|
this._writeString(view, 36, 'data') // data chunk identifier
|
||||||
|
view.setUint32(40, this.samples.length * 2, true) // data chunk length
|
||||||
|
|
||||||
|
this._floatTo16BitPCM(view, 44, this.samples)
|
||||||
|
|
||||||
|
return new Blob([view], {type: 'audio/wav'})
|
||||||
|
}
|
||||||
|
|
||||||
|
_floatTo16BitPCM (output, offset, input) {
|
||||||
|
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||||
|
let s = Math.max(-1, Math.min(1, input[i]))
|
||||||
|
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_joinSamples () {
|
||||||
|
let recordLength = this.samples.length * this.bufferSize
|
||||||
|
let joinedSamples = new Float64Array(recordLength)
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < this.samples.length; i++) {
|
||||||
|
let sample = this.samples[i]
|
||||||
|
joinedSamples.set(sample, offset)
|
||||||
|
offset += sample.length
|
||||||
|
}
|
||||||
|
|
||||||
|
this.samples = joinedSamples
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeString (view, offset, string) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
webpack.config.js
Normal file
65
webpack.config.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const webpack = require('webpack')
|
||||||
|
const merge = require('webpack-merge')
|
||||||
|
const env = `./webpack.${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'}.js`
|
||||||
|
|
||||||
|
module.exports = merge(require(env), {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
options: {
|
||||||
|
loaders: {
|
||||||
|
'scss': [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif|svg)$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[ext]?[hash]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'vue$': 'vue/dist/vue.esm.js'
|
||||||
|
},
|
||||||
|
extensions: ['*', '.js', '.vue', '.json']
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: `"${process.env.NODE_ENV}"`
|
||||||
|
},
|
||||||
|
VERSION: JSON.stringify(require("./package.json").version)
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
noInfo: true,
|
||||||
|
overlay: true
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: false
|
||||||
|
}
|
||||||
|
})
|
||||||
17
webpack.dev.js
Normal file
17
webpack.dev.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: '#eval-source-map',
|
||||||
|
entry: './demo/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, './demo')
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: 'index.html',
|
||||||
|
template: './demo/index.html'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
31
webpack.prod.js
Normal file
31
webpack.prod.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: '#source-map',
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'vue-audio-recorder.min.js',
|
||||||
|
library: 'VueAudioRecorder',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
libraryExport: 'default',
|
||||||
|
umdNamedDefine: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: '"production"'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
sourceMap: true,
|
||||||
|
compress: {
|
||||||
|
warnings: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
minimize: false
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user