sanity-plugin-mux-input 2.16.0 → 2.17.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 CHANGED
@@ -974,7 +974,7 @@ function tryWithSuspend(block, onError) {
974
974
  return onError ? onError(errorOrPromise) : void 0;
975
975
  }
976
976
  }
977
- const Image = styledComponents.styled.img`
977
+ const Image$1 = styledComponents.styled.img`
978
978
  transition: opacity 0.175s ease-out 0s;
979
979
  display: block;
980
980
  width: 100%;
@@ -1052,7 +1052,7 @@ function VideoThumbnail({
1052
1052
  }
1053
1053
  ),
1054
1054
  /* @__PURE__ */ jsxRuntime.jsx(
1055
- Image,
1055
+ Image$1,
1056
1056
  {
1057
1057
  src: thumbnailSrc ?? void 0,
1058
1058
  alt: `Preview for ${staticImage ? "image" : "video"} ${asset.filename || asset.assetId}`,
@@ -3060,7 +3060,7 @@ function VideoPlayer({
3060
3060
  hlsConfig,
3061
3061
  ...props
3062
3062
  }) {
3063
- const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), [error, setError] = React.useState(), playbackId = React.useMemo(() => {
3063
+ const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), playerContainerRef = React.useRef(null), [error, setError] = React.useState(), playbackId = React.useMemo(() => {
3064
3064
  try {
3065
3065
  return getPlaybackId(asset, ["public", "signed", "drm"]);
3066
3066
  } catch {
@@ -3118,6 +3118,7 @@ function VideoPlayer({
3118
3118
  /* @__PURE__ */ jsxRuntime.jsxs(
3119
3119
  ui.Card,
3120
3120
  {
3121
+ ref: playerContainerRef,
3121
3122
  tone: "transparent",
3122
3123
  style: {
3123
3124
  aspectRatio,
@@ -3154,7 +3155,7 @@ function VideoPlayer({
3154
3155
  crossOrigin: "anonymous",
3155
3156
  metadata: {
3156
3157
  player_name: "Sanity Admin Dashboard",
3157
- player_version: "2.16.0",
3158
+ player_version: "2.17.0",
3158
3159
  page_type: "Preview Player"
3159
3160
  },
3160
3161
  audio: isAudio,
@@ -4316,6 +4317,33 @@ function createUpChunkObservable(uuid2, uploadUrl2, source) {
4316
4317
  return upchunk$1.on("success", successHandler), upchunk$1.on("error", errorHandler), upchunk$1.on("progress", progressHandler), upchunk$1.on("offline", offlineHandler), upchunk$1.on("online", onlineHandler), () => upchunk$1.abort();
4317
4318
  });
4318
4319
  }
4320
+ function roundPxString(value) {
4321
+ if (typeof value != "string") return;
4322
+ const trimmed = value.trim();
4323
+ if (!trimmed.endsWith("px")) return;
4324
+ const n = Number(trimmed.slice(0, -2));
4325
+ if (!Number.isFinite(n)) return;
4326
+ let rounded = Math.round(n);
4327
+ return rounded === 0 && (rounded = n < 0 ? -1 : 1), `${rounded}px`;
4328
+ }
4329
+ function sanitizeOverlaySettingsInPlace(settings) {
4330
+ const inputs = settings.input;
4331
+ if (inputs)
4332
+ for (const input of inputs) {
4333
+ const overlay = input.overlay_settings;
4334
+ if (!overlay) continue;
4335
+ const hm = roundPxString(overlay.horizontal_margin), vm = roundPxString(overlay.vertical_margin), w = roundPxString(overlay.width);
4336
+ hm && (overlay.horizontal_margin = hm), vm && (overlay.vertical_margin = vm), w && (overlay.width = w);
4337
+ }
4338
+ }
4339
+ function sanitizePxStringsInJson(json) {
4340
+ return json.replace(/"(-?\d+(?:\.\d+)?)px"/g, (_match, num) => {
4341
+ const n = Number(num);
4342
+ if (!Number.isFinite(n)) return _match;
4343
+ let rounded = Math.round(n);
4344
+ return rounded === 0 && (rounded = n < 0 ? -1 : 1), `"${rounded}px"`;
4345
+ });
4346
+ }
4319
4347
  function cancelUpload(client, uuid2) {
4320
4348
  return client.observable.request({
4321
4349
  url: `/addons/mux/uploads/${client.config().dataset}/${uuid2}`,
@@ -4326,7 +4354,8 @@ function cancelUpload(client, uuid2) {
4326
4354
  function uploadUrl({
4327
4355
  url,
4328
4356
  settings,
4329
- client
4357
+ client,
4358
+ watermark
4330
4359
  }) {
4331
4360
  return testUrl(url).pipe(
4332
4361
  operators.switchMap((validUrl) => rxjs.concat(
@@ -4336,9 +4365,9 @@ function uploadUrl({
4336
4365
  if (!json || !json.status)
4337
4366
  return rxjs.throwError(new Error("Invalid credentials"));
4338
4367
  const uuid$1 = uuid.uuid(), muxBody = settings;
4339
- muxBody.input || (muxBody.input = [{ type: "video" }]), muxBody.input[0].url = validUrl;
4368
+ muxBody.input || (muxBody.input = [{ type: "video" }]), muxBody.input[0].url = validUrl, sanitizeOverlaySettingsInPlace(muxBody);
4340
4369
  const query = {
4341
- muxBody: JSON.stringify(muxBody),
4370
+ muxBody: sanitizePxStringsInJson(JSON.stringify(muxBody)),
4342
4371
  filename: validUrl.split("/").slice(-1)[0]
4343
4372
  }, dataset = client.config().dataset;
4344
4373
  return rxjs.defer(
@@ -4366,7 +4395,8 @@ function uploadUrl({
4366
4395
  function uploadFile({
4367
4396
  settings,
4368
4397
  client,
4369
- file
4398
+ file,
4399
+ watermark
4370
4400
  }) {
4371
4401
  return testFile(file).pipe(
4372
4402
  operators.switchMap((fileOptions) => rxjs.concat(
@@ -4376,7 +4406,7 @@ function uploadFile({
4376
4406
  if (!json || !json.status)
4377
4407
  return rxjs.throwError(() => new Error("Invalid credentials"));
4378
4408
  const uuid$1 = uuid.uuid(), body = settings;
4379
- return rxjs.concat(
4409
+ return sanitizeOverlaySettingsInPlace(body), rxjs.concat(
4380
4410
  rxjs.of({ type: "uuid", uuid: uuid$1 }),
4381
4411
  rxjs.defer(
4382
4412
  () => client.observable.request({
@@ -4430,7 +4460,7 @@ function pollUpload(client, uuid2) {
4430
4460
  }, 2e3);
4431
4461
  });
4432
4462
  }
4433
- async function updateAssetDocumentFromUpload(client, uuid2) {
4463
+ async function updateAssetDocumentFromUpload(client, uuid2, _watermark) {
4434
4464
  let upload, asset;
4435
4465
  try {
4436
4466
  upload = await pollUpload(client, uuid2);
@@ -4952,13 +4982,14 @@ function useMediaMetadata(stagedUpload) {
4952
4982
  setIsLoadingMetadata(!1);
4953
4983
  },
4954
4984
  () => {
4955
- const duration = videoElement.duration, width = videoElement.videoWidth, height = videoElement.videoHeight, isAudioOnly = width <= 0 && height <= 0;
4985
+ const duration = videoElement.duration, width = videoElement.videoWidth, height = videoElement.videoHeight, isAudioOnly = width <= 0 && height <= 0, aspectRatio = width / height;
4956
4986
  setVideoAssetMetadata((old) => ({
4957
4987
  ...old,
4958
4988
  duration,
4959
4989
  width,
4960
4990
  height,
4961
- isAudioOnly
4991
+ isAudioOnly,
4992
+ aspectRatio
4962
4993
  }));
4963
4994
  }
4964
4995
  ], cleanupVideo = (videoEl) => {
@@ -4980,6 +5011,51 @@ function useMediaMetadata(stagedUpload) {
4980
5011
  isLoadingMetadata
4981
5012
  };
4982
5013
  }
5014
+ function convertWatermarkToMuxOverlay(watermark, options) {
5015
+ if (!watermark.enabled || !watermark.imageUrl)
5016
+ return null;
5017
+ const size = watermark.size || 20, opacity = watermark.opacity ?? 0.7, toPxString = (valuePercent, axis) => {
5018
+ const videoAspectRatio2 = options?.videoAspectRatio ?? 1.7777777777777777, isVertical = videoAspectRatio2 > 0 && videoAspectRatio2 < 1, base = axis === "x" ? isVertical ? 1080 : 1920 : isVertical ? 1920 : 1080, px = valuePercent / 100 * base;
5019
+ let rounded = Math.round(px);
5020
+ return rounded === 0 && (rounded = px < 0 ? -1 : 1), `${rounded}px`;
5021
+ }, normalizeToPixels = (value, axis) => {
5022
+ if (!value) return value;
5023
+ const trimmed = value.trim();
5024
+ if (trimmed.endsWith("px"))
5025
+ return roundPxString(trimmed);
5026
+ if (trimmed.endsWith("%")) {
5027
+ const n = Number(trimmed.slice(0, -1));
5028
+ return Number.isFinite(n) ? toPxString(n, axis) : value;
5029
+ }
5030
+ return value;
5031
+ };
5032
+ if (watermark.overlay_settings) {
5033
+ const widthValue = watermark.overlay_settings.width, widthNormalized = options?.units === "px" ? normalizeToPixels(widthValue, "x") : widthValue;
5034
+ return {
5035
+ ...watermark.overlay_settings,
5036
+ horizontal_margin: options?.units === "px" ? normalizeToPixels(watermark.overlay_settings.horizontal_margin, "x") ?? watermark.overlay_settings.horizontal_margin : watermark.overlay_settings.horizontal_margin,
5037
+ vertical_margin: options?.units === "px" ? normalizeToPixels(watermark.overlay_settings.vertical_margin, "y") ?? watermark.overlay_settings.vertical_margin : watermark.overlay_settings.vertical_margin,
5038
+ width: widthNormalized ?? `${size}%`,
5039
+ opacity: watermark.overlay_settings.opacity ?? `${Math.round(opacity * 100)}%`
5040
+ };
5041
+ }
5042
+ const position = watermark.position || { x: 50, y: 50 }, clampPercent = (value) => Math.max(-100, Math.min(100, value)), toPercentString = (value) => `${value === 0 || Object.is(value, -0) || Math.abs(value) < 1e-9 ? 0.01 : value}%`, watermarkWidthPercentOfVideoWidth = size, videoAspectRatio = options?.videoAspectRatio ?? 16 / 9, imageAspectRatio = watermark.imageAspectRatio ?? 1, watermarkHeightPercentOfVideoHeight = Math.max(
5043
+ 0,
5044
+ Math.min(100, size * videoAspectRatio / imageAspectRatio)
5045
+ ), halfWidth = watermarkWidthPercentOfVideoWidth / 2, halfHeight = watermarkHeightPercentOfVideoHeight / 2, leftMargin = clampPercent(
5046
+ Math.min(position.x - halfWidth, 100 - watermarkWidthPercentOfVideoWidth)
5047
+ ), topMargin = clampPercent(
5048
+ Math.min(position.y - halfHeight, 100 - watermarkHeightPercentOfVideoHeight)
5049
+ ), units = options?.units ?? "%", marginX = units === "px" ? toPxString(leftMargin, "x") : toPercentString(leftMargin), marginY = units === "px" ? toPxString(topMargin, "y") : toPercentString(topMargin), width = units === "px" ? toPxString(size, "x") : `${size}%`;
5050
+ return {
5051
+ vertical_align: "top",
5052
+ vertical_margin: marginY,
5053
+ horizontal_align: "left",
5054
+ horizontal_margin: marginX,
5055
+ width,
5056
+ opacity: `${Math.round(opacity * 100)}%`
5057
+ };
5058
+ }
4983
5059
  function formatBytes(bytes, si = !1, dp = 1) {
4984
5060
  const thresh = si ? 1e3 : 1024;
4985
5061
  if (Math.abs(bytes) < thresh)
@@ -4992,6 +5068,566 @@ function formatBytes(bytes, si = !1, dp = 1) {
4992
5068
  while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
4993
5069
  return bytes.toFixed(dp) + " " + units[u2];
4994
5070
  }
5071
+ const RangeInput = styledComponents.styled.input`
5072
+ width: 100%;
5073
+ height: 4px;
5074
+ border-radius: 2px;
5075
+ background: var(--card-border-color);
5076
+ outline: none;
5077
+ -webkit-appearance: none;
5078
+ appearance: none;
5079
+
5080
+ &::-webkit-slider-thumb {
5081
+ -webkit-appearance: none;
5082
+ appearance: none;
5083
+ width: 16px;
5084
+ height: 16px;
5085
+ border-radius: 50%;
5086
+ background: var(--card-focus-ring-color, #2276fc);
5087
+ cursor: pointer;
5088
+ border: 2px solid white;
5089
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
5090
+ }
5091
+
5092
+ &::-moz-range-thumb {
5093
+ width: 16px;
5094
+ height: 16px;
5095
+ border-radius: 50%;
5096
+ background: var(--card-focus-ring-color, #2276fc);
5097
+ cursor: pointer;
5098
+ border: 2px solid white;
5099
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
5100
+ }
5101
+
5102
+ &:hover::-webkit-slider-thumb {
5103
+ background: var(--card-focus-ring-color, #1a5fc7);
5104
+ }
5105
+
5106
+ &:hover::-moz-range-thumb {
5107
+ background: var(--card-focus-ring-color, #1a5fc7);
5108
+ }
5109
+ `, WatermarkOverlay = styledComponents.styled.div`
5110
+ position: absolute;
5111
+ max-width: 200px;
5112
+ opacity: ${(props) => props.$opacity};
5113
+ cursor: move;
5114
+ user-select: none;
5115
+ z-index: 10;
5116
+ pointer-events: auto;
5117
+
5118
+ img {
5119
+ width: 100%;
5120
+ height: auto;
5121
+ display: block;
5122
+ pointer-events: none;
5123
+ }
5124
+
5125
+ &:hover {
5126
+ outline: 2px dashed rgba(255, 255, 255, 0.8);
5127
+ outline-offset: 4px;
5128
+ }
5129
+ `;
5130
+ function DraggableWatermark({
5131
+ watermark,
5132
+ onChange,
5133
+ containerRef,
5134
+ videoElementRef
5135
+ }) {
5136
+ const [isDragging, setIsDragging] = React.useState(!1), [dragStart, setDragStart] = React.useState({ x: 0, y: 0 }), [startPosition, setStartPosition] = React.useState({ x: 0, y: 0 }), watermarkRef = React.useRef(null), debounceTimeoutRef = React.useRef(null), [localPosition, setLocalPosition] = React.useState(watermark.position || { x: 50, y: 50 }), position = localPosition, size = watermark.size || 20, opacity = watermark.opacity ?? 0.7, parseOpacityPercent = (value) => {
5137
+ if (!value) return null;
5138
+ const trimmed = value.trim();
5139
+ if (!trimmed.endsWith("%")) return null;
5140
+ const num = Number(trimmed.slice(0, -1));
5141
+ return Number.isFinite(num) ? Math.max(0, Math.min(1, num / 100)) : null;
5142
+ }, getVideoContentBox = React.useCallback(() => {
5143
+ const container = containerRef?.current;
5144
+ if (!container) return { x: 0, y: 0, width: 0, height: 0 };
5145
+ const rect = container.getBoundingClientRect(), containerW = rect.width, containerH = rect.height, videoEl = videoElementRef?.current, videoW = videoEl?.videoWidth || 0, videoH = videoEl?.videoHeight || 0;
5146
+ if (!videoW || !videoH || !containerW || !containerH)
5147
+ return { x: 0, y: 0, width: containerW, height: containerH };
5148
+ const scale = Math.min(containerW / videoW, containerH / videoH), contentW = videoW * scale, contentH = videoH * scale, offsetX = (containerW - contentW) / 2, offsetY = (containerH - contentH) / 2;
5149
+ return { x: offsetX, y: offsetY, width: contentW, height: contentH };
5150
+ }, [containerRef, videoElementRef]), parseOverlayValue = (value) => {
5151
+ if (!value) return null;
5152
+ const trimmed = value.trim(), px = trimmed.endsWith("px"), pct = trimmed.endsWith("%"), num = Number(trimmed.replace(/px|%/g, ""));
5153
+ return Number.isFinite(num) ? px ? { n: num, unit: "px" } : pct ? { n: num, unit: "%" } : null : null;
5154
+ }, computeManualStyle = (overlay) => {
5155
+ const rect = containerRef?.current?.getBoundingClientRect(), w = rect?.width ?? 0, h = rect?.height ?? 0, isVertical = h > w, baseW = isVertical ? 1080 : 1920, baseH = isVertical ? 1920 : 1080, hm = parseOverlayValue(overlay.horizontal_margin), vm = parseOverlayValue(overlay.vertical_margin), ww = parseOverlayValue(overlay.width), manualOpacity = parseOpacityPercent(overlay.opacity), toCss = (v, axis) => {
5156
+ if (v)
5157
+ return v.unit === "%" ? `${v.n}%` : axis === "x" ? `${v.n * w / baseW}px` : `${v.n * h / baseH}px`;
5158
+ }, computeHorizontalStyle = () => overlay.horizontal_align === "left" ? { left: toCss(hm, "x"), right: void 0, transform: "translate(0, 0)" } : overlay.horizontal_align === "right" ? { right: toCss(hm, "x"), left: void 0, transform: "translate(0, 0)" } : { left: "50%", right: void 0, transform: "translate(-50%, 0)" }, computeVerticalStyle = () => overlay.vertical_align === "top" ? { top: toCss(vm, "y"), bottom: void 0 } : overlay.vertical_align === "bottom" ? { bottom: toCss(vm, "y"), top: void 0 } : { top: "50%", bottom: void 0 }, hStyle = computeHorizontalStyle(), vStyle = computeVerticalStyle();
5159
+ let transform = hStyle.transform;
5160
+ return overlay.vertical_align === "middle" && (transform = overlay.horizontal_align === "center" ? "translate(-50%, -50%)" : "translate(0, -50%)"), {
5161
+ position: "absolute",
5162
+ ...hStyle,
5163
+ ...vStyle,
5164
+ transform,
5165
+ width: ww ? toCss(ww, "x") : `${size}%`,
5166
+ opacity: manualOpacity ?? opacity,
5167
+ cursor: "default"
5168
+ };
5169
+ }, debouncedOnChange = React.useCallback(
5170
+ (newWatermark) => {
5171
+ debounceTimeoutRef.current && clearTimeout(debounceTimeoutRef.current), debounceTimeoutRef.current = setTimeout(() => {
5172
+ onChange(newWatermark);
5173
+ }, 300);
5174
+ },
5175
+ [onChange]
5176
+ );
5177
+ React.useEffect(() => () => {
5178
+ debounceTimeoutRef.current && clearTimeout(debounceTimeoutRef.current);
5179
+ }, []), React.useEffect(() => {
5180
+ !isDragging && watermark.position && setLocalPosition(watermark.position);
5181
+ }, [watermark.position, isDragging]);
5182
+ const handleMouseDown = React.useCallback(
5183
+ (e) => {
5184
+ e.preventDefault(), setIsDragging(!0), setDragStart({ x: e.clientX, y: e.clientY }), setStartPosition({ x: position.x, y: position.y });
5185
+ },
5186
+ [position]
5187
+ ), handleMouseMove = React.useCallback(
5188
+ (e) => {
5189
+ if (!isDragging || !containerRef?.current) return;
5190
+ const rect = containerRef.current.getBoundingClientRect(), content = getVideoContentBox(), contentW = content.width || rect.width, contentH = content.height || rect.height, dx = e.clientX - dragStart.x, dy = e.clientY - dragStart.y, deltaXPercent = dx / contentW * 100, deltaYPercent = dy / contentH * 100;
5191
+ let newX = startPosition.x + deltaXPercent, newY = startPosition.y + deltaYPercent;
5192
+ newX = Math.max(0, Math.min(100, newX)), newY = Math.max(0, Math.min(100, newY)), setLocalPosition({ x: newX, y: newY }), debouncedOnChange({
5193
+ ...watermark,
5194
+ position: { x: newX, y: newY }
5195
+ });
5196
+ },
5197
+ [
5198
+ isDragging,
5199
+ dragStart,
5200
+ startPosition,
5201
+ containerRef,
5202
+ watermark,
5203
+ debouncedOnChange,
5204
+ getVideoContentBox
5205
+ ]
5206
+ ), handleMouseUp = React.useCallback(() => {
5207
+ setIsDragging(!1), debounceTimeoutRef.current && (clearTimeout(debounceTimeoutRef.current), debounceTimeoutRef.current = null), onChange({
5208
+ ...watermark,
5209
+ position: localPosition
5210
+ });
5211
+ }, [watermark, localPosition, onChange]);
5212
+ if (React.useEffect(() => {
5213
+ if (isDragging)
5214
+ return document.addEventListener("mousemove", handleMouseMove), document.addEventListener("mouseup", handleMouseUp), () => {
5215
+ document.removeEventListener("mousemove", handleMouseMove), document.removeEventListener("mouseup", handleMouseUp);
5216
+ };
5217
+ }, [isDragging, handleMouseMove, handleMouseUp]), !watermark.imageUrl)
5218
+ return null;
5219
+ const hasManualOverlay = !!watermark.overlay_settings, opacityForRender = hasManualOverlay ? parseOpacityPercent(watermark.overlay_settings?.opacity) ?? opacity : opacity, contentBox = getVideoContentBox(), hasContentBox = contentBox.width > 0 && contentBox.height > 0;
5220
+ return /* @__PURE__ */ jsxRuntime.jsx(
5221
+ WatermarkOverlay,
5222
+ {
5223
+ ref: watermarkRef,
5224
+ $opacity: opacityForRender,
5225
+ onMouseDown: hasManualOverlay ? void 0 : handleMouseDown,
5226
+ style: hasManualOverlay ? computeManualStyle(watermark.overlay_settings) : hasContentBox ? {
5227
+ left: `${contentBox.x + position.x / 100 * contentBox.width}px`,
5228
+ top: `${contentBox.y + position.y / 100 * contentBox.height}px`,
5229
+ transform: "translate(-50%, -50%)",
5230
+ width: `${Math.max(1, size / 100 * contentBox.width)}px`,
5231
+ cursor: isDragging ? "grabbing" : "grab"
5232
+ } : {
5233
+ left: `${position.x}%`,
5234
+ top: `${position.y}%`,
5235
+ transform: "translate(-50%, -50%)",
5236
+ width: `${size}%`,
5237
+ cursor: isDragging ? "grabbing" : "grab"
5238
+ },
5239
+ children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: watermark.imageUrl, alt: "Watermark", draggable: !1 })
5240
+ }
5241
+ );
5242
+ }
5243
+ function WatermarkControls({
5244
+ watermark,
5245
+ onChange,
5246
+ onValidationChange,
5247
+ previewContainerRef,
5248
+ previewVideoRef
5249
+ }) {
5250
+ const [urlInput, setUrlInput] = React.useState(watermark.imageUrl || ""), [urlError, setUrlError] = React.useState(null), [isValidating, setIsValidating] = React.useState(!1), [isValid, setIsValid] = React.useState(null), validationTimeoutRef = React.useRef(null), [mode, setMode] = React.useState(
5251
+ watermark.overlay_settings ? "manual" : "canvas"
5252
+ ), isUpdatingRef = React.useRef(!1), isValidExtension = (extension) => extension.endsWith(".png") || extension.endsWith(".jpg") || extension.endsWith(".jpeg"), validateUrl = React.useCallback(
5253
+ (url) => {
5254
+ if (validationTimeoutRef.current && clearTimeout(validationTimeoutRef.current), !url) {
5255
+ setUrlError(null), setIsValid(null), setIsValidating(!1), onValidationChange?.(null), isUpdatingRef.current = !0, onChange({
5256
+ ...watermark,
5257
+ enabled: !1,
5258
+ imageUrl: void 0,
5259
+ overlay_settings: void 0
5260
+ });
5261
+ return;
5262
+ }
5263
+ setIsValidating(!0), setIsValid(null), setUrlError(null), validationTimeoutRef.current = setTimeout(() => {
5264
+ try {
5265
+ const pathname = new URL(url).pathname.toLowerCase();
5266
+ if (isValidExtension(pathname)) {
5267
+ setIsValid(!0), setUrlError(null), onValidationChange?.(null);
5268
+ const img = new Image();
5269
+ img.onload = () => {
5270
+ const imageAspectRatio = img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 1;
5271
+ isUpdatingRef.current = !0, onChange({
5272
+ ...watermark,
5273
+ enabled: !0,
5274
+ imageUrl: url,
5275
+ imageAspectRatio
5276
+ });
5277
+ }, img.onerror = () => {
5278
+ isUpdatingRef.current = !0, onChange({
5279
+ ...watermark,
5280
+ enabled: !0,
5281
+ imageUrl: url,
5282
+ imageAspectRatio: watermark.imageAspectRatio
5283
+ });
5284
+ }, img.src = url;
5285
+ } else {
5286
+ const errorMsg = "Mux only supports PNG and JPG watermark images. Please use a .png or .jpg file.";
5287
+ setIsValid(!1), setUrlError(errorMsg), onValidationChange?.(errorMsg), isUpdatingRef.current = !0, onChange({
5288
+ ...watermark,
5289
+ enabled: !1,
5290
+ imageUrl: void 0,
5291
+ imageAspectRatio: void 0,
5292
+ overlay_settings: void 0
5293
+ });
5294
+ }
5295
+ } catch {
5296
+ setIsValid(!1);
5297
+ const errorMsg = "Please enter a valid URL (e.g., https://example.com/watermark.png)";
5298
+ setUrlError(errorMsg), onValidationChange?.(errorMsg), isUpdatingRef.current = !0, onChange({
5299
+ ...watermark,
5300
+ enabled: !1,
5301
+ imageUrl: void 0,
5302
+ imageAspectRatio: void 0,
5303
+ overlay_settings: void 0
5304
+ });
5305
+ } finally {
5306
+ setIsValidating(!1);
5307
+ }
5308
+ }, 500);
5309
+ },
5310
+ [watermark, onChange, onValidationChange]
5311
+ );
5312
+ React.useEffect(() => () => {
5313
+ validationTimeoutRef.current && clearTimeout(validationTimeoutRef.current);
5314
+ }, []), React.useEffect(() => {
5315
+ setMode(watermark.overlay_settings ? "manual" : "canvas");
5316
+ }, [watermark.overlay_settings]);
5317
+ const handleUrlChange = (e) => {
5318
+ const url = e.target.value;
5319
+ setUrlInput(url), watermark.imageUrl && url !== watermark.imageUrl && (isUpdatingRef.current = !0, onChange({
5320
+ ...watermark,
5321
+ enabled: !1,
5322
+ imageUrl: void 0,
5323
+ imageAspectRatio: void 0,
5324
+ overlay_settings: void 0
5325
+ })), validateUrl(url);
5326
+ }, normalizeZeroPercent = (value) => {
5327
+ if (!value) return value;
5328
+ const trimmed = value.trim();
5329
+ if (!trimmed.endsWith("%")) return value;
5330
+ const n = Number(trimmed.slice(0, -1));
5331
+ return Number.isFinite(n) ? n === 0 || Object.is(n, -0) || Math.abs(n) < 1e-9 ? "0.01%" : `${n}%` : value;
5332
+ }, updateOverlaySettings = (next) => {
5333
+ const merged = {
5334
+ ...watermark.overlay_settings ?? {
5335
+ vertical_align: "bottom",
5336
+ vertical_margin: "2%",
5337
+ horizontal_align: "right",
5338
+ horizontal_margin: "2%",
5339
+ width: `${watermark.size ?? 20}%`,
5340
+ opacity: `${Math.round((watermark.opacity ?? 0.7) * 100)}%`
5341
+ },
5342
+ ...next
5343
+ };
5344
+ onChange({
5345
+ ...watermark,
5346
+ enabled: !0,
5347
+ overlay_settings: {
5348
+ ...merged,
5349
+ horizontal_margin: normalizeZeroPercent(merged.horizontal_margin) || merged.horizontal_margin,
5350
+ vertical_margin: normalizeZeroPercent(merged.vertical_margin) || merged.vertical_margin
5351
+ }
5352
+ });
5353
+ }, getVideoContentBox = () => {
5354
+ const container = previewContainerRef?.current;
5355
+ if (!container) return { x: 0, y: 0, width: 0, height: 0 };
5356
+ const rect = container.getBoundingClientRect(), containerW = rect.width, containerH = rect.height, videoEl = previewVideoRef?.current, videoW = videoEl?.videoWidth || 0, videoH = videoEl?.videoHeight || 0;
5357
+ if (!videoW || !videoH || !containerW || !containerH)
5358
+ return { x: 0, y: 0, width: containerW, height: containerH };
5359
+ const scale = Math.min(containerW / videoW, containerH / videoH), contentW = videoW * scale, contentH = videoH * scale, offsetX = (containerW - contentW) / 2, offsetY = (containerH - contentH) / 2;
5360
+ return { x: offsetX, y: offsetY, width: contentW, height: contentH };
5361
+ };
5362
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
5363
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5364
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: "Watermark Image URL" }),
5365
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "Enter a URL to a PNG or JPG image. Mux will download this image and overlay it on your video." }),
5366
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { style: { position: "relative", width: "100%" }, children: [
5367
+ /* @__PURE__ */ jsxRuntime.jsx(
5368
+ "input",
5369
+ {
5370
+ type: "url",
5371
+ value: urlInput,
5372
+ onChange: handleUrlChange,
5373
+ placeholder: "https://example.com/watermark.png",
5374
+ style: {
5375
+ padding: "8px 12px",
5376
+ paddingRight: urlInput ? "96px" : isValid !== null ? "36px" : "12px",
5377
+ border: urlError || isValid === !1 ? "1px solid #e74c3c" : isValid === !0 ? "1px solid #4caf50" : "1px solid #ccc",
5378
+ borderRadius: "4px",
5379
+ width: "100%",
5380
+ maxWidth: "100%",
5381
+ boxSizing: "border-box",
5382
+ fontSize: "14px"
5383
+ }
5384
+ }
5385
+ ),
5386
+ (urlInput || isValidating || isValid !== null) && /* @__PURE__ */ jsxRuntime.jsxs(
5387
+ ui.Box,
5388
+ {
5389
+ style: {
5390
+ position: "absolute",
5391
+ right: "8px",
5392
+ top: "50%",
5393
+ transform: "translateY(-50%)",
5394
+ display: "flex",
5395
+ alignItems: "center",
5396
+ gap: "4px"
5397
+ },
5398
+ children: [
5399
+ urlInput && /* @__PURE__ */ jsxRuntime.jsx(
5400
+ ui.Button,
5401
+ {
5402
+ text: "Clear",
5403
+ mode: "bleed",
5404
+ tone: "critical",
5405
+ onClick: () => {
5406
+ setUrlInput(""), validateUrl("");
5407
+ },
5408
+ disabled: isValidating,
5409
+ style: { fontSize: "11px", height: "24px" }
5410
+ }
5411
+ ),
5412
+ isValidating && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "Validating..." }),
5413
+ isValid === !0 && !isValidating && /* @__PURE__ */ jsxRuntime.jsx(icons.CheckmarkCircleIcon, { style: { color: "#4caf50", fontSize: "18px" } }),
5414
+ isValid === !1 && !isValidating && /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { color: "#e74c3c", fontSize: "18px" } })
5415
+ ]
5416
+ }
5417
+ )
5418
+ ] }),
5419
+ urlError && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, tone: "critical", radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5420
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { color: "#e74c3c", flexShrink: 0 } }),
5421
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, style: { color: "#e74c3c" }, children: urlError })
5422
+ ] }) })
5423
+ ] }),
5424
+ watermark.imageUrl && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5425
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "transparent", border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(
5426
+ ui.Flex,
5427
+ {
5428
+ align: "center",
5429
+ justify: "space-between",
5430
+ gap: 3,
5431
+ style: { flexWrap: "wrap", alignItems: "flex-start" },
5432
+ children: [
5433
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 240, flex: 1 }, children: [
5434
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: "Positioning mode" }),
5435
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 0, muted: !0, children: [
5436
+ "Choose between dragging on the canvas or manually editing the Mux",
5437
+ " ",
5438
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "overlay_settings" }),
5439
+ " fields (as in",
5440
+ " ",
5441
+ /* @__PURE__ */ jsxRuntime.jsx(
5442
+ "a",
5443
+ {
5444
+ href: "https://www.mux.com/docs/guides/add-watermarks-to-your-videos",
5445
+ target: "_blank",
5446
+ rel: "noopener noreferrer",
5447
+ children: "the docs"
5448
+ }
5449
+ ),
5450
+ ")."
5451
+ ] })
5452
+ ] }),
5453
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, style: { flexWrap: "wrap" }, children: [
5454
+ /* @__PURE__ */ jsxRuntime.jsx(
5455
+ ui.Button,
5456
+ {
5457
+ text: "Canvas",
5458
+ mode: mode === "canvas" ? "default" : "ghost",
5459
+ onClick: () => {
5460
+ setMode("canvas"), onChange({ ...watermark, enabled: !0, overlay_settings: void 0 });
5461
+ }
5462
+ }
5463
+ ),
5464
+ /* @__PURE__ */ jsxRuntime.jsx(
5465
+ ui.Button,
5466
+ {
5467
+ text: "Manual",
5468
+ mode: mode === "manual" ? "default" : "ghost",
5469
+ onClick: () => {
5470
+ setMode("manual");
5471
+ const overlay = convertWatermarkToMuxOverlay({ ...watermark, enabled: !0 });
5472
+ updateOverlaySettings(overlay ?? {});
5473
+ }
5474
+ }
5475
+ )
5476
+ ] })
5477
+ ]
5478
+ }
5479
+ ) }),
5480
+ mode === "manual" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "transparent", border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
5481
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: "Mux overlay_settings" }),
5482
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { columns: [1, 2], gap: 3, style: { width: "100%" }, children: [
5483
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 0 }, children: [
5484
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "horizontal_align" }),
5485
+ /* @__PURE__ */ jsxRuntime.jsxs(
5486
+ "select",
5487
+ {
5488
+ value: watermark.overlay_settings?.horizontal_align || "right",
5489
+ onChange: (e) => updateOverlaySettings({
5490
+ horizontal_align: e.target.value || "right"
5491
+ }),
5492
+ style: {
5493
+ width: "100%",
5494
+ padding: "8px 10px",
5495
+ border: "1px solid #ccc",
5496
+ borderRadius: 4
5497
+ },
5498
+ children: [
5499
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "left", children: "left" }),
5500
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "center", children: "center" }),
5501
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "right", children: "right" })
5502
+ ]
5503
+ }
5504
+ )
5505
+ ] }),
5506
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 0 }, children: [
5507
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "horizontal_margin (e.g. 2% or 40px)" }),
5508
+ /* @__PURE__ */ jsxRuntime.jsx(
5509
+ ui.TextInput,
5510
+ {
5511
+ value: watermark.overlay_settings?.horizontal_margin || "2%",
5512
+ onChange: (e) => updateOverlaySettings({ horizontal_margin: e.currentTarget.value })
5513
+ }
5514
+ )
5515
+ ] }),
5516
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 0 }, children: [
5517
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "vertical_align" }),
5518
+ /* @__PURE__ */ jsxRuntime.jsxs(
5519
+ "select",
5520
+ {
5521
+ value: watermark.overlay_settings?.vertical_align || "bottom",
5522
+ onChange: (e) => updateOverlaySettings({
5523
+ vertical_align: e.target.value || "bottom"
5524
+ }),
5525
+ style: {
5526
+ width: "100%",
5527
+ padding: "8px 10px",
5528
+ border: "1px solid #ccc",
5529
+ borderRadius: 4
5530
+ },
5531
+ children: [
5532
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "top", children: "top" }),
5533
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "middle", children: "middle" }),
5534
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "bottom", children: "bottom" })
5535
+ ]
5536
+ }
5537
+ )
5538
+ ] }),
5539
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 0 }, children: [
5540
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "vertical_margin (e.g. 2% or 40px)" }),
5541
+ /* @__PURE__ */ jsxRuntime.jsx(
5542
+ ui.TextInput,
5543
+ {
5544
+ value: watermark.overlay_settings?.vertical_margin || "2%",
5545
+ onChange: (e) => updateOverlaySettings({ vertical_margin: e.currentTarget.value })
5546
+ }
5547
+ )
5548
+ ] }),
5549
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 0 }, children: [
5550
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "width (e.g. 25% or 80px)" }),
5551
+ /* @__PURE__ */ jsxRuntime.jsx(
5552
+ ui.TextInput,
5553
+ {
5554
+ value: watermark.overlay_settings?.width || `${watermark.size ?? 20}%`,
5555
+ onChange: (e) => updateOverlaySettings({ width: e.currentTarget.value })
5556
+ }
5557
+ )
5558
+ ] }),
5559
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { minWidth: 0 }, children: [
5560
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "opacity (e.g. 90%)" }),
5561
+ /* @__PURE__ */ jsxRuntime.jsx(
5562
+ ui.TextInput,
5563
+ {
5564
+ value: watermark.overlay_settings?.opacity || `${Math.round((watermark.opacity ?? 0.7) * 100)}%`,
5565
+ onChange: (e) => updateOverlaySettings({ opacity: e.currentTarget.value })
5566
+ }
5567
+ )
5568
+ ] })
5569
+ ] }),
5570
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: "Margins and width accept either percentages or pixels, per the Mux guide." })
5571
+ ] }) }),
5572
+ mode === "canvas" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5573
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { children: [
5574
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: (() => {
5575
+ const sizePct = watermark.size || 20, contentW = getVideoContentBox().width;
5576
+ return contentW ? `Size: ${Math.max(1, Math.round(sizePct / 100 * contentW))}px` : `Size: ${sizePct}%`;
5577
+ })() }),
5578
+ /* @__PURE__ */ jsxRuntime.jsx(
5579
+ RangeInput,
5580
+ {
5581
+ type: "range",
5582
+ value: (() => {
5583
+ const sizePct = watermark.size || 20, contentW = getVideoContentBox().width;
5584
+ return contentW ? Math.max(1, Math.round(sizePct / 100 * contentW)) : sizePct;
5585
+ })(),
5586
+ min: (() => {
5587
+ const contentW = getVideoContentBox().width;
5588
+ return contentW ? Math.max(1, Math.round(contentW * 0.05)) : 5;
5589
+ })(),
5590
+ max: (() => {
5591
+ const contentW = getVideoContentBox().width;
5592
+ return contentW ? Math.max(1, Math.round(contentW * 0.5)) : 50;
5593
+ })(),
5594
+ step: 1,
5595
+ onChange: (e) => {
5596
+ const raw = Number(e.target.value), contentW = getVideoContentBox().width, nextPct = contentW ? raw / contentW * 100 : raw, clampedPct = Math.max(5, Math.min(50, nextPct));
5597
+ onChange({
5598
+ ...watermark,
5599
+ size: clampedPct
5600
+ });
5601
+ }
5602
+ }
5603
+ )
5604
+ ] }),
5605
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { children: [
5606
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, weight: "medium", children: [
5607
+ "Opacity: ",
5608
+ Math.round((watermark.opacity ?? 0.7) * 100),
5609
+ "%"
5610
+ ] }),
5611
+ /* @__PURE__ */ jsxRuntime.jsx(
5612
+ RangeInput,
5613
+ {
5614
+ type: "range",
5615
+ value: watermark.opacity ?? 0.7,
5616
+ min: 0,
5617
+ max: 1,
5618
+ step: 0.05,
5619
+ onChange: (e) => onChange({
5620
+ ...watermark,
5621
+ opacity: Number(e.target.value)
5622
+ })
5623
+ }
5624
+ )
5625
+ ] })
5626
+ ] }),
5627
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, tone: "transparent", border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, children: mode === "manual" ? "Manual mode: edit the overlay_settings fields above" : "\u{1F4A1} Drag the watermark on the preview to position it" }) })
5628
+ ] })
5629
+ ] });
5630
+ }
4995
5631
  const ALL_LANGUAGE_CODES = LanguagesList__default.default.getAllCodes().map((code) => ({
4996
5632
  value: code,
4997
5633
  label: LanguagesList__default.default.getNativeName(code)
@@ -5433,7 +6069,7 @@ function UploadConfiguration({
5433
6069
  startUpload,
5434
6070
  onClose
5435
6071
  }) {
5436
- const id = React.useId(), autoTextTracks = React.useRef(
6072
+ const id = React.useId(), [watermarkValidationError, setWatermarkValidationError] = React.useState(null), watermarkPreviewContainerRef = React.useRef(null), watermarkPreviewVideoRef = React.useRef(null), autoTextTracks = React.useRef(
5437
6073
  pluginConfig.video_quality === "plus" && pluginConfig.defaultAutogeneratedSubtitleLang ? [
5438
6074
  {
5439
6075
  _id: uuid.uuid(),
@@ -5469,6 +6105,8 @@ function UploadConfiguration({
5469
6105
  return Object.assign({}, prev, { [action.action]: action.value });
5470
6106
  case "drm_policy":
5471
6107
  return Object.assign({}, prev, { [action.action]: action.value });
6108
+ case "watermark":
6109
+ return Object.assign({}, prev, { watermark: action.value });
5472
6110
  // Updating individual tracks
5473
6111
  case "track": {
5474
6112
  const text_tracks = [...prev.text_tracks], target_track_i = text_tracks.findIndex(({ _id: _id2 }) => _id2 === action.id);
@@ -5536,7 +6174,12 @@ function UploadConfiguration({
5536
6174
  ]);
5537
6175
  const { disableTextTrackConfig, disableUploadConfig } = pluginConfig, skipConfig = disableTextTrackConfig && disableUploadConfig;
5538
6176
  if (React.useEffect(() => {
5539
- skipConfig && startUpload(formatUploadConfig(config, secrets));
6177
+ if (skipConfig) {
6178
+ const { settings, watermark } = formatUploadConfig(config, secrets, {
6179
+ videoAspectRatio: videoAssetMetadata?.aspectRatio
6180
+ });
6181
+ startUpload(settings, watermark);
6182
+ }
5540
6183
  }, []), skipConfig) return null;
5541
6184
  const basicConfig = config.video_quality !== "plus" && config.video_quality !== "premium", playbackPolicySelected = config.public_policy || config.signed_policy || config.drm_policy, maxSupportedResolution = RESOLUTION_TIERS.findIndex(
5542
6185
  (rt) => rt.value === pluginConfig.max_resolution_tier
@@ -5552,11 +6195,11 @@ function UploadConfiguration({
5552
6195
  header: "Configure Mux Upload",
5553
6196
  onClose,
5554
6197
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 2, children: [
5555
- validationError && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "critical", radius: 2, marginBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "flex-start", children: [
6198
+ (validationError || watermarkValidationError) && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "critical", radius: 2, marginBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "flex-start", children: [
5556
6199
  /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { width: 20, height: 20 }),
5557
6200
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5558
6201
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Validation Error" }),
5559
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: validationError })
6202
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: validationError || watermarkValidationError })
5560
6203
  ] })
5561
6204
  ] }) }),
5562
6205
  /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 3, children: "FILE TO UPLOAD" }),
@@ -5623,27 +6266,41 @@ function UploadConfiguration({
5623
6266
  }) })
