global player

This commit is contained in:
2025-10-07 09:51:38 +02:00
parent 5cd6741ae6
commit f796d91267
15 changed files with 1031 additions and 136 deletions
+9
View File
@@ -0,0 +1,9 @@
import { useContext } from "react";
import { PlayerContext } from "../providers/PlayerProvider";
const usePlayer = () => {
return useContext(PlayerContext);
};
export default usePlayer;
+148
View File
@@ -0,0 +1,148 @@
import { useEffect, useMemo, useRef } from "react";
import useTrackController from "./useTrackController";
const buildDescriptorFromSource = (source) => {
if (!source) return null;
const descriptor = {};
if (typeof source === "number") {
descriptor.source = source;
descriptor.id = `asset-${source}`;
} else if (typeof source === "string") {
descriptor.uri = source;
descriptor.id = `uri-${source}`;
} else if (typeof source === "object") {
descriptor.source = source.source;
descriptor.uri = source.uri;
descriptor.assetId = source.assetId;
descriptor.headers = source.headers;
descriptor.id = source.id;
if (!descriptor.id) {
if (typeof source.uri === "string") {
descriptor.id = `uri-${source.uri}`;
} else if (typeof source.assetId === "number") {
descriptor.id = `asset-${source.assetId}`;
}
}
}
if (!descriptor.id) {
try {
descriptor.id = `src-${JSON.stringify(source)}`;
} catch (e) {
descriptor.id = `src-${Date.now()}`;
}
}
if (!descriptor.uri && !descriptor.source && typeof descriptor.assetId === "number") {
descriptor.source = descriptor.assetId;
}
if (!descriptor.uri && !descriptor.source) return null;
return descriptor;
};
const useSharedAudioPlayer = (source, options = null) => {
const descriptor = useMemo(() => {
const base = buildDescriptorFromSource(source);
if (!base) return null;
if (!options || typeof options !== "object") return base;
const extended = { ...base };
if (options.id) extended.id = options.id;
if (options.title) extended.title = options.title;
if (options.artist) extended.artist = options.artist;
if (options.artwork) extended.artwork = options.artwork;
if (options.coverUrl) extended.coverUrl = options.coverUrl;
if (options.metadata) extended.metadata = options.metadata;
if (options.context) extended.context = options.context;
return extended;
}, [source, options]);
const {
play,
pause,
resume,
seekTo,
isPlaying,
positionMs,
durationMs,
} = useTrackController(descriptor);
const stateRef = useRef({
isPlaying: false,
positionMs: 0,
durationMs: 0,
});
useEffect(() => {
stateRef.current = {
isPlaying,
positionMs,
durationMs,
};
}, [isPlaying, positionMs, durationMs]);
const controlsRef = useRef({ play, pause, resume, seekTo });
useEffect(() => {
controlsRef.current = { play, pause, resume, seekTo };
}, [play, pause, resume, seekTo]);
const playerRef = useRef(null);
useEffect(() => {
if (!descriptor) {
playerRef.current = null;
return;
}
if (!playerRef.current || playerRef.current.__trackId !== descriptor.id) {
const player = {};
Object.defineProperty(player, "__trackId", {
value: descriptor.id,
enumerable: false,
writable: false,
});
player.play = async () => {
const { positionMs: currentPosition } = stateRef.current;
await controlsRef.current.play({ startPositionMs: currentPosition });
};
player.pause = async () => {
await controlsRef.current.pause();
};
player.resume = async () => {
await controlsRef.current.resume();
};
player.seekTo = async (seconds) => {
const target = Math.max(0, Number(seconds) || 0) * 1000;
await controlsRef.current.seekTo(target);
};
Object.defineProperties(player, {
playing: {
enumerable: true,
get: () => !!stateRef.current.isPlaying,
},
duration: {
enumerable: true,
get: () => stateRef.current.durationMs / 1000,
},
currentTime: {
enumerable: true,
get: () => stateRef.current.positionMs / 1000,
},
});
playerRef.current = player;
}
}, [descriptor]);
return playerRef.current;
};
export default useSharedAudioPlayer;
+143
View File
@@ -0,0 +1,143 @@
import { useCallback, useMemo } from "react";
import usePlayer from "./usePlayer";
const useTrackController = (descriptor) => {
const {
currentTrack,
play,
pause,
resume,
isPlaying,
positionMs,
durationMs,
seekTo,
seekBy,
} = usePlayer();
const normalizedDescriptor = useMemo(() => {
if (!descriptor || !descriptor.id) return null;
return descriptor;
}, [descriptor]);
const trackId = normalizedDescriptor?.id || null;
const isCurrent = trackId ? currentTrack?.id === trackId : false;
const effectivePositionMs = isCurrent ? positionMs : 0;
const effectiveDurationMs = isCurrent ? durationMs : 0;
const effectiveIsPlaying = isCurrent && isPlaying;
const ensureLoaded = useCallback(
async ({ startPositionMs = 0, autoPlay = true } = {}) => {
if (!normalizedDescriptor) return;
await play(normalizedDescriptor, {
startPositionMs,
autoPlay,
context: normalizedDescriptor.context,
});
},
[normalizedDescriptor, play]
);
const playNow = useCallback(
async ({ startPositionMs = 0 } = {}) => {
await ensureLoaded({ startPositionMs, autoPlay: true });
},
[ensureLoaded]
);
const pauseNow = useCallback(async () => {
if (isCurrent) {
await pause();
}
}, [isCurrent, pause]);
const resumeNow = useCallback(
async ({ startPositionMs } = {}) => {
if (!normalizedDescriptor) return;
if (!isCurrent) {
await ensureLoaded({
startPositionMs: startPositionMs ?? effectivePositionMs,
autoPlay: true,
});
return;
}
if (typeof startPositionMs === "number") {
await seekTo(Math.max(0, startPositionMs));
}
await resume();
},
[
normalizedDescriptor,
isCurrent,
ensureLoaded,
effectivePositionMs,
seekTo,
resume,
]
);
const seekToPosition = useCallback(
async (targetMs, { autoPlay = false } = {}) => {
const bounded = Math.max(0, Number(targetMs) || 0);
if (!normalizedDescriptor) return;
if (!isCurrent) {
await ensureLoaded({ startPositionMs: bounded, autoPlay });
return;
}
await seekTo(bounded);
if (autoPlay && !effectiveIsPlaying) {
await resume();
}
},
[
normalizedDescriptor,
isCurrent,
ensureLoaded,
seekTo,
effectiveIsPlaying,
resume,
]
);
const seekByDelta = useCallback(
async (deltaMs, { autoPlay = false } = {}) => {
const delta = Number(deltaMs || 0);
if (!normalizedDescriptor) return;
if (!isCurrent) {
const next = Math.max(0, effectivePositionMs + delta);
await ensureLoaded({ startPositionMs: next, autoPlay });
return;
}
await seekBy(delta);
if (autoPlay && !effectiveIsPlaying) {
await resume();
}
},
[
normalizedDescriptor,
isCurrent,
ensureLoaded,
effectivePositionMs,
seekBy,
effectiveIsPlaying,
resume,
]
);
return {
trackId,
descriptor: normalizedDescriptor,
isCurrent,
isPlaying: effectiveIsPlaying,
positionMs: effectivePositionMs,
durationMs: effectiveDurationMs,
ensureLoaded,
play: playNow,
pause: pauseNow,
resume: resumeNow,
seekTo: seekToPosition,
seekBy: seekByDelta,
};
};
export default useTrackController;