@interlucent/pixel-stream-react 0.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.
- package/dist/index.d.ts +253 -0
- package/dist/index.js +283 -0
- package/package.json +47 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for the <pixel-stream> web component.
|
|
3
|
+
*
|
|
4
|
+
* SSR-safe: renders an empty <pixel-stream> tag on the server, hydrates
|
|
5
|
+
* on the client by lazily importing the web component definition.
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* - A <PixelStream> React component with typed props and event callbacks
|
|
9
|
+
* - JSX IntrinsicElements augmentation so bare <pixel-stream> also type-checks
|
|
10
|
+
* - Re-exports of key types from @interlucent/pixel-stream
|
|
11
|
+
*/
|
|
12
|
+
import React, { type RefObject } from 'react';
|
|
13
|
+
import type { PixelStream as PixelStreamElement, PixelStreamStatus, ResolutionClampName, StreamResolutionClamp } from '@interlucent/pixel-stream';
|
|
14
|
+
export type { PixelStreamStatus, ResolutionClampName, StreamResolutionClamp, } from '@interlucent/pixel-stream';
|
|
15
|
+
export interface ChangeDetail<T = string | null> {
|
|
16
|
+
oldValue: T;
|
|
17
|
+
newValue: T;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionStateChangeDetail {
|
|
20
|
+
oldState: string;
|
|
21
|
+
newState: string;
|
|
22
|
+
}
|
|
23
|
+
export interface StreamStateChangeDetail {
|
|
24
|
+
oldState: string;
|
|
25
|
+
newState: string;
|
|
26
|
+
detail?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface StatusChangeDetail {
|
|
29
|
+
oldStatus: PixelStreamStatus;
|
|
30
|
+
newStatus: PixelStreamStatus;
|
|
31
|
+
}
|
|
32
|
+
export interface SessionErrorDetail {
|
|
33
|
+
message: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface AdmissionErrorDetail {
|
|
37
|
+
reason: string;
|
|
38
|
+
}
|
|
39
|
+
export interface StreamResolutionChangeDetail {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
clamp?: StreamResolutionClamp | null;
|
|
43
|
+
}
|
|
44
|
+
export interface PeerDetail {
|
|
45
|
+
peerId: string;
|
|
46
|
+
}
|
|
47
|
+
export interface MuteChangeDetail {
|
|
48
|
+
oldValue: boolean;
|
|
49
|
+
newValue: boolean;
|
|
50
|
+
muted: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface PermissionChangeDetail {
|
|
53
|
+
permission: string;
|
|
54
|
+
status: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ResizeModeDetectedDetail {
|
|
57
|
+
mode: string;
|
|
58
|
+
}
|
|
59
|
+
export interface XRStreamDetectedDetail {
|
|
60
|
+
isXRStream: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface PixelStreamEventProps {
|
|
63
|
+
onReady?: (e: CustomEvent) => void;
|
|
64
|
+
onAdmissionTokenChange?: (e: CustomEvent<ChangeDetail>) => void;
|
|
65
|
+
onAppIdChange?: (e: CustomEvent<ChangeDetail>) => void;
|
|
66
|
+
onAppVersionChange?: (e: CustomEvent<ChangeDetail>) => void;
|
|
67
|
+
onAdmitted?: (e: CustomEvent) => void;
|
|
68
|
+
onAdmissionRevoked?: (e: CustomEvent) => void;
|
|
69
|
+
onAdmissionError?: (e: CustomEvent<AdmissionErrorDetail>) => void;
|
|
70
|
+
onNoAutoConnectChange?: (e: CustomEvent<ChangeDetail<boolean>>) => void;
|
|
71
|
+
onSessionStateChange?: (e: CustomEvent<SessionStateChangeDetail>) => void;
|
|
72
|
+
onSessionError?: (e: CustomEvent<SessionErrorDetail>) => void;
|
|
73
|
+
onJobStateChange?: (e: CustomEvent) => void;
|
|
74
|
+
onStreamStateChange?: (e: CustomEvent<StreamStateChangeDetail>) => void;
|
|
75
|
+
onStreamResolutionChange?: (e: CustomEvent<StreamResolutionChangeDetail>) => void;
|
|
76
|
+
onStatusChange?: (e: CustomEvent<StatusChangeDetail>) => void;
|
|
77
|
+
onRendezvousStarted?: (e: CustomEvent) => void;
|
|
78
|
+
onRendezvousTimeout?: (e: CustomEvent) => void;
|
|
79
|
+
onRendezvousCancelled?: (e: CustomEvent) => void;
|
|
80
|
+
onLingerStarted?: (e: CustomEvent) => void;
|
|
81
|
+
onLingerTimeout?: (e: CustomEvent) => void;
|
|
82
|
+
onLingerCancelled?: (e: CustomEvent) => void;
|
|
83
|
+
onPeerConnected?: (e: CustomEvent<PeerDetail>) => void;
|
|
84
|
+
onPeerDisconnected?: (e: CustomEvent<PeerDetail>) => void;
|
|
85
|
+
onMuteChange?: (e: CustomEvent<MuteChangeDetail>) => void;
|
|
86
|
+
onVolumeChange?: (e: CustomEvent<ChangeDetail<number>>) => void;
|
|
87
|
+
onPermissionChange?: (e: CustomEvent<PermissionChangeDetail>) => void;
|
|
88
|
+
onRendezvousPreferenceChange?: (e: CustomEvent<ChangeDetail<number | null>>) => void;
|
|
89
|
+
onLingerPreferenceChange?: (e: CustomEvent<ChangeDetail<number | null>>) => void;
|
|
90
|
+
onLeftGracePeriodChange?: (e: CustomEvent<ChangeDetail<number | null>>) => void;
|
|
91
|
+
onApiEndpointChange?: (e: CustomEvent<ChangeDetail>) => void;
|
|
92
|
+
onResizeModeDetected?: (e: CustomEvent<ResizeModeDetectedDetail>) => void;
|
|
93
|
+
onXRStreamDetected?: (e: CustomEvent<XRStreamDetectedDetail>) => void;
|
|
94
|
+
onLog?: (e: CustomEvent) => void;
|
|
95
|
+
}
|
|
96
|
+
export interface PixelStreamAttributeProps {
|
|
97
|
+
admissionToken?: string | null;
|
|
98
|
+
appId?: string | null;
|
|
99
|
+
appVersion?: string | null;
|
|
100
|
+
noAutoConnect?: boolean;
|
|
101
|
+
nativeTouch?: boolean;
|
|
102
|
+
pointerLock?: boolean;
|
|
103
|
+
pointerLockRelease?: boolean;
|
|
104
|
+
suppressBrowserKeys?: boolean;
|
|
105
|
+
enableGamepad?: boolean;
|
|
106
|
+
enableXr?: boolean;
|
|
107
|
+
mute?: boolean;
|
|
108
|
+
volume?: number;
|
|
109
|
+
rendezvousPreference?: number;
|
|
110
|
+
lingerPreference?: number;
|
|
111
|
+
leftGracePeriod?: number;
|
|
112
|
+
apiEndpoint?: string;
|
|
113
|
+
forceRelay?: boolean;
|
|
114
|
+
queueWaitTolerance?: number;
|
|
115
|
+
webrtcNegotiationTolerance?: number;
|
|
116
|
+
reconnectMode?: 'none' | 'recover' | 'always';
|
|
117
|
+
reconnectAttempts?: number;
|
|
118
|
+
reconnectStrategy?: 'periodic' | 'exponential-backoff';
|
|
119
|
+
reconnectInterval?: number;
|
|
120
|
+
disconnectGraceMs?: number;
|
|
121
|
+
resizeMode?: 'none' | 'auto' | 'pureweb' | 'pre-5.4' | '5.4+';
|
|
122
|
+
dprCap?: number;
|
|
123
|
+
resolutionClamp?: ResolutionClampName;
|
|
124
|
+
debug?: boolean;
|
|
125
|
+
controls?: boolean;
|
|
126
|
+
swiftJobRequest?: boolean;
|
|
127
|
+
streamerId?: string;
|
|
128
|
+
lat?: number;
|
|
129
|
+
lng?: number;
|
|
130
|
+
streamQuality?: 'low-latency' | 'balanced' | 'quality';
|
|
131
|
+
videoBitrateMin?: number;
|
|
132
|
+
videoBitrateStart?: number;
|
|
133
|
+
videoBitrateMax?: number;
|
|
134
|
+
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
|
135
|
+
}
|
|
136
|
+
export interface PixelStreamProps extends PixelStreamAttributeProps, PixelStreamEventProps {
|
|
137
|
+
className?: string;
|
|
138
|
+
style?: React.CSSProperties;
|
|
139
|
+
children?: React.ReactNode;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* React component wrapping the `<pixel-stream>` web component.
|
|
143
|
+
*
|
|
144
|
+
* - SSR-safe: renders `<pixel-stream>` on the server (no crash), hydrates on client
|
|
145
|
+
* - Full typed props for all 51 HTML attributes
|
|
146
|
+
* - Event callbacks for all custom events (`onStatusChange`, `onReady`, etc.)
|
|
147
|
+
* - Ref forwarding — access the underlying element for imperative methods
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```tsx
|
|
151
|
+
* import { PixelStream } from '@interlucent/pixel-stream-react';
|
|
152
|
+
*
|
|
153
|
+
* function App() {
|
|
154
|
+
* const ref = useRef<PixelStreamElement>(null);
|
|
155
|
+
*
|
|
156
|
+
* return (
|
|
157
|
+
* <PixelStream
|
|
158
|
+
* ref={ref}
|
|
159
|
+
* admissionToken="tok_..."
|
|
160
|
+
* onStatusChange={(e) => console.log(e.detail.newStatus)}
|
|
161
|
+
* onReady={() => console.log('ready')}
|
|
162
|
+
* style={{ width: '100%', height: '100%' }}
|
|
163
|
+
* />
|
|
164
|
+
* );
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export declare const PixelStream: React.ForwardRefExoticComponent<PixelStreamProps & React.RefAttributes<PixelStreamElement>>;
|
|
169
|
+
/**
|
|
170
|
+
* Convenience hook providing a typed ref for the `<pixel-stream>` element.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```tsx
|
|
174
|
+
* function App() {
|
|
175
|
+
* const psRef = usePixelStreamRef();
|
|
176
|
+
* return <PixelStream ref={psRef} admissionToken="tok_..." />;
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export declare function usePixelStreamRef(): RefObject<PixelStreamElement | null>;
|
|
181
|
+
/**
|
|
182
|
+
* Hook that tracks the `status` of a `<pixel-stream>` element reactively.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```tsx
|
|
186
|
+
* function App() {
|
|
187
|
+
* const psRef = usePixelStreamRef();
|
|
188
|
+
* const status = usePixelStreamStatus(psRef);
|
|
189
|
+
* return (
|
|
190
|
+
* <div>
|
|
191
|
+
* <PixelStream ref={psRef} admissionToken="tok_..." />
|
|
192
|
+
* <p>Status: {status}</p>
|
|
193
|
+
* </div>
|
|
194
|
+
* );
|
|
195
|
+
* }
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export declare function usePixelStreamStatus(ref: RefObject<PixelStreamElement | null>): PixelStreamStatus;
|
|
199
|
+
type PixelStreamHTMLAttributes = {
|
|
200
|
+
'admission-token'?: string;
|
|
201
|
+
'app-id'?: string;
|
|
202
|
+
'app-version'?: string;
|
|
203
|
+
'no-auto-connect'?: boolean | string;
|
|
204
|
+
'native-touch'?: boolean | string;
|
|
205
|
+
'pointer-lock'?: boolean | string;
|
|
206
|
+
'pointer-lock-release'?: boolean | string;
|
|
207
|
+
'suppress-browser-keys'?: boolean | string;
|
|
208
|
+
'enable-gamepad'?: boolean | string;
|
|
209
|
+
'enable-xr'?: boolean | string;
|
|
210
|
+
mute?: boolean | string;
|
|
211
|
+
volume?: number | string;
|
|
212
|
+
'rendezvous-preference'?: number | string;
|
|
213
|
+
'linger-preference'?: number | string;
|
|
214
|
+
'left-grace-period'?: number | string;
|
|
215
|
+
'api-endpoint'?: string;
|
|
216
|
+
'force-relay'?: boolean | string;
|
|
217
|
+
'queue-wait-tolerance'?: number | string;
|
|
218
|
+
'webrtc-negotiation-tolerance'?: number | string;
|
|
219
|
+
'reconnect-mode'?: 'none' | 'recover' | 'always';
|
|
220
|
+
'reconnect-attempts'?: number | string;
|
|
221
|
+
'reconnect-strategy'?: 'periodic' | 'exponential-backoff';
|
|
222
|
+
'reconnect-interval'?: number | string;
|
|
223
|
+
'disconnect-grace-ms'?: number | string;
|
|
224
|
+
'resize-mode'?: 'none' | 'auto' | 'pureweb' | 'pre-5.4' | '5.4+';
|
|
225
|
+
'dpr-cap'?: number | string;
|
|
226
|
+
'resolution-clamp'?: ResolutionClampName;
|
|
227
|
+
debug?: boolean | string;
|
|
228
|
+
controls?: boolean | string;
|
|
229
|
+
'swift-job-request'?: boolean | string;
|
|
230
|
+
'streamer-id'?: string;
|
|
231
|
+
lat?: number | string;
|
|
232
|
+
lng?: number | string;
|
|
233
|
+
'stream-quality'?: 'low-latency' | 'balanced' | 'quality';
|
|
234
|
+
'video-bitrate-min'?: number | string;
|
|
235
|
+
'video-bitrate-start'?: number | string;
|
|
236
|
+
'video-bitrate-max'?: number | string;
|
|
237
|
+
'log-level'?: 'error' | 'warn' | 'info' | 'debug';
|
|
238
|
+
class?: string;
|
|
239
|
+
className?: string;
|
|
240
|
+
id?: string;
|
|
241
|
+
style?: React.CSSProperties | string;
|
|
242
|
+
slot?: string;
|
|
243
|
+
children?: React.ReactNode;
|
|
244
|
+
ref?: React.Ref<PixelStreamElement>;
|
|
245
|
+
suppressHydrationWarning?: boolean;
|
|
246
|
+
};
|
|
247
|
+
declare global {
|
|
248
|
+
namespace JSX {
|
|
249
|
+
interface IntrinsicElements {
|
|
250
|
+
'pixel-stream': PixelStreamHTMLAttributes;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for the <pixel-stream> web component.
|
|
3
|
+
*
|
|
4
|
+
* SSR-safe: renders an empty <pixel-stream> tag on the server, hydrates
|
|
5
|
+
* on the client by lazily importing the web component definition.
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* - A <PixelStream> React component with typed props and event callbacks
|
|
9
|
+
* - JSX IntrinsicElements augmentation so bare <pixel-stream> also type-checks
|
|
10
|
+
* - Re-exports of key types from @interlucent/pixel-stream
|
|
11
|
+
*/
|
|
12
|
+
import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, } from 'react';
|
|
13
|
+
// ===== SSR guard =====
|
|
14
|
+
const isBrowser = typeof window !== 'undefined';
|
|
15
|
+
// Lazy web-component registration — called once on first mount
|
|
16
|
+
let _registered = false;
|
|
17
|
+
function ensureRegistered() {
|
|
18
|
+
if (_registered || !isBrowser)
|
|
19
|
+
return;
|
|
20
|
+
_registered = true;
|
|
21
|
+
// Dynamic import so Node.js never evaluates the web component at module scope
|
|
22
|
+
import('@interlucent/pixel-stream').catch(() => {
|
|
23
|
+
// Web component already registered or unavailable — not fatal
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// ===== Mapping from callback prop to DOM event name =====
|
|
27
|
+
const EVENT_MAP = {
|
|
28
|
+
onReady: 'ready',
|
|
29
|
+
onAdmissionTokenChange: 'admission-token-change',
|
|
30
|
+
onAppIdChange: 'app-id-change',
|
|
31
|
+
onAppVersionChange: 'app-version-change',
|
|
32
|
+
onAdmitted: 'admitted',
|
|
33
|
+
onAdmissionRevoked: 'admission-revoked',
|
|
34
|
+
onAdmissionError: 'admission-error',
|
|
35
|
+
onNoAutoConnectChange: 'no-auto-connect-change',
|
|
36
|
+
onSessionStateChange: 'session-state-change',
|
|
37
|
+
onSessionError: 'session-error',
|
|
38
|
+
onJobStateChange: 'job-state-change',
|
|
39
|
+
onStreamStateChange: 'stream-state-change',
|
|
40
|
+
onStreamResolutionChange: 'stream-resolution-change',
|
|
41
|
+
onStatusChange: 'status-change',
|
|
42
|
+
onRendezvousStarted: 'rendezvous-started',
|
|
43
|
+
onRendezvousTimeout: 'rendezvous-timeout',
|
|
44
|
+
onRendezvousCancelled: 'rendezvous-cancelled',
|
|
45
|
+
onLingerStarted: 'linger-started',
|
|
46
|
+
onLingerTimeout: 'linger-timeout',
|
|
47
|
+
onLingerCancelled: 'linger-cancelled',
|
|
48
|
+
onPeerConnected: 'peer-connected',
|
|
49
|
+
onPeerDisconnected: 'peer-disconnected',
|
|
50
|
+
onMuteChange: 'mute-change',
|
|
51
|
+
onVolumeChange: 'volume-change',
|
|
52
|
+
onPermissionChange: 'permission-change',
|
|
53
|
+
onRendezvousPreferenceChange: 'rendezvous-preference-change',
|
|
54
|
+
onLingerPreferenceChange: 'linger-preference-change',
|
|
55
|
+
onLeftGracePeriodChange: 'left-grace-period-change',
|
|
56
|
+
onApiEndpointChange: 'api-endpoint-change',
|
|
57
|
+
onResizeModeDetected: 'resize-mode-detected',
|
|
58
|
+
onXRStreamDetected: 'xr-stream-detected',
|
|
59
|
+
onLog: 'interlucent:log',
|
|
60
|
+
};
|
|
61
|
+
// ===== Mapping from React prop to HTML attribute =====
|
|
62
|
+
const ATTR_MAP = {
|
|
63
|
+
admissionToken: 'admission-token',
|
|
64
|
+
appId: 'app-id',
|
|
65
|
+
appVersion: 'app-version',
|
|
66
|
+
noAutoConnect: 'no-auto-connect',
|
|
67
|
+
nativeTouch: 'native-touch',
|
|
68
|
+
pointerLock: 'pointer-lock',
|
|
69
|
+
pointerLockRelease: 'pointer-lock-release',
|
|
70
|
+
suppressBrowserKeys: 'suppress-browser-keys',
|
|
71
|
+
enableGamepad: 'enable-gamepad',
|
|
72
|
+
enableXr: 'enable-xr',
|
|
73
|
+
forceRelay: 'force-relay',
|
|
74
|
+
queueWaitTolerance: 'queue-wait-tolerance',
|
|
75
|
+
webrtcNegotiationTolerance: 'webrtc-negotiation-tolerance',
|
|
76
|
+
reconnectMode: 'reconnect-mode',
|
|
77
|
+
reconnectAttempts: 'reconnect-attempts',
|
|
78
|
+
reconnectStrategy: 'reconnect-strategy',
|
|
79
|
+
reconnectInterval: 'reconnect-interval',
|
|
80
|
+
disconnectGraceMs: 'disconnect-grace-ms',
|
|
81
|
+
resizeMode: 'resize-mode',
|
|
82
|
+
dprCap: 'dpr-cap',
|
|
83
|
+
resolutionClamp: 'resolution-clamp',
|
|
84
|
+
swiftJobRequest: 'swift-job-request',
|
|
85
|
+
streamerId: 'streamer-id',
|
|
86
|
+
streamQuality: 'stream-quality',
|
|
87
|
+
videoBitrateMin: 'video-bitrate-min',
|
|
88
|
+
videoBitrateStart: 'video-bitrate-start',
|
|
89
|
+
videoBitrateMax: 'video-bitrate-max',
|
|
90
|
+
logLevel: 'log-level',
|
|
91
|
+
rendezvousPreference: 'rendezvous-preference',
|
|
92
|
+
lingerPreference: 'linger-preference',
|
|
93
|
+
leftGracePeriod: 'left-grace-period',
|
|
94
|
+
apiEndpoint: 'api-endpoint',
|
|
95
|
+
};
|
|
96
|
+
// Props that are boolean HTML attributes (presence = true, absence = false)
|
|
97
|
+
const BOOLEAN_ATTRS = new Set([
|
|
98
|
+
'noAutoConnect', 'nativeTouch', 'pointerLock', 'pointerLockRelease',
|
|
99
|
+
'suppressBrowserKeys', 'enableGamepad', 'enableXr', 'mute', 'debug',
|
|
100
|
+
'controls', 'forceRelay', 'swiftJobRequest',
|
|
101
|
+
]);
|
|
102
|
+
// All known attribute prop names (for removal tracking)
|
|
103
|
+
const ALL_ATTR_NAMES = new Set([
|
|
104
|
+
...Object.keys(ATTR_MAP),
|
|
105
|
+
'mute', 'volume', 'debug', 'controls', 'lat', 'lng',
|
|
106
|
+
]);
|
|
107
|
+
// Props handled separately (not attributes or events)
|
|
108
|
+
const SPECIAL_PROPS = new Set(['className', 'style', 'children']);
|
|
109
|
+
/**
|
|
110
|
+
* React component wrapping the `<pixel-stream>` web component.
|
|
111
|
+
*
|
|
112
|
+
* - SSR-safe: renders `<pixel-stream>` on the server (no crash), hydrates on client
|
|
113
|
+
* - Full typed props for all 51 HTML attributes
|
|
114
|
+
* - Event callbacks for all custom events (`onStatusChange`, `onReady`, etc.)
|
|
115
|
+
* - Ref forwarding — access the underlying element for imperative methods
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```tsx
|
|
119
|
+
* import { PixelStream } from '@interlucent/pixel-stream-react';
|
|
120
|
+
*
|
|
121
|
+
* function App() {
|
|
122
|
+
* const ref = useRef<PixelStreamElement>(null);
|
|
123
|
+
*
|
|
124
|
+
* return (
|
|
125
|
+
* <PixelStream
|
|
126
|
+
* ref={ref}
|
|
127
|
+
* admissionToken="tok_..."
|
|
128
|
+
* onStatusChange={(e) => console.log(e.detail.newStatus)}
|
|
129
|
+
* onReady={() => console.log('ready')}
|
|
130
|
+
* style={{ width: '100%', height: '100%' }}
|
|
131
|
+
* />
|
|
132
|
+
* );
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export const PixelStream = forwardRef(function PixelStream(props, ref) {
|
|
137
|
+
const elementRef = useRef(null);
|
|
138
|
+
const [mounted, setMounted] = useState(false);
|
|
139
|
+
// Expose the raw element to the parent ref
|
|
140
|
+
useImperativeHandle(ref, () => elementRef.current, [mounted]);
|
|
141
|
+
// Register the web component on first client mount
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
ensureRegistered();
|
|
144
|
+
setMounted(true);
|
|
145
|
+
}, []);
|
|
146
|
+
// Sync custom event listeners.
|
|
147
|
+
// We store the latest props in a ref so the effect doesn't need to
|
|
148
|
+
// teardown/re-add listeners on every render — only when the set of
|
|
149
|
+
// subscribed event names changes.
|
|
150
|
+
const propsRef = useRef(props);
|
|
151
|
+
propsRef.current = props;
|
|
152
|
+
// Compute which event names are currently subscribed
|
|
153
|
+
const activeEvents = Object.keys(EVENT_MAP)
|
|
154
|
+
.filter(k => typeof props[k] === 'function')
|
|
155
|
+
.join(',');
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const el = elementRef.current;
|
|
158
|
+
if (!el || !mounted)
|
|
159
|
+
return;
|
|
160
|
+
const listeners = [];
|
|
161
|
+
for (const [propName, eventName] of Object.entries(EVENT_MAP)) {
|
|
162
|
+
const handler = propsRef.current[propName];
|
|
163
|
+
if (typeof handler !== 'function')
|
|
164
|
+
continue;
|
|
165
|
+
// Wrap so we always call the latest prop version
|
|
166
|
+
const wrapped = (e) => {
|
|
167
|
+
const fn = propsRef.current[propName];
|
|
168
|
+
if (typeof fn === 'function')
|
|
169
|
+
fn(e);
|
|
170
|
+
};
|
|
171
|
+
el.addEventListener(eventName, wrapped);
|
|
172
|
+
listeners.push([eventName, wrapped]);
|
|
173
|
+
}
|
|
174
|
+
return () => {
|
|
175
|
+
for (const [eventName, handler] of listeners) {
|
|
176
|
+
el.removeEventListener(eventName, handler);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
180
|
+
}, [mounted, activeEvents]);
|
|
181
|
+
// Sync HTML attributes from props
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const el = elementRef.current;
|
|
184
|
+
if (!el || !mounted)
|
|
185
|
+
return;
|
|
186
|
+
// Build desired attribute state
|
|
187
|
+
const desired = new Map();
|
|
188
|
+
for (const [propName, value] of Object.entries(props)) {
|
|
189
|
+
if (SPECIAL_PROPS.has(propName) || propName in EVENT_MAP)
|
|
190
|
+
continue;
|
|
191
|
+
if (!ALL_ATTR_NAMES.has(propName))
|
|
192
|
+
continue;
|
|
193
|
+
const attrName = ATTR_MAP[propName] ?? propName;
|
|
194
|
+
if (BOOLEAN_ATTRS.has(propName)) {
|
|
195
|
+
if (value)
|
|
196
|
+
desired.set(attrName, '');
|
|
197
|
+
}
|
|
198
|
+
else if (value != null) {
|
|
199
|
+
desired.set(attrName, String(value));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Set desired attributes
|
|
203
|
+
for (const [attr, value] of desired) {
|
|
204
|
+
if (el.getAttribute(attr) !== value) {
|
|
205
|
+
el.setAttribute(attr, value);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Remove attributes that are no longer in props
|
|
209
|
+
const allKnownAttrNames = [
|
|
210
|
+
...Object.values(ATTR_MAP),
|
|
211
|
+
'mute', 'volume', 'debug', 'controls', 'lat', 'lng',
|
|
212
|
+
];
|
|
213
|
+
for (const attrName of allKnownAttrNames) {
|
|
214
|
+
if (!desired.has(attrName) && el.hasAttribute(attrName)) {
|
|
215
|
+
el.removeAttribute(attrName);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
// On the server, render a bare <pixel-stream> tag.
|
|
220
|
+
// On the client, also render <pixel-stream> and let effects handle attrs/events.
|
|
221
|
+
return React.createElement('pixel-stream', {
|
|
222
|
+
ref: elementRef,
|
|
223
|
+
class: props.className,
|
|
224
|
+
style: props.style
|
|
225
|
+
? Object.entries(props.style).reduce((css, [k, v]) => {
|
|
226
|
+
const kebab = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
|
|
227
|
+
return `${css}${kebab}:${v};`;
|
|
228
|
+
}, '')
|
|
229
|
+
: undefined,
|
|
230
|
+
// Suppress React hydration warnings for attributes set by effects
|
|
231
|
+
suppressHydrationWarning: true,
|
|
232
|
+
}, props.children);
|
|
233
|
+
});
|
|
234
|
+
// ===== usePixelStreamRef hook =====
|
|
235
|
+
/**
|
|
236
|
+
* Convenience hook providing a typed ref for the `<pixel-stream>` element.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```tsx
|
|
240
|
+
* function App() {
|
|
241
|
+
* const psRef = usePixelStreamRef();
|
|
242
|
+
* return <PixelStream ref={psRef} admissionToken="tok_..." />;
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export function usePixelStreamRef() {
|
|
247
|
+
return useRef(null);
|
|
248
|
+
}
|
|
249
|
+
// ===== usePixelStreamStatus hook =====
|
|
250
|
+
/**
|
|
251
|
+
* Hook that tracks the `status` of a `<pixel-stream>` element reactively.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```tsx
|
|
255
|
+
* function App() {
|
|
256
|
+
* const psRef = usePixelStreamRef();
|
|
257
|
+
* const status = usePixelStreamStatus(psRef);
|
|
258
|
+
* return (
|
|
259
|
+
* <div>
|
|
260
|
+
* <PixelStream ref={psRef} admissionToken="tok_..." />
|
|
261
|
+
* <p>Status: {status}</p>
|
|
262
|
+
* </div>
|
|
263
|
+
* );
|
|
264
|
+
* }
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
export function usePixelStreamStatus(ref) {
|
|
268
|
+
const [status, setStatus] = useState('idle');
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
const el = ref.current;
|
|
271
|
+
if (!el)
|
|
272
|
+
return;
|
|
273
|
+
// Read initial status
|
|
274
|
+
if (el.status)
|
|
275
|
+
setStatus(el.status);
|
|
276
|
+
const handler = (e) => {
|
|
277
|
+
setStatus(e.detail.newStatus);
|
|
278
|
+
};
|
|
279
|
+
el.addEventListener('status-change', handler);
|
|
280
|
+
return () => el.removeEventListener('status-change', handler);
|
|
281
|
+
}, [ref]);
|
|
282
|
+
return status;
|
|
283
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@interlucent/pixel-stream-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React bindings for the @interlucent/pixel-stream web component",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.build.json"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@interlucent/pixel-stream": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^18.0.0",
|
|
32
|
+
"react": "^18.0.0",
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"pixel-streaming",
|
|
37
|
+
"react",
|
|
38
|
+
"webcomponent",
|
|
39
|
+
"webrtc",
|
|
40
|
+
"interlucent"
|
|
41
|
+
],
|
|
42
|
+
"author": "Interlucent",
|
|
43
|
+
"license": "UNLICENSED",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|