5624
6267
  }
5625
6268
  ),
5626
- !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(sanity.FormField, { title: "Additional Configuration", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
5627
- /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicy, { id, config, secrets, dispatch }),
5628
- maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
5629
- ResolutionTierSelector,
6269
+ !basicConfig && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6270
+ /* @__PURE__ */ jsxRuntime.jsx(sanity.FormField, { title: "Additional Configuration", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
6271
+ /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicy, { id, config, secrets, dispatch }),
6272
+ maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
6273
+ ResolutionTierSelector,
6274
+ {
6275
+ id,
6276
+ config,
6277
+ dispatch,
6278
+ maxSupportedResolution
6279
+ }
6280
+ ),
6281
+ /* @__PURE__ */ jsxRuntime.jsx(StaticRenditionSelector, { id, config, dispatch }),
6282
+ !disableTextTrackConfig && /* @__PURE__ */ jsxRuntime.jsx(
6283
+ TextTracksEditor,
6284
+ {
6285
+ tracks: config.text_tracks,
6286
+ dispatch,
6287
+ defaultLang: pluginConfig.defaultAutogeneratedSubtitleLang
6288
+ }
6289
+ )
6290
+ ] }) }),
6291
+ /* @__PURE__ */ jsxRuntime.jsx(
6292
+ WatermarkSection,
5630
6293
  {
5631
- id,
5632
6294
  config,
5633
6295
  dispatch,
5634
- maxSupportedResolution
5635
- }
5636
- ),
5637
- /* @__PURE__ */ jsxRuntime.jsx(StaticRenditionSelector, { id, config, dispatch }),
5638
- !disableTextTrackConfig && /* @__PURE__ */ jsxRuntime.jsx(
5639
- TextTracksEditor,
5640
- {
5641
- tracks: config.text_tracks,
5642
- dispatch,
5643
- defaultLang: pluginConfig.defaultAutogeneratedSubtitleLang
6296
+ stagedUpload,
6297
+ videoAssetMetadata,
6298
+ watermarkPreviewContainerRef,
6299
+ watermarkPreviewVideoRef,
6300
+ onValidationChange: setWatermarkValidationError
5644
6301
  }
5645
6302
  )
