Resolve "for chat mode, auto-play next audio message"

This commit is contained in:
N Pex 2023-05-26 15:56:59 +00:00
parent f49d374a76
commit daa52be9c0
11 changed files with 455 additions and 252 deletions

View file

@ -37,20 +37,18 @@
<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-btn id="btn-rewind" :disabled="!info || info.loading" @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-progress-circular v-if="info && info.loading" :value="info.loadPercent" @click.stop="pause" size="36" width="2" style="margin:26px"></v-progress-circular>
<v-btn v-else-if="info && info.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-btn id="btn-forward" :disabled="!info || info.loading" @click.stop="forward" icon>
<v-icon size="28">$vuetify.icons.forward</v-icon>
</v-btn>
</div>
@ -102,87 +100,38 @@ export default {
},
data() {
return {
src: null,
info: null,
currentAudioEvent: null,
autoPlayNextEvent: false,
currentAudioSource: null,
player: null,
duration: 0,
playPercent: 0,
playTime: 0,
playing: false,
analyzer: null,
analyzerDataArray: null,
showReadOnlyToast: false,
};
},
mounted() {
this.$root.$on('audio-playback-started', this.audioPlaybackStarted);
this.$root.$on('audio-playback-paused', this.audioPlaybackPaused);
this.$root.$on('audio-playback-ended', this.audioPlaybackEnded);
document.body.classList.add("dark");
this.$root.$on('playback-start', this.onPlaybackStart);
this.player = this.$refs.player;
this.player.autoplay = false;
this.player.addEventListener("timeupdate", this.updateProgressBar);
this.player.addEventListener("play", () => {
if (!this.analyser) {
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);
}
}
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();
});
this.$audioPlayer.setAutoplay(false);
},
beforeDestroy() {
this.$root.$off('audio-playback-started', this.audioPlaybackStarted);
this.$root.$off('audio-playback-paused', this.audioPlaybackPaused);
this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
document.body.classList.remove("dark");
this.$audioPlayer.removeListener(this._uid);
this.currentAudioEvent = null;
this.loadAudioAttachmentSource(); // Release
this.$root.$off('playback-start', this.onPlaybackStart);
},
computed: {
canRecordAudio() {
return !this.$matrix.currentRoomIsReadOnlyForUser && util.browserCanRecordAudio();
},
currentTime() {
return util.formatDuration(this.playTime);
return util.formatDuration(this.info ? this.info.currentTime : 0);
},
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;
}
},
return util.formatDuration(this.info ? this.info.duration : 0);
},
recordingMembersExceptMe() {
return this.recordingMembers.filter((member) => {
@ -202,18 +151,14 @@ export default {
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);
}
}
@ -222,85 +167,78 @@ export default {
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.info = this.$audioPlayer.addListener(this._uid, value);
const autoPlayWasSet = this.autoPlayNextEvent;
this.autoPlayNextEvent = false;
if (value.getSender() == this.$matrix.currentUserId) {
// Sent by us. Don't autoplay if we just sent this (i.e. it is ahead of our read marker)
if (this.room && !this.room.getReceiptsForEvent(value).includes(value.getSender())) {
this.player.autoplay = false;
this.$audioPlayer.setAutoplay(false);
this.autoPlayNextEvent = autoPlayWasSet;
}
}
this.loadAudioAttachmentSource();
this.$audioPlayer.load(value);
}
},
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();
}
if (this.currentAudioEvent) {
this.$audioPlayer.setAutoplay(false);
this.$audioPlayer.play(this.currentAudioEvent);
}
},
pause() {
this.player.autoplay = false;
if (this.player.src) {
this.player.pause();
this.$audioPlayer.setAutoplay(false);
if (this.currentAudioEvent) {
this.$audioPlayer.pause(this.currentAudioEvent);
}
},
rewind() {
if (this.player.src) {
this.player.currentTime = Math.max(0, this.player.currentTime - 15);
if (this.currentAudioEvent) {
this.$audioPlayer.seekRelative(this.currentAudioEvent, -15000);
}
},
forward() {
if (this.player.src) {
this.player.currentTime = Math.min(this.player.duration, this.player.currentTime + 15);
if (this.currentAudioEvent) {
this.$audioPlayer.seekRelative(this.currentAudioEvent, 15000);
}
},
updateProgressBar() {
if (this.player.duration > 0) {
this.playPercent = Math.floor(
(100 / this.player.duration) * this.player.currentTime
);
} else {
this.playPercent = 0;
audioPlaybackStarted() {
if (!this.analyser) {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let audioSource = null;
if (audioCtx) {
audioSource = audioCtx.createMediaElementSource(this.$audioPlayer.getPlayerElement());
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);
}
}
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();
this.updateVisualization();
if (this.currentAudioEvent) {
this.$emit("mark-read", this.currentAudioEvent.getId(), this.currentAudioEvent.getId());
}
},
onPlaybackEnd() {
audioPlaybackPaused() {
this.clearVisualization();
},
audioPlaybackEnded() {
this.clearVisualization();
this.loadNext(true && this.autoplay);
},
loadPrevious() {
@ -332,11 +270,11 @@ export default {
if (e.getId() === this.readMarker) {
if (i < (audioMessages.length - 1)) {
this.pause();
this.player.autoplay = autoplay;
this.$audioPlayer.setAutoplay(autoplay);
this.currentAudioEvent = audioMessages[i + 1];
} else {
this.autoPlayNextEvent = true;
this.player.autoplay = autoplay;
this.$audioPlayer.setAutoplay(autoplay);
this.currentAudioEvent = e;
this.$emit("loadnext");
}
@ -347,7 +285,7 @@ export default {
// No read marker found. Just use the first event here...
if (audioMessages.length > 0) {
this.pause();
this.player.autoplay = autoplay;
this.$audioPlayer.setAutoplay(autoplay);
this.currentAudioEvent = audioMessages[0];
}
return;
@ -358,11 +296,11 @@ export default {
if (e.getId() === this.currentAudioEvent.getId()) {
if (i < (audioMessages.length - 1)) {
this.pause();
this.player.autoplay = autoplay;
this.$audioPlayer.setAutoplay(autoplay);
this.currentAudioEvent = audioMessages[i + 1];
} else {
this.autoPlayNextEvent = true;
this.player.autoplay = autoplay;
this.$audioPlayer.setAutoplay(autoplay);
this.$emit("loadnext");
}
break;
@ -391,7 +329,7 @@ export default {
const color = 80 + (value * (256 - 80)) / 256;
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
if (this.playing) {
if (this.info && this.info.playing) {
requestAnimationFrame(this.updateVisualization);
} else {
this.clearVisualization();
@ -404,36 +342,6 @@ export default {
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);
});
}
},
memberAvatar(member) {
if (member) {
return member.getAvatarUrl(

View file

@ -422,6 +422,7 @@ export default {
},
mounted() {
this.$root.$on('audio-playback-ended', this.audioPlaybackEnded);
const container = this.chatContainer;
if (container) {
this.scrollPosition = new ScrollPosition(container);
@ -432,6 +433,8 @@ export default {
},
beforeDestroy() {
this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
this.$audioPlayer.pause();
this.stopRRTimer();
},
@ -1527,8 +1530,27 @@ export default {
} else {
this.showNoRecordingAvailableDialog = true;
}
}
},
/**
* Called when an audio message has played to the end. We listen to this so we can optionally auto-play
* the next audio event.
* @param matrixEvent The event that stopped playing
*/
audioPlaybackEnded(matrixEventId) {
if (!this.useVoiceMode) { // Voice mode has own autoplay handling inside "AudioLayout"!
// Auto play consecutive audio messages, either incoming or sent.
const filteredEvents = this.filteredEvents;
const index = filteredEvents.findIndex(e => e.getId() === matrixEventId);
if (index >= 0 && index < (filteredEvents.length - 1)) {
const nextEvent = filteredEvents[index + 1];
if (nextEvent.getContent().msgtype === "m.audio") {
// Yes, audio event!
this.$audioPlayer.play(nextEvent);
}
}
}
}
},
};
</script>

View file

@ -453,7 +453,7 @@ export default {
this.$emit("file", { file: this.recordedFile });
},
getFile(send) {
//const duration = Date.now() - this.recordStartedAt;
const duration = Date.now() - this.recordStartedAt;
this.recorder
.stop()
.getMp3()
@ -468,6 +468,7 @@ export default {
lastModified: Date.now(),
}
);
this.recordedFile.duration = duration;
if (send) {
this.send();
}

View file

@ -1,25 +1,13 @@
<template>
<div class="audio-player d-flex flex-row align-center">
<audio ref="player" :src="src" @durationchange="updateDuration">
<slot></slot>
</audio>
<v-btn v-if="playing" id="btn-pause" @click.stop="pause" icon
><v-icon size="20">pause</v-icon></v-btn
>
<v-btn v-else id="btn-play" @click.stop="play" icon
><v-icon size="20">play_arrow</v-icon></v-btn
>
<v-progress-circular v-if="info.loading" @click.stop="pause" :value="info.loadPercent" size="24" width="2" style="margin:6px"></v-progress-circular>
<v-btn v-else-if="info.playing" id="btn-pause" @click.stop="pause" icon><v-icon size="20">pause</v-icon></v-btn>
<v-btn v-else id="btn-play" @click.stop="play" icon><v-icon size="20">play_arrow</v-icon></v-btn>
<div class="play-time">
{{ currentTime }} / {{ totalTime }}
</div>
<v-slider
color="currentColor"
track-color="#cccccc"
class="play-progress"
v-model="playheadPercent"
min="0"
max="100"
/>
<v-slider @change="seeked" :disabled="!info.url" color="currentColor" track-color="#cccccc" class="play-progress" :value="info.playPercent" min="0"
max="100" />
</div>
</template>
@ -28,8 +16,8 @@ import util from "../../plugins/utils";
export default {
props: {
src: {
type: String,
event: {
type: Object,
default: function () {
return null;
},
@ -37,86 +25,37 @@ export default {
},
data() {
return {
player: null,
duration: 0,
playPercent: 0,
playTime: 0,
playing: false,
info: this.install(),
};
},
mounted() {
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.player.addEventListener("pause", () => {
this.playing = false;
});
this.player.addEventListener("ended", () => {
this.pause();
this.playing = false;
this.$emit("playback-ended");
});
},
beforeDestroy() {
this.$root.$off('playback-start', this.onPlaybackStart);
this.$audioPlayer.removeListener(this._uid);
},
computed: {
currentTime() {
return util.formatDuration(this.playTime);
return util.formatDuration(this.info.currentTime);
},
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;
}
},
return util.formatDuration(this.info.duration);
},
},
methods: {
install() {
return this.$audioPlayer.addListener(this._uid, this.event);
},
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();
}
}
this.$audioPlayer.play(this.event);
},
pause() {
if (this.player.src) {
this.player.pause();
}
},
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;
this.$audioPlayer.pause(this.event);
},
onPlaybackStart(item) {
if (item != this && this.playing) {
if (item != this.src && this.info.playing) {
this.pause();
}
},
seeked(percent) {
this.$audioPlayer.seek(this.event, percent);
}
},
};

View file

@ -1,20 +1,18 @@
<template>
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
<div class="bubble audio-bubble">
<audio-player :src="src">{{ $t('fallbacks.audio_file')}}</audio-player>
<audio-player :event="event">{{ $t('fallbacks.audio_file')}}</audio-player>
</div>
</message-incoming>
</template>
<script>
import attachmentMixin from "./attachmentMixin";
import MessageIncoming from './MessageIncoming.vue';
import AudioPlayer from './AudioPlayer.vue';
export default {
extends: MessageIncoming,
mixins: [attachmentMixin],
components: { MessageIncoming, AudioPlayer }
components: { MessageIncoming, AudioPlayer },
};
</script>

View file

@ -1,20 +1,18 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="audio-bubble">
<audio-player :src="src">{{ $t('fallbacks.audio_file')}}</audio-player>
<audio-player :event="event">{{ $t('fallbacks.audio_file')}}</audio-player>
</div>
</message-outgoing>
</template>
<script>
import attachmentMixin from "./attachmentMixin";
import AudioPlayer from './AudioPlayer.vue';
import MessageOutgoing from "./MessageOutgoing.vue";
export default {
extends: MessageOutgoing,
components: { MessageOutgoing, AudioPlayer },
mixins: [attachmentMixin],
};
</script>