UNPKG

minigame-std

Version:

Cross-platform standard library for WeChat minigame and web browsers with unified APIs for crypto, fs, fetch, storage, and more.

1 lines 60.4 kB
{"version":3,"file":"video.cjs","names":[],"sources":["../src/macros/env.ts","../src/std/video/web_video.ts","../src/std/fs/mina_fs_shared.ts","../src/std/video/frame-source/mina.ts","../src/std/video/frame-source/web.ts","../src/std/video/frame-source/mod.ts","../src/std/video/mod.ts"],"sourcesContent":["/**\n * @internal\n * 小游戏环境宏模块。\n */\n\n/**\n * 小游戏环境宏。\n *\n * 可通过打包工具在 build 时修改,如 esbuild webpack vite 等。\n */\ndeclare const __MINIGAME_STD_MINA__: boolean;\n\n/**\n * 如果在小游戏环境中返回 true,否则返回 false。\n */\nexport const IS_MINA = __MINIGAME_STD_MINA__;\n","/**\n * @internal\n * Web 平台的视频播放实现。\n */\n\n/**\n * Web平台创建视频播放器。\n * @param options - 视频配置选项。\n * @returns Video对象,模拟WechatMinigame.Video接口。\n */\nexport function createVideo(options: WechatMinigame.CreateVideoOption): WechatMinigame.Video {\n const {\n autoplay = false,\n backgroundColor,\n controls = false,\n height,\n initialTime,\n loop = false,\n muted = false,\n objectFit,\n playbackRate = 1,\n poster,\n src,\n width,\n x,\n y,\n } = options;\n\n const video = document.createElement('video');\n\n // 设置基本属性\n video.src = src;\n video.autoplay = autoplay;\n video.controls = controls;\n video.loop = loop;\n video.muted = muted;\n video.playbackRate = playbackRate;\n\n // 设置移动端内联播放\n video.setAttribute('playsinline', 'true');\n video.setAttribute('webkit-playsinline', 'true');\n\n if (poster) {\n video.poster = poster;\n }\n\n // 设置尺寸\n if (height !== undefined) {\n video.height = height;\n }\n if (width !== undefined) {\n video.width = width;\n }\n\n // 设置样式\n if (backgroundColor) {\n video.style.backgroundColor = backgroundColor;\n }\n if (objectFit) {\n video.style.objectFit = objectFit;\n }\n\n // 设置定位\n if (x !== undefined || y !== undefined) {\n video.style.position = 'absolute';\n if (x !== undefined) {\n video.style.left = `${x}px`;\n }\n if (y !== undefined) {\n video.style.top = `${y}px`;\n }\n }\n\n // 添加到DOM\n document.body.appendChild(video);\n\n // 如果设置了初始播放位置,在元数据加载后跳转\n if (initialTime !== undefined) {\n video.addEventListener('loadedmetadata', () => {\n video.currentTime = initialTime;\n }, {\n once: true,\n });\n }\n\n // 事件监听器存储\n type Callback = (...args: unknown[]) => void;\n const listeners = {\n ended: new Set<Callback>(),\n error: new Set<Callback>(),\n pause: new Set<Callback>(),\n play: new Set<Callback>(),\n progress: new Set<Callback>(),\n timeupdate: new Set<Callback>(),\n waiting: new Set<Callback>(),\n };\n\n // 绑定原生事件到监听器\n video.addEventListener('ended', () => {\n listeners.ended.forEach(cb => {\n cb();\n });\n });\n video.addEventListener('error', () => {\n const errMsg = video.error?.message ?? 'MEDIA_ERR_UNKNOWN';\n listeners.error.forEach(cb => cb({\n errMsg,\n }));\n });\n video.addEventListener('pause', () => {\n listeners.pause.forEach(cb => {\n cb();\n });\n });\n video.addEventListener('play', () => {\n listeners.play.forEach(cb => {\n cb();\n });\n });\n video.addEventListener('progress', () => {\n const buffered = video.buffered;\n const { duration } = video;\n const bufferedPercent = buffered.length > 0 && duration > 0\n ? (buffered.end(buffered.length - 1) / duration) * 100\n : 0;\n listeners.progress.forEach(cb => cb({\n buffered: bufferedPercent,\n }));\n });\n video.addEventListener('timeupdate', () => {\n listeners.timeupdate.forEach(cb => cb({\n position: video.currentTime,\n duration: video.duration,\n }));\n });\n video.addEventListener('waiting', () => {\n listeners.waiting.forEach(cb => {\n cb();\n });\n });\n\n // 构建WechatMinigame.Video兼容对象\n const videoProxy: WechatMinigame.Video = {\n // 属性\n get autoplay() {\n return video.autoplay;\n },\n set autoplay(value) {\n video.autoplay = value;\n },\n\n get backgroundColor() {\n return video.style.backgroundColor;\n },\n set backgroundColor(value) {\n video.style.backgroundColor = value;\n },\n\n get controls() {\n return video.controls;\n },\n set controls(value) {\n video.controls = value;\n },\n\n get enablePlayGesture() {\n return false;\n },\n set enablePlayGesture(_value) {\n /* web不支持 */\n },\n\n get enableProgressGesture() {\n return false;\n },\n set enableProgressGesture(_value) {\n /* web不支持 */\n },\n\n get height() {\n return video.height;\n },\n set height(value) {\n video.height = value;\n },\n\n get initialTime() {\n return initialTime ?? 0;\n },\n set initialTime(value) {\n video.currentTime = value;\n },\n\n get live() {\n return false;\n },\n set live(_value) {\n /* web不支持 */\n },\n\n get loop() {\n return video.loop;\n },\n set loop(value) {\n video.loop = value;\n },\n\n get muted() {\n return video.muted;\n },\n set muted(value) {\n video.muted = value;\n },\n\n get obeyMuteSwitch() {\n return false;\n },\n set obeyMuteSwitch(_value) {\n /* web不支持 */\n },\n\n get objectFit() {\n return video.style.objectFit;\n },\n set objectFit(value) {\n video.style.objectFit = value;\n },\n\n get playbackRate() {\n return video.playbackRate;\n },\n set playbackRate(value) {\n video.playbackRate = value;\n },\n\n get poster() {\n return video.poster;\n },\n set poster(value) {\n video.poster = value;\n },\n\n get showCenterPlayBtn() {\n return false;\n },\n set showCenterPlayBtn(_value) {\n /* web不支持 */\n },\n\n get showProgress() {\n return video.controls;\n },\n set showProgress(_value) {\n /* web通过controls控制 */\n },\n\n get showProgressInControlMode() {\n return video.controls;\n },\n set showProgressInControlMode(_value) {\n /* web通过controls控制 */\n },\n\n get src() {\n return video.src;\n },\n set src(value) {\n video.src = value;\n },\n\n get width() {\n return video.width;\n },\n set width(value) {\n video.width = value;\n },\n\n get x() {\n return parseFloat(video.style.left) || 0;\n },\n set x(value) {\n video.style.position = 'absolute';\n video.style.left = `${value}px`;\n },\n\n get y() {\n return parseFloat(video.style.top) || 0;\n },\n set y(value) {\n video.style.position = 'absolute';\n video.style.top = `${value}px`;\n },\n\n // on* 属性形式的事件回调\n get onended() {\n return video.onended as WechatMinigame.Video['onended'];\n },\n set onended(value) {\n video.onended = value;\n },\n\n get onerror() {\n return video.onerror as WechatMinigame.Video['onerror'];\n },\n set onerror(value) {\n video.onerror = value as HTMLVideoElement['onerror'];\n },\n\n get onpause() {\n return video.onpause as WechatMinigame.Video['onpause'];\n },\n set onpause(value) {\n video.onpause = value;\n },\n\n get onplay() {\n return video.onplay as WechatMinigame.Video['onplay'];\n },\n set onplay(value) {\n video.onplay = value;\n },\n\n get onprogress() {\n return video.onprogress as WechatMinigame.Video['onprogress'];\n },\n set onprogress(value) {\n video.onprogress = value;\n },\n\n get ontimeupdate() {\n return video.ontimeupdate as WechatMinigame.Video['ontimeupdate'];\n },\n set ontimeupdate(value) {\n video.ontimeupdate = value;\n },\n\n get onwaiting() {\n return video.onwaiting as WechatMinigame.Video['onwaiting'];\n },\n set onwaiting(value) {\n video.onwaiting = value;\n },\n\n // 方法\n play(): Promise<void> {\n return video.play();\n },\n\n pause(): Promise<void> {\n video.pause();\n return Promise.resolve();\n },\n\n stop(): Promise<void> {\n video.pause();\n video.currentTime = 0;\n return Promise.resolve();\n },\n\n seek(time: number): Promise<void> {\n video.currentTime = time;\n return Promise.resolve();\n },\n\n async requestFullScreen(direction: 0 | 90 | -90): Promise<void> {\n // 先进入全屏\n await video.requestFullscreen();\n\n // 根据 direction 参数锁定屏幕方向\n // 0: 正常竖向 (portrait)\n // 90: 屏幕逆时针90度 (landscape-secondary,home键在右)\n // -90: 屏幕顺时针90度 (landscape-primary,home键在左)\n const orientationApi = screen.orientation as ScreenOrientation & {\n lock?: (orientation: string) => Promise<void>;\n };\n\n if (orientationApi?.lock) {\n let orientation: string;\n switch (direction) {\n case 0:\n orientation = 'portrait';\n break;\n case 90:\n orientation = 'landscape-secondary';\n break;\n case -90:\n orientation = 'landscape-primary';\n break;\n default:\n return;\n }\n try {\n await orientationApi.lock(orientation);\n } catch {\n // 屏幕方向锁定可能不被支持或被拒绝,忽略错误\n }\n }\n },\n\n exitFullScreen(): Promise<void> {\n document.exitFullscreen();\n return Promise.resolve();\n },\n\n destroy(): void {\n video.pause();\n video.src = '';\n video.load();\n video.remove();\n // 清理所有监听器\n Object.values(listeners).forEach(set => set.clear());\n },\n\n // on/off 方法形式的事件监听\n onEnded(listener: WechatMinigame.OnEndedCallback): void {\n listeners.ended.add(listener as Callback);\n },\n offEnded(listener?: WechatMinigame.OffEndedCallback): void {\n if (listener) {\n listeners.ended.delete(listener as Callback);\n } else {\n listeners.ended.clear();\n }\n },\n\n onError(listener: WechatMinigame.VideoOnErrorCallback): void {\n listeners.error.add(listener as Callback);\n },\n offError(listener?: WechatMinigame.VideoOffErrorCallback): void {\n if (listener) {\n listeners.error.delete(listener as Callback);\n } else {\n listeners.error.clear();\n }\n },\n\n onPause(listener: WechatMinigame.OnPauseCallback): void {\n listeners.pause.add(listener as Callback);\n },\n offPause(listener?: WechatMinigame.OffPauseCallback): void {\n if (listener) {\n listeners.pause.delete(listener as Callback);\n } else {\n listeners.pause.clear();\n }\n },\n\n onPlay(listener: WechatMinigame.OnPlayCallback): void {\n listeners.play.add(listener as Callback);\n },\n offPlay(listener?: WechatMinigame.OffPlayCallback): void {\n if (listener) {\n listeners.play.delete(listener as Callback);\n } else {\n listeners.play.clear();\n }\n },\n\n onProgress(listener: WechatMinigame.OnProgressCallback): void {\n listeners.progress.add(listener as Callback);\n },\n offProgress(listener?: WechatMinigame.OffProgressCallback): void {\n if (listener) {\n listeners.progress.delete(listener as Callback);\n } else {\n listeners.progress.clear();\n }\n },\n\n onTimeUpdate(listener: WechatMinigame.VideoOnTimeUpdateCallback): void {\n listeners.timeupdate.add(listener as Callback);\n },\n offTimeUpdate(listener?: WechatMinigame.VideoOffTimeUpdateCallback): void {\n if (listener) {\n listeners.timeupdate.delete(listener as Callback);\n } else {\n listeners.timeupdate.clear();\n }\n },\n\n onWaiting(listener: WechatMinigame.OnWaitingCallback): void {\n listeners.waiting.add(listener as Callback);\n },\n offWaiting(listener?: WechatMinigame.OffWaitingCallback): void {\n if (listener) {\n listeners.waiting.delete(listener as Callback);\n } else {\n listeners.waiting.clear();\n }\n },\n };\n\n return videoProxy;\n}","/**\n * @internal\n * 同步/异步的公共代码。\n */\n\nimport { NOT_FOUND_ERROR, NOTHING_TO_ZIP_ERROR, ROOT_DIR, type ExistsOptions } from 'happy-opfs';\nimport { Err, Lazy, Ok, RESULT_FALSE, RESULT_VOID, tryResult, type AsyncIOResult, type IOResult, type Result, type VoidIOResult } from 'happy-rusty';\nimport { bufferSourceToAb, miniGameFailureToError } from '../internal/mod.ts';\nimport { normalize } from '../path/mod.ts';\nimport { asyncResultify } from '../utils/mod.ts';\nimport type { ReadOptions, WriteFileContent } from './fs_define.ts';\n\n// #region Internal Variables\n\n/**\n * 小游戏文件系统管理器实例。\n *\n */\nconst fs = /*#__PURE__*/ Lazy(() => wx.getFileSystemManager());\n\n/**\n * 用户数据根目录,`wxfile://usr` 或 `http://usr`。\n *\n */\nconst usrPath = /*#__PURE__*/ Lazy(() => wx.env.USER_DATA_PATH);\n\n/**\n * 根路径,`wxfile://` 或 `http://`。\n *\n */\nconst rootPath = /*#__PURE__*/ Lazy(() => {\n const path = usrPath.force();\n // 剥离 `usr`\n return `${path.split('://')[0]}://`;\n});\n\n// #endregion\n\n/**\n * zip 操作的结果。\n */\nexport type ZipIOResult = IOResult<Uint8Array<ArrayBuffer> | void>;\n\nexport const EMPTY_BYTES: Uint8Array<ArrayBuffer> = /*#__PURE__*/ new Uint8Array(0);\n\n/**\n * 获取小游戏文件系统管理器实例。\n * @returns 文件系统管理器实例。\n */\nexport function getFs(): WechatMinigame.FileSystemManager {\n return fs.force();\n}\n\n/**\n * 获取文件系统的根路径。\n * @returns 文件系统的根路径。\n */\nexport function getUsrPath(): string {\n return usrPath.force();\n}\n\n/**\n * 验证并标准化路径,返回绝对路径。\n *\n * 支持两种输入格式:\n * 1. 完整路径:以 `wxfile://` 或 `http://` 开头(如 `wxfile://usr/test`)\n * 2. 相对路径:以 `/` 开头(如 `/test`),会自动拼接 `wx.env.USER_DATA_PATH`\n *\n * @param path - 待验证的路径。\n * @returns 验证成功返回标准化后的绝对路径,失败返回错误信息。\n */\nexport function validateAbsolutePath(path: string): IOResult<string> {\n const typeError = validatePathType(path);\n if (typeError) return typeError;\n\n // 是否已经是完整路径\n let isFullPath = false;\n // 检查是否为完整路径(以 `wxfile://` 或 `http://` 开头)\n if (path.startsWith(rootPath.force())) {\n isFullPath = true;\n // 先剥离协议前缀,避免 normalize 将 `://` 转换为 `:/`\n path = path.slice(rootPath.force().length);\n // 传根路径没意义\n if (!path) {\n return Err(new Error('Path must not be root directory'));\n }\n }\n\n // 标准化路径(处理 `.`、`..` 等)并去除末尾的 `/`\n const normalized = normalize(path);\n path = normalized.length > 1 && normalized[normalized.length - 1] === ROOT_DIR\n ? normalized.slice(0, -1)\n : normalized;\n\n // 完整路径:重新拼接协议前缀\n if (isFullPath) {\n // 还是根路径\n if (path === ROOT_DIR) {\n return Err(new Error('Path must not be root directory'));\n }\n // 确保路径不是以 `/` 开头,避免 `wxfile:///usr` 这样的多斜杠情况\n if (path[0] === ROOT_DIR) {\n path = path.slice(1);\n }\n return Ok(rootPath.force() + path);\n }\n\n // 相对路径:必须以 `/` 开头\n if (path[0] !== ROOT_DIR) {\n return Err(new Error(`Path must be absolute (start with '/'): '${path}'`));\n }\n\n // 拼接用户数据根目录\n return Ok(usrPath.force() + path);\n}\n\n/**\n * 验证可读路径(用于只读操作:readFile、stat、readDir)。\n *\n * 支持三种输入格式:\n * 1. 完整路径:以 `wxfile://` 或 `http://` 开头(如 `wxfile://usr/test`)\n * 2. 用户数据相对路径:以 `/` 开头(如 `/test`),会自动拼接 `wx.env.USER_DATA_PATH`\n * 3. 代码包路径:不以 `./`、`../` 开头的相对路径(如 `images/logo.png`),直接返回原路径\n *\n * @param path - 待验证的路径。\n * @returns 验证成功返回标准化后的路径,失败返回错误信息。\n */\nexport function validateReadablePath(path: string): IOResult<string> {\n const typeError = validatePathType(path);\n if (typeError) return typeError;\n\n // 对于完整路径和以 / 开头的路径,使用现有的绝对路径验证\n if (path.startsWith(rootPath.force()) || path.startsWith(ROOT_DIR)) {\n return validateAbsolutePath(path);\n }\n\n // 既不是完整路径,也不以 / 开头,检查是否为代码包路径(不以 ./、../ 开头)\n if (path.startsWith('./') || path.startsWith('../')) {\n return Err(new Error(`Invalid path: '${path}'. Code package paths must not start with './' or '../'`));\n }\n\n // 代码包路径返回标准化后的结果\n const normalized = normalize(path);\n return Ok(normalized);\n}\n\n/**\n * 验证提供的 ExistsOptions 是否有效。\n * `isDirectory` 和 `isFile` 不能同时为 `true`。\n *\n * @param options - 要验证的 ExistsOptions。\n * @returns 表示成功或错误的 VoidIOResult。\n */\nexport function validateExistsOptions(options?: ExistsOptions): VoidIOResult {\n const { isDirectory = false, isFile = false } = options ?? {};\n\n return isDirectory && isFile\n ? Err(new Error('isDirectory and isFile cannot both be true'))\n : RESULT_VOID;\n}\n\n/**\n * 判断错误是否为 `NotFoundError`。\n * @param error - 要检查的错误。\n * @returns 如果是 `NotFoundError` 返回 `true`,否则返回 `false`。\n */\nexport function isNotFoundError(error: Error): boolean {\n return error.name === NOT_FOUND_ERROR;\n}\n\n/**\n * 将错误对象转换为 IOResult 类型。\n * @typeParam T - Result 的 Ok 类型。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n * @returns 转换后的 IOResult 对象。\n */\nexport function fileErrorToResult<T>(error: FileError): IOResult<T> {\n const err = miniGameFailureToError(error);\n\n if (isNotFoundFileError(err)) {\n err.name = NOT_FOUND_ERROR;\n }\n\n return Err(err);\n}\n\n/**\n * 处理 `mkdir` 的错误。\n */\nexport function fileErrorToMkdirResult(error: FileError): VoidIOResult {\n // 已存在当做成功\n return isAlreadyExistsFileError(error) ? RESULT_VOID : fileErrorToResult(error);\n}\n\n/**\n * 处理 `remove` 的错误。\n */\nexport function fileErrorToRemoveResult(error: FileError): VoidIOResult {\n // 目标 path 本就不存在,当做成功\n return isNotFoundFileError(error) ? RESULT_VOID : fileErrorToResult(error);\n}\n\n/*\n * 创建 `NothingToZipError` 错误。\n */\nexport function createNothingToZipError(): IOResult<never> {\n const error = new Error('Nothing to zip');\n error.name = NOTHING_TO_ZIP_ERROR;\n\n return Err(error);\n}\n\n/*\n * 创建\"文件不存在且 create 为 false\"的错误。\n * @param filePath - 文件路径。\n * @returns 错误结果。\n */\nexport function createFileNotExistsError(filePath: string): IOResult<never> {\n return Err(new Error(`Cannot append to non-existent file: ${filePath}`));\n}\n\n/*\n * 创建\"目录已存在但是文件\"的错误。\n * @param dirPath - 目录路径。\n * @returns 错误结果。\n */\nexport function createDirIsFileError(dirPath: string): IOResult<never> {\n return Err(new Error(`Path already exists but is a file: ${dirPath}`));\n}\n\n/**\n * 获取读取文件的编码。\n * @returns 返回 `'utf8'` 或 `undefined`(读取二进制时不传 encoding)。\n */\nexport function getReadFileEncoding(options?: ReadOptions): 'utf8' | undefined {\n // NOTE: 想要读取 ArrayBuffer 就不能传 encoding\n // 如果传了 'bytes' 或不传,返回 undefined\n return options?.encoding === 'utf8' ? 'utf8' : undefined;\n}\n\n/**\n * 获取写入文件的参数。\n */\nexport function getWriteFileContents(contents: WriteFileContent): IOResult<GetWriteFileContents> {\n const isBin = typeof contents !== 'string';\n\n let data: string | ArrayBuffer;\n\n if (isBin) {\n const result = tryResult(() => bufferSourceToAb(contents));\n if (result.isErr()) return result.asErr();\n\n data = result.unwrap();\n } else {\n data = contents;\n }\n\n const encoding = isBin ? undefined : 'utf8';\n\n return Ok({\n data,\n encoding,\n });\n}\n\n/**\n * 获取 `exists` 的结果。\n *\n * 注意:Windows 和 Mac 平台 `stat` 代码包内不存在的文件时, 平台会误报成功并返回 `stats.size = -1`。\n * 此处通过 `stats.size < 0` 判断并视为不存在, 以兼容该平台 bug。\n *\n * TODO(platform-wx): 待微信 Windows/Mac 平台修复该 stat 误报 bug 后, 移除 `stats.size < 0` 判断。\n * 跟踪点: `mina_fs_shared.ts` getExistsResult。\n */\nexport function getExistsResult(statResult: IOResult<WechatMinigame.Stats>, options?: ExistsOptions): IOResult<boolean> {\n return statResult.map(stats => {\n const { isDirectory = false, isFile = false } = options ?? {};\n\n const notExist =\n stats.size < 0 // TODO(platform-wx): Windows/Mac 平台 stat 代码包不存在文件时的误报, 待平台修复后移除\n || (isDirectory && stats.isFile())\n || (isFile && stats.isDirectory());\n\n return !notExist;\n }).orElse(err => {\n return isNotFoundError(err) ? RESULT_FALSE : statResult.asErr();\n });\n}\n\n/**\n * 判断文件/目录是否存在(不区分类型)。\n *\n * 相比 `stat` + `getExistsResult`,使用底层 `access` 接口开销更小,不会构造 `Stats` 对象。\n * 当 `ExistsOptions` 不需要检查 `isDirectory`/`isFile` 时使用此实现。\n *\n * @param path - 文件或目录路径。\n * @returns 存在返回 `true`,不存在返回 `false`,其他错误透传。\n */\nexport async function accessExists(path: string): AsyncIOResult<boolean> {\n const pathRes = validateReadablePath(path);\n if (pathRes.isErr()) return pathRes.asErr();\n path = pathRes.unwrap();\n\n const accessRes = await asyncResultify(getFs().access)({ path });\n return accessResultToExists(accessRes);\n}\n\n/**\n * `accessExists` 的同步版本。\n *\n * @param path - 文件或目录路径。\n * @returns 存在返回 `true`,不存在返回 `false`,其他错误透传。\n */\nexport function accessExistsSync(path: string): IOResult<boolean> {\n const pathRes = validateReadablePath(path);\n if (pathRes.isErr()) return pathRes.asErr();\n path = pathRes.unwrap();\n\n const accessRes = tryResult(() => getFs().accessSync(path));\n return accessResultToExists(accessRes);\n}\n\n\n/**\n * 根据 `recursive` 不同标准化 `stat` 的结果(recursive=true 的时候开发者工具对于文件和空文件夹会返回单个 Stats)。\n * - `recursive=false`: 返回单个 `Stats` 或 `FileStats[]`\n * - `recursive=true`: 始终返回 `FileStats[]`,即使是单个文件或空目录\n * - 如果是单个 `Stats`,包装成数组,path 设为 '' 表示当前项目\n */\nexport function normalizeStats(statsOrFileStats: WechatMinigame.Stats | WechatMinigame.FileStats[], recursive: boolean): WechatMinigame.Stats | WechatMinigame.FileStats[] {\n if (Array.isArray(statsOrFileStats)) {\n return statsOrFileStats.map(({ path, stats }) => ({\n path: path.replace(/^\\/+/, ''), // 返回相对路径, 去掉开头的 `/`(安卓子项目 path 不以 `/` 开头)\n stats,\n })).sort((a, b) => a.path.localeCompare(b.path)); // 按 path 排序\n }\n\n // 只要是 recursive 就返回数组(即使是文件或者空目录))\n return recursive\n ? [{\n path: '', // 当前文件夹本身的相对路径\n stats: statsOrFileStats,\n }]\n : statsOrFileStats;\n}\n\n// #region Internal Types\n\ntype FileError = WechatMinigame.FileError | (Error & {\n errno?: number;\n});\n\ninterface GetWriteFileContents {\n data: string | ArrayBuffer;\n encoding?: 'utf8';\n}\n\n// #endregion\n\n// #region Internal Functions\n\n/**\n * 标准化同步或异步的文件错误对象。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n */\nfunction normalizeFileError(error: FileError): WechatMinigame.FileError {\n return error instanceof Error\n ? {\n errCode: error.errno ?? 0,\n errMsg: error.message,\n }\n : error;\n}\n\n/**\n * 判断是否文件或者文件夹不存在。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n */\nfunction isNotFoundFileError(error: FileError): boolean {\n // 1300002\tno such file or directory ${path}\n const { errCode, errMsg } = normalizeFileError(error);\n // 可能没有errCode\n return errCode === 1300002\n || errMsg.includes('no such file or directory');\n}\n\n/**\n * 判断是否文件或者文件夹已存在。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n */\nfunction isAlreadyExistsFileError(error: FileError): boolean {\n // 1301005\tfile already exists ${dirPath}\t已有同名文件或目录\n const { errCode, errMsg } = normalizeFileError(error);\n // 可能没有errCode\n return errCode === 1301005\n || errMsg.includes('already exists');\n}\n\n/**\n * 验证 path 是否为字符串类型。\n * @param path - 待验证的路径。\n * @returns 如果不是字符串返回错误,否则返回 undefined。\n */\nfunction validatePathType(path: unknown): IOResult<never> | undefined {\n if (typeof path !== 'string') {\n return Err(new TypeError(`Path must be a string but received ${typeof path}`));\n }\n}\n\n/**\n * 将 `access`/`accessSync` 调用的结果转换为存在性结果。\n * - 成功 → `true`\n * - NotFound → `false`\n * - 其他错误 → 透传\n */\nfunction accessResultToExists<T, E>(accessRes: Result<T, E>): IOResult<boolean> {\n return accessRes\n .map(() => true)\n .orElse(err => {\n // 归一化错误(识别 not found 并设置 name),再判断是否为不存在\n const normalized = fileErrorToResult<boolean>(err as FileError).unwrapErr();\n return isNotFoundError(normalized) ? RESULT_FALSE : Err(normalized);\n });\n}\n\n// #endregion\n","/**\n * @internal\n * 小游戏平台的视频帧源实现。\n */\n\nimport { Err, Ok, tryAsyncResult, type AsyncVoidIOResult, type IOResult } from 'happy-rusty';\nimport { validateReadablePath } from '../../fs/mina_fs_shared.ts';\nimport type { CreateVideoFrameSourceFromFileOptions, CreateVideoFrameSourceOptions, PixelVideoFrame, VideoFrameSource, VideoFrameSourceFrame, VideoFrameSourceState } from './defines.ts';\n\n// #region Exports\n\n/**\n * 判断小游戏平台是否支持视频帧源。\n *\n * @returns 是否支持视频帧源。\n */\nexport function isVideoFrameSourceSupported(): boolean {\n return typeof wx.createVideoDecoder === 'function';\n}\n\n/**\n * 创建小游戏平台视频帧源。\n *\n * @param options - 视频帧源创建选项。\n * @returns 视频帧源创建结果。\n */\nexport function createVideoFrameSource(options: CreateVideoFrameSourceOptions): IOResult<VideoFrameSource> {\n if (!isVideoFrameSourceSupported()) {\n return Err(new Error('VideoFrameSource is not supported on this minigame platform'));\n }\n\n const decoder = wx.createVideoDecoder();\n let state: VideoFrameSourceState = 'idle';\n let lastFrame: PixelVideoFrame | null = null;\n let currentTime = 0;\n let width = options.width ?? 0;\n let height = options.height ?? 0;\n\n const frameListeners = new Set<FrameListener>();\n const endedListeners = new Set<EndedListener>();\n const errorListeners = new Set<ErrorListener>();\n\n const pullFrame = (): PixelVideoFrame | null => {\n const frameData = decoder.getFrameData() as WechatMinigame.FrameDataOptions | null;\n if (frameData == null) return null;\n\n const frame = toPixelFrame(frameData);\n currentTime = frame.timestamp;\n width = frame.width;\n height = frame.height;\n return frame;\n };\n\n const handleEnded = () => {\n if (state !== 'destroyed') state = 'ended';\n endedListeners.forEach(listener => listener());\n };\n\n decoder.on('ended', handleEnded);\n\n return Ok({\n get state() {\n return state;\n },\n get currentTime() {\n return currentTime;\n },\n get duration() {\n return Number.NaN;\n },\n get width() {\n return width;\n },\n get height() {\n return height;\n },\n async play(): AsyncVoidIOResult {\n const result = await tryAsyncResult(decoder.start({ source: options.source }));\n return result.inspect(() => {\n state = 'playing';\n });\n },\n async pause(): AsyncVoidIOResult {\n const result = await tryAsyncResult(() => decoder.stop());\n return result.inspect(() => {\n state = 'paused';\n });\n },\n async stop(): AsyncVoidIOResult {\n const result = await tryAsyncResult(() => decoder.stop());\n return result.inspect(() => {\n state = 'paused';\n currentTime = 0;\n });\n },\n async seek(time: number): AsyncVoidIOResult {\n // wx.VideoDecoder.seek 的 position 单位为 ms,公开 API 单位为秒,需转换\n const result = await tryAsyncResult(() => decoder.seek(time * 1000));\n return result.inspect(() => {\n currentTime = time;\n });\n },\n getFrame(): IOResult<VideoFrameSourceFrame | null> {\n lastFrame = pullFrame();\n if (lastFrame != null) {\n frameListeners.forEach(listener => listener(lastFrame as PixelVideoFrame));\n }\n const frame = lastFrame;\n lastFrame = null;\n return Ok(frame);\n },\n onFrame(listener: FrameListener): void {\n frameListeners.add(listener);\n },\n offFrame(listener?: FrameListener): void {\n if (listener) frameListeners.delete(listener);\n else frameListeners.clear();\n },\n onEnded(listener: EndedListener): void {\n endedListeners.add(listener);\n },\n offEnded(listener?: EndedListener): void {\n if (listener) endedListeners.delete(listener);\n else endedListeners.clear();\n },\n onError(listener: ErrorListener): void {\n errorListeners.add(listener);\n },\n offError(listener?: ErrorListener): void {\n if (listener) errorListeners.delete(listener);\n else errorListeners.clear();\n },\n destroy(): void {\n if (state === 'destroyed') return;\n state = 'destroyed';\n decoder.off('ended', handleEnded);\n void tryAsyncResult(decoder.remove());\n frameListeners.clear();\n endedListeners.clear();\n errorListeners.clear();\n },\n });\n}\n\n/**\n * 从小游戏本地文件创建视频帧源。\n *\n * @param filePath - 视频文件路径。\n * @param options - 视频帧源创建选项。\n * @returns 视频帧源创建结果。\n */\nexport function createVideoFrameSourceFromFile(filePath: string, options?: CreateVideoFrameSourceFromFileOptions): IOResult<VideoFrameSource> {\n return validateReadablePath(filePath)\n .andThen(source => createVideoFrameSource({\n ...options,\n source,\n }));\n}\n\n// #endregion\n\n// #region Internal Types\n\ntype FrameListener = (frame: VideoFrameSourceFrame) => void;\ntype EndedListener = () => void;\ntype ErrorListener = (error: Error) => void;\n\n// #endregion\n\n// #region Internal Functions\n\nfunction toPixelFrame(frameData: WechatMinigame.FrameDataOptions): PixelVideoFrame {\n return {\n kind: 'pixels',\n format: 'rgba',\n data: new Uint8Array(frameData.data),\n width: frameData.width,\n height: frameData.height,\n timestamp: frameData.pkPts / 1000,\n release(): void {\n return undefined;\n },\n };\n}\n\n// #endregion\n","/**\n * @internal\n * Web 平台的视频帧源实现。\n */\n\nimport { readBlobFile } from 'happy-opfs';\nimport { Ok, tryAsyncResult, type AsyncIOResult, type AsyncVoidIOResult, type IOResult } from 'happy-rusty';\nimport type { CreateVideoFrameSourceFromFileOptions, CreateVideoFrameSourceOptions, ElementVideoFrame, VideoFrameSource, VideoFrameSourceFrame, VideoFrameSourceState } from './defines.ts';\n\n// #region Exports\n\n/**\n * 判断 Web 平台是否支持视频帧源。\n *\n * @returns 是否支持视频帧源。\n */\nexport function isVideoFrameSourceSupported(): boolean {\n return typeof document !== 'undefined' && typeof document.createElement === 'function';\n}\n\n/**\n * 创建 Web 平台视频帧源。\n *\n * @param options - 视频帧源创建选项。\n * @returns 视频帧源。\n */\nexport function createVideoFrameSource(options: CreateVideoFrameSourceOptions): VideoFrameSource {\n const video = document.createElement('video') as VideoElementWithFrameCallback;\n let state: VideoFrameSourceState = 'idle';\n let hasNewFrame = false;\n let frameCallbackId: number | undefined;\n let rafId: number | undefined;\n let lastFrameTime = -1;\n\n const frameListeners = new Set<FrameListener>();\n const endedListeners = new Set<EndedListener>();\n const errorListeners = new Set<ErrorListener>();\n\n const createFrame = (): ElementVideoFrame => ({\n kind: 'element',\n element: video,\n width: video.videoWidth || video.width,\n height: video.videoHeight || video.height,\n timestamp: video.currentTime,\n release(): void {\n return undefined;\n },\n });\n\n const emitFrame = () => {\n if (state === 'destroyed') return;\n hasNewFrame = true;\n const frame = createFrame();\n frameListeners.forEach(listener => listener(frame));\n };\n\n const requestNextFrame = () => {\n if (state === 'destroyed') return;\n\n if (typeof video.requestVideoFrameCallback === 'function') {\n frameCallbackId = video.requestVideoFrameCallback((_now, metadata) => {\n lastFrameTime = metadata.mediaTime;\n emitFrame();\n requestNextFrame();\n });\n return;\n }\n\n rafId = requestAnimationFrame(() => {\n if (video.currentTime !== lastFrameTime) {\n lastFrameTime = video.currentTime;\n emitFrame();\n }\n requestNextFrame();\n });\n };\n\n const cancelFrameRequest = () => {\n if (frameCallbackId != null && typeof video.cancelVideoFrameCallback === 'function') {\n video.cancelVideoFrameCallback(frameCallbackId);\n frameCallbackId = undefined;\n }\n if (rafId != null) {\n cancelAnimationFrame(rafId);\n rafId = undefined;\n }\n };\n\n video.src = options.source;\n video.loop = options.loop ?? false;\n video.muted = options.muted ?? false;\n video.autoplay = options.autoplay ?? false;\n video.preload = 'auto';\n video.setAttribute('playsinline', 'true');\n video.setAttribute('webkit-playsinline', 'true');\n\n if (options.crossOrigin !== undefined) {\n video.crossOrigin = options.crossOrigin;\n }\n if (options.width !== undefined) {\n video.width = options.width;\n }\n if (options.height !== undefined) {\n video.height = options.height;\n }\n\n video.style.display = 'none';\n document.body.appendChild(video);\n state = 'loading';\n\n video.addEventListener('loadedmetadata', () => {\n if (state !== 'destroyed') state = 'ready';\n });\n video.addEventListener('ended', () => {\n if (state !== 'destroyed') state = 'ended';\n endedListeners.forEach(listener => listener());\n });\n video.addEventListener('error', () => {\n if (state !== 'destroyed') state = 'error';\n const error = new Error(video.error?.message ?? 'Video frame source error');\n errorListeners.forEach(listener => listener(error));\n });\n\n requestNextFrame();\n\n const source: VideoFrameSource = {\n get state() {\n return state;\n },\n get currentTime() {\n return video.currentTime;\n },\n get duration() {\n return video.duration;\n },\n get width() {\n return video.videoWidth || video.width;\n },\n get height() {\n return video.videoHeight || video.height;\n },\n async play(): AsyncVoidIOResult {\n const result = await tryAsyncResult(video.play());\n return result.inspect(() => {\n state = 'playing';\n });\n },\n async pause(): AsyncVoidIOResult {\n return tryAsyncResult(() => {\n video.pause();\n state = 'paused';\n });\n },\n async stop(): AsyncVoidIOResult {\n return tryAsyncResult(() => {\n video.pause();\n video.currentTime = 0;\n state = 'paused';\n });\n },\n async seek(time: number): AsyncVoidIOResult {\n return tryAsyncResult(() => {\n video.currentTime = time;\n });\n },\n getFrame(): IOResult<VideoFrameSourceFrame | null> {\n if (!hasNewFrame || state === 'destroyed') {\n return Ok(null);\n }\n hasNewFrame = false;\n return Ok(createFrame());\n },\n onFrame(listener: FrameListener): void {\n frameListeners.add(listener);\n },\n offFrame(listener?: FrameListener): void {\n if (listener) frameListeners.delete(listener);\n else frameListeners.clear();\n },\n onEnded(listener: EndedListener): void {\n endedListeners.add(listener);\n },\n offEnded(listener?: EndedListener): void {\n if (listener) endedListeners.delete(listener);\n else endedListeners.clear();\n },\n onError(listener: ErrorListener): void {\n errorListeners.add(listener);\n },\n offError(listener?: ErrorListener): void {\n if (listener) errorListeners.delete(listener);\n else errorListeners.clear();\n },\n destroy(): void {\n if (state === 'destroyed') return;\n state = 'destroyed';\n cancelFrameRequest();\n video.pause();\n video.src = '';\n video.load();\n video.remove();\n frameListeners.clear();\n endedListeners.clear();\n errorListeners.clear();\n },\n };\n\n if (options.autoplay) {\n void source.play();\n }\n\n return source;\n}\n\n/**\n * 从 Web 本地文件创建视频帧源。\n *\n * @param filePath - 视频文件路径。\n * @param options - 视频帧源创建选项。\n * @returns 视频帧源创建结果。\n */\nexport async function createVideoFrameSourceFromFile(filePath: string, options?: CreateVideoFrameSourceFromFileOptions): AsyncIOResult<VideoFrameSource> {\n const readRes = await readBlobFile(filePath);\n\n return readRes.andThen(blob => {\n const url = URL.createObjectURL(blob);\n const source = createVideoFrameSource({\n ...options,\n source: url,\n });\n const destroy = source.destroy.bind(source);\n\n source.destroy = () => {\n destroy();\n URL.revokeObjectURL(url);\n };\n\n return Ok(source);\n });\n}\n\n// #endregion\n\n// #region Internal Types\n\ntype FrameListener = (frame: VideoFrameSourceFrame) => void;\ntype EndedListener = () => void;\ntype ErrorListener = (error: Error) => void;\ntype RequestVideoFrameCallback = (now: number, metadata: RequestVideoFrameCallbackMetadata) => void;\ntype VideoElementWithFrameCallback = HTMLVideoElement & {\n requestVideoFrameCallback?: (callback: RequestVideoFrameCallback) => number;\n cancelVideoFrameCallback?: (id: number) => void;\n};\n\ninterface RequestVideoFrameCallbackMetadata {\n mediaTime: number;\n}\n\n// #endregion\n","import { Ok, type AsyncIOResult, type IOResult } from 'happy-rusty';\nimport { IS_MINA } from '../../../macros/env.ts';\nimport type { CreateVideoFrameSourceFromFileOptions, CreateVideoFrameSourceOptions, VideoFrameSource } from './defines.ts';\nimport {\n isVideoFrameSourceSupported as isMinaVideoFrameSourceSupported,\n createVideoFrameSource as minaCreateVideoFrameSource,\n createVideoFrameSourceFromFile as minaCreateVideoFrameSourceFromFile,\n} from './mina.ts';\nimport {\n isVideoFrameSourceSupported as isWebVideoFrameSourceSupported,\n createVideoFrameSource as webCreateVideoFrameSource,\n createVideoFrameSourceFromFile as webCreateVideoFrameSourceFromFile,\n} from './web.ts';\n\nexport * from './defines.ts';\n\n// #region Exports\n\n/**\n * 判断当前平台是否支持视频帧源。\n *\n * @returns 是否支持创建 VideoFrameSource。\n * @since 2.2.0\n * @example\n * ```ts\n * if (video.isVideoFrameSourceSupported()) {\n * const sourceRes = video.createVideoFrameSource({ source: 'https://example.com/video.mp4' });\n * }\n * ```\n */\nexport function isVideoFrameSourceSupported(): boolean {\n return IS_MINA ? isMinaVideoFrameSourceSupported() : isWebVideoFrameSourceSupported();\n}\n\n/**\n * 创建视频帧源,用于将视频帧上传到 RenderTexture/WebGL 纹理。\n *\n * @param options - 视频帧源创建选项。\n * @returns 视频帧源创建结果。\n * @since 2.2.0\n * @example\n * ```ts\n * const sourceRes = video.createVideoFrameSource({\n * source: 'https://example.com/video.mp4',\n * muted: true,\n * loop: true,\n * });\n * if (sourceRes.isOk()) {\n * const source = sourceRes.unwrap();\n * await source.play();\n * }\n * ```\n */\nexport function createVideoFrameSource(options: CreateVideoFrameSourceOptions): IOResult<VideoFrameSource> {\n return IS_MINA\n ? minaCreateVideoFrameSource(options)\n : Ok(webCreateVideoFrameSource(options));\n}\n\n/**\n * 从本地文件创建视频帧源。\n *\n * 小游戏平台会直接使用文件路径;Web 平台会从 OPFS 读取文件并创建 Blob URL。\n *\n * @param filePath - 视频文件路径。\n * @param options - 视频帧源创建选项。\n * @returns 视频帧源创建结果。\n * @since 2.2.0\n * @example\n * ```ts\n * const sourceRes = await video.createVideoFrameSourceFromFile('/videos/demo.mp4', {\n * muted: true,\n * });\n * if (sourceRes.isOk()) {\n * const source = sourceRes.unwrap();\n * await source.play();\n * }\n * ```\n */\nexport function createVideoFrameSourceFromFile(filePath: string, options?: CreateVideoFrameSourceFromFileOptions): AsyncIOResult<VideoFrameSource> {\n return IS_MINA\n ? Promise.resolve(minaCreateVideoFrameSourceFromFile(filePath, options))\n : webCreateVideoFrameSourceFromFile(filePath, options);\n}\n\n// #endregion\n","/**\n * 视频播放模块,提供创建和控制视频播放器的功能。\n * @module video\n */\nimport { IS_MINA } from '../../macros/env.ts';\nimport { createVideo as webCreateVideo } from './web_video.ts';\n\nexport * from './frame-source/mod.ts';\n\n/**\n * 创建视频播放器。\n * @param options - 视频配置选项。\n * @returns Video对象。\n * @since 2.0.0\n * @example\n * ```ts\n * const video = createVideo({\n * src: 'https://example.com/video.mp4',\n * width: 640,\n * height: 360,\n * autoplay: false,\n * });\n * video.play();\n * // 销毁视频\n * video.destroy();\n * ```\n */\nexport function createVideo(options: WechatMinigame.CreateVideoOption): WechatMinigame.Video {\n return (IS_MINA ? wx.createVideo : webCreateVideo)(options);\n}\n"],"mappings":"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