Skip to content

Commit 61bcd51

Browse files
committed
refactor(ws): 拆分业务逻辑
1 parent 4d9a4d1 commit 61bcd51

6 files changed

Lines changed: 489 additions & 379 deletions

File tree

src/App.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import {
2424
Typography,
2525
} from "@mui/material";
2626
import { createTheme, ThemeProvider } from "@mui/material/styles";
27-
import { Provider, useAtom, useAtomValue } from "jotai";
27+
import { Provider, useAtom, useAtomValue, useSetAtom } from "jotai";
2828
import { useEffect, useMemo, useState } from "react";
29-
import { AmllWsClient } from "./components/headless/AmllWsClient";
29+
import { AmllStateSync } from "./components/headless/AmllStateSync";
3030
import { InfLinkBridge } from "./components/headless/InfLinkBridge";
3131
import { LyricSync } from "./components/headless/LyricSync";
3232
import { DebugDialog } from "./components/ui/DebugDialog";
@@ -38,7 +38,9 @@ import {
3838
autoReconnectAtom,
3939
type ConnectionStatus,
4040
connectionErrorAtom,
41+
connectionIntentAtom,
4142
connectionStatusAtom,
43+
forceReconnectTriggerAtom,
4244
infLinkStatusAtom,
4345
timelineOffsetAtom,
4446
wsUrlAtom,
@@ -94,7 +96,7 @@ export default function App() {
9496
<ThemeProvider theme={theme}>
9597
<Provider>
9698
<InfLinkBridge />
97-
<AmllWsClient />
99+
<AmllStateSync />
98100
<LyricSync />
99101
<Main />
100102
</Provider>
@@ -107,8 +109,10 @@ function Main() {
107109
const [wsUrl, setWsUrl] = useAtom(wsUrlAtom);
108110
const [autoConnect, setAutoConnect] = useAtom(autoConnectAtom);
109111
const [autoReconnect, setAutoReconnect] = useAtom(autoReconnectAtom);
110-
const [status, setStatus] = useAtom(connectionStatusAtom);
112+
const status = useAtomValue(connectionStatusAtom);
113+
const [intent, setIntent] = useAtom(connectionIntentAtom);
111114
const [error, setError] = useAtom(connectionErrorAtom);
115+
const setForceTrigger = useSetAtom(forceReconnectTriggerAtom);
112116
const [displayError, setDisplayError] = useState(error);
113117
const [debugOpen, setDebugOpen] = useState(false);
114118
const [sourcesDialogOpen, setSourcesDialogOpen] = useState(false);
@@ -121,11 +125,15 @@ function Main() {
121125
const isConnecting = status === "connecting";
122126

123127
function handleToggleConnection() {
124-
if (isConnected || isConnecting) {
125-
setStatus("disconnected");
128+
if (isConnected) {
129+
setIntent(false);
126130
setError("");
127131
} else {
128-
setStatus("connecting");
132+
if (intent) {
133+
setForceTrigger((prev) => prev + 1);
134+
} else {
135+
setIntent(true);
136+
}
129137
setError("");
130138
}
131139
}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/**
2+
* @fileoverview
3+
* 将歌曲信息同步给 AMLL Player,同时处理 AMLL Player 的控制指令
4+
*/
5+
6+
import { useAtomValue, useSetAtom } from "jotai";
7+
import { useCallback, useEffect, useRef } from "react";
8+
import { useWsConnectionManager } from "@/hooks/useWsConnectionManager";
9+
import {
10+
autoConnectAtom,
11+
connectionIntentAtom,
12+
connectionStatusAtom,
13+
lyricAtom,
14+
nextAtom,
15+
pauseAtom,
16+
playAtom,
17+
playbackStatusAtom,
18+
playModeAtom,
19+
previousAtom,
20+
reconnectCountdownAtom,
21+
seekToAtom,
22+
setRepeatModeAtom,
23+
setShuffleModeAtom,
24+
setVolumeAtom,
25+
songInfoAtom,
26+
timelineInfoAtom,
27+
timelineOffsetAtom,
28+
volumeInfoAtom,
29+
} from "@/store";
30+
import type {
31+
AudioDataInfo,
32+
RepeatMode as NCMRepeatMode,
33+
} from "@/types/inflink";
34+
import type { AmllMessage, AmllRepeatMode, AmllStateUpdate } from "@/types/ws";
35+
import { CoverManager } from "@/utils/cover";
36+
import {
37+
BinaryMagicNumber,
38+
createAmllBinaryPayload,
39+
} from "@/utils/createAmllBinaryPayload";
40+
import { AudioDataBus } from "./InfLinkBridge";
41+
42+
export function AmllStateSync() {
43+
const status = useAtomValue(connectionStatusAtom);
44+
const autoConnect = useAtomValue(autoConnectAtom);
45+
const setIntent = useSetAtom(connectionIntentAtom);
46+
const isInitializedRef = useRef(false);
47+
48+
const coverManagerRef = useRef(new CoverManager());
49+
50+
const songInfo = useAtomValue(songInfoAtom);
51+
const playbackStatus = useAtomValue(playbackStatusAtom);
52+
const timelineInfo = useAtomValue(timelineInfoAtom);
53+
const playMode = useAtomValue(playModeAtom);
54+
const volumeInfo = useAtomValue(volumeInfoAtom);
55+
const timelineOffset = useAtomValue(timelineOffsetAtom);
56+
57+
const lyricContent = useAtomValue(lyricAtom);
58+
59+
const play = useSetAtom(playAtom);
60+
const pause = useSetAtom(pauseAtom);
61+
const next = useSetAtom(nextAtom);
62+
const previous = useSetAtom(previousAtom);
63+
const setVolume = useSetAtom(setVolumeAtom);
64+
const seekTo = useSetAtom(seekToAtom);
65+
const setRepeatMode = useSetAtom(setRepeatModeAtom);
66+
const setShuffleMode = useSetAtom(setShuffleModeAtom);
67+
68+
const setReconnectCountdown = useSetAtom(reconnectCountdownAtom);
69+
70+
const handleIncomingMessage = useCallback(
71+
(event: MessageEvent) => {
72+
if (typeof event.data !== "string") return;
73+
74+
try {
75+
const message: AmllMessage = JSON.parse(event.data);
76+
77+
if (message.type === "command") {
78+
const cmd = message.value;
79+
80+
switch (cmd.command) {
81+
case "pause":
82+
pause();
83+
break;
84+
case "resume":
85+
play();
86+
break;
87+
case "forwardSong":
88+
next();
89+
break;
90+
case "backwardSong":
91+
previous();
92+
break;
93+
case "setVolume":
94+
setVolume(cmd.volume);
95+
break;
96+
case "seekPlayProgress":
97+
seekTo(cmd.progress);
98+
break;
99+
case "setRepeatMode": {
100+
const modeMap: Record<AmllRepeatMode, NCMRepeatMode> = {
101+
off: "None",
102+
all: "List",
103+
one: "Track",
104+
};
105+
setRepeatMode(modeMap[cmd.mode]);
106+
break;
107+
}
108+
case "setShuffleMode":
109+
setShuffleMode(cmd.enabled);
110+
break;
111+
default: {
112+
const exhaustiveCheck: never = cmd;
113+
console.warn("未处理的控制指令", exhaustiveCheck);
114+
}
115+
}
116+
}
117+
} catch (err) {
118+
console.error("解析 AMLL WebSocket 消息失败:", err);
119+
}
120+
},
121+
[
122+
play,
123+
pause,
124+
next,
125+
previous,
126+
setVolume,
127+
seekTo,
128+
setRepeatMode,
129+
setShuffleMode,
130+
],
131+
);
132+
133+
useEffect(() => {
134+
if (!isInitializedRef.current) {
135+
setIntent(autoConnect);
136+
isInitializedRef.current = true;
137+
}
138+
}, [autoConnect, setIntent]);
139+
140+
const { send, countdown } = useWsConnectionManager({
141+
onMessage: handleIncomingMessage,
142+
onConnected: () => {
143+
send(JSON.stringify({ type: "initialize" }));
144+
},
145+
});
146+
147+
useEffect(() => {
148+
setReconnectCountdown(countdown);
149+
}, [countdown, setReconnectCountdown]);
150+
151+
const sendStateUpdate = useCallback(
152+
(updateObj: AmllStateUpdate) => {
153+
send(JSON.stringify({ type: "state", value: updateObj }));
154+
},
155+
[send],
156+
);
157+
158+
useEffect(() => {
159+
if (!songInfo || status !== "connected") return;
160+
161+
sendStateUpdate({
162+
update: "setMusic",
163+
musicId: songInfo.ncmId.toString(),
164+
musicName: songInfo.songName,
165+
albumId: "",
166+
albumName: songInfo.albumName,
167+
artists: [{ id: "", name: songInfo.authorName }],
168+
duration: songInfo.duration ?? 0,
169+
});
170+
171+
if (songInfo.cover?.url) {
172+
const fetchAndSendCover = async () => {
173+
try {
174+
const { cover } = await coverManagerRef.current.getCover(
175+
songInfo,
176+
"500",
177+
);
178+
179+
if (cover?.blob) {
180+
const arrayBuffer = await cover.blob.arrayBuffer();
181+
182+
const buffer = createAmllBinaryPayload(
183+
BinaryMagicNumber.SetCoverData,
184+
arrayBuffer,
185+
);
186+
187+
send(buffer);
188+
}
189+
} catch (e) {
190+
if ((e as Error).name !== "AbortError") {
191+
console.error("获取或发送缓存封面失败", e);
192+
sendStateUpdate({
193+
update: "setCover",
194+
source: "uri",
195+
url: songInfo.cover?.url || "",
196+
});
197+
}
198+
}
199+
};
200+
201+
fetchAndSendCover();
202+
}
203+
}, [songInfo, status, send, sendStateUpdate]);
204+
205+
useEffect(() => {
206+
if (!playbackStatus || status !== "connected") return;
207+
sendStateUpdate({
208+
update: playbackStatus === "Playing" ? "resumed" : "paused",
209+
});
210+
}, [playbackStatus, status, sendStateUpdate]);
211+
212+
useEffect(() => {
213+
if (!timelineInfo || status !== "connected") return;
214+
215+
const adjustedProgress = Math.max(
216+
0,
217+
timelineInfo.currentTime - timelineOffset,
218+
);
219+
220+
sendStateUpdate({
221+
update: "progress",
222+
progress: Math.floor(adjustedProgress),
223+
});
224+
}, [timelineInfo, timelineOffset, status, sendStateUpdate]);
225+
226+
useEffect(() => {
227+
if (!volumeInfo || status !== "connected") return;
228+
sendStateUpdate({
229+
update: "volume",
230+
volume: volumeInfo.isMuted ? 0 : volumeInfo.volume,
231+
});
232+
}, [volumeInfo, status, sendStateUpdate]);
233+
234+
useEffect(() => {
235+
if (!playMode || status !== "connected") return;
236+
const repeatMap: Record<NCMRepeatMode, AmllRepeatMode> = {
237+
None: "off",
238+
Track: "one",
239+
List: "all",
240+
AI: "all",
241+
};
242+
sendStateUpdate({
243+
update: "modeChanged",
244+
repeat: repeatMap[playMode.repeatMode] || "off",
245+
shuffle: playMode.isShuffling,
246+
});
247+
}, [playMode, status, sendStateUpdate]);
248+
249+
useEffect(() => {
250+
if (!lyricContent || status !== "connected") return;
251+
252+
sendStateUpdate({
253+
update: "setLyric",
254+
...lyricContent,
255+
});
256+
}, [lyricContent, status, sendStateUpdate]);
257+
258+
useEffect(() => {
259+
const handleAudioData = (e: Event) => {
260+
if (status !== "connected") return;
261+
262+
const { data } = (e as CustomEvent<AudioDataInfo>).detail;
263+
264+
const buffer = createAmllBinaryPayload(BinaryMagicNumber.AudioData, data);
265+
266+
send(buffer);
267+
};
268+
269+
AudioDataBus.addEventListener("audiodata", handleAudioData);
270+
return () => {
271+
AudioDataBus.removeEventListener("audiodata", handleAudioData);
272+
};
273+
}, [send, status]);
274+
275+
return null;
276+
}

0 commit comments

Comments
 (0)