sanity-plugin-mux-input 3.0.5 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +28 -28
- package/dist/index.js.map +1 -1
- package/package.json +5 -15
- package/dist/index.cjs +0 -5746
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -288
- package/dist/index.d.cts.map +0 -1
- package/sanity.json +0 -8
- package/src/_exports/index.ts +0 -73
- package/src/actions/assets.ts +0 -152
- package/src/actions/secrets.ts +0 -110
- package/src/actions/upload.ts +0 -308
- package/src/clients/upChunkObservable.ts +0 -54
- package/src/components/AddCaptionDialog.tsx +0 -440
- package/src/components/CaptionsDialog.tsx +0 -23
- package/src/components/ConfigureApi.styled.tsx +0 -19
- package/src/components/ConfigureApi.tsx +0 -296
- package/src/components/DraggableWatermark.tsx +0 -885
- package/src/components/EditCaptionDialog.tsx +0 -511
- package/src/components/EditThumbnailDialog.tsx +0 -121
- package/src/components/ErrorBoundaryCard.tsx +0 -97
- package/src/components/FileInputButton.tsx +0 -54
- package/src/components/FileInputMenuItem.styled.tsx +0 -36
- package/src/components/FileInputMenuItem.tsx +0 -85
- package/src/components/FormField.tsx +0 -38
- package/src/components/IconInfo.tsx +0 -22
- package/src/components/ImportVideosFromMux.tsx +0 -339
- package/src/components/Input.styled.tsx +0 -22
- package/src/components/Input.tsx +0 -78
- package/src/components/InputBrowser.tsx +0 -41
- package/src/components/MuxLogo.tsx +0 -42
- package/src/components/Onboard.tsx +0 -65
- package/src/components/PageSelector.tsx +0 -54
- package/src/components/Player.styled.tsx +0 -11
- package/src/components/Player.tsx +0 -117
- package/src/components/PlayerActionsMenu.tsx +0 -191
- package/src/components/ResyncMetadata.tsx +0 -278
- package/src/components/SelectAsset.tsx +0 -39
- package/src/components/SelectSortOptions.tsx +0 -45
- package/src/components/SpinnerBox.tsx +0 -16
- package/src/components/StudioTool.tsx +0 -24
- package/src/components/TextTracksEditor.tsx +0 -117
- package/src/components/TextTracksManager.tsx +0 -738
- package/src/components/UploadConfiguration.tsx +0 -696
- package/src/components/UploadPlaceholder.tsx +0 -88
- package/src/components/UploadProgress.tsx +0 -80
- package/src/components/Uploader.styled.tsx +0 -65
- package/src/components/Uploader.tsx +0 -499
- package/src/components/VideoDetails/DeleteDialog.tsx +0 -148
- package/src/components/VideoDetails/VideoDetails.tsx +0 -358
- package/src/components/VideoDetails/VideoReferences.tsx +0 -63
- package/src/components/VideoDetails/useVideoDetails.ts +0 -103
- package/src/components/VideoInBrowser.tsx +0 -245
- package/src/components/VideoMetadata.tsx +0 -45
- package/src/components/VideoPlayer.tsx +0 -241
- package/src/components/VideoThumbnail.tsx +0 -139
- package/src/components/VideosBrowser.tsx +0 -100
- package/src/components/documentPreview/DocumentPreview.tsx +0 -84
- package/src/components/documentPreview/DraftStatus.tsx +0 -34
- package/src/components/documentPreview/MissingSchemaType.tsx +0 -32
- package/src/components/documentPreview/PaneItemPreview.tsx +0 -67
- package/src/components/documentPreview/PublishedStatus.tsx +0 -35
- package/src/components/documentPreview/TimeAgo.tsx +0 -12
- package/src/components/icons/Audio.tsx +0 -13
- package/src/components/icons/Resolution.tsx +0 -12
- package/src/components/icons/StopWatch.tsx +0 -20
- package/src/components/icons/ToolIcon.tsx +0 -19
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +0 -133
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +0 -76
- package/src/components/uploadConfiguration/PlaybackPolicyWarning.tsx +0 -29
- package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +0 -72
- package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +0 -180
- package/src/components/withFocusRing/helpers.ts +0 -24
- package/src/components/withFocusRing/index.ts +0 -1
- package/src/components/withFocusRing/withFocusRing.ts +0 -30
- package/src/context/DialogStateContext.tsx +0 -33
- package/src/context/DrmPlaybackWarningContext.tsx +0 -97
- package/src/hooks/useAccessControl.ts +0 -13
- package/src/hooks/useAssetDocumentValues.ts +0 -11
- package/src/hooks/useAssets.ts +0 -73
- package/src/hooks/useCancelUpload.ts +0 -22
- package/src/hooks/useClient.ts +0 -8
- package/src/hooks/useDialogState.ts +0 -11
- package/src/hooks/useDocReferences.ts +0 -21
- package/src/hooks/useFetchFileSize.ts +0 -55
- package/src/hooks/useImportMuxAssets.ts +0 -132
- package/src/hooks/useInView.ts +0 -41
- package/src/hooks/useMediaMetadata.ts +0 -104
- package/src/hooks/useMuxAssets.ts +0 -179
- package/src/hooks/useMuxPolling.ts +0 -52
- package/src/hooks/useResyncAsset.ts +0 -110
- package/src/hooks/useResyncMuxMetadata.ts +0 -169
- package/src/hooks/useSaveSecrets.ts +0 -78
- package/src/hooks/useSecretsDocumentValues.ts +0 -38
- package/src/hooks/useSecretsFormState.ts +0 -47
- package/src/plugin.tsx +0 -31
- package/src/sanity-ui.d.ts +0 -5
- package/src/schema.ts +0 -196
- package/src/util/addKeysToMuxData.ts +0 -30
- package/src/util/asserters.ts +0 -23
- package/src/util/assetTitlePlaceholder.ts +0 -31
- package/src/util/constants.ts +0 -15
- package/src/util/convertWatermarkToMux.ts +0 -160
- package/src/util/createSearchFilter.ts +0 -76
- package/src/util/createUrlParamsObject.ts +0 -29
- package/src/util/extractFiles.ts +0 -67
- package/src/util/formatBytes.ts +0 -31
- package/src/util/formatDriveShareLink.ts +0 -64
- package/src/util/formatSeconds.ts +0 -48
- package/src/util/generateJwt.ts +0 -57
- package/src/util/getAnimatedPosterSrc.ts +0 -26
- package/src/util/getPlaybackPolicy.ts +0 -69
- package/src/util/getPosterSrc.ts +0 -28
- package/src/util/getVideoMetadata.ts +0 -23
- package/src/util/getVideoSrc.ts +0 -23
- package/src/util/parsers.ts +0 -5
- package/src/util/pluginVersion.ts +0 -5
- package/src/util/readSecrets.ts +0 -38
- package/src/util/roundPxString.ts +0 -16
- package/src/util/textTracks.ts +0 -222
- package/src/util/tryWithSuspend.ts +0 -22
- package/src/util/types.ts +0 -566
- package/v2-incompatible.js +0 -11
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import {roundPxString} from './roundPxString'
|
|
2
|
-
import type {MuxOverlaySettings, WatermarkConfig} from './types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Converts a draggable watermark position (x, y percentages) to Mux's overlay_settings format.
|
|
6
|
-
*
|
|
7
|
-
* @param watermark - The watermark configuration with position, size, and opacity
|
|
8
|
-
* @returns Mux overlay_settings object
|
|
9
|
-
* @see {@link https://www.mux.com/docs/guides/add-watermarks-to-your-videos}
|
|
10
|
-
*/
|
|
11
|
-
export function convertWatermarkToMuxOverlay(
|
|
12
|
-
watermark: WatermarkConfig,
|
|
13
|
-
options?: {
|
|
14
|
-
/**
|
|
15
|
-
* Video aspect ratio (width / height). Needed for correct vertical positioning,
|
|
16
|
-
* especially on vertical videos.
|
|
17
|
-
*/
|
|
18
|
-
videoAspectRatio?: number
|
|
19
|
-
/**
|
|
20
|
-
* Unit to emit for margins/width when generating overlay_settings from Canvas mode.
|
|
21
|
-
* - 'px' will generate pixel strings according to Mux's scaling rules:
|
|
22
|
-
* values are applied as if the video were scaled to 1920x1080 (horizontal)
|
|
23
|
-
* or 1080x1920 (vertical).
|
|
24
|
-
* - '%' preserves existing behavior.
|
|
25
|
-
*/
|
|
26
|
-
units?: '%' | 'px'
|
|
27
|
-
},
|
|
28
|
-
): MuxOverlaySettings | null {
|
|
29
|
-
if (!watermark.enabled || !watermark.imageUrl) {
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const size = watermark.size || 20
|
|
34
|
-
const opacity = watermark.opacity ?? 0.7
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Convert a percentage to whole-pixel string, using Mux's base dimensions:
|
|
38
|
-
* - Horizontal video: 1920x1080
|
|
39
|
-
* - Vertical video: 1080x1920
|
|
40
|
-
*/
|
|
41
|
-
const toPxString = (valuePercent: number, axis: 'x' | 'y') => {
|
|
42
|
-
const videoAspectRatio = options?.videoAspectRatio ?? 16 / 9
|
|
43
|
-
const isVertical = videoAspectRatio > 0 && videoAspectRatio < 1
|
|
44
|
-
const baseW = isVertical ? 1080 : 1920
|
|
45
|
-
const baseH = isVertical ? 1920 : 1080
|
|
46
|
-
const base = axis === 'x' ? baseW : baseH
|
|
47
|
-
const px = (valuePercent / 100) * base
|
|
48
|
-
let rounded = Math.round(px)
|
|
49
|
-
// Avoid sending 0px (and JS -0); keep sign for negative margins.
|
|
50
|
-
if (rounded === 0) rounded = px < 0 ? -1 : 1
|
|
51
|
-
return `${rounded}px`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const normalizeToPixels = (value: string | undefined, axis: 'x' | 'y'): string | undefined => {
|
|
55
|
-
if (!value) return value
|
|
56
|
-
const trimmed = value.trim()
|
|
57
|
-
if (trimmed.endsWith('px')) {
|
|
58
|
-
return roundPxString(trimmed)
|
|
59
|
-
}
|
|
60
|
-
if (trimmed.endsWith('%')) {
|
|
61
|
-
const n = Number(trimmed.slice(0, -1))
|
|
62
|
-
if (!Number.isFinite(n)) return value
|
|
63
|
-
return toPxString(n, axis)
|
|
64
|
-
}
|
|
65
|
-
return value
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// If user provided explicit overlay settings, use them (Mux-documented format).
|
|
69
|
-
// When `options.units === 'px'`, we normalize both % and px to whole-pixel strings,
|
|
70
|
-
// honoring vertical vs horizontal video bases.
|
|
71
|
-
if (watermark.overlay_settings) {
|
|
72
|
-
const widthValue = watermark.overlay_settings.width
|
|
73
|
-
const widthNormalized =
|
|
74
|
-
options?.units === 'px' ? normalizeToPixels(widthValue, 'x') : widthValue
|
|
75
|
-
return {
|
|
76
|
-
...watermark.overlay_settings,
|
|
77
|
-
horizontal_margin:
|
|
78
|
-
options?.units === 'px'
|
|
79
|
-
? (normalizeToPixels(watermark.overlay_settings.horizontal_margin, 'x') ??
|
|
80
|
-
watermark.overlay_settings.horizontal_margin)
|
|
81
|
-
: watermark.overlay_settings.horizontal_margin,
|
|
82
|
-
vertical_margin:
|
|
83
|
-
options?.units === 'px'
|
|
84
|
-
? (normalizeToPixels(watermark.overlay_settings.vertical_margin, 'y') ??
|
|
85
|
-
watermark.overlay_settings.vertical_margin)
|
|
86
|
-
: watermark.overlay_settings.vertical_margin,
|
|
87
|
-
width: widthNormalized ?? `${size}%`,
|
|
88
|
-
opacity: watermark.overlay_settings.opacity ?? `${Math.round(opacity * 100)}%`,
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const position = watermark.position || {x: 50, y: 50}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Our UI stores watermark position as the *center point* in percentages.
|
|
96
|
-
* Mux margins are interpreted relative to an *edge* (based on align).
|
|
97
|
-
*
|
|
98
|
-
* To make "corner" placements match what the user dragged, we convert from
|
|
99
|
-
* center-position to top-left margins by subtracting half the watermark size.
|
|
100
|
-
*
|
|
101
|
-
* Note: `size` is a percentage of video width. Mux `width` is also expressed
|
|
102
|
-
* as a percentage of the video width, so we can reuse it for horizontal math.
|
|
103
|
-
* For vertical math, we approximate using the same percentage to keep behavior
|
|
104
|
-
* consistent with the current draggable UI (which also uses `size` in both axes
|
|
105
|
-
* for bounds).
|
|
106
|
-
*/
|
|
107
|
-
// Allow negative margins to compensate for rounding / letterboxing edge-cases.
|
|
108
|
-
// We still clamp to a sane range so values don't explode.
|
|
109
|
-
const clampPercent = (value: number) => Math.max(-100, Math.min(100, value))
|
|
110
|
-
|
|
111
|
-
// Mux accepts percentage strings; avoid sending an exact "0%" by nudging to 0.01%.
|
|
112
|
-
// This also handles the JS -0 edge case and tiny floating point remnants.
|
|
113
|
-
const toPercentString = (value: number) => {
|
|
114
|
-
const epsilon = 1e-9
|
|
115
|
-
const isZeroish = value === 0 || Object.is(value, -0) || Math.abs(value) < epsilon
|
|
116
|
-
return `${isZeroish ? 0.01 : value}%`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const watermarkWidthPercentOfVideoWidth = size
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Convert watermark height into % of video height.
|
|
123
|
-
* height% = (watermarkWidthPx / imageAspectRatio) / videoHeightPx
|
|
124
|
-
* = (size% * videoWidthPx / imageAspectRatio) / videoHeightPx
|
|
125
|
-
* = size% * (videoWidthPx/videoHeightPx) / imageAspectRatio
|
|
126
|
-
* = size% * videoAspectRatio / imageAspectRatio
|
|
127
|
-
*/
|
|
128
|
-
const videoAspectRatio = options?.videoAspectRatio ?? 16 / 9
|
|
129
|
-
const imageAspectRatio = watermark.imageAspectRatio ?? 1
|
|
130
|
-
const watermarkHeightPercentOfVideoHeight = Math.max(
|
|
131
|
-
0,
|
|
132
|
-
Math.min(100, (size * videoAspectRatio) / imageAspectRatio),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
const halfWidth = watermarkWidthPercentOfVideoWidth / 2
|
|
136
|
-
const halfHeight = watermarkHeightPercentOfVideoHeight / 2
|
|
137
|
-
|
|
138
|
-
const leftMargin = clampPercent(
|
|
139
|
-
Math.min(position.x - halfWidth, 100 - watermarkWidthPercentOfVideoWidth),
|
|
140
|
-
)
|
|
141
|
-
const topMargin = clampPercent(
|
|
142
|
-
Math.min(position.y - halfHeight, 100 - watermarkHeightPercentOfVideoHeight),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
const units = options?.units ?? '%'
|
|
146
|
-
const marginX = units === 'px' ? toPxString(leftMargin, 'x') : toPercentString(leftMargin)
|
|
147
|
-
const marginY = units === 'px' ? toPxString(topMargin, 'y') : toPercentString(topMargin)
|
|
148
|
-
const width = units === 'px' ? toPxString(size, 'x') : `${size}%`
|
|
149
|
-
|
|
150
|
-
const overlaySettings: MuxOverlaySettings = {
|
|
151
|
-
vertical_align: 'top',
|
|
152
|
-
vertical_margin: marginY,
|
|
153
|
-
horizontal_align: 'left',
|
|
154
|
-
horizontal_margin: marginX,
|
|
155
|
-
width,
|
|
156
|
-
opacity: `${Math.round(opacity * 100)}%`,
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return overlaySettings
|
|
160
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Adaptation of Sanity's createSearchQuery for our limited use case:
|
|
2
|
-
// https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/core/search/weighted/createSearchQuery.ts
|
|
3
|
-
import {compact, toLower, trim, uniq, words} from 'lodash'
|
|
4
|
-
|
|
5
|
-
const SPECIAL_CHARS = /([^!@#$%^&*(),\\/?";:{}|[\]+<>\s-])+/g
|
|
6
|
-
const STRIP_EDGE_CHARS = /(^[.]+)|([.]+$)/
|
|
7
|
-
|
|
8
|
-
function tokenize(string: string): string[] {
|
|
9
|
-
return (string.match(SPECIAL_CHARS) || []).map((token) => token.replace(STRIP_EDGE_CHARS, ''))
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function toGroqParams(terms: string[]): Record<string, string> {
|
|
13
|
-
const params: Record<string, string> = {}
|
|
14
|
-
return terms.reduce((acc, term, i) => {
|
|
15
|
-
acc[`t${i}`] = `*${term}*` // "t" is short for term
|
|
16
|
-
return acc
|
|
17
|
-
}, params)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Convert a string into an array of tokenized terms.
|
|
22
|
-
*
|
|
23
|
-
* Any (multi word) text wrapped in double quotes will be treated as "phrases", or separate tokens that
|
|
24
|
-
* will not have its special characters removed.
|
|
25
|
-
* E.g.`"the" "fantastic mr" fox fox book` =\> ["the", `"fantastic mr"`, "fox", "book"]
|
|
26
|
-
*
|
|
27
|
-
* Phrases wrapped in quotes are assigned relevance scoring differently from regular words.
|
|
28
|
-
*
|
|
29
|
-
* @internal
|
|
30
|
-
*/
|
|
31
|
-
function extractTermsFromQuery(query: string): string[] {
|
|
32
|
-
const quotedQueries = [] as string[]
|
|
33
|
-
const unquotedQuery = query.replace(/("[^"]*")/g, (match) => {
|
|
34
|
-
if (words(match).length > 1) {
|
|
35
|
-
quotedQueries.push(match)
|
|
36
|
-
return ''
|
|
37
|
-
}
|
|
38
|
-
return match
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
// Lowercase and trim quoted queries
|
|
42
|
-
const quotedTerms = quotedQueries.map((str) => trim(toLower(str)))
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Convert (remaining) search query into an array of deduped, sanitized tokens.
|
|
46
|
-
* All white space and special characters are removed.
|
|
47
|
-
* e.g. "The saint of Saint-Germain-des-Prés" =\> ['the', 'saint', 'of', 'germain', 'des', 'pres']
|
|
48
|
-
*/
|
|
49
|
-
const remainingTerms = uniq(compact(tokenize(toLower(unquotedQuery))))
|
|
50
|
-
|
|
51
|
-
return [...quotedTerms, ...remainingTerms]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Create GROQ constraints, given search terms and the full spec of available document types and fields.
|
|
56
|
-
* Essentially a large list of all possible fields (joined by logical OR) to match our search terms against.
|
|
57
|
-
*/
|
|
58
|
-
function createConstraints(terms: string[], includeAssetId: boolean) {
|
|
59
|
-
const searchPaths = includeAssetId ? ['filename', 'assetId'] : ['filename']
|
|
60
|
-
const constraints = terms
|
|
61
|
-
.map((_term, i) => searchPaths.map((joinedPath) => `${joinedPath} match $t${i}`))
|
|
62
|
-
.filter((constraint) => constraint.length > 0)
|
|
63
|
-
|
|
64
|
-
return constraints.map((constraint) => `(${constraint.join(' || ')})`)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createSearchFilter(query: string) {
|
|
68
|
-
const terms = extractTermsFromQuery(query)
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
filter: createConstraints(terms, query.length >= 8), // if the search is big enough, include the assetId (mux id) in the results
|
|
72
|
-
params: {
|
|
73
|
-
...toGroqParams(terms),
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type {SanityClient} from 'sanity'
|
|
2
|
-
|
|
3
|
-
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
4
|
-
import {type Audience, generateJwt} from './generateJwt'
|
|
5
|
-
import {getPlaybackPolicyById} from './getPlaybackPolicy'
|
|
6
|
-
import type {AssetThumbnailOptions} from './types'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* May throw a Promise. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
10
|
-
*/
|
|
11
|
-
export function createUrlParamsObject(
|
|
12
|
-
client: SanityClient,
|
|
13
|
-
asset: AssetThumbnailOptions['asset'],
|
|
14
|
-
params: object,
|
|
15
|
-
audience: Audience,
|
|
16
|
-
) {
|
|
17
|
-
const playbackId = getPlaybackId(asset)
|
|
18
|
-
|
|
19
|
-
let searchParams = new URLSearchParams(
|
|
20
|
-
JSON.parse(JSON.stringify(params, (_, v) => v ?? undefined)),
|
|
21
|
-
)
|
|
22
|
-
const playbackPolicy = getPlaybackPolicyById(asset, playbackId)?.policy
|
|
23
|
-
if (playbackPolicy === 'signed' || playbackPolicy === 'drm') {
|
|
24
|
-
const token = generateJwt(client, playbackId, audience, params)
|
|
25
|
-
searchParams = new URLSearchParams({token})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {playbackId, searchParams}
|
|
29
|
-
}
|
package/src/util/extractFiles.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for extracting files from dataTransfer in a predictable cross-browser fashion.
|
|
3
|
-
* Also recursively extracts files from a directory
|
|
4
|
-
* Inspired by https://github.com/component/normalized-upload
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export function extractDroppedFiles(dataTransfer: DataTransfer) {
|
|
8
|
-
const files = Array.from(dataTransfer.files || [])
|
|
9
|
-
const items = Array.from(dataTransfer.items || [])
|
|
10
|
-
if (files && files.length > 0) {
|
|
11
|
-
return Promise.resolve(files)
|
|
12
|
-
}
|
|
13
|
-
return normalizeItems(items).then((arr) => arr.flat())
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizeItems(items: DataTransferItem[]) {
|
|
17
|
-
return Promise.all(
|
|
18
|
-
items.map((item) => {
|
|
19
|
-
// directory
|
|
20
|
-
if (item.kind === 'file' && item.webkitGetAsEntry) {
|
|
21
|
-
let entry: FileSystemEntry | File[] | null
|
|
22
|
-
// Edge throws
|
|
23
|
-
try {
|
|
24
|
-
entry = item.webkitGetAsEntry()
|
|
25
|
-
} catch {
|
|
26
|
-
return [item.getAsFile()]
|
|
27
|
-
}
|
|
28
|
-
if (!entry) {
|
|
29
|
-
return []
|
|
30
|
-
}
|
|
31
|
-
return entry.isDirectory ? walk(entry) : [item.getAsFile()]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// file
|
|
35
|
-
if (item.kind === 'file') {
|
|
36
|
-
const file = item.getAsFile()
|
|
37
|
-
return Promise.resolve(file ? [file] : [])
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// others
|
|
41
|
-
return new Promise((resolve) => item.getAsString(resolve)).then((str?: any) =>
|
|
42
|
-
str ? [new File([str], 'unknown.txt', {type: item.type})] : [],
|
|
43
|
-
)
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isFile(entry: FileSystemEntry): entry is FileSystemFileEntry {
|
|
49
|
-
return entry.isFile
|
|
50
|
-
}
|
|
51
|
-
function isDirectory(entry: FileSystemEntry): entry is FileSystemDirectoryEntry {
|
|
52
|
-
return entry.isDirectory
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function walk(entry: FileSystemEntry): any {
|
|
56
|
-
if (isFile(entry)) {
|
|
57
|
-
return new Promise((resolve) => entry.file(resolve)).then((file) => [file])
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (isDirectory(entry)) {
|
|
61
|
-
const dir = entry.createReader()
|
|
62
|
-
return new Promise<any>((resolve) => dir.readEntries(resolve))
|
|
63
|
-
.then((entries: FileSystemEntry[]) => entries.filter((entr) => !entr.name.startsWith('.')))
|
|
64
|
-
.then((entries) => Promise.all(entries.map(walk)).then((arr) => arr.flat()))
|
|
65
|
-
}
|
|
66
|
-
return Promise.resolve([])
|
|
67
|
-
}
|
package/src/util/formatBytes.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// From: https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
|
|
2
|
-
/**
|
|
3
|
-
* Format bytes as human-readable text.
|
|
4
|
-
*
|
|
5
|
-
* @param bytes Number of bytes.
|
|
6
|
-
* @param si True to use metric (SI) units, aka powers of 1000. False to use
|
|
7
|
-
* binary (IEC), aka powers of 1024.
|
|
8
|
-
* @param dp Number of decimal places to display.
|
|
9
|
-
*
|
|
10
|
-
* @return Formatted string.
|
|
11
|
-
*/
|
|
12
|
-
export default function formatBytes(bytes: number, si = false, dp = 1) {
|
|
13
|
-
const thresh = si ? 1000 : 1024
|
|
14
|
-
|
|
15
|
-
if (Math.abs(bytes) < thresh) {
|
|
16
|
-
return bytes + ' B'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const units = si
|
|
20
|
-
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
21
|
-
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
|
22
|
-
let u = -1
|
|
23
|
-
const r = 10 ** dp
|
|
24
|
-
|
|
25
|
-
do {
|
|
26
|
-
bytes /= thresh
|
|
27
|
-
++u
|
|
28
|
-
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
|
|
29
|
-
|
|
30
|
-
return bytes.toFixed(dp) + ' ' + units[u]
|
|
31
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Format Google Drive share links as Google Drive export links.
|
|
3
|
-
* Supported formats:
|
|
4
|
-
* - https://drive.google.com/uc?id=<ID>...
|
|
5
|
-
* - https://drive.google.com/open?id=<ID>...
|
|
6
|
-
* - https://drive.google.com/file/d/<ID>...
|
|
7
|
-
* - https://drive.google.com/folder/<ID>...
|
|
8
|
-
*
|
|
9
|
-
* @param url Google Drive share link to format.
|
|
10
|
-
* @returns Google Drive export link (URL passthrough if not share link).
|
|
11
|
-
*/
|
|
12
|
-
export function formatDriveShareLink(url: string): string {
|
|
13
|
-
// Export link formatter.
|
|
14
|
-
const formatExportLink = (id: string) => {
|
|
15
|
-
return `https://drive.google.com/uc?export=download&id=${id}`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// URL formatting.
|
|
19
|
-
try {
|
|
20
|
-
// Parse URL.
|
|
21
|
-
const trimmed = url.trim()
|
|
22
|
-
const parsed = new URL(trimmed)
|
|
23
|
-
|
|
24
|
-
// Enforce strict host name.
|
|
25
|
-
if (parsed.hostname !== 'drive.google.com') {
|
|
26
|
-
throw new Error('URL is not from Google Drive.')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Look for ID in search parameters.
|
|
30
|
-
const id = parsed.searchParams.get('id') || ''
|
|
31
|
-
if (id.length) {
|
|
32
|
-
return formatExportLink(id)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Look for ID in path name.
|
|
36
|
-
const path = parsed.pathname.split('/') || []
|
|
37
|
-
|
|
38
|
-
// Path is /file/d/<ID>...
|
|
39
|
-
if (path.includes('file') && path.includes('d')) {
|
|
40
|
-
const index =
|
|
41
|
-
path.findIndex((value: string) => {
|
|
42
|
-
return value === 'd'
|
|
43
|
-
}) + 1
|
|
44
|
-
const fileId = path.at(index) || ''
|
|
45
|
-
return formatExportLink(fileId)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Path is /folder/<ID>...
|
|
49
|
-
if (path.includes('folders')) {
|
|
50
|
-
const index =
|
|
51
|
-
path.findIndex((value: string) => {
|
|
52
|
-
return value === 'folders'
|
|
53
|
-
}) + 1
|
|
54
|
-
const folderId = path.at(index) || ''
|
|
55
|
-
return formatExportLink(folderId)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// URL not recognized.
|
|
59
|
-
throw new Error('URL was not recognized.')
|
|
60
|
-
} catch {
|
|
61
|
-
// URL passthrough by default.
|
|
62
|
-
return url
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// From: https://stackoverflow.com/a/11486026/10433647
|
|
2
|
-
export function formatSeconds(seconds: number): string {
|
|
3
|
-
if (typeof seconds !== 'number' || Number.isNaN(seconds)) {
|
|
4
|
-
return ''
|
|
5
|
-
}
|
|
6
|
-
// Hours, minutes and seconds
|
|
7
|
-
const hrs = ~~(seconds / 3600)
|
|
8
|
-
const mins = ~~((seconds % 3600) / 60)
|
|
9
|
-
const secs = ~~seconds % 60
|
|
10
|
-
|
|
11
|
-
// Output like "1:01" or "4:03:59" or "123:03:59"
|
|
12
|
-
let ret = ''
|
|
13
|
-
|
|
14
|
-
if (hrs > 0) {
|
|
15
|
-
ret += '' + hrs + ':' + (mins < 10 ? '0' : '')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
ret += '' + mins + ':' + (secs < 10 ? '0' : '')
|
|
19
|
-
ret += '' + secs
|
|
20
|
-
return ret
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Output like "05:14:01"
|
|
24
|
-
export function formatSecondsToHHMMSS(seconds: number): string {
|
|
25
|
-
const hrs = Math.floor(seconds / 3600)
|
|
26
|
-
.toString()
|
|
27
|
-
.padStart(2, '0')
|
|
28
|
-
const mins = Math.floor((seconds % 3600) / 60)
|
|
29
|
-
.toString()
|
|
30
|
-
.padStart(2, '0')
|
|
31
|
-
const secs = Math.floor(seconds % 60)
|
|
32
|
-
.toString()
|
|
33
|
-
.padStart(2, '0')
|
|
34
|
-
|
|
35
|
-
return `${hrs}:${mins}:${secs}`
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Checks if time has a HH:MM:SS format like "05:14:01"
|
|
39
|
-
export function isValidTimeFormat(time: string) {
|
|
40
|
-
const regex = /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/
|
|
41
|
-
return regex.test(time) || time === ''
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Converts a time like "05:14:01" to seconds
|
|
45
|
-
export function getSecondsFromTimeFormat(time: string): number {
|
|
46
|
-
const [hh = 0, mm = 0, ss = 0] = time.split(':').map(Number)
|
|
47
|
-
return hh * 3600 + mm * 60 + ss
|
|
48
|
-
}
|
package/src/util/generateJwt.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type {SanityClient} from 'sanity'
|
|
2
|
-
import {suspend} from 'suspend-react'
|
|
3
|
-
|
|
4
|
-
import {readSecrets} from './readSecrets'
|
|
5
|
-
import type {AnimatedThumbnailOptions, ThumbnailOptions} from './types'
|
|
6
|
-
|
|
7
|
-
export type Audience = 'g' | 's' | 't' | 'v' | 'd'
|
|
8
|
-
|
|
9
|
-
export type Payload<T extends Audience> = T extends 'g'
|
|
10
|
-
? AnimatedThumbnailOptions
|
|
11
|
-
: T extends 's'
|
|
12
|
-
? never
|
|
13
|
-
: T extends 't'
|
|
14
|
-
? ThumbnailOptions
|
|
15
|
-
: T extends 'v'
|
|
16
|
-
? never
|
|
17
|
-
: T extends 'd'
|
|
18
|
-
? never
|
|
19
|
-
: never
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Uses suspend. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
23
|
-
*/
|
|
24
|
-
export function generateJwt<T extends Audience>(
|
|
25
|
-
client: SanityClient,
|
|
26
|
-
playbackId: string,
|
|
27
|
-
aud: T,
|
|
28
|
-
payload?: Payload<T>,
|
|
29
|
-
): string {
|
|
30
|
-
const {signingKeyId, signingKeyPrivate} = readSecrets(client)
|
|
31
|
-
if (!signingKeyId) {
|
|
32
|
-
throw new TypeError("Missing `signingKeyId`.\n Check your plugin's configuration")
|
|
33
|
-
}
|
|
34
|
-
if (!signingKeyPrivate) {
|
|
35
|
-
throw new TypeError("Missing `signingKeyPrivate`.\n Check your plugin's configuration")
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* Using suspend means we need to use Suspense on parent components.
|
|
39
|
-
Also, this will throw a Promise under the hood (apparently common in React),
|
|
40
|
-
so if we want to catch errors we have to take this into account in catch blocks
|
|
41
|
-
and rethrow promises. */
|
|
42
|
-
// @ts-expect-error - handle missing typings for this package
|
|
43
|
-
const {default: sign} = suspend(() => import('jsonwebtoken-esm/sign'), ['jsonwebtoken-esm/sign'])
|
|
44
|
-
|
|
45
|
-
return sign(
|
|
46
|
-
payload ? JSON.parse(JSON.stringify(payload, (_, v) => v ?? undefined)) : {},
|
|
47
|
-
atob(signingKeyPrivate),
|
|
48
|
-
{
|
|
49
|
-
algorithm: 'RS256',
|
|
50
|
-
keyid: signingKeyId,
|
|
51
|
-
audience: aud,
|
|
52
|
-
subject: playbackId,
|
|
53
|
-
noTimestamp: true,
|
|
54
|
-
expiresIn: '12h',
|
|
55
|
-
},
|
|
56
|
-
)
|
|
57
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type {SanityClient} from 'sanity'
|
|
2
|
-
|
|
3
|
-
import {createUrlParamsObject} from './createUrlParamsObject'
|
|
4
|
-
import type {AnimatedThumbnailOptions, MuxAnimatedThumbnailUrl} from './types'
|
|
5
|
-
import {type AssetThumbnailOptions} from './types'
|
|
6
|
-
|
|
7
|
-
export interface AnimatedPosterSrcOptions extends AnimatedThumbnailOptions {
|
|
8
|
-
asset: AssetThumbnailOptions['asset']
|
|
9
|
-
client: SanityClient
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getAnimatedPosterSrc({
|
|
13
|
-
asset,
|
|
14
|
-
client,
|
|
15
|
-
height,
|
|
16
|
-
width,
|
|
17
|
-
start = asset.thumbTime ? Math.max(0, asset.thumbTime - 2.5) : 0,
|
|
18
|
-
end = start + 5,
|
|
19
|
-
fps = 15,
|
|
20
|
-
}: AnimatedPosterSrcOptions): MuxAnimatedThumbnailUrl {
|
|
21
|
-
const params = {height, width, start, end, fps}
|
|
22
|
-
|
|
23
|
-
const {playbackId, searchParams} = createUrlParamsObject(client, asset, params, 'g')
|
|
24
|
-
|
|
25
|
-
return `https://image.mux.com/${playbackId}/animated.gif?${searchParams}`
|
|
26
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AdvancedPlaybackPolicy,
|
|
3
|
-
MuxPlaybackId,
|
|
4
|
-
PlaybackPolicy,
|
|
5
|
-
VideoAssetDocument,
|
|
6
|
-
} from './types'
|
|
7
|
-
|
|
8
|
-
/* - Returns the playback id of the asset based on the specified priority.
|
|
9
|
-
By default chooses the "strongest" policy
|
|
10
|
-
- Otherwise, returns the first playback id in the array.
|
|
11
|
-
*/
|
|
12
|
-
export function getPlaybackId(
|
|
13
|
-
asset: Pick<VideoAssetDocument, 'data'>,
|
|
14
|
-
priority: string[] = ['drm', 'signed', 'public'],
|
|
15
|
-
): string {
|
|
16
|
-
try {
|
|
17
|
-
if (!asset) {
|
|
18
|
-
throw new TypeError('Tried to get playback Id with no asset')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const playbackIds = asset.data?.playback_ids
|
|
22
|
-
if (playbackIds && playbackIds.length > 0) {
|
|
23
|
-
for (const policy of priority) {
|
|
24
|
-
const match = playbackIds.find((entry) => entry.policy === policy)
|
|
25
|
-
if (match) {
|
|
26
|
-
return match.id
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return playbackIds[0]!.id
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
throw new TypeError('Missing playbackId')
|
|
34
|
-
} catch (e) {
|
|
35
|
-
console.error('Asset is missing a playbackId', {asset}, e)
|
|
36
|
-
throw e
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getPlaybackPolicy(
|
|
41
|
-
asset: Pick<VideoAssetDocument, 'data' | 'playbackId'>,
|
|
42
|
-
): MuxPlaybackId | undefined {
|
|
43
|
-
return (
|
|
44
|
-
asset.data?.playback_ids?.find(
|
|
45
|
-
(playbackId) => getPlaybackId(asset, ['drm', 'signed', 'public']) === playbackId.id,
|
|
46
|
-
) ?? {id: '', policy: 'public'}
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function getPlaybackPolicyById(
|
|
51
|
-
asset: Pick<VideoAssetDocument, 'data'>,
|
|
52
|
-
playbackId: string,
|
|
53
|
-
): MuxPlaybackId | undefined {
|
|
54
|
-
return asset.data?.playback_ids?.find((entry) => playbackId === entry.id)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function hasPlaybackPolicy(
|
|
58
|
-
data: Partial<{
|
|
59
|
-
playback_policy?: PlaybackPolicy[]
|
|
60
|
-
advanced_playback_policies: AdvancedPlaybackPolicy[]
|
|
61
|
-
}>,
|
|
62
|
-
policy: PlaybackPolicy,
|
|
63
|
-
) {
|
|
64
|
-
return (
|
|
65
|
-
(data.advanced_playback_policies &&
|
|
66
|
-
data.advanced_playback_policies.find((p) => p.policy === policy)) ||
|
|
67
|
-
data.playback_policy?.find((p) => p === policy)
|
|
68
|
-
)
|
|
69
|
-
}
|
package/src/util/getPosterSrc.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type {SanityClient} from 'sanity'
|
|
2
|
-
|
|
3
|
-
import {createUrlParamsObject} from './createUrlParamsObject'
|
|
4
|
-
import type {MuxThumbnailUrl, ThumbnailOptions} from './types'
|
|
5
|
-
import {type AssetThumbnailOptions} from './types'
|
|
6
|
-
|
|
7
|
-
export interface PosterSrcOptions extends ThumbnailOptions {
|
|
8
|
-
asset: AssetThumbnailOptions['asset']
|
|
9
|
-
client: SanityClient
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getPosterSrc({
|
|
13
|
-
asset,
|
|
14
|
-
client,
|
|
15
|
-
fit_mode,
|
|
16
|
-
height,
|
|
17
|
-
time = asset.thumbTime ?? undefined,
|
|
18
|
-
width,
|
|
19
|
-
}: PosterSrcOptions): MuxThumbnailUrl {
|
|
20
|
-
const params = {fit_mode, height, width}
|
|
21
|
-
if (time !== undefined) {
|
|
22
|
-
;(params as any).time = time
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const {playbackId, searchParams} = createUrlParamsObject(client, asset, params, 't')
|
|
26
|
-
|
|
27
|
-
return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`
|
|
28
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import {formatSeconds} from './formatSeconds'
|
|
2
|
-
import type {MuxTextTrack, VideoAssetDocument} from './types'
|
|
3
|
-
|
|
4
|
-
export default function getVideoMetadata(doc: VideoAssetDocument) {
|
|
5
|
-
const id = doc.assetId || doc._id || ''
|
|
6
|
-
const date = doc.data?.created_at
|
|
7
|
-
? new Date(Number(doc.data.created_at) * 1000)
|
|
8
|
-
: new Date(doc._createdAt || doc._updatedAt || Date.now())
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
title: doc.filename || id.slice(0, 12),
|
|
12
|
-
id: id,
|
|
13
|
-
playbackId: doc.playbackId,
|
|
14
|
-
createdAt: date,
|
|
15
|
-
duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : undefined,
|
|
16
|
-
playback_ids: doc.data?.playback_ids,
|
|
17
|
-
aspect_ratio: doc.data?.aspect_ratio,
|
|
18
|
-
max_stored_resolution: doc.data?.max_stored_resolution,
|
|
19
|
-
max_stored_frame_rate: doc.data?.max_stored_frame_rate,
|
|
20
|
-
text_tracks:
|
|
21
|
-
doc.data?.tracks?.filter((track): track is MuxTextTrack => track.type === 'text') || [],
|
|
22
|
-
}
|
|
23
|
-
}
|