@streamplace/components 0.7.19 → 0.7.25

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 (36) hide show
  1. package/dist/components/chat/chat-box.js +5 -0
  2. package/dist/components/chat/chat-message.js +5 -4
  3. package/dist/components/chat/chat.js +14 -4
  4. package/dist/components/chat/mod-view.js +19 -1
  5. package/dist/components/mobile-player/fullscreen.js +2 -0
  6. package/dist/components/mobile-player/ui/autoplay-button.js +68 -0
  7. package/dist/components/mobile-player/ui/index.js +1 -0
  8. package/dist/components/mobile-player/video.js +11 -1
  9. package/dist/components/mobile-player/webrtc-diagnostics.js +67 -13
  10. package/dist/lib/system-messages.js +1 -0
  11. package/dist/livestream-store/chat.js +25 -1
  12. package/dist/livestream-store/stream-key.js +1 -0
  13. package/dist/livestream-store/websocket-consumer.js +4 -1
  14. package/dist/player-store/player-provider.js +2 -1
  15. package/dist/player-store/player-store.js +2 -0
  16. package/dist/streamplace-store/stream.js +2 -0
  17. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  18. package/package.json +4 -4
  19. package/src/components/chat/chat-box.tsx +3 -0
  20. package/src/components/chat/chat-message.tsx +5 -4
  21. package/src/components/chat/chat.tsx +20 -4
  22. package/src/components/chat/mod-view.tsx +39 -5
  23. package/src/components/mobile-player/fullscreen.tsx +2 -0
  24. package/src/components/mobile-player/ui/autoplay-button.tsx +86 -0
  25. package/src/components/mobile-player/ui/index.ts +1 -0
  26. package/src/components/mobile-player/video.tsx +11 -1
  27. package/src/components/mobile-player/webrtc-diagnostics.tsx +73 -15
  28. package/src/lib/system-messages.ts +1 -0
  29. package/src/livestream-store/chat.tsx +24 -0
  30. package/src/livestream-store/stream-key.tsx +1 -0
  31. package/src/livestream-store/websocket-consumer.tsx +4 -1
  32. package/src/player-store/player-provider.tsx +2 -1
  33. package/src/player-store/player-state.tsx +6 -0
  34. package/src/player-store/player-store.tsx +4 -0
  35. package/src/streamplace-store/stream.tsx +2 -0
  36. package/tsconfig.tsbuildinfo +1 -1
@@ -10,6 +10,7 @@ import { usePDSAgent } from "../../streamplace-store/xrpc";
10
10
 
11
11
  import { Linking } from "react-native";
12
12
  import { ChatMessageViewHydrated } from "streamplace";
13
+ import { useDeleteChatMessage } from "../../livestream-store";
13
14
  import { useStreamplaceStore } from "../../streamplace-store";
14
15
  import {
15
16
  atoms,
@@ -172,11 +173,16 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
172
173
  >
173
174
  <Text color="primary">View user on {BSKY_FRONTEND_DOMAIN}</Text>
174
175
  </DropdownMenuItem>
175
- <ReportButton
176
- message={message}
177
- setReportModalOpen={setReportModalOpen}
178
- setReportSubject={setReportSubject}
179
- />
176
+ {message.author.did === agent?.did && (
177
+ <DeleteButton message={message} />
178
+ )}
179
+ {message.author.did !== agent?.did && (
180
+ <ReportButton
181
+ message={message}
182
+ setReportModalOpen={setReportModalOpen}
183
+ setReportSubject={setReportSubject}
184
+ />
185
+ )}
180
186
  </DropdownMenuGroup>
181
187
  </>
182
188
  )}
@@ -185,6 +191,34 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
185
191
  );
186
192
  });
187
193
 
