Start on voice recording

This commit is contained in:
N-Pex 2021-02-22 16:34:19 +01:00
parent 3aef5b6b3e
commit fd86e753fe
7 changed files with 335 additions and 2 deletions

108
package-lock.json generated
View file

@ -1,11 +1,11 @@
{ {
"name": "keanuapp-weblite", "name": "keanuapp-weblite",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.1.0", "version": "0.1.1",
"dependencies": { "dependencies": {
"aes-js": "^3.1.2", "aes-js": "^3.1.2",
"axios": "^0.21.0", "axios": "^0.21.0",
@ -16,6 +16,7 @@
"json-web-key": "^0.4.0", "json-web-key": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1", "material-design-icons-iconfont": "^5.0.1",
"matrix-js-sdk": "^9.4.1", "matrix-js-sdk": "^9.4.1",
"mic-recorder-to-mp3": "^2.2.2",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
@ -8121,6 +8122,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/lamejs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/lamejs/-/lamejs-1.2.0.tgz",
"integrity": "sha1-Aln4PbRmYUGntnG4yqY2nZUXfQg=",
"dependencies": {
"use-strict": "1.0.1"
}
},
"node_modules/launch-editor": { "node_modules/launch-editor": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz",
@ -8490,6 +8499,17 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mic-recorder-to-mp3": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/mic-recorder-to-mp3/-/mic-recorder-to-mp3-2.2.2.tgz",
"integrity": "sha512-xDkOaHbojW3bdKOGn9CI5dT+Mc0RrfczsX/Y1zGJp3FUB4zei5ZKFnNm7Nguc9v910wkd7T3csnCTq5EtCF3Zw==",
"dependencies": {
"lamejs": "^1.2.0"
},
"peerDependencies": {
"webrtc-adapter": ">=4.1.1"
}
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@ -11376,6 +11396,19 @@
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==" "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
}, },
"node_modules/rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"peer": true,
"dependencies": {
"sdp": "^2.6.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/run-async": { "node_modules/run-async": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@ -11508,6 +11541,12 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/sdp": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==",
"peer": true
},
"node_modules/select": { "node_modules/select": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -13280,6 +13319,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/use-strict": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/use-strict/-/use-strict-1.0.1.tgz",
"integrity": "sha1-C7gNlPSaSgUZK4Sox9NOlfGn46A="
},
"node_modules/util": { "node_modules/util": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@ -14604,6 +14648,20 @@
"webpack": "^4.0.0" "webpack": "^4.0.0"
} }
}, },
"node_modules/webrtc-adapter": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.0.tgz",
"integrity": "sha512-7Bp9OBnx642oJRkom1tNAbeJjUadAq2rh5xLL9YXPw5hVyt2h4hHr5bcoPYDs1stp/mZHSPSQA34YISdnr0DBQ==",
"peer": true,
"dependencies": {
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.12.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/websocket-driver": { "node_modules/websocket-driver": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
@ -21529,6 +21587,14 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
}, },
"lamejs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/lamejs/-/lamejs-1.2.0.tgz",
"integrity": "sha1-Aln4PbRmYUGntnG4yqY2nZUXfQg=",
"requires": {
"use-strict": "1.0.1"
}
},
"launch-editor": { "launch-editor": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz",
@ -21842,6 +21908,14 @@
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"dev": true "dev": true
}, },
"mic-recorder-to-mp3": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/mic-recorder-to-mp3/-/mic-recorder-to-mp3-2.2.2.tgz",
"integrity": "sha512-xDkOaHbojW3bdKOGn9CI5dT+Mc0RrfczsX/Y1zGJp3FUB4zei5ZKFnNm7Nguc9v910wkd7T3csnCTq5EtCF3Zw==",
"requires": {
"lamejs": "^1.2.0"
}
},
"micromatch": { "micromatch": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@ -24192,6 +24266,15 @@
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==" "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
}, },
"rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"peer": true,
"requires": {
"sdp": "^2.6.0"
}
},
"run-async": { "run-async": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@ -24280,6 +24363,12 @@
"ajv-keywords": "^3.5.2" "ajv-keywords": "^3.5.2"
} }
}, },
"sdp": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==",
"peer": true
},
"select": { "select": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -25742,6 +25831,11 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
}, },
"use-strict": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/use-strict/-/use-strict-1.0.1.tgz",
"integrity": "sha1-C7gNlPSaSgUZK4Sox9NOlfGn46A="
},
"util": { "util": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@ -26785,6 +26879,16 @@
} }
} }
}, },
"webrtc-adapter": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.0.tgz",
"integrity": "sha512-7Bp9OBnx642oJRkom1tNAbeJjUadAq2rh5xLL9YXPw5hVyt2h4hHr5bcoPYDs1stp/mZHSPSQA34YISdnr0DBQ==",
"peer": true,
"requires": {
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.12.0"
}
},
"websocket-driver": { "websocket-driver": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",

View file

@ -17,6 +17,7 @@
"json-web-key": "^0.4.0", "json-web-key": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1", "material-design-icons-iconfont": "^5.0.1",
"matrix-js-sdk": "^9.4.1", "matrix-js-sdk": "^9.4.1",
"mic-recorder-to-mp3": "^2.2.2",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",

View file

@ -597,4 +597,19 @@ $admin-fg: white;
width: 100%; width: 100%;
} }
} }
}
.voice-recorder {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: black;
&.will-cancel {
background-color: grey;
}
.recording-time {
color: white;
}
} }

