Use Wavesurfer for reimplementing audio file preview component.pull/1/head
@@ -26,10 +26,11 @@ | |||||
"react-dom": "18.2.0", | "react-dom": "18.2.0", | ||||
"tailwindcss": "3.3.2", | "tailwindcss": "3.3.2", | ||||
"typescript": "5.1.3", | "typescript": "5.1.3", | ||||
"wavesurfer.js": "7.0.0-beta.6" | |||||
"wavesurfer.js": "7.0.0-beta.11" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/mime-types": "^2.1.1", | "@types/mime-types": "^2.1.1", | ||||
"@types/prismjs": "^1.26.0" | |||||
"@types/prismjs": "^1.26.0", | |||||
"@types/wavesurfer.js": "^6.0.6" | |||||
} | } | ||||
} | } |
@@ -57,8 +57,8 @@ dependencies: | |||||
specifier: 5.1.3 | specifier: 5.1.3 | ||||
version: 5.1.3 | version: 5.1.3 | ||||
wavesurfer.js: | wavesurfer.js: | ||||
specifier: 7.0.0-beta.6 | |||||
version: 7.0.0-beta.6 | |||||
specifier: 7.0.0-beta.11 | |||||
version: 7.0.0-beta.11 | |||||
devDependencies: | devDependencies: | ||||
'@types/mime-types': | '@types/mime-types': | ||||
@@ -67,6 +67,9 @@ devDependencies: | |||||
'@types/prismjs': | '@types/prismjs': | ||||
specifier: ^1.26.0 | specifier: ^1.26.0 | ||||
version: 1.26.0 | version: 1.26.0 | ||||
'@types/wavesurfer.js': | |||||
specifier: ^6.0.6 | |||||
version: 6.0.6 | |||||
packages: | packages: | ||||
@@ -349,6 +352,10 @@ packages: | |||||
tslib: 2.5.3 | tslib: 2.5.3 | ||||
dev: false | dev: false | ||||
/@types/debounce@1.2.1: | |||||
resolution: {integrity: sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==} | |||||
dev: true | |||||
/@types/json5@0.0.29: | /@types/json5@0.0.29: | ||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} | resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} | ||||
dev: false | dev: false | ||||
@@ -387,6 +394,12 @@ packages: | |||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} | resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} | ||||
dev: false | dev: false | ||||
/@types/wavesurfer.js@6.0.6: | |||||
resolution: {integrity: sha512-fD54o0RXZXxkOb+69Rt6rGViaHpIc1Mmde2aOX9qPhlQhrCPepybGnsekiG407+7scPlaK+hmuPez5AnnmlzGg==} | |||||
dependencies: | |||||
'@types/debounce': 1.2.1 | |||||
dev: true | |||||
/@typescript-eslint/parser@5.60.0(eslint@8.43.0)(typescript@5.1.3): | /@typescript-eslint/parser@5.60.0(eslint@8.43.0)(typescript@5.1.3): | ||||
resolution: {integrity: sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==} | resolution: {integrity: sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==} | ||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} | ||||
@@ -2702,8 +2715,8 @@ packages: | |||||
graceful-fs: 4.2.11 | graceful-fs: 4.2.11 | ||||
dev: false | dev: false | ||||
/wavesurfer.js@7.0.0-beta.6: | |||||
resolution: {integrity: sha512-vB8J1ppZ58vozmBDmqADDdKBYY6bebSYKUgIaDXB56Qo/CpPdExSlg91tN1FAubN5swZ1IUyB8Z9xOY/TsYRoA==} | |||||
/wavesurfer.js@7.0.0-beta.11: | |||||
resolution: {integrity: sha512-PwcnEIcV3x8Zi0XMWFkzRy0NsJyTaFITIXByoQn/y6OqtJn9W5jzryTvt/mxv+FcKAWA7yGqkxRGX336D0iWTQ==} | |||||
dev: false | dev: false | ||||
/which-boxed-primitive@1.0.2: | /which-boxed-primitive@1.0.2: | ||||
@@ -1,87 +1,253 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import {AudioFile, getMimeTypeDescription} from '@/utils/blob'; | |||||
import {formatFileSize, formatNumeral, formatSecondsDurationPrecise} from '@/utils/numeral'; | |||||
import {useAudioControls} from '../../hooks/media'; | |||||
import {augmentAudioFile, getMimeTypeDescription} from '@/utils/blob'; | |||||
import { | |||||
formatFileSize, | |||||
formatNumeral, | |||||
formatSecondsDurationConcise, | |||||
formatSecondsDurationPrecise, | |||||
} from '@/utils/numeral'; | |||||
import {useMediaControls} from '../../hooks/media'; | |||||
import {useAugmentedFile} from '@/categories/blob/react'; | |||||
export interface AudioFilePreviewProps { | |||||
file: AudioFile; | |||||
import clsx from 'clsx'; | |||||
import {WaveSurferCanvas} from '@/packages/react-wavesurfer'; | |||||
type AudioFilePreviewDerivedComponent = HTMLAudioElement; | |||||
export interface AudioFilePreviewProps extends Omit<React.HTMLProps<AudioFilePreviewDerivedComponent>, 'controls'> { | |||||
file?: File; | |||||
disabled?: boolean; | |||||
enhanced?: boolean; | |||||
} | } | ||||
export const AudioFilePreview: React.FC<AudioFilePreviewProps> = ({ | |||||
file: f, | |||||
}) => { | |||||
export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponent, AudioFilePreviewProps>(({ | |||||
file, | |||||
style, | |||||
className, | |||||
enhanced = false, | |||||
disabled = false, | |||||
}, forwardedRef) => { | |||||
const { augmentedFile, error } = useAugmentedFile({ | |||||
file, | |||||
augmentFunction: augmentAudioFile, | |||||
}); | |||||
const { | const { | ||||
mediaContainerRef, | |||||
playMedia, | |||||
mediaControllerRef, | |||||
refreshControls, | |||||
reset, | |||||
updateSeekFromPlayback, | |||||
isPlaying, | isPlaying, | ||||
} = useAudioControls({ file: f }); | |||||
isSeeking, | |||||
currentTimeDisplay = 0, | |||||
seekTimeDisplay = 0, | |||||
durationDisplay = 0, | |||||
isSeekTimeCountingDown, | |||||
adjustVolume, | |||||
volumeRef, | |||||
handleAction, | |||||
filenameRef, | |||||
seekRef, | |||||
startSeek, | |||||
endSeek, | |||||
setSeek, | |||||
} = useMediaControls<HTMLAudioElement>({ | |||||
controllerRef: forwardedRef, | |||||
}); | |||||
const formId = React.useId(); | |||||
if (!augmentedFile) { | |||||
return null; | |||||
} | |||||
const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay; | |||||
const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay; | |||||
return ( | return ( | ||||
<div className="flex flex-col gap-4 w-full h-full relative"> | |||||
<div className="h-2/5 flex-shrink-0 cursor-pointer relative"> | |||||
<div | |||||
ref={mediaContainerRef} | |||||
className="relative h-full w-full" | |||||
/> | |||||
<div className="absolute bottom-0 left-0 z-[2]"> | |||||
<button | |||||
onClick={playMedia} | |||||
> | |||||
{isPlaying ? '⏸' : '▶'} | |||||
</button> | |||||
</div> | |||||
</div> | |||||
<dl className="h-3/5 flex-shrink-0 m-0 flex flex-col items-end" data-testid="infoBox"> | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Name | |||||
</dt> | |||||
<dd | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
title={f.name} | |||||
> | |||||
{f.name} | |||||
</dd> | |||||
</div> | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Type | |||||
</dt> | |||||
<dd | |||||
title={f.type} | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
> | |||||
{getMimeTypeDescription(f.type, f.name)} | |||||
</dd> | |||||
</div> | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Size | |||||
</dt> | |||||
<dd | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
title={`${formatNumeral(f.size ?? 0)} bytes`} | |||||
> | |||||
{formatFileSize(f.size)} | |||||
</dd> | |||||
</div> | |||||
<div | |||||
className={clsx( | |||||
'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full', | |||||
className, | |||||
)} | |||||
style={style} | |||||
> | |||||
<div className="h-full relative col-span-2"> | |||||
{ | { | ||||
typeof f.metadata?.duration === 'number' | |||||
typeof augmentedFile.metadata?.previewUrl === 'string' | |||||
&& ( | && ( | ||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Duration | |||||
</dt> | |||||
<dd | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
title={`${formatNumeral(f.metadata.duration ?? 0)} seconds`} | |||||
> | |||||
{formatSecondsDurationPrecise(f.metadata.duration)} | |||||
</dd> | |||||
<div | |||||
className="w-full h-full bg-black flex flex-col items-stretch" | |||||
data-testid="preview" | |||||
> | |||||
<div className="w-full flex-auto relative"> | |||||
<WaveSurferCanvas | |||||
className="sm:absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-[#000000]" | |||||
ref={mediaControllerRef} | |||||
onLoadedMetadata={refreshControls} | |||||
onDurationChange={refreshControls} | |||||
onEnded={reset} | |||||
onTimeUpdate={updateSeekFromPlayback} | |||||
data-testid="preview" | |||||
> | |||||
<source | |||||
src={augmentedFile.metadata.previewUrl} | |||||
type={augmentedFile.type} | |||||
/> | |||||
</WaveSurferCanvas> | |||||
</div> | |||||
{enhanced && ( | |||||
<div className="w-full flex-shrink-0 h-10 flex gap-4 relative"> | |||||
<button | |||||
className="w-10 h-full flex-shrink-0 text-primary" | |||||
type="submit" | |||||
name="action" | |||||
value="togglePlayback" | |||||
form={formId} | |||||
> | |||||
{isPlaying ? '⏸' : '▶'} | |||||
</button> | |||||
<div className="flex-auto w-full flex items-center gap-2 text-sm relative"> | |||||
<button | |||||
className="absolute overflow-hidden w-12 opacity-0 h-10" | |||||
title="Toggle Seek Time Count Mode" | |||||
type="submit" | |||||
name="action" | |||||
value="toggleSeekTimeCountMode" | |||||
form={formId} | |||||
> | |||||
Toggle Seek Time Count Mode | |||||
</button> | |||||
<span | |||||
className="before:block before:content-[attr(title)] contents tabular-nums" | |||||
title={ | |||||
`${ | |||||
isSeekTimeCountingDown ? '−' : '+' | |||||
}${ | |||||
isSeeking | |||||
? formatSecondsDurationConcise(finalSeekTimeDisplay) | |||||
: formatSecondsDurationConcise(finalCurrentTimeDisplay) | |||||
}` | |||||
} | |||||
> | |||||
<input | |||||
type="range" | |||||
className="flex-auto w-full tabular-nums" | |||||
ref={seekRef} | |||||
onMouseDown={startSeek} | |||||
onMouseUp={endSeek} | |||||
onChange={setSeek} | |||||
defaultValue="0" | |||||
step="any" | |||||
/> | |||||
</span> | |||||
<span> | |||||
{formatSecondsDurationConcise(durationDisplay)} | |||||
</span> | |||||
</div> | |||||
<input | |||||
type="range" | |||||
ref={volumeRef} | |||||
max={1} | |||||
min={0} | |||||
onChange={adjustVolume} | |||||
step="any" | |||||
className="flex-shrink-0 w-12" | |||||
defaultValue="1" | |||||
title="Volume" | |||||
/> | |||||
</div> | |||||
)} | |||||
</div> | </div> | ||||
) | ) | ||||
} | } | ||||
</dl> | |||||
</div> | |||||
<div | |||||
className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between" | |||||
> | |||||
<dl data-testid="infoBox"> | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Name | |||||
</dt> | |||||
<dd | |||||
className="m-0 w-full text-ellipsis overflow-hidden font-bold" | |||||
title={augmentedFile.name} | |||||
ref={filenameRef} | |||||
> | |||||
{augmentedFile.name} | |||||
</dd> | |||||
</div> | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Type | |||||
</dt> | |||||
<dd | |||||
title={augmentedFile.type} | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
> | |||||
{getMimeTypeDescription(augmentedFile.type, augmentedFile.name)} | |||||
</dd> | |||||
</div> | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Size | |||||
</dt> | |||||
<dd | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
title={`${formatNumeral(augmentedFile.size ?? 0)} bytes`} | |||||
> | |||||
{formatFileSize(augmentedFile.size)} | |||||
</dd> | |||||
</div> | |||||
{ | |||||
typeof augmentedFile.metadata?.duration === 'number' | |||||
&& ( | |||||
<div className="w-full"> | |||||
<dt className="sr-only"> | |||||
Duration | |||||
</dt> | |||||
<dd | |||||
className="m-0 w-full text-ellipsis overflow-hidden" | |||||
title={`${formatNumeral(augmentedFile.metadata.duration ?? 0)} seconds`} | |||||
> | |||||
{formatSecondsDurationPrecise(augmentedFile.metadata.duration)} | |||||
</dd> | |||||
</div> | |||||
) | |||||
} | |||||
</dl> | |||||
<form | |||||
id={formId} | |||||
onSubmit={handleAction} | |||||
className="flex gap-4" | |||||
> | |||||
<fieldset | |||||
disabled={disabled || typeof error !== 'undefined'} | |||||
className="contents" | |||||
> | |||||
<legend className="sr-only"> | |||||
Controls | |||||
</legend> | |||||
<button | |||||
type="submit" | |||||
name="action" | |||||
value="download" | |||||
className={clsx( | |||||
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none', | |||||
'focus:outline-0', | |||||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||||
)} | |||||
> | |||||
<span | |||||
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" | |||||
> | |||||
Download | |||||
</span> | |||||
</button> | |||||
</fieldset> | |||||
</form> | |||||
</div> | |||||
</div> | </div> | ||||
); | ); | ||||
}; | |||||
}); | |||||
AudioFilePreview.displayName = 'AudioFilePreview'; |
@@ -11,7 +11,7 @@ export const AudioMiniFilePreview: React.FC<AudioMiniFilePreviewProps> = ({ | |||||
file: f, | file: f, | ||||
}) => { | }) => { | ||||
const { | const { | ||||
mediaContainerRef, | |||||
mountRef, | |||||
playMedia, | playMedia, | ||||
isPlaying, | isPlaying, | ||||
} = useAudioControls({ file: f }); | } = useAudioControls({ file: f }); | ||||
@@ -19,7 +19,7 @@ export const AudioMiniFilePreview: React.FC<AudioMiniFilePreviewProps> = ({ | |||||
return ( | return ( | ||||
<div | <div | ||||
className="absolute top-0 left-0 w-full h-full cursor-pointer" | className="absolute top-0 left-0 w-full h-full cursor-pointer" | ||||
ref={mediaContainerRef} | |||||
ref={mountRef} | |||||
onClick={playMedia} | onClick={playMedia} | ||||
/> | /> | ||||
); | ); | ||||
@@ -1,5 +1,5 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import {augmentImageFile, getMimeTypeDescription, ImageFile} from '@/utils/blob'; | |||||
import {augmentImageFile, getMimeTypeDescription} from '@/utils/blob'; | |||||
import {formatFileSize, formatNumeral} from '@/utils/numeral'; | import {formatFileSize, formatNumeral} from '@/utils/numeral'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import {useAugmentedFile, useImageControls} from '@/categories/blob/react'; | import {useAugmentedFile, useImageControls} from '@/categories/blob/react'; | ||||
@@ -18,7 +18,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen | |||||
disabled = false, | disabled = false, | ||||
...etcProps | ...etcProps | ||||
}, forwardedRef) => { | }, forwardedRef) => { | ||||
const { augmentedFile, error } = useAugmentedFile<ImageFile>({ | |||||
const { augmentedFile, error } = useAugmentedFile({ | |||||
file, | file, | ||||
augmentFunction: augmentImageFile, | augmentFunction: augmentImageFile, | ||||
}); | }); | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import {augmentVideoFile, getMimeTypeDescription, VideoFile} from '@/utils/blob'; | |||||
import {augmentVideoFile, getMimeTypeDescription} from '@/utils/blob'; | |||||
import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from '@/utils/numeral'; | import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from '@/utils/numeral'; | ||||
import {useAugmentedFile, useVideoControls} from '@tesseract-design/web-blob-react'; | |||||
import {useAugmentedFile, useMediaControls} from '@tesseract-design/web-blob-react'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
type VideoFilePreviewDerivedComponent = HTMLVideoElement; | type VideoFilePreviewDerivedComponent = HTMLVideoElement; | ||||
@@ -12,7 +12,7 @@ export interface VideoFilePreviewProps extends Omit<React.HTMLProps<VideoFilePre | |||||
enhanced?: boolean; | enhanced?: boolean; | ||||
} | } | ||||
export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePreviewProps>(({ | |||||
export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponent, VideoFilePreviewProps>(({ | |||||
file, | file, | ||||
className, | className, | ||||
style, | style, | ||||
@@ -20,7 +20,7 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
enhanced = false, | enhanced = false, | ||||
...etcProps | ...etcProps | ||||
}, forwardedRef) => { | }, forwardedRef) => { | ||||
const { augmentedFile, error } = useAugmentedFile<VideoFile>({ | |||||
const { augmentedFile, error } = useAugmentedFile({ | |||||
file, | file, | ||||
augmentFunction: augmentVideoFile, | augmentFunction: augmentVideoFile, | ||||
}); | }); | ||||
@@ -29,7 +29,7 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
volumeRef, | volumeRef, | ||||
isPlaying, | isPlaying, | ||||
adjustVolume, | adjustVolume, | ||||
resetVideo, | |||||
reset, | |||||
startSeek, | startSeek, | ||||
endSeek, | endSeek, | ||||
setSeek, | setSeek, | ||||
@@ -43,8 +43,8 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
mediaControllerRef, | mediaControllerRef, | ||||
handleAction, | handleAction, | ||||
filenameRef, | filenameRef, | ||||
} = useVideoControls({ | |||||
mediaControllerRef: forwardedRef, | |||||
} = useMediaControls<HTMLVideoElement>({ | |||||
controllerRef: forwardedRef, | |||||
}); | }); | ||||
const formId = React.useId(); | const formId = React.useId(); | ||||
@@ -78,7 +78,7 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
ref={mediaControllerRef} | ref={mediaControllerRef} | ||||
onLoadedMetadata={refreshControls} | onLoadedMetadata={refreshControls} | ||||
onDurationChange={refreshControls} | onDurationChange={refreshControls} | ||||
onEnded={resetVideo} | |||||
onEnded={reset} | |||||
onTimeUpdate={updateSeekFromPlayback} | onTimeUpdate={updateSeekFromPlayback} | ||||
data-testid="preview" | data-testid="preview" | ||||
controls={!enhanced} | controls={!enhanced} | ||||
@@ -100,7 +100,7 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
> | > | ||||
{isPlaying ? '⏸' : '▶'} | {isPlaying ? '⏸' : '▶'} | ||||
</button> | </button> | ||||
<div className="flex-auto w-full flex items-center gap-1 font-mono text-sm relative"> | |||||
<div className="flex-auto w-full flex items-center gap-2 text-sm relative"> | |||||
<button | <button | ||||
className="absolute overflow-hidden w-12 opacity-0 h-10" | className="absolute overflow-hidden w-12 opacity-0 h-10" | ||||
title="Toggle Seek Time Count Mode" | title="Toggle Seek Time Count Mode" | ||||
@@ -112,7 +112,7 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
Toggle Seek Time Count Mode | Toggle Seek Time Count Mode | ||||
</button> | </button> | ||||
<span | <span | ||||
className="font-mono before:block before:content-[attr(title)] contents tabular-nums" | |||||
className="before:block before:content-[attr(title)] contents tabular-nums" | |||||
title={ | title={ | ||||
`${ | `${ | ||||
isSeekTimeCountingDown ? '−' : '+' | isSeekTimeCountingDown ? '−' : '+' | ||||
@@ -131,6 +131,7 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev | |||||
onMouseUp={endSeek} | onMouseUp={endSeek} | ||||
onChange={setSeek} | onChange={setSeek} | ||||
defaultValue="0" | defaultValue="0" | ||||
step="any" | |||||
/> | /> | ||||
</span> | </span> | ||||
<span> | <span> | ||||
@@ -22,7 +22,7 @@ export const useAugmentedFile = <T extends Partial<File>>(options = {} as UseAug | |||||
.catch((error) => { | .catch((error) => { | ||||
setError(error); | setError(error); | ||||
}); | }); | ||||
}, [file]); | |||||
}, [file, augmentFunction]); | |||||
return React.useMemo(() => ({ | return React.useMemo(() => ({ | ||||
augmentedFile: (augmentedFile ?? file) as T | undefined, | augmentedFile: (augmentedFile ?? file) as T | undefined, | ||||
@@ -1,87 +1,146 @@ | |||||
import {AudioFile} from '@/utils/blob'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import WaveSurfer from 'wavesurfer.js'; | |||||
export interface UseAudioFilePreviewControlsOptions { | |||||
file: AudioFile; | |||||
export interface UseAudioControlsOptions { | |||||
actionFormKey?: string; | |||||
forwardedRef?: React.Ref<any>; | |||||
} | } | ||||
export const useAudioControls = ({ file: f }: UseAudioFilePreviewControlsOptions) => { | |||||
const mediaContainerRef = React.useRef<HTMLDivElement>(null); | |||||
const mediaControllerRef = React.useRef<WaveSurfer>(null); | |||||
export const useAudioControls = ({ | |||||
forwardedRef, | |||||
actionFormKey = 'action' as const, | |||||
}: UseAudioControlsOptions) => { | |||||
const mediaControllerRef = React.useRef<HTMLAudioElement>(null); | |||||
const volumeRef = React.useRef<HTMLInputElement>(null); | |||||
const filenameRef = React.useRef<HTMLElement>(null); | |||||
const [isPlaying, setIsPlaying] = React.useState(false); | const [isPlaying, setIsPlaying] = React.useState(false); | ||||
const [isSeeking, setIsSeeking] = React.useState(false); | |||||
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>(); | |||||
const [seekTimeDisplay, setSeekTimeDisplay] = React.useState<number>(); | |||||
const [durationDisplay, setDurationDisplay] = React.useState<number>(); | |||||
const [isSeekTimeCountingDown, setIsSeekTimeCountingDown] = React.useState(false); | |||||
React.useEffect(() => { | |||||
if (!mediaControllerRef.current) { | |||||
return; | |||||
} | |||||
const refreshControls: React.ReactEventHandler<HTMLAudioElement> = React.useCallback((e) => { | |||||
const { currentTarget: mediaController } = e; | |||||
if (isPlaying) { | |||||
void mediaControllerRef.current.play(); | |||||
return; | |||||
} | |||||
setCurrentTimeDisplay(mediaController.currentTime); | |||||
setDurationDisplay(mediaController.duration); | |||||
}, []); | |||||
mediaControllerRef.current.pause(); | |||||
}, [isPlaying]); | |||||
const reset: React.ReactEventHandler<HTMLAudioElement> = React.useCallback((e) => { | |||||
const videoElement = e.currentTarget; | |||||
setIsPlaying(false); | |||||
videoElement.currentTime = 0; | |||||
}, []); | |||||
React.useEffect(() => { | |||||
if (!mediaControllerRef.current) { | |||||
const updateSeekFromPlayback = React.useCallback((e) => { | |||||
if (isSeeking) { | |||||
return; | return; | ||||
} | } | ||||
if (!mediaContainerRef.current) { | |||||
const videoElement = e.currentTarget; | |||||
const currentTime = videoElement.currentTime; | |||||
setCurrentTimeDisplay(currentTime); | |||||
}, [isSeeking]); | |||||
const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | |||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
return; | return; | ||||
} | } | ||||
if (typeof f.metadata?.previewUrl !== 'string') { | |||||
if (!mediaControllerRef.current) { | |||||
return; | return; | ||||
} | } | ||||
const { value } = e.currentTarget; | |||||
mediaControllerRef.current.volume = Number(value); | |||||
}, [mediaControllerRef]); | |||||
const togglePlayback = React.useCallback(() => { | |||||
setIsPlaying((p) => !p); | |||||
}, []); | |||||
mediaContainerRef.current.innerHTML = ''; | |||||
const toggleSeekTimeCountMode = React.useCallback(() => { | |||||
setIsSeekTimeCountingDown((b) => !b); | |||||
}, []); | |||||
if (f.type === 'audio/mid') { | |||||
const download = React.useCallback(() => { | |||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef?.current !== null)) { | |||||
return; | return; | ||||
} | } | ||||
const mediaControllerRefMutable = mediaControllerRef as React.MutableRefObject<WaveSurfer>; | |||||
mediaControllerRefMutable.current = WaveSurfer.create({ | |||||
container: mediaContainerRef.current, | |||||
cursorWidth: 0, | |||||
height: mediaContainerRef.current.offsetHeight, | |||||
barWidth: 2, | |||||
barGap: 2, | |||||
barRadius: 1, | |||||
}); | |||||
if (!(typeof filenameRef === 'object' && filenameRef?.current !== null)) { | |||||
return; | |||||
} | |||||
mediaControllerRefMutable.current.on('finish', () => { | |||||
setIsPlaying(false); | |||||
mediaControllerRefMutable.current.seekTo(0); | |||||
const downloadLink = window.document.createElement('a'); | |||||
downloadLink.download = filenameRef.current.textContent ?? 'image'; | |||||
downloadLink.href = mediaControllerRef.current.currentSrc; | |||||
downloadLink.addEventListener('click', () => { | |||||
downloadLink.remove(); | |||||
}); | }); | ||||
downloadLink.click(); | |||||
}, [mediaControllerRef, filenameRef]); | |||||
const actions = React.useMemo(() => ({ | |||||
togglePlayback, | |||||
toggleSeekTimeCountMode, | |||||
download, | |||||
}), [togglePlayback, toggleSeekTimeCountMode, download]); | |||||
const handleAction: React.FormEventHandler<HTMLFormElement> = React.useCallback((e) => { | |||||
e.preventDefault(); | |||||
const formData = new FormData(e.currentTarget, (e.nativeEvent as unknown as { submitter: HTMLElement }).submitter); | |||||
const actionName = formData.get(actionFormKey) as keyof typeof actions; | |||||
const { [actionName]: actionFunction } = actions; | |||||
actionFunction?.(); | |||||
}, [actions, actionFormKey]); | |||||
// TODO get the preview URL here | |||||
mediaControllerRefMutable.current.load(f.metadata.previewUrl); | |||||
React.useEffect(() => { | |||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
return; | |||||
} | |||||
return () => { | |||||
if (!mediaControllerRefMutable.current) { | |||||
return; | |||||
} | |||||
if (!mediaControllerRef.current) { | |||||
return; | |||||
} | |||||
mediaControllerRefMutable.current.destroy(); | |||||
if (isPlaying) { | |||||
//console.log(mediaControllerRef.current); | |||||
void mediaControllerRef.current.play(); | |||||
return | |||||
} | } | ||||
}, [f, mediaContainerRef, mediaControllerRef]); | |||||
const playMedia = React.useCallback(() => { | |||||
setIsPlaying((p) => !p); | |||||
}, []); | |||||
mediaControllerRef.current.pause(); | |||||
}, [isPlaying, mediaControllerRef]); | |||||
return React.useMemo(() => ({ | return React.useMemo(() => ({ | ||||
mediaContainerRef, | |||||
playMedia, | |||||
mediaControllerRef, | |||||
refreshControls, | |||||
reset: reset, | |||||
updateSeekFromPlayback, | |||||
isPlaying, | isPlaying, | ||||
isSeeking, | |||||
currentTimeDisplay, | |||||
seekTimeDisplay, | |||||
durationDisplay, | |||||
isSeekTimeCountingDown, | |||||
volumeRef, | |||||
adjustVolume, | |||||
filenameRef, | |||||
handleAction, | |||||
}), [ | }), [ | ||||
mediaContainerRef, | |||||
playMedia, | |||||
mediaControllerRef, | |||||
refreshControls, | |||||
reset, | |||||
updateSeekFromPlayback, | |||||
isPlaying, | isPlaying, | ||||
isSeeking, | |||||
currentTimeDisplay, | |||||
seekTimeDisplay, | |||||
durationDisplay, | |||||
isSeekTimeCountingDown, | |||||
volumeRef, | |||||
adjustVolume, | |||||
filenameRef, | |||||
handleAction, | |||||
]); | ]); | ||||
}; | }; |
@@ -1,16 +1,16 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
export interface UseVideoControlsOptions { | |||||
mediaControllerRef: React.Ref<HTMLVideoElement>; | |||||
export interface UseMediaControlsOptions<T extends HTMLMediaElement> { | |||||
controllerRef: React.Ref<T>; | |||||
actionFormKey?: string; | actionFormKey?: string; | ||||
} | } | ||||
export const useVideoControls = ({ | |||||
mediaControllerRef: forwardedRef, | |||||
export const useMediaControls = <T extends HTMLMediaElement>({ | |||||
controllerRef: forwardedRef, | |||||
actionFormKey = 'action' as const, | actionFormKey = 'action' as const, | ||||
}: UseVideoControlsOptions) => { | |||||
const defaultRef = React.useRef<HTMLVideoElement>(null); | |||||
const mediaControllerRef = forwardedRef ?? defaultRef; | |||||
}: UseMediaControlsOptions<T>) => { | |||||
const defaultRef = React.useRef<T>(null); | |||||
const ref = forwardedRef ?? defaultRef; | |||||
const seekRef = React.useRef<HTMLInputElement>(null); | const seekRef = React.useRef<HTMLInputElement>(null); | ||||
const volumeRef = React.useRef<HTMLInputElement>(null); | const volumeRef = React.useRef<HTMLInputElement>(null); | ||||
const filenameRef = React.useRef<HTMLElement>(null); | const filenameRef = React.useRef<HTMLElement>(null); | ||||
@@ -21,16 +21,12 @@ export const useVideoControls = ({ | |||||
const [durationDisplay, setDurationDisplay] = React.useState<number>(); | const [durationDisplay, setDurationDisplay] = React.useState<number>(); | ||||
const [isSeekTimeCountingDown, setIsSeekTimeCountingDown] = React.useState(false); | const [isSeekTimeCountingDown, setIsSeekTimeCountingDown] = React.useState(false); | ||||
const refreshControls: React.ReactEventHandler<HTMLVideoElement> = (e) => { | |||||
const refreshControls: React.ReactEventHandler<T> = React.useCallback((e) => { | |||||
const { currentTarget: mediaController } = e; | const { currentTarget: mediaController } = e; | ||||
const { current: seek } = seekRef; | |||||
if (!seek) { | |||||
return; | |||||
} | |||||
setCurrentTimeDisplay(mediaController.currentTime); | setCurrentTimeDisplay(mediaController.currentTime); | ||||
setDurationDisplay(mediaController.duration); | setDurationDisplay(mediaController.duration); | ||||
}; | |||||
}, []); | |||||
const togglePlayback = React.useCallback(() => { | const togglePlayback = React.useCallback(() => { | ||||
setIsPlaying((p) => !p); | setIsPlaying((p) => !p); | ||||
@@ -40,16 +36,16 @@ export const useVideoControls = ({ | |||||
setIsSeeking(true); | setIsSeeking(true); | ||||
}, []); | }, []); | ||||
const doSetSeek = (thisElement: HTMLInputElement, mediaController: HTMLVideoElement) => { | |||||
const doSetSeek = React.useCallback((thisElement: HTMLInputElement, mediaController: HTMLMediaElement) => { | |||||
mediaController.currentTime = thisElement.valueAsNumber; | mediaController.currentTime = thisElement.valueAsNumber; | ||||
}; | |||||
}, []); | |||||
const setSeek: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | const setSeek: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | ||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
if (!(typeof ref === 'object' && ref !== null)) { | |||||
return; | return; | ||||
} | } | ||||
const { current: mediaController } = mediaControllerRef; | |||||
const { current: mediaController } = ref; | |||||
if (!mediaController) { | if (!mediaController) { | ||||
return; | return; | ||||
} | } | ||||
@@ -62,14 +58,14 @@ export const useVideoControls = ({ | |||||
} | } | ||||
doSetSeek(thisElement, mediaController); | doSetSeek(thisElement, mediaController); | ||||
}, [mediaControllerRef, isSeeking]); | |||||
}, [ref, isSeeking, doSetSeek]); | |||||
const endSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => { | const endSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => { | ||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
if (!(typeof ref === 'object' && ref !== null)) { | |||||
return; | return; | ||||
} | } | ||||
const { current: mediaController } = mediaControllerRef; | |||||
const { current: mediaController } = ref; | |||||
if (!mediaController) { | if (!mediaController) { | ||||
return; | return; | ||||
@@ -78,32 +74,30 @@ export const useVideoControls = ({ | |||||
const { currentTarget: thisElement } = e; | const { currentTarget: thisElement } = e; | ||||
setIsSeeking(false); | setIsSeeking(false); | ||||
doSetSeek(thisElement, mediaController); | doSetSeek(thisElement, mediaController); | ||||
}, [mediaControllerRef]); | |||||
}, [ref, doSetSeek]); | |||||
const resetVideo: React.ReactEventHandler<HTMLVideoElement> = React.useCallback((e) => { | |||||
const reset: React.ReactEventHandler<T> = React.useCallback((e) => { | |||||
const videoElement = e.currentTarget; | const videoElement = e.currentTarget; | ||||
setIsPlaying(false); | setIsPlaying(false); | ||||
videoElement.currentTime = 0; | videoElement.currentTime = 0; | ||||
}, []); | }, []); | ||||
const updateSeekFromPlayback: React.ReactEventHandler<HTMLVideoElement> = React.useCallback((e) => { | |||||
if (!seekRef.current) { | |||||
const updateSeekFromPlayback: React.ReactEventHandler<T> = React.useCallback((e) => { | |||||
if (isSeeking) { | |||||
return; | return; | ||||
} | } | ||||
if (isSeeking) { | |||||
const videoElement = e.currentTarget; | |||||
const currentTime = videoElement.currentTime; | |||||
setCurrentTimeDisplay(currentTime); | |||||
if (!seekRef.current) { | |||||
return; | return; | ||||
} | } | ||||
const { current: seek } = seekRef; | const { current: seek } = seekRef; | ||||
if (!seek) { | if (!seek) { | ||||
return; | return; | ||||
} | } | ||||
const videoElement = e.currentTarget; | |||||
const currentTime = videoElement.currentTime; | |||||
setCurrentTimeDisplay(currentTime); | |||||
seek.value = String(currentTime); | seek.value = String(currentTime); | ||||
}, [isSeeking]); | }, [isSeeking]); | ||||
@@ -112,7 +106,7 @@ export const useVideoControls = ({ | |||||
}, []); | }, []); | ||||
const download = React.useCallback(() => { | const download = React.useCallback(() => { | ||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef?.current !== null)) { | |||||
if (!(typeof ref === 'object' && ref?.current !== null)) { | |||||
return; | return; | ||||
} | } | ||||
@@ -122,12 +116,12 @@ export const useVideoControls = ({ | |||||
const downloadLink = window.document.createElement('a'); | const downloadLink = window.document.createElement('a'); | ||||
downloadLink.download = filenameRef.current.textContent ?? 'image'; | downloadLink.download = filenameRef.current.textContent ?? 'image'; | ||||
downloadLink.href = mediaControllerRef.current.currentSrc; | |||||
downloadLink.href = ref.current.currentSrc; | |||||
downloadLink.addEventListener('click', () => { | downloadLink.addEventListener('click', () => { | ||||
downloadLink.remove(); | downloadLink.remove(); | ||||
}); | }); | ||||
downloadLink.click(); | downloadLink.click(); | ||||
}, [mediaControllerRef, filenameRef]); | |||||
}, [ref, filenameRef]); | |||||
const actions = React.useMemo(() => ({ | const actions = React.useMemo(() => ({ | ||||
togglePlayback, | togglePlayback, | ||||
@@ -144,33 +138,33 @@ export const useVideoControls = ({ | |||||
}, [actions, actionFormKey]); | }, [actions, actionFormKey]); | ||||
const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { | ||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
if (!(typeof ref === 'object' && ref !== null)) { | |||||
return; | return; | ||||
} | } | ||||
if (!mediaControllerRef.current) { | |||||
if (!ref.current) { | |||||
return; | return; | ||||
} | } | ||||
const { value } = e.currentTarget; | const { value } = e.currentTarget; | ||||
mediaControllerRef.current.volume = Number(value); | |||||
}, [mediaControllerRef]); | |||||
ref.current.volume = Number(value); | |||||
}, [ref]); | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
if (!(typeof ref === 'object' && ref !== null)) { | |||||
return; | return; | ||||
} | } | ||||
if (!mediaControllerRef.current) { | |||||
if (!ref.current) { | |||||
return; | return; | ||||
} | } | ||||
if (isPlaying) { | if (isPlaying) { | ||||
void mediaControllerRef.current.play(); | |||||
void ref.current.play(); | |||||
return | return | ||||
} | } | ||||
mediaControllerRef.current.pause(); | |||||
}, [isPlaying, mediaControllerRef]); | |||||
ref.current.pause(); | |||||
}, [isPlaying, ref]); | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (!seekRef.current) { | if (!seekRef.current) { | ||||
@@ -203,18 +197,18 @@ export const useVideoControls = ({ | |||||
return; | return; | ||||
} | } | ||||
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) { | |||||
if (!(typeof ref === 'object' && ref !== null)) { | |||||
return; | return; | ||||
} | } | ||||
if (!mediaControllerRef.current) { | |||||
if (!ref.current) { | |||||
return; | return; | ||||
} | } | ||||
const { current: mediaController } = mediaControllerRef; | |||||
const { current: mediaController } = ref; | |||||
const { current: volume } = volumeRef; | const { current: volume } = volumeRef; | ||||
volume.value = String(mediaController.volume); | volume.value = String(mediaController.volume); | ||||
}, [mediaControllerRef]); | |||||
}, [ref]); | |||||
return React.useMemo(() => ({ | return React.useMemo(() => ({ | ||||
seekRef, | seekRef, | ||||
@@ -222,7 +216,7 @@ export const useVideoControls = ({ | |||||
isPlaying, | isPlaying, | ||||
refreshControls, | refreshControls, | ||||
adjustVolume, | adjustVolume, | ||||
resetVideo, | |||||
reset, | |||||
startSeek, | startSeek, | ||||
endSeek, | endSeek, | ||||
setSeek, | setSeek, | ||||
@@ -232,14 +226,15 @@ export const useVideoControls = ({ | |||||
seekTimeDisplay, | seekTimeDisplay, | ||||
isSeeking, | isSeeking, | ||||
isSeekTimeCountingDown, | isSeekTimeCountingDown, | ||||
mediaControllerRef, | |||||
mediaControllerRef: ref, | |||||
handleAction, | handleAction, | ||||
filenameRef, | filenameRef, | ||||
}), [ | }), [ | ||||
refreshControls, | |||||
isPlaying, | isPlaying, | ||||
isSeeking, | isSeeking, | ||||
adjustVolume, | adjustVolume, | ||||
resetVideo, | |||||
reset, | |||||
startSeek, | startSeek, | ||||
endSeek, | endSeek, | ||||
setSeek, | setSeek, | ||||
@@ -248,7 +243,7 @@ export const useVideoControls = ({ | |||||
currentTimeDisplay, | currentTimeDisplay, | ||||
seekTimeDisplay, | seekTimeDisplay, | ||||
isSeekTimeCountingDown, | isSeekTimeCountingDown, | ||||
mediaControllerRef, | |||||
ref, | |||||
handleAction, | handleAction, | ||||
filenameRef, | filenameRef, | ||||
]); | ]); | ||||
@@ -1,7 +1,6 @@ | |||||
//export * from './components/AudioFilePreview'; | |||||
export * from './components/AudioFilePreview'; | |||||
//export * from './components/AudioMiniFilePreview'; | //export * from './components/AudioMiniFilePreview'; | ||||
//export * from './components/BinaryFilePreview'; | //export * from './components/BinaryFilePreview'; | ||||
//export * from './components/BinaryFilePreview'; | |||||
//export * from './components/FileSelectBox'; | //export * from './components/FileSelectBox'; | ||||
export * from './components/ImageFilePreview'; | export * from './components/ImageFilePreview'; | ||||
//export * from './components/TextFilePreview'; | //export * from './components/TextFilePreview'; | ||||
@@ -0,0 +1,109 @@ | |||||
import * as React from 'react'; | |||||
type WaveSurferCanvasDerivedComponent = HTMLAudioElement; | |||||
export interface WaveSurferCanvasProps extends React.HTMLProps<WaveSurferCanvasDerivedComponent> {} | |||||
export const WaveSurferCanvas = React.forwardRef<WaveSurferCanvasDerivedComponent, WaveSurferCanvasProps>(({ | |||||
className, | |||||
children, | |||||
controls, | |||||
...etcProps | |||||
}, forwardedRef) => { | |||||
const [isPlaying, setIsPlaying] = React.useState(false); | |||||
const defaultRef = React.useRef<HTMLAudioElement>(null); | |||||
const ref = forwardedRef ?? defaultRef; | |||||
const containerRef = React.useRef<HTMLDivElement>(null); | |||||
const waveSurferRef = React.useRef<any>(null); | |||||
const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => { | |||||
e.preventDefault(); | |||||
const formData = new FormData(e.currentTarget, (e.nativeEvent as unknown as { submitter: HTMLElement }).submitter); | |||||
const action = formData.get('action'); | |||||
switch (action) { | |||||
case 'togglePlayback': | |||||
setIsPlaying((prev) => !prev); | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
}; | |||||
React.useEffect(() => { | |||||
const { current: container } = containerRef; | |||||
const media = typeof ref === 'object' ? ref?.current : null; | |||||
const { current: waveSurferCurrent } = waveSurferRef; | |||||
const load = async (ref: React.Ref<HTMLAudioElement>) => { | |||||
if (!(typeof ref === 'object' && ref?.current)) { | |||||
return; | |||||
} | |||||
if (!(typeof containerRef === 'object' && containerRef?.current)) { | |||||
return; | |||||
} | |||||
const { default: WaveSurfer } = await import('wavesurfer.js'); | |||||
const waveSurferInstance = WaveSurfer.create({ | |||||
container: containerRef.current, | |||||
barWidth: 2, | |||||
barGap: 2, | |||||
height: containerRef.current.clientHeight, | |||||
fillParent: true, | |||||
media, | |||||
} as any); | |||||
waveSurferInstance.load(ref.current.currentSrc); | |||||
waveSurferInstance.on('ready', () => { | |||||
if (!container) { | |||||
return; | |||||
} | |||||
while (container.children.length > 1) { | |||||
container.removeChild(container.children[0]); | |||||
} | |||||
}); | |||||
waveSurferRef.current = waveSurferInstance; | |||||
}; | |||||
void load(ref); | |||||
return () => { | |||||
if (waveSurferCurrent) { | |||||
(waveSurferCurrent as unknown as Record<string, Function>).destroy(); | |||||
} | |||||
}; | |||||
}, [ref]); | |||||
return ( | |||||
<div | |||||
className="relative h-full w-full flex flex-col" | |||||
> | |||||
<audio | |||||
{...etcProps} | |||||
controls={controls} | |||||
className={className} | |||||
ref={ref} | |||||
> | |||||
{children} | |||||
</audio> | |||||
<div | |||||
className="flex-auto relative" | |||||
> | |||||
<div className="absolute top-0 left-0 w-full h-full" | |||||
ref={containerRef} | |||||
/> | |||||
</div> | |||||
{controls && ( | |||||
<form | |||||
onSubmit={handleAction} | |||||
> | |||||
<button | |||||
type="submit" | |||||
name="action" | |||||
value="togglePlayback" | |||||
> | |||||
{isPlaying ? '⏸' : '▶'} | |||||
</button> | |||||
</form> | |||||
)} | |||||
</div> | |||||
); | |||||
}); | |||||
WaveSurferCanvas.displayName = 'WavesurferCanvas'; |
@@ -0,0 +1,10 @@ | |||||
export * from './WaveSurferCanvas'; | |||||
export interface WaveSurfer extends Omit<HTMLAudioElement, 'load'> { | |||||
play: () => Promise<void>; | |||||
pause: () => void; | |||||
on: (event: string, callback: () => void) => void; | |||||
seekTo: (time: number) => void; | |||||
load: (url: string) => void; | |||||
destroy: () => void; | |||||
} |
@@ -27,6 +27,17 @@ const BlobPage: NextPage = () => { | |||||
}); | }); | ||||
}, []); | }, []); | ||||
const [audioFile, setAudioFile] = React.useState<File>(); | |||||
React.useEffect(() => { | |||||
fetch('/audio.wav').then((response) => { | |||||
response.blob().then((blob) => { | |||||
setAudioFile(new File([blob], 'audio.wav', { | |||||
type: 'audio/wav', | |||||
})); | |||||
}); | |||||
}); | |||||
}, []); | |||||
return ( | return ( | ||||
<DefaultLayout> | <DefaultLayout> | ||||
<Section title="ImageFilePreview"> | <Section title="ImageFilePreview"> | ||||
@@ -46,6 +57,15 @@ const BlobPage: NextPage = () => { | |||||
/> | /> | ||||
</Subsection> | </Subsection> | ||||
</Section> | </Section> | ||||
<Section title="AudioFilePreview"> | |||||
<Subsection title="Single File"> | |||||
<BlobReact.AudioFilePreview | |||||
file={audioFile} | |||||
className="sm:h-64" | |||||
enhanced | |||||
/> | |||||
</Subsection> | |||||
</Section> | |||||
<Section title="FileSelectBox"> | <Section title="FileSelectBox"> | ||||
<Subsection title="Single File"> | <Subsection title="Single File"> | ||||
{/*<BlobReact.FileSelectBox*/} | {/*<BlobReact.FileSelectBox*/} | ||||
@@ -29,6 +29,7 @@ const MIME_TYPE_DESCRIPTIONS = { | |||||
'application/postscript': 'PostScript Document', | 'application/postscript': 'PostScript Document', | ||||
'application/epub+zip': 'EPUB Document', | 'application/epub+zip': 'EPUB Document', | ||||
'message/rfc822': 'Email Message', | 'message/rfc822': 'Email Message', | ||||
'video/mp4': 'MP4 Video', | |||||
} as const; | } as const; | ||||
const EXTENSION_DESCRIPTIONS = { | const EXTENSION_DESCRIPTIONS = { | ||||
@@ -207,7 +208,7 @@ export interface AudioFile extends FileWithResolvedType<ContentType.AUDIO> { | |||||
metadata?: AudioFileMetadata; | metadata?: AudioFileMetadata; | ||||
} | } | ||||
const augmentAudioFile = async (f: File): Promise<AudioFile> => { | |||||
export const augmentAudioFile = async (f: File): Promise<AudioFile> => { | |||||
const previewUrl = await readAsDataURL(f); | const previewUrl = await readAsDataURL(f); | ||||
const audioExtensions = await getAudioMetadata(previewUrl, f.type) as AudioFileMetadata; | const audioExtensions = await getAudioMetadata(previewUrl, f.type) as AudioFileMetadata; | ||||
return { | return { | ||||