浏览代码

Implement video file preview

Implement video file preview interactive controls.
pull/1/head
父节点
当前提交
50bb3b62e7
共有 12 个文件被更改,包括 393 次插入1914 次删除
  1. 二进制
      packages/web-kitchensink-reactnext/public/image.png
  2. +16
    -82
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx
  3. +183
    -116
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx
  4. +1
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/index.ts
  5. +31
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts
  6. +56
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/image.ts
  7. +1
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/index.ts
  8. +49
    -11
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/video.ts
  9. +3
    -2
      packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts
  10. +26
    -1702
      packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx
  11. +5
    -1
      packages/web-kitchensink-reactnext/src/utils/blob.ts
  12. +22
    -0
      packages/web-kitchensink-reactnext/src/utils/video.ts

二进制
packages/web-kitchensink-reactnext/public/image.png 查看文件

之前 之后
宽度: 75  |  高度: 106  |  大小: 20 KiB 宽度: 2480  |  高度: 3508  |  大小: 16 MiB

+ 16
- 82
packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx 查看文件

@@ -2,6 +2,7 @@ import * as React from 'react';
import {augmentImageFile, getMimeTypeDescription, ImageFile} from '@/utils/blob';
import {formatFileSize, formatNumeral} from '@/utils/numeral';
import clsx from 'clsx';
import {useAugmentedFile, useImageControls} from '@/categories/blob/react';

type ImageFilePreviewDerivedComponent = HTMLImageElement;

@@ -10,90 +11,19 @@ export interface ImageFilePreviewProps extends Omit<React.HTMLProps<ImageFilePre
disabled?: boolean;
}

const useImageFilePreview = (file?: File) => {
const [augmentedFile, setAugmentedFile] = React.useState<ImageFile>();
const [error, setError] = React.useState<Error>();

React.useEffect(() => {
if (!file) {
return;
}

augmentImageFile(file)
.then((theAugmentedFile) => {
setAugmentedFile(theAugmentedFile);
})
.catch((error) => {
setError(error);
});
}, [file]);

return React.useMemo(() => ({
augmentedFile: (augmentedFile ?? file) as ImageFile | undefined,
error,
}), [augmentedFile, file, error]);
};

interface UseImageControlsOptions {
file?: ImageFile;
actionFormKey?: string;
}

const useImageControls = (options = {} as UseImageControlsOptions) => {
const { actionFormKey = 'action' as const, file } = options;
const [fullScreen, setFullScreen] = React.useState(false);

const toggleFullScreen = React.useCallback(() => {
setFullScreen((b) => !b);
}, []);

const download = React.useCallback(() => {
if (!file) {
return;
}

if (!file.metadata?.previewUrl) {
return;
}

const downloadLink = window.document.createElement('a');
downloadLink.download = file.name ?? 'file';
downloadLink.href = file.metadata.previewUrl;
downloadLink.addEventListener('click', () => {
downloadLink.remove();
});
downloadLink.click();
}, [file]);

const actions = React.useMemo(() => ({
toggleFullScreen,
download,
}), [toggleFullScreen, 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]);

return React.useMemo(() => ({
fullScreen,
handleAction,
}), [fullScreen, handleAction]);
};

