@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.
Files changed (44) hide show
  1. package/.github/workflows/build-js-sdk.yml +70 -0
  2. package/README.md +463 -0
  3. package/eslint.config.js +3 -0
  4. package/examples/react-app/README.md +73 -0
  5. package/examples/react-app/eslint.config.js +22 -0
  6. package/examples/react-app/index.html +13 -0
  7. package/examples/react-app/package-lock.json +2239 -0
  8. package/examples/react-app/package.json +31 -0
  9. package/examples/react-app/public/favicon.svg +1 -0
  10. package/examples/react-app/public/icons.svg +24 -0
  11. package/examples/react-app/src/App.css +184 -0
  12. package/examples/react-app/src/App.tsx +157 -0
  13. package/examples/react-app/src/EmergencyTicker.tsx +25 -0
  14. package/examples/react-app/src/HeadlessExample.tsx +66 -0
  15. package/examples/react-app/src/RendererExample.tsx +70 -0
  16. package/examples/react-app/src/assets/hero.png +0 -0
  17. package/examples/react-app/src/assets/react.svg +1 -0
  18. package/examples/react-app/src/assets/vite.svg +1 -0
  19. package/examples/react-app/src/index.css +183 -0
  20. package/examples/react-app/src/main.tsx +10 -0
  21. package/examples/react-app/src/mockNetworkDataSource.ts +116 -0
  22. package/examples/react-app/src/usePlayer.ts +71 -0
  23. package/examples/react-app/tsconfig.app.json +25 -0
  24. package/examples/react-app/tsconfig.json +7 -0
  25. package/examples/react-app/tsconfig.node.json +24 -0
  26. package/examples/react-app/vite.config.ts +7 -0
  27. package/examples/react-app/yarn.lock +1089 -0
  28. package/package.json +49 -0
  29. package/src/__tests__/cache/SquareScreenCache.test.ts +375 -0
  30. package/src/__tests__/network/NetworkClient.test.ts +217 -0
  31. package/src/__tests__/network/mappers.test.ts +163 -0
  32. package/src/__tests__/player/SquareScreenPlayer.test.ts +840 -0
  33. package/src/cache/SquareScreenCache.ts +154 -0
  34. package/src/constants.ts +9 -0
  35. package/src/core/types.ts +251 -0
  36. package/src/env.d.ts +4 -0
  37. package/src/index.ts +34 -0
  38. package/src/network/NetworkClient.ts +234 -0
  39. package/src/network/apiTypes.ts +89 -0
  40. package/src/network/mappers.ts +106 -0
  41. package/src/player/SquareScreenPlayer.ts +414 -0
  42. package/src/renderer/SquareScreenRenderer.ts +282 -0
  43. package/tsconfig.json +12 -0
  44. package/tsdown.config.ts +23 -0
