|
|
@@ -0,0 +1,218 @@
|
|
|
+import { throttle } from "lodash";
|
|
|
+import { ref } from "vue";
|
|
|
+
|
|
|
+export const PRELOADER_STATUS = {
|
|
|
+ ERROR: "error",
|
|
|
+ LOADING: "loading",
|
|
|
+ WAITING: "waiting",
|
|
|
+ DONE: "done",
|
|
|
+ ABORT: "abort",
|
|
|
+};
|
|
|
+
|
|
|
+export const getMediaType = (url) => {
|
|
|
+ const MEDIA_TYPES = {
|
|
|
+ mp4: "video/mp4",
|
|
|
+ webm: "video/webm",
|
|
|
+ ogg: "video/ogg",
|
|
|
+ mp3: "audio/mp3",
|
|
|
+ wav: "audio/wav",
|
|
|
+ };
|
|
|
+ const urlExtension = url.split(".").pop()?.toLowerCase() || "";
|
|
|
+
|
|
|
+ if (MEDIA_TYPES.hasOwnProperty(urlExtension)) {
|
|
|
+ return MEDIA_TYPES[urlExtension];
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+export const parseUrlName = (url) => {
|
|
|
+ const regex = /\/([^/]+)\.\w+$/;
|
|
|
+ const match = url.match(regex);
|
|
|
+ if (match && match.length >= 2) {
|
|
|
+ return match[1].split(".")[0];
|
|
|
+ } else {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+export function usePreloader(params) {
|
|
|
+ let fetchControllerStack = [];
|
|
|
+ const decimals = params.decimals ? params.decimals * 10 : 1;
|
|
|
+ const _list = params.list;
|
|
|
+ const _retry = params.retry ?? 3;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 总字节长度
|
|
|
+ */
|
|
|
+ let totalLength = 0;
|
|
|
+ const fileSizeStack = new Map();
|
|
|
+ /**
|
|
|
+ * 已接收字节长度
|
|
|
+ */
|
|
|
+ let downloadLength = 0;
|
|
|
+ /**
|
|
|
+ * 进度
|
|
|
+ */
|
|
|
+ const percent = ref("0");
|
|
|
+ const status = ref(PRELOADER_STATUS.WAITING);
|
|
|
+ /**
|
|
|
+ * 缓存的媒体资源本地url
|
|
|
+ */
|
|
|
+ const mediaUrlMap = new Map();
|
|
|
+
|
|
|
+ const setThrottlePercent = throttle((val) => (percent.value = val), 200);
|
|
|
+
|
|
|
+ const calculateSum = (map) => {
|
|
|
+ let sum = 0;
|
|
|
+ for (const value of map.values()) {
|
|
|
+ sum += value;
|
|
|
+ }
|
|
|
+ return sum;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handlePreload = (url, retry = _retry, loadedLength = 0) => {
|
|
|
+ const controller = new AbortController();
|
|
|
+ const signal = controller.signal;
|
|
|
+ const mediaType = getMediaType(url);
|
|
|
+ let loadingLength = 0;
|
|
|
+
|
|
|
+ return new Promise((res, rej) => {
|
|
|
+ fetch(url, { signal })
|
|
|
+ .then((response) => {
|
|
|
+ const contentLength = Number(response.headers.get("Content-Length"));
|
|
|
+
|
|
|
+ fileSizeStack.set(url, contentLength);
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ return response.body;
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .then(async (body) => {
|
|
|
+ if (!body) {
|
|
|
+ rej(`request.body is not find`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let diffed = false;
|
|
|
+ const chunks = [];
|
|
|
+ const reader = body.getReader();
|
|
|
+ const loop = true;
|
|
|
+
|
|
|
+ while (loop) {
|
|
|
+ const { value, done } = await reader.read();
|
|
|
+
|
|
|
+ if (done) break;
|
|
|
+
|
|
|
+ loadingLength += value.length;
|
|
|
+ if (loadingLength >= loadedLength) {
|
|
|
+ if (!diffed && loadedLength) {
|
|
|
+ const diff = loadingLength - loadedLength;
|
|
|
+ console.log(diff);
|
|
|
+ downloadLength += diff;
|
|
|
+ diffed = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ downloadLength += value.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mediaType) {
|
|
|
+ chunks.push(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fileSizeStack.size === _list.length) {
|
|
|
+ const sum = calculateSum(fileSizeStack);
|
|
|
+ const percent = Math.round(
|
|
|
+ (downloadLength / sum) * 100 * decimals,
|
|
|
+ );
|
|
|
+
|
|
|
+ setThrottlePercent((percent / decimals).toFixed(params.decimals));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mediaType) {
|
|
|
+ const blob = new Blob(chunks, { type: mediaType });
|
|
|
+ mediaUrlMap.set(parseUrlName(url), URL.createObjectURL(blob));
|
|
|
+ }
|
|
|
+
|
|
|
+ res(true);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ if (err.name === "AbortError") {
|
|
|
+ rej(err);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (retry) {
|
|
|
+ console.log("尝试重新预加载");
|
|
|
+ setTimeout(() => {
|
|
|
+ handlePreload(url, retry - 1, loadingLength)
|
|
|
+ .then(res)
|
|
|
+ .catch(rej);
|
|
|
+ }, 2000);
|
|
|
+ } else {
|
|
|
+ if (mediaType) {
|
|
|
+ mediaUrlMap.set(parseUrlName(url), url);
|
|
|
+ }
|
|
|
+
|
|
|
+ rej(err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ fetchControllerStack.push(controller);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const resetParams = () => {
|
|
|
+ abort();
|
|
|
+
|
|
|
+ totalLength = 0;
|
|
|
+ downloadLength = 0;
|
|
|
+ fileSizeStack.clear();
|
|
|
+ mediaUrlMap.clear();
|
|
|
+ setThrottlePercent("0");
|
|
|
+ };
|
|
|
+
|
|
|
+ const start = () => {
|
|
|
+ if (status.value === PRELOADER_STATUS.LOADING) return;
|
|
|
+
|
|
|
+ if (status.value !== PRELOADER_STATUS.WAITING) {
|
|
|
+ resetParams();
|
|
|
+ }
|
|
|
+
|
|
|
+ status.value = PRELOADER_STATUS.LOADING;
|
|
|
+
|
|
|
+ Promise.all(_list.map((url) => handlePreload(url)))
|
|
|
+ .then(() => {
|
|
|
+ setThrottlePercent("100");
|
|
|
+ params.success?.();
|
|
|
+ status.value = PRELOADER_STATUS.DONE;
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ if (err.name === "AbortError") return;
|
|
|
+
|
|
|
+ abort();
|
|
|
+ params.error?.(err);
|
|
|
+ status.value = PRELOADER_STATUS.ERROR;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const abort = () => {
|
|
|
+ fetchControllerStack.forEach((controller) => {
|
|
|
+ controller.abort();
|
|
|
+ });
|
|
|
+ fetchControllerStack = [];
|
|
|
+
|
|
|
+ status.value = PRELOADER_STATUS.ABORT;
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ status,
|
|
|
+ percent,
|
|
|
+ totalLength,
|
|
|
+ downloadLength,
|
|
|
+ mediaUrlMap,
|
|
|
+ start,
|
|
|
+ abort,
|
|
|
+ };
|
|
|
+}
|