@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,154 +0,0 @@
1
- import { openDB, deleteDB } from "idb";
2
- import { Playlist, SquareScreenCacheProvider } from "../core/types";
3
-
4
- const DEFAULT_DB_NAME = "square-screen-cache";
5
- const DEFAULT_CACHE_NAME = "square-screen-cache";
6
-
7
- /**
8
- * Default {@link SquareScreenCacheProvider} backed by IndexedDB (playlist metadata)
9
- * and the Cache API (media blobs).
10
- *
11
- * **IndexedDB** — a single `playlists` object store holds one document per
12
- * playlist UUID. A private `_ttl` field (milliseconds) is stored alongside the
13
- * record and stripped before returning data to callers.
14
- *
15
- * **Cache API** — media files are stored under their original URL as the cache
16
- * key, matching the same URL-keyed approach used by the Android SDK's
17
- * `MediaFileCache`. Once a blob is retrieved from the Cache API it is wrapped
18
- * in a `URL.createObjectURL` handle that is kept in an in-memory map and reused
19
- * on subsequent calls to avoid redundant allocations. All handles are revoked
20
- * when {@link clear} is called.
21
- *
22
- * Integrators who need different storage behaviour (a service-worker cache,
23
- * an in-memory store for tests, a custom database) should implement
24
- * {@link SquareScreenCacheProvider} and pass it via
25
- * `SquareScreenPlayerConfig.cacheProvider`.
26
- */
27
- export class SquareScreenCache implements SquareScreenCacheProvider {
28
- private readonly dbName: string;
29
- private readonly cacheName: string;
30
- /** Tracks the blob URL created for each media URL so it is reused across calls. */
31
- private readonly objectUrls = new Map<string, string>();
32
-
33
- /**
34
- * @param dbName Name of the IndexedDB database. Override in tests to isolate state.
35
- * @param cacheName Name of the Cache API bucket. Override in tests to isolate state.
36
- */
37
- constructor({
38
- dbName = DEFAULT_DB_NAME,
39
- cacheName = DEFAULT_CACHE_NAME,
40
- }: { dbName?: string; cacheName?: string } = {}) {
41
- this.dbName = dbName;
42
- this.cacheName = cacheName;
43
- }
44
-
45
- /** Opens (or lazily creates) the IndexedDB database with the `playlists` object store. */
46
- private openDB() {
47
- return openDB(this.dbName, 1, {
48
- upgrade(db) {
49
- if (!db.objectStoreNames.contains("playlists")) {
50
- db.createObjectStore("playlists", { keyPath: "uuid" });
51
- }
52
- },
53
- });
54
- }
55
-
56
- /**
57
- * Returns the cached playlist for the given UUID, or `null` if:
58
- * - nothing has been stored for that UUID, or
59
- * - the record's age exceeds its TTL and `allowStale` is `false`.
60
- *
61
- * @param uuid The playlist UUID to look up.
62
- * @param allowStale When `true`, expired records are returned as-is (useful
63
- * for serving a fallback when the network is unreachable).
64
- */
65
- async getPlaylist(uuid: string, allowStale = false): Promise<Playlist | null> {
66
- const db = await this.openDB();
67
- const record = await db.get("playlists", uuid);
68
- if (!record) return null;
69
- if (!allowStale && Date.now() - record.cachedAt > record._ttl) return null;
70
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- strip cache metadata before return
71
- const { _ttl, ...playlist } = record;
72
- return playlist as Playlist;
73
- }
74
-
75
- /**
76
- * Persists a playlist to IndexedDB, overwriting any previous record with the
77
- * same UUID. `cachedAt` is set to the current wall-clock time so that TTL
78
- * checks in {@link getPlaylist} have an accurate baseline.
79
- *
80
- * @param playlist The playlist to store. Items are persisted inline as part
81
- * of the same document — no separate per-item records.
82
- * @param ttlMs How long (in milliseconds) the record is considered fresh.
83
- * Passed through `SquareScreenPlayerConfig.ttl` by the player.
84
- */
85
- async savePlaylist(playlist: Playlist, ttlMs: number): Promise<void> {
86
- const db = await this.openDB();
87
- await db.put("playlists", { ...playlist, cachedAt: Date.now(), _ttl: ttlMs });
88
- }
89
-
90
- /**
91
- * Returns a blob URL for the locally cached copy of `url`, or `null` if the
92
- * media has not been downloaded yet.
93
- *
94
- * The blob URL is created once and stored in an in-memory map. Subsequent
95
- * calls for the same `url` return the same handle without re-reading the
96
- * Cache API, which avoids both I/O and unnecessary `URL.createObjectURL`
97
- * allocations on long-running signage devices.
98
- *
99
- * @param url The original remote URL used as the Cache API key.
100
- */
101
- async getMediaUrl(url: string): Promise<string | null> {
102
- const existing = this.objectUrls.get(url);
103
- if (existing) return existing;
104
-
105
- const cache = await caches.open(this.cacheName);
106
- const response = await cache.match(url);
107
- if (!response) return null;
108
-
109
- const blob = await response.blob();
110
- const objectUrl = URL.createObjectURL(blob);
111
- this.objectUrls.set(url, objectUrl);
112
- return objectUrl;
113
- }
114
-
115
- /**
116
- * Stores `blob` in the Cache API under `url` and returns a blob URL for
117
- * immediate use by the renderer.
118
- *
119
- * Called by the player after a successful `fetch()` so that subsequent
120
- * {@link getMediaUrl} calls for the same URL skip the network entirely.
121
- *
122
- * @param url The original remote URL — used as the Cache API key so that
123
- * {@link getMediaUrl} can retrieve the entry with a simple `match`.
124
- * @param blob The downloaded media blob to persist.
125
- * @returns A blob URL that the renderer can set as `src` on an
126
- * `<img>` or `<video>` element.
127
- */
128
- async saveMedia(url: string, blob: Blob): Promise<string> {
129
- const cache = await caches.open(this.cacheName);
130
- await cache.put(url, new Response(blob));
131
- const objectUrl = URL.createObjectURL(blob);
132
- this.objectUrls.set(url, objectUrl);
133
- return objectUrl;
134
- }
135
-
136
- /**
137
- * Wipes all cached data:
138
- * - Revokes every outstanding blob URL to release the underlying Blob references.
139
- * - Deletes the IndexedDB database entirely (recreated on next access).
140
- * - Removes all entries from the Cache API bucket.
141
- *
142
- * Useful for a "factory reset" flow or in tests to guarantee a clean slate.
143
- */
144
- async clear(): Promise<void> {
145
- for (const url of this.objectUrls.values()) URL.revokeObjectURL(url);
146
- this.objectUrls.clear();
147
- const db = await this.openDB();
148
- db.close();
149
- await deleteDB(this.dbName);
150
- const cache = await caches.open(this.cacheName);
151
- const keys = await cache.keys();
152
- await Promise.all(keys.map((req) => cache.delete(req)));
153
- }
154
- }
package/src/constants.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Production root URL for the SquareScreen REST API.
3
- *
4
- * {@link SquareScreenPlayer} uses this for all network requests. Integrators cannot
5
- * override it via player configuration; supply a custom {@link NetworkDataSource}
6
- * only if you must talk to a different host (e.g. tests or a private gateway).
7
- */
8
- export const SQUARESCREEN_API_BASE_URL =
9
- "https://square-screen-api-development-f7zuxa.laravel.cloud/api/v1";
package/src/core/types.ts DELETED
@@ -1,251 +0,0 @@
1
- /** The type of media content in a playlist item. Matches the string values returned by the API. */
2
- export type MediaType = "image" | "video";
3
-
4
- /** The transition animation to apply when moving between playlist items. */
5
- export type TransitionType = "fade" | "slide" | "none";
6
-
7
- /** The current connectivity and sync state of the device. */
8
- export type DeviceStatus = "connecting" | "online" | "offline" | "syncing";
9
-
10
- /** Playback strategy metadata returned alongside a playlist. */
11
- export interface PlaybackStrategy {
12
- loop: boolean;
13
- shuffle: boolean;
14
- /** Number of upcoming items to pre-download. 0 means no preloading. */
15
- preloadCount?: number;
16
- }
17
-
18
- /** Schedule metadata associated with a playlist. */
19
- export interface Schedule {
20
- uuid: string;
21
- name: string;
22
- /** Higher value means higher priority when multiple schedules overlap. */
23
- priority: number;
24
- }
25
-
26
- /** Metadata about the playlist container returned by the API. */
27
- export interface PlaylistMeta {
28
- uuid: string;
29
- name: string;
30
- }
31
-
32
- /** A playlist returned from the API, representing everything the device should display. */
33
- export interface Playlist {
34
- uuid: string;
35
- items: PlaylistItem[];
36
- /** Present when the backend includes scheduling/playback hints. Not always populated. */
37
- strategy?: PlaybackStrategy;
38
- /** Metadata about the active schedule driving this playlist. */
39
- schedule?: Schedule;
40
- /** Metadata about the playlist container itself. */
41
- playlistMeta?: PlaylistMeta;
42
- /** Unix timestamp (ms) recording when this playlist was stored in the local cache. */
43
- cachedAt: number;
44
- }
45
-
46
- /** A single piece of content within a playlist. */
47
- export interface PlaylistItem {
48
- uuid: string;
49
- name: string;
50
- type: MediaType;
51
- /** URL of the media file to display. */
52
- url: string;
53
- /** How long to display this item, in seconds. */
54
- duration: number;
55
- /** Animation to use when transitioning away from this item. Defaults to none if absent. */
56
- transition?: TransitionType;
57
- /** Native width of the media in pixels. */
58
- width?: number;
59
- /** Native height of the media in pixels. */
60
- height?: number;
61
- /** Human-readable title. Present on some item types (e.g. video). */
62
- title?: string;
63
- /** Thumbnail URL for preloading previews. */
64
- thumbnail?: string;
65
- }
66
-
67
- /** An active emergency broadcast targeting this device. */
68
- export interface EmergencyAlert {
69
- /** Server-assigned integer ID. */
70
- id: number;
71
- uuid: string;
72
- /** ID of the company that issued the broadcast. */
73
- companyId: number;
74
- title: string;
75
- message: string;
76
- /** Hex color string for the alert background, e.g. "#FF0000". */
77
- backgroundColor: string;
78
- /** Hex color string for the alert text, e.g. "#FFFFFF". */
79
- textColor: string;
80
- /** The scope this broadcast targets, e.g. "all", "workspace", "group", or "device". */
81
- targetScope: string;
82
- /** Whether this broadcast is currently active. */
83
- isActive: boolean;
84
- /** ISO 8601 timestamp when the broadcast started. */
85
- startedAt: string;
86
- /** ISO 8601 timestamp when the broadcast ended, or undefined if still active. */
87
- endedAt?: string;
88
- }
89
-
90
- /**
91
- * Device health metrics sent to the server on each heartbeat.
92
- * All hardware metrics are optional — not all browsers expose them.
93
- */
94
- export interface HeartbeatPayload {
95
- cpuUsage?: number;
96
- memoryUsage?: number;
97
- diskUsage?: number;
98
- temperature?: number;
99
- osVersion?: string;
100
- /** The version string of this SDK, included on every heartbeat. */
101
- playerVersion: string;
102
- }
103
-
104
- /** Acknowledgement returned by the server after a successful heartbeat POST. */
105
- export interface HeartbeatAck {
106
- received: boolean;
107
- }
108
-
109
- /** Query parameters for filtering a playlist to video items only. */
110
- export interface VideoPlaylistParams {
111
- category?: string;
112
- tags?: string[];
113
- quality?: string;
114
- limit?: number;
115
- }
116
-
117
- /** Query parameters for filtering a playlist to image items only. */
118
- export interface ImagePlaylistParams {
119
- category?: string;
120
- tags?: string[];
121
- limit?: number;
122
- }
123
-
124
- /** Playback event data sent to the server. */
125
- export interface PlaybackEvent {
126
- media_uuid: string;
127
- playlist_uuid: string;
128
- schedule_uuid: string;
129
- started_at: string;
130
- ended_at: string;
131
- /** Actual number of seconds the item was displayed. */
132
- duration_seconds: number;
133
- /** Whether the item played to its full duration without interruption. */
134
- completed: boolean;
135
- }
136
- // ---------------------------------------------------------------------------
137
- // Error types
138
- // ---------------------------------------------------------------------------
139
-
140
- /** An HTTP-level failure — the request reached the server but returned an error status. */
141
- export interface NetworkError {
142
- kind: "network";
143
- code: number;
144
- message: string;
145
- }
146
-
147
- /** The device credentials (ID or token) were rejected by the server. */
148
- export interface AuthError {
149
- kind: "auth";
150
- message: string;
151
- }
152
-
153
- /** Reading from or writing to the local cache failed. */
154
- export interface CacheError {
155
- kind: "cache";
156
- message: string;
157
- }
158
-
159
- /** The server response could not be parsed into the expected shape. */
160
- export interface ParseError {
161
- kind: "parse";
162
- message: string;
163
- }
164
-
165
- /**
166
- * Signals that an emergency broadcast is active.
167
- * The caller should switch to displaying the EmergencyAlert instead of normal content.
168
- */
169
- export interface EmergencyOverrideActive {
170
- kind: "emergency_override";
171
- }
172
-
173
- /**
174
- * Every error the SDK can produce.
175
- * Check the `kind` field to identify and handle each case.
176
- */
177
- export type SquareScreenError =
178
- | NetworkError
179
- | AuthError
180
- | CacheError
181
- | ParseError
182
- | EmergencyOverrideActive;
183
-
184
- // ---------------------------------------------------------------------------
185
- // Result type
186
- // ---------------------------------------------------------------------------
187
-
188
- /**
189
- * The return type for all SDK operations that can fail.
190
- *
191
- * Check `result.success` first — TypeScript will then narrow the type
192
- * so that `result.data` is only accessible on success and `result.error`
193
- * is only accessible on failure.
194
- *
195
- * @example
196
- * if (result.success) {
197
- * console.log(result.data);
198
- * } else {
199
- * console.error(result.error.kind);
200
- * }
201
- */
202
- export type SquareScreenResult<T> =
203
- | { success: true; data: T }
204
- | { success: false; error: SquareScreenError };
205
-
206
- // ---------------------------------------------------------------------------
207
- // Network interface
208
- // ---------------------------------------------------------------------------
209
-
210
- export interface NetworkDataSource {
211
- fetchPlaylist: () => Promise<SquareScreenResult<Playlist>>;
212
- fetchVideoPlaylist: (
213
- params: VideoPlaylistParams,
214
- ) => Promise<SquareScreenResult<Playlist>>;
215
- fetchImagePlaylist: (
216
- params: ImagePlaylistParams,
217
- ) => Promise<SquareScreenResult<Playlist>>;
218
- /** Resolves to null when no emergency is active. */
219
- checkForEmergencyAlert: () => Promise<
220
- SquareScreenResult<EmergencyAlert | null>
221
- >;
222
- healthCheck: (
223
- payload: HeartbeatPayload,
224
- ) => Promise<SquareScreenResult<HeartbeatAck>>;
225
- reportPlaybackEvent: (
226
- event: PlaybackEvent,
227
- ) => Promise<SquareScreenResult<{ recorded: boolean }>>;
228
- }
229
-
230
- // ---------------------------------------------------------------------------
231
- // Cache interface
232
- // ---------------------------------------------------------------------------
233
-
234
- /**
235
- * Abstraction over local storage. The default implementation is `SquareScreenCache`
236
- * (IndexedDB for playlist metadata + Cache API for media blobs). Pass a custom
237
- * implementation via `SquareScreenPlayerConfig.cacheProvider` to integrate with
238
- * an existing storage layer.
239
- */
240
- export interface SquareScreenCacheProvider {
241
- /** Returns the cached playlist, or null if not found / TTL expired. Pass `allowStale` to bypass TTL for offline fallback. */
242
- getPlaylist(uuid: string, allowStale?: boolean): Promise<Playlist | null>;
243
- /** Persists a playlist. `ttlMs` controls how long it is considered fresh. */
244
- savePlaylist(playlist: Playlist, ttlMs: number): Promise<void>;
245
- /** Returns a blob URL for the cached media, or null if not yet downloaded. */
246
- getMediaUrl(url: string): Promise<string | null>;
247
- /** Downloads and stores the media blob, returns a blob URL for immediate use. */
248
- saveMedia(url: string, blob: Blob): Promise<string>;
249
- /** Wipes all cached data (playlist metadata and media blobs). */
250
- clear(): Promise<void>;
251
- }
package/src/env.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare module "*?raw" {
2
- const content: string;
3
- export default content;
4
- }
package/src/index.ts DELETED
@@ -1,34 +0,0 @@
1
- export { SQUARESCREEN_API_BASE_URL } from "./constants";
2
- export { SquareScreenPlayer } from "./player/SquareScreenPlayer";
3
- export type { SquareScreenPlayerConfig, PlayerEventMap } from "./player/SquareScreenPlayer";
4
-
5
- export { SquareScreenRenderer } from "./renderer/SquareScreenRenderer";
6
- export type { SquareScreenRendererConfig } from "./renderer/SquareScreenRenderer";
7
-
8
- export { SquareScreenCache } from "./cache/SquareScreenCache";
9
-
10
- export type {
11
- Playlist,
12
- PlaylistItem,
13
- PlaybackStrategy,
14
- Schedule,
15
- PlaylistMeta,
16
- MediaType,
17
- TransitionType,
18
- DeviceStatus,
19
- EmergencyAlert,
20
- HeartbeatPayload,
21
- HeartbeatAck,
22
- SquareScreenError,
23
- SquareScreenResult,
24
- NetworkError,
25
- AuthError,
26
- CacheError,
27
- ParseError,
28
- EmergencyOverrideActive,
29
- NetworkDataSource,
30
- SquareScreenCacheProvider,
31
- VideoPlaylistParams,
32
- ImagePlaylistParams,
33
- PlaybackEvent,
34
- } from "./core/types";
@@ -1,234 +0,0 @@
1
- import {
2
- EmergencyAlert,
3
- HeartbeatAck,
4
- HeartbeatPayload,
5
- ImagePlaylistParams,
6
- NetworkDataSource,
7
- PlaybackEvent,
8
- Playlist,
9
- SquareScreenResult,
10
- VideoPlaylistParams,
11
- } from "../core/types";
12
- import {
13
- ApiEmergencyResponse,
14
- ApiHeartbeatAck,
15
- ApiPlaylistResponse,
16
- } from "./apiTypes";
17
- import {
18
- mapEmergencyResponse,
19
- mapHeartbeatAck,
20
- mapPlaylistResponse,
21
- mapHeartbeatPayload,
22
- } from "./mappers";
23
-
24
- /**
25
- * Handles all HTTP communication with the SquareScreen API.
26
- *
27
- * Implements {@link NetworkDataSource} and is the only class in the SDK
28
- * that calls `fetch` directly. Auth headers are injected automatically on
29
- * every request — callers never set them manually.
30
- *
31
- * This class is internal to the SDK. Consumers interact with it only through
32
- * the `NetworkDataSource` interface via the player module.
33
- */
34
- export class NetworkClient implements NetworkDataSource {
35
- private readonly baseUrl: string;
36
- private readonly deviceId: string;
37
- private readonly deviceToken: string;
38
-
39
- /**
40
- * @param baseUrl - Root URL of the SquareScreen API, e.g. `"https://api.squarescreen.io/api/v1"`.
41
- * @param deviceId - Unique identifier for this device.
42
- * @param deviceToken - Secret token used to authenticate this device. Never log or expose this value.
43
- */
44
- constructor(baseUrl: string, deviceId: string, deviceToken: string) {
45
- this.baseUrl = baseUrl;
46
- this.deviceId = deviceId;
47
- this.deviceToken = deviceToken;
48
- }
49
-
50
- /**
51
- * Central fetch wrapper used by all public methods.
52
- *
53
- * Injects auth headers, separates HTTP errors from network failures,
54
- * and isolates JSON parse errors — so callers always receive a
55
- * {@link SquareScreenResult} and never an uncaught exception.
56
- *
57
- * Error mapping:
58
- * - 401/403 → `AuthError`
59
- * - Other non-2xx → `NetworkError`
60
- * - JSON parse failure → `ParseError`
61
- * - `fetch` throw (no internet, CORS, timeout) → `NetworkError` with code `0`
62
- */
63
- private async fetchWrapper<T>(
64
- endpoint: string,
65
- options: RequestInit,
66
- ): Promise<SquareScreenResult<T>> {
67
- try {
68
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
69
- ...options,
70
- headers: {
71
- ...(options?.headers ?? {}),
72
- "Content-Type": "application/json",
73
- "X-Device-Id": this.deviceId,
74
- "X-Device-Token": this.deviceToken,
75
- },
76
- });
77
-
78
- if (!response.ok) {
79
- if (response.status === 401) {
80
- return {
81
- success: false,
82
- error: {
83
- kind: "auth",
84
- message: "Unauthorized",
85
- },
86
- };
87
- }
88
- if (response.status === 403) {
89
- return {
90
- success: false,
91
- error: {
92
- kind: "auth",
93
- message: "Forbidden",
94
- },
95
- };
96
- }
97
- return {
98
- success: false,
99
- error: {
100
- kind: "network",
101
- code: response.status,
102
- message: response.statusText || `HTTP error ${response.status}`,
103
- },
104
- };
105
- }
106
-
107
- try {
108
- const data = (await response.json()) as T;
109
- return { success: true, data };
110
- } catch {
111
- return {
112
- success: false,
113
- error: {
114
- kind: "parse",
115
- message: "Response could not be parsed as JSON",
116
- },
117
- };
118
- }
119
- } catch (err) {
120
- return {
121
- success: false,
122
- error: {
123
- kind: "network",
124
- code: 0,
125
- message:
126
- err instanceof Error ? err.message : "Network request failed",
127
- },
128
- };
129
- }
130
- }
131
-
132
- /** Fetches the full active playlist for this device. */
133
- async fetchPlaylist(): Promise<SquareScreenResult<Playlist>> {
134
- const result = await this.fetchWrapper<ApiPlaylistResponse>(
135
- "/screen/now-playing",
136
- { method: "GET" },
137
- );
138
- if (!result.success) return result;
139
- return { success: true, data: mapPlaylistResponse(result.data) };
140
- }
141
-
142
- /** Fetches a playlist filtered to video items only. */
143
- async fetchVideoPlaylist(
144
- params: VideoPlaylistParams,
145
- ): Promise<SquareScreenResult<Playlist>> {
146
- const query = new URLSearchParams({
147
- type: "video",
148
- ...(params.category && { category: params.category }),
149
- ...(params.quality && { quality: params.quality }),
150
- ...(params.limit && { limit: String(params.limit) }),
151
- ...(params.tags && { tags: params.tags.join(",") }),
152
- });
153
- const result = await this.fetchWrapper<ApiPlaylistResponse>(
154
- `/screen/now-playing?${query}`,
155
- { method: "GET" },
156
- );
157
- if (!result.success) return result;
158
- return { success: true, data: mapPlaylistResponse(result.data) };
159
- }
160
-
161
- /** Fetches a playlist filtered to image items only. */
162
- async fetchImagePlaylist(
163
- params: ImagePlaylistParams,
164
- ): Promise<SquareScreenResult<Playlist>> {
165
- const query = new URLSearchParams({
166
- type: "image",
167
- ...(params.category && { category: params.category }),
168
- ...(params.limit && { limit: String(params.limit) }),
169
- ...(params.tags && { tags: params.tags.join(",") }),
170
- });
171
- const result = await this.fetchWrapper<ApiPlaylistResponse>(
172
- `/screen/now-playing?${query}`,
173
- { method: "GET" },
174
- );
175
- if (!result.success) return result;
176
- return { success: true, data: mapPlaylistResponse(result.data) };
177
- }
178
-
179
- /**
180
- * Checks whether an emergency broadcast is currently active for this device.
181
- * Resolves to `null` when no emergency is active.
182
- */
183
- async checkForEmergencyAlert(): Promise<
184
- SquareScreenResult<EmergencyAlert | null>
185
- > {
186
- const result = await this.fetchWrapper<ApiEmergencyResponse>(
187
- "/screen/emergency",
188
- { method: "GET" },
189
- );
190
- if (!result.success) return result;
191
- return { success: true, data: mapEmergencyResponse(result.data) };
192
- }
193
-
194
- /**
195
- * Posts device health metrics to the server.
196
- * The payload is mapped to snake_case before sending to match the API's expected format.
197
- *
198
- * @param payload - Collected device metrics. Hardware fields are optional;
199
- * `playerVersion` is always required.
200
- */
201
- async healthCheck(
202
- payload: HeartbeatPayload,
203
- ): Promise<SquareScreenResult<HeartbeatAck>> {
204
- const result = await this.fetchWrapper<ApiHeartbeatAck>(
205
- "/screen/heartbeat",
206
- { method: "POST", body: JSON.stringify(mapHeartbeatPayload(payload)) },
207
- );
208
- if (!result.success) return result;
209
- return { success: true, data: mapHeartbeatAck(result.data) };
210
- }
211
-
212
- /** Reports a playback event to the server.
213
- * @param event - The playback event to report.
214
- * @returns A result indicating whether the event was recorded successfully.
215
- * @example
216
- * const result = await networkClient.reportPlaybackEvent({
217
- * media_uuid: "mf-00000001-0000-0000-0000-000000000001",
218
- * playlist_uuid: "pl-00000001-0000-0000-0000-000000000001",
219
- * schedule_uuid: "sc-00000001-0000-0000-0000-000000000001",
220
- * started_at: "2026-04-28T07:05:00Z",
221
- * ended_at: "2026-04-28T07:05:10Z",
222
- * });
223
- */
224
- async reportPlaybackEvent(
225
- event: PlaybackEvent,
226
- ): Promise<SquareScreenResult<{ recorded: boolean }>> {
227
- const result = await this.fetchWrapper<{ recorded: boolean }>(
228
- "/screen/playback",
229
- { method: "POST", body: JSON.stringify(event) },
230
- );
231
- if (!result.success) return result;
232
- return { success: true, data: { recorded: result.data.recorded } };
233
- }
234
- }