mirror of
https://github.com/kevin-DL/vue-audio-recorder.git
synced 2026-01-11 10:54:26 +00:00
Update version to 3.0.0
- use MP3 instead of WAV - new callbacks & properties - refactoring
This commit is contained in:
51
README.md
51
README.md
@@ -29,22 +29,23 @@ npm i vue-audio-recorder --save
|
|||||||
|
|
||||||
## AudioRecorder props
|
## AudioRecorder props
|
||||||
|
|
||||||
| Prop | Type | Description |
|
| Prop | Type | Description |
|
||||||
| --------------------- | -------- | --------------------------------------------------------------- |
|
| --------------------- | -------- | ------------------------------------------------------------------------ |
|
||||||
| attempts | Number | Number of recording attempts |
|
| attempts | Number | Number of recording attempts |
|
||||||
| compact | Boolean | Hide the download and upload buttons |
|
| headers | Object | HTTP headers |
|
||||||
| headers | Object | HTTP headers |
|
| time | Number | Time limit for the record (minutes) |
|
||||||
| time | Number | Time limit for the record (minutes) |
|
| filename | String | Download/Upload filename |
|
||||||
| upload-url | String | URL for uploading |
|
| upload-url | String | URL for uploading |
|
||||||
| start-record | Function | Fires after click the record button |
|
| show-download-button | Boolean | If it is true show a download button. Default: true |
|
||||||
| stop-record | Function | Fires after click the stop button or exceeding the time limit |
|
| show-upload-button | Boolean | If it is true show an upload button. Default: true |
|
||||||
| start-upload | Function | Fires after start uploading |
|
| before-upload | Function | Callback fires before uploading |
|
||||||
| attempts-limit | Function | Fires after exceeding the attempts |
|
| successful-upload | Function | Callback fires after successful uploading |
|
||||||
| failed-upload | Function | Fires after a failure uploading |
|
| failed-upload | Function | Callback fires after failure uploading |
|
||||||
| mic-failed | Function | Fires if your microphone doesn't work |
|
| mic-failed | Function | Callback fires if your microphone doesn't work |
|
||||||
| successful-upload | Function | Fires after a successful uploading |
|
| before-recording | Function | Callback fires after click the record button |
|
||||||
| successful-upload-msg | String | Displays the message after a successful uploading |
|
| pause-recording | Function | Callback fires after pause recording |
|
||||||
| failed-upload-msg | String | Displays the message after a failure uploading |
|
| after-recording | Function | Callback fires after click the stop button or exceeding the time limit |
|
||||||
|
| select-record | Function | Callback fires after choise a record. Returns the record |
|
||||||
|
|
||||||
## AudioPlayer props
|
## AudioPlayer props
|
||||||
| Prop | Type | Description |
|
| Prop | Type | Description |
|
||||||
@@ -59,15 +60,25 @@ npm i vue-audio-recorder --save
|
|||||||
Vue.use(AudioRecorder)
|
Vue.use(AudioRecorder)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
methods: {
|
||||||
|
callback (data) {
|
||||||
|
console.debug(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<audio-recorder
|
<audio-recorder
|
||||||
upload-url="YOUR_API_URL"
|
upload-url="YOUR_API_URL"
|
||||||
:attempts="3"
|
:attempts="3"
|
||||||
:headers="headers"
|
|
||||||
:time="2"
|
:time="2"
|
||||||
:start-record="callback"
|
:headers="headers"
|
||||||
:stop-record="callback"
|
:before-recording="callback"
|
||||||
:start-upload="callback"
|
:pause-recording="callback"
|
||||||
|
:after-recording="callback"
|
||||||
|
:select-record="callback"
|
||||||
|
:before-upload="callback"
|
||||||
:successful-upload="callback"
|
:successful-upload="callback"
|
||||||
:failed-upload="callback"/>
|
:failed-upload="callback"/>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
:attempts="3"
|
:attempts="3"
|
||||||
:time="2"
|
:time="2"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:start-record="callback"
|
:before-recording="callback"
|
||||||
:stop-record="callback"
|
:pause-recording="callback"
|
||||||
:start-upload="callback"
|
:after-recording="callback"
|
||||||
|
:select-record="callback"
|
||||||
|
:before-upload="callback"
|
||||||
:successful-upload="callback"
|
:successful-upload="callback"
|
||||||
:failed-upload="callback"/>
|
:failed-upload="callback"/>
|
||||||
|
|
||||||
|
|||||||
2
dist/vue-audio-recorder.min.js
vendored
2
dist/vue-audio-recorder.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-audio-recorder",
|
"name": "vue-audio-recorder",
|
||||||
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
|
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
|
||||||
"version": "2.2.0",
|
"version": "3.0.0",
|
||||||
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
|
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack-dev-server --env.NODE_ENV=development --mode development --open --hot --https",
|
"dev": "webpack-dev-server --env.NODE_ENV=development --mode development --open --hot --https",
|
||||||
"build": "webpack --env.NODE_ENV=production --mode production --progress --hide-modules"
|
"build": "webpack --env.NODE_ENV=production --mode production --progress --hide-modules"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"lamejs": "^1.2.0"
|
||||||
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
|
|||||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB |
37
src/components/downloader.vue
Normal file
37
src/components/downloader.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<style lang="scss">
|
||||||
|
@import '../scss/icons';
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<icon-button
|
||||||
|
id="download"
|
||||||
|
class="ar-icon ar-icon__xs ar-icon--no-border"
|
||||||
|
name="download"
|
||||||
|
@click.native="download"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import IconButton from './icon-button'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
record : { type: Object },
|
||||||
|
filename : { type: String }
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconButton
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
download () {
|
||||||
|
if (!this.record.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = this.record.url
|
||||||
|
link.download = `${this.filename}.mp3`
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onMouseDown (ev) {
|
onMouseDown (ev) {
|
||||||
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||||
this.$emit('change-linehead', seekPos)
|
this.$emit('change-linehead', seekPos)
|
||||||
document.addEventListener('mousemove', this.onMouseMove)
|
document.addEventListener('mousemove', this.onMouseMove)
|
||||||
document.addEventListener('mouseup', this.onMouseUp)
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
@@ -43,17 +43,17 @@
|
|||||||
onMouseUp (ev) {
|
onMouseUp (ev) {
|
||||||
document.removeEventListener('mouseup', this.onMouseUp)
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
document.removeEventListener('mousemove', this.onMouseMove)
|
document.removeEventListener('mousemove', this.onMouseMove)
|
||||||
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||||
this.$emit('change-linehead', seekPos)
|
this.$emit('change-linehead', seekPos)
|
||||||
},
|
},
|
||||||
onMouseMove (ev) {
|
onMouseMove (ev) {
|
||||||
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
|
||||||
this.$emit('change-linehead', seekPos)
|
this.$emit('change-linehead', seekPos)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
calculateSize () {
|
calculateSize () {
|
||||||
let value = this.percentage < 1 ? this.percentage * 100 : this.percentage
|
const value = this.percentage < 1 ? this.percentage * 100 : this.percentage
|
||||||
return `${this.rowDirection ? 'width' : 'height'}: ${value}%`
|
return `${this.rowDirection ? 'width' : 'height'}: ${value}%`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ar-player {
|
.ar-player {
|
||||||
width: 380px;
|
width: 380px;
|
||||||
height: 120px;
|
height: unset;
|
||||||
border: 1px solid #E8E8E8;
|
border: 0;
|
||||||
border-radius: 24px;
|
border-radius: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #FAFAFA;
|
background-color: unset;
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
|
|
||||||
|
& > .ar-player-bar {
|
||||||
|
border: 1px solid #E8E8E8;
|
||||||
|
border-radius: 24px;
|
||||||
|
margin: 0 0 0 5px;
|
||||||
|
|
||||||
|
& > .ar-player__progress {
|
||||||
|
width: 125px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-bar {
|
&-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -26,33 +36,6 @@
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--compact {
|
|
||||||
height: unset;
|
|
||||||
flex-direction: row;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: unset;
|
|
||||||
|
|
||||||
& > .ar-player-actions {
|
|
||||||
width: unset;
|
|
||||||
|
|
||||||
& > #download,
|
|
||||||
& > #upload {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .ar-player-bar {
|
|
||||||
border: 1px solid #E8E8E8;
|
|
||||||
border-radius: 24px;
|
|
||||||
margin: 0 0 0 5px;
|
|
||||||
|
|
||||||
& > .ar-player__progress {
|
|
||||||
width: 125px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__progress {
|
&__progress {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
@@ -81,27 +64,14 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ar-player" :class="{'ar-player--compact': compact}">
|
<div class="ar-player">
|
||||||
|
|
||||||
<div class="ar-player-actions">
|
<div class="ar-player-actions">
|
||||||
<icon-button
|
|
||||||
id="download"
|
|
||||||
class="ar-icon ar-icon__sm"
|
|
||||||
name="download"
|
|
||||||
@click.native="decorator(download)"/>
|
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
id="play"
|
id="play"
|
||||||
class="ar-icon ar-icon__lg ar-player__play"
|
class="ar-icon ar-icon__lg ar-player__play"
|
||||||
:name="playBtnIcon"
|
:name="playBtnIcon"
|
||||||
:class="{'ar-player__play--active': isPlaying}"
|
:class="{'ar-player__play--active': isPlaying}"
|
||||||
@click.native="decorator(playback)"/>
|
@click.native="playback"/>
|
||||||
|
|
||||||
<uploader
|
|
||||||
id="upload"
|
|
||||||
class="ar-icon ar-icon__sm"
|
|
||||||
:record="record"
|
|
||||||
:options="uploaderOptions"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ar-player-bar">
|
<div class="ar-player-bar">
|
||||||
@@ -122,16 +92,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import IconButton from './icon-button'
|
import IconButton from './icon-button'
|
||||||
import LineControl from './line-control'
|
import LineControl from './line-control'
|
||||||
import Uploader from './uploader'
|
|
||||||
import VolumeControl from './volume-control'
|
import VolumeControl from './volume-control'
|
||||||
import { convertTimeMMSS } from '@/lib/utils'
|
import { convertTimeMMSS } from '@/lib/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
src : { type: String },
|
src : { type: String },
|
||||||
record : { type: Object },
|
record : { type: Object },
|
||||||
compact : { type: Boolean, default: true },
|
filename : { type: String }
|
||||||
uploaderOptions : { type: Object, default: () => new Object }
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -144,7 +112,6 @@
|
|||||||
components: {
|
components: {
|
||||||
IconButton,
|
IconButton,
|
||||||
LineControl,
|
LineControl,
|
||||||
Uploader,
|
|
||||||
VolumeControl
|
VolumeControl
|
||||||
},
|
},
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
@@ -167,7 +134,7 @@
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audioSource () {
|
audioSource () {
|
||||||
let url = this.src || this.record.url
|
const url = this.src || this.record.url
|
||||||
if (url) {
|
if (url) {
|
||||||
return url
|
return url
|
||||||
} else {
|
} else {
|
||||||
@@ -183,6 +150,10 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
playback () {
|
playback () {
|
||||||
|
if (!this.audioSource) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isPlaying) {
|
if (this.isPlaying) {
|
||||||
this.player.pause()
|
this.player.pause()
|
||||||
} else {
|
} else {
|
||||||
@@ -191,18 +162,6 @@
|
|||||||
|
|
||||||
this.isPlaying = !this.isPlaying
|
this.isPlaying = !this.isPlaying
|
||||||
},
|
},
|
||||||
download () {
|
|
||||||
let link = document.createElement('a')
|
|
||||||
link.href = this.record.url
|
|
||||||
link.download = 'record.wav'
|
|
||||||
link.click()
|
|
||||||
},
|
|
||||||
decorator (func) {
|
|
||||||
if (!this.audioSource) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
func()
|
|
||||||
},
|
|
||||||
_resetProgress () {
|
_resetProgress () {
|
||||||
if (this.isPlaying) {
|
if (this.isPlaying) {
|
||||||
this.player.pause()
|
this.player.pause()
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
&__records-limit {
|
&__records-limit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: #AEAEAE;
|
color: #AEAEAE;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
top: 78px;
|
top: 78px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +160,22 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
color: rgb(244, 120, 90);
|
color: rgb(244, 120, 90);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__downloader,
|
||||||
|
&__uploader {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__downloader {
|
||||||
|
right: 115px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__uploader {
|
||||||
|
right: 85px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '../scss/icons';
|
@import '../scss/icons';
|
||||||
@@ -168,7 +184,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ar">
|
<div class="ar">
|
||||||
<div class="ar__overlay" v-if="isUploading"></div>
|
<div class="ar__overlay" v-if="isUploading"></div>
|
||||||
|
|
||||||
<div class="ar-spinner" v-if="isUploading">
|
<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 class="ar-spinner__dot"></div>
|
||||||
@@ -201,89 +216,91 @@
|
|||||||
:class="{'ar-records__record--selected': record.id === selected.id}"
|
:class="{'ar-records__record--selected': record.id === selected.id}"
|
||||||
:key="record.id"
|
:key="record.id"
|
||||||
v-for="(record, idx) in recordList"
|
v-for="(record, idx) in recordList"
|
||||||
@click="selected = record">
|
@click="choiceRecord(record)">
|
||||||
<div
|
<div
|
||||||
class="ar__rm"
|
class="ar__rm"
|
||||||
v-if="record.id === selected.id"
|
v-if="record.id === selected.id"
|
||||||
@click="removeRecord(idx)">×</div>
|
@click="removeRecord(idx)">×</div>
|
||||||
<div class="ar__text">Record {{idx + 1}}</div>
|
<div class="ar__text">Record {{idx + 1}}</div>
|
||||||
<div class="ar__text">{{record.duration}}</div>
|
<div class="ar__text">{{record.duration}}</div>
|
||||||
|
|
||||||
|
<downloader
|
||||||
|
v-if="record.id === selected.id && showDownloadButton"
|
||||||
|
class="ar__downloader"
|
||||||
|
:record="record"
|
||||||
|
:filename="filename"/>
|
||||||
|
|
||||||
|
<uploader
|
||||||
|
v-if="record.id === selected.id && showUploadButton"
|
||||||
|
class="ar__uploader"
|
||||||
|
:record="record"
|
||||||
|
:filename="filename"
|
||||||
|
:headers="headers"
|
||||||
|
:upload-url="uploadUrl"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio-player :compact="compact" :record="selected" :uploader-options="uploaderOptions"/>
|
<audio-player :record="selected"/>
|
||||||
|
|
||||||
<div :class="uploadStatusClasses" v-if="uploadStatus">{{message}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AudioPlayer from './player'
|
import AudioPlayer from './player'
|
||||||
|
import Downloader from './downloader'
|
||||||
import IconButton from './icon-button'
|
import IconButton from './icon-button'
|
||||||
import Recorder from '@/lib/recorder'
|
import Recorder from '@/lib/recorder'
|
||||||
|
import Uploader from './uploader'
|
||||||
|
import UploaderPropsMixin from '@/mixins/uploader-props'
|
||||||
import { convertTimeMMSS } from '@/lib/utils'
|
import { convertTimeMMSS } from '@/lib/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [UploaderPropsMixin],
|
||||||
props: {
|
props: {
|
||||||
attempts : { type: Number },
|
attempts : { type: Number },
|
||||||
compact : { type: Boolean, default: false },
|
time : { type: Number },
|
||||||
time : { type: Number },
|
|
||||||
|
|
||||||
attemptsLimit : { type: Function },
|
showDownloadButton : { type: Boolean, default: true },
|
||||||
micFailed : { type: Function },
|
showUploadButton : { type: Boolean, default: true },
|
||||||
startRecord : { type: Function },
|
|
||||||
stopRecord : { type: Function },
|
|
||||||
|
|
||||||
|
micFailed : { type: Function },
|
||||||
|
beforeRecording : { type: Function },
|
||||||
|
pauseRecording : { type: Function },
|
||||||
|
afterRecording : { type: Function },
|
||||||
failedUpload : { type: Function },
|
failedUpload : { type: Function },
|
||||||
headers : { type: Object },
|
beforeUpload : { type: Function },
|
||||||
startUpload : { type: Function },
|
|
||||||
successfulUpload : { type: Function },
|
successfulUpload : { type: Function },
|
||||||
uploadUrl : { type: String },
|
selectRecord : { type: Function }
|
||||||
|
|
||||||
successfulUploadMsg : { type: String, default: 'Upload successful' },
|
|
||||||
failedUploadMsg : { type: String, default: 'Upload fail' }
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isUploading : false,
|
isUploading : false,
|
||||||
recorder : new Recorder({
|
recorder : this._initRecorder(),
|
||||||
afterStop: () => {
|
recordList : [],
|
||||||
this.recordList = this.recorder.recordList()
|
selected : {},
|
||||||
if (this.stopRecord) {
|
uploadStatus : null,
|
||||||
this.stopRecord('stop record')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
attempts: this.attempts,
|
|
||||||
time: this.time
|
|
||||||
}),
|
|
||||||
recordList : [],
|
|
||||||
selected : {},
|
|
||||||
uploadStatus : null,
|
|
||||||
uploaderOptions : {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
AudioPlayer,
|
AudioPlayer,
|
||||||
IconButton
|
Downloader,
|
||||||
|
IconButton,
|
||||||
|
Uploader
|
||||||
},
|
},
|
||||||
created () {
|
mounted () {
|
||||||
this.uploaderOptions = {
|
|
||||||
failedUpload : this.failedUpload,
|
|
||||||
headers : this.headers,
|
|
||||||
startUpload : this.startUpload,
|
|
||||||
successfulUpload : this.successfulUpload,
|
|
||||||
uploadUrl : this.uploadUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$eventBus.$on('start-upload', () => {
|
this.$eventBus.$on('start-upload', () => {
|
||||||
this.isUploading = true
|
this.isUploading = true
|
||||||
|
this.beforeUpload && this.beforeUpload('before upload')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$eventBus.$on('end-upload', (resp) => {
|
this.$eventBus.$on('end-upload', (msg) => {
|
||||||
this.isUploading = false
|
this.isUploading = false
|
||||||
this.uploadStatus = status
|
|
||||||
setTimeout(() => {this.uploadStatus = null}, 1500)
|
if (msg.status === 'success') {
|
||||||
|
this.successfulUpload && this.successfulUpload(msg.response)
|
||||||
|
} else {
|
||||||
|
this.failedUpload && this.failedUpload(msg.response)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
@@ -297,14 +314,8 @@
|
|||||||
|
|
||||||
if (!this.isRecording || (this.isRecording && this.isPause)) {
|
if (!this.isRecording || (this.isRecording && this.isPause)) {
|
||||||
this.recorder.start()
|
this.recorder.start()
|
||||||
if (this.startRecord) {
|
|
||||||
this.startRecord('start record')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.recorder.pause()
|
this.recorder.pause()
|
||||||
if (this.startRecord) {
|
|
||||||
this.startRecord('pause record')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stopRecorder () {
|
stopRecorder () {
|
||||||
@@ -313,16 +324,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.recorder.stop()
|
this.recorder.stop()
|
||||||
|
this.recordList = this.recorder.recordList()
|
||||||
},
|
},
|
||||||
removeRecord (idx) {
|
removeRecord (idx) {
|
||||||
this.recordList.splice(idx, 1)
|
this.recordList.splice(idx, 1)
|
||||||
this.$set(this.selected, 'url', null)
|
this.$set(this.selected, 'url', null)
|
||||||
this.$eventBus.$emit('remove-record')
|
this.$eventBus.$emit('remove-record')
|
||||||
|
},
|
||||||
|
choiceRecord (record) {
|
||||||
|
if (this.selected === record) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selected = record
|
||||||
|
this.selectRecord && this.selectRecord(record)
|
||||||
|
},
|
||||||
|
_initRecorder () {
|
||||||
|
return new Recorder({
|
||||||
|
beforeRecording : this.beforeRecording,
|
||||||
|
afterRecording : this.afterRecording,
|
||||||
|
pauseRecording : this.pauseRecording,
|
||||||
|
micFailed : this.micFailed
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
attemptsLeft () {
|
attemptsLeft () {
|
||||||
return this.attempts - this.recorder.records.length
|
return this.attempts - this.recordList.length
|
||||||
},
|
},
|
||||||
iconButtonType () {
|
iconButtonType () {
|
||||||
return this.isRecording && this.isPause ? 'mic' : this.isRecording ? 'pause' : 'mic'
|
return this.isRecording && this.isPause ? 'mic' : this.isRecording ? 'pause' : 'mic'
|
||||||
@@ -333,20 +360,12 @@
|
|||||||
isRecording () {
|
isRecording () {
|
||||||
return this.recorder.isRecording
|
return this.recorder.isRecording
|
||||||
},
|
},
|
||||||
message () {
|
|
||||||
return this.uploadStatus === 'success' ? this.successfulUploadMsg : this.failedUploadMsg
|
|
||||||
},
|
|
||||||
recordedTime () {
|
recordedTime () {
|
||||||
if (this.time && this.recorder.duration >= this.time * 60) {
|
if (this.time && this.recorder.duration >= this.time * 60) {
|
||||||
this.stopRecorder()
|
this.stopRecorder()
|
||||||
}
|
}
|
||||||
return convertTimeMMSS(this.recorder.duration)
|
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 () {
|
volume () {
|
||||||
return parseFloat(this.recorder.volume)
|
return parseFloat(this.recorder.volume)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<icon-button name="save" @click.native="upload"/>
|
<icon-button name="save" class="ar-icon ar-icon__xs ar-icon--no-border" @click.native="upload"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import IconButton from './icon-button'
|
import IconButton from './icon-button'
|
||||||
|
import UploaderPropsMixin from '@/mixins/uploader-props'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [UploaderPropsMixin],
|
||||||
props: {
|
props: {
|
||||||
options : { type: Object },
|
record: { type: Object }
|
||||||
record : { type: Object }
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
IconButton
|
IconButton
|
||||||
@@ -25,28 +26,16 @@
|
|||||||
|
|
||||||
this.$eventBus.$emit('start-upload')
|
this.$eventBus.$emit('start-upload')
|
||||||
|
|
||||||
if (this.options.startUpload) {
|
const data = new FormData()
|
||||||
this.options.startUpload()
|
data.append('audio', this.record.blob, `${this.filename}.mp3`)
|
||||||
}
|
|
||||||
|
|
||||||
let data = new FormData()
|
const headers = Object.assign(this.headers, {})
|
||||||
data.append('audio', this.record.blob, 'my-record')
|
|
||||||
|
|
||||||
let headers = Object.assign(this.options.headers, {})
|
|
||||||
headers['Content-Type'] = `multipart/form-data; boundary=${data._boundary}`
|
headers['Content-Type'] = `multipart/form-data; boundary=${data._boundary}`
|
||||||
|
|
||||||
this.$http.post(this.options.uploadUrl, data, { headers: headers }).then(resp => {
|
this.$http.post(this.uploadUrl, data, { headers: headers }).then(resp => {
|
||||||
this.$eventBus.$emit('end-upload', 'success')
|
this.$eventBus.$emit('end-upload', { status: 'success', response: resp })
|
||||||
|
|
||||||
if (this.options.successfulUpload) {
|
|
||||||
this.options.successfulUpload(resp)
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.$eventBus.$emit('end-upload', 'fail')
|
this.$eventBus.$emit('end-upload', { status: 'fail', response: error })
|
||||||
|
|
||||||
if (this.options.failedUpload) {
|
|
||||||
this.options.failedUpload(error)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/lib/encoder.js
Normal file
49
src/lib/encoder.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Mp3Encoder } from 'lamejs'
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
constructor(config) {
|
||||||
|
this.bitRate = config.bitRate || 128
|
||||||
|
this.sampleRate = config.sampleRate || 44100
|
||||||
|
this.dataBuffer = []
|
||||||
|
this.encoder = new Mp3Encoder(1, this.sampleRate, this.bitRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(arrayBuffer) {
|
||||||
|
const maxSamples = 1152
|
||||||
|
const samples = this._convertBuffer(arrayBuffer)
|
||||||
|
let remaining = samples.length
|
||||||
|
|
||||||
|
for (let i = 0; remaining >= 0; i += maxSamples) {
|
||||||
|
const left = samples.subarray(i, i + maxSamples)
|
||||||
|
const buffer = this.encoder.encodeBuffer(left)
|
||||||
|
this.dataBuffer.push(new Int8Array(buffer))
|
||||||
|
remaining -= maxSamples
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
this.dataBuffer.push(this.encoder.flush())
|
||||||
|
const blob = new Blob(this.dataBuffer, { type: 'audio/mp3' })
|
||||||
|
this.dataBuffer = []
|
||||||
|
|
||||||
|
return {
|
||||||
|
id : Date.now(),
|
||||||
|
blob : blob,
|
||||||
|
url : URL.createObjectURL(blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_floatTo16BitPCM(input, output) {
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const s = Math.max(-1, Math.min(1, input[i]))
|
||||||
|
output[i] = (s < 0 ? s * 0x8000 : s * 0x7FFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_convertBuffer(arrayBuffer) {
|
||||||
|
const data = new Float32Array(arrayBuffer)
|
||||||
|
const out = new Int16Array(arrayBuffer.length)
|
||||||
|
this._floatTo16BitPCM(data, out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import WavEncoder from './wav-encoder'
|
import Encoder from './encoder'
|
||||||
import { convertTimeMMSS } from './utils'
|
import { convertTimeMMSS } from './utils'
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
constructor (options = {}) {
|
constructor (options = {}) {
|
||||||
this.afterStop = options.afterStop
|
this.beforeRecording = options.beforeRecording
|
||||||
this.micFailed = options.micFailed
|
this.pauseRecording = options.pauseRecording
|
||||||
|
this.afterRecording = options.afterRecording
|
||||||
|
this.micFailed = options.micFailed
|
||||||
|
|
||||||
this.bufferSize = 4096
|
this.bufferSize = 4096
|
||||||
this.records = []
|
this.records = []
|
||||||
this.samples = []
|
|
||||||
|
|
||||||
this.isPause = false
|
this.isPause = false
|
||||||
this.isRecording = false
|
this.isRecording = false
|
||||||
|
|
||||||
this.duration = 0
|
this.duration = 0
|
||||||
this.volume = 0
|
this.volume = 0
|
||||||
@@ -20,11 +21,23 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start () {
|
start () {
|
||||||
navigator.mediaDevices.getUserMedia({audio: true})
|
const constraints = {
|
||||||
.then(this._micCaptured.bind(this))
|
video: false,
|
||||||
.catch(this._micError.bind(this))
|
audio: {
|
||||||
this.isPause = false
|
channelCount: 1,
|
||||||
|
echoCancellation: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.beforeRecording && this.beforeRecording('start recording')
|
||||||
|
|
||||||
|
navigator.mediaDevices
|
||||||
|
.getUserMedia(constraints)
|
||||||
|
.then(this._micCaptured.bind(this))
|
||||||
|
.catch(this._micError.bind(this))
|
||||||
|
this.isPause = false
|
||||||
this.isRecording = true
|
this.isRecording = true
|
||||||
|
this.lameEncoder = new Encoder({})
|
||||||
}
|
}
|
||||||
|
|
||||||
stop () {
|
stop () {
|
||||||
@@ -33,23 +46,9 @@ export default class {
|
|||||||
this.processor.disconnect()
|
this.processor.disconnect()
|
||||||
this.context.close()
|
this.context.close()
|
||||||
|
|
||||||
let encoder = new WavEncoder({
|
const record = this.lameEncoder.finish()
|
||||||
bufferSize : this.bufferSize,
|
record.duration = convertTimeMMSS(this.duration)
|
||||||
sampleRate : this.context.sampleRate,
|
this.records.push(record)
|
||||||
samples : this.samples
|
|
||||||
})
|
|
||||||
|
|
||||||
let audioBlob = encoder.getData()
|
|
||||||
let audioUrl = URL.createObjectURL(audioBlob)
|
|
||||||
|
|
||||||
this.samples = []
|
|
||||||
|
|
||||||
this.records.push({
|
|
||||||
id : Date.now(),
|
|
||||||
blob : audioBlob,
|
|
||||||
duration : convertTimeMMSS(this.duration),
|
|
||||||
url : audioUrl
|
|
||||||
})
|
|
||||||
|
|
||||||
this._duration = 0
|
this._duration = 0
|
||||||
this.duration = 0
|
this.duration = 0
|
||||||
@@ -57,9 +56,7 @@ export default class {
|
|||||||
this.isPause = false
|
this.isPause = false
|
||||||
this.isRecording = false
|
this.isRecording = false
|
||||||
|
|
||||||
if (this.afterStop) {
|
this.afterRecording && this.afterRecording(record)
|
||||||
this.afterStop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pause () {
|
pause () {
|
||||||
@@ -70,6 +67,8 @@ export default class {
|
|||||||
|
|
||||||
this._duration = this.duration
|
this._duration = this.duration
|
||||||
this.isPause = true
|
this.isPause = true
|
||||||
|
|
||||||
|
this.pauseRecording && this.pauseRecording('pause recording')
|
||||||
}
|
}
|
||||||
|
|
||||||
recordList () {
|
recordList () {
|
||||||
@@ -88,8 +87,10 @@ export default class {
|
|||||||
this.stream = stream
|
this.stream = stream
|
||||||
|
|
||||||
this.processor.onaudioprocess = (ev) => {
|
this.processor.onaudioprocess = (ev) => {
|
||||||
let sample = ev.inputBuffer.getChannelData(0)
|
const sample = ev.inputBuffer.getChannelData(0)
|
||||||
let sum = 0.0
|
let sum = 0.0
|
||||||
|
|
||||||
|
this.lameEncoder.encode(sample)
|
||||||
|
|
||||||
for (let i = 0; i < sample.length; ++i) {
|
for (let i = 0; i < sample.length; ++i) {
|
||||||
sum += sample[i] * sample[i]
|
sum += sample[i] * sample[i]
|
||||||
@@ -97,7 +98,6 @@ export default class {
|
|||||||
|
|
||||||
this.duration = parseFloat(this._duration) + parseFloat(this.context.currentTime.toFixed(2))
|
this.duration = parseFloat(this._duration) + parseFloat(this.context.currentTime.toFixed(2))
|
||||||
this.volume = Math.sqrt(sum / sample.length).toFixed(2)
|
this.volume = Math.sqrt(sum / sample.length).toFixed(2)
|
||||||
this.samples.push(new Float32Array(sample))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.input.connect(this.processor)
|
this.input.connect(this.processor)
|
||||||
@@ -105,8 +105,6 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_micError (error) {
|
_micError (error) {
|
||||||
if (this.micFailed) {
|
this.micFailed && this.micFailed(error)
|
||||||
this.micFailed(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export function calculateLineHeadPosition (ev, element) {
|
export function calculateLineHeadPosition (ev, element) {
|
||||||
let progressWidth = element.getBoundingClientRect().width
|
const progressWidth = element.getBoundingClientRect().width
|
||||||
let leftPosition = ev.target.getBoundingClientRect().left
|
const leftPosition = ev.target.getBoundingClientRect().left
|
||||||
let pos = (ev.clientX - leftPosition) / progressWidth
|
let pos = (ev.clientX - leftPosition) / progressWidth
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
src/mixins/uploader-props.js
Normal file
7
src/mixins/uploader-props.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
filename : { type: String, default: 'record' },
|
||||||
|
headers : { type: Object },
|
||||||
|
uploadUrl : { type: String }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: .2s;
|
transition: .2s;
|
||||||
|
|
||||||
|
&--no-border {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&--rec {
|
&--rec {
|
||||||
fill: white;
|
fill: white;
|
||||||
background-color: #FF6B64;
|
background-color: #FF6B64;
|
||||||
@@ -15,7 +21,6 @@
|
|||||||
|
|
||||||
&--pulse {
|
&--pulse {
|
||||||
animation: ripple .5s linear infinite;
|
animation: ripple .5s linear infinite;
|
||||||
|
|
||||||
@keyframes ripple {
|
@keyframes ripple {
|
||||||
0% {
|
0% {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -32,14 +37,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__xs {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
&__sm {
|
&__sm {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__lg {
|
&__lg {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
|
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user