194
+ export function DeleteButton({
195
+ message,
196
+ }: {
197
+ message: ChatMessageViewHydrated;
198
+ }) {
199
+ const deleteChatMessage = useDeleteChatMessage();
200
+ const [confirming, setConfirming] = useState(false);
201
+ const { onOpenChange } = useRootContext();
202
+ return (
203
+ <DropdownMenuItem
204
+ onPress={() => {
205
+ if (!message) return;
206
+ if (!confirming) {
207
+ setConfirming(true);
208
+ return;
209
+ }
210
+ deleteChatMessage(message.uri).then(() => {
211
+ onOpenChange?.(false);
212
+ });
213
+ }}
214
+ >
215
+ <Text color="destructive">
216
+ {confirming ? "Are you sure?" : "Delete message"}
217
+ </Text>
218
+ </DropdownMenuItem>
219
+ );
220
+ }
221
+
188
222
  export function ReportButton({
189
223
  message,
190
224
  setReportModalOpen,
@@ -16,12 +16,14 @@ export function Fullscreen(props: {
16
16
  const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
17
17
  const setFullscreen = usePlayerStore((x) => x.setFullscreen, playerId);
18
18
  const setSrc = usePlayerStore((x) => x.setSrc);
19
+ const setAutoplayFailed = usePlayerStore((x) => x.setAutoplayFailed);
19
20
 
20
21
  const divRef = useRef<RNView>(null);
21
22
  const videoRef = useRef<HTMLVideoElement | null>(null);
22
23
 
23
24
  useEffect(() => {
24
25
  setSrc(props.src);
26
+ setAutoplayFailed(false);
25
27
  }, [props.src]);
26
28
 
27
29
  useEffect(() => {
@@ -0,0 +1,86 @@
1
+ import { Play } from "lucide-react-native";
2
+ import { Pressable } from "react-native";
3
+ import { View, layout, usePlayerStore } from "../../..";
4
+ import { h, p, w } from "../../../ui";
5
+
6
+ export function AutoplayButton() {
7
+ const autoplayFailed = usePlayerStore((x) => x.autoplayFailed);
8
+ const setAutoplayFailed = usePlayerStore((x) => x.setAutoplayFailed);
9
+ const setMuted = usePlayerStore((x) => x.setMuted);
10
+ const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
11
+ const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
12
+ const videoRef = usePlayerStore((x) => x.videoRef);
13
+
14
+ const handlePlayButtonPress = () => {
15
+ if (videoRef && typeof videoRef === "object" && videoRef.current) {
16
+ videoRef.current
17
+ .play()
18
+ .then(() => {
19
+ setAutoplayFailed(false);
20
+ setUserInteraction();
21
+ })
22
+ .catch((err) => {
23
+ console.error("Manual play failed", err);
24
+ if (err.name === "NotAllowedError") {
25
+ setMuted(true);
26
+ videoRef.current!.muted = true;
27
+ videoRef
28
+ .current!.play()
29
+ .then(() => {
30
+ setAutoplayFailed(false);
31
+ setMuteWasForced(true);
32
+ setUserInteraction();
33
+ })
34
+ .catch((err) => {
35
+ console.error("Manual muted play also failed", err);
36
+ });
37
+ }
38
+ });
39
+ }
40
+ };
41
+
42
+ if (!autoplayFailed) return null;
43
+
44
+ return (
45
+ <View
46
+ style={[
47
+ layout.position.absolute,
48
+ layout.flex.center,
49
+ h.percent[100],
50
+ w.percent[100],
51
+ ]}
52
+ >
53
+ <Pressable
54
+ onPress={handlePlayButtonPress}
55
+ style={[
56
+ {
57
+ flexDirection: "column",
58
+ alignItems: "center",
59
+ justifyContent: "center",
60
+ gap: 8,
61
+ },
62
+ ]}
63
+ >
64
+ <View
65
+ style={[
66
+ p[4],
67
+ {
68
+ backgroundColor: "rgba(200,200,255, 0.1)",
69
+ borderRadius: 999,
70
+ borderWidth: 2,
71
+ borderColor: "rgba(200,200,255, 0.45)",
72
+ boxShadow: "0 0px 4px rgba(0, 0, 0, 1)",
73
+ shadowColor: "rgba(0, 0, 0, 1)",
74
+ },
75
+ ]}
76
+ >
77
+ <Play
78
+ size="48"
79
+ color="rgba(120,120,120,0.3)"
80
+ fill="rgba(200,200,255,1)"
81
+ />
82
+ </View>
83
+ </Pressable>
84
+ </View>
85
+ );
86
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./autoplay-button";
1
2
  export * from "./countdown";
2
3
  export * from "./input";
3
4
  export * from "./metrics";
@@ -154,6 +154,7 @@ const VideoElement = forwardRef<
154
154
  playerEvent(url, now.toISOString(), evType, {});
155
155
  };
156
156
  const [firstAttempt, setFirstAttempt] = useState(true);
157
+ const setAutoplayFailed = usePlayerStore((x) => x.setAutoplayFailed);
157
158
 
158
159
  const localVideoRef = props.videoRef ?? useRef<HTMLVideoElement | null>(null);
159
160
 
@@ -206,13 +207,22 @@ const VideoElement = forwardRef<
206
207
  })
207
208
  .catch((err) => {
208
209
  console.error("Muted play also failed", err);
210
+ setAutoplayFailed(true);
209
211
  });
210
212
  }
213
+ } else {
214
+ // For other errors (not NotAllowedError), also show play button
215
+ setAutoplayFailed(true);
211
216
  }
212
217
  });
213
218
  }
214
219
  };
215
220
 
221
+ const handlePlaying = (e) => {
222
+ setAutoplayFailed(false);
223
+ event("playing")(e);
224
+ };
225
+
216
226
  useEffect(() => {
217
227
  return () => {
218
228
  setStatus(PlayerStatus.START);
@@ -275,7 +285,7 @@ const VideoElement = forwardRef<
275
285
  onLoadStart={event("loadstart")}
276
286
  onPause={event("pause")}
277
287
  onPlay={event("play")}
278
- onPlaying={event("playing")}
288
+ onPlaying={handlePlaying}
279
289
  onRateChange={event("ratechange")}
280
290
  onSeeked={event("seeked")}
281
291
  onSeeking={event("seeking")}
@@ -7,6 +7,7 @@ export interface WebRTCDiagnostics {
7
7
  rtcSessionDescription: boolean;
8
8
  getUserMedia: boolean;
9
9
  getDisplayMedia: boolean;
10
+ isHwH264Supported: boolean;
10
11
  errors: string[];
11
12
  warnings: string[];
12
13
  }
@@ -19,6 +20,7 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
19
20
  rtcSessionDescription: false,
20
21
  getUserMedia: false,
21
22
  getDisplayMedia: false,
23
+ isHwH264Supported: false,
22
24
  errors: [],
23
25
  warnings: [],
24
26
  });
@@ -27,6 +29,23 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
27
29
  const errors: string[] = [];
28
30
  const warnings: string[] = [];
29
31
 
32
+ const checkH264Support = async (): Promise<boolean> => {
33
+ try {
34
+ const pc = new RTCPeerConnection();
35
+ const offer = await pc.createOffer();
36
+ pc.close();
37
+
38
+ if (offer.sdp) {
39
+ const h264Match = offer.sdp.search(/rtpmap:([0-9]+) H264/g);
40
+ return h264Match !== -1;
41
+ }
42
+ return false;
43
+ } catch (error) {
44
+ console.warn("Failed to check H.264 support:", error);
45
+ return false;
46
+ }
47
+ };
48
+
30
49
  // Check if we're in a browser environment
31
50
  if (typeof window === "undefined") {
32
51
  errors.push("Running in non-browser environment");
@@ -37,6 +56,7 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
37
56
  rtcSessionDescription: false,
38
57
  getUserMedia: false,
39
58
  getDisplayMedia: false,
59
+ isHwH264Supported: false,
40
60
  errors,
41
61
  warnings,
42
62
  });
@@ -105,22 +125,45 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
105
125
 
106
126
  const browserSupport = rtcPeerConnection && rtcSessionDescription;
107
127
 
108
- setDiagnostics({
109
- done: true,
110
- browserSupport,
111
- rtcPeerConnection,
112
- rtcSessionDescription,
113
- getUserMedia,
114
- getDisplayMedia,
115
- errors,
116
- warnings,
117
- });
128
+ // Check H.264 support asynchronously
129
+ if (rtcPeerConnection) {
130
+ checkH264Support().then((isHwH264Supported) => {
131
+ if (!isHwH264Supported) {
132
+ warnings.push(
133
+ "H.264 hardware acceleration is not supported\n In Firefox, try enabling 'media.webrtc.hw.h264.enabled' in about:config",
134
+ );
135
+ }
136
+ setDiagnostics({
137
+ done: true,
138
+ browserSupport,
139
+ rtcPeerConnection,
140
+ rtcSessionDescription,
141
+ getUserMedia,
142
+ getDisplayMedia,
143
+ isHwH264Supported,
144
+ errors,
145
+ warnings,
146
+ });
147
+ });
148
+ } else {
149
+ setDiagnostics({
150
+ done: true,
151
+ browserSupport,
152
+ rtcPeerConnection,
153
+ rtcSessionDescription,
154
+ getUserMedia,
155
+ getDisplayMedia,
156
+ isHwH264Supported: false,
157
+ errors,
158
+ warnings,
159
+ });
160
+ }
118
161
  }, []);
