Resolve "for chat mode, auto-play next audio message"
This commit is contained in:
parent
f49d374a76
commit
daa52be9c0
11 changed files with 455 additions and 252 deletions
97
package-lock.json
generated
97
package-lock.json
generated
|
|
@ -10,7 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@matrix-org/olm": "^3.2.12",
|
"@matrix-org/olm": "^3.2.12",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"axios": "^0.21.0",
|
"axios": "^1.4.0",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"clean-insights-sdk": "^2.4",
|
"clean-insights-sdk": "^2.4",
|
||||||
|
|
@ -4097,6 +4097,11 @@
|
||||||
"lodash": "^4.17.14"
|
"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": {
|
"node_modules/at-least-node": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||||
|
|
@ -4153,11 +4158,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.21.4",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||||
"dependencies": {
|
"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": {
|
"node_modules/babel-eslint": {
|
||||||
|
|
@ -5497,6 +5504,17 @@
|
||||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
|
@ -6378,6 +6396,14 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/delegate": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz",
|
||||||
"integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
|
@ -11929,6 +11968,11 @@
|
||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/prr": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||||
|
|
@ -19358,6 +19402,11 @@
|
||||||
"lodash": "^4.17.14"
|
"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": {
|
"at-least-node": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||||
|
|
@ -19386,11 +19435,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.21.4",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.14.0"
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-eslint": {
|
"babel-eslint": {
|
||||||
|
|
@ -20464,6 +20515,14 @@
|
||||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
||||||
"dev": true
|
"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": {
|
"commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
|
@ -21145,6 +21204,11 @@
|
||||||
"isobject": "^3.0.1"
|
"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": {
|
"delegate": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz",
|
||||||
"integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="
|
"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": {
|
"forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"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": {
|
"prr": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@matrix-org/olm": "^3.2.12",
|
"@matrix-org/olm": "^3.2.12",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"axios": "^0.21.0",
|
"axios": "^1.4.0",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"clean-insights-sdk": "^2.4",
|
"clean-insights-sdk": "^2.4",
|
||||||
|
|
|
||||||
|
|
@ -37,20 +37,18 @@
|
||||||
<div class="play-time">
|
<div class="play-time">
|
||||||
{{ currentTime }} / {{ totalTime }}
|
{{ currentTime }} / {{ totalTime }}
|
||||||
</div>
|
</div>
|
||||||
<audio ref="player" :src="src" @durationchange="updateDuration">
|
|
||||||
{{ $t('fallbacks.audio_file') }}
|
|
||||||
</audio>
|
|
||||||
<div v-if="currentAudioEvent" class="auto-audio-player">
|
<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-icon size="28">$vuetify.icons.rewind</v-icon>
|
||||||
</v-btn>
|
</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-icon size="56">$vuetify.icons.pause_circle</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-else id="btn-play" @click.stop="play" icon>
|
<v-btn v-else id="btn-play" @click.stop="play" icon>
|
||||||
<v-icon size="56">$vuetify.icons.play_circle</v-icon>
|
<v-icon size="56">$vuetify.icons.play_circle</v-icon>
|
||||||
</v-btn>
|
</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-icon size="28">$vuetify.icons.forward</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,87 +100,38 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
src: null,
|
info: null,
|
||||||
currentAudioEvent: null,
|
currentAudioEvent: null,
|
||||||
autoPlayNextEvent: false,
|
autoPlayNextEvent: false,
|
||||||
currentAudioSource: null,
|
|
||||||
player: null,
|
|
||||||
duration: 0,
|
|
||||||
playPercent: 0,
|
|
||||||
playTime: 0,
|
|
||||||
playing: false,
|
|
||||||
analyzer: null,
|
analyzer: null,
|
||||||
analyzerDataArray: null,
|
analyzerDataArray: null,
|
||||||
showReadOnlyToast: false,
|
showReadOnlyToast: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
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");
|
document.body.classList.add("dark");
|
||||||
this.$root.$on('playback-start', this.onPlaybackStart);
|
this.$audioPlayer.setAutoplay(false);
|
||||||
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();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
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");
|
document.body.classList.remove("dark");
|
||||||
|
this.$audioPlayer.removeListener(this._uid);
|
||||||
this.currentAudioEvent = null;
|
this.currentAudioEvent = null;
|
||||||
this.loadAudioAttachmentSource(); // Release
|
|
||||||
this.$root.$off('playback-start', this.onPlaybackStart);
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
canRecordAudio() {
|
canRecordAudio() {
|
||||||
return !this.$matrix.currentRoomIsReadOnlyForUser && util.browserCanRecordAudio();
|
return !this.$matrix.currentRoomIsReadOnlyForUser && util.browserCanRecordAudio();
|
||||||
},
|
},
|
||||||
currentTime() {
|
currentTime() {
|
||||||
return util.formatDuration(this.playTime);
|
return util.formatDuration(this.info ? this.info.currentTime : 0);
|
||||||
},
|
},
|
||||||
totalTime() {
|
totalTime() {
|
||||||
return util.formatDuration(this.duration);
|
return util.formatDuration(this.info ? this.info.duration : 0);
|
||||||
},
|
|
||||||
playheadPercent: {
|
|
||||||
get: function () {
|
|
||||||
return this.playPercent;
|
|
||||||
},
|
|
||||||
set: function (percent) {
|
|
||||||
if (this.player.src) {
|
|
||||||
this.playPercent = percent;
|
|
||||||
this.player.currentTime = (percent / 100) * this.player.duration;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
recordingMembersExceptMe() {
|
recordingMembersExceptMe() {
|
||||||
return this.recordingMembers.filter((member) => {
|
return this.recordingMembers.filter((member) => {
|
||||||
|
|
@ -202,18 +151,14 @@ export default {
|
||||||
events: {
|
events: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(events, ignoredOldValue) {
|
handler(events, ignoredOldValue) {
|
||||||
console.log("Events changed", this.currentAudioEvent, this.autoPlayNextEvent);
|
|
||||||
if (!this.currentAudioEvent || this.autoPlayNextEvent) {
|
if (!this.currentAudioEvent || this.autoPlayNextEvent) {
|
||||||
// Make sure all events are decrypted!
|
// Make sure all events are decrypted!
|
||||||
const eventsBeingDecrypted = events.filter((e) => e.isBeingDecrypted());
|
const eventsBeingDecrypted = events.filter((e) => e.isBeingDecrypted());
|
||||||
if (eventsBeingDecrypted.length > 0) {
|
if (eventsBeingDecrypted.length > 0) {
|
||||||
console.log("All not decrypted, wait");
|
|
||||||
Promise.allSettled(eventsBeingDecrypted.map((e) => e.getDecryptionPromise())).then(() => {
|
Promise.allSettled(eventsBeingDecrypted.map((e) => e.getDecryptionPromise())).then(() => {
|
||||||
console.log("DONE DECRYPTING!")
|
|
||||||
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("All decrypted, load next");
|
|
||||||
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
this.loadNext(this.autoPlayNextEvent && this.autoplay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -222,85 +167,78 @@ export default {
|
||||||
currentAudioEvent: {
|
currentAudioEvent: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value, oldValue) {
|
handler(value, oldValue) {
|
||||||
console.log("Current audio derom", value, oldValue);
|
|
||||||
if (value && oldValue && value.getId && oldValue.getId && value.getId() === oldValue.getId()) {
|
if (value && oldValue && value.getId && oldValue.getId && value.getId() === oldValue.getId()) {
|
||||||
console.log("Ignoring change!!!");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!value || !value.getId) {
|
if (!value || !value.getId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.src = null;
|
|
||||||
|
this.info = this.$audioPlayer.addListener(this._uid, value);
|
||||||
|
|
||||||
const autoPlayWasSet = this.autoPlayNextEvent;
|
const autoPlayWasSet = this.autoPlayNextEvent;
|
||||||
this.autoPlayNextEvent = false;
|
this.autoPlayNextEvent = false;
|
||||||
|
|
||||||
if (value.getSender() == this.$matrix.currentUserId) {
|
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)
|
// 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())) {
|
if (this.room && !this.room.getReceiptsForEvent(value).includes(value.getSender())) {
|
||||||
this.player.autoplay = false;
|
this.$audioPlayer.setAutoplay(false);
|
||||||
this.autoPlayNextEvent = autoPlayWasSet;
|
this.autoPlayNextEvent = autoPlayWasSet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadAudioAttachmentSource();
|
this.$audioPlayer.load(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
src: {
|
|
||||||
immediate: true,
|
|
||||||
handler(value, ignoredOldValue) {
|
|
||||||
console.log("Source changed to", value, ignoredOldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
play() {
|
play() {
|
||||||
if (this.player.src) {
|
if (this.currentAudioEvent) {
|
||||||
this.$root.$emit("playback-start", this);
|
this.$audioPlayer.setAutoplay(false);
|
||||||
if (this.player.paused) {
|
this.$audioPlayer.play(this.currentAudioEvent);
|
||||||
this.player.play();
|
|
||||||
} else if (this.player.ended) {
|
|
||||||
// restart
|
|
||||||
this.player.currentTime = 0;
|
|
||||||
this.player.play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pause() {
|
pause() {
|
||||||
this.player.autoplay = false;
|
this.$audioPlayer.setAutoplay(false);
|
||||||
if (this.player.src) {
|
if (this.currentAudioEvent) {
|
||||||
this.player.pause();
|
this.$audioPlayer.pause(this.currentAudioEvent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rewind() {
|
rewind() {
|
||||||
if (this.player.src) {
|
if (this.currentAudioEvent) {
|
||||||
this.player.currentTime = Math.max(0, this.player.currentTime - 15);
|
this.$audioPlayer.seekRelative(this.currentAudioEvent, -15000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
forward() {
|
forward() {
|
||||||
if (this.player.src) {
|
if (this.currentAudioEvent) {
|
||||||
this.player.currentTime = Math.min(this.player.duration, this.player.currentTime + 15);
|
this.$audioPlayer.seekRelative(this.currentAudioEvent, 15000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateProgressBar() {
|
audioPlaybackStarted() {
|
||||||
if (this.player.duration > 0) {
|
if (!this.analyser) {
|
||||||
this.playPercent = Math.floor(
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
(100 / this.player.duration) * this.player.currentTime
|
let audioSource = null;
|
||||||
);
|
if (audioCtx) {
|
||||||
} else {
|
audioSource = audioCtx.createMediaElementSource(this.$audioPlayer.getPlayerElement());
|
||||||
this.playPercent = 0;
|
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;
|
this.updateVisualization();
|
||||||
},
|
if (this.currentAudioEvent) {
|
||||||
updateDuration() {
|
this.$emit("mark-read", this.currentAudioEvent.getId(), this.currentAudioEvent.getId());
|
||||||
this.duration = 1000 * this.player.duration;
|
|
||||||
},
|
|
||||||
onPlaybackStart(item) {
|
|
||||||
this.player.autoplay = false;
|
|
||||||
if (item != this && this.playing) {
|
|
||||||
this.pause();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPlaybackEnd() {
|
audioPlaybackPaused() {
|
||||||
|
this.clearVisualization();
|
||||||
|
},
|
||||||
|
audioPlaybackEnded() {
|
||||||
|
this.clearVisualization();
|
||||||
this.loadNext(true && this.autoplay);
|
this.loadNext(true && this.autoplay);
|
||||||
},
|
},
|
||||||
loadPrevious() {
|
loadPrevious() {
|
||||||
|
|
@ -332,11 +270,11 @@ export default {
|
||||||
if (e.getId() === this.readMarker) {
|
if (e.getId() === this.readMarker) {
|
||||||
if (i < (audioMessages.length - 1)) {
|
if (i < (audioMessages.length - 1)) {
|
||||||
this.pause();
|
this.pause();
|
||||||
this.player.autoplay = autoplay;
|
this.$audioPlayer.setAutoplay(autoplay);
|
||||||
this.currentAudioEvent = audioMessages[i + 1];
|
this.currentAudioEvent = audioMessages[i + 1];
|
||||||
} else {
|
} else {
|
||||||
this.autoPlayNextEvent = true;
|
this.autoPlayNextEvent = true;
|
||||||
this.player.autoplay = autoplay;
|
this.$audioPlayer.setAutoplay(autoplay);
|
||||||
this.currentAudioEvent = e;
|
this.currentAudioEvent = e;
|
||||||
this.$emit("loadnext");
|
this.$emit("loadnext");
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +285,7 @@ export default {
|
||||||
// No read marker found. Just use the first event here...
|
// No read marker found. Just use the first event here...
|
||||||
if (audioMessages.length > 0) {
|
if (audioMessages.length > 0) {
|
||||||
this.pause();
|
this.pause();
|
||||||
this.player.autoplay = autoplay;
|
this.$audioPlayer.setAutoplay(autoplay);
|
||||||
this.currentAudioEvent = audioMessages[0];
|
this.currentAudioEvent = audioMessages[0];
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -358,11 +296,11 @@ export default {
|
||||||
if (e.getId() === this.currentAudioEvent.getId()) {
|
if (e.getId() === this.currentAudioEvent.getId()) {
|
||||||
if (i < (audioMessages.length - 1)) {
|
if (i < (audioMessages.length - 1)) {
|
||||||
this.pause();
|
this.pause();
|
||||||
this.player.autoplay = autoplay;
|
this.$audioPlayer.setAutoplay(autoplay);
|
||||||
this.currentAudioEvent = audioMessages[i + 1];
|
this.currentAudioEvent = audioMessages[i + 1];
|
||||||
} else {
|
} else {
|
||||||
this.autoPlayNextEvent = true;
|
this.autoPlayNextEvent = true;
|
||||||
this.player.autoplay = autoplay;
|
this.$audioPlayer.setAutoplay(autoplay);
|
||||||
this.$emit("loadnext");
|
this.$emit("loadnext");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -391,7 +329,7 @@ export default {
|
||||||
const color = 80 + (value * (256 - 80)) / 256;
|
const color = 80 + (value * (256 - 80)) / 256;
|
||||||
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
|
volume.style.backgroundColor = `rgb(${color},${color},${color})`;
|
||||||
|
|
||||||
if (this.playing) {
|
if (this.info && this.info.playing) {
|
||||||
requestAnimationFrame(this.updateVisualization);
|
requestAnimationFrame(this.updateVisualization);
|
||||||
} else {
|
} else {
|
||||||
this.clearVisualization();
|
this.clearVisualization();
|
||||||
|
|
@ -404,36 +342,6 @@ export default {
|
||||||
volume.style.height = "0px";
|
volume.style.height = "0px";
|
||||||
volume.style.backgroundColor = "transparent";
|
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) {
|
memberAvatar(member) {
|
||||||
if (member) {
|
if (member) {
|
||||||
return member.getAvatarUrl(
|
return member.getAvatarUrl(
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$root.$on('audio-playback-ended', this.audioPlaybackEnded);
|
||||||
const container = this.chatContainer;
|
const container = this.chatContainer;
|
||||||
if (container) {
|
if (container) {
|
||||||
this.scrollPosition = new ScrollPosition(container);
|
this.scrollPosition = new ScrollPosition(container);
|
||||||
|
|
@ -432,6 +433,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
|
||||||
|
this.$audioPlayer.pause();
|
||||||
this.stopRRTimer();
|
this.stopRRTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1527,8 +1530,27 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.showNoRecordingAvailableDialog = true;
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,7 @@ export default {
|
||||||
this.$emit("file", { file: this.recordedFile });
|
this.$emit("file", { file: this.recordedFile });
|
||||||
},
|
},
|
||||||
getFile(send) {
|
getFile(send) {
|
||||||
//const duration = Date.now() - this.recordStartedAt;
|
const duration = Date.now() - this.recordStartedAt;
|
||||||
this.recorder
|
this.recorder
|
||||||
.stop()
|
.stop()
|
||||||
.getMp3()
|
.getMp3()
|
||||||
|
|
@ -468,6 +468,7 @@ export default {
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
this.recordedFile.duration = duration;
|
||||||
if (send) {
|
if (send) {
|
||||||
this.send();
|
this.send();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="audio-player d-flex flex-row align-center">
|
<div class="audio-player d-flex flex-row align-center">
|
||||||
<audio ref="player" :src="src" @durationchange="updateDuration">
|
<v-progress-circular v-if="info.loading" @click.stop="pause" :value="info.loadPercent" size="24" width="2" style="margin:6px"></v-progress-circular>
|
||||||
<slot></slot>
|
<v-btn v-else-if="info.playing" id="btn-pause" @click.stop="pause" icon><v-icon size="20">pause</v-icon></v-btn>
|
||||||
</audio>
|
<v-btn v-else id="btn-play" @click.stop="play" icon><v-icon size="20">play_arrow</v-icon></v-btn>
|
||||||
<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
|
|
||||||
>
|
|
||||||
<div class="play-time">
|
<div class="play-time">
|
||||||
{{ currentTime }} / {{ totalTime }}
|
{{ currentTime }} / {{ totalTime }}
|
||||||
</div>
|
</div>
|
||||||
<v-slider
|
<v-slider @change="seeked" :disabled="!info.url" color="currentColor" track-color="#cccccc" class="play-progress" :value="info.playPercent" min="0"
|
||||||
color="currentColor"
|
max="100" />
|
||||||
track-color="#cccccc"
|
|
||||||
class="play-progress"
|
|
||||||
v-model="playheadPercent"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -28,8 +16,8 @@ import util from "../../plugins/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
src: {
|
event: {
|
||||||
type: String,
|
type: Object,
|
||||||
default: function () {
|
default: function () {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -37,86 +25,37 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
player: null,
|
info: this.install(),
|
||||||
duration: 0,
|
|
||||||
playPercent: 0,
|
|
||||||
playTime: 0,
|
|
||||||
playing: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
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() {
|
beforeDestroy() {
|
||||||
this.$root.$off('playback-start', this.onPlaybackStart);
|
this.$audioPlayer.removeListener(this._uid);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentTime() {
|
currentTime() {
|
||||||
return util.formatDuration(this.playTime);
|
return util.formatDuration(this.info.currentTime);
|
||||||
},
|
},
|
||||||
totalTime() {
|
totalTime() {
|
||||||
return util.formatDuration(this.duration);
|
return util.formatDuration(this.info.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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
install() {
|
||||||
|
return this.$audioPlayer.addListener(this._uid, this.event);
|
||||||
|
},
|
||||||
play() {
|
play() {
|
||||||
if (this.player.src) {
|
this.$audioPlayer.play(this.event);
|
||||||
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() {
|
pause() {
|
||||||
if (this.player.src) {
|
this.$audioPlayer.pause(this.event);
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
onPlaybackStart(item) {
|
onPlaybackStart(item) {
|
||||||
if (item != this && this.playing) {
|
if (item != this.src && this.info.playing) {
|
||||||
this.pause();
|
this.pause();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
seeked(percent) {
|
||||||
|
this.$audioPlayer.seek(this.event, percent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
|
||||||
<div class="bubble audio-bubble">
|
<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>
|
</div>
|
||||||
</message-incoming>
|
</message-incoming>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import attachmentMixin from "./attachmentMixin";
|
|
||||||
import MessageIncoming from './MessageIncoming.vue';
|
import MessageIncoming from './MessageIncoming.vue';
|
||||||
import AudioPlayer from './AudioPlayer.vue';
|
import AudioPlayer from './AudioPlayer.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: MessageIncoming,
|
extends: MessageIncoming,
|
||||||
mixins: [attachmentMixin],
|
components: { MessageIncoming, AudioPlayer },
|
||||||
components: { MessageIncoming, AudioPlayer }
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
<div class="audio-bubble">
|
<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>
|
</div>
|
||||||
</message-outgoing>
|
</message-outgoing>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import attachmentMixin from "./attachmentMixin";
|
|
||||||
import AudioPlayer from './AudioPlayer.vue';
|
import AudioPlayer from './AudioPlayer.vue';
|
||||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: MessageOutgoing,
|
extends: MessageOutgoing,
|
||||||
components: { MessageOutgoing, AudioPlayer },
|
components: { MessageOutgoing, AudioPlayer },
|
||||||
mixins: [attachmentMixin],
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import matrix from './services/matrix.service'
|
||||||
import navigation from './services/navigation.service'
|
import navigation from './services/navigation.service'
|
||||||
import config from './services/config.service'
|
import config from './services/config.service'
|
||||||
import analytics from './services/analytics.service'
|
import analytics from './services/analytics.service'
|
||||||
|
import audioPlayer from './services/audio.service';
|
||||||
import 'roboto-fontface/css/roboto/roboto-fontface.css'
|
import 'roboto-fontface/css/roboto/roboto-fontface.css'
|
||||||
import 'material-design-icons-iconfont/dist/material-design-icons.css'
|
import 'material-design-icons-iconfont/dist/material-design-icons.css'
|
||||||
import VEmojiPicker from 'v-emoji-picker';
|
import VEmojiPicker from 'v-emoji-picker';
|
||||||
|
|
@ -35,6 +36,7 @@ const configLoadedPromise = new Promise((resolve, ignoredreject) => {
|
||||||
});
|
});
|
||||||
Vue.use(analytics);
|
Vue.use(analytics);
|
||||||
Vue.use(VueClipboard);
|
Vue.use(VueClipboard);
|
||||||
|
Vue.use(audioPlayer);
|
||||||
|
|
||||||
const vuetify = createVuetify(config);
|
const vuetify = createVuetify(config);
|
||||||
|
|
||||||
|
|
@ -176,9 +178,11 @@ const vueInstance = new Vue({
|
||||||
matrix,
|
matrix,
|
||||||
config,
|
config,
|
||||||
analytics,
|
analytics,
|
||||||
render: h => h(App)
|
audioPlayer,
|
||||||
|
render: h => h(App),
|
||||||
});
|
});
|
||||||
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
|
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
|
||||||
|
vueInstance.$audioPlayer.$root = vueInstance; // Make sure a $root is available here
|
||||||
configLoadedPromise.then((config) => {
|
configLoadedPromise.then((config) => {
|
||||||
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
|
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
|
||||||
vueInstance.$mount('#app');
|
vueInstance.$mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,22 @@ class UploadPromise extends Promise {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Util {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (content.url != null) {
|
if (content.url != null) {
|
||||||
|
|
@ -73,6 +88,7 @@ class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.get(url, {
|
axios.get(url, {
|
||||||
|
signal: abortController ? abortController.signal : undefined,
|
||||||
responseType: 'arraybuffer', onDownloadProgress: progressEvent => {
|
responseType: 'arraybuffer', onDownloadProgress: progressEvent => {
|
||||||
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
|
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
|
|
@ -337,6 +353,11 @@ class Util {
|
||||||
mimetype: file.type,
|
mimetype: file.type,
|
||||||
size: file.size
|
size: file.size
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If audio, send duration in ms as well
|
||||||
|
if (file.duration) {
|
||||||
|
info.duration = file.duration;
|
||||||
|
}
|
||||||
|
|
||||||
var description = file.name;
|
var description = file.name;
|
||||||
var msgtype = 'm.image';
|
var msgtype = 'm.image';
|
||||||
|
|
|
||||||
233
src/services/audio.service.js
Normal file
233
src/services/audio.service.js
Normal file
|
|
@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue