@glivion/square-screen-js-sdk 0.1.0 → 1.0.1

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.
Files changed (49) hide show
  1. package/dist/index.cjs +874 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +522 -0
  4. package/dist/index.d.mts +522 -0
  5. package/dist/index.mjs +870 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +8 -1
  8. package/.github/workflows/build-js-sdk.yml +0 -70
  9. package/eslint.config.js +0 -3
  10. package/examples/react-app/README.md +0 -73
  11. package/examples/react-app/eslint.config.js +0 -22
  12. package/examples/react-app/index.html +0 -13
  13. package/examples/react-app/package-lock.json +0 -2239
  14. package/examples/react-app/package.json +0 -31
  15. package/examples/react-app/public/favicon.svg +0 -1
  16. package/examples/react-app/public/icons.svg +0 -24
  17. package/examples/react-app/src/App.css +0 -184
  18. package/examples/react-app/src/App.tsx +0 -157
  19. package/examples/react-app/src/EmergencyTicker.tsx +0 -25
  20. package/examples/react-app/src/HeadlessExample.tsx +0 -66
  21. package/examples/react-app/src/RendererExample.tsx +0 -70
  22. package/examples/react-app/src/assets/hero.png +0 -0
  23. package/examples/react-app/src/assets/react.svg +0 -1
  24. package/examples/react-app/src/assets/vite.svg +0 -1
  25. package/examples/react-app/src/index.css +0 -183
  26. package/examples/react-app/src/main.tsx +0 -10
  27. package/examples/react-app/src/mockNetworkDataSource.ts +0 -116
  28. package/examples/react-app/src/usePlayer.ts +0 -71
  29. package/examples/react-app/tsconfig.app.json +0 -25
  30. package/examples/react-app/tsconfig.json +0 -7
  31. package/examples/react-app/tsconfig.node.json +0 -24
  32. package/examples/react-app/vite.config.ts +0 -7
  33. package/examples/react-app/yarn.lock +0 -1089
  34. package/src/__tests__/cache/SquareScreenCache.test.ts +0 -375
  35. package/src/__tests__/network/NetworkClient.test.ts +0 -217
  36. package/src/__tests__/network/mappers.test.ts +0 -163
  37. package/src/__tests__/player/SquareScreenPlayer.test.ts +0 -840
  38. package/src/cache/SquareScreenCache.ts +0 -154
  39. package/src/constants.ts +0 -9
  40. package/src/core/types.ts +0 -251
  41. package/src/env.d.ts +0 -4
  42. package/src/index.ts +0 -34
  43. package/src/network/NetworkClient.ts +0 -234
  44. package/src/network/apiTypes.ts +0 -89
  45. package/src/network/mappers.ts +0 -106
  46. package/src/player/SquareScreenPlayer.ts +0 -414
  47. package/src/renderer/SquareScreenRenderer.ts +0 -282
  48. package/tsconfig.json +0 -12
  49. package/tsdown.config.ts +0 -23