@@ -0,0 +1,183 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ body {
4
+ font-family: system-ui, sans-serif;
5
+ background: #0f0f0f;
6
+ color: #f0f0f0;
7
+ min-height: 100dvh;
8
+ }
9
+
10
+ .app {
11
+ max-width: 960px;
12
+ margin: 0 auto;
13
+ padding: 2rem;
14
+ }
15
+
16
+ h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
17
+ .subtitle { color: #888; font-size: 0.9rem; margin-bottom: 2rem; }
18
+
19
+ .tabs {
20
+ display: flex;
21
+ gap: 0.5rem;
22
+ margin-bottom: 2rem;
23
+ border-bottom: 1px solid #333;
24
+ padding-bottom: 0.5rem;
25
+ }
26
+
27
+ .tabs button {
28
+ background: none;
29
+ border: none;
30
+ color: #888;
31
+ font-size: 0.95rem;
32
+ padding: 0.4rem 0.8rem;
33
+ cursor: pointer;
34
+ border-radius: 4px;
35
+ }
36
+
37
+ .tabs button.active {
38
+ background: #1e1e1e;
39
+ color: #fff;
40
+ }
41
+
42
+ .config-form {
43
+ display: grid;
44
+ gap: 0.75rem;
45
+ margin-bottom: 2rem;
46
+ background: #1a1a1a;
47
+ padding: 1.25rem;
48
+ border-radius: 8px;
49
+ }
50
+
51
+ .config-form label { font-size: 0.8rem; color: #aaa; display: block; margin-bottom: 0.25rem; }
52
+
53
+ .config-form input {
54
+ width: 100%;
55
+ background: #111;
56
+ border: 1px solid #333;
57
+ border-radius: 4px;
58
+ color: #f0f0f0;
59
+ font-size: 0.9rem;
60
+ padding: 0.5rem 0.75rem;
61
+ }
62
+
63
+ .config-form input:focus { outline: none; border-color: #555; }
64
+
65
+ .actions { display: flex; gap: 0.5rem; }
66
+
67
+ button.primary {
68
+ background: #fff;
69
+ color: #000;
70
+ border: none;
71
+ border-radius: 4px;
72
+ padding: 0.5rem 1.25rem;
73
+ font-size: 0.9rem;
74
+ cursor: pointer;
75
+ }
76
+
77
+ button.secondary {
78
+ background: #2a2a2a;
79
+ color: #f0f0f0;
80
+ border: none;
81
+ border-radius: 4px;
82
+ padding: 0.5rem 1.25rem;
83
+ font-size: 0.9rem;
84
+ cursor: pointer;
85
+ }
86
+
87
+ .screen-wrap {
88
+ position: relative;
89
+ width: 100%;
90
+ aspect-ratio: 16 / 9;
91
+ background: #000;
92
+ border-radius: 8px;
93
+ overflow: hidden;
94
+ margin-bottom: 1rem;
95
+ }
96
+
97
+ .status-bar {
98
+ display: flex;
99
+ gap: 1rem;
100
+ align-items: center;
101
+ font-size: 0.8rem;
102
+ color: #888;
103
+ margin-bottom: 1rem;
104
+ }
105
+
106
+ .dot {
107
+ width: 8px;
108
+ height: 8px;
109
+ border-radius: 50%;
110
+ background: #555;
111
+ display: inline-block;
112
+ margin-right: 0.4rem;
113
+ }
114
+ .dot.online { background: #4caf50; }
115
+ .dot.offline { background: #f44336; }
116
+ .dot.connecting { background: #ff9800; }
117
+ .dot.syncing { background: #2196f3; }
118
+
119
+ .item-info {
120
+ background: #1a1a1a;
121
+ border-radius: 6px;
122
+ padding: 0.75rem 1rem;
123
+ font-size: 0.85rem;
124
+ color: #aaa;
125
+ }
126
+
127
+ .item-info strong { color: #fff; }
128
+
129
+ .emergency-ticker {
130
+ position: absolute;
131
+ bottom: 0;
132
+ left: 0;
133
+ right: 0;
134
+ height: 72px;
135
+ z-index: 10;
136
+ display: flex;
137
+ align-items: center;
138
+ overflow: hidden;
139
+ }
140
+
141
+ .emergency-ticker-track {
142
+ display: flex;
143
+ width: max-content;
144
+ animation: emergency-scroll 18s linear infinite;
145
+ }
146
+
147
+ .emergency-ticker-text {
148
+ white-space: nowrap;
149
+ font-size: 1.1rem;
150
+ font-weight: 600;
151
+ padding-right: 6rem;
152
+ }
153
+
154
+ @keyframes emergency-scroll {
155
+ from { transform: translateX(0); }
156
+ to { transform: translateX(-50%); }
157
+ }
158
+
159
+ .placeholder {
160
+ position: absolute;
161
+ inset: 0;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ color: #555;
166
+ font-size: 0.9rem;
167
+ }
168
+
169
+ .headless-media {
170
+ position: absolute;
171
+ inset: 0;
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ background: #000;
176
+ }
177
+
178
+ .headless-media img,
179
+ .headless-media video {
180
+ max-width: 100%;
181
+ max-height: 100%;
182
+ object-fit: contain;
183
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,116 @@
1
+ import type {
2
+ NetworkDataSource,
3
+ Playlist,
4
+ SquareScreenResult,
5
+ EmergencyAlert,
6
+ HeartbeatPayload,
7
+ HeartbeatAck,
8
+ VideoPlaylistParams,
9
+ ImagePlaylistParams,
10
+ PlaybackEvent,
11
+ } from "square-screen-js-sdk";
12
+
13
+ const MOCK_PLAYLIST: Playlist = {
14
+ uuid: "mock-playlist-001",
15
+ cachedAt: Date.now(),
16
+ playlistMeta: { uuid: "mock-playlist-001", name: "Demo Playlist" },
17
+ strategy: { loop: true, shuffle: false, preloadCount: 1 },
18
+ items: [
19
+ {
20
+ uuid: "item-001",
21
+ name: "Mountain landscape",
22
+ type: "image",
23
+ url: "https://fastly.picsum.photos/id/10/2500/1667.jpg?hmac=J04WWC_ebchx3WwzbM-Z4_KC_LeLBWr5LZMaAkWkF68",
24
+ duration: 5,
25
+ transition: "fade",
26
+ },
27
+ {
28
+ uuid: "item-002",
29
+ name: "Nature video",
30
+ type: "video",
31
+ url: "https://lorem.video/480p",
32
+ duration: 10,
33
+ transition: "slide",
34
+ },
35
+ {
36
+ uuid: "item-004",
37
+ name: "Cat video",
38
+ type: "video",
39
+ url: "https://lorem.video/cat",
40
+ duration: 12,
41
+ transition: "slide",
42
+ },
43
+ {
44
+ uuid: "item-003",
45
+ name: "Forest path",
46
+ type: "image",
47
+ url: "https://fastly.picsum.photos/id/10/2500/1667.jpg?hmac=J04WWC_ebchx3WwzbM-Z4_KC_LeLBWr5LZMaAkWkF68",
48
+ duration: 5,
49
+ transition: "fade",
50
+ },
51
+ ],
52
+ };
53
+ const MOCK_EMERGENCY: EmergencyAlert = {
54
+ uuid: "emergency-001",
55
+ title: "Emergency Broadcast",
56
+ message:
57
+ "This is a test emergency alert. Normal playback will resume shortly.",
58
+ backgroundColor: "#cc0000",
59
+ textColor: "#ffffff",
60
+ targetScope: "all",
61
+ };
62
+
63
+ let emergencyActive = false;
64
+
65
+ export function setMockEmergency(active: boolean) {
66
+ emergencyActive = active;
67
+ }
68
+
69
+ export const mockNetworkDataSource: NetworkDataSource = {
70
+ fetchPlaylist: async (): Promise<SquareScreenResult<Playlist>> => ({
71
+ success: true,
72
+ data: { ...MOCK_PLAYLIST, cachedAt: Date.now() },
73
+ }),
74
+
75
+ fetchVideoPlaylist: async (
76
+ _params: VideoPlaylistParams,
77
+ ): Promise<SquareScreenResult<Playlist>> => ({
78
+ success: true,
79
+ data: {
80
+ ...MOCK_PLAYLIST,
81
+ items: MOCK_PLAYLIST.items.filter((i) => i.type === "video"),
82
+ cachedAt: Date.now(),
83
+ },
84
+ }),
85
+
86
+ fetchImagePlaylist: async (
87
+ _params: ImagePlaylistParams,
88
+ ): Promise<SquareScreenResult<Playlist>> => ({
89
+ success: true,
90
+ data: {
91
+ ...MOCK_PLAYLIST,
92
+ items: MOCK_PLAYLIST.items.filter((i) => i.type === "image"),
93
+ cachedAt: Date.now(),
94
+ },
95
+ }),
96
+
97
+ checkForEmergencyAlert: async (): Promise<
98
+ SquareScreenResult<EmergencyAlert | null>
99
+ > => ({
100
+ success: true,
101
+ data: emergencyActive ? MOCK_EMERGENCY : null,
102
+ }),
103
+
104
+ healthCheck: async (
105
+ _payload: HeartbeatPayload,
106
+ ): Promise<SquareScreenResult<HeartbeatAck>> => ({
107
+ success: true,
108
+ data: { received: true },
109
+ }),
110
+ reportPlaybackEvent: async (
111
+ _event: PlaybackEvent,
112
+ ): Promise<SquareScreenResult<{ recorded: boolean }>> => ({
113
+ success: true,
114
+ data: { recorded: true },
115
+ }),
116
+ };
@@ -0,0 +1,71 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import {
3
+ SquareScreenPlayer,
4
+ type SquareScreenPlayerConfig,
5
+ type DeviceStatus,
6
+ type EmergencyAlert,
7
+ type PlaylistItem,
8
+ } from "square-screen-js-sdk";
9
+
10
+ export interface PlayerState {
11
+ status: DeviceStatus;
12
+ currentItem: PlaylistItem | null;
13
+ currentIndex: number;
14
+ total: number;
15
+ alert: EmergencyAlert | null;
16
+ }
17
+
18
+ /**
19
+ * Creates and manages a SquareScreenPlayer instance for the lifetime of the component.
20
+ * Returns the player instance and reactive state derived from player events.
21
+ */
22
+ export function usePlayer(config: SquareScreenPlayerConfig | null): {
23
+ player: SquareScreenPlayer | null;
24
+ state: PlayerState;
25
+ } {
26
+ const playerRef = useRef<SquareScreenPlayer | null>(null);
27
+ const [state, setState] = useState<PlayerState>({
28
+ status: "connecting",
29
+ currentItem: null,
30
+ currentIndex: 0,
31
+ total: 0,
32
+ alert: null,
33
+ });
34
+
35
+ useEffect(() => {
36
+ if (!config) return;
37
+
38
+ const player = new SquareScreenPlayer(config);
39
+ playerRef.current = player;
40
+
41
+ const onStatus: Parameters<typeof player.addEventListener<"statuschange">>[1] = (e) =>
42
+ setState((s) => ({ ...s, status: e.detail.status }));
43
+
44
+ const onItem: Parameters<typeof player.addEventListener<"itemchange">>[1] = (e) =>
45
+ setState((s) => ({
46
+ ...s,
47
+ currentItem: e.detail.item,
48
+ currentIndex: e.detail.index,
49
+ total: e.detail.total,
50
+ }));
51
+
52
+ const onAlert: Parameters<typeof player.addEventListener<"emergencyalert">>[1] = (e) =>
53
+ setState((s) => ({ ...s, alert: e.detail.alert }));
54
+
55
+ player.addEventListener("statuschange", onStatus);
56
+ player.addEventListener("itemchange", onItem);
57
+ player.addEventListener("emergencyalert", onAlert);
58
+
59
+ player.start();
60
+
61
+ return () => {
62
+ player.removeEventListener("statuschange", onStatus);
63
+ player.removeEventListener("itemchange", onItem);
64
+ player.removeEventListener("emergencyalert", onAlert);
65
+ player.stop();
66
+ playerRef.current = null;
67
+ };
68
+ }, [config]);
69
+
70
+ return { player: playerRef.current, state };
71
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023", "DOM"],
6
+ "module": "esnext",
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true
23
+ },
24
+ "include": ["src"]
25
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023"],
6
+ "module": "esnext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })