sa2kit 1.6.34 → 1.6.36

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.
@@ -1,4 +1,4 @@
1
- import React3, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
1
+ import React3, { useState, useRef, useCallback, useMemo, useEffect } from 'react';
2
2
  import * as THREE2 from 'three';
3
3
 
4
4
  // src/mikuFireworks3D/components/MikuFireworks3D.tsx
@@ -71,7 +71,9 @@ function FireworksControlPanel({
71
71
  avatarUrl,
72
72
  onAvatarUrlChange,
73
73
  onLaunch,
74
- fps
74
+ fps,
75
+ realtimeConnected,
76
+ onlineCount
75
77
  }) {
76
78
  return /* @__PURE__ */ React3.createElement("div", { className: "rounded-xl border border-slate-600/40 bg-slate-900/70 p-3 text-slate-100 backdrop-blur-sm" }, /* @__PURE__ */ React3.createElement("div", { className: "mb-3 flex flex-wrap items-center gap-2" }, Object.keys(FIREWORK_KIND_LABELS).map((kind) => {
77
79
  const active = kind === selectedKind;
@@ -100,7 +102,7 @@ function FireworksControlPanel({
100
102
  checked: autoLaunchOnDanmaku,
101
103
  onChange: (event) => onAutoLaunchChange(event.target.checked)
102
104
  }
103
- ), "\u53D1\u9001\u5F39\u5E55\u540E\u81EA\u52A8\u653E\u70DF\u82B1"), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-slate-300" }, "FPS: ", fps)), selectedKind === "avatar" ? /* @__PURE__ */ React3.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React3.createElement(
105
+ ), "\u53D1\u9001\u5F39\u5E55\u540E\u81EA\u52A8\u653E\u70DF\u82B1"), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-slate-300" }, "FPS: ", fps), typeof realtimeConnected === "boolean" ? /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-slate-300" }, "\u5B9E\u65F6\u72B6\u6001: ", realtimeConnected ? "\u5DF2\u8FDE\u63A5" : "\u672A\u8FDE\u63A5", typeof onlineCount === "number" ? ` \xB7 \u5728\u7EBF ${onlineCount}` : "") : null), selectedKind === "avatar" ? /* @__PURE__ */ React3.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React3.createElement(
104
106
  "input",
105
107
  {
106
108
  type: "url",
@@ -113,12 +115,24 @@ function FireworksControlPanel({
113
115
  }
114
116
  function useDanmakuController(options) {
115
117
  const [items, setItems] = useState([]);
116
- const [cursor, setCursor] = useState(0);
118
+ const cursorRef = useRef(0);
117
119
  const removeItem = useCallback((id) => {
118
120
  setItems((prev) => prev.filter((item) => item.id !== id));
119
121
  }, []);
122
+ const addIncoming = useCallback((message) => {
123
+ setItems((prev) => {
124
+ const track = cursorRef.current % DANMAKU_TRACK_COUNT;
125
+ const item = {
126
+ ...message,
127
+ track,
128
+ durationMs: 8e3 + Math.floor(Math.random() * 2800)
129
+ };
130
+ return [...prev.slice(-40), item];
131
+ });
132
+ cursorRef.current += 1;
133
+ }, []);
120
134
  const send = useCallback(
121
- (text, color) => {
135
+ (text, color, sendOptions) => {
122
136
  const trimmed = text.trim();
123
137
  if (!trimmed) {
124
138
  return null;
@@ -134,31 +148,26 @@ function useDanmakuController(options) {
134
148
  color,
135
149
  timestamp: Date.now()
136
150
  };
137
- setItems((prev) => {
138
- const track = cursor % DANMAKU_TRACK_COUNT;
139
- const item = {
140
- ...message,
141
- track,
142
- durationMs: 8e3 + Math.floor(Math.random() * 2800)
143
- };
144
- return [...prev.slice(-40), item];
145
- });
146
- setCursor((prev) => prev + 1);
151
+ const optimistic = sendOptions?.optimistic ?? true;
152
+ if (optimistic) {
153
+ addIncoming(message);
154
+ }
147
155
  options?.onSend?.(message);
148
156
  return {
149
157
  message,
150
158
  launchKind
151
159
  };
152
160
  },
153
- [cursor, options]
161
+ [addIncoming, options]
154
162
  );
155
163
  return useMemo(
156
164
  () => ({
157
165
  items,
158
166
  send,
167
+ addIncoming,
159
168
  removeItem
160
169
  }),
161
- [items, removeItem, send]
170
+ [addIncoming, items, removeItem, send]
162
171
  );
163
172
  }
164
173
  function parseCommand(text) {
@@ -672,6 +681,302 @@ function useFireworksEngine(options) {
672
681
  return api;
673
682
  }
674
683
 
684
+ // src/mikuFireworks3D/client/WebSocketTransport.ts
685
+ var WebSocketTransport = class {
686
+ constructor(config, callbacks) {
687
+ this.socket = null;
688
+ this.reconnectTimer = null;
689
+ this.isManualClose = false;
690
+ this.pendingQueue = [];
691
+ this.config = config;
692
+ this.callbacks = callbacks || {};
693
+ this.state = {
694
+ connected: false,
695
+ joined: false,
696
+ onlineCount: 0,
697
+ roomId: config.roomId
698
+ };
699
+ }
700
+ connect() {
701
+ if (this.socket && (this.socket.readyState === window.WebSocket.OPEN || this.socket.readyState === window.WebSocket.CONNECTING)) {
702
+ return;
703
+ }
704
+ this.isManualClose = false;
705
+ try {
706
+ this.socket = this.config.protocols ? new window.WebSocket(this.config.serverUrl, this.config.protocols) : new window.WebSocket(this.config.serverUrl);
707
+ } catch {
708
+ this.callbacks.onError?.(new Error("Failed to create WebSocket connection."));
709
+ return;
710
+ }
711
+ this.socket.onopen = () => {
712
+ this.updateState({ connected: true, joined: false });
713
+ this.send({
714
+ type: "join",
715
+ roomId: this.config.roomId,
716
+ user: this.config.user
717
+ });
718
+ };
719
+ this.socket.onmessage = (event) => {
720
+ const parsed = parseServerMessage(event.data);
721
+ if (!parsed) {
722
+ return;
723
+ }
724
+ this.handleServerMessage(parsed);
725
+ };
726
+ this.socket.onerror = () => {
727
+ this.callbacks.onError?.(new Error("WebSocket transport error."));
728
+ };
729
+ this.socket.onclose = () => {
730
+ this.updateState({ connected: false, joined: false });
731
+ this.scheduleReconnect();
732
+ };
733
+ }
734
+ disconnect() {
735
+ this.isManualClose = true;
736
+ this.pendingQueue.length = 0;
737
+ if (this.reconnectTimer != null) {
738
+ window.clearTimeout(this.reconnectTimer);
739
+ this.reconnectTimer = null;
740
+ }
741
+ if (!this.socket) {
742
+ return;
743
+ }
744
+ this.send({ type: "leave" });
745
+ this.socket.close();
746
+ this.socket = null;
747
+ }
748
+ sendDanmaku(payload) {
749
+ this.send({
750
+ type: "danmaku.send",
751
+ payload
752
+ });
753
+ }
754
+ sendFirework(payload) {
755
+ this.send({
756
+ type: "firework.launch",
757
+ payload
758
+ });
759
+ }
760
+ getState() {
761
+ return this.state;
762
+ }
763
+ send(message) {
764
+ if (!this.socket || this.socket.readyState !== window.WebSocket.OPEN) {
765
+ if (message.type === "danmaku.send" || message.type === "firework.launch") {
766
+ this.pendingQueue.push(message);
767
+ }
768
+ return;
769
+ }
770
+ if ((message.type === "danmaku.send" || message.type === "firework.launch") && !this.state.joined) {
771
+ this.pendingQueue.push(message);
772
+ return;
773
+ }
774
+ this.socket.send(JSON.stringify(message));
775
+ }
776
+ updateState(partial) {
777
+ this.state = {
778
+ ...this.state,
779
+ ...partial
780
+ };
781
+ this.callbacks.onStateChange?.(this.state);
782
+ }
783
+ handleServerMessage(message) {
784
+ if (message.type === "joined") {
785
+ this.updateState({ roomId: message.roomId, onlineCount: message.onlineCount, joined: true });
786
+ this.flushPendingQueue();
787
+ return;
788
+ }
789
+ if (message.type === "room.user_joined" || message.type === "room.user_left") {
790
+ this.updateState({ onlineCount: message.onlineCount, roomId: message.roomId });
791
+ return;
792
+ }
793
+ if (message.type === "room.snapshot") {
794
+ this.updateState({ roomId: message.roomId, onlineCount: message.users.length });
795
+ this.callbacks.onSnapshot?.(message);
796
+ return;
797
+ }
798
+ if (message.type === "danmaku.broadcast") {
799
+ this.callbacks.onDanmakuBroadcast?.(message.event);
800
+ return;
801
+ }
802
+ if (message.type === "firework.broadcast") {
803
+ this.callbacks.onFireworkBroadcast?.(message.event);
804
+ return;
805
+ }
806
+ if (message.type === "error") {
807
+ this.callbacks.onError?.(new Error(`${message.code}: ${message.message}`));
808
+ }
809
+ }
810
+ scheduleReconnect() {
811
+ const reconnect = this.config.reconnect ?? true;
812
+ if (this.isManualClose || !reconnect) {
813
+ return;
814
+ }
815
+ if (this.reconnectTimer != null) {
816
+ return;
817
+ }
818
+ const delay = this.config.reconnectIntervalMs ?? 1500;
819
+ this.reconnectTimer = window.setTimeout(() => {
820
+ this.reconnectTimer = null;
821
+ this.connect();
822
+ }, delay);
823
+ }
824
+ flushPendingQueue() {
825
+ if (!this.socket || this.socket.readyState !== window.WebSocket.OPEN || !this.state.joined) {
826
+ return;
827
+ }
828
+ while (this.pendingQueue.length > 0) {
829
+ const message = this.pendingQueue.shift();
830
+ if (!message) {
831
+ break;
832
+ }
833
+ this.socket.send(JSON.stringify(message));
834
+ }
835
+ }
836
+ };
837
+ function parseServerMessage(raw) {
838
+ if (typeof raw !== "string") {
839
+ return null;
840
+ }
841
+ try {
842
+ return JSON.parse(raw);
843
+ } catch {
844
+ return null;
845
+ }
846
+ }
847
+
848
+ // src/mikuFireworks3D/hooks/useFireworksRealtime.ts
849
+ function useFireworksRealtime(options) {
850
+ const { config, enabled, onDanmakuBroadcast, onFireworkBroadcast, onSnapshot, onError, onStateChange } = options;
851
+ const transportRef = useRef(null);
852
+ const callbackRef = useRef({
853
+ onDanmakuBroadcast,
854
+ onFireworkBroadcast,
855
+ onSnapshot,
856
+ onError,
857
+ onStateChange
858
+ });
859
+ const serverUrl = config?.serverUrl ?? "";
860
+ const roomId = config?.roomId ?? "";
861
+ const userId = config?.user.userId ?? "";
862
+ const nickname = config?.user.nickname ?? "";
863
+ const avatarUrl = config?.user.avatarUrl ?? "";
864
+ const reconnect = config?.reconnect ?? true;
865
+ const reconnectIntervalMs = config?.reconnectIntervalMs ?? 1500;
866
+ const [state, setState] = useState({
867
+ connected: false,
868
+ joined: false,
869
+ onlineCount: 0,
870
+ roomId
871
+ });
872
+ useEffect(() => {
873
+ callbackRef.current = {
874
+ onDanmakuBroadcast,
875
+ onFireworkBroadcast,
876
+ onSnapshot,
877
+ onError,
878
+ onStateChange
879
+ };
880
+ }, [onDanmakuBroadcast, onError, onFireworkBroadcast, onSnapshot, onStateChange]);
881
+ const normalizedConfig = useMemo(() => {
882
+ if (!serverUrl || !roomId || !userId) {
883
+ return void 0;
884
+ }
885
+ const protocols = Array.isArray(config?.protocols) ? [...config.protocols] : config?.protocols;
886
+ return {
887
+ serverUrl,
888
+ roomId,
889
+ user: {
890
+ userId,
891
+ nickname: nickname || void 0,
892
+ avatarUrl: avatarUrl || void 0
893
+ },
894
+ protocols,
895
+ reconnect,
896
+ reconnectIntervalMs
897
+ };
898
+ }, [avatarUrl, config?.protocols, nickname, reconnect, reconnectIntervalMs, roomId, serverUrl, userId]);
899
+ useEffect(() => {
900
+ if (!enabled || !normalizedConfig) {
901
+ transportRef.current?.disconnect();
902
+ transportRef.current = null;
903
+ setState({
904
+ connected: false,
905
+ joined: false,
906
+ onlineCount: 0,
907
+ roomId: normalizedConfig?.roomId
908
+ });
909
+ return;
910
+ }
911
+ const transport = new WebSocketTransport(normalizedConfig, {
912
+ onStateChange: (nextState) => {
913
+ setState(nextState);
914
+ callbackRef.current.onStateChange?.(nextState);
915
+ },
916
+ onDanmakuBroadcast: (event) => {
917
+ callbackRef.current.onDanmakuBroadcast?.({
918
+ id: event.id,
919
+ text: event.text,
920
+ color: event.color,
921
+ kind: event.kind,
922
+ userId: event.user.userId,
923
+ timestamp: event.timestamp
924
+ });
925
+ },
926
+ onFireworkBroadcast: (event) => {
927
+ callbackRef.current.onFireworkBroadcast?.({
928
+ id: event.id,
929
+ payload: event.payload,
930
+ userId: event.user.userId,
931
+ timestamp: event.timestamp
932
+ });
933
+ },
934
+ onSnapshot: (snapshot) => {
935
+ callbackRef.current.onSnapshot?.({
936
+ roomId: snapshot.roomId,
937
+ danmakuHistory: snapshot.danmakuHistory.map((item) => ({
938
+ id: item.id,
939
+ text: item.text,
940
+ color: item.color,
941
+ kind: item.kind,
942
+ userId: item.user.userId,
943
+ timestamp: item.timestamp
944
+ })),
945
+ fireworkHistory: snapshot.fireworkHistory.map((item) => ({
946
+ id: item.id,
947
+ payload: item.payload,
948
+ userId: item.user.userId,
949
+ timestamp: item.timestamp
950
+ }))
951
+ });
952
+ },
953
+ onError: (error) => {
954
+ callbackRef.current.onError?.(error);
955
+ }
956
+ });
957
+ transport.connect();
958
+ transportRef.current = transport;
959
+ return () => {
960
+ transport.disconnect();
961
+ if (transportRef.current === transport) {
962
+ transportRef.current = null;
963
+ }
964
+ };
965
+ }, [enabled, normalizedConfig]);
966
+ return useMemo(
967
+ () => ({
968
+ state,
969
+ sendDanmaku: (payload) => {
970
+ transportRef.current?.sendDanmaku(payload);
971
+ },
972
+ sendFirework: (payload) => {
973
+ transportRef.current?.sendFirework(payload);
974
+ }
975
+ }),
976
+ [state]
977
+ );
978
+ }
979
+
675
980
  // src/mikuFireworks3D/components/MikuFireworks3D.tsx
676
981
  function MikuFireworks3D({
677
982
  width = "100%",
@@ -685,11 +990,15 @@ function MikuFireworks3D({
685
990
  onLaunch,
686
991
  onDanmakuSend,
687
992
  onError,
688
- onFpsReport
993
+ onFpsReport,
994
+ onRealtimeStateChange,
995
+ realtime
689
996
  }) {
690
997
  const [selectedKind, setSelectedKind] = useState(defaultKind);
691
998
  const [avatarUrl, setAvatarUrl] = useState(defaultAvatarUrl);
692
999
  const [autoLaunch, setAutoLaunch] = useState(autoLaunchOnDanmaku);
1000
+ const seenDanmakuIdsRef = useRef(/* @__PURE__ */ new Set());
1001
+ const seenFireworkIdsRef = useRef(/* @__PURE__ */ new Set());
693
1002
  const { containerRef, canvasRef, launch, fps } = useFireworksEngine({
694
1003
  maxParticles,
695
1004
  maxActiveFireworks,
@@ -697,22 +1006,93 @@ function MikuFireworks3D({
697
1006
  onError,
698
1007
  onFpsReport
699
1008
  });
700
- const { items, send, removeItem } = useDanmakuController({
1009
+ const { items, send, addIncoming, removeItem } = useDanmakuController({
701
1010
  onSend: onDanmakuSend
702
1011
  });
1012
+ const realtimeEnabled = Boolean(realtime && (realtime.enabled ?? true));
1013
+ const realtimeApi = useFireworksRealtime({
1014
+ enabled: realtimeEnabled,
1015
+ config: realtime,
1016
+ onStateChange: onRealtimeStateChange,
1017
+ onError,
1018
+ onDanmakuBroadcast: (event) => {
1019
+ if (seenDanmakuIdsRef.current.has(event.id)) {
1020
+ return;
1021
+ }
1022
+ seenDanmakuIdsRef.current.add(event.id);
1023
+ addIncoming({
1024
+ id: event.id,
1025
+ userId: event.userId,
1026
+ text: event.text,
1027
+ color: event.color,
1028
+ timestamp: event.timestamp
1029
+ });
1030
+ },
1031
+ onFireworkBroadcast: (event) => {
1032
+ if (seenFireworkIdsRef.current.has(event.id)) {
1033
+ return;
1034
+ }
1035
+ seenFireworkIdsRef.current.add(event.id);
1036
+ launch(event.payload);
1037
+ },
1038
+ onSnapshot: (snapshot) => {
1039
+ for (const danmaku of snapshot.danmakuHistory) {
1040
+ if (seenDanmakuIdsRef.current.has(danmaku.id)) {
1041
+ continue;
1042
+ }
1043
+ seenDanmakuIdsRef.current.add(danmaku.id);
1044
+ addIncoming({
1045
+ id: danmaku.id,
1046
+ userId: danmaku.userId,
1047
+ text: danmaku.text,
1048
+ color: danmaku.color,
1049
+ timestamp: danmaku.timestamp
1050
+ });
1051
+ }
1052
+ for (const firework of snapshot.fireworkHistory) {
1053
+ if (seenFireworkIdsRef.current.has(firework.id)) {
1054
+ continue;
1055
+ }
1056
+ seenFireworkIdsRef.current.add(firework.id);
1057
+ launch(firework.payload);
1058
+ }
1059
+ }
1060
+ });
703
1061
  const handleLaunch = (kind) => {
704
- launch({
1062
+ const payload = {
705
1063
  kind,
706
1064
  avatarUrl: kind === "avatar" ? avatarUrl || void 0 : void 0
707
- });
1065
+ };
1066
+ if (realtimeEnabled && realtimeApi.state.connected && realtimeApi.state.joined) {
1067
+ realtimeApi.sendFirework(payload);
1068
+ return;
1069
+ }
1070
+ launch(payload);
708
1071
  };
709
1072
  const handleSendDanmaku = (text) => {
710
- const result = send(text);
1073
+ const result = send(text, void 0, {
1074
+ optimistic: !(realtimeEnabled && realtimeApi.state.connected && realtimeApi.state.joined)
1075
+ });
711
1076
  if (!result) {
712
1077
  return;
713
1078
  }
714
1079
  const launchKind = result.launchKind ?? selectedKind;
715
- if (autoLaunch) {
1080
+ if (realtimeEnabled && realtimeApi.state.connected && realtimeApi.state.joined) {
1081
+ realtimeApi.sendDanmaku({
1082
+ text: result.message.text,
1083
+ color: result.message.color,
1084
+ kind: result.launchKind
1085
+ });
1086
+ if (autoLaunch) {
1087
+ realtimeApi.sendFirework({
1088
+ kind: launchKind,
1089
+ avatarUrl: launchKind === "avatar" ? avatarUrl || void 0 : void 0,
1090
+ message: result.message
1091
+ });
1092
+ }
1093
+ return;
1094
+ }
1095
+ if (autoLaunch || result.launchKind) {
716
1096
  launch({
717
1097
  kind: launchKind,
718
1098
  avatarUrl: launchKind === "avatar" ? avatarUrl || void 0 : void 0,
@@ -751,7 +1131,9 @@ function MikuFireworks3D({
751
1131
  avatarUrl,
752
1132
  onAvatarUrlChange: setAvatarUrl,
753
1133
  onLaunch: () => handleLaunch(selectedKind),
754
- fps
1134
+ fps,
1135
+ realtimeConnected: realtimeEnabled ? realtimeApi.state.connected : void 0,
1136
+ onlineCount: realtimeEnabled ? realtimeApi.state.onlineCount : void 0
755
1137
  }
756
1138
  ), /* @__PURE__ */ React3.createElement(DanmakuPanel, { onSend: handleSendDanmaku }), /* @__PURE__ */ React3.createElement("style", null, `
757
1139
  @keyframes sa2kit-danmaku-move {
@@ -767,6 +1149,6 @@ function MikuFireworks3D({
767
1149
  `));
768
1150
  }
769
1151
 
770
- export { DANMAKU_MAX_LENGTH, DANMAKU_TRACK_COUNT, DEFAULT_MAX_ACTIVE_FIREWORKS, DEFAULT_MAX_PARTICLES, DanmakuPanel, FIREWORK_KIND_LABELS, FireworksCanvas, FireworksControlPanel, MIKU_PALETTE, MikuFireworks3D, NORMAL_PALETTE, useDanmakuController, useFireworksEngine };
771
- //# sourceMappingURL=chunk-HQ7VHIEK.mjs.map
772
- //# sourceMappingURL=chunk-HQ7VHIEK.mjs.map
1152
+ export { DANMAKU_MAX_LENGTH, DANMAKU_TRACK_COUNT, DEFAULT_MAX_ACTIVE_FIREWORKS, DEFAULT_MAX_PARTICLES, DanmakuPanel, FIREWORK_KIND_LABELS, FireworksCanvas, FireworksControlPanel, MIKU_PALETTE, MikuFireworks3D, NORMAL_PALETTE, WebSocketTransport, useDanmakuController, useFireworksEngine, useFireworksRealtime };
1153
+ //# sourceMappingURL=chunk-55FBYGRK.mjs.map
1154
+ //# sourceMappingURL=chunk-55FBYGRK.mjs.map