119
162
 
120
163
  return diagnostics;
121
164
  }
122
165
 
123
- export function logWebRTCDiagnostics() {
166
+ export async function logWebRTCDiagnostics() {
124
167
  console.group("WebRTC Diagnostics");
125
168
 
126
169
  // Log browser support
@@ -133,17 +176,32 @@ export function logWebRTCDiagnostics() {
133
176
  console.log("User Agent:", navigator.userAgent);
134
177
  console.log("Protocol:", location.protocol);
135
178
  console.log("Host:", location.hostname);
136
-
137
- // Test basic WebRTC functionality
179
+ console.groupEnd();
138
180
  if (window.RTCPeerConnection) {
139
181
  try {
140
182
  const pc = new RTCPeerConnection();
141
- console.log("RTCPeerConnection creation: Success");
183
+ // Check H.264 support
184
+ try {
185
+ const offer = await pc.createOffer({ offerToReceiveVideo: true });
186
+ const isHwH264Supported = offer.sdp
187
+ ? offer.sdp.search(/rtpmap:([0-9]+) H264/g) !== -1
188
+ : false;
189
+ console.group("WebRTC Peer Connection Test");
190
+ console.log("RTCPeerConnection creation: ✓ Success");
191
+ console.log(
192
+ "H.264 support:",
193
+ isHwH264Supported ? "✓ Supported" : "✗ Not supported",
194
+ );
195
+ } catch (error) {
196
+ console.group("WebRTC Peer Connection Test");
197
+ console.error("H.264 check failed:", error);
198
+ }
199
+
142
200
  pc.close();
143
201
  } catch (error) {
202
+ console.group("WebRTC Peer Connection Test");
144
203
  console.error("RTCPeerConnection creation: ✗ Failed", error);
145
204
  }
146
205
  }
147
-
148
206
  console.groupEnd();
149
207
  }
@@ -46,6 +46,7 @@ export const createSystemMessage = (
46
46
  indexedAt: now.toISOString(),
47
47
  chatProfile: {
48
48
  color: { red: 128, green: 128, blue: 128 }, // Gray color for system messages
49
+ $type: "place.stream.chat.profile",
49
50
  },
50
51
  };
51
52
  };
@@ -76,6 +76,7 @@ export const useCreateChatMessage = () => {
76
76
  await rt.detectFacets(pdsAgent);
77
77
 
78
78
  const record: PlaceStreamChatMessage.Record = {
79
+ $type: "place.stream.chat.message",
79
80
  text: msg.text,
80
81
  createdAt: new Date().toISOString(),
81
82
  streamer: streamerProfile.did,
@@ -119,6 +120,28 @@ export const useCreateChatMessage = () => {
119
120
  };
120
121
  };
121
122
 
123
+ export const useDeleteChatMessage = () => {
124
+ const pdsAgent = usePDSAgent();
125
+ if (!pdsAgent) {
126
+ throw new Error("No PDS agent found");
127
+ }
128
+ const userDID = useDID();
129
+ if (!userDID) {
130
+ throw new Error("No user DID found");
131
+ }
132
+ return async (uri: string) => {
133
+ const rkey = uri.split("/").pop();
134
+ if (!rkey) {
135
+ throw new Error("No rkey found");
136
+ }
137
+ return await pdsAgent.com.atproto.repo.deleteRecord({
138
+ repo: userDID,
139
+ collection: "place.stream.chat.message",
140
+ rkey: rkey,
141
+ });
142
+ };
143
+ };
144
+
122
145
  const buildSortedChatList = (
123
146
  chatIndex: { [key: string]: ChatMessageViewHydrated },
124
147
  existingChatList: ChatMessageViewHydrated[],
@@ -273,6 +296,7 @@ export const reduceChatIncremental = (
273
296
  processedMessage = {
274
297
  ...message,
275
298
  replyTo: {
299
+ $type: "place.stream.chat.defs#messageView",
276
300
  cid: parentMsg.cid,
277
301
  uri: parentMsg.uri,
278
302
  author: parentMsg.author,
@@ -75,6 +75,7 @@ export const useStreamKey = (): {
75
75
  }
76
76
 
77
77
  const record: PlaceStreamKey.Record = {
78
+ $type: "place.stream.key",
78
79
  signingKey: keypair.did(),
79
80
  createdAt: new Date().toISOString(),
80
81
  createdBy: "Streamplace on " + platform,
@@ -20,7 +20,7 @@ export const handleWebSocketMessages = (
20
20
  state: LivestreamState,
21
21
  messages: any[],
22
22
  ): LivestreamState => {
23
- for (const message of messages) {
23
+ for (let message of messages) {
24
24
  if (PlaceStreamLivestream.isLivestreamView(message)) {
25
25
  const newLivestream = message as LivestreamViewHydrated;
26
26
  const oldLivestream = state.livestream;
@@ -41,11 +41,13 @@ export const handleWebSocketMessages = (
41
41
  livestream: newLivestream,
42
42
  };
43
43
  } else if (PlaceStreamLivestream.isViewerCount(message)) {
44
+ message = message as PlaceStreamLivestream.ViewerCount;
44
45
  state = {
45
46
  ...state,
46
47
  viewers: message.count,
47
48
  };
48
49
  } else if (PlaceStreamChatDefs.isMessageView(message)) {
50
+ message = message as PlaceStreamChatDefs.MessageView;
49
51
  // Explicitly map MessageView to MessageViewHydrated
50
52
  const hydrated: ChatMessageViewHydrated = {
51
53
  uri: message.uri,
@@ -74,6 +76,7 @@ export const handleWebSocketMessages = (
74
76
  const block = message as PlaceStreamDefs.BlockView;
75
77
  state = reduceChat(state, [], [block], []);
76
78
  } else if (PlaceStreamDefs.isRenditions(message)) {
79
+ message = message as PlaceStreamDefs.Renditions;
77
80
  state = {
78
81
  ...state,
79
82
  renditions: message.renditions,
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "crypto";
1
2
  import React, { useCallback, useMemo, useState } from "react";
2
3
  import { StoreApi } from "zustand";
3
4
  import { PlayerContext } from "./context";
@@ -33,7 +34,7 @@ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
33
34
  );
34
35
 
35
36
  const createPlayer = useCallback((id?: string) => {
36
- const playerId = id || Math.random().toString(36).slice(8);
37
+ const playerId = id || randomUUID();
37
38
  const playerStore = makePlayerStore(playerId);
38
39
 
39
40
  setPlayers((prev) => ({
@@ -142,6 +142,12 @@ export interface PlayerState {
142
142
  /** Function to set the muteWasForced flag */
143
143
  setMuteWasForced: (muteWasForced: boolean) => void;
144
144
 
145
+ /** Flag indicating if autoplay failed and needs user interaction */
146
+ autoplayFailed: boolean;
147
+
148
+ /** Function to set the autoplayFailed flag */
149
+ setAutoplayFailed: (autoplayFailed: boolean) => void;
150
+
145
151
  /** Flag indicating if the player is embedded in another context */
146
152
  embedded: boolean;
147
153
 
@@ -99,6 +99,10 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
99
99
  setMuteWasForced: (muteWasForced: boolean) =>
100
100
  set(() => ({ muteWasForced })),
101
101
 
102
+ autoplayFailed: false,
103
+ setAutoplayFailed: (autoplayFailed: boolean) =>
104
+ set(() => ({ autoplayFailed })),
105
+
102
106
  embedded: false,
103
107
  setEmbedded: (embedded: boolean) => set(() => ({ embedded })),
104
108
 
@@ -248,6 +248,7 @@ export function useCreateStreamRecord() {
248
248
  }
249
249
 
250
250
  const record: PlaceStreamLivestream.Record = {
251
+ $type: "place.stream.livestream",
251
252
  title: title,
252
253
  url: finalUrl,
253
254
  createdAt: new Date().toISOString(),
@@ -313,6 +314,7 @@ export function useUpdateStreamRecord(customUrl: string | null = null) {
313
314
  }
314
315
 
315
316
  const record: PlaceStreamLivestream.Record = {
317
+ $type: "place.stream.livestream",
316
318
  title: title,
317
319
  url: finalUrl,
318
320
  createdAt: new Date().toISOString(),