@@ -1,282 +0,0 @@
1
- import { EmergencyAlert, PlaylistItem, TransitionType } from "../core/types";
2
- import { SquareScreenPlayer, PlayerEventMap } from "../player/SquareScreenPlayer";
3
-
4
- export interface SquareScreenRendererConfig {
5
- /** Transition to use when an item doesn't specify one. Defaults to `"none"`. */
6
- defaultTransition?: TransitionType;
7
- /** Duration of transition animations in ms. Defaults to `500`. */
8
- transitionDuration?: number;
9
- /**
10
- * Maximum ms to wait for a video to reach `canplay` before transitioning anyway.
11
- * Keeps transitions on-time when media loads from a slow network.
12
- * Defaults to `3000`. Set to `0` to transition immediately without waiting.
13
- */
14
- canPlayTimeout?: number;
15
- }
16
-
17
- /**
18
- * Optional vanilla JS renderer that wires a {@link SquareScreenPlayer} to a DOM container.
19
- * Handles `<img>` / `<video>` element lifecycle, autoplay, transitions, and emergency alerts.
20
- *
21
- * When an emergency alert is active it renders a full-screen overlay on top of all content.
22
- * Normal playback resumes automatically once the alert is cleared.
23
- *
24
- * @example
25
- * const player = new SquareScreenPlayer({ ... });
26
- * const renderer = new SquareScreenRenderer(document.getElementById("screen"), player);
27
- * renderer.mount();
28
- * await player.start();
29
- *
30
- * // Tear down
31
- * player.stop();
32
- * renderer.unmount();
33
- */
34
- export class SquareScreenRenderer {
35
- private readonly container: HTMLElement;
36
- private readonly player: SquareScreenPlayer;
37
- private readonly config: Required<SquareScreenRendererConfig>;
38
-
39
- private wrapper: HTMLDivElement | null = null;
40
- /** Two slots that alternate as current/next during transitions. */
41
- private slots: [HTMLDivElement, HTMLDivElement] | null = null;
42
- private activeSlot = 0;
43
- private alertOverlay: HTMLDivElement | null = null;
44
-
45
- private readonly onItemChange: (event: PlayerEventMap["itemchange"]) => void;
46
- private readonly onEmergencyAlert: (event: PlayerEventMap["emergencyalert"]) => void;
47
-
48
- constructor(
49
- container: HTMLElement,
50
- player: SquareScreenPlayer,
51
- config: SquareScreenRendererConfig = {},
52
- ) {
53
- this.container = container;
54
- this.player = player;
55
- this.config = {
56
- defaultTransition: "none",
57
- transitionDuration: 500,
58
- canPlayTimeout: 3000,
59
- ...config,
60
- };
61
- this.onItemChange = (e) => this.handleItemChange(e.detail.item);
62
- this.onEmergencyAlert = (e) => this.handleEmergencyAlert(e.detail.alert);
63
- }
64
-
65
- /** Injects the renderer DOM into the container and begins listening to the player. */
66
- mount(): void {
67
- this.buildDOM();
68
- this.player.addEventListener("itemchange", this.onItemChange);
69
- this.player.addEventListener("emergencyalert", this.onEmergencyAlert);
70
- }
71
-
72
- /** Removes the renderer DOM and stops listening to the player. */
73
- unmount(): void {
74
- this.player.removeEventListener("itemchange", this.onItemChange);
75
- this.player.removeEventListener("emergencyalert", this.onEmergencyAlert);
76
- if (this.wrapper && this.container.contains(this.wrapper)) {
77
- this.container.removeChild(this.wrapper);
78
- }
79
- this.wrapper = null;
80
- this.slots = null;
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // DOM setup
85
- // ---------------------------------------------------------------------------
86
-
87
- private buildDOM(): void {
88
- this.wrapper = document.createElement("div");
89
- Object.assign(this.wrapper.style, {
90
- position: "relative",
91
- width: "100%",
92
- height: "100%",
93
- overflow: "hidden",
94
- backgroundColor: "#000",
95
- });
96
-
97
- const slotA = this.createSlot(true);
98
- const slotB = this.createSlot(false);
99
- this.wrapper.appendChild(slotA);
100
- this.wrapper.appendChild(slotB);
101
- this.slots = [slotA, slotB];
102
-
103
- this.alertOverlay = document.createElement("div");
104
- Object.assign(this.alertOverlay.style, {
105
- display: "none",
106
- position: "absolute",
107
- top: "0",
108
- right: "0",
109
- bottom: "0",
110
- left: "0",
111
- zIndex: "9999",
112
- flexDirection: "column",
113
- alignItems: "center",
114
- justifyContent: "center",
115
- padding: "2rem",
116
- textAlign: "center",
117
- boxSizing: "border-box",
118
- });
119
- this.wrapper.appendChild(this.alertOverlay);
120
-
121
- this.container.appendChild(this.wrapper);
122
- }
123
-
124
- private createSlot(visible: boolean): HTMLDivElement {
125
- const slot = document.createElement("div");
126
- Object.assign(slot.style, {
127
- position: "absolute",
128
- top: "0",
129
- right: "0",
130
- bottom: "0",
131
- left: "0",
132
- display: "flex",
133
- alignItems: "center",
134
- justifyContent: "center",
135
- opacity: visible ? "1" : "0",
136
- transition: `opacity ${this.config.transitionDuration}ms ease`,
137
- });
138
- return slot;
139
- }
140
-
141
- // ---------------------------------------------------------------------------
142
- // Item rendering
143
- // ---------------------------------------------------------------------------
144
-
145
- private async handleItemChange(item: PlaylistItem): Promise<void> {
146
- if (!this.slots) return;
147
-
148
- const nextIndex = (this.activeSlot + 1) % 2;
149
- const currentSlot = this.slots[this.activeSlot];
150
- const nextSlot = this.slots[nextIndex];
151
-
152
- nextSlot.innerHTML = "";
153
- const media =
154
- item.type === "video"
155
- ? this.createVideo(item.url)
156
- : this.createImage(item.url);
157
- nextSlot.appendChild(media);
158
-
159
- if (item.type === "video") {
160
- await this.waitForCanPlay(media as HTMLVideoElement);
161
- }
162
-
163
- const transitionType = item.transition ?? this.config.defaultTransition;
164
- await this.applyTransition(currentSlot, nextSlot, transitionType);
165
-
166
- this.activeSlot = nextIndex;
167
- }
168
-
169
- private handleEmergencyAlert(alert: EmergencyAlert | null): void {
170
- if (!this.alertOverlay) return;
171
-
172
- if (!alert) {
173
- this.alertOverlay.style.display = "none";
174
- this.alertOverlay.innerHTML = "";
175
- return;
176
- }
177
-
178
- this.alertOverlay.style.display = "flex";
179
- this.alertOverlay.style.backgroundColor = alert.backgroundColor;
180
- this.alertOverlay.style.color = alert.textColor;
181
-
182
- const title = document.createElement("h1");
183
- title.textContent = alert.title;
184
- Object.assign(title.style, { margin: "0 0 1rem", fontSize: "2.5rem", fontWeight: "bold" });
185
-
186
- const message = document.createElement("p");
187
- message.textContent = alert.message;
188
- Object.assign(message.style, { margin: "0", fontSize: "1.5rem" });
189
-
190
- this.alertOverlay.innerHTML = "";
191
- this.alertOverlay.appendChild(title);
192
- this.alertOverlay.appendChild(message);
193
- }
194
-
195
- private createImage(src: string): HTMLImageElement {
196
- const img = document.createElement("img");
197
- Object.assign(img.style, {
198
- maxWidth: "100%",
199
- maxHeight: "100%",
200
- objectFit: "contain",
201
- });
202
- img.src = src;
203
- return img;
204
- }
205
-
206
- private createVideo(src: string): HTMLVideoElement {
207
- const video = document.createElement("video");
208
- Object.assign(video.style, {
209
- width: "100%",
210
- height: "100%",
211
- objectFit: "contain",
212
- });
213
- video.src = src;
214
- video.autoplay = true;
215
- video.muted = true; // required for autoplay in most browsers
216
- video.playsInline = true; // required on iOS
217
- video.loop = true; // loop within the item's duration window
218
- return video;
219
- }
220
-
221
- private waitForCanPlay(video: HTMLVideoElement): Promise<void> {
222
- return new Promise((resolve) => {
223
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
224
- resolve();
225
- return;
226
- }
227
- const timeout = this.config.canPlayTimeout;
228
- const timer = timeout > 0 ? setTimeout(resolve, timeout) : null;
229
- const handler = () => {
230
- if (timer) clearTimeout(timer);
231
- video.removeEventListener("canplay", handler);
232
- resolve();
233
- };
234
- video.addEventListener("canplay", handler);
235
- });
236
- }
237
-
238
- // ---------------------------------------------------------------------------
239
- // Transitions
240
- // ---------------------------------------------------------------------------
241
-
242
- private async applyTransition(
243
- from: HTMLDivElement,
244
- to: HTMLDivElement,
245
- type: TransitionType,
246
- ): Promise<void> {
247
- const duration = this.config.transitionDuration;
248
-
249
- if (type === "none" || duration === 0) {
250
- from.style.opacity = "0";
251
- to.style.opacity = "1";
252
- } else if (type === "fade") {
253
- to.style.opacity = "1";
254
- from.style.opacity = "0";
255
- await this.wait(duration);
256
- } else if (type === "slide") {
257
- // Snap next slot into position off-screen (no animation), then slide both
258
- to.style.transition = "none";
259
- to.style.transform = "translateX(100%)";
260
- to.style.opacity = "1";
261
- void to.offsetWidth; // force reflow so the snap takes effect before animating
262
-
263
- to.style.transition = `transform ${duration}ms ease`;
264
- from.style.transition = `transform ${duration}ms ease`;
265
- to.style.transform = "translateX(0)";
266
- from.style.transform = "translateX(-100%)";
267
- await this.wait(duration);
268
- }
269
-
270
- // Reset both slots to a clean opacity-only state for the next transition
271
- from.style.transition = `opacity ${duration}ms ease`;
272
- from.style.transform = "";
273
- from.style.opacity = "0";
274
- to.style.transition = `opacity ${duration}ms ease`;
275
- to.style.transform = "";
276
- to.style.opacity = "1";
277
- }
278
-
279
- private wait(ms: number): Promise<void> {
280
- return new Promise((resolve) => setTimeout(resolve, ms));
281
- }
282
- }
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "Preserve",
5
- "moduleResolution": "Bundler",
6
- "lib": ["ES2020", "DOM"],
7
- "strict": true,
8
- "skipLibCheck": true
9
- },
10
- "include": ["src"],
11
- "exclude": ["node_modules", "dist", "examples"]
12
- }
package/tsdown.config.ts DELETED
@@ -1,23 +0,0 @@
1
- import { defineConfig } from "tsdown";
2
- import { readFileSync } from "fs";
3
- import { resolve, dirname } from "path";
4
-
5
- const RAW_PREFIX = "\0raw:";
6
-
7
- export default defineConfig({
8
- plugins: [
9
- {
10
- name: "raw-loader",
11
- resolveId(id: string, importer?: string) {
12
- if (!id.endsWith("?raw") || !importer) return null;
13
- const filePath = resolve(dirname(importer), id.slice(0, -4));
14
- return RAW_PREFIX + filePath;
15
- },
16
- load(id: string) {
17
- if (!id.startsWith(RAW_PREFIX)) return null;
18
- const content = readFileSync(id.slice(RAW_PREFIX.length), "utf-8");
19
- return `export default ${JSON.stringify(content)}`;
20
- },
21
- },
22
- ],
23
- });