@glivion/square-screen-js-sdk 0.1.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/.github/workflows/build-js-sdk.yml +70 -0
- package/README.md +463 -0
- package/eslint.config.js +3 -0
- package/examples/react-app/README.md +73 -0
- package/examples/react-app/eslint.config.js +22 -0
- package/examples/react-app/index.html +13 -0
- package/examples/react-app/package-lock.json +2239 -0
- package/examples/react-app/package.json +31 -0
- package/examples/react-app/public/favicon.svg +1 -0
- package/examples/react-app/public/icons.svg +24 -0
- package/examples/react-app/src/App.css +184 -0
- package/examples/react-app/src/App.tsx +157 -0
- package/examples/react-app/src/EmergencyTicker.tsx +25 -0
- package/examples/react-app/src/HeadlessExample.tsx +66 -0
- package/examples/react-app/src/RendererExample.tsx +70 -0
- package/examples/react-app/src/assets/hero.png +0 -0
- package/examples/react-app/src/assets/react.svg +1 -0
- package/examples/react-app/src/assets/vite.svg +1 -0
- package/examples/react-app/src/index.css +183 -0
- package/examples/react-app/src/main.tsx +10 -0
- package/examples/react-app/src/mockNetworkDataSource.ts +116 -0
- package/examples/react-app/src/usePlayer.ts +71 -0
- package/examples/react-app/tsconfig.app.json +25 -0
- package/examples/react-app/tsconfig.json +7 -0
- package/examples/react-app/tsconfig.node.json +24 -0
- package/examples/react-app/vite.config.ts +7 -0
- package/examples/react-app/yarn.lock +1089 -0
- package/package.json +49 -0
- package/src/__tests__/cache/SquareScreenCache.test.ts +375 -0
- package/src/__tests__/network/NetworkClient.test.ts +217 -0
- package/src/__tests__/network/mappers.test.ts +163 -0
- package/src/__tests__/player/SquareScreenPlayer.test.ts +840 -0
- package/src/cache/SquareScreenCache.ts +154 -0
- package/src/constants.ts +9 -0
- package/src/core/types.ts +251 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +34 -0
- package/src/network/NetworkClient.ts +234 -0
- package/src/network/apiTypes.ts +89 -0
- package/src/network/mappers.ts +106 -0
- package/src/player/SquareScreenPlayer.ts +414 -0
- package/src/renderer/SquareScreenRenderer.ts +282 -0
- package/tsconfig.json +12 -0
- package/tsdown.config.ts +23 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Build JS SDK
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
cache: yarn
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: yarn install --frozen-lockfile
|
|
23
|
+
|
|
24
|
+
- name: ESLint
|
|
25
|
+
run: yarn lint
|
|
26
|
+
|
|
27
|
+
- name: Type check
|
|
28
|
+
run: yarn typecheck
|
|
29
|
+
|
|
30
|
+
test:
|
|
31
|
+
name: Test
|
|
32
|
+
needs: lint
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
|
|
37
|
+
- uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: 20
|
|
40
|
+
cache: yarn
|
|
41
|
+
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: yarn install --frozen-lockfile
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
run: yarn test
|
|
47
|
+
|
|
48
|
+
build:
|
|
49
|
+
name: Build
|
|
50
|
+
needs: test
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: 20
|
|
58
|
+
cache: yarn
|
|
59
|
+
|
|
60
|
+
- name: Install dependencies
|
|
61
|
+
run: yarn install --frozen-lockfile
|
|
62
|
+
|
|
63
|
+
- name: Build
|
|
64
|
+
run: yarn build
|
|
65
|
+
|
|
66
|
+
- name: Upload dist artifact
|
|
67
|
+
uses: actions/upload-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
name: dist
|
|
70
|
+
path: dist/
|
package/README.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# SquareScreen JS SDK
|
|
2
|
+
|
|
3
|
+
A framework-agnostic JavaScript/TypeScript SDK for building SquareScreen digital signage players. It handles playlist fetching, local media caching, item advancement, background preloading, polling, heartbeats, and emergency alerts — all without touching the DOM.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick start](#quick-start)
|
|
11
|
+
- [Architecture](#architecture)
|
|
12
|
+
- [Player](#player)
|
|
13
|
+
- [Configuration](#configuration)
|
|
14
|
+
- [Events](#events)
|
|
15
|
+
- [Methods](#methods)
|
|
16
|
+
- [Renderer (optional)](#renderer-optional)
|
|
17
|
+
- [Renderer configuration](#renderer-configuration)
|
|
18
|
+
- [Cache layer](#cache-layer)
|
|
19
|
+
- [Custom cache provider](#custom-cache-provider)
|
|
20
|
+
- [Framework integrations](#framework-integrations)
|
|
21
|
+
- [React](#react)
|
|
22
|
+
- [Vanilla JS](#vanilla-js)
|
|
23
|
+
- [Mocking for development](#mocking-for-development)
|
|
24
|
+
- [Example app](#example-app)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install square-screen-js-sdk
|
|
32
|
+
# or
|
|
33
|
+
yarn add square-screen-js-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { SquareScreenPlayer, SquareScreenRenderer } from "square-screen-js-sdk";
|
|
42
|
+
|
|
43
|
+
const player = new SquareScreenPlayer({
|
|
44
|
+
deviceId: "your-device-uuid",
|
|
45
|
+
deviceToken: "your-device-token",
|
|
46
|
+
version: "1.0.0",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Optional: attach the built-in renderer to a container element
|
|
50
|
+
const renderer = new SquareScreenRenderer(
|
|
51
|
+
document.getElementById("screen"),
|
|
52
|
+
player,
|
|
53
|
+
{ defaultTransition: "fade", transitionDuration: 500 },
|
|
54
|
+
);
|
|
55
|
+
renderer.mount();
|
|
56
|
+
|
|
57
|
+
// Listen to player events
|
|
58
|
+
player.addEventListener("itemchange", ({ detail }) => {
|
|
59
|
+
console.log("Now playing:", detail.item.name, `(${detail.index + 1}/${detail.total})`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
player.addEventListener("emergencyalert", ({ detail }) => {
|
|
63
|
+
if (detail.alert) console.warn("Emergency:", detail.alert.title);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
player.addEventListener("statuschange", ({ detail }) => {
|
|
67
|
+
console.log("Status:", detail.status); // "connecting" | "online" | "offline"
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await player.start();
|
|
71
|
+
|
|
72
|
+
// Tear down
|
|
73
|
+
player.stop();
|
|
74
|
+
renderer.unmount();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
The SDK is split into three independent layers:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
┌─────────────────────────────────────────┐
|
|
85
|
+
│ Your application │
|
|
86
|
+
└────────────────┬────────────────────────┘
|
|
87
|
+
│ events / state
|
|
88
|
+
┌────────────────▼────────────────────────┐
|
|
89
|
+
│ SquareScreenPlayer │ headless — no DOM
|
|
90
|
+
│ · fetches & caches playlists │
|
|
91
|
+
│ · advances items by duration │
|
|
92
|
+
│ · background media preloading │
|
|
93
|
+
│ · polls for updates & emergency alerts │
|
|
94
|
+
│ · heartbeat │
|
|
95
|
+
└──────┬──────────────────┬───────────────┘
|
|
96
|
+
│ │
|
|
97
|
+
┌──────▼──────┐ ┌────────▼──────────────┐
|
|
98
|
+
│ NetworkClient│ │ SquareScreenCache │
|
|
99
|
+
│ (fetch/API)│ │ IndexedDB + Cache API│
|
|
100
|
+
└─────────────┘ └───────────────────────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The optional `SquareScreenRenderer` sits above your application and listens to `itemchange` events to manage `<img>` / `<video>` elements and CSS transitions. You can ignore it entirely and render with any framework.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Player
|
|
108
|
+
|
|
109
|
+
### Configuration
|
|
110
|
+
|
|
111
|
+
All REST traffic uses the production API root bundled with the package (`SQUARESCREEN_API_BASE_URL`, currently `https://api.squarescreen.io/api/v1`). You cannot point the default client at another host; pass a custom `networkDataSource` only for tests, staging gateways, or non-standard deployments.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface SquareScreenPlayerConfig {
|
|
115
|
+
/** Device UUID */
|
|
116
|
+
deviceId: string;
|
|
117
|
+
/** Device secret token — never expose this in client-side logs */
|
|
118
|
+
deviceToken: string;
|
|
119
|
+
/** SDK version string sent on every heartbeat */
|
|
120
|
+
version: string;
|
|
121
|
+
/** How long a cached playlist stays fresh in ms. Default: 5 minutes */
|
|
122
|
+
ttl?: number;
|
|
123
|
+
/** How often to poll for playlist updates in ms. Default: 30 seconds */
|
|
124
|
+
pollInterval?: number;
|
|
125
|
+
/** How often to poll for emergency alerts in ms. Default: 15 seconds */
|
|
126
|
+
emergencyPollInterval?: number;
|
|
127
|
+
/** How often to send a heartbeat in ms. Default: 60 seconds */
|
|
128
|
+
heartbeatInterval?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Override the network layer with a custom implementation — useful for
|
|
131
|
+
* testing or mocking without a real API. When provided, deviceId and
|
|
132
|
+
* deviceToken are ignored for network calls.
|
|
133
|
+
*/
|
|
134
|
+
networkDataSource?: NetworkDataSource;
|
|
135
|
+
/**
|
|
136
|
+
* Override the cache layer with a custom implementation. When omitted the
|
|
137
|
+
* default SquareScreenCache (IndexedDB + Cache API) is used.
|
|
138
|
+
*/
|
|
139
|
+
cacheProvider?: SquareScreenCacheProvider;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> **Note:** The constructor throws if `deviceId` or `deviceToken` is missing when no `networkDataSource` override is provided. With a mock or custom network layer, those credentials are not validated.
|
|
144
|
+
|
|
145
|
+
### Events
|
|
146
|
+
|
|
147
|
+
Use `addEventListener` / `removeEventListener` exactly like any DOM `EventTarget`. TypeScript users get full type inference on `event.detail`.
|
|
148
|
+
|
|
149
|
+
| Event | Detail | When it fires |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `itemchange` | `{ item: PlaylistItem, index: number, total: number }` | Current item advances |
|
|
152
|
+
| `statuschange` | `{ status: DeviceStatus }` | Connectivity state changes (`"connecting"` → `"online"` / `"offline"`) |
|
|
153
|
+
| `emergencyalert` | `{ alert: EmergencyAlert \| null }` | Alert becomes active, changes, or is cleared |
|
|
154
|
+
| `playlistupdate` | `{ playlist: Playlist }` | Playlist is refreshed from the network or cache |
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
player.addEventListener("itemchange", ({ detail }) => {
|
|
158
|
+
const { item, index, total } = detail;
|
|
159
|
+
// item.url is a blob: URL if the media is cached locally,
|
|
160
|
+
// or the original HTTPS URL on first play (cached in the background).
|
|
161
|
+
// item.type is "image" | "video"
|
|
162
|
+
// item.duration is how long this item plays in seconds
|
|
163
|
+
// item.transition is "fade" | "slide" | "none"
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Methods
|
|
168
|
+
|
|
169
|
+
| Method | Description |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `start()` | Fetches the playlist, begins playback, starts polling and heartbeat. Returns a `Promise` that resolves once the first load attempt completes. |
|
|
172
|
+
| `stop()` | Clears all timers and internal state. Call this when tearing down the player. |
|
|
173
|
+
| `refreshPlaylist()` | Manually trigger a playlist refresh outside the normal poll cycle. |
|
|
174
|
+
| `currentItem` | Getter — returns the currently playing `PlaylistItem`, or `null`. |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Renderer (optional)
|
|
179
|
+
|
|
180
|
+
`SquareScreenRenderer` is a vanilla JS DOM renderer that wires directly to a player instance. It is entirely optional — you can build your own rendering layer using the player's events.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { SquareScreenRenderer } from "square-screen-js-sdk";
|
|
184
|
+
|
|
185
|
+
const renderer = new SquareScreenRenderer(containerElement, player, options);
|
|
186
|
+
renderer.mount(); // inject DOM and start listening
|
|
187
|
+
renderer.unmount(); // remove DOM and stop listening
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The renderer maintains two absolutely-positioned slot elements inside the container and alternates between them on each item change, enabling smooth cross-slot transitions.
|
|
191
|
+
|
|
192
|
+
**What it handles:**
|
|
193
|
+
- Creates and manages `<img>` and `<video>` elements
|
|
194
|
+
- Waits for video `canplay` before transitioning (with a configurable timeout to prevent blocking on slow networks)
|
|
195
|
+
- Sets `autoplay`, `muted`, `playsInline`, and `loop` on videos (required for browser autoplay policies and iOS)
|
|
196
|
+
- Full-screen emergency alert overlay
|
|
197
|
+
|
|
198
|
+
**What it does not handle:**
|
|
199
|
+
- Emergency alert UI beyond the built-in overlay — listen to `"emergencyalert"` if you need custom presentation
|
|
200
|
+
|
|
201
|
+
### Renderer configuration
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
interface SquareScreenRendererConfig {
|
|
205
|
+
/** Transition to use when an item has no transition set. Default: "none" */
|
|
206
|
+
defaultTransition?: "fade" | "slide" | "none";
|
|
207
|
+
/** Transition animation duration in ms. Default: 500 */
|
|
208
|
+
transitionDuration?: number;
|
|
209
|
+
/**
|
|
210
|
+
* Maximum ms to wait for a video to reach canplay before transitioning anyway.
|
|
211
|
+
* Prevents long stalls when media loads from a slow network on the first cycle.
|
|
212
|
+
* Default: 3000. Set to 0 to transition immediately without waiting.
|
|
213
|
+
*/
|
|
214
|
+
canPlayTimeout?: number;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Cache layer
|
|
221
|
+
|
|
222
|
+
The cache layer is used internally by the player and requires no direct interaction in most cases.
|
|
223
|
+
|
|
224
|
+
**How it works:**
|
|
225
|
+
|
|
226
|
+
- **Playlist metadata** is stored in IndexedDB (via the `idb` library) with a per-record TTL. Each playlist is stored as a single document keyed by UUID; items are stored inline.
|
|
227
|
+
- **Media blobs** are stored in the browser's Cache API, keyed by the original media URL. This is the same storage used by service workers, making future SW integration straightforward.
|
|
228
|
+
- Once a blob is retrieved from the Cache API it is wrapped in a `URL.createObjectURL` handle that is kept in an in-memory map and reused on subsequent calls. All handles are revoked when `clear()` is called.
|
|
229
|
+
|
|
230
|
+
**Media URL lifecycle:**
|
|
231
|
+
|
|
232
|
+
On the **first cycle**, if a media file hasn't been downloaded yet, the player dispatches `itemchange` with the raw HTTPS URL immediately (no blocking). The file is downloaded in the background and stored in the Cache API. On every subsequent cycle the blob URL is used — transitions are instant.
|
|
233
|
+
|
|
234
|
+
**Preloading:**
|
|
235
|
+
|
|
236
|
+
Set `strategy.preloadCount` on the playlist to control how many upcoming items are pre-downloaded while the current item plays.
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
preloadCount: 1 → always 1 item ahead
|
|
240
|
+
preloadCount: 3 → 3 items ahead
|
|
241
|
+
preloadCount: 0 → disabled
|
|
242
|
+
preloadCount: (absent) → download entire playlist up front
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Offline fallback:**
|
|
246
|
+
|
|
247
|
+
If the network is unavailable on start-up and the player has never loaded a playlist in the current session, it falls back to the last-known playlist UUID (stored in `localStorage`) and loads it from IndexedDB with `allowStale: true` — serving it even if its TTL has expired.
|
|
248
|
+
|
|
249
|
+
### Custom cache provider
|
|
250
|
+
|
|
251
|
+
Implement `SquareScreenCacheProvider` and pass it via `cacheProvider` to replace the default storage entirely:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import type { SquareScreenCacheProvider, Playlist } from "square-screen-js-sdk";
|
|
255
|
+
|
|
256
|
+
const myCache: SquareScreenCacheProvider = {
|
|
257
|
+
async getPlaylist(uuid, allowStale) { /* ... */ return null; },
|
|
258
|
+
async savePlaylist(playlist, ttlMs) { /* ... */ },
|
|
259
|
+
async getMediaUrl(url) { /* ... */ return null; },
|
|
260
|
+
async saveMedia(url, blob) { /* ... */ return URL.createObjectURL(blob); },
|
|
261
|
+
async clear() { /* ... */ },
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const player = new SquareScreenPlayer({ ..., cacheProvider: myCache });
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Framework integrations
|
|
270
|
+
|
|
271
|
+
### React
|
|
272
|
+
|
|
273
|
+
Store named listener references so cleanup can call `removeEventListener` precisely — required for React StrictMode, which mounts effects twice.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
import { useEffect, useRef, useState } from "react";
|
|
277
|
+
import {
|
|
278
|
+
SquareScreenPlayer,
|
|
279
|
+
SquareScreenRenderer,
|
|
280
|
+
type SquareScreenPlayerConfig,
|
|
281
|
+
type DeviceStatus,
|
|
282
|
+
type EmergencyAlert,
|
|
283
|
+
type PlaylistItem,
|
|
284
|
+
} from "square-screen-js-sdk";
|
|
285
|
+
|
|
286
|
+
function usePlayer(config: SquareScreenPlayerConfig | null) {
|
|
287
|
+
const playerRef = useRef<SquareScreenPlayer | null>(null);
|
|
288
|
+
const [state, setState] = useState({
|
|
289
|
+
status: "connecting" as DeviceStatus,
|
|
290
|
+
currentItem: null as PlaylistItem | null,
|
|
291
|
+
currentIndex: 0,
|
|
292
|
+
total: 0,
|
|
293
|
+
alert: null as EmergencyAlert | null,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!config) return;
|
|
298
|
+
const player = new SquareScreenPlayer(config);
|
|
299
|
+
playerRef.current = player;
|
|
300
|
+
|
|
301
|
+
const onStatus: Parameters<typeof player.addEventListener<"statuschange">>[1] =
|
|
302
|
+
(e) => setState((s) => ({ ...s, status: e.detail.status }));
|
|
303
|
+
|
|
304
|
+
const onItem: Parameters<typeof player.addEventListener<"itemchange">>[1] =
|
|
305
|
+
(e) => setState((s) => ({
|
|
306
|
+
...s,
|
|
307
|
+
currentItem: e.detail.item,
|
|
308
|
+
currentIndex: e.detail.index,
|
|
309
|
+
total: e.detail.total,
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
const onAlert: Parameters<typeof player.addEventListener<"emergencyalert">>[1] =
|
|
313
|
+
(e) => setState((s) => ({ ...s, alert: e.detail.alert }));
|
|
314
|
+
|
|
315
|
+
player.addEventListener("statuschange", onStatus);
|
|
316
|
+
player.addEventListener("itemchange", onItem);
|
|
317
|
+
player.addEventListener("emergencyalert", onAlert);
|
|
318
|
+
player.start();
|
|
319
|
+
|
|
320
|
+
return () => {
|
|
321
|
+
player.removeEventListener("statuschange", onStatus);
|
|
322
|
+
player.removeEventListener("itemchange", onItem);
|
|
323
|
+
player.removeEventListener("emergencyalert", onAlert);
|
|
324
|
+
player.stop();
|
|
325
|
+
playerRef.current = null;
|
|
326
|
+
};
|
|
327
|
+
}, [config]);
|
|
328
|
+
|
|
329
|
+
return { player: playerRef.current, state };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Renderer approach — SDK handles the DOM
|
|
333
|
+
function RendererScreen({ config }: { config: SquareScreenPlayerConfig }) {
|
|
334
|
+
const { player } = usePlayer(config);
|
|
335
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (!player || !containerRef.current) return;
|
|
339
|
+
const renderer = new SquareScreenRenderer(containerRef.current, player, {
|
|
340
|
+
defaultTransition: "fade",
|
|
341
|
+
transitionDuration: 500,
|
|
342
|
+
});
|
|
343
|
+
renderer.mount();
|
|
344
|
+
return () => renderer.unmount();
|
|
345
|
+
}, [player]);
|
|
346
|
+
|
|
347
|
+
return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Headless approach — you control the DOM
|
|
351
|
+
function HeadlessScreen({ config }: { config: SquareScreenPlayerConfig }) {
|
|
352
|
+
const { state } = usePlayer(config);
|
|
353
|
+
const { currentItem } = state;
|
|
354
|
+
if (!currentItem) return null;
|
|
355
|
+
return currentItem.type === "video"
|
|
356
|
+
? <video key={currentItem.uuid} src={currentItem.url} autoPlay muted playsInline loop />
|
|
357
|
+
: <img key={currentItem.uuid} src={currentItem.url} alt={currentItem.name} />;
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Vanilla JS
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
import { SquareScreenPlayer, SquareScreenRenderer } from "square-screen-js-sdk";
|
|
365
|
+
|
|
366
|
+
const player = new SquareScreenPlayer({
|
|
367
|
+
deviceId: "device-uuid",
|
|
368
|
+
deviceToken: "device-token",
|
|
369
|
+
version: "1.0.0",
|
|
370
|
+
pollInterval: 30_000,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const renderer = new SquareScreenRenderer(
|
|
374
|
+
document.getElementById("screen"),
|
|
375
|
+
player,
|
|
376
|
+
{ defaultTransition: "slide", transitionDuration: 400 },
|
|
377
|
+
);
|
|
378
|
+
renderer.mount();
|
|
379
|
+
|
|
380
|
+
// Handle emergency alerts yourself
|
|
381
|
+
player.addEventListener("emergencyalert", ({ detail }) => {
|
|
382
|
+
const ticker = document.getElementById("emergency-ticker");
|
|
383
|
+
if (detail.alert) {
|
|
384
|
+
ticker.textContent = `${detail.alert.title}: ${detail.alert.message}`;
|
|
385
|
+
ticker.hidden = false;
|
|
386
|
+
} else {
|
|
387
|
+
ticker.hidden = true;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await player.start();
|
|
392
|
+
|
|
393
|
+
// Tear down when done
|
|
394
|
+
player.stop();
|
|
395
|
+
renderer.unmount();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Mocking for development
|
|
401
|
+
|
|
402
|
+
Pass a `networkDataSource` to bypass the real API entirely:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import type { NetworkDataSource } from "square-screen-js-sdk";
|
|
406
|
+
|
|
407
|
+
const mockDataSource: NetworkDataSource = {
|
|
408
|
+
fetchPlaylist: async () => ({
|
|
409
|
+
success: true,
|
|
410
|
+
data: {
|
|
411
|
+
uuid: "playlist-001",
|
|
412
|
+
cachedAt: Date.now(),
|
|
413
|
+
strategy: { loop: true, shuffle: false, preloadCount: 2 },
|
|
414
|
+
items: [
|
|
415
|
+
{
|
|
416
|
+
uuid: "item-001",
|
|
417
|
+
name: "Demo image",
|
|
418
|
+
type: "image",
|
|
419
|
+
url: "https://fastly.picsum.photos/id/10/2500/1667.jpg",
|
|
420
|
+
duration: 5,
|
|
421
|
+
transition: "fade",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
uuid: "item-002",
|
|
425
|
+
name: "Demo video",
|
|
426
|
+
type: "video",
|
|
427
|
+
url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
|
428
|
+
duration: 10,
|
|
429
|
+
transition: "slide",
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
}),
|
|
434
|
+
fetchVideoPlaylist: async () => ({ success: false, error: { kind: "network", code: 0, message: "mock" } }),
|
|
435
|
+
fetchImagePlaylist: async () => ({ success: false, error: { kind: "network", code: 0, message: "mock" } }),
|
|
436
|
+
checkForEmergencyAlert: async () => ({ success: true, data: null }),
|
|
437
|
+
healthCheck: async () => ({ success: true, data: { received: true } }),
|
|
438
|
+
reportPlaybackEvent: async () => ({ success: true, data: { recorded: true } }),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const player = new SquareScreenPlayer({
|
|
442
|
+
deviceId: "",
|
|
443
|
+
deviceToken: "",
|
|
444
|
+
version: "1.0.0",
|
|
445
|
+
networkDataSource: mockDataSource,
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Example app
|
|
452
|
+
|
|
453
|
+
A React example app lives in [`examples/react-app`](./examples/react-app). It demonstrates both the renderer and headless approaches side by side, with a built-in mock data source and an emergency alert toggle.
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
cd examples/react-app
|
|
457
|
+
npm install
|
|
458
|
+
npm run dev
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The **Renderer** tab uses `SquareScreenRenderer` attached to a div — transitions and media lifecycle are fully managed by the SDK.
|
|
462
|
+
|
|
463
|
+
The **Headless** tab renders directly with React JSX, using `key={item.uuid}` on media elements to force remount when the item changes.
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
globals: globals.browser,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
])
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>react-app</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|