5646
- ] }) })
6303
+ ] })
5647
6304
  ] }),
5648
6305
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
5649
6306
  ui.Button,
@@ -5653,7 +6310,12 @@ function UploadConfiguration({
5653
6310
  text: "Upload",
5654
6311
  tone: "positive",
5655
6312
  onClick: () => {
5656
- validationError || startUpload(formatUploadConfig(config, secrets));
6313
+ if (!validationError) {
6314
+ const { settings, watermark } = formatUploadConfig(config, secrets, {
6315
+ videoAspectRatio: videoAssetMetadata?.aspectRatio
6316
+ });
6317
+ startUpload(settings, watermark);
6318
+ }
5657
6319
  }
5658
6320
  }
5659
6321
  ) })
@@ -5668,36 +6330,178 @@ function setAdvancedPlaybackPolicy(config, secrets) {
5668
6330
  drm_configuration_id: secrets.drmConfigId ?? void 0
5669
6331
  }) : console.error("Selected DRM Policy but missing DRM Configuration Id")), advanced_playback_policies;
5670
6332
  }
5671
- function formatUploadConfig(config, secrets) {
6333
+ function formatUploadConfig(config, secrets, options) {
5672
6334
  const generated_subtitles = config.text_tracks.filter(isAutogeneratedTrack).map((track) => ({
5673
6335
  name: track.name,
5674
6336
  language_code: track.language_code
5675
- }));
6337
+ })), inputs = [
6338
+ {
6339
+ type: "video",
6340
+ generated_subtitles: generated_subtitles.length > 0 ? generated_subtitles : void 0
6341
+ },
6342
+ ...config.text_tracks.filter(isCustomTextTrack).reduce(
6343
+ (acc, track) => (track.language_code && track.file && track.name && acc.push({
6344
+ url: track.file.contents,
6345
+ type: "text",
6346
+ text_type: track.type === "subtitles" ? "subtitles" : void 0,
6347
+ language_code: track.language_code,
6348
+ name: track.name,
6349
+ closed_captions: track.type === "captions"
6350
+ }), acc),
6351
+ []
6352
+ )
6353
+ ];
6354
+ if (config.watermark?.imageUrl) {
6355
+ const watermarkForMux = { ...config.watermark, enabled: !0 }, overlaySettings = convertWatermarkToMuxOverlay(watermarkForMux, {
6356
+ videoAspectRatio: options?.videoAspectRatio ?? void 0,
6357
+ units: "px"
6358
+ });
6359
+ overlaySettings && inputs.push({
6360
+ url: config.watermark.imageUrl,
6361
+ overlay_settings: overlaySettings
6362
+ });
6363
+ }
5676
6364
  return {
5677
- input: [
5678
- {
5679
- type: "video",
5680
- generated_subtitles: generated_subtitles.length > 0 ? generated_subtitles : void 0
5681
- },
5682
- ...config.text_tracks.filter(isCustomTextTrack).reduce(
5683
- (acc, track) => (track.language_code && track.file && track.name && acc.push({
5684
- url: track.file.contents,
5685
- type: "text",
5686
- text_type: track.type === "subtitles" ? "subtitles" : void 0,
5687
- language_code: track.language_code,
5688
- name: track.name,
5689
- closed_captions: track.type === "captions"
5690
- }), acc),
5691
- []
5692
- )
5693
- ],
5694
- static_renditions: config.static_renditions.length > 0 ? config.static_renditions.map((resolution) => ({ resolution })) : void 0,
5695
- advanced_playback_policies: setAdvancedPlaybackPolicy(config, secrets),
5696
- max_resolution_tier: config.max_resolution_tier,
5697
- video_quality: config.video_quality,
5698
- normalize_audio: config.normalize_audio
6365
+ settings: {
6366
+ input: inputs,
6367
+ static_renditions: config.static_renditions.length > 0 ? config.static_renditions.map((resolution) => ({ resolution })) : void 0,
6368
+ advanced_playback_policies: setAdvancedPlaybackPolicy(config, secrets),
6369
+ max_resolution_tier: config.max_resolution_tier,
6370
+ video_quality: config.video_quality,
6371
+ normalize_audio: config.normalize_audio
6372
+ },
6373
+ watermark: config.watermark?.imageUrl ? { ...config.watermark, enabled: !0 } : void 0
5699
6374
  };
5700
6375
  }
6376
+ function WatermarkSection({
6377
+ config,
6378
+ dispatch,
6379
+ stagedUpload,
6380
+ videoAssetMetadata,
6381
+ watermarkPreviewContainerRef,
6382
+ watermarkPreviewVideoRef,
6383
+ onValidationChange
6384
+ }) {
6385
+ return videoAssetMetadata?.isAudioOnly !== !1 ? null : /* @__PURE__ */ jsxRuntime.jsx(
6386
+ sanity.FormField,
6387
+ {
6388
+ title: "Watermark",
6389
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6390
+ "Add a watermark overlay to your video using Mux's native watermark support.",
6391
+ " ",
6392
+ /* @__PURE__ */ jsxRuntime.jsx(
6393
+ "a",
6394
+ {
6395
+ href: "https://www.mux.com/docs/guides/add-watermarks-to-your-videos",
6396
+ target: "_blank",
6397
+ rel: "noopener noreferrer",
6398
+ children: "Learn more about Mux watermarks."
6399
+ }
6400
+ )
6401
+ ] }),
6402
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
6403
+ /* @__PURE__ */ jsxRuntime.jsx(
6404
+ WatermarkControls,
6405
+ {
6406
+ watermark: config.watermark || { enabled: !1 },
6407
+ onChange: (watermark) => {
6408
+ dispatch({ action: "watermark", value: watermark });
6409
+ },
6410
+ onValidationChange,
6411
+ previewContainerRef: watermarkPreviewContainerRef,
6412
+ previewVideoRef: watermarkPreviewVideoRef
6413
+ }
6414
+ ),
6415
+ config.watermark?.imageUrl && stagedUpload.type === "file" && // Canvas preview is only shown in "Canvas" mode (no explicit overlay_settings)
6416
+ !config.watermark.overlay_settings && /* @__PURE__ */ jsxRuntime.jsx(
6417
+ WatermarkPreview,
6418
+ {
6419
+ stagedUpload,
6420
+ watermark: config.watermark,
6421
+ videoAspectRatio: videoAssetMetadata.aspectRatio,
6422
+ onWatermarkChange: (watermark) => {
6423
+ dispatch({ action: "watermark", value: watermark });
6424
+ },
6425
+ previewContainerRef: watermarkPreviewContainerRef,
6426
+ videoRef: watermarkPreviewVideoRef
6427
+ }
6428
+ )
6429
+ ] })
6430
+ }
6431
+ );
6432
+ }
6433
+ const WatermarkPreview = React.memo(function({
6434
+ stagedUpload,
6435
+ watermark,
6436
+ onWatermarkChange,
6437
+ videoAspectRatio,
6438
+ previewContainerRef,
6439
+ videoRef
6440
+ }) {
6441
+ React.useEffect(() => {
6442
+ if (videoRef.current && stagedUpload.type === "file") {
6443
+ const file = stagedUpload.files[0], url = URL.createObjectURL(file);
6444
+ return videoRef.current.src = url, () => {
6445
+ URL.revokeObjectURL(url);
6446
+ };
6447
+ }
6448
+ }, [stagedUpload, videoRef]);
6449
+ const isVertical = videoAspectRatio != null && videoAspectRatio < 1;
6450
+ return /* @__PURE__ */ jsxRuntime.jsx(
6451
+ ui.Card,
6452
+ {
6453
+ tone: "transparent",
6454
+ border: !0,
6455
+ style: {
6456
+ overflow: "hidden",
6457
+ // For vertical videos, center the preview and limit its width
6458
+ display: "flex",
6459
+ justifyContent: "center"
6460
+ },
6461
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
6462
+ "div",
6463
+ {
6464
+ ref: previewContainerRef,
6465
+ style: {
6466
+ position: "relative",
6467
+ // For vertical videos: limit width so the preview doesn't get too tall
6468
+ // For horizontal videos: use full width
6469
+ width: isVertical ? "auto" : "100%",
6470
+ aspectRatio: videoAspectRatio ? String(videoAspectRatio) : "16/9",
6471
+ ...isVertical ? { height: "400px", maxHeight: "50vh" } : { minHeight: "200px" },
6472
+ overflow: "hidden"
6473
+ },
6474
+ children: [
6475
+ /* @__PURE__ */ jsxRuntime.jsx(
6476
+ "video",
6477
+ {
6478
+ ref: videoRef,
6479
+ style: {
6480
+ position: "absolute",
6481
+ top: 0,
6482
+ left: 0,
6483
+ width: "100%",
6484
+ height: "100%",
6485
+ objectFit: "fill",
6486
+ display: "block"
6487
+ }
6488
+ }
6489
+ ),
6490
+ /* @__PURE__ */ jsxRuntime.jsx(
6491
+ DraggableWatermark,
6492
+ {
6493
+ watermark,
6494
+ onChange: onWatermarkChange,
6495
+ containerRef: previewContainerRef,
6496
+ videoElementRef: videoRef
6497
+ }
6498
+ )
6499
+ ]
6500
+ }
6501
+ )
6502
+ }
6503
+ );
6504
+ });
5701
6505
  function withFocusRing(component) {
5702
6506
  return styledComponents.styled(component)((props) => {
5703
6507
  const border = {
@@ -5936,7 +6740,7 @@ function Uploader(props) {
5936
6740
  window.removeEventListener("beforeunload", handleBeforeUnload), window.removeEventListener("pagehide", handleBeforeUnload), cleanup();
5937
6741
  };
5938
6742
  }, [props.client, props.asset?._id]);
5939
- const startUpload = (settings) => {
6743
+ const startUpload = (settings, watermark) => {
5940
6744
  const { stagedUpload } = state;
5941
6745
  if (!stagedUpload || uploadRef.current) return;
5942
6746
  dispatch({ action: "commitUpload" });
@@ -5946,14 +6750,16 @@ function Uploader(props) {
5946
6750
  uploadObservable = uploadUrl({
5947
6751
  client: props.client,
5948
6752
  url: stagedUpload.url,
5949
- settings
6753
+ settings,
6754
+ watermark
5950
6755
  });
5951
6756
  break;
5952
6757
  case "file":
5953
6758
  uploadObservable = uploadFile({
5954
6759
  client: props.client,
5955
6760
  file: stagedUpload.files[0],
5956
- settings
6761
+ settings,
6762
+ watermark
5957
6763
  }).pipe(
5958
6764
  operators.takeUntil(
5959
6765
  cancelUploadButton.observable.pipe(