aboutsummaryrefslogtreecommitdiff
path: root/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
diff options
context:
space:
mode:
authorRon Lucke <lucke@elan-ev.de>2023-11-30 12:13:56 +0000
committerDavid Siegfried <david.siegfried@uni-vechta.de>2023-11-30 12:13:56 +0000
commit3a88a638cf15b1a9f4910a51bd7a3bb64abe04b2 (patch)
tree1ca84cb38185d8c8bb69f2cecd3f41698d9ce696 /resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
parent443aefb2a61cb306ddc5caa5ebd2f4342f5c7780 (diff)
TIC #3388
Closes #3388 Merge request studip/studip!2340
Diffstat (limited to 'resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue')
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue688
1 files changed, 539 insertions, 149 deletions
diff --git a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
index e346d20..9ca52b0 100644
--- a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
@@ -19,106 +19,260 @@
@durationchange="setDuration"
@ended="onEndedListener"
/>
- <div v-if="!emptyAudio" class="cw-audio-container">
- <div class="cw-audio-current-track">
- <p>{{ activeTrackName }}</p>
+ <div class="cw-audio-container">
+ <div
+ v-if="!userRecorderEnabled"
+ class="cw-audio-player"
+ :class="{ 'with-playlist': playlistEnabled }"
+ >
+ <div class="cw-audio-cover" :class="{ loading: loadingCover, 'with-edit-button': canEditFile }">
+ <img v-if="cover" :src="cover" class="cover" />
+ <studip-icon
+ v-else
+ :shape="emptyAudio ? 'file' : 'file-audio'"
+ :size="128"
+ role="info"
+ class="default-cover"
+ />
+ <button v-if="canEditFile" :title="$gettext('Bearbeiten')" @click="displayEditMP3">
+ <studip-icon shape="edit" />
+ </button>
+ </div>
+ <div class="cw-audio-controls-wrapper">
+ <div class="cw-audio-current-track">
+ <h2>{{ trackTitle }}</h2>
+ <h3>{{ trackArtist }}</h3>
+ </div>
+ <div class="cw-audio-controls">
+ <div class="cw-audio-progress">
+ <template v-if="!emptyAudio">
+ <input
+ class="cw-audio-range"
+ ref="range"
+ type="range"
+ :value="currentSeconds"
+ min="0"
+ :max="Math.round(durationSeconds)"
+ @input="rangeAction"
+ />
+ <p class="cw-audio-time">
+ <span>{{ currentTime }}</span>
+ <span>{{ durationTime }}</span>
+ </p>
+ </template>
+ <hr v-else />
+ </div>
+ <div class="cw-audio-buttons">
+ <button :title="$gettext('Zurück')" :disabled="!hasPlaylist" @click="prevAudio">
+ <studip-icon
+ shape="arr_eol-left"
+ :role="hasPlaylist ? 'clickable' : 'inactive'"
+ :size="24"
+ />
+ </button>
+ <button
+ v-if="!playing"
+ :title="$gettext('Abspielen')"
+ :disabled="emptyAudio"
+ @click="playAudio"
+ >
+ <studip-icon
+ shape="play"
+ :role="emptyAudio ? 'inactive' : 'clickable'"
+ :size="48"
+ />
+ </button>
+ <button v-else :title="$gettext('Pause')" @click="pauseAudio">
+ <studip-icon shape="pause" :size="48" />
+ </button>
+ <button :title="$gettext('Weiter')" :disabled="!hasPlaylist" @click="nextAudio">
+ <studip-icon
+ shape="arr_eol-right"
+ :role="hasPlaylist ? 'clickable' : 'inactive'"
+ :size="24"
+ />
+ </button>
+ </div>
+ </div>
+ </div>
</div>
- <div class="cw-audio-controls">
- <input
- class="cw-audio-range"
- ref="range"
- type="range"
- :value="currentSeconds"
- min="0"
- :max="Math.round(durationSeconds)"
- @input="rangeAction"
- />
- <span class="cw-audio-time">{{ currentTime }} {{ durationTime ? '/ ' + durationTime : '' }}</span>
-
- <button v-if="hasPlaylist" class="cw-audio-button cw-audio-prevbutton" :title="$gettext('Zurück')" @click="prevAudio" />
- <button v-if="!playing" class="cw-audio-button cw-audio-playbutton" :title="$gettext('Abspielen')" @click="playAudio" />
- <button v-if="playing" class="cw-audio-button cw-audio-pausebutton" :title="$gettext('Pause')" @click="pauseAudio" />
- <button v-if="hasPlaylist" class="cw-audio-button cw-audio-nextbutton" :title="$gettext('Weiter')" @click="nextAudio" />
- <button class="cw-audio-button cw-audio-stopbutton" :title="$gettext('Anhalten')" @click="stopAudio" />
+ <div v-else class="cw-audio-recorder with-playlist">
+ <div class="cw-audio-cover">
+ <studip-icon
+ shape="microphone"
+ :size="128"
+ :role="isRecording ? 'status-red' : 'info'"
+ class="default-cover"
+ />
+ </div>
+ <div class="cw-audio-controls-wrapper">
+ <div class="cw-audio-current-track">
+ <h2>{{ $gettext('Aufnahme') }}</h2>
+ <h3 v-if="isRecording">{{ $gettext('Aufnahme läuft') }}: {{ seconds2time(timer) }}</h3>
+ <h3 v-if="newRecording && !isRecording">{{ seconds2time(timer) }}</h3>
+ </div>
+ <div class="cw-audio-controls">
+ <div class="cw-recorder-visualization">
+ <div
+ v-for="(value, key) in recorderFrequencyData"
+ :key="'bar' + key"
+ :ref="'bar' + key"
+ class="cw-recorder-visualization-bar"
+ :class="{ 'idle-bar': !isRecording }"
+ ></div>
+ </div>
+ <div class="cw-audio-buttons">
+ <button
+ v-if="newRecording && !isRecording"
+ :title="$gettext('Aufnahme löschen')"
+ @click="resetRecorder"
+ >
+ <studip-icon shape="trash" :size="24" />
+ </button>
+ <button
+ v-if="!isRecording && !newRecording"
+ :title="$gettext('Neue Aufnahme starten')"
+ @click="startRecording"
+ >
+ <studip-icon shape="span-full" :size="48" role="status-red" />
+ </button>
+ <button
+ v-if="isRecording"
+ :title="$gettext('Aufnahme beenden')"
+ @click="stopRecording"
+ >
+ <studip-icon shape="stop" :size="48" />
+ </button>
+ <button
+ v-if="newRecording && !isRecording"
+ :title="$gettext('Aufnahme speichern')"
+ @click="storeRecording"
+ >
+ <studip-icon shape="download" :size="48" />
+ </button>
+ <button
+ v-if="newRecording && !isRecording"
+ :title="$gettext('Aufnahme wiederholen')"
+ @click="startRecording"
+ >
+ <studip-icon shape="span-full" :size="24" role="status-red" />
+ </button>
+ </div>
+ </div>
+ </div>
</div>
- </div>
- <div v-if="emptyAudio" class="cw-audio-empty">
- <p>{{ $gettext('Es ist keine Audio-Datei verfügbar') }}</p>
- </div>
- <div v-show="currentSource === 'studip_folder'" class="cw-audio-playlist-wrapper" :class="[!showRecorder && emptyAudio ? 'empty' : '']">
- <ul v-show="hasPlaylist" class="cw-audio-playlist" :class="[showRecorder ? 'with-recorder' : '']">
- <li v-for="(file, index) in files" :key="file.id">
- <a
- :aria-current="(index === currentPlaylistItem) ? 'true' : 'false'"
- :class="{
- 'is-playing': index === currentPlaylistItem && playing,
- 'current-item': index === currentPlaylistItem,
- }"
- :title="$gettext('Audiodatei:') + ' ' + file.name"
- href="#"
- class="cw-playlist-item"
- @click.prevent="setCurrentPlaylistItem(index)"
- >
- {{ file.name }}
- </a>
- </li>
- </ul>
- <div v-if="showRecorder && canGetMediaDevices" class="cw-audio-playlist-recorder">
- <button
- v-show="!userRecorderEnabled"
- class="button"
- :disabled="!folderSelected || folderLoadError"
- :title="enableRecorderTitle"
- @click="enableRecorder"
- >
- {{ $gettext('Aufnahme aktivieren') }}
- </button>
- <button
- v-show="userRecorderEnabled && !isRecording && !newRecording"
- class="button"
- @click="startRecording"
- >
- {{ $gettext('Aufnahme starten') }}
- </button>
- <button
- v-show="newRecording && !isRecording"
- class="button"
- @click="startRecording"
- >
- {{ $gettext('Aufnahme wiederholen') }}
- </button>
- <button
- v-show="isRecording"
- class="button"
- @click="stopRecording"
- >
- {{ $gettext('Aufnahme beenden') }}
- </button>
- <button
- v-show="newRecording && !isRecording"
- class="button"
- @click="resetRecorder"
- >
- {{ $gettext('Aufnahme löschen') }}
- </button>
- <button
- v-show="newRecording && !isRecording"
- class="button"
- @click="storeRecording"
- >
- {{ $gettext('Aufnahme speichern') }}
- </button>
- <span v-show="isRecording">
- {{ $gettext('Aufnahme läuft') }}: {{seconds2time(timer)}}
- </span>
+ <div v-show="playlistEnabled" class="cw-audio-playlist-wrapper">
+ <ul class="cw-audio-playlist" :class="[showRecorder ? 'with-recorder' : '']">
+ <li v-for="(file, index) in files" :key="file.id">
+ <a
+ :aria-current="index === currentPlaylistItem ? 'true' : 'false'"
+ :title="$gettext('Audiodatei:') + ' ' + file.name"
+ href="#"
+ class="cw-playlist-item"
+ @click.prevent="setCurrentPlaylistItem(index)"
+ >
+ <studip-icon
+ :shape="
+ index === currentPlaylistItem && !userRecorderEnabled
+ ? playing
+ ? 'pause'
+ : 'play'
+ : 'file-audio2'
+ "
+ />
+ {{ file.name }}
+ </a>
+ </li>
+ <li v-if="emptyAudio">
+ <p class="cw-playlist-item">
+ <studip-icon shape="file" role="info" />
+ {{ $gettext('Ordner enthält keine Audio-Dateien') }}
+ </p>
+ </li>
+ </ul>
</div>
</div>
+ <div v-if="showRecorder && canGetMediaDevices" class="cw-call-to-action">
+ <button
+ v-if="!userRecorderEnabled"
+ :title="enableRecorderTitle"
+ @click.prevent="enableRecorder"
+ >
+ <studip-icon shape="microphone" :size="48"/>
+ {{ $gettext('Aufnahme aktivieren') }}
+ </button>
+ <button v-else @click.prevent="resetRecorder">
+ <studip-icon shape="decline" :size="48"/>
+ {{ $gettext('Aufnahme abbrechen') }}
+ </button>
+ </div>
+ <studip-dialog
+ v-if="showEditMP3"
+ :title="$gettext('MP3 Metadaten bearbeiten')"
+ :confirmText="$gettext('Speichern')"
+ confirmClass="accept"
+ :closeText="$gettext('Abbrechen')"
+ closeClass="cancel"
+ @close="closeEditMP3"
+ @confirm="updateMP3"
+ height="550"
+ width="450"
+ >
+ <template v-slot:dialogContent>
+ <div class="edit-mp3-cover-wrapper">
+ <img v-if="newCoverUrl" :src="newCoverUrl" class="edit-mp3-cover" />
+ <template v-else>
+ <template v-if="cover && !deleteCover">
+ <img :src="cover" class="edit-mp3-cover" />
+ <button
+ v-if="cover"
+ class="remove-cover"
+ :title="$gettext('Cover entfernen')"
+ @click="removeCover"
+ >
+ <studip-icon shape="trash" />
+ </button>
+ </template>
+ <studip-icon
+ v-if="cover === '' || deleteCover"
+ shape="file-audio"
+ :size="64"
+ role="info"
+ class="edit-mp3-cover default-cover"
+ />
+ </template>
+ </div>
+ <form class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Cover') }}
+ <template v-if="!deleteCover">
+ <input
+ class="cw-file-input"
+ type="file"
+ ref="newCover"
+ accept="image/jpeg"
+ @change="updateCover"
+ />
+ </template>
+ <input v-else type="text" disabled :placeholder="$gettext('Cover wird entfernt')" />
+ </label>
+ <label>
+ {{ $gettext('Titel') }}
+ <input type="text" v-model="currentMP3Title" />
+ </label>
+ <label>
+ {{ $gettext('Künstler') }}
+ <input type="text" v-model="currentMP3Artist" />
+ </label>
+ </form>
+ </template>
+ </studip-dialog>
</template>
<template v-if="canEdit" #edit>
<form class="default" @submit.prevent="">
<label>
{{ $gettext('Überschrift') }}
- <input type="text" v-model="currentTitle" />
+ <input type="text" v-model="currentTitle" :placeholder="$gettext('optional')" />
</label>
<label>
{{ $gettext('Quelle') }}
@@ -168,6 +322,7 @@
import BlockComponents from './block-components.js';
import blockMixin from '@/vue/mixins/courseware/block.js';
import { mapActions, mapGetters } from 'vuex';
+import MP3Tag from 'mp3tag.js';
export default {
name: 'courseware-audio-block',
@@ -197,7 +352,24 @@ export default {
timer: 0,
isRecording: false,
newRecording: false,
- folderLoadError: false
+ folderLoadError: false,
+ recorderAudioCtx: null,
+ recorderAnalyser: null,
+ recorderSource: null,
+ recorderBufferLength: 0,
+ recorderTimeData: null,
+ recorderFrequencyData: null,
+
+ mp3tag: null,
+ loadingCover: false,
+ volume: 100,
+
+ showEditMP3: false,
+ currentMP3Title: '',
+ currentMP3Artist: '',
+ imageBytes: null,
+ newCoverUrl: '',
+ deleteCover: false,
};
},
computed: {
@@ -207,26 +379,33 @@ export default {
urlHelper: 'urlHelper',
userId: 'userId',
usersById: 'users/byId',
- relatedTermOfUse: 'terms-of-use/related'
+ relatedTermOfUse: 'terms-of-use/related',
}),
files() {
const files =
this.relatedFileRefs({
parent: { type: 'folders', id: this.currentFolderId },
- relationship: 'file-refs'
+ relationship: 'file-refs',
}) ?? [];
return files
.filter((file) => {
- if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) {
+ if (
+ this.relatedTermOfUse({ parent: file, relationship: 'terms-of-use' }).attributes[
+ 'download-condition'
+ ] !== 0
+ ) {
return false;
- }
- if (! file.attributes['mime-type'].includes('audio')) {
+ }
+ if (!file.attributes['mime-type'].includes('audio')) {
return false;
}
return true;
})
+ .sort((a, b) => {
+ return new Date(a.attributes.mkdate) - new Date(b.attributes.mkdate);
+ })
.map(({ id, attributes }) => {
return {
id,
@@ -236,7 +415,8 @@ export default {
{ type: 0, file_id: id, file_name: attributes.name },
true
),
- mime_type: attributes['mime-type']
+ mime_type: attributes['mime-type'],
+ isRecording: attributes.description === 'CoursewareRecording',
};
});
},
@@ -271,25 +451,36 @@ export default {
return this.block?.attributes?.payload?.recorder_enabled;
},
showRecorder() {
- return this.currentRecorderEnabled && this.currentSource === 'studip_folder';
+ return this.currentRecorderEnabled && this.playlistEnabled;
},
hasPlaylist() {
- return this.files.length > 0 && this.currentSource === 'studip_folder';
+ return this.files.length > 0 && this.playlistEnabled;
+ },
+ playlistEnabled() {
+ return this.currentSource === 'studip_folder';
},
canGetMediaDevices() {
return navigator.mediaDevices !== undefined;
},
- currentURL() {
+ activeFile() {
if (this.currentSource === 'studip_file') {
- return this.currentFile.download_url;
+ return this.currentFile;
}
- if (this.currentSource === 'studip_folder') {
+ if (this.playlistEnabled) {
if (this.files.length > 0) {
- return this.files[this.currentPlaylistItem].download_url;
- } else {
- return '';
+ return this.files[this.currentPlaylistItem];
}
}
+
+ return null;
+ },
+ fileIsRecording() {
+ return this.activeFile?.isRecording ?? false;
+ },
+ currentURL() {
+ if (this.activeFile) {
+ return this.activeFile.download_url;
+ }
if (this.currentSource === 'web') {
return this.currentWebUrl;
}
@@ -297,15 +488,8 @@ export default {
return '';
},
activeTrackName() {
- if (this.currentSource === 'studip_file') {
- return this.currentFile.name;
- }
- if (this.currentSource === 'studip_folder') {
- if (this.files.length > 0) {
- return this.files[this.currentPlaylistItem].name;
- } else {
- return '';
- }
+ if (this.activeFile) {
+ return this.activeFile.name;
}
if (this.currentSource === 'web') {
return this.currentWebUrl;
@@ -313,6 +497,19 @@ export default {
return '';
},
+ trackTitle() {
+ if (this.emptyAudio) {
+ return this.$gettext('Es ist keine Audio-Datei verfügbar');
+ }
+ if (this.tags && this.tags.title !== '') {
+ return this.tags.title;
+ }
+
+ return this.activeTrackName;
+ },
+ trackArtist() {
+ return this.tags?.artist ?? '';
+ },
emptyAudio() {
if (this.currentSource === 'studip_folder' && this.currentFolderId !== '' && this.files.length > 0) {
return false;
@@ -335,21 +532,69 @@ export default {
}
return this.$gettext('Aktiviert die Aufnahmefunktion');
- }
+ },
+ tags() {
+ return this.mp3tag?.tags ?? {};
+ },
+ hasMP3Tags() {
+ return Object.keys(this.tags).length > 0;
+ },
+ cover() {
+ const image = this.tags?.v2?.APIC?.[0];
+ if (image) {
+ return this.imageURL(image.data, image.format);
+ }
+
+ if (this.fileIsRecording) {
+ const ownerId = this.activeFileRef?.relationships?.owner?.data?.id;
+ if (ownerId) {
+ const owner = this.usersById({ id: ownerId });
+ return owner?.meta?.avatar?.normal ?? '';
+ }
+ }
+
+ return '';
+ },
+ activeFileRef() {
+ return this.fileRefById({ id: this.activeFile.id });
+ },
+ canEditFile() {
+ return this.hasMP3Tags && this.activeFileRef.attributes['is-editable'];
+ },
},
- mounted() {
+ async mounted() {
this.initCurrentData();
},
methods: {
...mapActions({
loadFileRef: 'file-refs/loadById',
loadRelatedFileRefs: 'file-refs/loadRelated',
+ updateFileRefs: 'file-refs/update',
updateBlock: 'updateBlockInContainer',
companionWarning: 'companionWarning',
companionSuccess: 'companionSuccess',
companionError: 'companionError',
createFile: 'createFile',
+ updateFileContent: 'updateFileContent',
+ loadUser: 'users/loadById',
}),
+
+ toDataURL(url) {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ var reader = new FileReader();
+ reader.onloadend = function () {
+ resolve(reader.result);
+ };
+ reader.readAsArrayBuffer(xhr.response);
+ };
+ xhr.open('GET', url);
+ xhr.responseType = 'blob';
+ xhr.send();
+ });
+ },
+
initCurrentData() {
this.currentTitle = this.title;
this.currentSource = this.source;
@@ -370,9 +615,9 @@ export default {
await this.loadRelatedFileRefs({
parent: { type: 'folders', id: this.currentFolderId },
relationship: 'file-refs',
- options: { include: 'terms-of-use' }
+ options: { include: 'terms-of-use' },
});
- } catch(error) {
+ } catch (error) {
this.folderLoadError = true;
}
},
@@ -388,7 +633,7 @@ export default {
if (this.currentSource === 'studip_file') {
if (this.currentFileId === '') {
this.companionWarning({
- info: this.$gettext('Bitte wählen Sie eine Datei aus.')
+ info: this.$gettext('Bitte wählen Sie eine Datei aus.'),
});
return false;
}
@@ -398,7 +643,7 @@ export default {
} else if (this.currentSource === 'studip_folder') {
if (this.currentFolderId === '') {
this.companionWarning({
- info: this.$gettext('Bitte wählen Sie einen Ordner aus.')
+ info: this.$gettext('Bitte wählen Sie einen Ordner aus.'),
});
return false;
}
@@ -419,8 +664,11 @@ export default {
this.$refs.audio.currentTime = this.$refs.range.value;
}
},
+ setVolume() {
+ this.$refs.audio.volume = this.volume / 100;
+ },
setDuration() {
- let duration = this.$refs.audio.duration
+ let duration = this.$refs.audio.duration;
if (!isNaN(duration) && isFinite(duration)) {
this.durationSeconds = duration;
} else {
@@ -441,9 +689,9 @@ export default {
this.playing = true;
} else {
this.companionError({
- info: this.$gettext('Ihr Browser unterstützt dieses Audioformat leider nicht.')
+ info: this.$gettext('Ihr Browser unterstützt dieses Audioformat leider nicht.'),
});
- if(this.hasPlaylist) {
+ if (this.hasPlaylist) {
this.nextAudio();
}
}
@@ -461,7 +709,7 @@ export default {
},
onEndedListener() {
this.stopAudio();
- if(this.hasPlaylist) {
+ if (this.hasPlaylist) {
this.nextAudio();
}
},
@@ -486,6 +734,7 @@ export default {
return time;
},
setCurrentPlaylistItem(index) {
+ this.userRecorderEnabled = false;
if (this.currentPlaylistItem === index) {
if (this.playing) {
this.pauseAudio();
@@ -494,7 +743,7 @@ export default {
}
} else {
this.currentPlaylistItem = index;
- this.$nextTick(()=> {
+ this.$nextTick(() => {
this.playAudio();
});
}
@@ -506,7 +755,7 @@ export default {
} else {
this.currentPlaylistItem = this.files.length - 1;
}
- this.$nextTick(()=> {
+ this.$nextTick(() => {
this.playAudio();
});
},
@@ -514,7 +763,7 @@ export default {
this.stopAudio();
if (this.currentPlaylistItem < this.files.length - 1) {
this.currentPlaylistItem = this.currentPlaylistItem + 1;
- this.$nextTick(()=> {
+ this.$nextTick(() => {
this.playAudio();
});
}
@@ -534,26 +783,59 @@ export default {
{ type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
true
),
- mime_type: fileRef.attributes['mime-type']
+ mime_type: fileRef.attributes['mime-type'],
});
}
},
+ async loadTags() {
+ this.mp3tag = null;
+ let view = this;
+ let response = await fetch(this.currentURL);
+ let data = await response.blob();
+ let file = new File([data], this.activeTrackName);
+
+ let reader = new FileReader();
+ reader.onload = function () {
+ const buffer = this.result;
+ view.mp3tag = new MP3Tag(buffer);
+ view.mp3tag.read();
+ };
+
+ reader.readAsArrayBuffer(file);
+ },
+ imageURL(bytes, format) {
+ let encoded = '';
+ bytes.forEach(function (byte) {
+ encoded += String.fromCharCode(byte);
+ });
+
+ return `data:${format};base64,${btoa(encoded)}`;
+ },
enableRecorder() {
if (!this.folderSelected || this.folderLoadError) {
return false;
}
let view = this;
- navigator.mediaDevices.getUserMedia({ audio: true })
- .then(function(stream) {
- view.recorder = new MediaRecorder(stream, {type: 'audio/webm; codecs:vp9' });
+ navigator.mediaDevices
+ .getUserMedia({ audio: true })
+ .then(function (stream) {
+ view.recorder = new MediaRecorder(stream, { type: 'audio/webm; codecs:vp9' });
view.userRecorderEnabled = true;
- view.recorder.ondataavailable = e => {
+ view.recorder.ondataavailable = (e) => {
view.chunks.push(e.data);
};
+
+ view.recorderAudioCtx = new AudioContext();
+ view.recorderAnalyser = view.recorderAudioCtx.createAnalyser();
+ view.recorderSource = view.recorderAudioCtx.createMediaStreamSource(stream);
+ view.recorderSource.connect(view.recorderAnalyser);
+ view.recorderAnalyser.fftSize = 2 ** 6;
+ view.recorderBufferLength = view.recorderAnalyser.frequencyBinCount;
+ view.recorderFrequencyData = new Uint8Array(view.recorderBufferLength);
})
.catch(() => {
view.companionWarning({
- info: view.$gettext('Sie müssen ein Mikrofon freigeben, um eine Aufnahme starten zu können.')
+ info: view.$gettext('Sie müssen ein Mikrofon freigeben, um eine Aufnahme starten zu können.'),
});
});
},
@@ -563,7 +845,9 @@ export default {
this.timer = 0;
this.recorder.start();
this.isRecording = true;
- setTimeout(function(){ view.setTimer(); }, 1000);
+ setTimeout(function () {
+ view.setTimer();
+ }, 1000);
},
stopRecording() {
this.isRecording = false;
@@ -574,48 +858,136 @@ export default {
let view = this;
if (this.recorder.state === 'recording') {
this.timer++;
- setTimeout(function(){ view.setTimer(); }, 1000);
+ setTimeout(function () {
+ view.setTimer();
+ }, 1000);
+ }
+ },
+ recorderDrawTimeData() {
+ this.recorderAnalyser.getByteFrequencyData(this.recorderFrequencyData);
+
+ for (let i = 0; i < this.recorderFrequencyData.length; i++) {
+ let ref = 'bar' + i;
+ this.$refs[ref][0].style.height = (this.recorderFrequencyData[i] / 255) * 28 + 'px';
+ }
+
+ if (this.isRecording) {
+ let view = this;
+ requestAnimationFrame(() => view.recorderDrawTimeData());
}
},
async storeRecording() {
let view = this;
- let user = this.usersById({id: this.userId});
- let blob = new Blob(view.chunks, {type: 'audio/webm; codecs:vp9' });
+ let user = this.usersById({ id: this.userId });
+ let blob = new Blob(view.chunks, { type: 'audio/webm; codecs:vp9' });
+
let file = {
attributes: {
- name: (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '.webm'
+ name: user.attributes['formatted-name'].replace(/\s+/g, '_') + '.webm',
},
relationships: {
'terms-of-use': {
data: {
- id: 'SELFMADE_NONPUB'
- }
- }
- }
+ id: 'SELFMADE_NONPUB',
+ },
+ },
+ },
};
let fileObj = await this.createFile({
file: file,
filedata: blob,
- folder: {id: this.currentFolderId}
+ folder: { id: this.currentFolderId },
});
- if(fileObj && fileObj.type === 'file-refs') {
+ if (fileObj && fileObj.type === 'file-refs') {
this.companionSuccess({
- info: this.$gettext('Die Aufnahme wurde erfolgreich im Dateibereich abgelegt.')
+ info: this.$gettext('Die Aufnahme wurde erfolgreich im Dateibereich abgelegt.'),
});
+ fileObj.attributes.description = 'CoursewareRecording';
+ await this.updateFileRefs(fileObj);
} else {
this.companionError({
- info: this.$gettext('Es ist ein Fehler aufgetreten! Die Aufnahme konnte nicht gespeichert werden.')
+ info: this.$gettext('Es ist ein Fehler aufgetreten! Die Aufnahme konnte nicht gespeichert werden.'),
});
}
this.newRecording = false;
+ this.userRecorderEnabled = false;
this.getFolderFiles();
},
resetRecorder() {
+ this.userRecorderEnabled = false;
+ this.isRecording = false;
this.newRecording = false;
this.chunks = [];
this.timer = 0;
this.blob = null;
},
+ displayEditMP3() {
+ this.stopAudio();
+ this.currentMP3Title = this.tags.title;
+ this.currentMP3Artist = this.tags.artist;
+ this.showEditMP3 = true;
+ },
+ closeEditMP3() {
+ this.showEditMP3 = false;
+ this.currentMP3Title = '';
+ this.currentMP3Artist = '';
+ this.imageBytes = null;
+ this.newCoverUrl = '';
+ this.deleteCover = false;
+ },
+ removeCover() {
+ this.deleteCover = true;
+ this.$refs.newCover.value = '';
+ },
+ async updateCover() {
+ this.deleteCover = false;
+ const file = this.$refs?.newCover?.files[0];
+ const buffer = await this.readFile(file);
+ this.imageBytes = new Uint8Array(buffer);
+ this.newCoverUrl = this.imageURL(this.imageBytes, 'image/jpeg');
+ },
+ readFile(file) {
+ return new Promise(function (resolve, reject) {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result);
+ };
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(file);
+ });
+ },
+ async updateMP3() {
+ this.mp3tag.tags.title = this.currentMP3Title;
+ this.mp3tag.tags.artist = this.currentMP3Artist;
+
+ if (this.imageBytes) {
+ this.mp3tag.tags.v2.APIC = [
+ {
+ format: 'image/jpeg',
+ type: 3,
+ description: '',
+ data: this.imageBytes,
+ },
+ ];
+ }
+ if (this.deleteCover) {
+ this.mp3tag.tags.v2.APIC = [];
+ }
+ this.mp3tag.save();
+ const modifiedFile = new File([this.mp3tag.buffer], this.activeTrackName, {
+ type: 'audio/mpeg',
+ });
+
+ const fileRef = await this.fileRefById({ id: this.activeFile.id });
+
+ let fileObj = await this.updateFileContent({
+ file: fileRef,
+ filedata: modifiedFile,
+ });
+
+ this.closeEditMP3();
+ this.getFolderFiles();
+ },
},
watch: {
currentFolderId(newState) {
@@ -625,9 +997,27 @@ export default {
this.getFolderFiles();
}
},
+ currentURL() {
+ this.loadingCover = true;
+ this.loadTags();
+ if (this.fileIsRecording) {
+ const ownerId = this.activeFileRef?.relationships?.owner?.data?.id;
+ if (ownerId) {
+ this.loadUser({ id: ownerId });
+ }
+ }
+ setTimeout(() => {
+ this.loadingCover = false;
+ }, 200);
+ },
+ isRecording(newState) {
+ if (newState) {
+ this.recorderDrawTimeData();
+ }
+ },
},
};
</script>
<style scoped lang="scss">
- @import "../../../../assets/stylesheets/scss/courseware/blocks/audio.scss";
-</style> \ No newline at end of file
+@import '../../../../assets/stylesheets/scss/courseware/blocks/audio.scss';
+</style>