View file

@ -154,6 +154,21 @@
/> />
</v-col> </v-col>
<v-col
class="input-area-button text-center flex-grow-0 flex-shrink-1"
>
<v-btn
fab
small
elevation="0"
color="transparent"
v-blur
style="z-index:10"
@mousedown.stop="startRecording"
>
<v-icon :color="showRecorder ? 'white' : 'black'">mic</v-icon>
</v-btn>
</v-col>
<v-col <v-col
class="input-area-button text-center flex-grow-0 flex-shrink-1" class="input-area-button text-center flex-grow-0 flex-shrink-1"
v-if="editedEvent || replyToEvent" v-if="editedEvent || replyToEvent"
@ -183,6 +198,7 @@
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
<VoiceRecorder :show="showRecorder" v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
</v-container> </v-container>
<div v-if="currentImageInput"> <div v-if="currentImageInput">
@ -271,6 +287,7 @@ import DebugEvent from "./messages/DebugEvent.vue";
import util from "../plugins/utils"; import util from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue"; import MessageOperations from "./messages/MessageOperations.vue";
import ChatHeader from "./ChatHeader"; import ChatHeader from "./ChatHeader";
import VoiceRecorder from "./VoiceRecorder";
const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */ const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
@ -320,6 +337,7 @@ export default {
RoomAvatarChanged, RoomAvatarChanged,
DebugEvent, DebugEvent,
MessageOperations, MessageOperations,
VoiceRecorder
}, },
data() { data() {
@ -341,6 +359,7 @@ export default {
showContextMenu: false, showContextMenu: false,
showContextMenuAnchor: null, showContextMenuAnchor: null,
initialLoadDone: false, initialLoadDone: false,
showRecorder: false,
/** /**
* Current chat container size. We need to keep track of this so that if and when * Current chat container size. We need to keep track of this so that if and when
@ -1061,6 +1080,19 @@ export default {
dayForEvent(event) { dayForEvent(event) {
return util.formatDay(event.getTs()); return util.formatDay(event.getTs());
}, },
startRecording() {
this.showRecorder = true;
},
onVoiceRecording(event) {
util.sendImage(
this.$matrix.matrixClient,
this.roomId,
event.file,
undefined
);
}
}, },
}; };
</script> </script>

View file

@ -0,0 +1,163 @@
<template>
<div
v-show="show"
:class="{ 'voice-recorder': true, 'will-cancel': willCancel }"
ref="vr_root"
>
<v-container fluid fill-height>
<v-row align="center" justify="center">
<v-col class="text-center">
<div class="recording-time">
{{ recordingTime }}
</div>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
const State = {
INITIAL: "intial",
RECORDING: "recording",
RECORDED: "recorded",
ERROR: "error",
};
import util from "../plugins/utils";
export default {
name: "VoiceRecorder",
props: {
show: {
type: Boolean,
default: function () {
return false;
},
},
},
data() {
return {
willCancel: false,
states: State,
state: State.INITIAL,
recordStartedAt: null,
recordingTime: null,
recordTimer: null,
};
},
watch: {
show(val) {
if (val) {
// Add listeners
document.addEventListener("mouseup", this.mouseUp, false);
document.addEventListener("mousemove", this.mouseMove, false);
this.startRecording();
} else {
// Remove listeners
document.removeEventListener("mouseup", this.mouseUp, false);
document.removeEventListener("mousemove", this.mouseMove, false);
}
},
},
methods: {
close() {
this.stopRecordTimer();
this.$emit("close");
},
mouseUp(ignoredEvent) {
document.removeEventListener("mouseup", this.mouseUp, false);
document.removeEventListener("mousemove", this.mouseMove, false);
document.body.style.cursor = "";
if (this.willCancel) {
this.cancelRecording();
} else {
this.stopRecording();
}
this.close();
},
mouseMove(event) {
document.body.style.cursor = "ns-resize";
let rect = this.$refs.vr_root.getBoundingClientRect();
this.willCancel = event.clientY < rect.top;
console.log(
"Cancel: " + this.willCancel + " " + event.clientY + " " + rect.top
);
// let bottom = rect.bottom;
// let mouseBottom = event.clientY;
// let newHeight = Math.max(
// 200,
// Math.min(0.7 * window.innerHeight, bottom - mouseBottom)
// );
// this.$refs.chatPane.style.height = newHeight + "px";
event.preventDefault();
event.stopPropagation();
},
startRecording() {
const MicRecorder = require("mic-recorder-to-mp3");
// 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);
//this.state = State.ERROR;
});
},
cancelRecording() {
this.state = State.INITIAL;
this.recorder.stop();
this.stopRecordTimer();
},
stopRecording() {
this.state = State.RECORDED;
this.stopRecordTimer();
this.recorder
.stop()
.getMp3()
.then(([buffer, blob]) => {
// do what ever you want with buffer and blob
// Example: Create a mp3 file and play
const file = new File(buffer, util.formatRecordStartTime(this.recordStartedAt) + ".mp3", {
type: blob.type,
lastModified: Date.now(),
});
this.$emit("file", {file: file});
// const player = new Audio(URL.createObjectURL(file));
// player.play();
})
.catch((e) => {
alert("We could not retrieve your message");
console.log(e);
});
},
startRecordTimer() {
this.stopRecordTimer();
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;
this.recordingTime = null;
}
},
},
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -32,6 +32,13 @@ Vue.use((Vue) => {
}; };
}); });
// Register a global custom directive called `v-blur` that prevents focus
Vue.directive('blur', {
inserted: function (el) {
el.onfocus = (ev) => ev.target.blur()
}
});
Vue.use(navigation, router); Vue.use(navigation, router);
new Vue({ new Vue({

View file

@ -8,6 +8,8 @@ var base64Url = require('json-web-key/lib/base64url');
// Install extended localized format // Install extended localized format
var localizedFormat = require('dayjs/plugin/localizedFormat') var localizedFormat = require('dayjs/plugin/localizedFormat')
dayjs.extend(localizedFormat) dayjs.extend(localizedFormat)
var duration = require('dayjs/plugin/duration')
dayjs.extend(duration);
class Util { class Util {
getAttachment(matrixClient, event) { getAttachment(matrixClient, event) {
@ -443,6 +445,15 @@ class Util {
return then.format('L'); return then.format('L');
} }
} }
formatRecordDuration(ms) {
return dayjs.duration(ms).format("HH:mm:ss");
}
formatRecordStartTime(timestamp) {
var then = dayjs(timestamp);
return then.format('lll');
}
} }
export default new Util(); export default new Util();