diff --git a/package-lock.json b/package-lock.json
index e04c4eb..a3fe23c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@matrix-org/olm": "^3.2.12",
"aes-js": "^3.1.2",
- "axios": "^0.21.0",
+ "axios": "^1.4.0",
"browserify-fs": "^1.0.0",
"buffer": "^6.0.3",
"clean-insights-sdk": "^2.4",
@@ -4097,6 +4097,11 @@
"lodash": "^4.17.14"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -4153,11 +4158,13 @@
}
},
"node_modules/axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dependencies": {
- "follow-redirects": "^1.14.0"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-eslint": {
@@ -5497,6 +5504,17 @@
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -6378,6 +6396,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
@@ -7869,6 +7895,19 @@
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz",
"integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="
},
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -11929,6 +11968,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -19358,6 +19402,11 @@
"lodash": "^4.17.14"
}
},
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -19386,11 +19435,13 @@
}
},
"axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"requires": {
- "follow-redirects": "^1.14.0"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
}
},
"babel-eslint": {
@@ -20464,6 +20515,14 @@
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -21145,6 +21204,11 @@
"isobject": "^3.0.1"
}
},
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+ },
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
@@ -22325,6 +22389,16 @@
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz",
"integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="
},
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -25482,6 +25556,11 @@
}
}
},
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
diff --git a/package.json b/package.json
index 156bd9f..ee3deaa 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@matrix-org/olm": "^3.2.12",
"aes-js": "^3.1.2",
- "axios": "^0.21.0",
+ "axios": "^1.4.0",
"browserify-fs": "^1.0.0",
"buffer": "^6.0.3",
"clean-insights-sdk": "^2.4",
diff --git a/src/components/AudioLayout.vue b/src/components/AudioLayout.vue
index 735573e..081891c 100644
--- a/src/components/AudioLayout.vue
+++ b/src/components/AudioLayout.vue
@@ -37,20 +37,18 @@
{{ currentTime }} / {{ totalTime }}
-
-
+
$vuetify.icons.rewind
-
+
+
$vuetify.icons.pause_circle
$vuetify.icons.play_circle
-
+
$vuetify.icons.forward
@@ -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(
diff --git a/src/components/Chat.vue b/src/components/Chat.vue
index d705ada..a068ebd 100644
--- a/src/components/Chat.vue
+++ b/src/components/Chat.vue
@@ -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);
+ }
+ }
+ }
+ }
},
};
diff --git a/src/components/VoiceRecorder.vue b/src/components/VoiceRecorder.vue
index 4ba61bb..1d1bc65 100644
--- a/src/components/VoiceRecorder.vue
+++ b/src/components/VoiceRecorder.vue
@@ -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();
}
diff --git a/src/components/messages/AudioPlayer.vue b/src/components/messages/AudioPlayer.vue
index b5c0815..1f21d2b 100644
--- a/src/components/messages/AudioPlayer.vue
+++ b/src/components/messages/AudioPlayer.vue
@@ -1,25 +1,13 @@
-
-
pause
-
play_arrow
+
+
pause
+
play_arrow
{{ currentTime }} / {{ totalTime }}
-
+
@@ -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);
}
},
};
diff --git a/src/components/messages/MessageIncomingAudio.vue b/src/components/messages/MessageIncomingAudio.vue
index 95ddab9..763781e 100644
--- a/src/components/messages/MessageIncomingAudio.vue
+++ b/src/components/messages/MessageIncomingAudio.vue
@@ -1,20 +1,18 @@
-
{{ $t('fallbacks.audio_file')}}
+
{{ $t('fallbacks.audio_file')}}
diff --git a/src/components/messages/MessageOutgoingAudio.vue b/src/components/messages/MessageOutgoingAudio.vue
index f8cea39..c28e6ee 100644
--- a/src/components/messages/MessageOutgoingAudio.vue
+++ b/src/components/messages/MessageOutgoingAudio.vue
@@ -1,20 +1,18 @@
-
{{ $t('fallbacks.audio_file')}}
+
{{ $t('fallbacks.audio_file')}}
diff --git a/src/main.js b/src/main.js
index 9e8ee19..c0fdf09 100644
--- a/src/main.js
+++ b/src/main.js
@@ -7,6 +7,7 @@ import matrix from './services/matrix.service'
import navigation from './services/navigation.service'
import config from './services/config.service'
import analytics from './services/analytics.service'
+import audioPlayer from './services/audio.service';
import 'roboto-fontface/css/roboto/roboto-fontface.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'
import VEmojiPicker from 'v-emoji-picker';
@@ -35,6 +36,7 @@ const configLoadedPromise = new Promise((resolve, ignoredreject) => {
});
Vue.use(analytics);
Vue.use(VueClipboard);
+Vue.use(audioPlayer);
const vuetify = createVuetify(config);
@@ -176,9 +178,11 @@ const vueInstance = new Vue({
matrix,
config,
analytics,
- render: h => h(App)
+ audioPlayer,
+ render: h => h(App),
});
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
+vueInstance.$audioPlayer.$root = vueInstance; // Make sure a $root is available here
configLoadedPromise.then((config) => {
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
vueInstance.$mount('#app');
diff --git a/src/plugins/utils.js b/src/plugins/utils.js
index 898e93e..38b862e 100644
--- a/src/plugins/utils.js
+++ b/src/plugins/utils.js
@@ -53,7 +53,22 @@ class UploadPromise extends Promise {
}
class Util {
- getAttachment(matrixClient, event, progressCallback, asBlob = false) {
+ getAttachmentUrlAndDuration(event) {
+ return new Promise((resolve, reject) => {
+ const content = event.getContent();
+ if (content.url != null) {
+ resolve([content.url, content.info.duration]);
+ return;
+ }
+ if (content.file && content.file.url) {
+ resolve([content.file.url, content.info.duration]);
+ } else {
+ reject("No url found!");
+ }
+ });
+ }
+
+ getAttachment(matrixClient, event, progressCallback, asBlob = false, abortController = undefined) {
return new Promise((resolve, reject) => {
const content = event.getContent();
if (content.url != null) {
@@ -73,6 +88,7 @@ class Util {
}
axios.get(url, {
+ signal: abortController ? abortController.signal : undefined,
responseType: 'arraybuffer', onDownloadProgress: progressEvent => {
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
if (progressCallback) {
@@ -337,6 +353,11 @@ class Util {
mimetype: file.type,
size: file.size
};
+
+ // If audio, send duration in ms as well
+ if (file.duration) {
+ info.duration = file.duration;
+ }
var description = file.name;
var msgtype = 'm.image';
diff --git a/src/services/audio.service.js b/src/services/audio.service.js
new file mode 100644
index 0000000..2470e5c
--- /dev/null
+++ b/src/services/audio.service.js
@@ -0,0 +1,233 @@
+import utils from "../plugins/utils";
+
+/**
+ * This plugin (available in all vue components as $audioPlayer) handles
+ * access to the shared audio player, and events related to loading and
+ * playback of audio attachments.
+ *
+ * Components use this by calling "addListener" (and corresponding removeListener) with
+ * an audio matrix event and a unique component id (for example the ._uid property).
+ */
+export default {
+ install(Vue) {
+ class SharedAudioPlayer {
+ constructor() {
+ this.player = new Audio();
+ this.currentEvent = null;
+ this.infoMap = new Map();
+ this.player.addEventListener("durationchange", this.onDurationChange.bind(this));
+ this.player.addEventListener("timeupdate", this.onTimeUpdate.bind(this));
+ this.player.addEventListener("play", this.onPlay.bind(this));
+ this.player.addEventListener("pause", this.onPause.bind(this));
+ this.player.addEventListener("ended", this.onEnded.bind(this));
+ }
+
+ getPlayerElement() {
+ return this.player;
+ }
+
+ addListener(uid, event) {
+ const eventId = event.getId();
+ var entry = this.infoMap.get(eventId);
+ if (!entry) {
+ // Listeners is just a Set of component "uid" entries for now.
+ entry = { url: null, listeners: new Set() };
+ // Make these reactive, so AudioPlayer (and others) can listen to them
+ Vue.set(entry, "loading", false);
+ Vue.set(entry, "loadPercent", 0);
+ Vue.set(entry, "duration", 0);
+ Vue.set(entry, "currentTime", 0);
+ Vue.set(entry, "playPercent", 0);
+ Vue.set(entry, "playing", false);
+ this.infoMap.set(eventId, entry);
+
+ // Get duration information
+ utils
+ .getAttachmentUrlAndDuration(event)
+ .then(([ignoredurl, duration]) => {
+ entry.duration = duration;
+ })
+ .catch((err) => {
+ console.error("Failed to fetch attachment duration: ", err);
+ });
+ }
+ entry.listeners.add(uid);
+ return entry;
+ }
+ removeListener(uid) {
+ [...this.infoMap].forEach(([ignoredeventid, info]) => {
+ info.listeners.delete(uid);
+ if (info.listeners.size == 0 && info.url) {
+ // No more listeners, release audio blob
+ URL.revokeObjectURL(info.url);
+ info.url = null;
+ }
+ });
+ this.infoMap = new Map(
+ [...this.infoMap].filter(([ignoredeventid, info]) => {
+ return info.listeners.size > 0;
+ })
+ );
+ }
+
+ play(event) {
+ this.play_(event, false);
+ }
+
+ load(event) {
+ this.play_(event, true);
+ }
+
+ play_(event, onlyLoad) {
+ const eventId = event.getId();
+ if (this.currentEvent != eventId) {
+ // Media change, pause the one currently playing.
+ this.player.pause();
+ var entry = this.infoMap.get(this.currentEvent);
+ if (entry) {
+ entry.playing = false;
+ }
+ }
+ this.currentEvent = eventId;
+ const info = this.infoMap.get(eventId);
+ if (info) {
+ if (info.url) {
+ // Restart from beginning?
+ if (info.currentTime == info.duration) {
+ info.currentTime = 0;
+ info.playPercent = 0;
+ }
+ if (this.player.src != info.url) {
+ this.player.src = info.url;
+ this.player.currentTime = (info.currentTime || 0) / 1000;
+ }
+ if (onlyLoad) {
+ this.player.load();
+ } else {
+ this.player.play();
+ }
+ } else {
+ // Download it!
+ info.loadPercent = 0;
+ info.loading = true;
+ info.abortController = new AbortController();
+ utils
+ .getAttachment(this.$root.$matrix.matrixClient, event, (progress) => {
+ info.loadPercent = progress;
+ }, false, info.abortController)
+ .then((url) => {
+ info.url = url;
+
+ // Still on this item? Call ourselves recursively.
+ if (this.currentEvent == eventId) {
+ if (onlyLoad) {
+ this.load(event);
+ } else {
+ this.play(event);
+ }
+ }
+ })
+ .catch((err) => {
+ console.error("Failed to fetch attachment: ", err);
+ })
+ .finally(() => {
+ info.loading = false;
+ info.abortController = undefined;
+ });
+ }
+ }
+ }
+
+ /**
+ * Set the "autoplay" property on the underlying player object.
+ * @param {} autoplay
+ */
+ setAutoplay(autoplay) {
+ this.player.autoplay = autoplay;
+ }
+
+ pause(event) {
+ if (!event || this.currentEvent == event.getId()) {
+ this.player.pause();
+ }
+
+ if (event) {
+ // If downloading, abort that!
+ var entry = this.infoMap.get(event.getId());
+ if (entry && entry.abortController) {
+ entry.abortController.abort();
+ }
+ }
+ }
+ seek(event, percent) {
+ var entry = this.infoMap.get(event.getId());
+ if (entry) {
+ entry.currentTime = ((percent / 100) * (entry.duration || 0));
+ this.updatePlayPercent(entry);
+ if (this.currentEvent == event.getId()) {
+ this.player.currentTime = entry.currentTime / 1000;
+ }
+ }
+ }
+ seekRelative(event, milliseconds) {
+ var entry = this.infoMap.get(event.getId());
+ if (entry) {
+ entry.currentTime = Math.max(0, Math.min(entry.currentTime + milliseconds, entry.duration));
+ this.updatePlayPercent(entry);
+ if (this.currentEvent == event.getId()) {
+ this.player.currentTime = entry.currentTime / 1000;
+ }
+ }
+ }
+ onPlay() {
+ var entry = this.infoMap.get(this.currentEvent);
+ if (entry) {
+ entry.playing = true;
+ }
+ this.$root.$emit("audio-playback-started", this.currentEvent);
+ }
+ onPause() {
+ var entry = this.infoMap.get(this.currentEvent);
+ if (entry) {
+ entry.playing = false;
+ }
+ this.$root.$emit("audio-playback-paused", this.currentEvent);
+ }
+ onEnded() {
+ var entry = this.infoMap.get(this.currentEvent);
+ if (entry) {
+ entry.playing = false;
+ entry.currentTime = entry.duration; // Next time restart
+ }
+ this.$root.$emit("audio-playback-ended", this.currentEvent);
+ }
+ onTimeUpdate() {
+ var entry = this.infoMap.get(this.currentEvent);
+ if (entry) {
+ entry.currentTime = 1000 * this.player.currentTime;
+ this.updatePlayPercent(entry);
+ }
+ }
+ onDurationChange() {
+ const duration =
+ this.player.duration && isFinite(this.player.duration) && !isNaN(this.player.duration)
+ ? 1000 * this.player.duration
+ : 0;
+ var entry = this.infoMap.get(this.currentEvent);
+ if (entry) {
+ entry.duration = duration;
+ this.updatePlayPercent(entry);
+ }
+ }
+ updatePlayPercent(entry) {
+ if (entry.duration > 0) {
+ entry.playPercent = Math.floor((100 / entry.duration) * entry.currentTime);
+ } else {
+ entry.playPercent = 0;
+ }
+ }
+ }
+
+ Vue.prototype.$audioPlayer = new SharedAudioPlayer();
+ },
+};