Initial implementation of "audio mode"
This commit is contained in:
parent
d5942fdb8e
commit
09173a65f1
14 changed files with 944 additions and 410 deletions
406
src/components/AudioLayout.vue
Normal file
406
src/components/AudioLayout.vue
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
<template>
|
||||
<div v-bind="{...$props, ...$attrs}" v-on="$listeners" class="messageIn">
|
||||
<div class="load-earlier clickable" @click="loadPrevious">
|
||||
<v-icon color="white" size="28">expand_less</v-icon>
|
||||
</div>
|
||||
<div class="sound-wave-view">
|
||||
<div class="volume-container">
|
||||
<div ref="volume"></div>
|
||||
</div>
|
||||
<v-avatar v-if="currentAudioEvent" class="avatar" ref="avatar" size="32" color="#ededed"
|
||||
@click.stop="otherAvatarClicked($refs.avatar.$el)">
|
||||
<img v-if="messageEventAvatar(currentAudioEvent)" :src="messageEventAvatar(currentAudioEvent)" />
|
||||
<span v-else class="white--text headline">{{
|
||||
eventSenderDisplayName(currentAudioEvent).substring(0, 1).toUpperCase()
|
||||
}}</span>
|
||||
</v-avatar>
|
||||
</div>
|
||||
<div v-if="currentAudioEvent" class="senderAndTime">
|
||||
<div class="sender">{{ eventSenderDisplayName(currentAudioEvent) }}</div>
|
||||
<div class="time">
|
||||
{{ formatTime(currentAudioEvent.event.origin_server_ts) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="play-time">
|
||||
{{ currentTime }} / {{ totalTime }}
|
||||
</div>
|
||||
<audio ref="player" :src="src" @durationchange="updateDuration">
|
||||
{{ $t('fallbacks.audio_file')}}
|
||||
</audio>
|
||||
<div v-if="currentAudioEvent" class="auto-audio-player">
|
||||
<v-btn id="btn-rewind" @click.stop="rewind" icon>
|
||||
<v-icon size="28">$vuetify.icons.rewind</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="playing" id="btn-pause" @click.stop="pause" icon>
|
||||
<v-icon size="56">$vuetify.icons.pause_circle</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else id="btn-play" @click.stop="play" icon>
|
||||
<v-icon size="56">$vuetify.icons.play_circle</v-icon>
|
||||
</v-btn>
|
||||
<v-btn id="btn-forward" @click.stop="forward" icon>
|
||||
<v-icon size="28">$vuetify.icons.forward</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="load-later">
|
||||
<v-btn v-if="canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v-blur
|
||||
@click.stop="$emit('start-recording')">
|
||||
<v-icon color="white">mic</v-icon>
|
||||
</v-btn>
|
||||
<v-icon class="clickable" @click="loadNext" color="white" size="28">expand_more</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "./messages/messageMixin";
|
||||
import util from "../plugins/utils";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: {},
|
||||
props: {
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return true
|
||||
}
|
||||
},
|
||||
events: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
readMarker: {
|
||||
type: String,
|
||||
default: function () {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
src: null,
|
||||
currentAudioEvent: null,
|
||||
autoPlayNextEvent: false,
|
||||
currentAudioSource: null,
|
||||
player: null,
|
||||
duration: 0,
|
||||
playPercent: 0,
|
||||
playTime: 0,
|
||||
playing: false,
|
||||
analyzer: null,
|
||||
analyzerDataArray: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.body.classList.add("dark");
|
||||
this.$root.$on('playback-start', this.onPlaybackStart);
|
||||
this.player = this.$refs.player;
|
||||
this.player.addEventListener("timeupdate", this.updateProgressBar);
|
||||
this.player.addEventListener("play", () => {
|
||||
this.playing = true;
|
||||
this.updateVisualization();
|
||||
if (this.currentAudioEvent) {
|
||||
this.$emit("mark-read", this.currentAudioEvent.getId(), this.currentAudioEvent.getId());
|
||||
}
|
||||
});
|
||||
this.player.addEventListener("pause", () => {
|
||||
this.playing = false;
|
||||
this.clearVisualization();
|
||||
});
|
||||
this.player.addEventListener("ended", () => {
|
||||
this.pause();
|
||||
this.playing = false;
|
||||
this.clearVisualization();
|
||||
this.onPlaybackEnd();
|
||||
});
|
||||
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
let audioSource = null;
|
||||
if (audioCtx) {
|
||||
audioSource = audioCtx.createMediaElementSource(this.player);
|
||||
this.analyser = audioCtx.createAnalyser();
|
||||
audioSource.connect(this.analyser);
|
||||
this.analyser.connect(audioCtx.destination);
|
||||
|
||||
this.analyser.fftSize = 128;
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.analyzerDataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.classList.remove("dark");
|
||||
this.currentAudioEvent = null;
|
||||
this.loadAudioAttachmentSource(); // Release
|
||||
this.$root.$off('playback-start', this.onPlaybackStart);
|
||||
},
|
||||
computed: {
|
||||
canRecordAudio() {
|
||||
if (this.room) {
|
||||
const myUserId = this.$matrix.currentUserId;
|
||||
const me = this.room.getMember(myUserId);
|
||||
return me && me.powerLevelNorm > 0 && util.browserCanRecordAudio();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
currentTime() {
|
||||
return util.formatDuration(this.playTime);
|
||||
},
|
||||
totalTime() {
|
||||
return util.formatDuration(this.duration);
|
||||
},
|
||||
playheadPercent: {
|
||||
get: function () {
|
||||
return this.playPercent;
|
||||
},
|
||||
set: function (percent) {
|
||||
if (this.player.src) {
|
||||
this.playPercent = percent;
|
||||
this.player.currentTime = (percent / 100) * this.player.duration;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
autoplay: {
|
||||
immediate: true,
|
||||
handler(autoplay, ignoredOldValue) {
|
||||
if (!autoplay) {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
},
|
||||
events: {
|
||||
immediate: true,
|
||||
handler(events, ignoredOldValue) {
|
||||
console.log("Events changed", this.currentAudioEvent, this.autoPlayNextEvent);
|
||||
if (!this.currentAudioEvent || this.autoPlayNextEvent) {
|
||||
// Make sure all events are decrypted!
|
||||
const eventsBeingDecrypted = events.filter((e) => e.isBeingDecrypted());
|
||||
if (eventsBeingDecrypted.length > 0) {
|
||||
console.log("All not decrypted, wait");
|
||||
Promise.allSettled(eventsBeingDecrypted.map((e) => e.getDecryptionPromise())).then(() => {
|
||||
console.log("DONE DECRYPTING!")
|
||||
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
||||
});
|
||||
} else {
|
||||
console.log("All decrypted, load next");
|
||||
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
currentAudioEvent: {
|
||||
immediate: true,
|
||||
handler(value, oldValue) {
|
||||
console.log("Current audio derom", value, oldValue);
|
||||
if (value && oldValue && value.getId && oldValue.getId && value.getId() === oldValue.getId()) {
|
||||
console.log("Ignoring change!!!");
|
||||
return;
|
||||
}
|
||||
if (!value || !value.getId) {
|
||||
return;
|
||||
}
|
||||
this.src = null;
|
||||
this.autoPlayNextEvent = false;
|
||||
this.loadAudioAttachmentSource();
|
||||
}
|
||||
},
|
||||
src: {
|
||||
immediate: true,
|
||||
handler(value, ignoredOldValue) {
|
||||
console.log("Source changed to", value, ignoredOldValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
if (this.player.src) {
|
||||
this.$root.$emit("playback-start", this);
|
||||
if (this.player.paused) {
|
||||
this.player.play();
|
||||
} else if (this.player.ended) {
|
||||
// restart
|
||||
this.player.currentTime = 0;
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
this.player.autoplay = false;
|
||||
if (this.player.src) {
|
||||
this.player.pause();
|
||||
}
|
||||
},
|
||||
rewind() {
|
||||
if (this.player.src) {
|
||||
this.player.currentTime = Math.max(0, this.player.currentTime - 15);
|
||||
}
|
||||
},
|
||||
forward() {
|
||||
if (this.player.src) {
|
||||
this.player.currentTime = Math.min(this.player.duration, this.player.currentTime + 15);
|
||||
}
|
||||
},
|
||||
updateProgressBar() {
|
||||
if (this.player.duration > 0) {
|
||||
this.playPercent = Math.floor(
|
||||
(100 / this.player.duration) * this.player.currentTime
|
||||
);
|
||||
} else {
|
||||
this.playPercent = 0;
|
||||
}
|
||||
this.playTime = 1000 * this.player.currentTime;
|
||||
},
|
||||
updateDuration() {
|
||||
this.duration = 1000 * this.player.duration;
|
||||
},
|
||||
onPlaybackStart(item) {
|
||||
this.player.autoplay = false;
|
||||
if (item != this && this.playing) {
|
||||
this.pause();
|
||||
}
|
||||
},
|
||||
onPlaybackEnd() {
|
||||
this.loadNext(true && this.autoplay);
|
||||
},
|
||||
loadPrevious() {
|
||||
const audioMessages = this.events.filter((e) => e.getContent().msgtype === "m.audio");
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
const e = audioMessages[i];
|
||||
if (this.currentAudioEvent && e.getId() === this.currentAudioEvent.getId()) {
|
||||
if (i > 0) {
|
||||
this.pause();
|
||||
this.currentAudioEvent = audioMessages[i - 1];
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.$emit("loadprevious");
|
||||
},
|
||||
loadNext(autoplay = false) {
|
||||
const audioMessages = this.events.filter((e) => e.getContent().msgtype === "m.audio");
|
||||
if (audioMessages.length == 0) {
|
||||
// Try to load earlier
|
||||
this.$emit("loadprevious");
|
||||
return;
|
||||
}
|
||||
if (!this.currentAudioEvent) {
|
||||
// Figure out which audio event to start with, i.e. our "read marker"
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
const e = audioMessages[i];
|
||||
if (e.getId() === this.readMarker) {
|
||||
if (i < (audioMessages.length - 1)) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = audioMessages[i + 1];
|
||||
} else {
|
||||
this.autoPlayNextEvent = true;
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = e;
|
||||
this.$emit("loadnext");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No read marker found. Just use the first event here...
|
||||
if (audioMessages.length > 0) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = audioMessages[0];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < audioMessages.length; i++) {
|
||||
const e = audioMessages[i];
|
||||
if (e.getId() === this.currentAudioEvent.getId()) {
|
||||
if (i < (audioMessages.length - 1)) {
|
||||
this.pause();
|
||||
this.player.autoplay = autoplay;
|
||||
this.currentAudioEvent = audioMessages[i + 1];
|
||||
} else {
|
||||
this.autoPlayNextEvent = true;
|
||||
this.player.autoplay = autoplay;
|
||||
this.$emit("loadnext");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateVisualization() {
|
||||
|
||||
const volume = this.$refs.volume;
|
||||
if (volume && this.analyser) {
|
||||
const volumeContainer = volume.parentElement;
|
||||
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
|
||||
this.analyser.getByteFrequencyData(this.analyzerDataArray);
|
||||
var value = 0;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
value += this.analyzerDataArray[i];
|
||||
}
|
||||
value = value / bufferLength;
|
||||
const avatarWidth = 1.1 * this.$refs.avatar ? this.$refs.avatar.clientWidth : 104;
|
||||
const range = Math.max(0, (volumeContainer.clientWidth - avatarWidth));
|
||||
const w = avatarWidth + (value * range) / 256;
|
||||
volume.style.width = "" + w + "px";
|
||||
volume.style.height = "" + w + "px";
|
||||
const color = 80 + (value * (256 - 80)) / 256;
|
||||
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
|
||||
|
||||
if (this.playing) {
|
||||
requestAnimationFrame(this.updateVisualization);
|
||||
} else {
|
||||
this.clearVisualization();
|
||||
}
|
||||
}
|
||||
},
|
||||
clearVisualization() {
|
||||
const volume = this.$refs.volume;
|
||||
volume.style.width = "0px";
|
||||
volume.style.height = "0px";
|
||||
volume.style.backgroundColor = "transparent";
|
||||
},
|
||||
loadAudioAttachmentSource() {
|
||||
console.log("loadAUto");
|
||||
if (this.src) {
|
||||
const objectUrl = this.src;
|
||||
this.src = null;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
if (this.currentAudioEvent) {
|
||||
console.log("Will load");
|
||||
if (this.currentAudioSource) {
|
||||
this.currentAudioSource.reject("Aborted");
|
||||
}
|
||||
this.currentAudioSource =
|
||||
util
|
||||
.getAttachment(this.$matrix.matrixClient, this.currentAudioEvent, (progress) => {
|
||||
this.downloadProgress = progress;
|
||||
})
|
||||
.then((url) => {
|
||||
console.log("Loaded", url);
|
||||
this.src = url;
|
||||
this.currentAudioSource = null;
|
||||
this.$nextTick(() => {
|
||||
this.player.load();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to fetch attachment: ", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue