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