diff --git a/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/Project.java b/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/Project.java index faf64e45948..138fe59053f 100644 --- a/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/Project.java +++ b/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/Project.java @@ -128,19 +128,24 @@ public class Project extends BaseIndexedBean implements Comparable { private Folder preview; /** - * Folder with media to use for the video preview. + * Folder with media to use for the audio preview. */ @ManyToOne @JoinColumn(name = "preview_audio_folder_id", foreignKey = @ForeignKey(name = "FK_project_preview_audio_folder_id")) private Folder audioPreview; /** - * Folder with media to use for the video viewer. + * Folder with media to use for the audio viewer. */ @ManyToOne @JoinColumn(name = "mediaView_audio_folder_id", foreignKey = @ForeignKey(name = "FK_project_mediaView_audio_folder_id")) private Folder audioMediaView; + /** + * Field to define the status of the audio media view waveform. + */ + @Column(name = "mediaView_audio_waveform") + private Boolean audioMediaViewWaveform = false; /** * Folder with media to use for the video preview. @@ -553,6 +558,24 @@ public void setAudioMediaView(Folder audioMediaView) { this.audioMediaView = audioMediaView; } + /** + * Get the status of the audio media view waveform. + * + * @return True if is active + */ + public boolean isAudioMediaViewWaveform() { + return audioMediaViewWaveform; + } + + /** + * Set the status of the audio media view waveform. + * + * @param audioMediaViewWaveform True if is active + */ + public void setAudioMediaViewWaveform(boolean audioMediaViewWaveform) { + this.audioMediaViewWaveform = audioMediaViewWaveform; + } + /** * Returns the folder to use for video preview. * diff --git a/Kitodo-DataManagement/src/main/resources/db/migration/V2_125__Add_audio_media_view_waveform_to_project.sql b/Kitodo-DataManagement/src/main/resources/db/migration/V2_125__Add_audio_media_view_waveform_to_project.sql new file mode 100644 index 00000000000..3918267acbf --- /dev/null +++ b/Kitodo-DataManagement/src/main/resources/db/migration/V2_125__Add_audio_media_view_waveform_to_project.sql @@ -0,0 +1,14 @@ +-- +-- (c) Kitodo. Key to digital objects e. V. +-- +-- This file is part of the Kitodo project. +-- +-- It is licensed under GNU General Public License version 3 or later. +-- +-- For the full copyright and license information, please read the +-- GPL3-License.txt file that was distributed with this source code. +-- + +-- +-- Migration: Add column for state of audio waveform in media view to project table. +ALTER TABLE project ADD mediaView_audio_waveform TINYINT(1) NOT NULL DEFAULT 0; diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/ProjectForm.java b/Kitodo/src/main/java/org/kitodo/production/forms/ProjectForm.java index d03d39cb624..3a4011f548d 100644 --- a/Kitodo/src/main/java/org/kitodo/production/forms/ProjectForm.java +++ b/Kitodo/src/main/java/org/kitodo/production/forms/ProjectForm.java @@ -674,6 +674,25 @@ public void setAudioMediaView(String audioMediaView) { project.setAudioMediaView(getFolderMap().get(audioMediaView)); } + /** + * Returns the state of the audio media view waveform. + * + * @return True if enabled + */ + public boolean isAudioMediaViewWaveform() { + return project.isAudioMediaViewWaveform(); + } + + /** + * Sets the state of the audio media view waveform. + * + * @param audioMediaViewWaveform True if enabled + * + */ + public void setAudioMediaViewWaveform(boolean audioMediaViewWaveform) { + project.setAudioMediaViewWaveform(audioMediaViewWaveform); + } + /** * Returns the folder to use for the video media view. * diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java index 9818611bfb6..2d93f89f83d 100644 --- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java +++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java @@ -228,6 +228,13 @@ public void onPageDrop() { } } + /** + * Check if audio media view waveform is activated in project. + */ + public boolean isAudioMediaViewWaveform() { + return dataEditor.getProcess().getProject().isAudioMediaViewWaveform(); + } + private boolean dragStripeIndexMatches(String dragId) { Matcher dragStripeImageMatcher = DRAG_STRIPE_IMAGE.matcher(dragId); Matcher dragUnstructuredMediaMatcher = DRAG_UNSTRUCTURED_MEDIA.matcher(dragId); @@ -829,7 +836,8 @@ private void selectMedia(String physicalDivisionOrder, String stripeIndex, Strin String scrollScripts = "scrollToSelectedTreeNode();scrollToSelectedPaginationRow();"; if (GalleryViewMode.PREVIEW.equals(galleryViewMode)) { - PrimeFaces.current().executeScript("checkScrollPosition();initializeImage();" + scrollScripts); + PrimeFaces.current().executeScript( + "checkScrollPosition();initializeImage();metadataEditor.gallery.mediaView.update();" + scrollScripts); } else { PrimeFaces.current().executeScript(scrollScripts); } diff --git a/Kitodo/src/main/resources/messages/messages_de.properties b/Kitodo/src/main/resources/messages/messages_de.properties index 231baf4286d..a3946368762 100644 --- a/Kitodo/src/main/resources/messages/messages_de.properties +++ b/Kitodo/src/main/resources/messages/messages_de.properties @@ -9,6 +9,12 @@ # GPL3-License.txt file that was distributed with this source code. # AND=Enth\u00E4lt +audioWaveformToolsCenteredCursor=zentrierter Cursor +audioWaveformToolsJumpForwardFiveSeconds=5 Sekunden vorspringen +audioWaveformToolsJumpForwardOneSecond=1 Sekunde vorspringen +audioWaveformToolsJumpBackFiveSeconds=5 Sekunden zur\u00FCckspringen +audioWaveformToolsJumpBackOneSecond=1 Sekunde zur\u00FCckspringen +audioWaveformToolsZoom=Zoom authorities=Berechtigungen authority=Berechtigung DMSExportByThread=Der Vorgang wird vom Taskmanager ins DMS exportiert\: @@ -464,6 +470,7 @@ folderUse.audioPreview=Als Vorschau f\u00fcr Audios verwenden folderUse.audioPreview.disabled=Vorschau f\u00fcr Audios deaktiviert folderUse.audioMediaView=F\u00FCr die Medienansicht f\u00fcr Audios verwenden folderUse.audioMediaView.disabled=Medienansicht f\u00fcr Audios deaktiviert +folderUse.audioMediaViewWaveform=Wellenform in der Medienansicht f\u00fcr Audios anzeigen folderUse.generatorSource=Als Quelle zum Generieren von Inhalten verwenden folderUse.generatorSource.disabled=Generieren von Inhalten deaktiviert folderUse.mediaView=F\u00FCr die Medienansicht verwenden diff --git a/Kitodo/src/main/resources/messages/messages_en.properties b/Kitodo/src/main/resources/messages/messages_en.properties index 1e586e4e647..c27732562d1 100644 --- a/Kitodo/src/main/resources/messages/messages_en.properties +++ b/Kitodo/src/main/resources/messages/messages_en.properties @@ -9,6 +9,12 @@ # GPL3-License.txt file that was distributed with this source code. # AND=Contains +audioWaveformToolsCenteredCursor=Centered cursor +audioWaveformToolsJumpForwardFiveSeconds=Jump forward 5 seconds +audioWaveformToolsJumpForwardOneSecond=Jump forward 1 second +audioWaveformToolsJumpBackFiveSeconds=Jump back 5 seconds +audioWaveformToolsJumpBackOneSecond=Jump back 1 second +audioWaveformToolsZoom=Zoom authorities=Authorities authority=Authority DMSExportByThread=The process is being exported into the DMS by the task manager\: @@ -465,6 +471,7 @@ folderUse.audioPreview=Use as preview for audios folderUse.audioPreview.disabled=Preview for audios disabled folderUse.audioMediaView=Use for the media view for audios folderUse.audioMediaView.disabled=Media view for audios disabled +folderUse.audioMediaViewWaveform=Show waveform in media view for audios folderUse.generatorSource=Use as source to generate contents folderUse.generatorSource.disabled=Content generation disabled folderUse.mediaView=Use for the media view diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css index 54569928ba2..0e9625af3d4 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css +++ b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css @@ -3061,15 +3061,35 @@ Column content #imagePreviewForm\:mediaDetail { display: flex; - width: 100%; + width: calc(100% - 100px); align-items: center; justify-content: center; + flex-direction: column; + gap: 24px; } #imagePreviewForm\:mediaDetail .mediaPreviewItem { max-height: 100%; } +#imagePreviewForm\:mediaDetail #audioWaveformTools{ + display: flex; + gap: 30px; + justify-content: center; + align-items: center; + padding: 14px 16px; + background: var(--trans-blue); + border-radius: 3px; +} + +#imagePreviewForm\:mediaDetail #audioWaveformTools > div > button:first-child { + margin-left: 0; +} + +#imagePreviewForm\:mediaDetail #audioWaveformTools > div > button { + margin-left: 10px; +} + #imagePreviewForm .mediaListIconItem { height: 100%; width: 100%; @@ -3944,6 +3964,16 @@ footer { max-width: 300px; } +.loader { + width: 30px; + height: 30px; + color: white; + display: flex; + justify-content: center; + align-items: center; + padding: 10px; +} + /*---------------------------------------------------------------------- min 700px ----------------------------------------------------------------------*/ diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/libs/wavesurfer/LICENSE b/Kitodo/src/main/webapp/WEB-INF/resources/js/libs/wavesurfer/LICENSE new file mode 100644 index 00000000000..88998ae68f3 --- /dev/null +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/libs/wavesurfer/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2012-2023, katspaugh and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/libs/wavesurfer/wavesurfer.esm.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/libs/wavesurfer/wavesurfer.esm.js new file mode 100644 index 00000000000..013504e3ff4 --- /dev/null +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/libs/wavesurfer/wavesurfer.esm.js @@ -0,0 +1,2 @@ +// wavesurfer 7.3.2 https://wavesurfer-js.org +function t(t,e,i,s){return new(i||(i=Promise))((function(n,r){function o(t){try{h(s.next(t))}catch(t){r(t)}}function a(t){try{h(s.throw(t))}catch(t){r(t)}}function h(t){var e;t.done?n(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(o,a)}h((s=s.apply(t,e||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;const e={decode:function(e,i){return t(this,void 0,void 0,(function*(){const t=new AudioContext({sampleRate:i});return t.decodeAudioData(e).finally((()=>t.close()))}))},createBuffer:function(t,e){return"number"==typeof t[0]&&(t=[t]),function(t){const e=t[0];if(e.some((t=>t>1||t<-1))){const i=e.length;let s=0;for(let t=0;ts&&(s=i)}for(const e of t)for(let t=0;tnull==t?void 0:t[e],copyFromChannel:AudioBuffer.prototype.copyFromChannel,copyToChannel:AudioBuffer.prototype.copyToChannel}}};const i={fetchBlob:function(e,i,s){var n,r;return t(this,void 0,void 0,(function*(){const o=yield fetch(e,s);{const e=null===(n=o.clone().body)||void 0===n?void 0:n.getReader(),s=Number(null===(r=o.headers)||void 0===r?void 0:r.get("Content-Length"));let a=0;const h=(n,r)=>t(this,void 0,void 0,(function*(){if(n)return;a+=(null==r?void 0:r.length)||0;const t=Math.round(a/s*100);return i(t),null==e?void 0:e.read().then((({done:t,value:e})=>h(t,e)))}));null==e||e.read().then((({done:t,value:e})=>h(t,e)))}return o.blob()}))}};class s{constructor(){this.listeners={}}on(t,e){return this.listeners[t]||(this.listeners[t]=new Set),this.listeners[t].add(e),()=>this.un(t,e)}once(t,e){const i=this.on(t,e),s=this.on(t,(()=>{i(),s()}));return i}un(t,e){this.listeners[t]&&(e?this.listeners[t].delete(e):delete this.listeners[t])}unAll(){this.listeners={}}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)))}}class n extends s{constructor(t){super(),t.media?this.media=t.media:this.media=document.createElement("audio"),t.mediaControls&&(this.media.controls=!0),t.autoplay&&(this.media.autoplay=!0),null!=t.playbackRate&&this.onceMediaEvent("canplay",(()=>{null!=t.playbackRate&&(this.media.playbackRate=t.playbackRate)}))}onMediaEvent(t,e,i){return this.media.addEventListener(t,e,i),()=>this.media.removeEventListener(t,e)}onceMediaEvent(t,e){return this.onMediaEvent(t,e,{once:!0})}getSrc(){return this.media.currentSrc||this.media.src||""}revokeSrc(){const t=this.getSrc();t.startsWith("blob:")&&URL.revokeObjectURL(t)}setSrc(t,e){if(this.getSrc()===t)return;this.revokeSrc();const i=e instanceof Blob?URL.createObjectURL(e):t;this.media.src=i,this.media.load()}destroy(){this.media.pause(),this.revokeSrc(),this.media.src="",this.media.load()}play(){return this.media.play()}pause(){this.media.pause()}isPlaying(){return this.media.currentTime>0&&!this.media.paused&&!this.media.ended}setTime(t){this.media.currentTime=t}getDuration(){return this.media.duration}getCurrentTime(){return this.media.currentTime}getVolume(){return this.media.volume}setVolume(t){this.media.volume=t}getMuted(){return this.media.muted}setMuted(t){this.media.muted=t}getPlaybackRate(){return this.media.playbackRate}setPlaybackRate(t,e){null!=e&&(this.media.preservesPitch=e),this.media.playbackRate=t}getMediaElement(){return this.media}setSinkId(t){return this.media.setSinkId(t)}}class r extends s{constructor(t,e){let i;if(super(),this.timeouts=[],this.isScrolling=!1,this.audioData=null,this.resizeObserver=null,this.isDragging=!1,this.options=t,"string"==typeof t.container?i=document.querySelector(t.container):t.container instanceof HTMLElement&&(i=t.container),!i)throw new Error("Container not found");this.parent=i;const[s,n]=this.initHtml();i.appendChild(s),this.container=s,this.scrollContainer=n.querySelector(".scroll"),this.wrapper=n.querySelector(".wrapper"),this.canvasWrapper=n.querySelector(".canvases"),this.progressWrapper=n.querySelector(".progress"),this.cursor=n.querySelector(".cursor"),e&&n.appendChild(e),this.initEvents()}initEvents(){const t=t=>{const e=this.wrapper.getBoundingClientRect(),i=t.clientX-e.left,s=t.clientX-e.left;return[i/e.width,s/e.height]};this.wrapper.addEventListener("click",(e=>{const[i,s]=t(e);this.emit("click",i,s)})),this.wrapper.addEventListener("dblclick",(e=>{const[i,s]=t(e);this.emit("dblclick",i,s)})),this.options.dragToSeek&&this.initDrag(),this.scrollContainer.addEventListener("scroll",(()=>{const{scrollLeft:t,scrollWidth:e,clientWidth:i}=this.scrollContainer,s=t/e,n=(t+i)/e;this.emit("scroll",s,n)}));const e=this.createDelay(100);this.resizeObserver=new ResizeObserver((()=>{e((()=>this.reRender()))})),this.resizeObserver.observe(this.scrollContainer)}initDrag(){!function(t,e,i,s,n=5){let r=()=>{};if(!t)return r;const o=o=>{if(2===o.button)return;o.preventDefault(),o.stopPropagation();let a=o.clientX,h=o.clientY,l=!1;const d=s=>{s.preventDefault(),s.stopPropagation();const r=s.clientX,o=s.clientY;if(l||Math.abs(r-a)>=n||Math.abs(o-h)>=n){const{left:s,top:n}=t.getBoundingClientRect();l||(l=!0,null==i||i(a-s,h-n)),e(r-a,o-h,r-s,o-n),a=r,h=o}},c=t=>{l&&(t.preventDefault(),t.stopPropagation())},u=()=>{l&&(null==s||s()),r()};document.addEventListener("pointermove",d),document.addEventListener("pointerup",u),document.addEventListener("pointerleave",u),document.addEventListener("click",c,!0),r=()=>{document.removeEventListener("pointermove",d),document.removeEventListener("pointerup",u),document.removeEventListener("pointerleave",u),setTimeout((()=>{document.removeEventListener("click",c,!0)}),10)}};t.addEventListener("pointerdown",o)}(this.wrapper,((t,e,i)=>{this.emit("drag",Math.max(0,Math.min(1,i/this.wrapper.getBoundingClientRect().width)))}),(()=>this.isDragging=!0),(()=>this.isDragging=!1))}getHeight(){return null==this.options.height?128:isNaN(Number(this.options.height))?"auto"===this.options.height&&this.parent.clientHeight||128:Number(this.options.height)}initHtml(){const t=document.createElement("div"),e=t.attachShadow({mode:"open"});return e.innerHTML=`\n \n\n
\n
\n
\n
\n
\n
\n
\n `,[t,e]}setOptions(t){this.options=t,this.reRender()}getWrapper(){return this.wrapper}getScroll(){return this.scrollContainer.scrollLeft}destroy(){var t;this.container.remove(),null===(t=this.resizeObserver)||void 0===t||t.disconnect()}createDelay(t=10){const e={};return this.timeouts.push(e),i=>{e.timeout&&clearTimeout(e.timeout),e.timeout=setTimeout(i,t)}}convertColorValues(t){if(!Array.isArray(t))return t||"";if(t.length<2)return t[0]||"";const e=document.createElement("canvas"),i=e.getContext("2d").createLinearGradient(0,0,0,e.height),s=1/(t.length-1);return t.forEach(((t,e)=>{const n=e*s;i.addColorStop(n,t)})),i}renderBarWaveform(t,e,i,s){const n=t[0],r=t[1]||t[0],o=n.length,{width:a,height:h}=i.canvas,l=h/2,d=window.devicePixelRatio||1,c=e.barWidth?e.barWidth*d:1,u=e.barGap?e.barGap*d:e.barWidth?c/2:0,p=e.barRadius||0,m=a/(c+u)/o,g=p&&"roundRect"in i?"roundRect":"rect";i.beginPath();let v=0,f=0,b=0;for(let t=0;t<=o;t++){const o=Math.round(t*m);if(o>v){const t=Math.round(f*l*s),n=t+Math.round(b*l*s)||1;let r=l-t;"top"===e.barAlign?r=0:"bottom"===e.barAlign&&(r=h-n),i[g](v*(c+u),r,c,n,p),v=o,f=0,b=0}const a=Math.abs(n[t]||0),d=Math.abs(r[t]||0);a>f&&(f=a),d>b&&(b=d)}i.fill(),i.closePath()}renderLineWaveform(t,e,i,s){const n=e=>{const n=t[e]||t[0],r=n.length,{height:o}=i.canvas,a=o/2,h=i.canvas.width/r;i.moveTo(0,a);let l=0,d=0;for(let t=0;t<=r;t++){const r=Math.round(t*h);if(r>l){const t=a+(Math.round(d*a*s)||1)*(0===e?-1:1);i.lineTo(l,t),l=r,d=0}const o=Math.abs(n[t]||0);o>d&&(d=o)}i.lineTo(l,a)};i.beginPath(),n(0),n(1),i.fill(),i.closePath()}renderWaveform(t,e,i){if(i.fillStyle=this.convertColorValues(e.waveColor),e.renderFunction)return void e.renderFunction(t,i);let s=e.barHeight||1;if(e.normalize){const e=Array.from(t[0]).reduce(((t,e)=>Math.max(t,Math.abs(e))),0);s=e?1/e:1}e.barWidth||e.barGap||e.barAlign?this.renderBarWaveform(t,e,i,s):this.renderLineWaveform(t,e,i,s)}renderSingleCanvas(t,e,i,s,n,r,o,a){const h=window.devicePixelRatio||1,l=document.createElement("canvas"),d=t[0].length;l.width=Math.round(i*(r-n)/d),l.height=s*h,l.style.width=`${Math.floor(l.width/h)}px`,l.style.height=`${s}px`,l.style.left=`${Math.floor(n*i/h/d)}px`,o.appendChild(l);const c=l.getContext("2d");this.renderWaveform(t.map((t=>t.slice(n,r))),e,c);const u=l.cloneNode();a.appendChild(u);const p=u.getContext("2d");l.width>0&&l.height>0&&p.drawImage(l,0,0),p.globalCompositeOperation="source-in",p.fillStyle=this.convertColorValues(e.progressColor),p.fillRect(0,0,l.width,l.height)}renderChannel(t,e,i){const s=document.createElement("div"),n=this.getHeight();s.style.height=`${n}px`,this.canvasWrapper.style.minHeight=`${n}px`,this.canvasWrapper.appendChild(s);const o=s.cloneNode();this.progressWrapper.appendChild(o);const{scrollLeft:a,scrollWidth:h,clientWidth:l}=this.scrollContainer,d=t[0].length,c=d/h;let u=Math.min(r.MAX_CANVAS_WIDTH,l);if(e.barWidth||e.barGap){const t=e.barWidth||.5,i=t+(e.barGap||t/2);u%i!=0&&(u=Math.floor(u/i)*i)}const p=Math.floor(Math.abs(a)*c),m=Math.floor(p+u*c),g=m-p,v=(r,a)=>{this.renderSingleCanvas(t,e,i,n,Math.max(0,r),Math.min(a,d),s,o)},f=this.createDelay(),b=this.createDelay(),y=(t,e)=>{v(t,e),t>0&&f((()=>{y(t-g,e-g)}))},C=(t,e)=>{v(t,e),e{C(t+g,e+g)}))};y(p,m),mt.timeout&&clearTimeout(t.timeout))),this.timeouts=[],this.canvasWrapper.innerHTML="",this.progressWrapper.innerHTML="",this.wrapper.style.width="";const e=window.devicePixelRatio||1,i=this.scrollContainer.clientWidth,s=Math.ceil(t.duration*(this.options.minPxPerSec||0));this.isScrolling=s>i;const n=this.options.fillParent&&!this.isScrolling,r=(n?i:s)*e;if(this.wrapper.style.width=n?"100%":`${s}px`,this.scrollContainer.style.overflowX=this.isScrolling?"auto":"hidden",this.scrollContainer.classList.toggle("noScrollbar",!!this.options.hideScrollbar),this.cursor.style.backgroundColor=`${this.options.cursorColor||this.options.progressColor}`,this.cursor.style.width=`${this.options.cursorWidth}px`,this.options.splitChannels)for(let e=0;e1&&e.push(t.getChannelData(1)),this.renderChannel(e,this.options,r)}this.audioData=t,this.emit("render")}reRender(){if(!this.audioData)return;const t=this.progressWrapper.clientWidth;this.render(this.audioData);const e=this.progressWrapper.clientWidth;this.scrollContainer.scrollLeft+=e-t}zoom(t){this.options.minPxPerSec=t,this.reRender()}scrollIntoView(t,e=!1){const{clientWidth:i,scrollLeft:s,scrollWidth:n}=this.scrollContainer,r=n*t,o=i/2;if(r>s+(e&&this.options.autoCenter&&!this.isDragging?o:i)||r=t&&r{}}start(){this.unsubscribe=this.on("tick",(()=>{requestAnimationFrame((()=>{this.emit("tick")}))})),this.emit("tick")}stop(){this.unsubscribe()}destroy(){this.unsubscribe()}}const a={waveColor:"#999",progressColor:"#555",cursorWidth:1,minPxPerSec:0,fillParent:!0,interact:!0,dragToSeek:!1,autoScroll:!0,autoCenter:!0,sampleRate:8e3};class h extends n{static create(t){return new h(t)}constructor(t){var e,i;super({media:t.media,mediaControls:t.mediaControls,autoplay:t.autoplay,playbackRate:t.audioRate}),this.plugins=[],this.decodedData=null,this.subscriptions=[],this.options=Object.assign({},a,t),this.timer=new o;const s=t.media?void 0:this.getMediaElement();this.renderer=new r(this.options,s),this.initPlayerEvents(),this.initRendererEvents(),this.initTimerEvents(),this.initPlugins();const n=this.options.url||(null===(e=this.options.media)||void 0===e?void 0:e.currentSrc)||(null===(i=this.options.media)||void 0===i?void 0:i.src);n&&this.load(n,this.options.peaks,this.options.duration)}initTimerEvents(){this.subscriptions.push(this.timer.on("tick",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.getDuration(),!0),this.emit("timeupdate",t),this.emit("audioprocess",t)})))}initPlayerEvents(){this.subscriptions.push(this.onMediaEvent("timeupdate",(()=>{const t=this.getCurrentTime();this.renderer.renderProgress(t/this.getDuration(),this.isPlaying()),this.emit("timeupdate",t)})),this.onMediaEvent("play",(()=>{this.emit("play"),this.timer.start()})),this.onMediaEvent("pause",(()=>{this.emit("pause"),this.timer.stop()})),this.onMediaEvent("emptied",(()=>{this.timer.stop()})),this.onMediaEvent("ended",(()=>{this.emit("finish")})),this.onMediaEvent("seeking",(()=>{this.emit("seeking",this.getCurrentTime())})))}initRendererEvents(){this.subscriptions.push(this.renderer.on("click",((t,e)=>{this.options.interact&&(this.seekTo(t),this.emit("interaction",t*this.getDuration()),this.emit("click",t,e))})),this.renderer.on("dblclick",((t,e)=>{this.emit("dblclick",t,e)})),this.renderer.on("scroll",((t,e)=>{const i=this.getDuration();this.emit("scroll",t*i,e*i)})),this.renderer.on("render",(()=>{this.emit("redraw")})));{let t;this.subscriptions.push(this.renderer.on("drag",(e=>{this.options.interact&&(this.renderer.renderProgress(e),clearTimeout(t),t=setTimeout((()=>{this.seekTo(e)}),this.isPlaying()?0:200),this.emit("interaction",e*this.getDuration()),this.emit("drag",e))})))}}initPlugins(){var t;(null===(t=this.options.plugins)||void 0===t?void 0:t.length)&&this.options.plugins.forEach((t=>{this.registerPlugin(t)}))}setOptions(t){this.options=Object.assign({},this.options,t),this.renderer.setOptions(this.options),t.audioRate&&this.setPlaybackRate(t.audioRate),null!=t.mediaControls&&(this.getMediaElement().controls=t.mediaControls)}registerPlugin(t){return t.init(this),this.plugins.push(t),this.subscriptions.push(t.once("destroy",(()=>{this.plugins=this.plugins.filter((e=>e!==t))}))),t}getWrapper(){return this.renderer.getWrapper()}getScroll(){return this.renderer.getScroll()}getActivePlugins(){return this.plugins}loadAudio(s,n,r,o){return t(this,void 0,void 0,(function*(){if(this.emit("load",s),this.isPlaying()&&this.pause(),this.decodedData=null,!n&&!r){const t=t=>this.emit("loading",t);n=yield i.fetchBlob(s,t,this.options.fetchParams)}if(this.setSrc(s,n),r)o=(yield Promise.resolve(o||this.getDuration()))||(yield new Promise((t=>{this.onceMediaEvent("loadedmetadata",(()=>t(this.getDuration())))})))||(yield Promise.resolve(0)),this.decodedData=e.createBuffer(r,o);else if(n){const t=yield n.arrayBuffer();this.decodedData=yield e.decode(t,this.options.sampleRate)}this.emit("decode",this.getDuration()),this.decodedData&&this.renderer.render(this.decodedData),this.emit("ready",this.getDuration())}))}load(e,i,s){return t(this,void 0,void 0,(function*(){yield this.loadAudio(e,void 0,i,s)}))}loadBlob(e,i,s){return t(this,void 0,void 0,(function*(){yield this.loadAudio("blob",e,i,s)}))}zoom(t){if(!this.decodedData)throw new Error("No audio loaded");this.renderer.zoom(t),this.emit("zoom",t)}getDecodedData(){return this.decodedData}exportPeaks({channels:t=1,maxLength:e=8e3,precision:i=1e4}={}){if(!this.decodedData)throw new Error("The audio has not been decoded yet");const s=Math.min(t,this.decodedData.numberOfChannels),n=[];for(let t=0;tt.destroy())),this.subscriptions.forEach((t=>t())),this.timer.destroy(),this.renderer.destroy(),super.destroy()}}export{h as default}; diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/media_detail_audio_waveform.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/media_detail_audio_waveform.js new file mode 100644 index 00000000000..72360c1cd27 --- /dev/null +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/media_detail_audio_waveform.js @@ -0,0 +1,118 @@ +/** + * (c) Kitodo. Key to digital objects e. V. + * + * This file is part of the Kitodo project. + * + * It is licensed under GNU General Public License version 3 or later. + * + * For the full copyright and license information, please read the + * GPL3-License.txt file that was distributed with this source code. + */ + +import WaveSurfer from './libs/wavesurfer/wavesurfer.esm.js.jsf'; + +class AudioWaveform { + #audioElement; + #wavesurfer; + #buildTimeout; + #loader; + #peaksCache = []; + + constructor() { + this.init(); + } + + init() { + let self = this; + this.#audioElement = document.querySelector('audio.mediaPreviewItem'); + if (this.#audioElement && this.#audioElement.getAttribute("data-audio-waveform") !== "initialized") { + this.#audioElement.setAttribute("data-audio-waveform", "initialized"); + + // add a loader to visualize loading process + this.#loader = document.createElement("div"); + this.#loader.innerHTML = ''; + this.#loader.classList.add('loader'); + this.#audioElement.parentNode.insertBefore(this.#loader, this.#audioElement); + + // when the user agent can play the media + this.#audioElement && this.#audioElement.addEventListener("canplay", () => { + // Prevent browser crashes during audio decoding when multiple rapid clicks, such as double-clicking, occur. + clearTimeout(this.#buildTimeout); + self.#buildTimeout = setTimeout(function() { + self.#build(); + }, 500); + }, {once: true}); + + } + } + + #build() { + let self = this; + // wavesurfer uses the 'src' attribute of the audio element, and we add this attribute based on the browser's current source selection + this.#audioElement.src = this.#audioElement.currentSrc; + + // get the media id from the source parameter + const urlParams = new URLSearchParams(this.#audioElement.src); + let mediaId = urlParams.get('mediaId'); + + let waveContainer = document.createElement("div"); + waveContainer.setAttribute("id", "wave-container"); + waveContainer.onclick = function () { + self.#wavesurfer.playPause(); + }; + waveContainer.style.width = "90%"; + waveContainer.style.display = "none"; + this.#audioElement.parentNode.insertBefore(waveContainer, this.#audioElement); + + this.#wavesurfer = WaveSurfer.create({ + container: document.getElementById(waveContainer.getAttribute("id")), + height: 100, + waveColor: "#f3f3f3", + progressColor: "#ff4e00", + cursorColor: "#ffffff", + media: this.#audioElement, + minPxPerSec: 0, + peaks: this.#peaksCache[mediaId] + }); + + this.#wavesurfer.on("decode", function () { + // cache peaks after when audio has been decoded + self.#peaksCache[mediaId] = self.#wavesurfer.getDecodedData().getChannelData(0); + }); + + this.#wavesurfer.on("ready", function () { + waveContainer.style.display = "block"; + self.#loader.style.display = "none"; + + let waveToolsContainer = document.getElementById("audioWaveformTools"); + const waveToolsSlider = waveToolsContainer.querySelector('input[type="range"]'); + + waveToolsSlider.addEventListener('input', (e) => { + const minPxPerSec = e.target.valueAsNumber; + self.#wavesurfer.zoom(minPxPerSec); + }); + + waveToolsContainer.querySelectorAll('input[type="checkbox"]').forEach((input) => { + input.onchange = (e) => { + self.#wavesurfer.setOptions({ + [input.value]: e.target.checked, + }); + } + }); + const jumpButtons = document.getElementsByClassName("audio-waveform-tools-jump-button"); + Array.from(jumpButtons).forEach(function (jumpButton) { + jumpButton.addEventListener('click', function () { + let jumpSeconds = parseInt(this.getAttribute("data-audio-waveform-tools-jump-seconds")); + self.#wavesurfer.setTime(self.#wavesurfer.getCurrentTime() + jumpSeconds); + }); + }); + }); + } + +} + +const audioWaveform= new AudioWaveform(); + +document.addEventListener("kitodo-metadataditor-mediaview-update", function () { + audioWaveform.init(); +}); diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js index 476ad6b0009..e0922870cd9 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js @@ -433,6 +433,13 @@ metadataEditor.gallery = { } }, + mediaView: { + updateEventName : "kitodo-metadataditor-mediaview-update", + update(){ + document.dispatchEvent(new Event(this.updateEventName)); + } + }, + /** * Event handlers methods related to gallery stripes */ diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js index 26a1f5f33b3..208bb40dcc1 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js @@ -8,7 +8,7 @@ * For the full copyright and license information, please read the * GPL3-License.txt file that was distributed with this source code. */ -/* globals PF */ +/* globals PF, metadataEditor */ // jshint unused:false var SEPARATOR_WIDTH = 3; @@ -523,6 +523,7 @@ function updateMetadataEditorView(showMetadataColumn) { expandThirdColumn(); scrollToSelectedThumbnail(); initializeImage(); + metadataEditor.gallery.mediaView.update(); scrollToSelectedTreeNode(); scrollToSelectedPaginationRow(); } diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml index f0ba415c27e..06b43559ddd 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml @@ -47,7 +47,7 @@ showCheckbox="true"> diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml index 7c089dc4879..963816c6116 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml @@ -65,7 +65,7 @@ diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml index 9cdcf3b2054..ff16f5f16ac 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml @@ -73,6 +73,7 @@ oncomplete="scrollToSelectedThumbnail(); scrollToSelectedPaginationRow(); changeToMapView(); + metadataEditor.gallery.mediaView.update(); expandMetadata('logical-metadata-tab');" update="galleryHeadingWrapper imagePreviewForm:mediaDetail @@ -86,6 +87,7 @@ oncomplete="scrollToSelectedThumbnail(); scrollToSelectedPaginationRow(); changeToMapView(); + metadataEditor.gallery.mediaView.update(); PF('contextMenuLogicalTree').show(currentEvent)" update="@(.stripe) @(.thumbnail) diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/partials/media-detail.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/partials/media-detail.xhtml index b11360fff84..ea62892ec87 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/partials/media-detail.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/partials/media-detail.xhtml @@ -16,7 +16,8 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions" xmlns:p="http://primefaces.org/ui" - xmlns:ui="http://xmlns.jcp.org/jsf/facelets"> + xmlns:ui="http://xmlns.jcp.org/jsf/facelets" + xmlns:a="http://xmlns.jcp.org/jsf/passthrough"> + + +
+
+ +
+
+ +
+
+ + + + +
+
+
+ @@ -36,6 +71,10 @@ + + + + + + + +