diff --git a/package.json b/package.json index 38c7a6921..7bf94ea9f 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,8 @@ "test:scaffold": "node --test test/plugin-scaffold.test.js", "typecheck:library": "node node_modules/typescript/bin/tsc -p scripts/tsconfig.library.json --noEmit", "test:library": "node --test refactor/scripts/build.test.mjs test/library-build.test.js test/performance-report.test.js test/plugin-scaffold.test.js test/dev-server.test.js", - "test:dev-server": "node --test test/dev-server.test.js" + "test:dev-server": "node --test test/dev-server.test.js", + "probe:auto-thumbnail-native": "node refactor/scripts/auto-thumbnail-native-probe.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md index ac4179582..2f5a3a066 100644 --- a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md +++ b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md @@ -158,6 +158,26 @@ These variants still fail the unique-first-frame check on the recorded Windows WebKit host. They are diagnostic page-owned callbacks, not production fixes or cancellation code. See refactor's first-seek checkpoint before repeating them. +`yarn probe:auto-thumbnail-native` removes ArtPlayer and this plugin entirely. +It serves the frozen timeline with the owned dev server, plus a diagnostic H.264 +transcode with B frames disabled, and samples native video/canvas in hidden paused, +visible paused and hidden play-then-immediately-pause modes. It verifies both +files' first purple pixel with FFmpeg before browser capture. FFmpeg/FFprobe must +be installed (optional FFMPEG/FFPROBE executable paths); exact version, arguments, +media hashes, browser versions, events and all31 samples are recorded in an ignored +cache directory. It does not replace the acceptance fixture or add an npm dependency. + +The Windows18-case capture still misses the first purple frame in all6 WebKit +cases without any plugin code; Chromium/Firefox controls recover it. The visible +original video can remain at currentTime0 while canvas exposes the later red +frame. This narrows investigation to the native surface on that tested host, but +does not certify all Safari versions or excuse plugin pixel gates. Exploratory +rate0/slow-rate, fastSeek, tiny first-seek offsets and pre-load/metadata layout or +paint waits did not fix it either. The next useful pixel evidence is the same +native/candidate case on another supported backend/device or a decoded-frame +path with explicit timestamps, not another arbitrary wait. See the native-decoder +checkpoint in refactor; AUTO-THUMB-PIXEL-01 and task03 remain open. + The frozen eight-second timeline has one unique purple first frame followed by red, black, blue and yellow sections. Its command and fingerprint are in `refactor/baselines/auto-thumbnail-timeline-media.json`. Reproduce into a new diff --git a/refactor/baselines/auto-thumbnail-native-decoder.json b/refactor/baselines/auto-thumbnail-native-decoder.json new file mode 100644 index 000000000..b1f1ff886 --- /dev/null +++ b/refactor/baselines/auto-thumbnail-native-decoder.json @@ -0,0 +1,1904 @@ +{ + "task": "PKG-AUTO-THUMB-03", + "status": "doing", + "baselineCommit": "772d79a67196ff61dc1e90499c275fee936cb942", + "outcome": "Native first-frame defect reproduced without plugin; production fix remains unproven", + "script": { + "file": "refactor/scripts/auto-thumbnail-native-probe.mjs", + "sha256": "448c701a8b91ecd352da349ac6afca3ba20e3abc234fe508fbb5289b8d6933cc" + }, + "final": { + "report": { + "file": "refactor/.cache/auto-thumbnail-native-1n8pV4/report.json", + "sha256": "bc686de7b8eac909fc3f28684bc4801ec9ad2c8848cbe0b9d1e05428f48d9192" + }, + "log": { + "file": "refactor/.cache/auto-native-probe-final.log", + "sha256": "7c6a890751dace746348c545689bcd8341aff6f709c4059877624fa1b7cdb2c9" + }, + "capturedAt": "2026-09-14T03:37:51.659Z", + "node": "v24.21.0", + "platform": "win32", + "ffmpegVersion": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers", + "args": [ + "-hide_banner", + "-loglevel", + "error", + "-n", + "-i", + "test/browser/media/auto-thumbnail-timeline.mp4", + "-map", + "0:v:0", + "-an", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-bf", + "0", + "-g", + "30", + "-crf", + "18", + "-movflags", + "+faststart", + "refactor\\.cache\\auto-thumbnail-native-1n8pV4\\no-b-frames.mp4" + ], + "media": [ + { + "file": "refactor\\.cache\\auto-thumbnail-native-1n8pV4\\original.mp4", + "sha256": "10370156cffada462d3ea00aae0caadd62752369cd6538cc55248b3ddd7bf994", + "metadata": { + "programs": [], + "stream_groups": [], + "streams": [ + { + "codec_name": "h264", + "profile": "High", + "has_b_frames": 2, + "time_base": "1/15360", + "start_time": "0.000000", + "duration": "8.000000" + } + ] + }, + "ffmpegFirstPixel": [ + 128, + 0, + 128, + 255 + ] + }, + { + "file": "refactor\\.cache\\auto-thumbnail-native-1n8pV4\\no-b-frames.mp4", + "sha256": "d08af99bc5021eedd4c32112e3f0b7d08c85bd35da39711362a71a9a9274ce3e", + "metadata": { + "programs": [], + "stream_groups": [], + "streams": [ + { + "codec_name": "h264", + "profile": "High", + "has_b_frames": 0, + "time_base": "1/15360", + "start_time": "0.000000", + "duration": "8.000000" + } + ] + }, + "ffmpegFirstPixel": [ + 128, + 0, + 128, + 255 + ] + } + ], + "results": [ + { + "engine": "webkit", + "browser": "26.6", + "file": "original.mp4", + "mode": "paused", + "purpleSamples": 0, + "mediaError": 0, + "supportsFrames": false, + "sampleCount": 31, + "first": { + "elapsed": 411, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 1328, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "uniqueRGBA": [ + "0,0,0,0" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "webkit", + "browser": "26.6", + "file": "original.mp4", + "mode": "visible-paused", + "purpleSamples": 0, + "mediaError": 0, + "supportsFrames": false, + "sampleCount": 31, + "first": { + "elapsed": 440, + "time": 0, + "readyState": 2, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 1364, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 238, + 14, + 14, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "238,14,14,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "webkit", + "browser": "26.6", + "file": "original.mp4", + "mode": "play-pause", + "purpleSamples": 0, + "mediaError": 0, + "supportsFrames": false, + "sampleCount": 31, + "first": { + "elapsed": 415, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 1321, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "uniqueRGBA": [ + "0,0,0,0" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": false + }, + { + "name": "play", + "time": 0, + "paused": false + }, + { + "name": "playing", + "time": 0, + "paused": false + }, + { + "name": "pause", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "webkit", + "browser": "26.6", + "file": "no-b-frames.mp4", + "mode": "paused", + "purpleSamples": 0, + "mediaError": 0, + "supportsFrames": false, + "sampleCount": 31, + "first": { + "elapsed": 410, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 1332, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "uniqueRGBA": [ + "0,0,0,0" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "webkit", + "browser": "26.6", + "file": "no-b-frames.mp4", + "mode": "visible-paused", + "purpleSamples": 0, + "mediaError": 0, + "supportsFrames": false, + "sampleCount": 31, + "first": { + "elapsed": 388, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 1298, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "uniqueRGBA": [ + "0,0,0,0" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "webkit", + "browser": "26.6", + "file": "no-b-frames.mp4", + "mode": "play-pause", + "purpleSamples": 0, + "mediaError": 0, + "supportsFrames": false, + "sampleCount": 31, + "first": { + "elapsed": 426, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 1334, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "uniqueRGBA": [ + "0,0,0,0" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": false + }, + { + "name": "play", + "time": 0, + "paused": false + }, + { + "name": "playing", + "time": 0, + "paused": false + }, + { + "name": "pause", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "file": "original.mp4", + "mode": "paused", + "purpleSamples": 30, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 52.19999998807907, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 556.6000000238419, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "127,0,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "file": "original.mp4", + "mode": "visible-paused", + "purpleSamples": 30, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 4.899999976158142, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 514.8000000119209, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "127,0,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "file": "original.mp4", + "mode": "play-pause", + "purpleSamples": 30, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 4.400000035762787, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 510.9000000357628, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "127,0,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": false + }, + { + "name": "play", + "time": 0, + "paused": false + }, + { + "name": "playing", + "time": 0, + "paused": false + }, + { + "name": "pause", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "file": "no-b-frames.mp4", + "mode": "paused", + "purpleSamples": 30, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 4.5, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 508, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "127,0,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "file": "no-b-frames.mp4", + "mode": "visible-paused", + "purpleSamples": 30, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 5.800000011920929, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 513.5, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "127,0,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "file": "no-b-frames.mp4", + "mode": "play-pause", + "purpleSamples": 30, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 5.899999976158142, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 0, + 0, + 0, + 0 + ] + }, + "last": { + "elapsed": 509.39999997615814, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 127, + 0, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "0,0,0,0", + "127,0,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": false + }, + { + "name": "play", + "time": 0, + "paused": false + }, + { + "name": "playing", + "time": 0, + "paused": false + }, + { + "name": "pause", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "firefox", + "browser": "155.0", + "file": "original.mp4", + "mode": "paused", + "purpleSamples": 31, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 18, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "last": { + "elapsed": 540, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "137,19,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "firefox", + "browser": "155.0", + "file": "original.mp4", + "mode": "visible-paused", + "purpleSamples": 31, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 14, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "last": { + "elapsed": 524, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "137,19,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "firefox", + "browser": "155.0", + "file": "original.mp4", + "mode": "play-pause", + "purpleSamples": 31, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 14, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "last": { + "elapsed": 537, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "137,19,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": false + }, + { + "name": "play", + "time": 0, + "paused": false + }, + { + "name": "playing", + "time": 0, + "paused": false + }, + { + "name": "pause", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "firefox", + "browser": "155.0", + "file": "no-b-frames.mp4", + "mode": "paused", + "purpleSamples": 31, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 32, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "last": { + "elapsed": 554, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "137,19,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "firefox", + "browser": "155.0", + "file": "no-b-frames.mp4", + "mode": "visible-paused", + "purpleSamples": 31, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 12, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "last": { + "elapsed": 537, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "137,19,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": true + } + ] + }, + { + "engine": "firefox", + "browser": "155.0", + "file": "no-b-frames.mp4", + "mode": "play-pause", + "purpleSamples": 31, + "mediaError": 0, + "supportsFrames": true, + "sampleCount": 31, + "first": { + "elapsed": 15, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "last": { + "elapsed": 559, + "time": 0, + "readyState": 4, + "paused": true, + "seeking": false, + "rgba": [ + 137, + 19, + 126, + 255 + ] + }, + "uniqueRGBA": [ + "137,19,126,255" + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "paused": true + }, + { + "name": "loadeddata", + "time": 0, + "paused": true + }, + { + "name": "canplay", + "time": 0, + "paused": false + }, + { + "name": "play", + "time": 0, + "paused": false + }, + { + "name": "playing", + "time": 0, + "paused": false + }, + { + "name": "pause", + "time": 0, + "paused": true + } + ] + } + ] + }, + "earlierNativeLog": { + "file": "refactor/.cache/auto-native-probe-initial.log", + "sha256": "96df2a72e766e01555b7c4985db437a81e6431c712bab10adf7d5c43dd58bcf8" + }, + "exploratoryImportFailure": { + "file": "refactor/.cache/auto-clock-probe.log", + "sha256": "918d343cf558e6a887da687f39e6648a0dca81ccc9fd990a9fe7a548c929e689" + }, + "exploratoryProbes": [ + { + "name": "clock", + "report": { + "file": "refactor/.cache/auto-thumbnail-clock-first-seek-hBU230/report.json", + "sha256": "e83affa0c89be1d61ef707e188e744b022bf2dd5c57a99503939b566ba9bc9f6" + }, + "log": { + "file": "refactor/.cache/auto-clock-probe-second.log", + "sha256": "6b4c088e23cadc26f9ac82b05dec26ad7d30c7af3cd3d31d9203e4d12f716ea5" + }, + "generator": { + "file": "refactor/.cache/auto-clock-probe.mjs", + "sha256": "02dd2abde85025895c5dada5099e5681b25aae5a06df7c14f8c884068d853a57" + }, + "generatedRunner": { + "file": "refactor/.cache/auto-clock-generated.mjs", + "sha256": "9ac05243e63dd1c5ba4612ac986c15851c64f49a0d90f8755cd06ae9af70e05f" + }, + "results": [ + { + "engine": "webkit", + "browser": "26.6", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "rate-zero", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "ba3c6fd443ebbd50624c7c402e4eb5337cbbd72c0d8648853f0d72174100994d" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "slow-clock", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0008537, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "e15c5dd8c027384835a8c9b84323ef9133d1c3253cad183e06adbdb3c169da8c" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "fast-zero", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0014379, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "927735ef099f7a8c9d9c8c98bf08044844b60cfa313e69795ba65adcf5bf17f4" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "playing-zero", + "outcome": "timeout", + "firstFrameMatches": false, + "firstDraw": null, + "codeSha256": "7359a1f6aa6b978c94dbcd6caf015bcd34b9e4d760f3880fc84dce285180f3ee" + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "variant": "rate-zero", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "ba3c6fd443ebbd50624c7c402e4eb5337cbbd72c0d8648853f0d72174100994d" + }, + { + "engine": "firefox", + "browser": "155.0", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "firefox", + "browser": "155.0", + "variant": "rate-zero", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "ba3c6fd443ebbd50624c7c402e4eb5337cbbd72c0d8648853f0d72174100994d" + } + ] + }, + { + "name": "offset", + "report": { + "file": "refactor/.cache/auto-thumbnail-offset-first-seek-NuuxYe/report.json", + "sha256": "33315efd20a10dd31af5a6a9ad8255012a71de5e5d65e0d836292705f51873c2" + }, + "log": { + "file": "refactor/.cache/auto-offset-probe.log", + "sha256": "2cdffa5c5e42160a6f586132cbc2668a9439f3485ae2d188e70b7ca873b049e0" + }, + "generator": { + "file": "refactor/.cache/auto-offset-probe.mjs", + "sha256": "01258392dea7d8b46ec1353b8805c180ee83bb872a840f0fd5f6d5ccbfa6086b" + }, + "generatedRunner": { + "file": "refactor/.cache/auto-offset-generated.mjs", + "sha256": "df48ee2b5fbfbfac882326449d551a7cdadbc4fb48c4abb0f3846c3d36366432" + }, + "results": [ + { + "engine": "webkit", + "browser": "26.6", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "epsilon", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0013635, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "b8b8a8cacdcbea0796144f07e4dd0a6be4f5075be8b242a7a45ca4219463fd5b" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "millisecond", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0005088, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "22fae830686901f5cbe45ad5b095f0228454d1a3a76967852543e9eb5862b06d" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "half-frame", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0021867, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6a59def246096f03b664166dd9111128d5dfab374b913af6e23507b319cd5cef" + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "variant": "epsilon", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0.000001, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "b8b8a8cacdcbea0796144f07e4dd0a6be4f5075be8b242a7a45ca4219463fd5b" + }, + { + "engine": "firefox", + "browser": "155.0", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "firefox", + "browser": "155.0", + "variant": "epsilon", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0.000001, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "b8b8a8cacdcbea0796144f07e4dd0a6be4f5075be8b242a7a45ca4219463fd5b" + } + ] + }, + { + "name": "layout", + "report": { + "file": "refactor/.cache/auto-thumbnail-layout-first-seek-GubiAN/report.json", + "sha256": "7f89eee1352e16ee8a20f390e1052c18338f4b1baaa3a3356a283178316e76d0" + }, + "log": { + "file": "refactor/.cache/auto-layout-probe.log", + "sha256": "41e83c785348abb7b4de1b8a0a2b569880b18302e793ad77fa57d8436a0e9366" + }, + "generator": { + "file": "refactor/.cache/auto-layout-probe.mjs", + "sha256": "bb48890ad01bf1b98f624381b1f692dd08329c672492d4842867c455f628f981" + }, + "generatedRunner": { + "file": "refactor/.cache/auto-layout-generated.mjs", + "sha256": "68484ecb8316a61c1037ee246deb61793c3f2f819f811413e1b1d4a4c5edcc16" + }, + "results": [ + { + "engine": "webkit", + "browser": "26.6", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "layout-before-src", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "808efa8818d5f317a92777ada48d39dfaadb9c6eba63845ff42b606dadfc60c8" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "paint-before-src", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0022993, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "45a48611d5e887f2957a35b4e95ef13ac6d69a5a0a29b64e70accd49a39481e1" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "layout-metadata", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0020653, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "d2277236eaf1eeda7c81e2e60475461b66e584a27c16c8df3533c778e2936472" + }, + { + "engine": "webkit", + "browser": "26.6", + "variant": "paint-metadata", + "outcome": "completed", + "firstFrameMatches": false, + "firstDraw": { + "time": 0.0011836, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "86a958c31466f1b361572f64e76ad0706ae7a401802cefee53a3951190a4df5e" + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "variant": "layout-before-src", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "808efa8818d5f317a92777ada48d39dfaadb9c6eba63845ff42b606dadfc60c8" + }, + { + "engine": "firefox", + "browser": "155.0", + "variant": "current", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302" + }, + { + "engine": "firefox", + "browser": "155.0", + "variant": "layout-before-src", + "outcome": "completed", + "firstFrameMatches": true, + "firstDraw": { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + "codeSha256": "808efa8818d5f317a92777ada48d39dfaadb9c6eba63845ff42b606dadfc60c8" + } + ] + } + ], + "validation": { + "lint": { + "file": "refactor/.cache/auto-native-lint.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "exitCode": 0 + }, + "nativeCases": 18, + "webkitFirstFrameFailures": 6, + "successfulEngineControls": 12, + "planAndRisk": { + "file": "refactor/.cache/auto-native-final.log", + "sha256": "db44816eb60ba734d6c5991b7031e9a4f4d30bce467f5fbf699c0cf30f52fe85", + "exitCode": 0 + } + }, + "limitations": [ + "Windows host only; not exact Safari/device diagnosis", + "Original acceptance pixels/media unchanged; no-B-frame media is only a diagnostic control", + "No production source or declaration change", + "No task completion or risk closure", + "Current upstream source is contextual, not an exact installed binary match", + "Cached exploratory variants are not production lifecycle implementations" + ] +} diff --git a/refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md new file mode 100644 index 000000000..54fbfbe6c --- /dev/null +++ b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md @@ -0,0 +1,72 @@ +# PKG-AUTO-THUMB-03: isolate the native decoder boundary + +Checkpoint from 772d79a67196ff61dc1e90499c275fee936cb942. Task03 and +AUTO-THUMB-PIXEL-01 remain open; no source fix, completion or publication is claimed. + +## New evidence + +The earlier first-seek experiments still imported the plugin. This checkpoint +adds `refactor/scripts/auto-thumbnail-native-probe.mjs` and the root command +`yarn probe:auto-thumbnail-native` to remove that dependency. It imports neither +ArtPlayer nor the thumbnail factory: only a native video, canvas, Playwright and +the already-verified local dev HTTP server. The original frozen MP4 remains +byte-identical. A separately generated H.264 variant with B frames disabled probes +whether frame reordering alone explains the observation; it is never substituted +for the original acceptance sample. FFmpeg decoding must show a purple first +pixel in both files before the browser run begins. + +The final18 cases cover2 media files,3 browsers and3 native modes: hidden paused, +visible paused and hidden play-then-immediate-pause. Every case records31 draws, +native events, currentTime, readyState, paused/seeking flags and RGBA. Requested +16ms sample timers run slower in Windows WebKit; elapsed times are retained rather +than pretending the nominal half-second is exact. All6 WebKit26.6 cases miss the +purple first frame. Visible original video eventually yields red while time stays0; +other WebKit cases remain transparent. All12 Chromium153.0.8010.12/Firefox155.0 +controls obtain purple. This proves the issue can occur without our plugin on +this Windows host; it does not prove the cause in all WebKit/Safari backends. + +The initial12-case run omitted visible-paused; its report is retained separately. +The final capture adds visible control and bounds native play Promise waiting. +Results, media metadata, FFmpeg version/arguments, source and raw-report hashes +are in [auto-thumbnail-native-decoder.json](../baselines/auto-thumbnail-native-decoder.json). + +Exploratory current-bundle variants also failed: zero/slow playback rate, +fastSeek(0), zero-rate play; first-seek offsets1us/1ms/16ms; layout before src, +two paints before src, layout after metadata and two paints after metadata. +The zero-rate play variant timed out, so it supplies no completed drawing result. +These cache-only experiments are not production cancellation implementations or +accepted fixes. Raw output and script/report hashes are retained for diagnosis. +One exploratory launch failed before browser execution because a data URL could +not resolve the bare Playwright import; executing a real cache .mjs fixed the +runner. No pixel assertion was relaxed to hide these outcomes. + +## Source interpretation and next action + +Current upstream [Windows Media Foundation implementation](https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/win/MediaPlayerPrivateMediaFoundation.cpp) +starts a native session for seeking and resolves seek completion from the session +started handler, separately from frame painting. That source makes a timing/surface +boundary plausible; it is not the pinned binary's exact source revision and is +not proof of the underlying defect. Runtime captures above are the actual evidence. + +Do not add an arbitrary wait, first-time offset or sample replacement to pass this +host's gate. The next useful pixel investigation requires another supported native +backend/device or an explicit decoded-frame path. No change to task03 acceptance, +the first-frame pixel check, public time formula or declared browser support is +authorized by this checkpoint. Other resource/typing/combination work may proceed +without calling this defect fixed. + +## Reproduction, ownership and validation + +Run under canonical Node24.21.0/Yarn1.22.22 with installed FFmpeg and FFprobe. +Optional FFMPEG/FFPROBE specify executable paths. No new npm dependency, lock or +package artifact change; the probe is explicit, not added to regular CI because +it needs the external encoder binaries. It owns its random-port server, browser +instances/pages and video/canvas cleanup, and retains only its generated diagnostic +directory. Existing8082 remains untouched. It creates no real workspace package. + +The command's successful exit means capture completed, not pixel acceptance. +Targeted script lint and plan/risk checks validate only this changed diagnostic and +metadata. Production tests are not rerun because runtime/type/acceptance-test files +are unchanged. The package maintenance guide explains the new diagnostic boundary. +Commit as a task03 checkpoint; revert that commit to remove this diagnostic and +records together. Local only, no push or publication. diff --git a/refactor/plan.md b/refactor/plan.md index 4d616588a..1d344131a 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -530,7 +530,7 @@ - PKG-AUDIO-05: [记录](changes/2026-09-12-PKG-AUDIO-05-combinations.md) [记录](baselines/audio-combinations-first.json) [记录](baselines/audio-combinations-checkpoint.json) [记录](audio-validation.md) - PKG-AUTO-THUMB-01: [记录](baselines/auto-thumbnail-release.json) [记录](baselines/auto-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-AUTO-THUMB-01-contract.md) [记录](baselines/auto-thumbnail-contract-validation.json) - PKG-AUTO-THUMB-02: [记录](baselines/auto-thumbnail-failures.md) [记录](baselines/auto-thumbnail-failures-validation.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-02-failures.md) -- PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md) [记录](baselines/auto-thumbnail-frame-presentation.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md) [记录](baselines/auto-thumbnail-rendering-readiness.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md) [记录](baselines/auto-thumbnail-first-seek.json) +- PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md) [记录](baselines/auto-thumbnail-frame-presentation.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md) [记录](baselines/auto-thumbnail-rendering-readiness.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md) [记录](baselines/auto-thumbnail-first-seek.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md) [记录](baselines/auto-thumbnail-native-decoder.json) - PKG-VTT-THUMB-01: [记录](baselines/vtt-thumbnail-release.json) [记录](baselines/vtt-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-VTT-THUMB-01-contract.md) - PKG-VTT-THUMB-02: [记录](baselines/vtt-thumbnail-failures.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-02-failures.md) - PKG-VTT-THUMB-03: [记录](changes/2026-09-13-PKG-VTT-THUMB-03-resources.md) [记录](baselines/vtt-thumbnail-resources.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-03-parser.md) [记录](baselines/vtt-thumbnail-parser.json) diff --git a/refactor/progress.md b/refactor/progress.md index a65212b51..aa0e3a0cc 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,19 @@ # 进度与证据 +## Auto Thumbnail 原生首帧边界检查点 + +新增独立 video/canvas 诊断,完全不加载 ArtPlayer 或插件。冻结原视频不变,另用 +FFmpeg生成无B帧对照,确认两文件首帧均为紫色。Windows三引擎/两文件/三模式 +共18组:WebKit26.6六组均未得到紫色,Chromium/Firefox十二组得到;可见视频也 +会在currentTime0读到后续红帧。此证据将问题缩小到该宿主的原生画面路径,不能 +推断所有Safari或标为修复。速率、微小seek偏移和布局/绘制等待的探索同样无效, +没有采用绕过首帧断言的方案。命令、实际FFmpeg/浏览器版本、事件/像素和报告指纹 +见[检查点](changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md)和[证据](baselines/auto-thumbnail-native-decoder.json)。 +源码/类型/分发与像素验收未改,仅新增诊断脚本和文档;目标lint与计划/风险检查 +通过。PKG-AUTO-THUMB-03/AUTO-THUMB-PIXEL-01保持开放,245项/173 done不变。 +下一步按原生边界选择其他支持后端/设备取证,同时继续该包的资源与异步边界审查; +不重复追加任意等待。VAST默认行为选择仍待答复。本地检查点提交,无推送/发布。 + ## MOD-DEV-01 自有开发服务器与关闭流程完成 HTTP、静态资源与 SSE 拆为严格 TS 模块,修复 Servor4.0.2 端口占用却退出0的 diff --git a/refactor/risks.json b/refactor/risks.json index 06ceefaf0..e740fea03 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4187,11 +4187,13 @@ "refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md", "refactor/baselines/auto-thumbnail-rendering-readiness.json", "refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md", - "refactor/baselines/auto-thumbnail-first-seek.json" + "refactor/baselines/auto-thumbnail-first-seek.json", + "refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md", + "refactor/baselines/auto-thumbnail-native-decoder.json" ], "compatibleResolution": "Establish actual decoded-frame readiness for the existing timeline/grid and preserve silent independent extraction; do not waive colored pixel checks as unsupported media. Intrinsic-size hidden/opacity/clip/visible rendering crossed with current/readyState/first-loadeddata-event strategies still misses the unique first frame in all 12 Windows WebKit profiles. Correct Firefox frames retain totalVideoFrames=0, so that counter cannot be a portable readiness gate. Do not reintroduce these unvalidated workarounds. First-seek diagnostics additionally exclude drawing the first loadeddata frame without seek, one/two RAF waits, post-seek RAF waits and verified forward/back seeking on the recorded Windows WebKit host. The warm comparison first had an invalid stale-event completion, then a timeout; final bounded retries verify actual [1,1,0] seeking and still miss purple. Controls retain purple. These are exclusions, not production fixes.", "closureCriteria": "Candidate source and artifacts generate usable sampled pixels on native browser controls with lifecycle checks; supported Safari and core combinations remain explicit final gates.", - "workspaceState": "The frames module now waits for native presentation and seek completion on capable browsers. Chromium/Firefox source/main/legacy assert all five cells including first purple frame, second red cell, legitimate black and presentation times. Callback-less Windows WebKit first two cells remain diagnostic, later cells asserted; physical Safari/core combinations remain open. 84 native lifecycle/pixel cases passed with explicit held native-frame vs Blob-fallback paths; 19 new Node cases fail 17 ways on prior checkpoint and all 52 current cases pass per candidate. Risk and task03 remain open." + "workspaceState": "The frames module now waits for native presentation and seek completion on capable browsers. Chromium/Firefox source/main/legacy assert all five cells including first purple frame, second red cell, legitimate black and presentation times. Callback-less Windows WebKit first two cells remain diagnostic, later cells asserted; physical Safari/core combinations remain open. 84 native lifecycle/pixel cases passed with explicit held native-frame vs Blob-fallback paths; 19 new Node cases fail 17 ways on prior checkpoint and all 52 current cases pass per candidate. Risk and task03 remain open. Native-only18-case Windows probe also misses the purple first frame in all6 WebKit26.6 cases (hidden/visible paused and immediate play-pause, original and no-B-frame H.264);12 Chromium/Firefox controls recover it. No ArtPlayer/plugin is imported, narrowing but not resolving the native surface boundary. Rate/offset/layout explorations failed and must not be reintroduced as assumed fixes." }, { "id": "VTT-THUMB-DIST-01", diff --git a/refactor/scripts/auto-thumbnail-native-probe.mjs b/refactor/scripts/auto-thumbnail-native-probe.mjs new file mode 100644 index 000000000..2cab49739 --- /dev/null +++ b/refactor/scripts/auto-thumbnail-native-probe.mjs @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { chromium, firefox, webkit } from '@playwright/test' +import { startDevServer } from '../../scripts/library/server.ts' + +assert.equal(process.version.slice(1), fs.readFileSync('.node-version', 'utf8').trim()) +assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn probe:auto-thumbnail-native') +assert.equal(process.argv.length, 2, 'No probe arguments are supported') +const fingerprint = data => createHash('sha256').update(data).digest('hex') +const original = 'test/browser/media/auto-thumbnail-timeline.mp4' +const originalBytes = fs.readFileSync(original) +assert.equal(fingerprint(originalBytes), JSON.parse(fs.readFileSync('refactor/baselines/auto-thumbnail-timeline-media.json')).file.sha256) +const directory = fs.mkdtempSync('refactor/.cache/auto-thumbnail-native-') +fs.writeFileSync(path.join(directory, 'index.html'), 'Native decoder diagnostic') +fs.copyFileSync(original, path.join(directory, 'original.mp4')) +const ffmpeg = process.env.FFMPEG || 'ffmpeg' +const ffprobe = process.env.FFPROBE || 'ffprobe' +const args = ['-hide_banner', '-loglevel', 'error', '-n', '-i', original, '-map', '0:v:0', '-an', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-bf', '0', '-g', '30', '-crf', '18', '-movflags', '+faststart', path.join(directory, 'no-b-frames.mp4')] +execFileSync(ffmpeg, args, { windowsHide: true }) +const ffmpegVersion = execFileSync(ffmpeg, ['-version'], { encoding: 'utf8', windowsHide: true }).split('\n')[0].trim() +const media = ['original.mp4', 'no-b-frames.mp4'].map((name) => { + const file = path.join(directory, name) + const metadata = JSON.parse(execFileSync(ffprobe, ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=codec_name,profile,time_base,start_time,duration,has_b_frames', '-of', 'json', file], { encoding: 'utf8', windowsHide: true })) + const raw = execFileSync(ffmpeg, ['-v', 'error', '-i', file, '-frames:v', '1', '-vf', 'scale=1:1', '-pix_fmt', 'rgba', '-f', 'rawvideo', 'pipe:1'], { windowsHide: true }) + assert.equal(raw.length, 4) + assert(raw[0] > 100 && raw[2] > 100 && raw[1] < 40, 'Both files must actually decode a purple first frame') + return { file, sha256: fingerprint(fs.readFileSync(file)), metadata, ffmpegFirstPixel: [...raw] } +}) +const errors = [] +const server = await startDevServer({ root: directory, port: 0, onError: error => errors.push(String(error)) }) +const results = [] +console.log(`Native decoder diagnostics: ${directory}`) +try { + for (const [engineName, engine] of Object.entries({ webkit, chromium, firefox })) { + const browser = await engine.launch() + try { + for (const file of ['original.mp4', 'no-b-frames.mp4']) { + for (const mode of ['paused', 'visible-paused', 'play-pause']) { + const page = await browser.newPage() + try { + await page.goto(server.url) + const result = await page.evaluate(async ({ file, mode }) => { + const video = document.createElement('video') + video.width = 320 + video.height = 180 + video.muted = true + video.playsInline = true + video.style.cssText = 'position:fixed;visibility:hidden;width:320px;height:180px' + if (mode === 'visible-paused') + video.style.visibility = 'visible' + document.body.append(video) + const canvas = document.createElement('canvas') + canvas.width = 320 + canvas.height = 180 + const context = canvas.getContext('2d') + const samples = [] + const events = [] + const started = performance.now() + let timer + const state = () => ({ elapsed: performance.now() - started, time: video.currentTime, readyState: video.readyState, paused: video.paused, seeking: video.seeking }) + for (const name of ['loadedmetadata', 'loadeddata', 'canplay', 'play', 'playing', 'pause', 'seeked', 'error']) + video.addEventListener(name, () => events.push({ name, ...state() })) + try { + const ready = new Promise((resolve, reject) => { + video.onloadeddata = resolve + video.onerror = () => reject(new Error(`Media error ${video.error?.code}`)) + timer = setTimeout(() => reject(new Error('Native loadeddata timeout')), 8000) + }) + video.src = `/${file}` + await ready + clearTimeout(timer) + if (mode === 'play-pause') { + await Promise.race([ + video.play(), + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Native play timeout')), 8000) }), + ]) + clearTimeout(timer) + video.pause() + } + for (let index = 0; index < 31; index++) { + context.clearRect(0, 0, 320, 180) + context.drawImage(video, 0, 0) + samples.push({ ...state(), rgba: [...context.getImageData(160, 90, 1, 1).data] }) + await new Promise(resolve => setTimeout(resolve, 16)) + } + return { samples, events, mediaError: video.error?.code || 0, supportsFrames: typeof video.requestVideoFrameCallback === 'function' } + } + finally { + clearTimeout(timer) + video.pause() + video.removeAttribute('src') + video.load() + video.remove() + canvas.width = canvas.height = 0 + } + }, { file, mode }) + const purple = result.samples.filter(({ rgba: [r, g, b, a] }) => a === 255 && Math.min(r, b) > 100 && Math.abs(r - b) < 30 && g < 40) + const entry = { engine: engineName, browser: browser.version(), file, mode, purpleSamples: purple.length, ...result } + results.push(entry) + console.log(JSON.stringify({ engine: engineName, file, mode, purpleSamples: purple.length, first: result.samples[0], last: result.samples.at(-1) })) + } + finally { await page.close() } + } + } + } + finally { await browser.close() } + } +} +finally { + await server.close() + assert.deepEqual(errors, []) + assert.equal(fingerprint(fs.readFileSync(original)), fingerprint(originalBytes)) + fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify({ task: 'PKG-AUTO-THUMB-03', capturedAt: new Date().toISOString(), node: process.version, platform: process.platform, ffmpegVersion, args, media, results, scope: 'Native video/canvas only; no ArtPlayer/plugin imported. No-seek hidden/visible paused and hidden play-pause controls capture31 draws separated by16ms requested timers; actual elapsed durations are recorded. The no-B-frame transcode is diagnostic only and never replaces the frozen acceptance fixture. Counts report observations, not acceptance or a production fix.' }, null, 2)}\n`) +} diff --git a/refactor/tasks.json b/refactor/tasks.json index 49fde8d10..3428698c7 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1801,7 +1801,9 @@ "changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md", "baselines/auto-thumbnail-rendering-readiness.json", "changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md", - "baselines/auto-thumbnail-first-seek.json" + "baselines/auto-thumbnail-first-seek.json", + "changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md", + "baselines/auto-thumbnail-native-decoder.json" ] }, {