export const ImageFilePreview: React.FC<ImageFilePreviewProps> = ({
export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponent, ImageFilePreviewProps>(({
file,
className,
style,
disabled = false,
...etcProps
}) => {
const { augmentedFile, error } = useImageFilePreview(file);
const { fullScreen, handleAction } = useImageControls({
file: augmentedFile,
}, forwardedRef) => {
const { augmentedFile, error } = useAugmentedFile<ImageFile>({
file,
augmentFunction: augmentImageFile,
});
const { fullScreen, handleAction, imageRef, filenameRef } = useImageControls({
forwardedRef,
});

if (!augmentedFile) {
@@ -109,12 +39,13 @@ export const ImageFilePreview: React.FC<ImageFilePreviewProps> = ({
style={style}
>
<div className="h-full relative">
<div className="sm:absolute top-0 left-0 w-full sm:h-full">
<div className="sm:absolute top-0 left-0 w-full sm:h-full z-[1]">
{
typeof augmentedFile.metadata?.previewUrl === 'string'
&& (
<img
{...etcProps}
ref={imageRef}
className={clsx(
'block h-full max-w-full object-center bg-[#000000]',
{
@@ -149,6 +80,7 @@ export const ImageFilePreview: React.FC<ImageFilePreviewProps> = ({
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={augmentedFile.name}
ref={filenameRef}
>
{augmentedFile.name}
</dd>
@@ -212,7 +144,7 @@ export const ImageFilePreview: React.FC<ImageFilePreviewProps> = ({
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'fixed top-0 left-0 w-full h-full opacity-0': fullScreen,
'fixed top-0 left-0 w-full h-full opacity-0 z-[1]': fullScreen,
}
)}
>
@@ -244,4 +176,6 @@ export const ImageFilePreview: React.FC<ImageFilePreviewProps> = ({
</div>
</div>
);
}
});

ImageFilePreview.displayName = 'ImageFilePreview';

+ 183
- 116
packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx 查看文件

@@ -1,23 +1,34 @@
import * as React from 'react';
import {getMimeTypeDescription, VideoFile} from '@/utils/blob';
import {augmentVideoFile, getMimeTypeDescription, VideoFile} from '@/utils/blob';
import {formatFileSize, formatNumeral, formatSecondsDurationConcise} from '@/utils/numeral';
import {useVideoControls} from '@tesseract-design/web-blob-react';
import {useAugmentedFile, useVideoControls} from '@tesseract-design/web-blob-react';
import clsx from 'clsx';

export interface VideoFilePreviewProps {
file: VideoFile;
type VideoFilePreviewDerivedComponent = HTMLVideoElement;

export interface VideoFilePreviewProps extends Omit<React.HTMLProps<VideoFilePreviewDerivedComponent>, 'controls'> {
file?: File;
disabled?: boolean;
enhanced?: boolean;
}

export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePreviewProps>(({
file: f,
file,
className,
style,
disabled = false,
enhanced = false,
...etcProps
}, forwardedRef) => {
const defaultRef = React.useRef<HTMLVideoElement>(null);
const mediaControllerRef = forwardedRef ?? defaultRef;
const { augmentedFile, error } = useAugmentedFile<VideoFile>({
file,
augmentFunction: augmentVideoFile,
});
const {
seekRef,
volumeRef,
isPlaying,
adjustVolume,
playMedia,
resetVideo,
startSeek,
endSeek,
@@ -29,19 +40,32 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev
seekTimeDisplay = 0,
isSeeking,
isSeekTimeCountingDown,
toggleSeekTimeCountMode,
} = useVideoControls({
mediaControllerRef,
handleAction,
filenameRef,
} = useVideoControls({
mediaControllerRef: forwardedRef,
});
const formId = React.useId();

if (!augmentedFile) {
return null;
}

const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay;
const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay;

return (
<div className="flex gap-4 w-full h-full">
<div className={`h-full w-1/3 flex-shrink-0`}>
<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?.previewUrl === 'string'
typeof augmentedFile.metadata?.previewUrl === 'string'
&& (
<div
className="w-full h-full bg-black flex flex-col items-stretch"
@@ -49,130 +73,173 @@ export const VideoFilePreview = React.forwardRef<HTMLVideoElement, VideoFilePrev
>
<div className="w-full flex-auto relative">
<video
className="absolute w-full h-full top-0 left-0 block object-center object-contain flex-auto"
ref={mediaControllerRef as React.RefObject<HTMLVideoElement>}
{...etcProps}
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={resetVideo}
onTimeUpdate={updateSeekFromPlayback}
data-testid="preview"
controls={!enhanced}
>
<source
src={f.metadata.previewUrl}
type={f.type}
src={augmentedFile.metadata.previewUrl}
type={augmentedFile.type}
/>
</video>
</div>
<div className="w-full flex-shrink-0 h-10 flex gap-4">
<button
onClick={playMedia}
className="w-10 h-full flex-shrink-0"
type="button"
>
{isPlaying ? '⏸' : '▶'}
</button>
<div className="flex-auto w-full flex items-center gap-1 font-mono text-sm relative">
{enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4">
<button
className="absolute overflow-hidden w-12 opacity-0 h-10"
onClick={toggleSeekTimeCountMode}
title="Toggle Seek Time Count Mode"
type="button"
className="w-10 h-full flex-shrink-0 text-primary"
type="submit"
name="action"
value="togglePlayback"
form={formId}
>
Toggle Seek Time Count Mode
{isPlaying ? '⏸' : '▶'}
</button>
<span
className="font-mono before:block before:content-[attr(title)] contents"
title={
`${
isSeekTimeCountingDown ? '-' : '+'
}${
isSeeking
? formatSecondsDurationConcise(finalSeekTimeDisplay)
: formatSecondsDurationConcise(finalCurrentTimeDisplay)
}`
}
>
<input
type="range"
className="flex-auto w-full"
ref={seekRef}
onMouseDown={startSeek}
onMouseUp={endSeek}
onChange={setSeek}
defaultValue="0"

/>
</span>
<span>
<div className="flex-auto w-full flex items-center gap-1 font-mono 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="font-mono 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"
/>
</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>
<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 className="w-2/3 flex-shrink-0 m-0" 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`}
<div
className="flex-shrink-0 m-0 flex flex-col gap-4 justify-between"
>
<dl data-testid="infoBox">
<div className="w-full font-bold">
<dt className="sr-only">
Name
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
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?.width === 'number'
&& typeof augmentedFile.metadata?.height === 'number'
&& (
<div>
<dt className="sr-only">
Pixel Dimensions
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
>
{formatNumeral(augmentedFile.metadata.width)}&times;{formatNumeral(augmentedFile.metadata.height)} pixels
</dd>
</div>
)
}
</dl>
<form
id={formId}
onSubmit={handleAction}
className="flex gap-4"
>
<fieldset
disabled={disabled || typeof error !== 'undefined'}
className="contents"
>
{formatFileSize(f.size)}
</dd>
</div>
{
typeof f.metadata?.width === 'number'
&& typeof f.metadata?.height === 'number'
&& (
<div>
<dt className="sr-only">
Pixel Dimensions
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
<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"
>
{formatNumeral(f.metadata.width)}&times;{formatNumeral(f.metadata.height)} pixels
</dd>
</div>
)
}
</dl>
Download
</span>
</button>
</fieldset>
</form>
</div>
</div>
);
});


+ 1
- 0
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/index.ts 查看文件

@@ -0,0 +1 @@
export * from './metadata';

+ 31
- 0
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts 查看文件

@@ -0,0 +1,31 @@
import * as React from 'react';

export interface UseAugmentedFileOptions<T extends Partial<File> = Partial<File>> {
file?: File;
augmentFunction: (file: File) => Promise<T>;
}

export const useAugmentedFile = <T extends Partial<File>>(options = {} as UseAugmentedFileOptions<T>) => {
const { file, augmentFunction } = options;
const [augmentedFile, setAugmentedFile] = React.useState<T>();
const [error, setError] = React.useState<Error>();

React.useEffect(() => {
if (!file) {
return;
}

augmentFunction(file)
.then((theAugmentedFile) => {
setAugmentedFile(theAugmentedFile);
})
.catch((error) => {
setError(error);
});
}, [file]);

return React.useMemo(() => ({
augmentedFile: (augmentedFile ?? file) as T | undefined,
error,
}), [augmentedFile, file, error]);
};

+ 56
- 0
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/image.ts 查看文件

@@ -0,0 +1,56 @@
import * as React from 'react';

export interface UseImageControlsOptions {
actionFormKey?: string;
forwardedRef?: React.Ref<HTMLImageElement>;
}

export const useImageControls = (options = {} as UseImageControlsOptions) => {
const { actionFormKey = 'action' as const, forwardedRef } = options;
const [fullScreen, setFullScreen] = React.useState(false);
const defaultRef = React.useRef<HTMLImageElement>(null);
const imageRef = forwardedRef ?? defaultRef;
const filenameRef = React.useRef<HTMLElement>(null);

const toggleFullScreen = React.useCallback(() => {
setFullScreen((b) => !b);
}, []);

const download = React.useCallback(() => {
if (!(typeof imageRef === 'object' && imageRef?.current !== null)) {
return;
}

if (!(typeof filenameRef === 'object' && filenameRef?.current !== null)) {
return;
}

const downloadLink = window.document.createElement('a');
downloadLink.download = filenameRef.current.textContent ?? 'image';
downloadLink.href = imageRef.current.src;
downloadLink.addEventListener('click', () => {
downloadLink.remove();
});
downloadLink.click();
}, [imageRef, filenameRef]);

const actions = React.useMemo(() => ({
toggleFullScreen,
download,
}), [toggleFullScreen, 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]);

return React.useMemo(() => ({
fullScreen,
handleAction,
imageRef,
filenameRef,
}), [fullScreen, handleAction, imageRef, filenameRef]);
};

+ 1
- 0
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/index.ts 查看文件

@@ -1,2 +1,3 @@
export * from './audio';
export * from './video';
export * from './image';

+ 49
- 11
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/media/video.ts 查看文件

@@ -2,13 +2,18 @@ import * as React from 'react';

export interface UseVideoControlsOptions {
mediaControllerRef: React.Ref<HTMLVideoElement>;
actionFormKey?: string;
}

export const useVideoControls = ({
mediaControllerRef,
mediaControllerRef: forwardedRef,
actionFormKey = 'action' as const,
}: UseVideoControlsOptions) => {
const defaultRef = React.useRef<HTMLVideoElement>(null);
const mediaControllerRef = forwardedRef ?? defaultRef;
const seekRef = React.useRef<HTMLInputElement>(null);
const volumeRef = React.useRef<HTMLInputElement>(null);
const filenameRef = React.useRef<HTMLElement>(null);
const [isPlaying, setIsPlaying] = React.useState(false);
const [isSeeking, setIsSeeking] = React.useState(false);
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>();
@@ -27,7 +32,7 @@ export const useVideoControls = ({
setDurationDisplay(mediaController.duration);
};

const playMedia = React.useCallback(() => {
const togglePlayback = React.useCallback(() => {
setIsPlaying((p) => !p);
}, []);

@@ -102,6 +107,42 @@ export const useVideoControls = ({
seek.value = String(currentTime);
}, [isSeeking]);

const toggleSeekTimeCountMode = React.useCallback(() => {
setIsSeekTimeCountingDown((b) => !b);
}, []);

const download = React.useCallback(() => {
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef?.current !== null)) {
return;
}

if (!(typeof filenameRef === 'object' && filenameRef?.current !== null)) {
return;
}

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]);

const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) {
return;
@@ -114,11 +155,6 @@ export const useVideoControls = ({
mediaControllerRef.current.volume = Number(value);
}, [mediaControllerRef]);

const toggleSeekTimeCountMode: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((e) => {
e.preventDefault();
setIsSeekTimeCountingDown((b) => !b);
}, []);

React.useEffect(() => {
if (!(typeof mediaControllerRef === 'object' && mediaControllerRef !== null)) {
return;
@@ -191,18 +227,18 @@ export const useVideoControls = ({
endSeek,
setSeek,
updateSeekFromPlayback,
playMedia,
durationDisplay,
currentTimeDisplay,
seekTimeDisplay,
isSeeking,
isSeekTimeCountingDown,
toggleSeekTimeCountMode,
mediaControllerRef,
handleAction,
filenameRef,
}), [
isPlaying,
isSeeking,
adjustVolume,
playMedia,
resetVideo,
startSeek,
endSeek,
@@ -212,6 +248,8 @@ export const useVideoControls = ({
currentTimeDisplay,
seekTimeDisplay,
isSeekTimeCountingDown,
toggleSeekTimeCountMode,
mediaControllerRef,
handleAction,
filenameRef,
]);
};

+ 3
- 2
packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts 查看文件

@@ -5,6 +5,7 @@
//export * from './components/FileSelectBox';
export * from './components/ImageFilePreview';
//export * from './components/TextFilePreview';
//export * from './components/VideoFilePreview';
export * from './components/VideoFilePreview';

//export * from './hooks/media';
export * from './hooks/blob';
export * from './hooks/media';

+ 26
- 1702
packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx
文件差异内容过多而无法显示
查看文件


+ 5
- 1
packages/web-kitchensink-reactnext/src/utils/blob.ts 查看文件

@@ -3,6 +3,7 @@ import Blob from '../pages/categories/blob';
import {getTextMetadata} from './text';
import {getImageMetadata} from './image';
import {getAudioMetadata} from './audio';
import {getVideoMetadata} from '@/utils/video';

const MIME_TYPE_DESCRIPTIONS = {
'image/gif': 'GIF Image',
@@ -256,8 +257,9 @@ export interface VideoFile extends FileWithResolvedType<ContentType.VIDEO> {
metadata?: VideoFileMetadata;
}

const augmentVideoFile = async (f: File): Promise<VideoFile> => {
export const augmentVideoFile = async (f: File): Promise<VideoFile> => {
const previewUrl = await readAsDataURL(f);
const videoMetadata = await getVideoMetadata(previewUrl);
return {
name: f.name,
type: f.type,
@@ -267,6 +269,8 @@ const augmentVideoFile = async (f: File): Promise<VideoFile> => {
originalFile: f,
metadata: {
previewUrl,
width: videoMetadata.width as number,
height: videoMetadata.height as number,
},
};
};


+ 22
- 0
packages/web-kitchensink-reactnext/src/utils/video.ts 查看文件

@@ -0,0 +1,22 @@
export const getVideoMetadata = (videoUrl: string) => new Promise<Record<string, string | number>>((resolve, reject) => {
const video = window.document.createElement('video');
const source = window.document.createElement('source');

video.addEventListener('loadedmetadata', (videoLoadEvent) => {
const thisVideo = videoLoadEvent.currentTarget as HTMLVideoElement;
const metadata = {
width: thisVideo.videoWidth,
height: thisVideo.videoHeight,
};
video.remove();
resolve(metadata);
});

video.addEventListener('error', () => {
reject(new Error('Could not load file as video'));
video.remove();
});

source.src = videoUrl;
video.appendChild(source);
});

正在加载...
取消
保存