Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New audio plugin UI #4374

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
5 changes: 5 additions & 0 deletions apps/web/src/serlo-editor-integration/create-renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from '@editor/plugin/helpers/editor-renderer'
import { AnchorStaticRenderer } from '@editor/plugins/anchor/static'
import { ArticleStaticRenderer } from '@editor/plugins/article/static'
import { AudioStaticRenderer } from '@editor/plugins/audio/static'
import { BoxStaticRenderer } from '@editor/plugins/box/static'
import { ImageGalleryStaticRenderer } from '@editor/plugins/image-gallery/static'
import { RowsStaticRenderer } from '@editor/plugins/rows/static'
Expand Down Expand Up @@ -184,6 +185,10 @@ export function createRenderers(): InitRenderersArgs {
type: EditorPluginType.Video,
renderer: VideoSerloStaticRenderer,
},
{
type: EditorPluginType.Audio,
renderer: AudioStaticRenderer,
},
{
type: EditorPluginType.Anchor,
renderer: AnchorStaticRenderer,
Expand Down

This file was deleted.

3 changes: 2 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"react": "^18.2.0",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-resizable": "^3.0.5"
"react-resizable": "^3.0.5",
"wavesurfer.js": "^7.8.11"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
Expand Down
4 changes: 1 addition & 3 deletions packages/editor/src/editor-ui/editor-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ export function EditorModal({
)
}

// The moodle navigation bar has a z-index of 1030... We are using 1040 to make
// sure the modal can be on top and is not cut off
export const defaultModalOverlayStyles = cn(
'fixed bottom-0 left-0 right-0 top-0 z-[1040] bg-white bg-opacity-75'
'fixed bottom-0 left-0 right-0 top-0 z-[101] bg-white bg-opacity-75'
)
4 changes: 3 additions & 1 deletion packages/editor/src/i18n/strings/de/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,10 @@ export const editStrings = {
},
audio: {
title: 'Audio',
description: 'Audioaufnahmen von Vocaroo einbinden',
description: 'Audioaufnahme direkt im Browser',
audioUrl: 'Audio URL eingeben',
unexpectedErrorWhileRecording: 'Unerwarteter Fehler bei der Aufnahme!',
download: 'Herunterladen',
},
exercise: {
title: 'Aufgabe',
Expand Down
4 changes: 3 additions & 1 deletion packages/editor/src/i18n/strings/en/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,10 @@ export const editStrings = {
},
audio: {
title: 'Audio',
description: 'Link to audio files on Vocaroo',
description: 'Record a audio file directly in the browser',
audioUrl: 'Enter Audio URL',
unexpectedErrorWhileRecording: 'Unexpected error while recording',
download: 'Download',
},
exercise: {
title: 'Exercise',
Expand Down
199 changes: 199 additions & 0 deletions packages/editor/src/plugins/audio/audio-player.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useState, useRef, useEffect } from 'react'

import { formatTime } from './format-time'

type AudioUrl = string
type AudioData = Blob

interface AudioPlayerProps {
audioFile: AudioData | AudioUrl
}

export function AudioPlayer({ audioFile }: AudioPlayerProps) {
const [isPlaying, setIsPlaying] = useState(false)
const [currentTimePlaying, setCurrentTimePlaying] = useState(0)
const [duration, setDuration] = useState(0)
const audioRef = useRef<HTMLAudioElement | null>(null)

const audioURL =
audioFile instanceof Blob ? URL.createObjectURL(audioFile) : audioFile

useEffect(() => {
const audioElement = audioRef.current

if (!audioElement) {
return
}

// const handleCanPlayThrough = () => {
// console.log('HandleCanPlayThrough called')
// setCorrectDuration()
// }

const handleMetadataLoad = () => {
setCorrectDuration()
}

/**
* Sometimes the duration of the audio is not defined yet and shows
* Infinity. This seems to be a browser problem (see
* https://bugs.chromium.org/p/chromium/issues/detail?id=642012). In those
* cases (100% of the time in Chrome..), we need to skip to the end of the
* audio file so that we can get an accurate duration.
*/
const setCorrectDuration = () => {
if (audioElement.duration === Infinity) {
// Set the current time to the end of the audio file
audioElement.currentTime = 1e101

// On the next tick, set the current time back to 0 and update the duration
setTimeout(() => {
audioElement.currentTime = 0
setDuration(audioElement.duration)
// A timeout of one ms was actually not enough to make this work
}, 100)
} else {
setDuration(audioElement.duration)
}
}

const handleTimeUpdate = () => {
setCurrentTimePlaying(audioElement.currentTime)
}

const handleDurationChange = () => {
if (audioElement.duration === Infinity || isNaN(audioElement.duration)) {
setDuration(0)
} else {
setDuration(audioElement.duration)
}
}

audioElement.addEventListener('loadedmetadata', handleMetadataLoad)
// Adding canplaythrough as a fallback to loadedmetadata which should
// calculate the correct duration right after recording
// audioElement.addEventListener('canplaythrough', handleCanPlayThrough)
audioElement.addEventListener('durationchange', handleDurationChange)
audioElement.addEventListener('timeupdate', handleTimeUpdate)

return () => {
console.log(
'Cleanup of event handlers called. May want to clean up the old url too!'
)
audioElement.removeEventListener('loadedmetadata', handleMetadataLoad)
// audioElement.removeEventListener('canplaythrough', handleCanPlayThrough)
audioElement.removeEventListener('durationchange', handleDurationChange)
audioElement.removeEventListener('timeupdate', handleTimeUpdate)
}
}, [])

useEffect(() => {
// Reset states when the audio file changes
setDuration(0)
setCurrentTimePlaying(0)

setIsPlaying(false)

// I think this is not needed as we should only be dealing
// with urls now. Leaving it here for reference until having updated the
// prop types. If it's a Blob object, we need to release the object URL created for the
// previous Blob. We are now handling this when recording a new audio.
// if (audioFile instanceof Blob && typeof audioURL === 'string') {
// URL.revokeObjectURL(audioURL)
// }
}, [audioFile])

useEffect(() => {
const audioElement = audioRef.current
if (!audioElement) return

const handleAudioEnd = () => {
// Once the audio has finished playing, we wait another 500ms and then
// jump to the start. If we jump to the start immediately, the progress
// bar may not have finished updating all the way to the end and it'll
// look like there is a tiny bit of audio left to play.
setTimeout(() => {
setCurrentTimePlaying(0)
setIsPlaying(false)
}, 500)
}

audioElement.addEventListener('ended', handleAudioEnd)

return () => {
audioElement.removeEventListener('ended', handleAudioEnd)
}
}, [])

const togglePlay = () => {
if (!audioRef.current) return

if (isPlaying) {
audioRef.current.pause()
} else {
audioRef.current
.play()
.then(() => {
// eslint-disable-next-line no-console
console.log('Playing audio!')
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error('Error occurred when trying to play audio', error)
})
}

setIsPlaying(!isPlaying)
}

return (
<div className="flex w-full items-center">
<button
onClick={togglePlay}
className="flex items-center rounded border border-editor-primary-400 bg-editor-primary-400 p-4 "
>
{/* Pause button (2 vertical, parallel rectangles) */}
{isPlaying ? (
<div className="flex space-x-1">
<div className="h-4 w-2 bg-gray-700"></div>
<div className="h-4 w-2 bg-gray-700"></div>
</div>
) : (
// Play button (triangle with the tip pointing to the right)
<div
className="h-4 w-4 bg-gray-700"
style={{ clipPath: 'polygon(100% 50%, 0 0, 0 100%)' }}
/>
)}
</button>
<div className="relative mr-2 flex h-12 w-full items-center rounded-r-lg bg-editor-primary-500 px-2">
<audio ref={audioRef} src={audioURL} className="w-full" />
<div className="relative w-full px-2">
{/* Background track */}
<div className="absolute h-1 w-full bg-gray-300"></div>
{/* Progress indicator */}
<div
className="absolute h-1 bg-gray-700"
style={{ width: `${(currentTimePlaying / duration) * 100}%` }}
/>
{/* Draggable thumb. TODO Should make this interactive */}
<div
className="absolute h-3 w-3 -translate-y-1/3 transform rounded-full bg-gray-700"
style={{
left: `${(currentTimePlaying / duration) * 100}%`,
}}
/>
{/* Time indicators */}
<div className="absolute -bottom-6 left-0 text-xs">
{formatTime(Math.round(currentTimePlaying))}
</div>
<div className="absolute -bottom-6 right-0 text-xs">
{!Number.isNaN(duration) &&
duration !== Infinity &&
formatTime(Math.round(duration), false)}
</div>
</div>
</div>
</div>
)
}
Loading
Loading