global player
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { useContext } from "react";
|
||||
import { PlayerContext } from "../providers/PlayerProvider";
|
||||
|
||||
const usePlayer = () => {
|
||||
return useContext(PlayerContext);
|
||||
};
|
||||
|
||||
export default usePlayer;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user