Files
糖猫猫andrionzhao 0896841d23 feat: batch video upscaling with folder upload, directory save, and result comparison (#1051)
* feat: support batch video upscaling with folder upload and directory save

- Add multi-file selection and entire folder upload (webkitdirectory)
- Support drag & drop of both files and folders via FileSystemEntry API
- Add save directory picker using File System Access API (showDirectoryPicker)
- Process videos sequentially with per-file progress and status tracking
- Add right-side result list with click-to-compare functionality
- Add per-video duration and total batch duration display
- Add retry button for failed items with error detail display
- Remove auto-play on comparison viewer load
- Fix worker error handling: handle "error" command to reject stale promises
- Fix event listener accumulation in video comparison (AbortController)
- Fix XSS risk by escaping HTML in all innerHTML injections
- Fix URL revocation timing in download to prevent large file truncation
- Extract writeToSaveDirectory and truncateName helpers to reduce duplication
- Add Usage Guide panel explaining single/multiple/folder upload options

Files changed:
  docs/upscaler/index.html  (+1110/-181)
  docs/upscaler/upscaler.js (+110)

* fix: prevent auto-play and resource consumption during upscaling

- Set preload="metadata" on preview and comparison video elements
- Explicitly pause preview video after setting src in showVideoPreview
- Remove preview video seek sync from batch processing progress callbacks
  (was causing continuous decode during upscaling, wasting GPU resources)
- Remove preview video seek sync from retry progress callback
- All videos now require manual play; no background resource usage

* fix: improve upscaled video quality with higher bitrate and codec support

- Raise base bitrate from 2.5 Mbps (25e5) to 8 Mbps (8e6) for standard and
  firefox processors, preventing quality loss when source videos have high
  bitrates (e.g. 720p @ 16.7 Mbps was producing 1440p @ 10 Mbps, smaller
  than the original file)
- Double the bitrate for mediabunny processor (X.n1 * 2) to account for the
  4x pixel increase from 2x upscaling
- Add H.264 Level 5.1 (avc1.640033) and Level 5.2 (avc1.640034) codecs to
  the fallback lists, since Level 4.0 (avc1.640028, ~24.6 Mbps) cannot
  support the higher bitrates for 1440p+ outputs
- Add latencyMode: "quality" to VideoEncoder configs for both standard and
  firefox processors, prioritizing encoding quality over latency (suitable
  for offline processing)

* feat: add video data comparison panel for original vs upscaled metrics

- Add a comparison data table below the file info bar showing 6 metrics:
  Resolution, File Size, Duration, Est. Bitrate, Pixels, Process Time
- Display per-metric change percentage with color-coded indicators:
  green (↑) for improvements, red (↓) for regressions, gray for neutral
- Estimated bitrate calculated from file size × 8 / video duration
- Table renders after both original and upscaled video metadata loads
- Auto-hide panel on reset

* fix: make video data comparison panel full-width and responsive

- Move comparison data table from inside the left column to the full-width
  area below the left/right layout, so it is no longer constrained by the
  comparison viewer column width
- Remove table-fixed colgroup and overflow-x-auto that caused horizontal
  scrolling on smaller screens
- Table now uses w-full auto-layout, displaying all 4 columns (Metric,
  Original, Upscaled, Change) without requiring horizontal scrolling

---------

Co-authored-by: rionzhao <rionzhao@tencent.com>
2026-09-10 17:19:38 +08:00

393 lines
8.4 KiB
CSS

@charset "utf-8";
:root {
--mocha-color: #000;
--mocha-bg-color: #fff;
--mocha-pass-icon-color: #00d6b2;
--mocha-pass-color: #fff;
--mocha-pass-shadow-color: rgba(0,0,0,.2);
--mocha-pass-mediump-color: #c09853;
--mocha-pass-slow-color: #b94a48;
--mocha-test-pending-color: #0b97c4;
--mocha-test-pending-icon-color: #0b97c4;
--mocha-test-fail-color: #c00;
--mocha-test-fail-icon-color: #c00;
--mocha-test-fail-pre-color: #000;
--mocha-test-fail-pre-error-color: #c00;
--mocha-test-html-error-color: #000;
--mocha-box-shadow-color: #eee;
--mocha-box-bottom-color: #ddd;
--mocha-test-replay-color: #000;
--mocha-test-replay-bg-color: #eee;
--mocha-stats-color: #888;
--mocha-stats-em-color: #000;
--mocha-stats-hover-color: #eee;
--mocha-error-color: #c00;
--mocha-code-comment: #ddd;
--mocha-code-init: #2f6fad;
--mocha-code-string: #5890ad;
--mocha-code-keyword: #8a6343;
--mocha-code-number: #2f6fad;
}
@media (prefers-color-scheme: dark) {
:root {
--mocha-color: #fff;
--mocha-bg-color: #222;
--mocha-pass-icon-color: #00d6b2;
--mocha-pass-color: #222;
--mocha-pass-shadow-color: rgba(255,255,255,.2);
--mocha-pass-mediump-color: #f1be67;
--mocha-pass-slow-color: #f49896;
--mocha-test-pending-color: #0b97c4;
--mocha-test-pending-icon-color: #0b97c4;
--mocha-test-fail-color: #f44;
--mocha-test-fail-icon-color: #f44;
--mocha-test-fail-pre-color: #fff;
--mocha-test-fail-pre-error-color: #f44;
--mocha-test-html-error-color: #fff;
--mocha-box-shadow-color: #444;
--mocha-box-bottom-color: #555;
--mocha-test-replay-color: #fff;
--mocha-test-replay-bg-color: #444;
--mocha-stats-color: #aaa;
--mocha-stats-em-color: #fff;
--mocha-stats-hover-color: #444;
--mocha-error-color: #f44;
--mocha-code-comment: #ddd;
--mocha-code-init: #9cc7f1;
--mocha-code-string: #80d4ff;
--mocha-code-keyword: #e3a470;
--mocha-code-number: #4ca7ff;
}
}
body {
margin:0;
background-color: var(--mocha-bg-color);
color: var(--mocha-color);
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 60px 50px;
}
#mocha ul,
#mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1,
#mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha .hidden {
display: none;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
overflow: hidden;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial, sans-serif;
}
#mocha .test.pass.medium .duration {
background: var(--mocha-pass-mediump-color);
}
#mocha .test.pass.slow .duration {
background: var(--mocha-pass-slow-color);
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: var(--mocha-pass-icon-color);
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: var(--mocha-pass-color);
-webkit-box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color);
-moz-box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color);
box-shadow: inset 0 1px 1px var(--mocha-pass-shadow-color);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: var(--mocha-test-pending-color);
}
#mocha .test.pending::before {
content: '◦';
color: var(--mocha-test-pending-icon-color);
}
#mocha .test.fail {
color: var(--mocha-test-fail-color);
}
#mocha .test.fail pre {
color: var(--mocha-test-fail-pre-color);
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: var(--mocha-test-fail-icon-color);
}
#mocha .test pre.error {
color: var(--mocha-test-fail-pre-error-color);
max-height: 300px;
overflow: auto;
}
#mocha .test .html-error {
overflow: auto;
color: var(--mocha-test-html-error-color);
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid var(--mocha-box-shadow-color);
max-width: 85%; /*(1)*/
max-width: -webkit-calc(100% - 42px);
max-width: -moz-calc(100% - 42px);
max-width: calc(100% - 42px); /*(2)*/
max-height: 300px;
word-wrap: break-word;
border-bottom-color: var(--mocha-box-bottom-color);
-webkit-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-moz-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#mocha .test .html-error pre.error {
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
box-shadow: 0;
padding: 0;
margin: 0;
margin-top: 18px;
max-height: none;
}
/**
* (1): approximate for browsers not supporting calc
* (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
* ^^ seriously
*/
#mocha .test pre {
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid var(--mocha-box-shadow-color);
max-width: 85%; /*(1)*/
max-width: -webkit-calc(100% - 42px);
max-width: -moz-calc(100% - 42px);
max-width: calc(100% - 42px); /*(2)*/
word-wrap: break-word;
border-bottom-color: var(--mocha-box-bottom-color);
-webkit-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-moz-box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
box-shadow: 0 1px 3px var(--mocha-box-shadow-color);
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#mocha .test h2 {
position: relative;
}
#mocha .test a.replay {
position: absolute;
top: 3px;
right: 0;
text-decoration: none;
vertical-align: middle;
display: block;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background: var(--mocha-test-replay-bg-color);
font-size: 15px;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
-webkit-transition:opacity 200ms;
-moz-transition:opacity 200ms;
-o-transition:opacity 200ms;
transition: opacity 200ms;
opacity: 0.7;
color: var(--mocha-test-replay-color);
}
#mocha .test:hover a.replay {
box-shadow: 0 0 1px inset var(--mocha-test-replay-color);
opacity: 1;
}
#mocha-report.pass .test.fail {
display: none;
}
#mocha-report.fail .test.pass {
display: none;
}
#mocha-report.pending .test.pass,
#mocha-report.pending .test.fail {
display: none;
}
#mocha-report.pending .test.pass.pending {
display: block;
}
#mocha-error {
color: var(--mocha-error-color);
font-size: 1.5em;
font-weight: 100;
letter-spacing: 1px;
}
#mocha-stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: var(--mocha-stats-color);
z-index: 1;
}
#mocha-stats .progress {
float: right;
padding-top: 0;
/**
* Set safe initial values, so mochas .progress does not inherit these
* properties from Bootstrap .progress (which causes .progress height to
* equal line height set in Bootstrap).
*/
height: auto;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background-color: initial;
}
#mocha-stats em {
color: var(--mocha-stats-em-color);
}
#mocha-stats a {
text-decoration: none;
color: inherit;
}
#mocha-stats a:hover {
border-bottom: 1px solid var(--mocha-stats-hover-color);
}
#mocha-stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
#mocha-stats canvas {
width: 40px;
height: 40px;
}
#mocha code .comment { color: var(--mocha-code-comment); }
#mocha code .init { color: var(--mocha-code-init); }
#mocha code .string { color: var(--mocha-code-string); }
#mocha code .keyword { color: var(--mocha-code-keyword); }
#mocha code .number { color: var(--mocha-code-number); }
@media screen and (max-device-width: 480px) {
#mocha {
margin: 60px 0px;
}
#mocha #stats {
position: absolute;
}
}