keanu-weblite/src/components/VoiceRecorder.vue

440 lines
13 KiB
Vue

<template>
<transition name="grow" mode="out-in">
<div
v-show="show"
:class="{ 'voice-recorder': true, ptt: ptt, row: !ptt }"
ref="vrroot"
>
<v-container v-if="!ptt" fluid fill-height>
<v-row align="center" class="mt-3">
<v-col cols="4" align="center">
<v-btn v-show="state == states.RECORDED" icon @click.stop="redo">
<v-icon color="white">undo</v-icon>
</v-btn>
</v-col>
<v-col cols="4" align="center">
<v-btn
v-if="state == states.RECORDING"
id="btn-pause"
icon
class="voice-recorder-btn recording"
@click.stop="pauseRecording"
>
<v-icon color="white">stop</v-icon>
</v-btn>
<v-btn
v-else-if="state == states.RECORDED"
id="btn-send"
class="voice-recorder-btn recorded"
icon
:disabled="!recordedFile"
@click.stop="send"
>
<v-icon color="black">arrow_upward</v-icon>
</v-btn>
<v-btn
v-else
id="btn-record"
class="voice-recorder-btn"
icon
@click.stop="startRecording"
>
<v-icon color="white">fiber_manual_record</v-icon>
</v-btn>
</v-col>
<v-col cols="4" align="center">
<v-btn id="btn-record-cancel" icon @click.stop="cancelRecording">
<v-icon color="white">close</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
<v-container fluid fill-height>
<v-row align="center">
<v-col cols="3">
<div class="recording-time">
{{ recordingTime }}
</div>
</v-col>
<v-col cols="6" v-if="ptt">
<div class="swipe-info">
&lt;&lt; {{ $t("voice_recorder.swipe_to_cancel") }}
</div>
</v-col>
</v-row>
</v-container>
<transition name="fade" mode="out-in">
<div
v-if="willCancel"
class="will-cancel"
>
<v-container fluid fill-height>
<v-row align="center">
<v-col cols="3">
<v-icon color="white">delete_outline</v-icon>
</v-col>
<v-col cols="6">
<div class="swipe-info">
{{ $t("voice_recorder.release_to_cancel") }}
</div>
</v-col>
</v-row>
</v-container>
</div>
</transition>
<transition name="fade" mode="out-in">
<div
v-if="recordingLocked"
class="locked"
>
<v-container fluid fill-height>
<v-row align="center">
<v-col cols="3">
<div class="recording-time">
{{ recordingTime }}
</div>
</v-col>
<v-col cols="3">
<v-btn id="btn-record-cancel" @click.stop="cancelRecording" text class="swipe-info">{{
$t("menu.cancel")
}}</v-btn>
</v-col>
<v-col cols="3">
<v-btn id="btn-record-stop" @click.stop="stopRecording" icon class="swipe-info"
><v-icon color="white">stop</v-icon></v-btn
>
</v-col>
</v-row>
</v-container>
</div>
</transition>
<div
v-if="state == states.ERROR"
class="error"
>
<v-container fluid fill-height>
<v-row align="center">
<v-col>
<div class="swipe-info">
{{ errorMessage || $t("voice_recorder.failed_to_record") }}
</div>
</v-col>
<v-col align="right">
<v-btn id="btn-record-cancel" icon @click.stop="cancelRecording">
<v-icon color="white">close</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</div>
<VoiceRecorderLock
v-show="state == states.RECORDING && ptt"
:style="lockButtonStyle"
:isLocked="recordingLocked"
/>
</div>
</transition>
</template>
<script>
const State = {
INITIAL: "intial",
RECORDING: "recording",
RECORDED: "recorded",
ERROR: "error",
};
import util from "../plugins/utils";
import VoiceRecorderLock from "./VoiceRecorderLock";
require("md-gum-polyfill");
import MicRecorder from "mic-recorder-to-mp3";
import ysFixWebmDuration from "fix-webm-duration";
//import { duration } from "dayjs";
export default {
name: "VoiceRecorder",
components: {
VoiceRecorderLock,
},
props: {
show: {
type: Boolean,
default: function() {
return false;
},
},
ptt: {
type: Boolean,
default: function() {
return false;
},
},
micButtonRef: {
type: Object,
default: function() {
return null;
},
},
},
data() {
return {
willCancel: false,
/** Starting X coordinate of dragging operations */
startCoordinateX: null,
startCoordinateY: null,
states: State,
state: State.INITIAL,
recordStartedAt: null,
recordingTime: String.fromCharCode(160), // nbsp!
recordTimer: null,
recordingLocked: false,
recordedFile: null,
errorMessage: null,
recorder: null
};
},
watch: {
micButtonRef(buttonRef) {
if (buttonRef) {
var r = buttonRef.$el.getBoundingClientRect();
var left = r.left;
var width = r.right - r.left;
r = this.$refs.vrroot.parentElement.getBoundingClientRect();
var widthParent = r.right - r.left;
document.documentElement.style.setProperty(
"--v-mic-button-left",
left + "px"
);
document.documentElement.style.setProperty(
"--v-mic-button-width",
width + "px"
);
document.documentElement.style.setProperty(
"--v-mic-button-container-width",
widthParent + "px"
);
var initialScale = width / widthParent;
document.documentElement.style.setProperty(
"--v-mic-button-initial-scale",
initialScale
);
var initialTranslate = left + width / 2 - widthParent / 2;
document.documentElement.style.setProperty(
"--v-mic-button-initial-translate",
initialTranslate + "px"
);
}
},
show(val) {
if (val) {
// Add listeners
this.state = State.INITIAL;
this.errorMessage = null;
this.recordedFile = null;
if (this.ptt) {
document.addEventListener("mouseup", this.mouseUp, false);
document.addEventListener("mousemove", this.mouseMove, false);
document.addEventListener("touchend", this.mouseUp, false);
document.addEventListener("touchmove", this.mouseMove, false);
this.startRecording();
} else {
console.log("Not PTT");
//eslint-disable-next-line
this.micButtonRef.$el.style.display = "none";
}
} else {
// Remove listeners
document.removeEventListener("mouseup", this.mouseUp, false);
document.removeEventListener("mousemove", this.mouseMove, false);
document.removeEventListener("touchend", this.mouseUp, false);
document.removeEventListener("touchmove", this.mouseMove, false);
this.startCoordinateX = null;
this.startCoordinateY = null;
this.willCancel = false;
this.startCoordinateX = null;
this.startCoordinateY = null;
this.recordingLocked = false;
//eslint-disable-next-line
this.micButtonRef.$el.style.display = "block";
}
},
},
computed: {
lockButtonStyle() {
/**
Calculate where to show the lock button (it should be at the same X-coord as the)
mic button (given as a reference!)
*/
var left = 0;
var width = 20;
if (this.micButtonRef) {
var r = this.micButtonRef.$el.getBoundingClientRect();
left = r.left;
width = r.right - r.left;
}
const s =
"position:absolute;top:-50px;left:" +
left +
"px;width:" +
width +
"px;height:40px";
return s;
},
},
methods: {
close() {
this.stopRecordTimer();
this.recordingTime = String.fromCharCode(160); // nbsp;
this.$emit("close");
},
mouseUp(ignoredEvent) {
document.removeEventListener("mouseup", this.mouseUp, false);
document.removeEventListener("mousemove", this.mouseMove, false);
document.removeEventListener("touchend", this.mouseUp, false);
document.removeEventListener("touchmove", this.mouseMove, false);
//document.body.style.cursor = "";
if (this.state == State.RECORDING) {
if (!this.recordingLocked) {
if (this.willCancel) {
this.cancelRecording();
} else {
this.stopRecording();
}
}
} else {
this.cancelRecording();
}
},
mouseMove(event) {
var x = event.clientX;
var y = event.clientY;
if (event.touches && event.touches.length > 0) {
x = event.touches[0].clientX;
y = event.touches[0].clientY;
}
if (!this.startCoordinateX) {
// First move, set it!
this.startCoordinateX = x;
this.startCoordinateY = y;
}
//document.body.style.cursor = "ns-resize";
this.willCancel = x < this.startCoordinateX - 30;
if (y < this.startCoordinateY - 30 && !this.willCancel) {
this.recordingLocked = true;
}
event.preventDefault();
event.stopPropagation();
},
startRecording() {
// Start recording. Browser will request permission to use your microphone.
this.recorder = new MicRecorder({
bitRate: 128,
});
this.recorder
.start()
.then(() => {
this.state = State.RECORDING;
this.recordStartedAt = Date.now();
this.startRecordTimer();
})
.catch((e) => {
console.error(e);
if (e && e.name == "NotAllowedError") {
this.errorMessage = e.message;
}
this.state = State.ERROR;
});
},
cancelRecording() {
this.state = State.INITIAL;
this.recorder.stop();
this.recorder = null;
this.close();
},
pauseRecording() {
this.state = State.RECORDED;
this.stopRecordTimer();
this.getFile(false);
},
stopRecording() {
this.state = State.RECORDED;
this.stopRecordTimer();
this.recordingTime = String.fromCharCode(160); // nbsp;
this.close();
this.getFile(true);
},
redo() {
this.state = State.INITIAL;
this.recordedFile = null;
this.recordingTime = String.fromCharCode(160); // nbsp;
},
send() {
this.$emit("file", { file: this.recordedFile });
},
getFile(send) {
//const duration = Date.now() - this.recordStartedAt;
this.recorder
.stop()
.getMp3()
.then(([buffer, blob]) => {
// do what ever you want with buffer and blob
// Example: Create a mp3 file and play
this.recordedFile = new File(
buffer,
util.formatRecordStartTime(this.recordStartedAt) + ".mp3",
{
type: blob.type,
lastModified: Date.now(),
}
);
if (send) {
this.send();
}
});
},
startRecordTimer() {
this.stopRecordTimer();
this.recordingTime = String.fromCharCode(160); // nbsp;
this.recordTimer = setInterval(() => {
const now = Date.now();
this.recordingTime = util.formatRecordDuration(
now - this.recordStartedAt
);
}, 500);
},
stopRecordTimer() {
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
},
/*
* There is an issue with browsers not setting correct metadata in the generated webm file.
* See here: https://bugs.chromium.org/p/chromium/issues/detail?id=642012
* Use fix-webm-duration package to try to update the cues section.
*/
async correctMetadata(blob, duration) {
return new Promise((resolve, reject) => {
try {
ysFixWebmDuration(blob, duration, function(fixedBlob) {
const b = new Blob([fixedBlob], { type: blob.type });
resolve(b);
});
} catch (err) {
console.error(err);
reject(err);
}
});
},
},
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
@import "@/assets/css/components/voice-recorder.scss";
</style>