@streamplace/components 0.0.1 → 0.7.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/LICENSE +18 -0
- package/README.md +35 -0
- package/dist/components/chat/chat-box.js +109 -0
- package/dist/components/chat/chat-message.js +76 -0
- package/dist/components/chat/chat.js +56 -0
- package/dist/components/chat/mention-suggestions.js +39 -0
- package/dist/components/chat/mod-view.js +33 -0
- package/dist/components/mobile-player/fullscreen.js +69 -0
- package/dist/components/mobile-player/fullscreen.native.js +151 -0
- package/dist/components/mobile-player/player.js +103 -0
- package/dist/components/mobile-player/props.js +1 -0
- package/dist/components/mobile-player/shared.js +51 -0
- package/dist/components/mobile-player/ui/countdown.js +79 -0
- package/dist/components/mobile-player/ui/index.js +5 -0
- package/dist/components/mobile-player/ui/input.js +38 -0
- package/dist/components/mobile-player/ui/metrics.js +40 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
- package/dist/components/mobile-player/use-webrtc.js +232 -0
- package/dist/components/mobile-player/video.js +375 -0
- package/dist/components/mobile-player/video.native.js +238 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
- package/dist/components/mobile-player/webrtc-primitives.js +25 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
- package/dist/components/ui/button.js +220 -0
- package/dist/components/ui/dialog.js +203 -0
- package/dist/components/ui/dropdown.js +148 -0
- package/dist/components/ui/icons.js +22 -0
- package/dist/components/ui/index.js +22 -0
- package/dist/components/ui/input.js +202 -0
- package/dist/components/ui/loader.js +7 -0
- package/dist/components/ui/primitives/button.js +121 -0
- package/dist/components/ui/primitives/input.js +202 -0
- package/dist/components/ui/primitives/modal.js +203 -0
- package/dist/components/ui/primitives/text.js +286 -0
- package/dist/components/ui/resizeable.js +101 -0
- package/dist/components/ui/text.js +175 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/toast.js +129 -0
- package/dist/components/ui/view.js +250 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useAvatars.js +32 -0
- package/dist/hooks/useCameraToggle.js +9 -0
- package/dist/hooks/useKeyboard.js +33 -0
- package/dist/hooks/useKeyboardSlide.js +11 -0
- package/dist/hooks/useLivestreamInfo.js +62 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
- package/dist/hooks/usePlayerDimensions.js +19 -0
- package/dist/hooks/useSegmentTiming.js +62 -0
- package/dist/index.js +16 -0
- package/dist/lib/facet.js +88 -0
- package/dist/lib/theme/atoms.js +620 -0
- package/dist/lib/theme/atoms.types.js +5 -0
- package/dist/lib/theme/index.js +9 -0
- package/dist/lib/theme/theme.js +248 -0
- package/dist/lib/theme/tokens.js +383 -0
- package/dist/lib/utils.js +94 -0
- package/dist/livestream-provider/index.js +25 -0
- package/dist/livestream-provider/websocket.js +41 -0
- package/dist/livestream-store/chat.js +186 -0
- package/dist/livestream-store/context.js +2 -0
- package/dist/livestream-store/index.js +4 -0
- package/dist/livestream-store/livestream-state.js +1 -0
- package/dist/livestream-store/livestream-store.js +42 -0
- package/dist/livestream-store/stream-key.js +115 -0
- package/dist/livestream-store/websocket-consumer.js +55 -0
- package/dist/player-store/context.js +2 -0
- package/dist/player-store/index.js +6 -0
- package/dist/player-store/player-provider.js +52 -0
- package/dist/player-store/player-state.js +22 -0
- package/dist/player-store/player-store.js +159 -0
- package/dist/player-store/single-player-provider.js +109 -0
- package/dist/streamplace-provider/context.js +2 -0
- package/dist/streamplace-provider/index.js +16 -0
- package/dist/streamplace-provider/poller.js +46 -0
- package/dist/streamplace-provider/xrpc.js +0 -0
- package/dist/streamplace-store/block.js +23 -0
- package/dist/streamplace-store/index.js +3 -0
- package/dist/streamplace-store/stream.js +193 -0
- package/dist/streamplace-store/streamplace-store.js +37 -0
- package/dist/streamplace-store/user.js +47 -0
- package/dist/streamplace-store/xrpc.js +12 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +50 -8
- package/src/components/chat/chat-box.tsx +195 -0
- package/src/components/chat/chat-message.tsx +192 -0
- package/src/components/chat/chat.tsx +128 -0
- package/src/components/chat/mention-suggestions.tsx +71 -0
- package/src/components/chat/mod-view.tsx +118 -0
- package/src/components/mobile-player/fullscreen.native.tsx +193 -0
- package/src/components/mobile-player/fullscreen.tsx +79 -0
- package/src/components/mobile-player/player.tsx +134 -0
- package/src/components/mobile-player/props.tsx +11 -0
- package/src/components/mobile-player/shared.tsx +56 -0
- package/src/components/mobile-player/ui/countdown.tsx +119 -0
- package/src/components/mobile-player/ui/index.ts +5 -0
- package/src/components/mobile-player/ui/input.tsx +85 -0
- package/src/components/mobile-player/ui/metrics.tsx +69 -0
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
- package/src/components/mobile-player/use-webrtc.tsx +282 -0
- package/src/components/mobile-player/video.native.tsx +360 -0
- package/src/components/mobile-player/video.tsx +557 -0
- package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
- package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
- package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
- package/src/components/ui/button.tsx +309 -0
- package/src/components/ui/dialog.tsx +376 -0
- package/src/components/ui/dropdown.tsx +399 -0
- package/src/components/ui/icons.tsx +50 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +350 -0
- package/src/components/ui/loader.tsx +9 -0
- package/src/components/ui/primitives/button.tsx +292 -0
- package/src/components/ui/primitives/input.tsx +422 -0
- package/src/components/ui/primitives/modal.tsx +421 -0
- package/src/components/ui/primitives/text.tsx +499 -0
- package/src/components/ui/resizeable.tsx +169 -0
- package/src/components/ui/text.tsx +330 -0
- package/src/components/ui/textarea.tsx +34 -0
- package/src/components/ui/toast.tsx +203 -0
- package/src/components/ui/view.tsx +344 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAvatars.tsx +44 -0
- package/src/hooks/useCameraToggle.ts +12 -0
- package/src/hooks/useKeyboard.tsx +41 -0
- package/src/hooks/useKeyboardSlide.ts +12 -0
- package/src/hooks/useLivestreamInfo.ts +67 -0
- package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
- package/src/hooks/usePlayerDimensions.ts +23 -0
- package/src/hooks/useSegmentTiming.tsx +88 -0
- package/src/index.tsx +27 -0
- package/src/lib/facet.ts +131 -0
- package/src/lib/theme/atoms.ts +760 -0
- package/src/lib/theme/atoms.types.ts +258 -0
- package/src/lib/theme/index.ts +48 -0
- package/src/lib/theme/theme.tsx +436 -0
- package/src/lib/theme/tokens.ts +409 -0
- package/src/lib/utils.ts +132 -0
- package/src/livestream-provider/index.tsx +48 -0
- package/src/livestream-provider/websocket.tsx +47 -0
- package/src/livestream-store/chat.tsx +261 -0
- package/src/livestream-store/context.tsx +10 -0
- package/src/livestream-store/index.tsx +4 -0
- package/src/livestream-store/livestream-state.tsx +21 -0
- package/src/livestream-store/livestream-store.tsx +59 -0
- package/src/livestream-store/stream-key.tsx +124 -0
- package/src/livestream-store/websocket-consumer.tsx +62 -0
- package/src/player-store/context.tsx +11 -0
- package/src/player-store/index.tsx +6 -0
- package/src/player-store/player-provider.tsx +89 -0
- package/src/player-store/player-state.tsx +187 -0
- package/src/player-store/player-store.tsx +239 -0
- package/src/player-store/single-player-provider.tsx +181 -0
- package/src/streamplace-provider/context.tsx +10 -0
- package/src/streamplace-provider/index.tsx +32 -0
- package/src/streamplace-provider/poller.tsx +55 -0
- package/src/streamplace-provider/xrpc.tsx +0 -0
- package/src/streamplace-store/block.tsx +29 -0
- package/src/streamplace-store/index.tsx +3 -0
- package/src/streamplace-store/stream.tsx +262 -0
- package/src/streamplace-store/streamplace-store.tsx +89 -0
- package/src/streamplace-store/user.tsx +57 -0
- package/src/streamplace-store/xrpc.tsx +15 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import Hls from "hls.js";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
IngestMediaSource,
|
|
5
|
+
PlayerProtocol,
|
|
6
|
+
PlayerStatus,
|
|
7
|
+
usePlayerStore,
|
|
8
|
+
useStreamplaceStore,
|
|
9
|
+
} from "../..";
|
|
10
|
+
import { borderRadius, colors, mt, p } from "../../lib/theme/atoms";
|
|
11
|
+
import { Text, View } from "../ui/index";
|
|
12
|
+
import { srcToUrl } from "./shared";
|
|
13
|
+
import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
|
|
14
|
+
import {
|
|
15
|
+
logWebRTCDiagnostics,
|
|
16
|
+
useWebRTCDiagnostics,
|
|
17
|
+
} from "./webrtc-diagnostics";
|
|
18
|
+
import { checkWebRTCSupport } from "./webrtc-primitives";
|
|
19
|
+
|
|
20
|
+
function assignVideoRef(
|
|
21
|
+
ref:
|
|
22
|
+
| React.MutableRefObject<HTMLVideoElement | null>
|
|
23
|
+
| ((instance: HTMLVideoElement | null) => void)
|
|
24
|
+
| null
|
|
25
|
+
| undefined,
|
|
26
|
+
instance: HTMLVideoElement | null,
|
|
27
|
+
) {
|
|
28
|
+
if (!ref) return;
|
|
29
|
+
if (typeof ref === "function") ref(instance);
|
|
30
|
+
else ref.current = instance;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type VideoProps = {
|
|
34
|
+
url: string;
|
|
35
|
+
videoRef?: React.RefObject<HTMLVideoElement>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function useVideoDimensions(videoRef: React.RefObject<HTMLVideoElement>) {
|
|
39
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!videoRef.current) return;
|
|
43
|
+
|
|
44
|
+
function updateSize() {
|
|
45
|
+
setDimensions({
|
|
46
|
+
width: videoRef.current?.videoWidth || 0,
|
|
47
|
+
height: videoRef.current?.videoHeight || 0,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
updateSize();
|
|
52
|
+
|
|
53
|
+
const observer = new window.ResizeObserver(updateSize);
|
|
54
|
+
observer.observe(videoRef.current);
|
|
55
|
+
|
|
56
|
+
videoRef.current.addEventListener("loadedmetadata", updateSize);
|
|
57
|
+
videoRef.current.addEventListener("resize", updateSize);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
observer.disconnect();
|
|
61
|
+
videoRef.current?.removeEventListener("loadedmetadata", updateSize);
|
|
62
|
+
videoRef.current?.removeEventListener("resize", updateSize);
|
|
63
|
+
};
|
|
64
|
+
}, [videoRef, videoRef.current]);
|
|
65
|
+
|
|
66
|
+
return dimensions;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function WebVideo() {
|
|
70
|
+
const inProto = usePlayerStore((x) => x.protocol);
|
|
71
|
+
const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null);
|
|
72
|
+
const selectedRendition = usePlayerStore((x) => x.selectedRendition);
|
|
73
|
+
const src = usePlayerStore((x) => x.src);
|
|
74
|
+
const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
|
|
75
|
+
const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
|
|
76
|
+
const { url, protocol } = srcToUrl({ src: src, selectedRendition }, inProto);
|
|
77
|
+
|
|
78
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
79
|
+
const dimensions = useVideoDimensions(videoRef);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (videoRef.current) {
|
|
83
|
+
setPlayerWidth(dimensions.width);
|
|
84
|
+
setPlayerHeight(dimensions.height);
|
|
85
|
+
}
|
|
86
|
+
}, [dimensions, setPlayerWidth, setPlayerHeight]);
|
|
87
|
+
|
|
88
|
+
const playerProps = { url, videoRef };
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
{isIngesting ? (
|
|
93
|
+
<WebcamIngestPlayer {...playerProps} />
|
|
94
|
+
) : protocol === PlayerProtocol.PROGRESSIVE_MP4 ? (
|
|
95
|
+
<ProgressiveMP4Player {...playerProps} />
|
|
96
|
+
) : protocol === PlayerProtocol.PROGRESSIVE_WEBM ? (
|
|
97
|
+
<ProgressiveWebMPlayer {...playerProps} />
|
|
98
|
+
) : protocol === PlayerProtocol.HLS ? (
|
|
99
|
+
<HLSPlayer {...playerProps} />
|
|
100
|
+
) : protocol === PlayerProtocol.WEBRTC ? (
|
|
101
|
+
<WebRTCPlayer {...playerProps} />
|
|
102
|
+
) : (
|
|
103
|
+
(() => {
|
|
104
|
+
throw new Error(`unknown playback protocol ${inProto}`);
|
|
105
|
+
})()
|
|
106
|
+
)}
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const updateEvents = {
|
|
112
|
+
playing: true,
|
|
113
|
+
waiting: true,
|
|
114
|
+
stalled: true,
|
|
115
|
+
pause: true,
|
|
116
|
+
suspend: true,
|
|
117
|
+
mute: true,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const VideoElement = forwardRef<
|
|
121
|
+
HTMLVideoElement,
|
|
122
|
+
VideoProps & { videoRef?: React.RefObject<HTMLVideoElement> }
|
|
123
|
+
>((props, ref) => {
|
|
124
|
+
const x = usePlayerStore((x) => x);
|
|
125
|
+
const url = useStreamplaceStore((x) => x.url);
|
|
126
|
+
const playerEvent = usePlayerStore((x) => x.playerEvent);
|
|
127
|
+
const setMuted = usePlayerStore((x) => x.setMuted);
|
|
128
|
+
const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
|
|
129
|
+
const muted = usePlayerStore((x) => x.muted);
|
|
130
|
+
const ingest = usePlayerStore((x) => x.ingestConnectionState !== null);
|
|
131
|
+
const volume = usePlayerStore((x) => x.volume);
|
|
132
|
+
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
133
|
+
const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
|
|
134
|
+
const setVideoRef = usePlayerStore((x) => x.setVideoRef);
|
|
135
|
+
|
|
136
|
+
const event = (evType) => (e) => {
|
|
137
|
+
console.log(evType);
|
|
138
|
+
const now = new Date();
|
|
139
|
+
if (updateEvents[evType]) {
|
|
140
|
+
x.setStatus(evType);
|
|
141
|
+
}
|
|
142
|
+
console.log("Sending", evType, "status to", url);
|
|
143
|
+
playerEvent(url, now.toISOString(), evType, {});
|
|
144
|
+
};
|
|
145
|
+
const [firstAttempt, setFirstAttempt] = useState(true);
|
|
146
|
+
|
|
147
|
+
const localVideoRef = props.videoRef ?? useRef<HTMLVideoElement | null>(null);
|
|
148
|
+
|
|
149
|
+
const canPlayThrough = (e) => {
|
|
150
|
+
event("canplaythrough")(e);
|
|
151
|
+
if (firstAttempt && localVideoRef.current) {
|
|
152
|
+
setFirstAttempt(false);
|
|
153
|
+
localVideoRef.current.play().catch((err) => {
|
|
154
|
+
if (err.name === "NotAllowedError") {
|
|
155
|
+
if (localVideoRef.current) {
|
|
156
|
+
setMuted(true);
|
|
157
|
+
localVideoRef.current.muted = true;
|
|
158
|
+
localVideoRef.current
|
|
159
|
+
.play()
|
|
160
|
+
.then(() => {
|
|
161
|
+
console.warn("Browser forced video to start muted");
|
|
162
|
+
setMuteWasForced(true);
|
|
163
|
+
})
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
console.error("error playing video", err);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
return () => {
|
|
175
|
+
setStatus(PlayerStatus.START);
|
|
176
|
+
};
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (localVideoRef.current) {
|
|
181
|
+
localVideoRef.current.volume = volume;
|
|
182
|
+
console.log("Setting volume to", volume);
|
|
183
|
+
}
|
|
184
|
+
}, [volume]);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
console.log(localVideoRef.current?.width, localVideoRef.current?.height);
|
|
188
|
+
setVideoRef(localVideoRef);
|
|
189
|
+
}, [setVideoRef, localVideoRef]);
|
|
190
|
+
|
|
191
|
+
const handleVideoRef = (videoElement: HTMLVideoElement | null) => {
|
|
192
|
+
if (typeof ref === "function") {
|
|
193
|
+
ref(videoElement);
|
|
194
|
+
} else if (ref) {
|
|
195
|
+
(ref as React.MutableRefObject<HTMLVideoElement | null>).current =
|
|
196
|
+
videoElement;
|
|
197
|
+
}
|
|
198
|
+
// if (localVideoRef && typeof localVideoRef !== "function") {
|
|
199
|
+
// localVideoRef.current = videoElement;
|
|
200
|
+
// }
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<video
|
|
205
|
+
autoPlay={true}
|
|
206
|
+
playsInline={true}
|
|
207
|
+
ref={handleVideoRef}
|
|
208
|
+
controls={false}
|
|
209
|
+
src={ingest ? undefined : props.url}
|
|
210
|
+
muted={muted}
|
|
211
|
+
crossOrigin="anonymous"
|
|
212
|
+
onMouseMove={setUserInteraction}
|
|
213
|
+
onClick={setUserInteraction}
|
|
214
|
+
onAbort={event("abort")}
|
|
215
|
+
onCanPlay={event("canplay")}
|
|
216
|
+
onCanPlayThrough={canPlayThrough}
|
|
217
|
+
onEmptied={event("emptied")}
|
|
218
|
+
onEncrypted={event("encrypted")}
|
|
219
|
+
onEnded={event("ended")}
|
|
220
|
+
onError={event("error")}
|
|
221
|
+
onLoadedData={event("loadeddata")}
|
|
222
|
+
onLoadedMetadata={event("loadedmetadata")}
|
|
223
|
+
onLoadStart={event("loadstart")}
|
|
224
|
+
onPause={event("pause")}
|
|
225
|
+
onPlay={event("play")}
|
|
226
|
+
onPlaying={event("playing")}
|
|
227
|
+
onRateChange={event("ratechange")}
|
|
228
|
+
onSeeked={event("seeked")}
|
|
229
|
+
onSeeking={event("seeking")}
|
|
230
|
+
onStalled={event("stalled")}
|
|
231
|
+
onSuspend={event("suspend")}
|
|
232
|
+
onVolumeChange={event("volumechange")}
|
|
233
|
+
onWaiting={event("waiting")}
|
|
234
|
+
style={{
|
|
235
|
+
objectFit: "contain",
|
|
236
|
+
backgroundColor: "transparent",
|
|
237
|
+
width: "100%",
|
|
238
|
+
height: "100%",
|
|
239
|
+
transform: ingest ? "scaleX(-1)" : undefined,
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
export function ProgressiveMP4Player(props: VideoProps) {
|
|
246
|
+
return <VideoElement {...props} />;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function ProgressiveWebMPlayer(props: VideoProps) {
|
|
250
|
+
return <VideoElement {...props} />;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function HLSPlayer(props: VideoProps) {
|
|
254
|
+
const localRef = useRef<HTMLVideoElement | null>(null);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (!localRef.current) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (Hls.isSupported()) {
|
|
261
|
+
var hls = new Hls({ maxAudioFramesDrift: 20 });
|
|
262
|
+
hls.loadSource(props.url);
|
|
263
|
+
try {
|
|
264
|
+
hls.attachMedia(localRef.current);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error("error on attachMedia");
|
|
267
|
+
hls.stopLoad();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
271
|
+
if (!localRef.current) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
localRef.current.play();
|
|
275
|
+
});
|
|
276
|
+
return () => {
|
|
277
|
+
hls.stopLoad();
|
|
278
|
+
};
|
|
279
|
+
} else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) {
|
|
280
|
+
localRef.current.src = props.url;
|
|
281
|
+
localRef.current.addEventListener("canplay", () => {
|
|
282
|
+
if (!localRef.current) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
localRef.current.play();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}, [props.url]);
|
|
289
|
+
return <VideoElement {...props} ref={localRef} />;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function WebRTCPlayer(
|
|
293
|
+
props: VideoProps & { videoRef?: React.RefObject<HTMLVideoElement> },
|
|
294
|
+
) {
|
|
295
|
+
const [webrtcError, setWebrtcError] = useState<string | null>(null);
|
|
296
|
+
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
297
|
+
const setProtocol = usePlayerStore((x) => x.setProtocol);
|
|
298
|
+
const diagnostics = useWebRTCDiagnostics();
|
|
299
|
+
// Check WebRTC compatibility on component mount
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
try {
|
|
302
|
+
checkWebRTCSupport();
|
|
303
|
+
console.log("WebRTC Player - Browser compatibility check passed");
|
|
304
|
+
logWebRTCDiagnostics();
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error("WebRTC Player - Compatibility error:", error.message);
|
|
307
|
+
setWebrtcError(error.message);
|
|
308
|
+
setStatus(PlayerStatus.START);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
// Monitor diagnostics for errors
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (!diagnostics.browserSupport && diagnostics.errors.length > 0) {
|
|
316
|
+
setWebrtcError(diagnostics.errors.join(", "));
|
|
317
|
+
}
|
|
318
|
+
}, [diagnostics]);
|
|
319
|
+
|
|
320
|
+
if (!diagnostics.done) return <></>;
|
|
321
|
+
|
|
322
|
+
if (webrtcError) {
|
|
323
|
+
setProtocol(PlayerProtocol.HLS);
|
|
324
|
+
return (
|
|
325
|
+
<View backgroundColor="#111">
|
|
326
|
+
<View>
|
|
327
|
+
<View>
|
|
328
|
+
<Text>WebRTC Not Supported</Text>
|
|
329
|
+
</View>
|
|
330
|
+
<Text>{webrtcError}</Text>
|
|
331
|
+
{diagnostics.errors.length > 0 && (
|
|
332
|
+
<View>
|
|
333
|
+
<Text>Technical Details:</Text>
|
|
334
|
+
{diagnostics.errors.map((error, index) => (
|
|
335
|
+
<Text key={index}>• {error}</Text>
|
|
336
|
+
))}
|
|
337
|
+
</View>
|
|
338
|
+
)}
|
|
339
|
+
<Text>
|
|
340
|
+
• To use WebRTC, you may need to disable any blocking extensions or
|
|
341
|
+
update your browser.
|
|
342
|
+
</Text>
|
|
343
|
+
<Text style={[mt[2]]}>Switching to HLS...</Text>
|
|
344
|
+
</View>
|
|
345
|
+
</View>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return <WebRTCPlayerInner url={props.url} videoRef={props.videoRef} />;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function WebRTCPlayerInner({
|
|
352
|
+
videoRef,
|
|
353
|
+
url,
|
|
354
|
+
width,
|
|
355
|
+
height,
|
|
356
|
+
}: {
|
|
357
|
+
videoRef?: React.RefObject<HTMLVideoElement>;
|
|
358
|
+
url: string;
|
|
359
|
+
width?: string | number;
|
|
360
|
+
height?: string | number;
|
|
361
|
+
}) {
|
|
362
|
+
const [connectionStatus, setConnectionStatus] =
|
|
363
|
+
useState<string>("initializing");
|
|
364
|
+
|
|
365
|
+
const status = usePlayerStore((x) => x.status);
|
|
366
|
+
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
367
|
+
|
|
368
|
+
const playerEvent = usePlayerStore((x) => x.playerEvent);
|
|
369
|
+
const spurl = useStreamplaceStore((x) => x.url);
|
|
370
|
+
|
|
371
|
+
const [mediaStream, stuck] = useWebRTC(url);
|
|
372
|
+
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (stuck) {
|
|
375
|
+
setConnectionStatus("connection-failed");
|
|
376
|
+
} else if (mediaStream) {
|
|
377
|
+
setConnectionStatus("connected");
|
|
378
|
+
} else {
|
|
379
|
+
setConnectionStatus("connecting");
|
|
380
|
+
}
|
|
381
|
+
}, [url, mediaStream, stuck, status]);
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (stuck && status === PlayerStatus.PLAYING) {
|
|
385
|
+
setStatus(PlayerStatus.STALLED);
|
|
386
|
+
}
|
|
387
|
+
if (!stuck && mediaStream) {
|
|
388
|
+
setStatus(PlayerStatus.PLAYING);
|
|
389
|
+
}
|
|
390
|
+
}, [stuck, status, mediaStream]);
|
|
391
|
+
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (!mediaStream) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const evt = (evType) => (e) => {
|
|
397
|
+
console.log("webrtc event", evType);
|
|
398
|
+
playerEvent(spurl, new Date().toISOString(), evType, {});
|
|
399
|
+
};
|
|
400
|
+
const active = evt("active");
|
|
401
|
+
const inactive = evt("inactive");
|
|
402
|
+
const ended = evt("ended");
|
|
403
|
+
const mute = evt("mute");
|
|
404
|
+
const unmute = evt("playing");
|
|
405
|
+
|
|
406
|
+
mediaStream.addEventListener("active", active);
|
|
407
|
+
mediaStream.addEventListener("inactive", inactive);
|
|
408
|
+
mediaStream.addEventListener("ended", ended);
|
|
409
|
+
for (const track of mediaStream.getTracks()) {
|
|
410
|
+
track.addEventListener("ended", ended);
|
|
411
|
+
track.addEventListener("mute", mute);
|
|
412
|
+
track.addEventListener("unmute", unmute);
|
|
413
|
+
}
|
|
414
|
+
return () => {
|
|
415
|
+
for (const track of mediaStream.getTracks()) {
|
|
416
|
+
track.removeEventListener("ended", ended);
|
|
417
|
+
track.removeEventListener("mute", mute);
|
|
418
|
+
track.removeEventListener("unmute", unmute);
|
|
419
|
+
}
|
|
420
|
+
mediaStream.removeEventListener("active", active);
|
|
421
|
+
mediaStream.removeEventListener("inactive", inactive);
|
|
422
|
+
mediaStream.removeEventListener("ended", ended);
|
|
423
|
+
};
|
|
424
|
+
}, [mediaStream]);
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (!videoRef || !videoRef.current) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
videoRef.current.srcObject = mediaStream;
|
|
431
|
+
}, [mediaStream]);
|
|
432
|
+
|
|
433
|
+
if (!mediaStream) {
|
|
434
|
+
return (
|
|
435
|
+
<View
|
|
436
|
+
backgroundColor="#111"
|
|
437
|
+
style={{ minWidth: "100%", minHeight: "100%" }}
|
|
438
|
+
>
|
|
439
|
+
<View
|
|
440
|
+
backgroundColor={colors.primary[800]}
|
|
441
|
+
style={{ borderRadius: borderRadius.md }}
|
|
442
|
+
>
|
|
443
|
+
<View>
|
|
444
|
+
<Text>Connecting...</Text>
|
|
445
|
+
</View>
|
|
446
|
+
<Text>Establishing WebRTC connection ({connectionStatus})</Text>
|
|
447
|
+
</View>
|
|
448
|
+
</View>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return <VideoElement url={url} ref={videoRef} />;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function WebcamIngestPlayer(props: VideoProps) {
|
|
455
|
+
const ingestStarting = usePlayerStore((x) => x.ingestStarting);
|
|
456
|
+
const ingestMediaSource = usePlayerStore((x) => x.ingestMediaSource);
|
|
457
|
+
const ingestAutoStart = usePlayerStore((x) => x.ingestAutoStart);
|
|
458
|
+
|
|
459
|
+
const [error, setError] = useState<Error | null>(null);
|
|
460
|
+
|
|
461
|
+
let streamKey = null;
|
|
462
|
+
|
|
463
|
+
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
|
464
|
+
null,
|
|
465
|
+
);
|
|
466
|
+
const handleRef = useCallback((node: HTMLVideoElement | null) => {
|
|
467
|
+
if (node) {
|
|
468
|
+
setVideoElement(node);
|
|
469
|
+
}
|
|
470
|
+
}, []);
|
|
471
|
+
|
|
472
|
+
const url = useStreamplaceStore((x) => x.url);
|
|
473
|
+
const [localMediaStream, setLocalMediaStream] = useState<MediaStream | null>(
|
|
474
|
+
null,
|
|
475
|
+
);
|
|
476
|
+
// we assign a stream key in the webrtcingest hook
|
|
477
|
+
const [remoteMediaStream, setRemoteMediaStream] = useWebRTCIngest({
|
|
478
|
+
endpoint: `${url}/api/ingest/webrtc`,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (ingestMediaSource === IngestMediaSource.DISPLAY) {
|
|
483
|
+
navigator.mediaDevices
|
|
484
|
+
.getDisplayMedia({
|
|
485
|
+
audio: true,
|
|
486
|
+
video: true,
|
|
487
|
+
})
|
|
488
|
+
.then((stream) => {
|
|
489
|
+
setLocalMediaStream(stream);
|
|
490
|
+
})
|
|
491
|
+
.catch((e) => {
|
|
492
|
+
console.error("error getting display media", e);
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
navigator.mediaDevices
|
|
496
|
+
.getUserMedia({
|
|
497
|
+
audio: true,
|
|
498
|
+
video: {
|
|
499
|
+
width: { min: 200, ideal: 1080, max: 2160 },
|
|
500
|
+
height: { min: 200, ideal: 1920, max: 3840 },
|
|
501
|
+
},
|
|
502
|
+
})
|
|
503
|
+
.then((stream) => {
|
|
504
|
+
setLocalMediaStream(stream);
|
|
505
|
+
})
|
|
506
|
+
.catch((e) => {
|
|
507
|
+
console.error("error getting user media", e.name);
|
|
508
|
+
if (e.name == "NotAllowedError") {
|
|
509
|
+
setError(
|
|
510
|
+
new Error(
|
|
511
|
+
"Unable to access video! Please allow it in your browser settings.",
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}, [ingestMediaSource]);
|
|
518
|
+
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
if (!ingestStarting && !ingestAutoStart) {
|
|
521
|
+
setRemoteMediaStream(null);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (!localMediaStream) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
setRemoteMediaStream(localMediaStream);
|
|
528
|
+
}, [localMediaStream, ingestStarting, ingestAutoStart]);
|
|
529
|
+
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
if (!videoElement) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (!localMediaStream) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
videoElement.srcObject = localMediaStream;
|
|
538
|
+
}, [videoElement, localMediaStream]);
|
|
539
|
+
|
|
540
|
+
if (error) {
|
|
541
|
+
return (
|
|
542
|
+
<View
|
|
543
|
+
backgroundColor={colors.destructive[900]}
|
|
544
|
+
style={[p[4], { borderRadius: borderRadius.md }]}
|
|
545
|
+
>
|
|
546
|
+
<View>
|
|
547
|
+
<Text size="xl" weight="extrabold">
|
|
548
|
+
Error encountered!
|
|
549
|
+
</Text>
|
|
550
|
+
</View>
|
|
551
|
+
<Text>{error.message}</Text>
|
|
552
|
+
</View>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return <VideoElement {...props} ref={handleRef} />;
|
|
557
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface WebRTCDiagnostics {
|
|
4
|
+
done: boolean;
|
|
5
|
+
browserSupport: boolean;
|
|
6
|
+
rtcPeerConnection: boolean;
|
|
7
|
+
rtcSessionDescription: boolean;
|
|
8
|
+
getUserMedia: boolean;
|
|
9
|
+
getDisplayMedia: boolean;
|
|
10
|
+
errors: string[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useWebRTCDiagnostics(): WebRTCDiagnostics {
|
|
15
|
+
const [diagnostics, setDiagnostics] = useState<WebRTCDiagnostics>({
|
|
16
|
+
done: false,
|
|
17
|
+
browserSupport: false,
|
|
18
|
+
rtcPeerConnection: false,
|
|
19
|
+
rtcSessionDescription: false,
|
|
20
|
+
getUserMedia: false,
|
|
21
|
+
getDisplayMedia: false,
|
|
22
|
+
errors: [],
|
|
23
|
+
warnings: [],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const errors: string[] = [];
|
|
28
|
+
const warnings: string[] = [];
|
|
29
|
+
|
|
30
|
+
// Check if we're in a browser environment
|
|
31
|
+
if (typeof window === "undefined") {
|
|
32
|
+
errors.push("Running in non-browser environment");
|
|
33
|
+
setDiagnostics({
|
|
34
|
+
done: false,
|
|
35
|
+
browserSupport: false,
|
|
36
|
+
rtcPeerConnection: false,
|
|
37
|
+
rtcSessionDescription: false,
|
|
38
|
+
getUserMedia: false,
|
|
39
|
+
getDisplayMedia: false,
|
|
40
|
+
errors,
|
|
41
|
+
warnings,
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check RTCPeerConnection support
|
|
47
|
+
const rtcPeerConnection = !!(
|
|
48
|
+
window.RTCPeerConnection ||
|
|
49
|
+
(window as any).webkitRTCPeerConnection ||
|
|
50
|
+
(window as any).mozRTCPeerConnection
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!rtcPeerConnection) {
|
|
54
|
+
errors.push("RTCPeerConnection is not supported");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check RTCSessionDescription support
|
|
58
|
+
const rtcSessionDescription = !!(
|
|
59
|
+
window.RTCSessionDescription ||
|
|
60
|
+
(window as any).webkitRTCSessionDescription ||
|
|
61
|
+
(window as any).mozRTCSessionDescription
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!rtcSessionDescription) {
|
|
65
|
+
errors.push("RTCSessionDescription is not supported");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check getUserMedia support
|
|
69
|
+
const getUserMedia = !!(
|
|
70
|
+
navigator.mediaDevices?.getUserMedia ||
|
|
71
|
+
(navigator as any).getUserMedia ||
|
|
72
|
+
(navigator as any).webkitGetUserMedia ||
|
|
73
|
+
(navigator as any).mozGetUserMedia
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!getUserMedia) {
|
|
77
|
+
warnings.push(
|
|
78
|
+
"getUserMedia is not supported - webcam features unavailable",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check getDisplayMedia support
|
|
83
|
+
const getDisplayMedia = !!navigator.mediaDevices?.getDisplayMedia;
|
|
84
|
+
|
|
85
|
+
if (!getDisplayMedia) {
|
|
86
|
+
warnings.push(
|
|
87
|
+
"getDisplayMedia is not supported - screen sharing unavailable",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if running over HTTPS (required for some WebRTC features)
|
|
92
|
+
if (
|
|
93
|
+
location.protocol !== "https:" &&
|
|
94
|
+
location.hostname !== "localhost" &&
|
|
95
|
+
location.hostname !== "127.0.0.1"
|
|
96
|
+
) {
|
|
97
|
+
warnings.push("WebRTC features may be limited over HTTP connections");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check browser-specific issues
|
|
101
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
102
|
+
if (userAgent.includes("safari") && !userAgent.includes("chrome")) {
|
|
103
|
+
warnings.push("Safari may have limited WebRTC codec support");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const browserSupport = rtcPeerConnection && rtcSessionDescription;
|
|
107
|
+
|
|
108
|
+
setDiagnostics({
|
|
109
|
+
done: true,
|
|
110
|
+
browserSupport,
|
|
111
|
+
rtcPeerConnection,
|
|
112
|
+
rtcSessionDescription,
|
|
113
|
+
getUserMedia,
|
|
114
|
+
getDisplayMedia,
|
|
115
|
+
errors,
|
|
116
|
+
warnings,
|
|
117
|
+
});
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
return diagnostics;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function logWebRTCDiagnostics() {
|
|
124
|
+
console.group("WebRTC Diagnostics");
|
|
125
|
+
|
|
126
|
+
// Log browser support
|
|
127
|
+
console.log("RTCPeerConnection:", !!window.RTCPeerConnection);
|
|
128
|
+
console.log("RTCSessionDescription:", !!window.RTCSessionDescription);
|
|
129
|
+
console.log("getUserMedia:", !!navigator.mediaDevices?.getUserMedia);
|
|
130
|
+
console.log("getDisplayMedia:", !!navigator.mediaDevices?.getDisplayMedia);
|
|
131
|
+
|
|
132
|
+
// Log browser info
|
|
133
|
+
console.log("User Agent:", navigator.userAgent);
|
|
134
|
+
console.log("Protocol:", location.protocol);
|
|
135
|
+
console.log("Host:", location.hostname);
|
|
136
|
+
|
|
137
|
+
// Test basic WebRTC functionality
|
|
138
|
+
if (window.RTCPeerConnection) {
|
|
139
|
+
try {
|
|
140
|
+
const pc = new RTCPeerConnection();
|
|
141
|
+
console.log("RTCPeerConnection creation: ✓ Success");
|
|
142
|
+
pc.close();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error("RTCPeerConnection creation: ✗ Failed", error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.groupEnd();
|
|
149
|
+
}
|