@scarlett-player/watermark 1.12.0 → 1.14.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @scarlett-player/watermark
2
2
 
3
- Anti-piracy watermark plugin for [Scarlett Player](https://scarlettplayer.com). Overlays text (typically the viewer's email or account id) or an image on the player, at a fixed corner or moving to a random position on a timer, with an optional delay before it first appears. The overlay is shown on play and hidden on pause and ended, so it never sits on the poster.
3
+ Anti-piracy watermark plugin for [Scarlett Player](https://scarlettplayer.com). Overlays text (typically the viewer's email or account id) or an image on the player, at a fixed corner or moving to a random position on a timer, with an optional delay before it first appears. The overlay is hidden until the first play and hidden again on ended, so it never sits on the poster. It stays visible while paused — hiding it there would leave screenshots and screen captures unmarked.
4
4
 
5
5
  ## Installation
6
6
 
@@ -81,7 +81,7 @@ watermark?.getConfig();
81
81
  | `setOpacity(opacity)` | Set opacity, clamped to 0 to 1 |
82
82
  | `setImageHeight(height)` | Set the maximum image height in px |
83
83
  | `setPadding(padding)` | Set the edge padding in px for every edge and re-apply the current position |
84
- | `show()` / `hide()` | Toggle visibility without touching the timers |
84
+ | `show()` / `hide()` | Toggle the mark's inline `visibility` without touching the timers |
85
85
  | `getConfig()` | The original config merged with the current position, opacity, image height and padding |
86
86
 
87
87
  ## Per-track watermarks
@@ -94,17 +94,19 @@ When `@scarlett-player/playlist` is present the plugin listens for `playlist:cha
94
94
 
95
95
  ## Styling
96
96
 
97
- The overlay is a single `div` appended to the player container with inline positioning, `pointer-events: none`, white text with a subtle shadow, and a 0.5s transition. It carries these classes for your own CSS:
97
+ The overlay is a single `div` appended to the player container with inline positioning, `pointer-events: none`, white text with a subtle shadow, and a 0.5s transition. Visibility is driven by an inline `visibility` (`hidden` until the first play), not by the classes below, so the plugin needs no stylesheet of its own. It carries these classes for your own CSS:
98
98
 
99
99
  | Class | When |
100
100
  |---|---|
101
101
  | `sp-watermark` | Always |
102
- | `sp-watermark--visible` / `sp-watermark--hidden` | Current visibility |
102
+ | `sp-watermark--visible` / `sp-watermark--hidden` | Current visibility, mirroring the inline `visibility` |
103
103
  | `sp-watermark--top-left`, `sp-watermark--top-right`, `sp-watermark--bottom-left`, `sp-watermark--bottom-right`, `sp-watermark--center` | Current position, applied after the first `setPosition()` or dynamic move |
104
104
  | `sp-watermark--dynamic` | Present when `dynamic` is on, applied after the first position change |
105
105
 
106
106
  The element also has a `data-position` attribute holding the current position. No CSS custom properties are used.
107
107
 
108
+ A `MutationObserver` on the player container re-attaches the overlay if it is removed and restores its `opacity`, `visibility`, `pointer-events`, `position` and `z-index` if its inline style is edited. It restores the values the plugin last applied — the ones your `setOpacity()` / `hide()` calls set, not the original config — and it only watches the overlay's own style, so styling anything else inside the container is unaffected.
109
+
108
110
  ## Events
109
111
 
110
112
  The plugin emits no events. It listens to `playback:play`, `playback:pause`, `playback:ended` and `playlist:change`.
package/dist/index.cjs CHANGED
@@ -26,10 +26,18 @@ __export(index_exports, {
26
26
  module.exports = __toCommonJS(index_exports);
27
27
 
28
28
  // src/version.ts
29
- var PKG_VERSION = true ? "1.12.0" : "0.0.0-dev";
29
+ var PKG_VERSION = true ? "1.13.0" : "0.0.0-dev";
30
30
 
31
31
  // src/index.ts
32
32
  var POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
33
+ function clampOpacity(value, fallback = 0.5) {
34
+ if (!Number.isFinite(value)) return fallback;
35
+ return Math.max(0, Math.min(1, value));
36
+ }
37
+ function clampPx(value, floor, fallback) {
38
+ if (!Number.isFinite(value)) return fallback;
39
+ return Math.max(floor, value);
40
+ }
33
41
  function getPositionStyles(padding, bottomPadding) {
34
42
  return {
35
43
  "top-left": `top:${padding}px;left:${padding}px;`,
@@ -45,11 +53,12 @@ function createWatermarkPlugin(config = {}) {
45
53
  let dynamicTimer = null;
46
54
  let showDelayTimer = null;
47
55
  let currentPosition = config.position || "bottom-right";
48
- const opacity = config.opacity ?? 0.5;
56
+ let currentOpacity = clampOpacity(config.opacity ?? 0.5);
57
+ let currentVisible = false;
49
58
  const fontSize = config.fontSize ?? 14;
50
- let currentImageHeight = config.imageHeight ?? 40;
51
- let currentPadding = config.padding ?? 10;
52
- let currentBottomPadding = config.padding ?? 40;
59
+ let currentImageHeight = clampPx(config.imageHeight ?? 40, 1, 40);
60
+ let currentPadding = clampPx(config.padding ?? 10, 0, 10);
61
+ let currentBottomPadding = clampPx(config.padding ?? 40, 0, 40);
53
62
  const dynamic = config.dynamic ?? false;
54
63
  const dynamicInterval = config.dynamicInterval ?? 1e4;
55
64
  const showDelay = config.showDelay ?? 0;
@@ -60,7 +69,7 @@ function createWatermarkPlugin(config = {}) {
60
69
  const createElement = () => {
61
70
  const el = document.createElement("div");
62
71
  el.className = "sp-watermark sp-watermark--hidden";
63
- el.style.cssText = `position:absolute;z-index:10;pointer-events:none;opacity:${opacity};font-size:${fontSize}px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.6);font-family:sans-serif;transition:all 0.5s ease;${positionStyles[currentPosition]}`;
72
+ el.style.cssText = `position:absolute;z-index:10;pointer-events:none;visibility:${currentVisible ? "visible" : "hidden"};opacity:${currentOpacity};font-size:${fontSize}px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.6);font-family:sans-serif;transition:all 0.5s ease;${positionStyles[currentPosition]}`;
64
73
  el.setAttribute("data-position", currentPosition);
65
74
  updateContent(el);
66
75
  return el;
@@ -106,8 +115,7 @@ function createWatermarkPlugin(config = {}) {
106
115
  }
107
116
  });
108
117
  element.setAttribute("data-position", position);
109
- const isVisible = element.classList.contains("sp-watermark--visible");
110
- const visClass = isVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
118
+ const visClass = currentVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
111
119
  element.className = `sp-watermark sp-watermark--${position}${visClass}${dynamic ? " sp-watermark--dynamic" : ""}`;
112
120
  };
113
121
  const randomizePosition = () => {
@@ -116,12 +124,16 @@ function createWatermarkPlugin(config = {}) {
116
124
  setPosition(next);
117
125
  };
118
126
  const show = () => {
127
+ currentVisible = true;
119
128
  if (!element) return;
129
+ element.style.visibility = "visible";
120
130
  element.classList.remove("sp-watermark--hidden");
121
131
  element.classList.add("sp-watermark--visible");
122
132
  };
123
133
  const hide = () => {
134
+ currentVisible = false;
124
135
  if (!element) return;
136
+ element.style.visibility = "hidden";
125
137
  element.classList.remove("sp-watermark--visible");
126
138
  element.classList.add("sp-watermark--hidden");
127
139
  };
@@ -175,11 +187,18 @@ function createWatermarkPlugin(config = {}) {
175
187
  }
176
188
  }
177
189
  } else if (mutation.type === "attributes" && mutation.attributeName === "style") {
178
- if (element) {
179
- element.style.opacity = String(opacity);
180
- element.style.pointerEvents = "none";
181
- element.style.position = "absolute";
182
- element.style.zIndex = "10";
190
+ if (mutation.target !== element) continue;
191
+ const restore = [
192
+ ["opacity", String(currentOpacity)],
193
+ ["visibility", currentVisible ? "visible" : "hidden"],
194
+ ["pointer-events", "none"],
195
+ ["position", "absolute"],
196
+ ["z-index", "10"]
197
+ ];
198
+ for (const [prop, value] of restore) {
199
+ if (element.style.getPropertyValue(prop) !== value) {
200
+ element.style.setProperty(prop, value);
201
+ }
183
202
  }
184
203
  }
185
204
  }
@@ -208,6 +227,10 @@ function createWatermarkPlugin(config = {}) {
208
227
  }
209
228
  });
210
229
  const unsubEnded = api.on("playback:ended", () => {
230
+ if (showDelayTimer) {
231
+ clearTimeout(showDelayTimer);
232
+ showDelayTimer = null;
233
+ }
211
234
  hide();
212
235
  stopDynamic();
213
236
  });
@@ -243,25 +266,30 @@ function createWatermarkPlugin(config = {}) {
243
266
  },
244
267
  setPosition,
245
268
  setOpacity(value) {
246
- if (element) element.style.opacity = String(Math.max(0, Math.min(1, value)));
269
+ currentOpacity = clampOpacity(value, currentOpacity);
270
+ if (element) element.style.opacity = String(currentOpacity);
247
271
  },
248
272
  setImageHeight(height) {
249
- currentImageHeight = Math.max(1, height);
273
+ currentImageHeight = clampPx(height, 1, currentImageHeight);
250
274
  if (element) {
251
275
  const img = element.querySelector("img");
252
276
  if (img) img.style.maxHeight = `${currentImageHeight}px`;
253
277
  }
254
278
  },
255
279
  setPadding(value) {
256
- currentPadding = Math.max(0, value);
257
- currentBottomPadding = Math.max(value, config.padding ?? 40);
280
+ currentPadding = clampPx(value, 0, currentPadding);
281
+ currentBottomPadding = clampPx(
282
+ Math.max(value, config.padding ?? 40),
283
+ 0,
284
+ currentBottomPadding
285
+ );
258
286
  positionStyles = getPositionStyles(currentPadding, currentBottomPadding);
259
287
  setPosition(currentPosition);
260
288
  },
261
289
  show,
262
290
  hide,
263
291
  getConfig() {
264
- return { ...config, position: currentPosition, opacity: element ? parseFloat(element.style.opacity) || opacity : opacity, imageHeight: currentImageHeight, padding: currentPadding };
292
+ return { ...config, position: currentPosition, opacity: currentOpacity, imageHeight: currentImageHeight, padding: currentPadding };
265
293
  }
266
294
  };
267
295
  }
package/dist/index.js CHANGED
@@ -1,8 +1,16 @@
1
1
  // src/version.ts
2
- var PKG_VERSION = true ? "1.12.0" : "0.0.0-dev";
2
+ var PKG_VERSION = true ? "1.13.0" : "0.0.0-dev";
3
3
 
4
4
  // src/index.ts
5
5
  var POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
6
+ function clampOpacity(value, fallback = 0.5) {
7
+ if (!Number.isFinite(value)) return fallback;
8
+ return Math.max(0, Math.min(1, value));
9
+ }
10
+ function clampPx(value, floor, fallback) {
11
+ if (!Number.isFinite(value)) return fallback;
12
+ return Math.max(floor, value);
13
+ }
6
14
  function getPositionStyles(padding, bottomPadding) {
7
15
  return {
8
16
  "top-left": `top:${padding}px;left:${padding}px;`,
@@ -18,11 +26,12 @@ function createWatermarkPlugin(config = {}) {
18
26
  let dynamicTimer = null;
19
27
  let showDelayTimer = null;
20
28
  let currentPosition = config.position || "bottom-right";
21
- const opacity = config.opacity ?? 0.5;
29
+ let currentOpacity = clampOpacity(config.opacity ?? 0.5);
30
+ let currentVisible = false;
22
31
  const fontSize = config.fontSize ?? 14;
23
- let currentImageHeight = config.imageHeight ?? 40;
24
- let currentPadding = config.padding ?? 10;
25
- let currentBottomPadding = config.padding ?? 40;
32
+ let currentImageHeight = clampPx(config.imageHeight ?? 40, 1, 40);
33
+ let currentPadding = clampPx(config.padding ?? 10, 0, 10);
34
+ let currentBottomPadding = clampPx(config.padding ?? 40, 0, 40);
26
35
  const dynamic = config.dynamic ?? false;
27
36
  const dynamicInterval = config.dynamicInterval ?? 1e4;
28
37
  const showDelay = config.showDelay ?? 0;
@@ -33,7 +42,7 @@ function createWatermarkPlugin(config = {}) {
33
42
  const createElement = () => {
34
43
  const el = document.createElement("div");
35
44
  el.className = "sp-watermark sp-watermark--hidden";
36
- el.style.cssText = `position:absolute;z-index:10;pointer-events:none;opacity:${opacity};font-size:${fontSize}px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.6);font-family:sans-serif;transition:all 0.5s ease;${positionStyles[currentPosition]}`;
45
+ el.style.cssText = `position:absolute;z-index:10;pointer-events:none;visibility:${currentVisible ? "visible" : "hidden"};opacity:${currentOpacity};font-size:${fontSize}px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.6);font-family:sans-serif;transition:all 0.5s ease;${positionStyles[currentPosition]}`;
37
46
  el.setAttribute("data-position", currentPosition);
38
47
  updateContent(el);
39
48
  return el;
@@ -79,8 +88,7 @@ function createWatermarkPlugin(config = {}) {
79
88
  }
80
89
  });
81
90
  element.setAttribute("data-position", position);
82
- const isVisible = element.classList.contains("sp-watermark--visible");
83
- const visClass = isVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
91
+ const visClass = currentVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
84
92
  element.className = `sp-watermark sp-watermark--${position}${visClass}${dynamic ? " sp-watermark--dynamic" : ""}`;
85
93
  };
86
94
  const randomizePosition = () => {
@@ -89,12 +97,16 @@ function createWatermarkPlugin(config = {}) {
89
97
  setPosition(next);
90
98
  };
91
99
  const show = () => {
100
+ currentVisible = true;
92
101
  if (!element) return;
102
+ element.style.visibility = "visible";
93
103
  element.classList.remove("sp-watermark--hidden");
94
104
  element.classList.add("sp-watermark--visible");
95
105
  };
96
106
  const hide = () => {
107
+ currentVisible = false;
97
108
  if (!element) return;
109
+ element.style.visibility = "hidden";
98
110
  element.classList.remove("sp-watermark--visible");
99
111
  element.classList.add("sp-watermark--hidden");
100
112
  };
@@ -148,11 +160,18 @@ function createWatermarkPlugin(config = {}) {
148
160
  }
149
161
  }
150
162
  } else if (mutation.type === "attributes" && mutation.attributeName === "style") {
151
- if (element) {
152
- element.style.opacity = String(opacity);
153
- element.style.pointerEvents = "none";
154
- element.style.position = "absolute";
155
- element.style.zIndex = "10";
163
+ if (mutation.target !== element) continue;
164
+ const restore = [
165
+ ["opacity", String(currentOpacity)],
166
+ ["visibility", currentVisible ? "visible" : "hidden"],
167
+ ["pointer-events", "none"],
168
+ ["position", "absolute"],
169
+ ["z-index", "10"]
170
+ ];
171
+ for (const [prop, value] of restore) {
172
+ if (element.style.getPropertyValue(prop) !== value) {
173
+ element.style.setProperty(prop, value);
174
+ }
156
175
  }
157
176
  }
158
177
  }
@@ -181,6 +200,10 @@ function createWatermarkPlugin(config = {}) {
181
200
  }
182
201
  });
183
202
  const unsubEnded = api.on("playback:ended", () => {
203
+ if (showDelayTimer) {
204
+ clearTimeout(showDelayTimer);
205
+ showDelayTimer = null;
206
+ }
184
207
  hide();
185
208
  stopDynamic();
186
209
  });
@@ -216,25 +239,30 @@ function createWatermarkPlugin(config = {}) {
216
239
  },
217
240
  setPosition,
218
241
  setOpacity(value) {
219
- if (element) element.style.opacity = String(Math.max(0, Math.min(1, value)));
242
+ currentOpacity = clampOpacity(value, currentOpacity);
243
+ if (element) element.style.opacity = String(currentOpacity);
220
244
  },
221
245
  setImageHeight(height) {
222
- currentImageHeight = Math.max(1, height);
246
+ currentImageHeight = clampPx(height, 1, currentImageHeight);
223
247
  if (element) {
224
248
  const img = element.querySelector("img");
225
249
  if (img) img.style.maxHeight = `${currentImageHeight}px`;
226
250
  }
227
251
  },
228
252
  setPadding(value) {
229
- currentPadding = Math.max(0, value);
230
- currentBottomPadding = Math.max(value, config.padding ?? 40);
253
+ currentPadding = clampPx(value, 0, currentPadding);
254
+ currentBottomPadding = clampPx(
255
+ Math.max(value, config.padding ?? 40),
256
+ 0,
257
+ currentBottomPadding
258
+ );
231
259
  positionStyles = getPositionStyles(currentPadding, currentBottomPadding);
232
260
  setPosition(currentPosition);
233
261
  },
234
262
  show,
235
263
  hide,
236
264
  getConfig() {
237
- return { ...config, position: currentPosition, opacity: element ? parseFloat(element.style.opacity) || opacity : opacity, imageHeight: currentImageHeight, padding: currentPadding };
265
+ return { ...config, position: currentPosition, opacity: currentOpacity, imageHeight: currentImageHeight, padding: currentPadding };
238
266
  }
239
267
  };
240
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/watermark",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Watermark Plugin for Scarlett Player - anti-piracy text/image overlay",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -30,7 +30,7 @@
30
30
  "tsup": "^8.5.1",
31
31
  "typescript": "^5.3.0",
32
32
  "vitest": "^1.6.0",
33
- "@scarlett-player/core": "1.12.0"
33
+ "@scarlett-player/core": "1.14.0"
34
34
  },
35
35
  "keywords": [
36
36
  "video",