diff --git a/README.md b/README.md index 59a1acb5..6de5c1ef 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ More information on signed URLs is available on Mux's [docs](https://docs.mux.co ### MP4 support (downloadable videos or offline viewing) -To enable [static MP4 renditions](https://docs.mux.com/guides/video/enable-static-mp4-renditions), add `mp4_support: 'standard'` to the `options` of your `mux.video` schema type. +To enable [static MP4 renditions](https://docs.mux.com/guides/video/enable-static-mp4-renditions), add `mp4_support: 'standard'` to the `options` of your `mux.video` schema type. Set default with `mp4_support: 'standard'` in the plugin configuration. ```js import {muxInput} from 'sanity-plugin-mux-input' @@ -214,7 +214,7 @@ MP4 allows users to download videos for later or offline viewing. More informati ### Video resolution (max_resolution_tier) -To edit [max_resolution_tier](https://docs.mux.com/api-reference#video/operation/create-direct-upload) to support other resolutions other than 1080p, add `max_resolution_tier: '1080p' | '1440p' | '2160p'` to the `options` of your `mux.video` schema type. Defaults to `1080p`. +To edit [max_resolution_tier](https://docs.mux.com/api-reference#video/operation/create-direct-upload) to support other resolutions other than 1080p, add `max_resolution_tier: '1080p' | '1440p' | '2160p'` to the `options` of your `mux.video` schema type. Defaults to `1080p`. Set default with `max_resolution_tier: '1080p' | '1440p' | '2160p'` in the plugin configuration. ```js import {muxInput} from 'sanity-plugin-mux-input' @@ -224,8 +224,42 @@ export default defineConfig({ }) ``` +```js +export default { + title: 'Main Video', + name: 'mainVideo', + type: 'mux.video', + options: { + max_resolution_tier: '2160p', + }, +} +``` + When uploading new assets, editors can still choose a lower resolution for each video than configured globally. This option controls the maximum resolution encoded or processed for the uploaded video. The option is particularly important to manage costs when uploaded videos are higher than `1080p` resolution. More information on the feature is available on Mux's [docs](https://docs.mux.com/guides/stream-videos-in-4k). Also, read more on this feature announcement on Mux's [blog](https://www.mux.com/blog/more-pixels-fewer-problems-introducing-4k-support-for-mux-video). +### Control Accepted File Types + +To control the file types accepted by the input, set the `acceptedMimeTypes` in the `options` of your `mux.video` schema type. Set default with `acceptedMimeTypes: ['video/*', 'audio/*']` in the plugin configuration. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) for more information on the `accept` attribute. + +```js +import {muxInput} from 'sanity-plugin-mux-input' + +export default defineConfig({ + plugins: [muxInput({acceptedMimeTypes: ['audio']})], +}) +``` + +```js +export default { + title: 'Main Video', + name: 'mainVideo', + type: 'mux.video', + options: { + acceptedMimeTypes: ['video'], + }, +} +``` + ### Encoding tier (smart or baseline) The [encoding tier](https://docs.mux.com/guides/use-encoding-tiers) informs the cost, quality, and available platform features for the asset. You can choose between `smart` and `baseline` at the plugin configuration. Defaults to `smart`. @@ -238,6 +272,17 @@ export default defineConfig({ }) ``` +```js +export default { + title: 'Main Video', + name: 'mainVideo', + type: 'mux.video', + options: { + encoding_tier: 'baseline', + }, +} +``` + If `encoding_tier: 'smart'`, editors can still choose to use the `baseline` encoding tier on a per-video basis when uploading new assets. More information on the feature is available on Mux's [documentation](https://docs.mux.com/guides/use-encoding-tiers). Also, read more on the feature announcement on Mux's [blog](https://www.mux.com/blog/our-next-pricing-lever-baseline-on-demand-assets-with-free-video-encoding) diff --git a/src/_exports/index.ts b/src/_exports/index.ts index 39625fb7..1a836b4d 100644 --- a/src/_exports/index.ts +++ b/src/_exports/index.ts @@ -13,6 +13,7 @@ export const defaultConfig: PluginConfig = { normalize_audio: false, defaultSigned: false, tool: DEFAULT_TOOL_CONFIG, + acceptedMimeTypes: ['video/*', 'audio/*'], } export const muxInput = definePlugin | void>((userConfig) => { diff --git a/src/components/FileInputArea.tsx b/src/components/FileInputArea.tsx deleted file mode 100644 index e4636a1c..00000000 --- a/src/components/FileInputArea.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import {UploadIcon} from '@sanity/icons' -import {Card, CardTone, Flex, Inline} from '@sanity/ui' -import {PropsWithChildren, useRef, useState} from 'react' - -import {extractDroppedFiles} from '../util/extractFiles' -import {FileInputButton} from './FileInputButton' - -interface FileInputAreaProps extends PropsWithChildren { - accept?: string - acceptMIMETypes?: string[] - label: React.ReactNode - onSelect: (files: FileList | File[]) => void -} - -export default function FileInputArea({ - label, - accept, - acceptMIMETypes, - onSelect, -}: FileInputAreaProps) { - const dragEnteredEls = useRef([]) - const [dragState, setDragState] = useState<'valid' | 'invalid' | null>(null) - - // Stages and validates an upload from dragging+dropping files or folders - const handleDrop: React.DragEventHandler = (event) => { - setDragState(null) - event.preventDefault() - event.stopPropagation() - extractDroppedFiles(event.nativeEvent.dataTransfer!).then(onSelect) - } - - /* ------------------------------- Drag State ------------------------------- */ - - const handleDragOver: React.DragEventHandler = (event) => { - event.preventDefault() - event.stopPropagation() - } - - const handleDragEnter: React.DragEventHandler = (event) => { - event.stopPropagation() - dragEnteredEls.current.push(event.target) - const type = event.dataTransfer.items?.[0]?.type - setDragState( - !acceptMIMETypes || acceptMIMETypes.some((mimeType) => type?.match(mimeType)) - ? 'valid' - : 'invalid' - ) - } - - const handleDragLeave: React.DragEventHandler = (event) => { - event.stopPropagation() - const idx = dragEnteredEls.current.indexOf(event.target) - if (idx > -1) { - dragEnteredEls.current.splice(idx, 1) - } - if (dragEnteredEls.current.length === 0) { - setDragState(null) - } - } - - let tone: CardTone = 'inherit' - if (dragState) tone = dragState === 'valid' ? 'positive' : 'critical' - return ( - - - - {label} - - - - - - - ) -} diff --git a/src/components/FileInputButton.tsx b/src/components/FileInputButton.tsx index 911e0527..628b141f 100644 --- a/src/components/FileInputButton.tsx +++ b/src/components/FileInputButton.tsx @@ -17,7 +17,7 @@ const Label = styled.label` export interface FileInputButtonProps extends ButtonProps { onSelect: (files: FileList) => void - accept?: string + accept: string } export const FileInputButton = ({onSelect, accept, ...props}: FileInputButtonProps) => { const inputId = `FileSelect${useId()}` @@ -34,7 +34,7 @@ export const FileInputButton = ({onSelect, accept, ...props}: FileInputButtonPro return ( setDialogState('select-video'), [setDialogState]) const handleConfigureApi = useCallback(() => setDialogState('secrets'), [setDialogState]) @@ -50,6 +52,7 @@ export default function UploadPlaceholder(props: UploadPlaceholderProps) { { + toast.push({ + status: 'error', + title: `Invalid file type. Accepted types: ${props.config.acceptedMimeTypes?.join(', ')}`, + }) + }, [props.config.acceptedMimeTypes, toast]) + + /** + * Validates if any file in the provided FileList or File array has an unsupported MIME type + * @param files - FileList or File array to validate + * @returns true if any file has an invalid MIME type, false if all files are valid + */ + + const isInvalidFile = (files: FileList | File[]) => { + const isInvalid = Array.from(files).some((file) => { + return !props.config.acceptedMimeTypes?.some((acceptedType) => + file.type.match(new RegExp(acceptedType)) + ) + }) + + if (isInvalid) { + invalidFileToast() + return true + } + return false + } + /* -------------------------- Upload Initialization ------------------------- */ // The below populate the uploadInput state field, which then triggers the // upload configuration, or the startUpload function if no config is required. // Stages an upload from the file selector const handleUpload = (files: FileList | File[]) => { + if (isInvalidFile(files)) return dispatch({ action: 'stageUpload', input: {type: 'file', files}, @@ -249,9 +277,14 @@ export default function Uploader(props: Props) { // Stages and validates an upload from dragging+dropping files or folders const handleDrop: React.DragEventHandler = (event) => { - setDragState(null) event.preventDefault() event.stopPropagation() + if (dragState === 'invalid') { + invalidFileToast() + setDragState(null) + return + } + setDragState(null) extractDroppedFiles(event.nativeEvent.dataTransfer!).then((files) => { dispatch({ action: 'stageUpload', @@ -271,7 +304,16 @@ export default function Uploader(props: Props) { event.stopPropagation() dragEnteredEls.current.push(event.target) const type = event.dataTransfer.items?.[0]?.type - setDragState(type?.startsWith('video/') ? 'valid' : 'invalid') + const mimeTypes = props.config.acceptedMimeTypes + + // Check if the dragged file type matches any of the accepted mime types + const isValidType = mimeTypes?.some((acceptedType) => { + // Convert mime type pattern to regex (e.g., 'video/*' -> /^video\/.*$/) + const pattern = `^${acceptedType.replace('*', '.*')}$` + return new RegExp(pattern).test(type) + }) + + setDragState(isValidType ? 'valid' : 'invalid') } const handleDragLeave: React.DragEventHandler = (event) => { @@ -335,6 +377,10 @@ export default function Uploader(props: Props) { let tone: CardTone | undefined if (dragState) tone = dragState === 'valid' ? 'positive' : 'critical' + const acceptMimeString = props.config?.acceptedMimeTypes?.length + ? props.config.acceptedMimeTypes.join(',') + : 'video/*, audio/*' + return ( <> ) : (