test(chromecast): [PKG-CAST-01] freeze published contracts and provenance

This commit is contained in:
Harvey Zhao committed 2026-09-13 16:35:26 +08:00
1 parent d5562b05b7
commit 30de007102
14 files changed
+833 -7

No files matched your search

+3 -2
View File
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js",
"test:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
@@ -96,7 +96,8 @@
"test:package:runtime": "node scripts/package-runtime.mjs --canonical",
"test:asr": "node --test test/asr.test.js test/asr-audio.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-distribution.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js",
"test:asr-types-package": "node refactor/scripts/asr-package-types.mjs",
"test:asr-demo": "node refactor/scripts/asr-local-demo.mjs"
"test:asr-demo": "node refactor/scripts/asr-local-demo.mjs",
"test:chromecast": "node --test test/chromecast.test.js"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -0,0 +1,40 @@
{
"schemaVersion": 1,
"task": "PKG-CAST-01",
"capturedAt": "2026-09-13T08:34:48.666Z",
"sourceCommit": "146b78d5767bba88612184869155a39317794537",
"baseline": {
"file": "refactor/baselines/chromecast-release.json",
"sha256": "a68745786d976afee10afb2bb0fd61aeba8f9fffa18737af3e395d25079e3eec"
},
"verifier": {
"file": "refactor/scripts/chromecast-contract.mjs",
"sha256LF": "6898481ebc4ab0374e80e0e6064a4ee983af912bf7d2880142631a992376ce8d"
},
"tests": {
"passed": 28,
"failed": 0,
"skipped": 0,
"file": "refactor/.cache/chromecast01-contract.log",
"sha256": "82412503a635cdeb2bf0ebaa64d611a70a9db51ccf151456d1da27c1a22096ef",
"scope": "Real frozen main/legacy 1.0.0 and 1.1.0, frozen source and workspace main/legacy; controlled DOM and SDK, no device session."
},
"inputs": {
"test/helpers/chromecast.js": "a51ad278dffde582c00d66cd3c0ea868141548d349a80a24a355856378ab5de7",
"test/chromecast.test.js": "be9d42e13ea495d7c266732ffe0273acb96d1a29b8ef330eba32156aaa5eccb8"
},
"differences": [
"1.0.0 eager SDK load and name-only result; 1.1.0 lazy load, raw SessionState getters and callbacks.",
"1.0.0 CJS namespace/default and no ESM/module/exports; 1.1.0 callable CJS and ESM.",
"Both registry gitHeads point to differently versioned Git manifests; archive authority retained.",
"Synchronous declarations omit actual asynchronous result and modern callbacks/getters.",
"Commercial Font Awesome Pro notice on the identical embedded icon; entitlement unverified."
],
"sourceReferences": [
"https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastContext",
"https://developers.google.com/cast/docs/web_sender/integrate"
],
"releaseReady": false,
"deviceValidated": false,
"sourceChanged": false
}
+50
View File
@@ -0,0 +1,50 @@
# Chromecast 历史契约
以 [真实发布归档](chromecast-release.json) 为准。1.0.0 和1.1.0 是捕获时 registry 的
全部稳定版;没有把工作区版本或 bundle banner 当作发布版本。1.0.0 的 banner 写
2.0.0,实际 tarball manifest 是1.0.0。两版 registry gitHead 对应插件 manifest 又
分别为2.0.0/1.0.2;逐成员差异独立冻结,来源核心5.1.2/5.3.1仅是提交关联。
## 分发与行为
| 项目 | 1.0.0 | 1.1.0 / 冻结工作区 |
| --- | --- | --- |
| 入口 | main/legacy/types;没有 module/exports | main/module/legacy/types/exports |
| CJS | 对象.default | 直接工厂 |
| 浏览器全局 | artplayerPluginChromecast | 同名工厂 |
| 工厂参数 | 必传 option,字段可选 | 相同;新增未声明的回调能力 |
| SDK加载 | 工厂覆盖availability回调,注册时加载 | 点击时初始化;注册不请求SDK |
| 注册 | async,SDK脚本onload后添加控件 | async,立即添加控件 |
| 注册结果 | name | name/getCastState/isCasting |
| 声明结果 | 同步、仅name | 仍错误声明同步、仅name |
| 点击 | void,查询当前SDK会话 | async,使用闭包保存的会话 |
| MIME/URL | live读取option或art.option.url | 相同 |
| 状态/错误回调 | 不执行新版回调 | onStateChange/onCastAvailable/onCastStart/onError |
选择1.1.0的懒加载作为候选正常行为,历史1.0.0 eager加载单独保留;不是要求恢复
旧版的提前网络访问。旧JS有效的CommonJS default与直接形式都属于后续兼容检查。
回调以属性方法调用,this为原option;自定义icon保留html包装和两个class。工厂不应
机械快照url/sdk/mimeType或回调。控件name为chromecast、position为right、tooltip为Chromecast。
getCastState实际返回原始SessionState,初始null;isCasting依据session是否为null,
不是接收端是否正在播放。状态回调则使用disconnected/connecting/connected/disconnecting。
MIME包含mp4/webm/ogg/ogv/mp3/wav/flv/mov/avi/wmv/mpd/m3u8及application/octet-stream
回退;查询/fragment去除与大小写处理保留。这张映射不证明Cast设备支持对应codec。
## SDK 与资源范围
SDK默认URL仍是gstatic的可变/v1入口,sdk允许覆盖。官方requestSession完成值并非
CastSession,应取得getCurrentSession;旧1.1.0实现存在相应错误。其他SDK初始状态、
并发、Promise和销毁缺陷由02冻结,03修复。依据:
[CastContext](https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastContext)、
[集成指南](https://developers.google.com/cast/docs/web_sender/integrate)。
默认SVG内嵌path与cast.svg相同,后者带Font Awesome Pro6.5.1商业许可标记;包的MIT
声明不能作为仓库已具备该资产授权的证据。VENDOR-10保持未决,03优先用自有图形
替换,06核对最终包与bundle;历史冻结文件不改写。SDK-06仍缺真实SDK版本和设备会话。
## 重放
`node refactor/scripts/chromecast-contract.mjs`核验SHA512/SHA256、成员、实际入口、
Git关联和图标来源;`yarn test:chromecast`重放受控公共行为。测试不发起外部网络
SDK加载或Cast会话;不等于真实Chrome、接收设备、媒体网络或发布验证。
+280
View File
@@ -0,0 +1,280 @@
{
"schemaVersion": 1,
"registry": "https://registry.npmjs.org/artplayer-plugin-chromecast",
"capturedAt": "2026-09-13T08:26:54.146Z",
"sourceCommit": "146b78d5767bba88612184869155a39317794537",
"sourceNormalization": "Git text normalized to LF; archive members hashed as bytes.",
"source": {
"docs/assets/example/chromecast.js": "edd4d232a667a3ca5bae247dca3c557431975b9ae38dc56cc590b57e327d048f",
"packages/artplayer-plugin-chromecast/README.md": "d277a82e54df2af679f94c92da2250d3779f07e2c37282d7d66e776f54fed323",
"packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.js": "4959661fc7f22eee3d32363bb0392a486c44499ec4a9c608ccfea55de29aaac6",
"packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.legacy.js": "e2ba7aebec099884bb806f3caf77f9ff739ff9f236aefdfd0a4e69a8295c9d01",
"packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.mjs": "b6d4e445a78f00fae53a35d239e62cc4a1448086d76058d69d8f9634a5754768",
"packages/artplayer-plugin-chromecast/package.json": "8157c06e4f764b1662ac108334e1240b07963400b906da6b21874e9a0e415adf",
"packages/artplayer-plugin-chromecast/src/cast.svg": "8c0f9ad82c2d6c70afb70ab5002204c4948dd282fbf52095aed4b07fa8d466d4",
"packages/artplayer-plugin-chromecast/src/index.js": "2ab8967cf7fdbf9fbac8a9afe73713e66047bad7e4d4afc802665ce594fa3274",
"packages/artplayer-plugin-chromecast/types/artplayer-plugin-chromecast.d.ts": "7826de339f5c027025c71215ed3c13995c01174bdf7d7697d01153d922c281a1"
},
"registrySnapshot": {
"status": 200,
"bodySha256": "357ef2db141c991366e9d1e0f4504a9fa9bb13c9cb99892a6d9e03da48c4d705",
"distTags": {
"latest": "1.1.0"
},
"stableVersions": [
"1.0.0",
"1.1.0"
],
"prereleaseVersions": [],
"scope": "All stable versions listed by the live npm package catalog at capture time; registry signing keys/signatures were not independently verified."
},
"release": {
"name": "artplayer-plugin-chromecast",
"version": "1.1.0",
"tarball": "https://registry.npmjs.org/artplayer-plugin-chromecast/-/artplayer-plugin-chromecast-1.1.0.tgz",
"integrity": "sha512-3xrHj1CHe0Tt8gNMUB9A1epuW4OrbPsa1T9MNFy4AfQsjhYAhWAmef6hYu1GdHdADDybXBtVHP50gQ+EW+ddcw==",
"sha256": "1941d41055ee02deaa343cbf9eaf714495cb9f04883f57966f5b27f128117c30",
"registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"publishedAt": "2026-03-12T14:21:25.045Z",
"files": {
"package/README.md": "d277a82e54df2af679f94c92da2250d3779f07e2c37282d7d66e776f54fed323",
"package/dist/artplayer-plugin-chromecast.js": "4959661fc7f22eee3d32363bb0392a486c44499ec4a9c608ccfea55de29aaac6",
"package/dist/artplayer-plugin-chromecast.legacy.js": "e2ba7aebec099884bb806f3caf77f9ff739ff9f236aefdfd0a4e69a8295c9d01",
"package/dist/artplayer-plugin-chromecast.mjs": "b6d4e445a78f00fae53a35d239e62cc4a1448086d76058d69d8f9634a5754768",
"package/package.json": "8157c06e4f764b1662ac108334e1240b07963400b906da6b21874e9a0e415adf",
"package/types/artplayer-plugin-chromecast.d.ts": "7826de339f5c027025c71215ed3c13995c01174bdf7d7697d01153d922c281a1"
},
"manifest": {
"name": "artplayer-plugin-chromecast",
"version": "1.1.0",
"description": "Chromecast plugin for ArtPlayer",
"author": "Harvey Zhao <laozhaochaguan@gmail.com>",
"license": "MIT",
"homepage": "https://artplayer.org",
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"keywords": [
"html5",
"video",
"player"
],
"exports": {
".": {
"types": "./types/artplayer-plugin-chromecast.d.ts",
"import": "./dist/artplayer-plugin-chromecast.mjs",
"require": "./dist/artplayer-plugin-chromecast.js"
},
"./legacy": {
"types": "./types/artplayer-plugin-chromecast.d.ts",
"import": "./dist/artplayer-plugin-chromecast.legacy.js",
"require": "./dist/artplayer-plugin-chromecast.legacy.js"
}
},
"main": "./dist/artplayer-plugin-chromecast.js",
"module": "./dist/artplayer-plugin-chromecast.mjs",
"types": "./types/artplayer-plugin-chromecast.d.ts",
"legacy": "./dist/artplayer-plugin-chromecast.legacy.js",
"browserslist": "last 1 Chrome version"
},
"historicalCore": {
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer/package.json",
"version": "5.3.1",
"sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c"
},
"entrypoints": {
"main": "./dist/artplayer-plugin-chromecast.js",
"module": "./dist/artplayer-plugin-chromecast.mjs",
"types": "./types/artplayer-plugin-chromecast.d.ts",
"legacy": "./dist/artplayer-plugin-chromecast.legacy.js"
},
"registrySourceManifest": {
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer-plugin-chromecast/package.json",
"version": "1.0.2",
"sha256LF": "875ba2d244df05666e22bc6a2cc48ac92f14349cef54aa00ff605caff96787a0",
"versionMatchesRelease": false
},
"sourceAssociationScope": "Actual manifests at the npm metadata gitHead; association only, not a minimum supported core or proof the source matches the published archive.",
"registrySourceComparison": [
{
"file": "packages/artplayer-plugin-chromecast/README.md",
"sha256LF": "d277a82e54df2af679f94c92da2250d3779f07e2c37282d7d66e776f54fed323",
"matchesArchiveBytes": true
},
{
"file": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.js",
"sha256LF": "48d624932ab98791a4b493d49343cc8419319034b6a0da310c4fad1da7a852c7",
"matchesArchiveBytes": false
},
{
"file": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.legacy.js",
"sha256LF": "b5d87717bc1a566699f9fbda69e346d4f6b378b9c28292447a576b5a23a5c2d3",
"matchesArchiveBytes": false
},
{
"file": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.mjs",
"sha256LF": "0d60f9f4ab2c6cf676c355d6db0e0315fac02e130ce0b1865d12da106c27af26",
"matchesArchiveBytes": false
},
{
"file": "packages/artplayer-plugin-chromecast/package.json",
"sha256LF": "875ba2d244df05666e22bc6a2cc48ac92f14349cef54aa00ff605caff96787a0",
"matchesArchiveBytes": false
},
{
"file": "packages/artplayer-plugin-chromecast/types/artplayer-plugin-chromecast.d.ts",
"sha256LF": "7826de339f5c027025c71215ed3c13995c01174bdf7d7697d01153d922c281a1",
"matchesArchiveBytes": true
}
]
},
"previous": [
{
"name": "artplayer-plugin-chromecast",
"version": "1.0.0",
"tarball": "https://registry.npmjs.org/artplayer-plugin-chromecast/-/artplayer-plugin-chromecast-1.0.0.tgz",
"integrity": "sha512-liF1ku8OYJqycgU/9HcAJsmv3amxp9/LG2Ce/RqVcQu+gTw8MoLELRfxeMolBK159s2sesmYp4N1043pDAtkAg==",
"sha256": "aa110388140046912d6da79f9891738a981aae3ec4e14722b987c46c9ecec3b2",
"registryGitHead": "43309b72cff84344bd61f4de505aee0e10a83e6b",
"publishedAt": "2024-05-27T03:32:00.044Z",
"files": {
"package/README.md": "87f2c3eedad755e97a545ddabe3c47fad1b9e7b28c0ae08cb22680b92ba2f905",
"package/dist/artplayer-plugin-chromecast.js": "6d3f0b5b2c2feb16e86dfbbbac13fcf815daea9aaa017d2eb72821bccbbbad4a",
"package/dist/artplayer-plugin-chromecast.legacy.js": "1e3aceff99060c2934ddce16aac3284e2d41d4a6d866dd8cdf3237a2310e941f",
"package/package.json": "1e61fb7ff05f04370cb45dc80cc55f8ab3d2a194bb488bb45d0b0c738d96150a",
"package/src/cast.svg": "8c0f9ad82c2d6c70afb70ab5002204c4948dd282fbf52095aed4b07fa8d466d4",
"package/src/index.js": "e6c66bf803828f1b03e9a52e68ca90eaef0c42431b8e9c5c5504992125db23d9",
"package/types/artplayer-plugin-chromecast.d.ts": "3ed160931cef6843537801b9ffb4887284d67ae46e1f31a5f611f34a15836a72"
},
"manifest": {
"license": "MIT",
"version": "1.0.0",
"name": "artplayer-plugin-chromecast",
"types": "types/artplayer-plugin-chromecast.d.ts",
"description": "Chromecast plugin for ArtPlayer",
"main": "dist/artplayer-plugin-chromecast.js",
"legacy": "dist/artplayer-plugin-chromecast.legacy.js",
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
"browserslist": "last 1 Chrome version",
"homepage": "https://artplayer.org",
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"keywords": [
"html5",
"video",
"player"
]
},
"historicalCore": {
"commit": "43309b72cff84344bd61f4de505aee0e10a83e6b",
"file": "packages/artplayer/package.json",
"version": "5.1.2",
"sha256LF": "aaf3ba9c7beaaea31170cc50e3958c0bb5d779ab4f90b6a213994ce8c9aace36"
},
"entrypoints": {
"main": "dist/artplayer-plugin-chromecast.js",
"module": null,
"types": "types/artplayer-plugin-chromecast.d.ts",
"legacy": "dist/artplayer-plugin-chromecast.legacy.js"
},
"registrySourceManifest": {
"commit": "43309b72cff84344bd61f4de505aee0e10a83e6b",
"file": "packages/artplayer-plugin-chromecast/package.json",
"version": "2.0.0",
"sha256LF": "ff9c26c916d181c0c30eee5a0481780f7b4f250809380c2dcf4be4cda124bf50",
"versionMatchesRelease": false
},
"sourceAssociationScope": "Actual manifests at the npm metadata gitHead; association only, not a minimum supported core or proof the source matches the published archive.",
"registrySourceComparison": [
{
"file": "packages/artplayer-plugin-chromecast/README.md",
"sha256LF": "87f2c3eedad755e97a545ddabe3c47fad1b9e7b28c0ae08cb22680b92ba2f905",
"matchesArchiveBytes": true
},
{
"file": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.js",
"sha256LF": "6d3f0b5b2c2feb16e86dfbbbac13fcf815daea9aaa017d2eb72821bccbbbad4a",
"matchesArchiveBytes": true
},
{
"file": "packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.legacy.js",
"sha256LF": "1e3aceff99060c2934ddce16aac3284e2d41d4a6d866dd8cdf3237a2310e941f",
"matchesArchiveBytes": true
},
{
"file": "packages/artplayer-plugin-chromecast/package.json",
"sha256LF": "ff9c26c916d181c0c30eee5a0481780f7b4f250809380c2dcf4be4cda124bf50",
"matchesArchiveBytes": false
},
{
"file": "packages/artplayer-plugin-chromecast/src/cast.svg",
"sha256LF": "8c0f9ad82c2d6c70afb70ab5002204c4948dd282fbf52095aed4b07fa8d466d4",
"matchesArchiveBytes": true
},
{
"file": "packages/artplayer-plugin-chromecast/src/index.js",
"sha256LF": "e6c66bf803828f1b03e9a52e68ca90eaef0c42431b8e9c5c5504992125db23d9",
"matchesArchiveBytes": true
},
{
"file": "packages/artplayer-plugin-chromecast/types/artplayer-plugin-chromecast.d.ts",
"sha256LF": "3ed160931cef6843537801b9ffb4887284d67ae46e1f31a5f611f34a15836a72",
"matchesArchiveBytes": true
}
]
}
],
"observations": {
"latestArchiveMatchesFrozenSourceFiles": [
"packages/artplayer-plugin-chromecast/README.md",
"packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.js",
"packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.legacy.js",
"packages/artplayer-plugin-chromecast/dist/artplayer-plugin-chromecast.mjs",
"packages/artplayer-plugin-chromecast/package.json",
"packages/artplayer-plugin-chromecast/types/artplayer-plugin-chromecast.d.ts"
],
"entrypointDifferences": [
"1.0.0 has main/types/legacy; module and exports are absent, and no ESM .mjs is shipped.",
"1.1.0 adds module and conditional root/legacy exports; it no longer ships src/index.js or src/cast.svg."
],
"registrySourceMismatch": "Both npm metadata gitHeads disagree with their published plugin versions: 1.0.0 points to a Git manifest declaring 2.0.0 (only manifest differs among shipped members); 1.1.0 points to a Git manifest declaring 1.0.2 (manifest and all three dist members differ). Keep archives and associated Git sources separate; registrySourceComparison records exact differences.",
"typeRuntimeDifferences": [
"The frozen declaration returns Chromecast synchronously, but the runtime registration function is async.",
"Runtime result exposes getCastState/isCasting; neither method appears in the frozen declaration.",
"Runtime reads onStateChange/onCastAvailable/onCastStart/onError callbacks; Option does not declare them."
],
"historicalCorePublication": "1.1.0 gitHead associates core workspace 5.3.1. Previous ASR registry evidence found no published stable 5.3.1; this baseline makes no published/minimum-core claim."
},
"thirdParty": {
"castSdk": {
"url": "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1",
"reference": "packages/artplayer-plugin-chromecast/src/index.js",
"loading": "Runtime script insertion on Cast initialization; consumers may override option.sdk.",
"frozenRuntimeBytes": false,
"version": "Mutable /v1 URL; exact served SDK build and transitive loads not captured.",
"status": "SDK availability, license terms, redistribution rights, sender initialization and real receiver validation remain separate work."
},
"icon": {
"reference": "packages/artplayer-plugin-chromecast/src/cast.svg",
"notice": "! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. ",
"pathSha256": "2e154456a8ebae91a32ba65d737baa54706b3d034dd546600cd6d5e3bf413e94",
"samePathInRuntime": true,
"publishedWithNotice": [
"1.0.0:package/src/cast.svg"
],
"status": "The asset says Font Awesome Pro 6.5.1 / Commercial License. Repository entitlement and redistribution permission are not established by the package MIT declaration; runtime inlines the same path without this notice."
}
},
"validationScope": "Archive integrity/member bytes/entrypoint existence and Git source association only. No Cast SDK download, browser, receiver/device, compatibility behavior, or npm publication acceptance."
}
@@ -0,0 +1,22 @@
# PKG-CAST-01 发布来源与公共契约
冻结真实npm1.0.0/1.1.0、全部成员与九个工作区源码/声明/README/demo/产物文件。
历史banner和registry gitHead均存在版本差异,按真实tarball归档保存,不反推或改写。
来源核心5.1.2/5.3.1仅为git关联,不能据此宣称对应正式核心发布或完整支持范围。
新增只读重放verifier与28项公共行为测试,覆盖两版本main/legacy、冻结source及
工作区main/legacy,分别验证CJS/global、eager/lazy加载、异步注册、实际结果、live
option、MIME、回调/状态及加载失败。初次统一套用1.1.0流程导致1.0.0等待注册加载,
已终止该测试进程并按真实归档拆开场景;最终28项均通过。不是掩盖生产异常。
详见[契约表](../baselines/chromecast-contract.md)、[来源](../baselines/chromecast-release.json)
及[验证摘要](../baselines/chromecast-contract-validation.json)。根脚本增加test:chromecast并
纳入test:unit;没有生产代码、声明、依赖、版本或分发产物变化,不需要本步重新构建。
CAST-TYPE-01登记旧同步声明、缺失回调和查询方法,04保持旧声明形状并增加准确入口。
VENDOR-10记录商业图标标记与内嵌path匹配,归03替换和06分发核查。README/demo内容
极少,demo仅相对媒体URL,接收端网络可达性与真实SDK版本/设备验证仍归05/06及SDK-06。
独立子代理核验npm与来源,另一个子代理只读对照Google官方规范,主代理审查并建立
实际归档行为测试。01完成不表示SDK实现正确或已具备npm准入;02/03继续修复已发现
的问题。没有连接Cast设备、推送或发布。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。
状态:todo 81 / doing 17 / blocked 0 / done 130 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 80 / doing 17 / blocked 0 / done 131 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -287,7 +287,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-CAST-01 | artplayer-plugin-chromecast<br>核对包契约与历史用法 | BASE-05 | url/sdk/icon/mimeType、loader、会话、全局 SDK 与结果 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | todo |
| PKG-CAST-01 | artplayer-plugin-chromecast<br>核对包契约与历史用法 | BASE-05 | url/sdk/icon/mimeType、loader、会话、全局 SDK 与结果 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-CAST-02 | artplayer-plugin-chromecast<br>建立特有行为与错误测试 | PKG-CAST-01, ENG-03, ENG-05 | SDK 脚本加载失败/重入、无设备/拒绝、重复实例与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
| PKG-CAST-03 | artplayer-plugin-chromecast<br>整理内部职责与资源 | PKG-CAST-02, CORE-11, CORE-13 | SDK singleton/实例订阅/会话与控件职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-CAST-04 | artplayer-plugin-chromecast<br>迁移自有源码和公开类型 | PKG-CAST-03, ENG-04, ENG-06, CORE-07 | Cast 能力 adapter、全局对象和元数据类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
@@ -542,6 +542,7 @@
- PKG-VAST-01: [记录](baselines/vast-contract.md) [记录](baselines/vast-release.json) [记录](changes/2026-09-12-PKG-VAST-01-contract.md) [记录](baselines/vast-contract-validation.json)
- PKG-VAST-02: [记录](changes/2026-09-12-PKG-VAST-02-tests.md) [记录](baselines/vast-behavior-validation.json) [记录](baselines/vast-core.json) [记录](vast-validation.md)
- PKG-VAST-03: [记录](changes/2026-09-12-PKG-VAST-03-lifecycle.md) [记录](baselines/vast-lifecycle-validation.json) [记录](vast-compatibility-decision.md)
- PKG-CAST-01: [记录](baselines/chromecast-release.json) [记录](baselines/chromecast-contract.md) [记录](baselines/chromecast-contract-validation.json) [记录](changes/2026-09-13-PKG-CAST-01-contracts.md)
- PKG-DPIP-01: [记录](changes/2026-09-12-PKG-DPIP-01-contract.md) [记录](baselines/dpip-release.json) [记录](baselines/dpip-contract.md) [记录](baselines/dpip-contract-validation.json) [记录](dpip-validation.md)
- PKG-DPIP-02: [记录](changes/2026-09-12-PKG-DPIP-02-tests.md) [记录](baselines/dpip-behavior-validation.json) [记录](dpip-validation.md)
- PKG-DPIP-03: [记录](changes/2026-09-12-PKG-DPIP-03-lifecycle.md) [记录](baselines/dpip-lifecycle-validation.json) [记录](dpip-validation.md)
+13
View File
@@ -1,5 +1,18 @@
# 进度与证据
## PKG-CAST-01 Chromecast 契约与来源完成
冻结真实npm1.0.0/1.1.0与9个工作区输入,逐成员、入口及git关联核验通过。1.0.0
提前加载SDK/仅name结果与1.1.0懒加载/状态回调分别测试,28项公共行为通过;
gitHead和banner版本差异明确记录。VENDOR-10登记商业图标来源,CAST-TYPE-01
登记同步声明缺口,SDK-06仍缺真实会话。见[变更](changes/2026-09-13-PKG-CAST-01-contracts.md)
与[契约](baselines/chromecast-contract.md)。228项:131 done、17 doing、80 todo;
212风险。下一步02错误基线、03拆分与修复;未改生产代码、未连接设备或发布。
用户已确认历史类型冲突统一规则,见[type-compatibility-policy.md](type-compatibility-policy.md);
Canvas/Ambilight/VTT/Multiple Subtitles可继续实施,无需重复等待类型取舍确认。
## PKG-ASR-09 显式捕获归属完成
新增可选capture策略,旧默认与根类型保留;Firefox无需强制异常,显式模式零直接
+2
View File
@@ -214,3 +214,5 @@
| ASR-FOREIGN-01 | resolved / 已复现 | Firefox permits parallel native media-element owners instead of selecting capture fallback | PKG-ASR-05, PKG-ASR-09 |
| ASR-CORS-01 | open / 已复现 | Default WebAudio binding silences CORS-cross-origin media that otherwise advances normally | PKG-ASR-05 |
| ASR-RESTART-01 | resolved / 已复现 | Delayed captured-source restart resumes recognition after a newer pause or stop | PKG-ASR-09 |
| VENDOR-10 | open / 源码/产物事实 | Chromecast embeds a Font Awesome Pro path with no established redistribution entitlement | PKG-CAST-03, PKG-CAST-06 |
| CAST-TYPE-01 | open / 源码/产物事实 | Chromecast declarations describe a synchronous name-only registration and omit existing runtime callbacks and query methods | PKG-CAST-04 |
+31
View File
@@ -4714,6 +4714,37 @@
"refactor/baselines/asr-explicit-capture-validation.json"
],
"resolutionRationale": "Two delayed-close red tests now pass in source/main/legacy, with subsequent play verified; native restart and default regressions pass."
},
{
"id": "VENDOR-10",
"title": "Chromecast embeds a Font Awesome Pro path with no established redistribution entitlement",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-CAST-03",
"PKG-CAST-06"
],
"evidence": [
"packages/artplayer-plugin-chromecast/src/cast.svg",
"refactor/baselines/chromecast-release.json"
],
"compatibleResolution": "Replace the default embedded artwork with an owned simple Cast indicator while retaining the custom icon option and wrapper classes; keep historical provenance separate.",
"closureCriteria": "Owned or properly licensed default artwork replaces the commercial-marked source and every shipped bundle, with source/provenance and generated-output checks."
},
{
"id": "CAST-TYPE-01",
"title": "Chromecast declarations describe a synchronous name-only registration and omit existing runtime callbacks and query methods",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-CAST-04"
],
"evidence": [
"packages/artplayer-plugin-chromecast/types/artplayer-plugin-chromecast.d.ts",
"refactor/baselines/chromecast-contract.md"
],
"compatibleResolution": "Preserve the latest published root factory type and provide a precise runtime type view; verify valid historical JavaScript module forms independently.",
"closureCriteria": "Frozen type consumers and replacements pass, accurate async/callback/state declarations work through an additive entry, and actual installed exports are tested."
}
]
}
+82
View File
@@ -0,0 +1,82 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
const workspace = path.dirname(refactorDir.replace(/[\\/]$/u, ''))
function gitText(commit, file) {
return execFileSync('git', ['show', `${commit}:${file}`], { cwd: workspace, encoding: 'utf8' }).replaceAll('\r\n', '\n')
}
function verifyManifestAssociation(association) {
const text = gitText(association.commit, association.file)
assert.equal(hash(text), association.sha256LF, association.file)
assert.equal(JSON.parse(text).version, association.version)
}
export async function verifyChromecastContract() {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/chromecast-release.json'), 'utf8'))
const archives = new Map()
const releases = [baseline.release, ...baseline.previous]
assert.deepEqual(releases.map(release => release.version).sort(), [...baseline.registrySnapshot.stableVersions].sort())
for (const release of releases) {
const archive = await ensureArchive(release)
assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort())
for (const [file, expected] of Object.entries(release.files))
assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`)
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
assert.deepEqual(manifest, release.manifest)
assert.equal(manifest.name, release.name)
assert.equal(manifest.version, release.version)
for (const field of ['main', 'module', 'types', 'legacy']) {
assert.equal(manifest[field] ?? null, release.entrypoints[field], `${release.version}/${field}`)
if (manifest[field])
assert(Object.hasOwn(release.files, `package/${manifest[field].replace(/^\.\//u, '')}`), `${release.version}/${field} member`)
}
// Check actual conditional export targets without inventing exports for 1.0.0.
function checkTargets(value) {
if (typeof value === 'string')
assert(Object.hasOwn(release.files, `package/${value.replace(/^\.\//u, '')}`), `${release.version}/${value}`)
else if (value && typeof value === 'object')
Object.values(value).forEach(checkTargets)
}
checkTargets(manifest.exports)
assert.equal(release.registrySourceManifest.commit, release.registryGitHead)
assert.equal(release.registrySourceManifest.versionMatchesRelease, release.registrySourceManifest.version === release.version)
assert.equal(release.historicalCore.commit, release.registryGitHead)
verifyManifestAssociation(release.registrySourceManifest)
verifyManifestAssociation(release.historicalCore)
for (const comparison of release.registrySourceComparison) {
const digest = hash(gitText(release.registryGitHead, comparison.file))
assert.equal(digest, comparison.sha256LF)
const member = `package/${comparison.file.slice('packages/artplayer-plugin-chromecast/'.length)}`
assert.equal(comparison.matchesArchiveBytes, digest === release.files[member])
}
archives.set(release.version, archive)
}
const sources = new Map()
for (const [file, expected] of Object.entries(baseline.source)) {
const source = gitText(baseline.sourceCommit, file)
assert.equal(hash(source), expected, file)
sources.set(file, source)
}
for (const file of baseline.observations.latestArchiveMatchesFrozenSourceFiles) {
const member = `package/${file.slice('packages/artplayer-plugin-chromecast/'.length)}`
assert.equal(hash(sources.get(file)), baseline.release.files[member])
}
const icon = sources.get(baseline.thirdParty.icon.reference)
const iconPath = icon.match(/<path d="([^"]+)"/u)[1]
assert.equal(hash(iconPath), baseline.thirdParty.icon.pathSha256)
assert(icon.includes(baseline.thirdParty.icon.notice))
assert(sources.get('packages/artplayer-plugin-chromecast/src/index.js').includes(iconPath))
return { baseline, archives, sources }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const { baseline, archives, sources } = await verifyChromecastContract()
console.log(JSON.stringify({ sourceCommit: baseline.sourceCommit, sourceFiles: sources.size, releases: [...archives.keys()], validation: 'SHA512/SHA256, archive members, actual entrypoints and Git source associations' }, null, 2))
}
+7 -2
View File
@@ -2992,11 +2992,16 @@
"dependsOn": [
"BASE-05"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "url/sdk/icon/mimeType、loader、会话、全局 SDK 与结果",
"acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结",
"evidence": []
"evidence": [
"baselines/chromecast-release.json",
"baselines/chromecast-contract.md",
"baselines/chromecast-contract-validation.json",
"changes/2026-09-13-PKG-CAST-01-contracts.md"
]
},
{
"id": "PKG-CAST-02",
+32 -1
View File
@@ -1072,6 +1072,37 @@
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md",
"refactor/baselines/thumbnail-emitter-validation.json"
]
},
{
"id": "chromecast-cast-icon",
"riskId": "VENDOR-10",
"inputs": [
"packages/artplayer-plugin-chromecast/src/cast.svg",
"packages/artplayer-plugin-chromecast/src/index.js"
],
"owners": [
"PKG-CAST-03",
"PKG-CAST-06"
],
"sourceStatus": "Exact SVG path matches the runtime default icon; source notice identifies Font Awesome Pro 6.5.1.",
"licenseStatus": "Commercial License notice exists in cast.svg; repository entitlement and redistribution authorization are not established by the package MIT metadata.",
"updatePolicy": "Replace with owned artwork or verify permission before release; preserve user-supplied icon support and icon wrapper classes, retain historical archive evidence.",
"fingerprints": [
{
"path": "packages/artplayer-plugin-chromecast/src/cast.svg",
"algorithm": "sha256-lf",
"sha256": "8c0f9ad82c2d6c70afb70ab5002204c4948dd282fbf52095aed4b07fa8d466d4"
},
{
"path": "packages/artplayer-plugin-chromecast/src/index.js",
"algorithm": "sha256-lf",
"sha256": "2ab8967cf7fdbf9fbac8a9afe73713e66047bad7e4d4afc802665ce594fa3274"
}
],
"reviewEvidence": [
"refactor/baselines/chromecast-release.json",
"refactor/changes/2026-09-13-PKG-CAST-01-contracts.md"
]
}
],
"integrations": [
@@ -1163,7 +1194,7 @@
"packages/artplayer-plugin-chromecast/src/index.js"
],
"contract": "Default remote cast_sender.js?loadCastFramework=1; sdk URL/options and actual Cast device/session behavior require separate validation.",
"validationStatus": "Source integration observed; complete real SDK/service/device matrix not yet verified.",
"validationStatus": "Real 1.0.0/1.1.0 archives and controlled public behavior frozen. Official requestSession completion is not a session; runtime/source gaps proceed through CAST-02/03. No actual SDK bytes, HTTPS sender, receiver, casting or media networking acceptance.",
"demoPathsSource": "refactor/baselines/demo-inventory.json"
},
{
+133
View File
@@ -0,0 +1,133 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Frozen Chromecast observations use the repository runner.
import test from 'node:test'
import { chromecastEnvironment, chromecastHistorical } from './helpers/chromecast.js'
for (const implementation of await chromecastHistorical()) {
if (implementation.name.startsWith('published-1.0.0-')) {
for (const script of [false, true]) {
test(`Chromecast ${implementation.name}: ${script ? 'global' : 'CJS'} eager SDK registration returns only name`, async () => {
const env = chromecastEnvironment(implementation, { script })
const player = env.player()
const register = env.factory({ icon: '<b>cast</b>' })
assert.equal(typeof env.context.__onGCastApiAvailable, 'function')
assert.equal(env.scripts.length, 0)
const pending = register(player.art)
assert.equal(player.controls.length, 0)
assert.equal(env.scripts.length, 1)
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.scripts[0].onload({ type: 'load' })
const plugin = await pending
assert.deepEqual(Object.keys(plugin), ['name'])
assert.equal(plugin.name, 'artplayerPluginChromecast')
assert.equal(plugin.getCastState, undefined)
assert.equal(player.controls[0].name, 'chromecast')
assert.equal(player.controls[0].html, '<i class="art-icon art-icon-cast"><b>cast</b></i>')
assert.equal(player.controls[0].click(), undefined)
await env.flush()
assert.equal(env.mediaLoads[0].media.contentId, '/video.mp4')
assert.equal(env.mediaLoads[0].media.contentType, 'video/mp4')
assert.equal(env.sdkListeners.size, 0)
})
}
test(`Chromecast ${implementation.name}: existing SDK skips loading and live options select media without modern callbacks`, async () => {
const env = chromecastEnvironment(implementation)
env.installSdk()
const player = env.player('/movie.M3U8?token=one#position')
const events = []
const option = { onCastStart: () => events.push('start') }
await env.factory(option)(player.art)
assert.equal(env.scripts.length, 0)
player.controls[0].click()
await env.flush()
assert.equal(env.mediaLoads[0].media.contentType, 'application/x-mpegURL')
option.url = '/custom'
option.mimeType = 'application/custom'
player.controls[0].click()
await env.flush()
assert.equal(env.mediaLoads.at(-1).media.contentId, '/custom')
assert.equal(env.mediaLoads.at(-1).media.contentType, 'application/custom')
assert.deepEqual(events, [])
assert.equal(player.art.notice.show, '')
})
test(`Chromecast ${implementation.name}: script failure rejects registration without adding controls`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const pending = env.factory({})(player.art)
const error = { type: 'error' }
env.scripts[0].onerror(error)
await assert.rejects(pending, value => value === error)
assert.equal(player.controls.length, 0)
})
continue
}
for (const script of [false, true]) {
test(`Chromecast ${implementation.name}: ${script ? 'global' : 'CJS'} lazy registration and public extraction`, async () => {
const env = chromecastEnvironment(implementation, { script })
const player = env.player()
const register = env.factory({ icon: '<b>cast</b>' })
assert.equal(env.scripts.length, 0)
assert.equal(player.controls.length, 0)
const pending = register(player.art)
assert.equal(typeof pending.then, 'function')
const plugin = await pending
assert.deepEqual(Object.keys(plugin), ['name', 'getCastState', 'isCasting'])
assert.equal(plugin.name, 'artplayerPluginChromecast')
assert.equal(plugin.getCastState(), null)
assert.equal(plugin.isCasting(), false)
assert.equal(player.controls[0].name, 'chromecast')
assert.equal(player.controls[0].position, 'right')
assert.equal(player.controls[0].tooltip, 'Chromecast')
assert.equal(player.controls[0].html, '<i class="art-icon art-icon-cast"><b>cast</b></i>')
assert.equal(env.scripts.length, 0)
})
}
test(`Chromecast ${implementation.name}: session callback order, casting request and MIME remain observable`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player('/movie.M3U8?token=one#position')
const events = []
const option = { sdk: '/local-sender.js', onStateChange: state => events.push(['state', state]), onCastAvailable: value => events.push(['available', value]), onCastStart: () => events.push(['start']) }
const plugin = await env.factory(option)(player.art)
const pending = player.controls[0].click()
assert.equal(env.scripts[0].src, '/local-sender.js')
const { session } = env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED', session)
await pending
await env.flush()
assert.equal(env.requests(), 0)
assert.equal(env.mediaLoads[0].media.contentId, '/movie.M3U8?token=one#position')
assert.equal(env.mediaLoads[0].media.contentType, 'application/x-mpegURL')
assert.equal(player.art.notice.show, 'Casting started')
assert.equal(plugin.getCastState(), 'SESSION_STARTED')
assert.equal(plugin.isCasting(), true)
assert.equal(player.icon.style.color, 'red')
assert.deepEqual(events, [['state', 'connected'], ['start']])
env.emit('caststatechanged', { castState: 'NO_DEVICES_AVAILABLE' })
env.emit('caststatechanged', { castState: 'NOT_CONNECTED' })
assert.deepEqual(events.slice(-2), [['available', false], ['available', true]])
option.url = 'https://media.test/custom'
option.mimeType = 'application/custom'
await player.controls[0].click()
await env.flush()
assert.equal(env.mediaLoads.at(-1).media.contentId, option.url)
assert.equal(env.mediaLoads.at(-1).media.contentType, option.mimeType)
assert.equal(env.scripts.length, 1)
})
test(`Chromecast ${implementation.name}: script failure preserves error identity and does not mark initialized`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const failures = []
const plugin = await env.factory({ onError: error => failures.push(error) })(player.art)
const pending = player.controls[0].click()
const error = { type: 'error', target: env.scripts[0] }
env.scripts[0].onerror(error)
await assert.rejects(pending, value => value === error)
assert.equal(failures[0], error)
assert.equal(player.art.notice.show, 'Failed to initialize Cast API')
assert.equal(plugin.isCasting(), false)
})
}
+135
View File
@@ -0,0 +1,135 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyChromecastContract } from '../../refactor/scripts/chromecast-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
export async function chromecastHistorical() {
const { baseline, archives, sources } = await verifyChromecastContract()
const implementations = []
for (const release of [baseline.release, ...baseline.previous]) {
for (const field of ['main', 'legacy'])
implementations.push({ name: `published-${release.version}-${field}`, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//u, '')}`).toString() })
}
const prefix = 'packages/artplayer-plugin-chromecast'
const source = sources.get(`${prefix}/src/index.js`)
implementations.push({ name: 'frozen-source', code: (await transform(source, { format: 'cjs', target: 'es2020' })).code, scriptCode: (await transform(source, { format: 'iife', globalName: 'artplayerPluginChromecast', target: 'es2020' })).code })
for (const suffix of ['js', 'legacy.js'])
implementations.push({ name: `frozen-workspace-${suffix}`, code: sources.get(`${prefix}/dist/artplayer-plugin-chromecast.${suffix}`) })
return implementations
}
export function chromecastEnvironment(implementation, { script = false } = {}) {
const scripts = []
const icons = []
const controls = []
const logs = []
const sdkListeners = new Map()
const pendingMedia = []
const mediaLoads = []
const sdkOptions = []
let requests = 0
let sdk
const context = vm.createContext({
console: { log: (...args) => logs.push(args), warn: (...args) => logs.push(args), error: (...args) => logs.push(args) },
document: {
createElement: tag => ({ tag, remove() { scripts.splice(scripts.indexOf(this), 1) } }),
body: { appendChild(element) { scripts.push(element) } },
querySelector: selector => selector === '.art-icon-cast' ? icons[0] : null,
},
})
context.window = context
if (!script) {
context.module = { exports: {} }
context.exports = context.module.exports
}
vm.runInContext(script ? implementation.scriptCode || implementation.code : `(function () {\n${implementation.code}\n}).call(globalThis)`, context, { timeout: 1000 })
const exported = script ? context.artplayerPluginChromecast : context.module.exports
const factory = typeof exported === 'function' ? exported : exported.default
assert.equal(typeof factory, 'function')
function player(url = '/video.mp4') {
const listeners = new Map()
const localControls = []
const icon = { style: {} }
icons.push(icon)
const art = {
option: { url },
notice: { show: '' },
isDestroy: false,
template: { $player: { querySelector: () => icon } },
controls: {
add(option) {
controls.push(option)
localControls.push(option)
return { querySelector: () => icon }
},
remove(name) {
const index = localControls.findIndex(control => control.name === name)
if (index >= 0)
localControls.splice(index, 1)
},
},
on(name, callback) { listeners.set(name, [...listeners.get(name) || [], callback]) },
off(name, callback) { listeners.set(name, (listeners.get(name) || []).filter(value => value !== callback)) },
}
return { art, icon, controls: localControls, listeners, destroy() {
art.isDestroy = true
for (const callback of listeners.get('destroy') || []) callback()
} }
}
// Observe SDK-returned promise chains without changing the plugin's Promise globals.
function watched(promise) {
pendingMedia.push(promise)
promise.catch(() => {})
return { then: (...args) => watched(promise.then(...args)), catch: (...args) => watched(promise.catch(...args)) }
}
function installSdk({ requestResult, requestError, loadError } = {}) {
const session = { loadMedia(request) {
mediaLoads.push(request)
return watched(loadError ? Promise.reject(loadError) : Promise.resolve())
} }
const castContext = {
setOptions(value) { sdkOptions.push(value) },
addEventListener(name, listener) { sdkListeners.set(name, [...sdkListeners.get(name) || [], listener]) },
removeEventListener(name, listener) { sdkListeners.set(name, (sdkListeners.get(name) || []).filter(value => value !== listener)) },
getCurrentSession: () => session,
requestSession() {
requests++
return requestError ? Promise.reject(requestError) : Promise.resolve(requestResult)
},
}
context.chrome = { cast: { AutoJoinPolicy: { ORIGIN_SCOPED: 'origin_scoped' }, media: {
DEFAULT_MEDIA_RECEIVER_APP_ID: 'CC1AD845',
MediaInfo: class {
constructor(contentId, contentType) {
this.contentId = contentId
this.contentType = contentType
}
},
LoadRequest: class {
constructor(media) { this.media = media }
},
} } }
context.cast = { framework: {
CastContext: { getInstance: () => castContext },
CastContextEventType: { SESSION_STATE_CHANGED: 'sessionstatechanged', CAST_STATE_CHANGED: 'caststatechanged' },
SessionState: Object.fromEntries(['NO_SESSION', 'SESSION_STARTING', 'SESSION_STARTED', 'SESSION_ENDING', 'SESSION_ENDED', 'SESSION_RESUMED'].map(value => [value, value])),
CastState: Object.fromEntries(['NO_DEVICES_AVAILABLE', 'NOT_CONNECTED', 'CONNECTING', 'CONNECTED'].map(value => [value, value])),
} }
sdk = { session, castContext }
return sdk
}
function emit(name, value) {
for (const callback of sdkListeners.get(name) || []) callback(value)
}
function sessionState(state, session = sdk.session) {
emit('sessionstatechanged', { sessionState: state, session })
}
async function flush() {
for (let index = 0; index < 8; index++) await Promise.resolve()
}
return { context, exported, factory, scripts, icons, controls, logs, sdkListeners, mediaLoads, pendingMedia, sdkOptions, player, installSdk, emit, sessionState, flush, requests: () => requests }
}