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.
@@ -96,7 +96,9 @@ function FireworksControlPanel({
96
96
  avatarUrl,
97
97
  onAvatarUrlChange,
98
98
  onLaunch,
99
- fps
99
+ fps,
100
+ realtimeConnected,
101
+ onlineCount
100
102
  }) {
101
103
  return /* @__PURE__ */ React3__default.default.createElement("div", { className: "rounded-xl border border-slate-600/40 bg-slate-900/70 p-3 text-slate-100 backdrop-blur-sm" }, /* @__PURE__ */ React3__default.default.createElement("div", { className: "mb-3 flex flex-wrap items-center gap-2" }, Object.keys(FIREWORK_KIND_LABELS).map((kind) => {
102
104
  const active = kind === selectedKind;
@@ -125,7 +127,7 @@ function FireworksControlPanel({
125
127
  checked: autoLaunchOnDanmaku,
126
128
  onChange: (event) => onAutoLaunchChange(event.target.checked)
127
129
  }
128
- ), "\u53D1\u9001\u5F39\u5E55\u540E\u81EA\u52A8\u653E\u70DF\u82B1"), /* @__PURE__ */ React3__default.default.createElement("div", { className: "text-sm text-slate-300" }, "FPS: ", fps)), selectedKind === "avatar" ? /* @__PURE__ */ React3__default.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React3__default.default.createElement(
130
+ ), "\u53D1\u9001\u5F39\u5E55\u540E\u81EA\u52A8\u653E\u70DF\u82B1"), /* @__PURE__ */ React3__default.default.createElement("div", { className: "text-sm text-slate-300" }, "FPS: ", fps), typeof realtimeConnected === "boolean" ? /* @__PURE__ */ React3__default.default.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__default.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React3__default.default.createElement(
129
131
  "input",
130
132
  {
131
133
  type: "url",
@@ -138,12 +140,24 @@ function FireworksControlPanel({
138
140
  }
139
141
  function useDanmakuController(options) {
140
142
  const [items, setItems] = React3.useState([]);
141
- const [cursor, setCursor] = React3.useState(0);
143
+ const cursorRef = React3.useRef(0);
142
144
  const removeItem = React3.useCallback((id) => {
143
145
  setItems((prev) => prev.filter((item) => item.id !== id));
144
146
  }, []);
147
+ const addIncoming = React3.useCallback((message) => {
148
+ setItems((prev) => {
149
+ const track = cursorRef.current % DANMAKU_TRACK_COUNT;
150
+ const item = {
151
+ ...message,
152
+ track,
153
+ durationMs: 8e3 + Math.floor(Math.random() * 2800)
154
+ };
155
+ return [...prev.slice(-40), item];
156
+ });
157
+ cursorRef.current += 1;
158
+ }, []);
145
159
  const send = React3.useCallback(
146
- (text, color) => {
160
+ (text, color, sendOptions) => {
147
161
  const trimmed = text.trim();
148
162
  if (!trimmed) {
149
163
  return null;
@@ -159,31 +173,26 @@ function useDanmakuController(options) {
159
173
  color,
160
174
  timestamp: Date.now()
161
175
  };
162
- setItems((prev) => {
163
- const track = cursor % DANMAKU_TRACK_COUNT;
164
- const item = {
165
- ...message,
166
- track,
167
- durationMs: 8e3 + Math.floor(Math.random() * 2800)
168
- };
169
- return [...prev.slice(-40), item];
170
- });
171
- setCursor((prev) => prev + 1);
176
+ const optimistic = sendOptions?.optimistic ?? true;
177
+ if (optimistic) {
178
+ addIncoming(message);
179
+ }
172
180
  options?.onSend?.(message);
173
181
  return {
174
182
  message,
175
183
  launchKind
176
184
  };
177
185
  },
178
- [cursor, options]
186
+ [addIncoming, options]
179
187
  );
180
188
  return React3.useMemo(
181
189
  () => ({
182
190
  items,
183
191
  send,
192
+ addIncoming,
184
193
  removeItem
185
194
  }),
186
- [items, removeItem, send]
195
+ [addIncoming, items, removeItem, send]
187
196
  );
188
197
  }
189
198
  function parseCommand(text) {
@@ -697,6 +706,302 @@ function useFireworksEngine(options) {
697
706
  return api;
698
707
  }
699
708
 
709
+ // src/mikuFireworks3D/client/WebSocketTransport.ts
710
+ var WebSocketTransport = class {
711
+ constructor(config, callbacks) {
712
+ this.socket = null;
713
+ this.reconnectTimer = null;
714
+ this.isManualClose = false;
715
+ this.pendingQueue = [];
716
+ this.config = config;
717
+ this.callbacks = callbacks || {};
718
+ this.state = {
719
+ connected: false,
720
+ joined: false,
721
+ onlineCount: 0,
722
+ roomId: config.roomId
723
+ };
724
+ }
725
+ connect() {
726
+ if (this.socket && (this.socket.readyState === window.WebSocket.OPEN || this.socket.readyState === window.WebSocket.CONNECTING)) {
727
+ return;
728
+ }
729
+ this.isManualClose = false;
730
+ try {
731
+ this.socket = this.config.protocols ? new window.WebSocket(this.config.serverUrl, this.config.protocols) : new window.WebSocket(this.config.serverUrl);
732
+ } catch {
733
+ this.callbacks.onError?.(new Error("Failed to create WebSocket connection."));
734
+ return;
735
+ }
736
+ this.socket.onopen = () => {
737
+ this.updateState({ connected: true, joined: false });
738
+ this.send({
739
+ type: "join",
740
+ roomId: this.config.roomId,
741
+ user: this.config.user
742
+ });
743
+ };
744
+ this.socket.onmessage = (event) => {
745
+ const parsed = parseServerMessage(event.data);
746
+ if (!parsed) {
747
+ return;
748
+ }
749
+ this.handleServerMessage(parsed);
750
+ };
751
+ this.socket.onerror = () => {
752
+ this.callbacks.onError?.(new Error("WebSocket transport error."));
753
+ };
754
+ this.socket.onclose = () => {
755
+ this.updateState({ connected: false, joined: false });
756
+ this.scheduleReconnect();
757
+ };
758
+ }
759
+ disconnect() {
760
+ this.isManualClose = true;
761
+ this.pendingQueue.length = 0;
762
+ if (this.reconnectTimer != null) {
763
+ window.clearTimeout(this.reconnectTimer);
764
+ this.reconnectTimer = null;
765
+ }
766
+ if (!this.socket) {
767
+ return;
768
+ }
769
+ this.send({ type: "leave" });
770
+ this.socket.close();
771
+ this.socket = null;
772
+ }
773
+ sendDanmaku(payload) {
774
+ this.send({
775
+ type: "danmaku.send",
776
+ payload
777
+ });
778
+ }
779
+ sendFirework(payload) {
780
+ this.send({
781
+ type: "firework.launch",
782
+ payload
783
+ });
784
+ }
785
+ getState() {
786
+ return this.state;
787
+ }
788
+ send(message) {
789
+ if (!this.socket || this.socket.readyState !== window.WebSocket.OPEN) {
790
+ if (message.type === "danmaku.send" || message.type === "firework.launch") {
791
+ this.pendingQueue.push(message);
792
+ }
793
+ return;
794
+ }
795
+ if ((message.type === "danmaku.send" || message.type === "firework.launch") && !this.state.joined) {
796
+ this.pendingQueue.push(message);
797
+ return;
798
+ }
799
+ this.socket.send(JSON.stringify(message));
800
+ }
801
+ updateState(partial) {
802
+ this.state = {
803
+ ...this.state,
804
+ ...partial
805
+ };
806
+ this.callbacks.onStateChange?.(this.state);
807
+ }
808
+ handleServerMessage(message) {
809
+ if (message.type === "joined") {
810
+ this.updateState({ roomId: message.roomId, onlineCount: message.onlineCount, joined: true });
811
+ this.flushPendingQueue();
812
+ return;
813
+ }
814
+ if (message.type === "room.user_joined" || message.type === "room.user_left") {
815
+ this.updateState({ onlineCount: message.onlineCount, roomId: message.roomId });
816
+ return;
817
+ }
818
+ if (message.type === "room.snapshot") {
819
+ this.updateState({ roomId: message.roomId, onlineCount: message.users.length });
820
+ this.callbacks.onSnapshot?.(message);
821
+ return;
822
+ }
823
+ if (message.type === "danmaku.broadcast") {
824
+ this.callbacks.onDanmakuBroadcast?.(message.event);
825
+ return;
826
+ }
827
+ if (message.type === "firework.broadcast") {
828
+ this.callbacks.onFireworkBroadcast?.(message.event);
829
+ return;
830
+ }
831
+ if (message.type === "error") {
832
+ this.callbacks.onError?.(new Error(`${message.code}: ${message.message}`));
833
+ }
834
+ }
835
+ scheduleReconnect() {
836
+ const reconnect = this.config.reconnect ?? true;
837
+ if (this.isManualClose || !reconnect) {
838
+ return;
839
+ }
840
+ if (this.reconnectTimer != null) {
841
+ return;
842
+ }
843
+ const delay = this.config.reconnectIntervalMs ?? 1500;
844
+ this.reconnectTimer = window.setTimeout(() => {
845
+ this.reconnectTimer = null;
846
+ this.connect();
847
+ }, delay);
848
+ }
849
+ flushPendingQueue() {
850
+ if (!this.socket || this.socket.readyState !== window.WebSocket.OPEN || !this.state.joined) {
851
+ return;
852
+ }
853
+ while (this.pendingQueue.length > 0) {
854
+ const message = this.pendingQueue.shift();
855
+ if (!message) {
856
+ break;
857
+ }
858
+ this.socket.send(JSON.stringify(message));
859
+ }
860
+ }
861
+ };
862
+ function parseServerMessage(raw) {
863
+ if (typeof raw !== "string") {
864
+ return null;
865
+ }
866
+ try {
867
+ return JSON.parse(raw);
868
+ } catch {
869
+ return null;
870
+ }
871
+ }
872
+
873
+ // src/mikuFireworks3D/hooks/useFireworksRealtime.ts
874
+ function useFireworksRealtime(options) {
875
+ const { config, enabled, onDanmakuBroadcast, onFireworkBroadcast, onSnapshot, onError, onStateChange } = options;
876
+ const transportRef = React3.useRef(null);
877
+ const callbackRef = React3.useRef({
878
+ onDanmakuBroadcast,
879
+ onFireworkBroadcast,
880
+ onSnapshot,
881
+ onError,
882
+ onStateChange
883
+ });
884
+ const serverUrl = config?.serverUrl ?? "";
885
+ const roomId = config?.roomId ?? "";
886
+ const userId = config?.user.userId ?? "";
887
+ const nickname = config?.user.nickname ?? "";
888
+ const avatarUrl = config?.user.avatarUrl ?? "";
889
+ const reconnect = config?.reconnect ?? true;
890
+ const reconnectIntervalMs = config?.reconnectIntervalMs ?? 1500;
891
+ const [state, setState] = React3.useState({
892
+ connected: false,
893
+ joined: false,
894
+ onlineCount: 0,
895
+ roomId
896
+ });
897
+ React3.useEffect(() => {
898
+ callbackRef.current = {
899
+ onDanmakuBroadcast,
900
+ onFireworkBroadcast,
901
+ onSnapshot,
902
+ onError,
903
+ onStateChange
904
+ };
905
+ }, [onDanmakuBroadcast, onError, onFireworkBroadcast, onSnapshot, onStateChange]);
906
+ const normalizedConfig = React3.useMemo(() => {
907
+ if (!serverUrl || !roomId || !userId) {
908
+ return void 0;
909
+ }
910
+ const protocols = Array.isArray(config?.protocols) ? [...config.protocols] : config?.protocols;
911
+ return {
912
+ serverUrl,
913
+ roomId,
914
+ user: {
915
+ userId,
916
+ nickname: nickname || void 0,
917
+ avatarUrl: avatarUrl || void 0
918
+ },
919
+ protocols,
920
+ reconnect,
921
+ reconnectIntervalMs
922
+ };
923
+ }, [avatarUrl, config?.protocols, nickname, reconnect, reconnectIntervalMs, roomId, serverUrl, userId]);
924
+ React3.useEffect(() => {
925
+ if (!enabled || !normalizedConfig) {
926
+ transportRef.current?.disconnect();
927
+ transportRef.current = null;
928
+ setState({
929
+ connected: false,
930
+ joined: false,
931
+ onlineCount: 0,
932
+ roomId: normalizedConfig?.roomId
933
+ });
934
+ return;
935
+ }
936
+ const transport = new WebSocketTransport(normalizedConfig, {
937
+ onStateChange: (nextState) => {
938
+ setState(nextState);
939
+ callbackRef.current.onStateChange?.(nextState);
940
+ },
941
+ onDanmakuBroadcast: (event) => {
942
+ callbackRef.current.onDanmakuBroadcast?.({
943
+ id: event.id,
944
+ text: event.text,
945
+ color: event.color,
946
+ kind: event.kind,
947
+ userId: event.user.userId,
948
+ timestamp: event.timestamp
949
+ });
950
+ },
951
+ onFireworkBroadcast: (event) => {
952
+ callbackRef.current.onFireworkBroadcast?.({
953
+ id: event.id,
954
+ payload: event.payload,
955
+ userId: event.user.userId,
956
+ timestamp: event.timestamp
957
+ });
958
+ },
959
+ onSnapshot: (snapshot) => {
960
+ callbackRef.current.onSnapshot?.({
961
+ roomId: snapshot.roomId,
962
+ danmakuHistory: snapshot.danmakuHistory.map((item) => ({
963
+ id: item.id,
964
+ text: item.text,
965
+ color: item.color,
966
+ kind: item.kind,
967
+ userId: item.user.userId,
968
+ timestamp: item.timestamp
969
+ })),
970
+ fireworkHistory: snapshot.fireworkHistory.map((item) => ({
971
+ id: item.id,
972
+ payload: item.payload,
973
+ userId: item.user.userId,
974
+ timestamp: item.timestamp
975
+ }))
976
+ });
977
+ },
978
+ onError: (error) => {
979
+ callbackRef.current.onError?.(error);
980
+ }
981
+ });
982
+ transport.connect();
983
+ transportRef.current = transport;
984
+ return () => {
985
+ transport.disconnect();
986
+ if (transportRef.current === transport) {
987
+ transportRef.current = null;
988
+ }
989
+ };
990
+ }, [enabled, normalizedConfig]);
991
+ return React3.useMemo(
992
+ () => ({
993
+ state,
994
+ sendDanmaku: (payload) => {
995
+ transportRef.current?.sendDanmaku(payload);
996
+ },
997
+ sendFirework: (payload) => {
998
+ transportRef.current?.sendFirework(payload);
999
+ }
1000
+ }),
1001
+ [state]
1002
+ );
1003
+ }
1004
+
700
1005
  // src/mikuFireworks3D/components/MikuFireworks3D.tsx
701
1006
  function MikuFireworks3D({
702
1007
  width = "100%",
@@ -710,11 +1015,15 @@ function MikuFireworks3D({
710
1015
  onLaunch,
711
1016
  onDanmakuSend,
712
1017
  onError,
713
- onFpsReport
1018
+ onFpsReport,
1019
+ onRealtimeStateChange,
1020
+ realtime
714
1021
  }) {
715
1022
  const [selectedKind, setSelectedKind] = React3.useState(defaultKind);
716
1023
  const [avatarUrl, setAvatarUrl] = React3.useState(defaultAvatarUrl);
717
1024
  const [autoLaunch, setAutoLaunch] = React3.useState(autoLaunchOnDanmaku);
1025
+ const seenDanmakuIdsRef = React3.useRef(/* @__PURE__ */ new Set());
1026
+ const seenFireworkIdsRef = React3.useRef(/* @__PURE__ */ new Set());
718
1027
  const { containerRef, canvasRef, launch, fps } = useFireworksEngine({
719
1028
  maxParticles,
720
1029
  maxActiveFireworks,
@@ -722,22 +1031,93 @@ function MikuFireworks3D({
722
1031
  onError,
723
1032
  onFpsReport
724
1033
  });
725
- const { items, send, removeItem } = useDanmakuController({
1034
+ const { items, send, addIncoming, removeItem } = useDanmakuController({
726
1035
  onSend: onDanmakuSend
727
1036
  });
1037
+ const realtimeEnabled = Boolean(realtime && (realtime.enabled ?? true));
1038
+ const realtimeApi = useFireworksRealtime({
1039
+ enabled: realtimeEnabled,
1040
+ config: realtime,
1041
+ onStateChange: onRealtimeStateChange,
1042
+ onError,
1043
+ onDanmakuBroadcast: (event) => {
1044
+ if (seenDanmakuIdsRef.current.has(event.id)) {
1045
+ return;
1046
+ }
1047
+ seenDanmakuIdsRef.current.add(event.id);
1048
+ addIncoming({
1049
+ id: event.id,
1050
+ userId: event.userId,
1051
+ text: event.text,
1052
+ color: event.color,
1053
+ timestamp: event.timestamp
1054
+ });
1055
+ },
1056
+ onFireworkBroadcast: (event) => {
1057
+ if (seenFireworkIdsRef.current.has(event.id)) {
1058
+ return;
1059
+ }
1060
+ seenFireworkIdsRef.current.add(event.id);
1061
+ launch(event.payload);
1062
+ },
1063
+ onSnapshot: (snapshot) => {
1064
+ for (const danmaku of snapshot.danmakuHistory) {
1065
+ if (seenDanmakuIdsRef.current.has(danmaku.id)) {
1066
+ continue;
1067
+ }
1068
+ seenDanmakuIdsRef.current.add(danmaku.id);
1069
+ addIncoming({
1070
+ id: danmaku.id,
1071
+ userId: danmaku.userId,
1072
+ text: danmaku.text,
1073
+ color: danmaku.color,
1074
+ timestamp: danmaku.timestamp
1075
+ });
1076
+ }
1077
+ for (const firework of snapshot.fireworkHistory) {
1078
+ if (seenFireworkIdsRef.current.has(firework.id)) {
1079
+ continue;
1080
+ }
1081
+ seenFireworkIdsRef.current.add(firework.id);
1082
+ launch(firework.payload);
1083
+ }
1084
+ }
1085
+ });
728
1086
  const handleLaunch = (kind) => {
729
- launch({
1087
+ const payload = {
730
1088
  kind,
731
1089
  avatarUrl: kind === "avatar" ? avatarUrl || void 0 : void 0
732
- });
1090
+ };
1091
+ if (realtimeEnabled && realtimeApi.state.connected && realtimeApi.state.joined) {
1092
+ realtimeApi.sendFirework(payload);
1093
+ return;
1094
+ }
1095
+ launch(payload);
733
1096
  };
734
1097
  const handleSendDanmaku = (text) => {
735
- const result = send(text);
1098
+ const result = send(text, void 0, {
1099
+ optimistic: !(realtimeEnabled && realtimeApi.state.connected && realtimeApi.state.joined)
1100
+ });
736
1101
  if (!result) {
737
1102
  return;
738
1103
  }
739
1104
  const launchKind = result.launchKind ?? selectedKind;
740
- if (autoLaunch) {
1105
+ if (realtimeEnabled && realtimeApi.state.connected && realtimeApi.state.joined) {
1106
+ realtimeApi.sendDanmaku({
1107
+ text: result.message.text,
1108
+ color: result.message.color,
1109
+ kind: result.launchKind
1110
+ });
1111
+ if (autoLaunch) {
1112
+ realtimeApi.sendFirework({
1113
+ kind: launchKind,
1114
+ avatarUrl: launchKind === "avatar" ? avatarUrl || void 0 : void 0,
1115
+ message: result.message
1116
+ });
1117
+ }
1118
+ return;
1119
+ }
1120
+ if (autoLaunch || result.launchKind) {
741
1121
  launch({
742
1122
  kind: launchKind,
743
1123
  avatarUrl: launchKind === "avatar" ? avatarUrl || void 0 : void 0,
@@ -776,7 +1156,9 @@ function MikuFireworks3D({
776
1156
  avatarUrl,
777
1157
  onAvatarUrlChange: setAvatarUrl,
778
1158
  onLaunch: () => handleLaunch(selectedKind),
779
- fps
1159
+ fps,
1160
+ realtimeConnected: realtimeEnabled ? realtimeApi.state.connected : void 0,
1161
+ onlineCount: realtimeEnabled ? realtimeApi.state.onlineCount : void 0
780
1162
  }
781
1163
  ), /* @__PURE__ */ React3__default.default.createElement(DanmakuPanel, { onSend: handleSendDanmaku }), /* @__PURE__ */ React3__default.default.createElement("style", null, `
782
1164
  @keyframes sa2kit-danmaku-move {
@@ -803,7 +1185,9 @@ exports.FireworksControlPanel = FireworksControlPanel;
803
1185
  exports.MIKU_PALETTE = MIKU_PALETTE;
804
1186
  exports.MikuFireworks3D = MikuFireworks3D;
805
1187
  exports.NORMAL_PALETTE = NORMAL_PALETTE;
1188
+ exports.WebSocketTransport = WebSocketTransport;
806
1189
  exports.useDanmakuController = useDanmakuController;
807
1190
  exports.useFireworksEngine = useFireworksEngine;
808
- //# sourceMappingURL=chunk-B34YUZRL.js.map
809
- //# sourceMappingURL=chunk-B34YUZRL.js.map
1191
+ exports.useFireworksRealtime = useFireworksRealtime;
1192
+ //# sourceMappingURL=chunk-XXDMARU7.js.map
1193
+ //# sourceMappingURL=chunk-XXDMARU7.js.map