Skip to content

feat(ui): use sv11-ui components in the media player#34

Merged
twangodev merged 21 commits into
mainfrom
feat/sv11-ui-refresh
Jun 7, 2026
Merged

feat(ui): use sv11-ui components in the media player#34
twangodev merged 21 commits into
mainfrom
feat/sv11-ui-refresh

Conversation

@twangodev

@twangodev twangodev commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Refresh the JCEF media player by adopting sv11-ui components.

Summary by CodeRabbit

  • New Features

    • Rich audio player with waveform visualization, draggable scrubber (audible scrubbing), live-microphone and recording views, metadata/codec inspector, and improved playback controls (play/pause, 10s skip, speed selector, progress & volume).
  • UI Components

    • New accessible buttons, dropdowns and waveform/audio controls with updated theming and styling.
  • Tests

    • Added browser and unit tests covering waveform, media-info, playback, and media-server behaviors.
  • Chores

    • Build/config and theming updates; updated project ignore rules.

Initialize shadcn-svelte in ui/ as the foundation for adopting sv11-ui
components in the JCEF player. No player behavior changes: App.svelte is
untouched and the new primitives are unused — this only lays groundwork.

- Add $lib path alias to tsconfig.json, tsconfig.app.json and vite.config.ts
  (the Vite project had no $lib; the shadcn-svelte CLI requires it)
- shadcn-svelte init: components.json (Nova preset, Lucide + Geist to match
  sv11-ui's stack), cn util, theme tokens
- Reconcile theme with jetplay's IDE-driven theming: shadcn's dark palette is
  the :root default (the player opens dark like the IDE) and the light palette
  lives under @media(prefers-color-scheme: light); jetplay uses no .dark class.
  Define --radius in the dark default so rounding isn't undefined in dark mode.
- Pull button + dropdown-menu primitives (default shadcn registry)
- Ignore the whole generated src/main/resources/player/ dir (vite outDir with
  emptyOutDir); previously only index.html was ignored, leaking ~4.7MB of
  rebuilt demo assets into git status

Deferred to Phase 1: the sv11 audio-player composite — its registry items land
at a doubled path (target-path bug in sv11 registry.json) and it still needs
waveform-sampler plus local-media wiring.

Verified: ./gradlew test green (48/48); npm run build green (single-file,
203kB / 108kB gzip). npm run check shows only 2 pre-existing App.svelte $state
errors that also fail on clean main (not introduced here).
Copilot AI review requested due to automatic review settings June 2, 2026 20:50
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Shadcn-Svelte UI library and Tailwind theming, configures module resolution, implements button and dropdown component systems, and introduces a full audio-player + waveform subsystem (core, UI controls, waveform renderers, sampling, scratchable scrubbing hook, JVM waveform extraction, MediaServer loopback, bridge wiring, and tests).

Changes

Shadcn-Svelte Component Library Setup

Layer / File(s) Summary
Build & Dependency Foundation
.gitignore, ui/package.json, ui/components.json
Ignore built player directory; add UI/dev dependencies and create Shadcn config for Tailwind and registry settings.
TypeScript & Module Resolution Configuration
ui/tsconfig.app.json, ui/tsconfig.json, ui/vite.config.ts
Add $lib path alias and set baseUrl for TypeScript and Vite so internal UI imports resolve to src/lib.
Design System & Theme Palette
ui/src/app.css
Import Tailwind/Shadcn CSS and Geist font; define oklch-based semantic tokens and prefers-color-scheme-driven theming and base styles.
Shared Utilities & Type Helpers
ui/src/lib/utils.ts
Add cn helper (clsx + twMerge) and TypeScript utility types for stripping child/children and adding element refs.
Button Component
ui/src/lib/components/ui/button/button.svelte, ui/src/lib/components/ui/button/index.ts
Typed button component using Tailwind Variants supporting anchor/button polymorphism, exported types, and a barrel module.
Dropdown Menu System
ui/src/lib/components/ui/dropdown-menu/*
Add dropdown Root, Trigger, Portal, Content, Group, Heading, Label, Items (base/checkbox/radio), Separator, Shortcut, Submenu, SubTrigger, and an index barrel exporting aliases.
Audio Graph & Core Player
ui/src/lib/components/ui/audio-player/*
Introduce AudioGraph for WebAudio wiring, audio-player context/state class, root <audio> wrapper syncing element state to shared player.
Audio Player UI & Controls
ui/src/lib/AudioPlayer.svelte, ui/src/lib/AudioPlayerBody.svelte, ui/src/lib/components/ui/audio-player/*
Refactor AudioPlayer to use shared UI components; add body with waveform scrubbing, progress/controls, speed/duration/time components, and control button implementations.
Waveform Core & Variants
ui/src/lib/components/ui/waveform/*
Add canvas-based Waveform core plus static, scrubber, scrolling, recording, microphone, and live-microphone variants, utilities, and barrel exports.
Waveform Sampling & Scratch Hook
ui/src/lib/components/ui/audio-player/waveform-sampler.ts, ui/src/lib/use-scratchable-waveform.svelte.ts
Add precompute/sample waveform utilities and a scratchable waveform hook implementing audible scratch playback, momentum, and keyboard seeking.
JVM Waveform Extraction & Bridge
src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt, src/main/kotlin/dev/twango/jetplay/browser/PlayerBridge.kt, src/main/kotlin/dev/twango/jetplay/editor/MediaFileEditor.kt, src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt
Add FFmpeg-based waveform extractor, loopback MediaServer to serve local files, editor-side extraction and send via PlayerBridge.sendWaveform, and serve/release lifecycle.
Examples & Tests
ui/src/lib/components/ui/audio-player/example-tracks.ts, ui/tests/audio-player.spec.ts, ui/tests/states.spec.ts, src/test/kotlin/...
Add example tracks, wire global window.jetplayWaveform callback and pass waveform to AudioPlayer, update Playwright tests and JVM tests for waveform extraction and MediaServer behavior.

Sequence Diagram: audio waveform extraction and delivery flow

sequenceDiagram
  participant MediaFileEditor as MediaFileEditor.kt
  participant WaveformExtractor as WaveformExtractor
  participant EditorBridge as PlayerBridge.sendWaveform
  participant BrowserWindow as window.jetplayWaveform
  participant AppSvelte as ui/src/App.svelte
  participant AudioPlayer as AudioPlayer.Root
  participant AudioGraph as AudioGraph

  MediaFileEditor->>WaveformExtractor: extract(localFile)
  WaveformExtractor-->>MediaFileEditor: bars[]
  MediaFileEditor->>EditorBridge: bridge.sendWaveform(bars)
  EditorBridge->>BrowserWindow: executeJs("window.jetplayWaveform(...)")
  BrowserWindow->>AppSvelte: jetplayWaveform(bars)
  AppSvelte->>AudioPlayer: set waveform prop
  AudioPlayer->>AudioGraph: ensureAnalyser(audioElement) / use waveform for scrub visuals
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"A rabbit hums a build-time song,
Buttons, waves, and tempos strong,
Colors in oklch bloom and play,
Scrub and listen, night and day,
🐇✨"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sv11-ui-refresh

@socket-security

socket-security Bot commented Jun 2, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR scaffolds a shadcn-svelte foundation inside ui/ for the upcoming player UI refresh (Phase 0), adding core primitives, build aliases, and theme/CSS integration without changing player behavior.

Changes:

  • Add $lib alias support across Vite + TypeScript config to match shadcn-svelte conventions.
  • Introduce shadcn-style UI primitives (Button + Dropdown Menu) and shared cn()/type utilities.
  • Integrate shadcn-svelte + tw-animate + Geist font and reconcile theme tokens in app.css; add shadcn dependencies and components.json.

Reviewed changes

Copilot reviewed 27 out of 29 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ui/vite.config.ts Adds $lib alias for Vite builds to support shadcn-style imports.
ui/tsconfig.json Adds baseUrl/paths so editors/TS can resolve $lib imports.
ui/tsconfig.app.json Adds $lib paths for app typechecking / svelte-check.
ui/src/lib/utils.ts Adds cn() helper and shared typing utilities used by UI primitives.
ui/src/lib/components/ui/dropdown-menu/* Adds dropdown-menu wrapper components around bits-ui primitives.
ui/src/lib/components/ui/dropdown-menu/index.ts Exposes dropdown-menu components via a single entrypoint.
ui/src/lib/components/ui/button/button.svelte Adds a shadcn-style Button with tailwind-variants styling.
ui/src/lib/components/ui/button/index.ts Re-exports Button component + types/variants.
ui/src/app.css Imports shadcn/tw-animate/Geist and reconciles theme variables + base styles.
ui/package.json Adds shadcn-svelte/bits-ui and related dependencies.
ui/package-lock.json Locks new dependency graph for the added UI/tooling packages.
ui/components.json Adds shadcn-svelte config (aliases, style, registry, etc.).
.gitignore Ignores generated src/main/resources/player/ build output directory.
Files not reviewed (1)
  • ui/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ui/src/app.css
Comment thread ui/src/lib/components/ui/button/button.svelte Outdated
Comment thread ui/src/lib/utils.ts Outdated
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Qodana Community for JVM

18 new problems were found

Inspection name Severity Problems
Constant conditions 🔶 Warning 16
Accessor call that can be replaced with property access syntax ◽️ Notice 2

💡 Qodana analysis was run in the pull request mode: only the changed files were checked

View the detailed Qodana report

To be able to view the detailed Qodana report, you can either:

To get *.log files or any other Qodana artifacts, run the action with upload-result option set to true,
so that the action will upload the files as the job artifacts:

      - name: 'Qodana Scan'
        uses: JetBrains/qodana-action@v2026.1.0
        with:
          upload-result: true
Contact Qodana team

Contact us at qodana-support@jetbrains.com

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/app.css (1)

53-127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope the new base styles so Phase 0 stays behavior-neutral.

Lines 58 and 121-125 are not inert scaffolding: * { @apply border-border ... } plus body { @apply bg-background text-foreground; } immediately restyle the existing player, even though none of the new shadcn primitives are mounted yet. That also conflicts with the earlier html, body palette on Lines 66-68, so the current app can switch from --color-surface/--color-primary to the new --background/--foreground values right away. If the goal is “no player behavior changes” in Phase 0, these resets need to be scoped behind a future container or deferred until the new UI is actually adopted.

Suggested direction
 `@layer` base {
   * {
     margin: 0;
     padding: 0;
     box-sizing: border-box;
-  	`@apply` border-border outline-ring/50;
   }
@@
-  body {
-		`@apply` bg-background text-foreground;
-  }
-	html {
-		`@apply` font-sans;
-  }
 }

Or, if you want to keep the shadcn reset ready, apply it under a dedicated root that the refreshed player will mount into in Phase 1 instead of globally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/app.css` around lines 53 - 127, The global base rules are
unintentionally overriding the existing app (the universal selector * with
`@apply` border-border/outline-ring and the body { `@apply` bg-background
text-foreground })—scope these shadcn primitives behind a dedicated root so
Phase 0 remains behavior-neutral: move the global resets inside a new container
selector (e.g., .shadcn-root or `#new-ui-root`) instead of applying them at `@layer`
base globally, and relocate the background/foreground `@apply` from body into that
same scoped root while leaving the existing html, body color variables and
layout rules untouched; update selectors in this file (the `@layer` base block,
the * rule, and the body rule) to use that dedicated root so the old player
style is preserved until the new UI mounts.
🧹 Nitpick comments (1)
ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte (1)

19-20: ⚡ Quick win

Confirm data-inset handling with inset={false}
In ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte, Svelte omits data-* attributes when the bound value is false, so inset={false} won’t leave a present data-inset="false" in the DOM. As a result, data-[inset]:ps-8 won’t match and the indentation issue described doesn’t apply; keep the current approach (conditional class composition is optional for readability).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte`
around lines 19 - 20, The bound attribute data-inset={inset} will be omitted
when inset is false (so data-[inset]:ps-8 won't match), so leave the current
implementation as-is or, if you want clearer intent, change the class
composition in the cn(...) call to conditionally add the padding class when
inset is true (e.g., use inset ? 'ps-8' : ''), referencing the inset prop, cn
helper, className and the data-[inset]:ps-8 usage in
dropdown-menu-group-heading.svelte.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/lib/components/ui/button/button.svelte`:
- Line 7: The anchor branch in Button.svelte currently never receives a real
disabled attribute so the shared "disabled:*" styles don't apply; update the
anchor rendering (the branch that uses an <a> element) to apply the same
non-interactive treatment when the component's disabled prop is true by: adding
aria-disabled="true" and tabindex="-1" and appending explicit non-interactive
classes (e.g. pointer-events-none and opacity-50 or the equivalent used on the
button branch) to the anchor's class list (the same place that uses the base
string), and ensure any click handlers early-return when disabled; this will
make the anchor match the button's visual and interaction contract.

In `@ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte`:
- Around line 15-17: The component overrides children as a plain Snippet so
consumers don't get {checked, indeterminate}; change the children type to a
Snippet that accepts props (e.g. Snippet<{ checked: boolean; indeterminate:
boolean }>) instead of plain Snippet and, when rendering, call childrenProp with
those values (invoke childrenProp?.({ checked, indeterminate })) in
dropdown-menu-checkbox-item.svelte; update the prop typing around
WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> and the
children/childrenProp usage to match how dropdown-menu-radio-item.svelte
forwards { checked }.

In `@ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte`:
- Around line 19-20: Collapse the overlapping inset padding utilities into a
single conditional class: in the DropdownMenuLabel Svelte component where you
set data-inset={inset} and build the class via cn(..., className), remove one of
the redundant entries and keep a single selector that matches when the attribute
exists (e.g., replace both "data-inset:pl-7 data-[inset]:pl-8" with a single
"data-[inset]:pl-8" or "data-inset:pl-7"), so the effective padding is defined
once while still relying on the data-inset attribute controlled by the inset
prop.

---

Outside diff comments:
In `@ui/src/app.css`:
- Around line 53-127: The global base rules are unintentionally overriding the
existing app (the universal selector * with `@apply` border-border/outline-ring
and the body { `@apply` bg-background text-foreground })—scope these shadcn
primitives behind a dedicated root so Phase 0 remains behavior-neutral: move the
global resets inside a new container selector (e.g., .shadcn-root or
`#new-ui-root`) instead of applying them at `@layer` base globally, and relocate the
background/foreground `@apply` from body into that same scoped root while leaving
the existing html, body color variables and layout rules untouched; update
selectors in this file (the `@layer` base block, the * rule, and the body rule) to
use that dedicated root so the old player style is preserved until the new UI
mounts.

---

Nitpick comments:
In `@ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte`:
- Around line 19-20: The bound attribute data-inset={inset} will be omitted when
inset is false (so data-[inset]:ps-8 won't match), so leave the current
implementation as-is or, if you want clearer intent, change the class
composition in the cn(...) call to conditionally add the padding class when
inset is true (e.g., use inset ? 'ps-8' : ''), referencing the inset prop, cn
helper, className and the data-[inset]:ps-8 usage in
dropdown-menu-group-heading.svelte.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9358c4d2-daff-4c9a-96f6-e16f0123de65

📥 Commits

Reviewing files that changed from the base of the PR and between 1c445cc and 76aa703.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • .gitignore
  • ui/components.json
  • ui/package.json
  • ui/src/app.css
  • ui/src/lib/components/ui/button/button.svelte
  • ui/src/lib/components/ui/button/index.ts
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte
  • ui/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
  • ui/src/lib/components/ui/dropdown-menu/index.ts
  • ui/src/lib/utils.ts
  • ui/tsconfig.app.json
  • ui/tsconfig.json
  • ui/vite.config.ts

Comment thread ui/src/lib/components/ui/button/button.svelte
Comment thread ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte Outdated
@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.80236% with 160 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...dev/twango/jetplay/transcode/MediaInfoExtractor.kt 57.25% 28 Missing and 28 partials ⚠️
...ain/kotlin/dev/twango/jetplay/media/MediaServer.kt 59.75% 18 Missing and 15 partials ⚠️
...otlin/dev/twango/jetplay/editor/MediaFileEditor.kt 0.00% 28 Missing ⚠️
.../kotlin/dev/twango/jetplay/browser/PlayerBridge.kt 57.14% 13 Missing and 8 partials ⚠️
.../dev/twango/jetplay/transcode/WaveformExtractor.kt 72.22% 4 Missing and 6 partials ⚠️
...n/dev/twango/jetplay/transcode/TranscodeSession.kt 0.00% 8 Missing ⚠️
...in/dev/twango/jetplay/transcode/MediaTranscoder.kt 20.00% 4 Missing ⚠️
Files with missing lines Coverage Δ
...n/dev/twango/jetplay/media/LocalFileMediaSource.kt 0.00% <ø> (ø)
.../dev/twango/jetplay/media/RemoteFileMediaSource.kt 0.00% <ø> (ø)
...in/dev/twango/jetplay/transcode/MediaTranscoder.kt 26.50% <20.00%> (-0.42%) ⬇️
...n/dev/twango/jetplay/transcode/TranscodeSession.kt 0.00% <0.00%> (ø)
.../dev/twango/jetplay/transcode/WaveformExtractor.kt 72.22% <72.22%> (ø)
.../kotlin/dev/twango/jetplay/browser/PlayerBridge.kt 50.00% <57.14%> (+22.00%) ⬆️
...otlin/dev/twango/jetplay/editor/MediaFileEditor.kt 0.00% <0.00%> (ø)
...ain/kotlin/dev/twango/jetplay/media/MediaServer.kt 59.75% <59.75%> (ø)
...dev/twango/jetplay/transcode/MediaInfoExtractor.kt 57.25% <57.25%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@twangodev twangodev changed the title feat(ui): scaffold shadcn-svelte foundation for player refresh (Phase 0) feat(ui): use sv11-ui components in the media player Jun 6, 2026
Replace the hand-rolled audio player with sv11 components: pull the
audio-player + waveform registry items and adapt speaker-01's scratchable
waveform into a single-file player driven by the IDE bridge.

- AudioPlayerBody: sv11 AudioPlayer.Root/Button/Time/Progress/Duration plus a
  scrolling waveform via speaker-01's use-scratchable-waveform hook (drag to
  scrub, momentum, keyboard seek). The one opened file is fed as the track.
- Hook adapted: playhead pinned at the LEFT edge so the waveform fills from the
  start; short clips stretch to fit, longer ones scroll.
- Waveform decodes at runtime (precomputeWaveform), length-capped. It only
  mounts once bars exist and slides in, so the resting/loading layout stays
  compact (no empty reserved box, title not floating above dead space).
- Volume kept via jetplay's VolumeControl (sv11 ships no standalone one);
  skip +/-10s and keyboard retained.
- Dropped speaker-01's orbs (three.js weight) and playlist (single-file).
- Fix the inert `dark:` variant in app.css so sv11 components theme under
  prefers-color-scheme (jetplay has no .dark class).

App.svelte unchanged. ./gradlew test green (48); npm run build single-file
295 kB / 137 kB gzip; 27 Playwright tests green.
@socket-security

socket-security Bot commented Jun 6, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @internationalized/date is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ui/package-lock.jsonnpm/@internationalized/date@3.12.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@internationalized/date@3.12.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm node-fetch-native is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ui/package-lock.jsonnpm/shadcn-svelte@1.3.0npm/node-fetch-native@1.6.7

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/node-fetch-native@1.6.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ui/src/app.css (2)

72-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

font-sans is likely not applied to body text.

Line 72 sets an explicit body font-family, so Line 129 (html { @apply font-sans; }) won’t affect most text nodes. If Geist is intended globally, apply font-sans on body (or remove the explicit body font-family).

Also applies to: 128-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/app.css` around lines 72 - 73, The body has an explicit font-family
declaration which overrides the Tailwind/Geist utility, so update the CSS so the
global Geist font utility is actually applied: either remove the explicit body
font-family line (font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
system-ui, sans-serif;) or move/apply the utility by changing html { `@apply`
font-sans; } to body { `@apply` font-sans; } (or add body { `@apply` font-sans; }
alongside/instead of the explicit rule) so the font-sans utility takes effect
for body text.

83-124: ⚠️ Potential issue | 🟠 Major

Unlayered :root dark defaults override the @layer base light-mode CSS variables.
ui/src/app.css defines the light :root variables inside @layer base under @media (prefers-color-scheme: light) (lines 57-123), but the default/dark :root block is unlayered (lines 140-175). When prefers-color-scheme: light is active, both sets apply and normal unlayered declarations take precedence over layered ones, so the dark variables can win.

  • Move the dark/default :root into @layer base (or move the light :root out of @layer base) so both palettes are in the same cascade layer and the light media query can override correctly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/app.css` around lines 83 - 124, The dark/default :root variables
currently outside `@layer` base conflict with the light-mode :root inside `@layer`
base because unlayered rules win; fix by placing both palettes in the same
cascade layer — either move the unlayered default/dark :root block into the
existing `@layer` base block, or move the `@media` (prefers-color-scheme: light)
:root block out of `@layer` base so both :root declarations share the same layer;
update the :root blocks around the `@layer` base and `@media` (prefers-color-scheme:
light) declarations accordingly (look for :root, `@layer` base, and `@media`
(prefers-color-scheme: light)).
🧹 Nitpick comments (2)
ui/src/lib/components/ui/waveform/waveform-scrubber.svelte (1)

80-121: 🏗️ Heavy lift

Slider lacks keyboard support for accessibility.

The element has role="slider" and tabindex={0} but no keyboard event handlers. Screen reader users and keyboard-only users cannot operate the scrubber. ARIA sliders require arrow key support per WCAG.

Consider adding onkeydown to handle arrow keys:

onkeydown={(e) => {
  if (!onSeek) return;
  const step = duration * 0.05; // 5% increments
  if (e.key === 'ArrowRight') onSeek(Math.min(duration, currentTime + step));
  else if (e.key === 'ArrowLeft') onSeek(Math.max(0, currentTime - step));
}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/components/ui/waveform/waveform-scrubber.svelte` around lines 80 -
121, Add keyboard support to the slider container (the div with role="slider"
that binds containerEl and uses handlePointerDown) by wiring an onkeydown
handler that calls the existing onSeek callback (or a new wrapper) to adjust
playback time using arrow keys and other slider keys; use currentTime and
duration to compute new positions (e.g., small step = duration*0.05 for
ArrowLeft/ArrowRight, larger step or jump to 0/duration for Home/End) and clamp
values to [0, duration], then update localProgress/aria-valuenow accordingly so
screen-reader and keyboard-only users can operate the scrubber.
ui/src/lib/components/ui/waveform/waveform-live-microphone.svelte (1)

83-93: 💤 Low value

decodeAudioData may throw on closed AudioContext.

processAudioBlob checks audioContextRef for nullity but not its state. If the context was closed (e.g., during cleanup race), decodeAudioData will throw. The error is caught, but decoding silently fails.

🛡️ Proposed improvement
 	async function processAudioBlob(blob: Blob) {
 		try {
 			const arrayBuffer = await blob.arrayBuffer();
-			if (audioContextRef) {
+			if (audioContextRef && audioContextRef.state !== "closed") {
 				const audioBuffer = await audioContextRef.decodeAudioData(arrayBuffer);
 				audioBufferRef = audioBuffer;
 			}
 		} catch (error) {
 			console.error("Error processing audio:", error);
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/components/ui/waveform/waveform-live-microphone.svelte` around
lines 83 - 93, processAudioBlob currently only checks audioContextRef for
non-null but not whether it's usable; guard against a closed AudioContext by
checking audioContextRef.state !== 'closed' (or that state is
'running'/'suspended') before calling decodeAudioData, and return early (or log
a clear warning) if the context is closed to avoid silent decoding failure;
ensure audioBufferRef assignment happens only after successful decodeAudioData
and keep the existing try/catch for other errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/lib/AudioPlayerBody.svelte`:
- Around line 148-159: The handler handleKeydown currently intercepts Space for
all bubbled events and toggles player.play()/pause(), which hijacks inner
controls; change the Space branch to only run when the chrome container is
focused (same pattern as Arrow keys) by checking e.target === containerEl before
calling e.preventDefault() and toggling using player.play()/player.pause(); keep
existing ArrowLeft/ArrowRight logic and calls to skipBack()/skipForward()
unchanged.
- Around line 60-65: The id used when calling player.setActiveItem({ id:
fileName, src, ... }) is not globally unique and can collide for different files
with the same basename; change the id to a stable unique identifier (for example
use the full src, a concatenation like `${fileName}:${src}`, or a deterministic
hash of src) so setActiveItem sees different items and swaps sources correctly;
update the call in AudioPlayerBody.svelte (the player.setActiveItem invocation)
to use that unique id instead of fileName.
- Around line 68-82: The component-level flag waveformStarted never resets when
src changes and async decode promises can race and overwrite newer results;
change logic so waveformStarted (or equivalent state) is keyed to src (e.g.,
track by src value) and reset whenever src changes, clear precomputedWaveform at
start of a new src, and guard the async precomputeWaveform result by capturing
the current src (or using a local token/AbortController) inside the $effect
callback so you only assign to precomputedWaveform if the src still matches and
the operation wasn't cancelled; keep checks for dur and WAVEFORM_MAX_SECONDS and
use BARS_PER_SECOND when calling precomputeWaveform.

In `@ui/src/lib/components/ui/audio-player/audio-player-progress.svelte`:
- Around line 57-61: The pointerup handler currently sets userInteracting =
false and resumes playback (using wasPlaying and player.play()) but
pointercancel isn’t handled, leaving state incorrect if interaction is canceled;
add an onpointercancel handler that performs the exact same steps as the
onpointerup block (set userInteracting = false, resume playback when wasPlaying
is true via player.play(), and call externalPointerUp with the event) so
userInteracting/wasPlaying resume logic is consistent; update any shared logic
by extracting the handler into a named function (e.g., handlePointerEnd) and
reuse it for both onpointerup and onpointercancel to avoid duplication.

In `@ui/src/lib/components/ui/waveform/waveform-live-microphone.svelte`:
- Around line 162-174: The ResizeObserver callback currently calls
ctx.scale(dpr, dpr) without resetting the canvas transform, causing scale to
accumulate; in the ResizeObserver callback (where container, canvas, dpr and ctx
are referenced) reset the context transform before applying the devicePixelRatio
scale (use ctx.resetTransform() or ctx.setTransform(1,0,0,1,0,0)), then call
ctx.scale(dpr, dpr) so the scale is applied cleanly after resizing.
- Around line 109-112: The code incorrectly treats negative playbackRate as
unsupported; instead add runtime detection for negative-rate support and fall
back if needed: create a helper (e.g., supportsNegativePlaybackRate) that
attempts to set source.playbackRate.value to a negative value (or performs a
small test using an ephemeral AudioBufferSourceNode) and returns true/false,
then keep using rate = direction > 0 ? ... : ... for source.playbackRate.value
when supported, otherwise implement a fallback path that reverses the
AudioBuffer data (or swaps to a reversed buffer) and sets a positive
playbackRate; reference the variables/functions source.playbackRate.value, rate,
direction and use the detection helper to choose between direct negative-rate
assignment and the reversed-buffer fallback.

In `@ui/src/lib/components/ui/waveform/waveform-microphone.svelte`:
- Around line 84-100: The fade-to-idle animation (fadeToIdle) currently uses
requestAnimationFrame recursively without tracking its id, causing updates to
continue after reruns/unmounts; modify the logic to store the returned animation
id (e.g., fadeAnimId) when calling requestAnimationFrame, cancel any existing
animation with cancelAnimationFrame before starting a new fade, and ensure you
cancel the animation in the component cleanup/unmount (onDestroy) or whenever
_active/_processing changes so stale callbacks don't update data on an unmounted
or stale component.

In `@ui/src/lib/components/ui/waveform/waveform-recording.svelte`:
- Around line 59-71: The ResizeObserver handler calls ctx.scale(dpr, dpr)
without resetting prior transforms, which accumulates scale on each resize;
update the handler in the ResizeObserver callback (where you access container,
canvas, and ctx) to reset the canvas transform before applying the
devicePixelRatio scaling — e.g., use ctx.resetTransform() if available or
ctx.setTransform(1,0,0,1,0,0) to clear transforms, then apply the new scale with
ctx.scale(dpr, dpr); keep the null-check for ctx.

In `@ui/src/lib/components/ui/waveform/waveform-scrolling.svelte`:
- Around line 49-61: The ResizeObserver callback is repeatedly calling
ctx.scale(dpr, dpr) without resetting transforms, causing cumulative scaling;
update the callback in waveform-scrolling.svelte (the ResizeObserver that reads
container, canvas, dpr and gets ctx) to reset the transform before scaling—e.g.,
call ctx.setTransform(1,0,0,1,0,0) or ctx.resetTransform() (if available) and
then apply the devicePixelRatio scale (or call
ctx.setTransform(dpr,0,0,dpr,0,0)) so the canvas is correctly scaled on each
resize without accumulation.

In `@ui/src/lib/components/ui/waveform/waveform.svelte`:
- Around line 172-185: The click handler handleClick can divide by zero when
rect.width < (barWidth + barGap); compute barCount = Math.floor(rect.width /
(barWidth + barGap)) and guard it before using it (either return early if
barCount === 0 or use barCount = Math.max(1, barCount)), then compute
barIndex/dataIndex using that safe barCount and ensure dataIndex is a finite
integer before calling onBarClick(dataIndex, data[dataIndex]); reference
symbols: handleClick, canvasEl, barWidth, barGap, rect.width, barCount, data,
onBarClick.

In `@ui/src/lib/use-scratchable-waveform.svelte.ts`:
- Around line 66-90: The BiquadFilterNode created in playScratch is never
disconnected and accumulates; update playScratch/stopScratch to track the filter
node (e.g., store it in a module-level scratchFilter alongside scratchSource),
and when stopping (in stopScratch and on error) call disconnect() on both
scratchSource and scratchFilter (safely in try/catch), then null them so nodes
are released; ensure playScratch assigns scratchFilter = filter and that
stopScratch disconnects and clears scratchSource and scratchFilter.
- Around line 155-190: Momentum RAF loops started in the momentum block (via
requestAnimationFrame(step)) are not cancelled when a new scrub/reset begins,
causing racing seeks; add a cancellation handle: store the requestAnimationFrame
return value in a variable (e.g., momentumRafId) when starting the momentum
loop, cancel any existing loop with cancelAnimationFrame(momentumRafId) and set
isMomentumActive = false before starting a new momentum or on reset(), and
ensure reset() and any start-scratch/scrub entry points call this cancellation
path so stopScratch()/setOffsetAndSeek()/opts.player.play() aren't interleaved
by prior RAF callbacks.
- Around line 20-25: getScratchBuffer currently writes null results into
scratchBufferCache which permanently disables that URL after a transient
failure; change the logic so you do not cache a permanent null on failure —
either (a) only set scratchBufferCache.set(url, promise) and ensure the promise
rejects on decode failure and remove the entry on rejection, or (b) await
fetchScratchBuffer(url, ctx) and only set scratchBufferCache.set(url, buffer)
when the resolved AudioBuffer is non-null. Update getScratchBuffer and its
interactions with fetchScratchBuffer and scratchBufferCache accordingly so
transient decode errors don't leave a null in the cache.

---

Outside diff comments:
In `@ui/src/app.css`:
- Around line 72-73: The body has an explicit font-family declaration which
overrides the Tailwind/Geist utility, so update the CSS so the global Geist font
utility is actually applied: either remove the explicit body font-family line
(font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
sans-serif;) or move/apply the utility by changing html { `@apply` font-sans; } to
body { `@apply` font-sans; } (or add body { `@apply` font-sans; } alongside/instead
of the explicit rule) so the font-sans utility takes effect for body text.
- Around line 83-124: The dark/default :root variables currently outside `@layer`
base conflict with the light-mode :root inside `@layer` base because unlayered
rules win; fix by placing both palettes in the same cascade layer — either move
the unlayered default/dark :root block into the existing `@layer` base block, or
move the `@media` (prefers-color-scheme: light) :root block out of `@layer` base so
both :root declarations share the same layer; update the :root blocks around the
`@layer` base and `@media` (prefers-color-scheme: light) declarations accordingly
(look for :root, `@layer` base, and `@media` (prefers-color-scheme: light)).

---

Nitpick comments:
In `@ui/src/lib/components/ui/waveform/waveform-live-microphone.svelte`:
- Around line 83-93: processAudioBlob currently only checks audioContextRef for
non-null but not whether it's usable; guard against a closed AudioContext by
checking audioContextRef.state !== 'closed' (or that state is
'running'/'suspended') before calling decodeAudioData, and return early (or log
a clear warning) if the context is closed to avoid silent decoding failure;
ensure audioBufferRef assignment happens only after successful decodeAudioData
and keep the existing try/catch for other errors.

In `@ui/src/lib/components/ui/waveform/waveform-scrubber.svelte`:
- Around line 80-121: Add keyboard support to the slider container (the div with
role="slider" that binds containerEl and uses handlePointerDown) by wiring an
onkeydown handler that calls the existing onSeek callback (or a new wrapper) to
adjust playback time using arrow keys and other slider keys; use currentTime and
duration to compute new positions (e.g., small step = duration*0.05 for
ArrowLeft/ArrowRight, larger step or jump to 0/duration for Home/End) and clamp
values to [0, duration], then update localProgress/aria-valuenow accordingly so
screen-reader and keyboard-only users can operate the scrubber.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac4e9751-fc22-46c0-9e36-61cf30aa28d4

📥 Commits

Reviewing files that changed from the base of the PR and between 76aa703 and 0304acc.

📒 Files selected for processing (27)
  • ui/src/app.css
  • ui/src/lib/AudioPlayer.svelte
  • ui/src/lib/AudioPlayerBody.svelte
  • ui/src/lib/components/ui/audio-player/audio-graph.svelte.ts
  • ui/src/lib/components/ui/audio-player/audio-player-button.svelte
  • ui/src/lib/components/ui/audio-player/audio-player-duration.svelte
  • ui/src/lib/components/ui/audio-player/audio-player-progress.svelte
  • ui/src/lib/components/ui/audio-player/audio-player-speed-button-group.svelte
  • ui/src/lib/components/ui/audio-player/audio-player-speed.svelte
  • ui/src/lib/components/ui/audio-player/audio-player-time.svelte
  • ui/src/lib/components/ui/audio-player/audio-player.svelte
  • ui/src/lib/components/ui/audio-player/context.svelte.ts
  • ui/src/lib/components/ui/audio-player/example-tracks.ts
  • ui/src/lib/components/ui/audio-player/index.ts
  • ui/src/lib/components/ui/audio-player/utils.ts
  • ui/src/lib/components/ui/audio-player/waveform-sampler.ts
  • ui/src/lib/components/ui/waveform/index.ts
  • ui/src/lib/components/ui/waveform/utils.ts
  • ui/src/lib/components/ui/waveform/waveform-live-microphone.svelte
  • ui/src/lib/components/ui/waveform/waveform-microphone.svelte
  • ui/src/lib/components/ui/waveform/waveform-recording.svelte
  • ui/src/lib/components/ui/waveform/waveform-scrolling.svelte
  • ui/src/lib/components/ui/waveform/waveform-scrubber.svelte
  • ui/src/lib/components/ui/waveform/waveform-static.svelte
  • ui/src/lib/components/ui/waveform/waveform.svelte
  • ui/src/lib/use-scratchable-waveform.svelte.ts
  • ui/tests/audio-player.spec.ts
✅ Files skipped from review due to trivial changes (5)
  • ui/src/lib/components/ui/audio-player/utils.ts
  • ui/src/lib/components/ui/audio-player/audio-player-duration.svelte
  • ui/src/lib/components/ui/waveform/waveform-static.svelte
  • ui/src/lib/components/ui/audio-player/example-tracks.ts
  • ui/src/lib/components/ui/waveform/index.ts

Comment thread ui/src/lib/AudioPlayerBody.svelte
Comment thread ui/src/lib/AudioPlayerBody.svelte Outdated
Comment thread ui/src/lib/AudioPlayerBody.svelte
Comment thread ui/src/lib/components/ui/audio-player/audio-player-progress.svelte
Comment thread ui/src/lib/components/ui/waveform/waveform-live-microphone.svelte
Comment thread ui/src/lib/components/ui/waveform/waveform-scrolling.svelte
Comment thread ui/src/lib/components/ui/waveform/waveform.svelte
Comment thread ui/src/lib/use-scratchable-waveform.svelte.ts
Comment thread ui/src/lib/use-scratchable-waveform.svelte.ts
Comment thread ui/src/lib/use-scratchable-waveform.svelte.ts
The sv11 <audio> shipped crossorigin="anonymous", which forces a CORS check.
The IDE loads the player via loadHTML (null origin) and serves media as a
file:// URL, so the element never loaded — nothing played, and the waveform
(gated on duration) never appeared. Remove it to match the previous working
player; sv11 only needs it for its orb visualizers, which jetplay doesn't use.

Add a regression test asserting the audio element has no crossorigin, since
Playwright can't reproduce JCEF's file:// / null-origin context.
Copilot AI review requested due to automatic review settings June 6, 2026 09:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The JCEF browser can play file:// media but can't fetch()/decode its bytes, so
the in-browser waveform never appeared in the IDE. Compute it natively instead:

- WaveformExtractor: decode audio with the bundled FFmpeg (JavaCV) into
  normalized [0,1] amplitude bars at 8/sec (matching the UI's sampleWaveform) —
  streamed, mono/S16, 30-min cap, graceful empty on failure.
- MediaFileEditor: for local non-video audio with ffmpeg available, extract on a
  pooled thread and push via PlayerBridge.sendWaveform -> window.jetplayWaveform.
- UI: App feeds the pushed bars to the player, which now prefers them over the
  in-browser precompute (kept only as the HTTP/test fallback).

Tests: WaveformExtractorTest (generated sine WAV) + Playwright push test.
gradle test green (49); 29 Playwright; single-file build.
Comment thread src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt Fixed
From the adversarial review of the ffmpeg waveform feature:

- Bridge push race: extraction can finish before the page defines
  window.jetplayWaveform (short files), silently dropping the bars. Stash them
  on window.__jetplayWaveform and read them on mount. This is why the waveform
  didn't show for the 5s sintel files.
- Unbounded decode: the 30-min cap relied on container duration, which is
  0/AV_NOPTS_VALUE for some inputs. Enforce a hard bar ceiling in the sample
  loop and honor thread interruption.
- Cancel the pooled decode on editor dispose (and interrupt the loop).
- Skip raw telephony codecs (they need demuxer hints the extractor lacks).
- Drop the dead audioChannels<=0 guard; align the browser fallback cap (was
  20min) with the 30min native cap.
- Tests: pushed bars take precedence over the fallback; a pre-load buffered
  push is picked up on mount.

gradle test green; npm check clean; 31 Playwright; single-file build.
Copilot AI review requested due to automatic review settings June 6, 2026 19:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/main/kotlin/dev/twango/jetplay/browser/PlayerBridge.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt Fixed
twangodev added 2 commits June 6, 2026 12:48
From the UI/UX audit vs sv11/speaker-01:
- Right-pinned waveform playhead (sv11): scrolls in from the right, empty at
  rest then fills as it plays (reverts the earlier left-pin).
- Muted gray bars (#a1a1aa / #71717a) instead of full --foreground.
- Card panel: bordered, rounded, shadow; compact waveform (h-12 / 32px).
- sv11 control styling: outline pill skip buttons, outline play button with a
  playing-state tint, always-visible volume (4-state icon + slider + %).
- Add a playback-speed control (AudioPlayer.Speed).
- Token/typography cleanup (text-foreground / text-muted-foreground).

31 Playwright green; npm check clean; single-file build.
The browser can't fetch()/decode file:// bytes, so the scrubber's DJ-scratch
sound never worked in the IDE (it builds a scratch buffer by fetch()+decode of
the audio). Serve local media through a tiny loopback HTTP server so the page
can fetch it; the scratch now works because `src` is a fetchable http URL. This
also unlocks in-browser decode and range-seeking for large files.

- MediaServer: HttpServer on 127.0.0.1, serves files registered under an
  unguessable token, with CORS + Range (206); daemon, lazily started.
- MediaFileEditor owns serving: serve(file) tracks the URL and releases it on
  dispose; TranscodeSession hands its output back via callback.
  resolvePlayableUrl removed (serving centralized).

Security/correctness hardening (adversarial review):
- Reject non-loopback Host headers (DNS-rebinding defense).
- Per-editor token release — closed editors stop being fetchable.
- Private-Network header only on the OPTIONS preflight.
- Correct suffix ranges (bytes=-N), 416 past EOF, multi-range/zero-length
  fall back to a full 200.

gradle test green (serve/range/suffix/416/host/404/release); 31 Playwright;
single-file build.
Copilot AI review requested due to automatic review settings June 6, 2026 20:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt Fixed
Comment thread src/test/kotlin/dev/twango/jetplay/media/MediaServerTest.kt Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt (1)

34-34: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject non-positive barsPerSecond at the API boundary.

sampleRate / barsPerSecond will throw for 0 and behave nonsensically for negative values. Guard this in extract() so callers get a deterministic argument error instead of failing deep in sampling.

Suggested fix
 fun extract(file: File, barsPerSecond: Int = DEFAULT_BARS_PER_SECOND): List<Double> {
+    require(barsPerSecond > 0) { "barsPerSecond must be > 0" }
     val grabber = FFmpegFrameGrabber(file).apply {
         audioChannels = 1 // request a mono downmix
         sampleMode = FrameGrabber.SampleMode.SHORT
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt` at line
34, The extract(file: File, barsPerSecond: Int = DEFAULT_BARS_PER_SECOND) API
should validate barsPerSecond at the boundary: add a guard at the start of
WaveformExtractor.extract that throws an IllegalArgumentException (with a clear
message) when barsPerSecond <= 0 so callers receive a deterministic argument
error instead of hitting division-by-zero or nonsensical behavior later during
sampleRate / barsPerSecond.
src/main/kotlin/dev/twango/jetplay/editor/MediaFileEditor.kt (1)

84-88: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check cancellation after waveform extraction.

waveformFuture.cancel(true) only interrupts the worker. WaveformExtractor.extract() can still return a partial non-empty list on interruption, and this block sends it without checking bridge.disposed or the thread's interrupted state again. That makes disposal race with sendWaveform(...) on a dead browser.

Suggested fix
         waveformFuture = ApplicationManager.getApplication().executeOnPooledThread {
             if (bridge.disposed) return@executeOnPooledThread
             val bars = WaveformExtractor.extract(localFile)
-            if (bars.isNotEmpty()) bridge.sendWaveform(bars)
+            if (Thread.currentThread().isInterrupted || bridge.disposed || bars.isEmpty()) {
+                return@executeOnPooledThread
+            }
+            bridge.sendWaveform(bars)
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/dev/twango/jetplay/editor/MediaFileEditor.kt` around lines 84
- 88, The waveform extraction task can return a partial list after cancellation
and still call bridge.sendWaveform(...), so after
WaveformExtractor.extract(localFile) re-check cancellation/disposal and only
send if still valid: verify Thread.currentThread().isInterrupted() is false (or
that waveformFuture is not cancelled) and bridge.disposed is false before
calling bridge.sendWaveform(bars); keep the existing bars.isNotEmpty() check and
perform these additional checks in the same Runnable that runs under
ApplicationManager.getApplication().executeOnPooledThread so you avoid sending a
waveform to a disposed bridge.
🧹 Nitpick comments (1)
ui/tests/audio-player.spec.ts (1)

45-60: 💤 Low value

Consider using the typed Window interface instead of any casts.

The test correctly verifies buffered waveform pickup, but the (window as any) casts on lines 49 and 56 bypass TypeScript's type safety. The global window interface is already declared in ui/src/global.d.ts with jetplay and __jetplayWaveform properties.

♻️ Optional type-safe refactor
  await page.addInitScript(() => {
-   ;(window as any).jetplay = {
+   window.jetplay = {
      state: 'ready',
      fileName: 'x.mp3',
      fileExtension: 'mp3',
      mediaUrl: '/assets/does-not-exist.mp3',
      isVideo: false,
    }
-   ;(window as any).__jetplayWaveform = [0.4, 0.5, 0.6, 0.7]
+   window.__jetplayWaveform = [0.4, 0.5, 0.6, 0.7]
  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/tests/audio-player.spec.ts` around lines 45 - 60, Replace the unsafe
(window as any) casts in the test "a waveform buffered before load is picked up"
with a typed Window cast that uses the declared global interface and the two
properties; e.g., create a typed alias like const w = window as unknown as
Window & { jetplay?: { state: string; fileName: string; fileExtension: string;
mediaUrl: string; isVideo: boolean }; __jetplayWaveform?: number[] } and then
assign w.jetplay = { ... } and w.__jetplayWaveform = [0.4, 0.5, 0.6, 0.7];
reference the existing jetplay and __jetplayWaveform symbols so the test uses
the proper types instead of any.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test/kotlin/dev/twango/jetplay/media/MediaServerTest.kt`:
- Around line 89-91: The test currently calls open(url).also { it.disconnect()
}.responseCode which disconnects the HttpURLConnection before the request is
forced; change to store the connection in a variable (e.g., val conn =
open(url)), read conn.responseCode into the assertion, then call
conn.disconnect(); do the same pattern for the second check around
MediaServer.release(url) so responseCode is read before calling disconnect() and
avoid lazy-connect flakes.

---

Outside diff comments:
In `@src/main/kotlin/dev/twango/jetplay/editor/MediaFileEditor.kt`:
- Around line 84-88: The waveform extraction task can return a partial list
after cancellation and still call bridge.sendWaveform(...), so after
WaveformExtractor.extract(localFile) re-check cancellation/disposal and only
send if still valid: verify Thread.currentThread().isInterrupted() is false (or
that waveformFuture is not cancelled) and bridge.disposed is false before
calling bridge.sendWaveform(bars); keep the existing bars.isNotEmpty() check and
perform these additional checks in the same Runnable that runs under
ApplicationManager.getApplication().executeOnPooledThread so you avoid sending a
waveform to a disposed bridge.

In `@src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt`:
- Line 34: The extract(file: File, barsPerSecond: Int = DEFAULT_BARS_PER_SECOND)
API should validate barsPerSecond at the boundary: add a guard at the start of
WaveformExtractor.extract that throws an IllegalArgumentException (with a clear
message) when barsPerSecond <= 0 so callers receive a deterministic argument
error instead of hitting division-by-zero or nonsensical behavior later during
sampleRate / barsPerSecond.

---

Nitpick comments:
In `@ui/tests/audio-player.spec.ts`:
- Around line 45-60: Replace the unsafe (window as any) casts in the test "a
waveform buffered before load is picked up" with a typed Window cast that uses
the declared global interface and the two properties; e.g., create a typed alias
like const w = window as unknown as Window & { jetplay?: { state: string;
fileName: string; fileExtension: string; mediaUrl: string; isVideo: boolean };
__jetplayWaveform?: number[] } and then assign w.jetplay = { ... } and
w.__jetplayWaveform = [0.4, 0.5, 0.6, 0.7]; reference the existing jetplay and
__jetplayWaveform symbols so the test uses the proper types instead of any.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49a9afc1-2c0d-444d-b269-bded1c2fe1c0

📥 Commits

Reviewing files that changed from the base of the PR and between e315075 and f389e77.

📒 Files selected for processing (16)
  • src/main/kotlin/dev/twango/jetplay/browser/PlayerBridge.kt
  • src/main/kotlin/dev/twango/jetplay/editor/MediaFileEditor.kt
  • src/main/kotlin/dev/twango/jetplay/media/LocalFileMediaSource.kt
  • src/main/kotlin/dev/twango/jetplay/media/MediaServer.kt
  • src/main/kotlin/dev/twango/jetplay/media/MediaSource.kt
  • src/main/kotlin/dev/twango/jetplay/media/RemoteFileMediaSource.kt
  • src/main/kotlin/dev/twango/jetplay/transcode/TranscodeSession.kt
  • src/main/kotlin/dev/twango/jetplay/transcode/WaveformExtractor.kt
  • src/test/kotlin/dev/twango/jetplay/media/MediaServerTest.kt
  • ui/src/App.svelte
  • ui/src/global.d.ts
  • ui/src/lib/AudioPlayerBody.svelte
  • ui/src/lib/components/ui/audio-player/audio-player.svelte
  • ui/src/lib/use-scratchable-waveform.svelte.ts
  • ui/tests/audio-player.spec.ts
  • ui/tests/states.spec.ts
💤 Files with no reviewable changes (3)
  • src/main/kotlin/dev/twango/jetplay/media/MediaSource.kt
  • src/main/kotlin/dev/twango/jetplay/media/RemoteFileMediaSource.kt
  • src/main/kotlin/dev/twango/jetplay/media/LocalFileMediaSource.kt
🚧 Files skipped from review as they are similar to previous changes (6)
  • ui/src/global.d.ts
  • ui/src/App.svelte
  • src/main/kotlin/dev/twango/jetplay/browser/PlayerBridge.kt
  • ui/src/lib/components/ui/audio-player/audio-player.svelte
  • ui/src/lib/AudioPlayerBody.svelte
  • ui/src/lib/use-scratchable-waveform.svelte.ts

Comment thread src/main/kotlin/dev/twango/jetplay/transcode/TranscodeSession.kt Outdated
Comment thread src/test/kotlin/dev/twango/jetplay/media/MediaServerTest.kt Outdated
twangodev added 2 commits June 6, 2026 14:22
- Volume control now uses the same bits-ui Slider as AudioPlayerProgress
  (matching track/range/thumb), instead of a hand-rolled click-only div — so it
  drags, shows a thumb on hover, and supports keyboard like the scrub bar.
- Swap the speed control's gear (Settings) icon for a speedometer (Gauge).
The audio player's metadata header now expands into a technical readout.
The IDE probes the opened file's container/codec/stream details with the
bundled FFmpeg (header only, off the EDT) and pushes them to the player the
same way the waveform is — stashed on window + handler call, so an early
push isn't dropped.

Collapsed shows the filename plus a glanceable summary line
(WAV · 48 kHz · 16-bit · stereo · 1.2 MB); clicking expands a full
label/value grid (codec, container, sample rate, channels, bit depth,
bitrate, duration, size). With no info pushed the header looks exactly as
before — remote / raw-codec / ffmpeg-unavailable paths degrade cleanly.

- MediaInfoExtractor: FFmpeg header probe → MediaInfo. RAW sample mode to
  read the true source sample format; PCM bit depth from the codec name;
  lossless via sample format; bitrate falls back to size/duration.
- PlayerBridge.sendMediaInfo / MediaFileEditor.maybeSendMediaInfo mirror the
  waveform path (pooled thread, same guards, cancelled on dispose).
- Gate the player's Space/Arrow shortcuts to the container so a focused inner
  control (the new toggle, mute, slider) receives its native key instead of
  having Space swallowed into play/pause.

Tests: MediaInfoExtractorTest (probe a generated WAV; null for non-audio);
Playwright for summary/expand, buffered push, and Space-on-toggle.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/main/kotlin/dev/twango/jetplay/transcode/MediaInfoExtractor.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/transcode/MediaInfoExtractor.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/transcode/MediaInfoExtractor.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/transcode/MediaInfoExtractor.kt Fixed
Comment thread src/main/kotlin/dev/twango/jetplay/transcode/MediaInfoExtractor.kt Fixed
twangodev added 2 commits June 6, 2026 18:40
…badge)

The extension badge already shows the format, so leading the summary line with
the container repeated it (e.g. "WAV" badge + "WAV · 48 kHz · …"). Remove the
container token from the summary; the exact demux container still appears in the
expanded grid's Container row.
The codec inspector now also reads the file's embedded metadata. FFmpeg's
getMetadata() gives the container tags (title/artist/album/date/genre/…, plus
rating when present); they render as a separate Tags group in the expanded
panel, above the technical stream grid.

Embedded cover art is pulled from the attached-picture stream (the packet data
is the encoded image — no decode) and shown as a blurred, dimmed ambient
background behind the player card, not a thumbnail.

- MediaInfoExtractor: buildTags() maps FFmpeg's normalized keys to ordered
  labels; extractAlbumArt() reads the first attached_pic stream, sniffs the
  image type, and base64s it into a data URL (capped at 4 MB).
- PlayerBridge: the media-info push is now built as strict JSON (jsonString
  escapes quotes/backslash/controls and U+2028/2029) since tag text is
  arbitrary user input.
- UI: Tags group + blurred album-art layer (fades in); the expanded panel
  groups descriptive tags then technical details; aria-controls points at it.

Tests: buildTags ordering/skip; mediaInfoJson tag embedding + escaping;
Playwright for the tags panel and the album-art background.
Copilot AI review requested due to automatic review settings June 7, 2026 02:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detekt found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

twangodev added 2 commits June 6, 2026 19:40
getAudioCodecName() returns the decoder name — "mp3float" for MP3 — which is
wrong and confusing in a codec inspector. Derive the codec from its id via
avcodec_get_name() instead, giving "mp3", "aac", "flac", "pcm_s16le", …
Caught by probing real MP3 files (the WAV unit test couldn't surface it,
since the PCM decoder name already matches the codec name).

Also gitignore the large local-only test tracks dropped into assets/: that
dir is symlinked into ui/public and bundled into the plugin jar, so the
multi-MB files must stay out of git history and the shipped artifact.
Five royalty-free MP3s in assets/ with real ID3 tags and embedded album cover
art, for exercising the codec inspector — the tags group and the blurred
album-art background — in runIde and the Playwright dev server.

Music by Kevin MacLeod (incompetech.com), CC BY 4.0; see assets/CREDITS.md.
Reverts the gitignore rule that had kept them local-only.
Copilot AI review requested due to automatic review settings June 7, 2026 02:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

twangodev added 2 commits June 6, 2026 19:54
Rewrites the cinema video player with modern overlay controls and extends the
codec inspector to video files.

UI:
- bits-ui Slider for the seek bar and volume (same primitive as the audio
  player), restyled white for over-video.
- Speed (Gauge dropdown), fullscreen, skip ±10s, and frame-step controls.
- Top overlay bar with filename + an expandable inspector panel grouping
  Video (resolution/fps/codec/color/bitrate), Audio, General, and tags.
- Auto-hide stays but is suspended while the inspector is open; Space/arrow
  shortcuts gate to the container so focused controls get their own keys.
- Retire the hand-rolled SeekBar/VolumeControl; share the inspector formatters
  via mediaInfoFormat.ts (audio player refactored onto it).

Data:
- MediaInfoExtractor reads video streams now (width/height/fps, codec via
  avcodec_get_name, pixel format via av_get_pix_fmt_name, bitrate) and returns
  info when either an audio OR video stream exists. ImageMode.RAW so the source
  pixel format is reported, not the decoder's output.
- MediaFileEditor probes video too (drops the isVideo skip); PlayerBridge and
  global.d.ts carry the video fields.

Tests: real assets/sintel.webm probe in MediaInfoExtractorTest; Playwright for
the new controls, the speed dropdown, and the video metadata panel.
Copilot AI review requested due to automatic review settings June 7, 2026 04:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

twangodev added 3 commits June 6, 2026 22:50
The seek bar's value tracks currentTime; bits-ui re-snaps that off-grid value
onto the step on every timeupdate and re-fires onValueChange, which re-seeked
the video every frame. Guard the seek so onValueChange only seeks during a real
pointer scrub, and drive keyboard nudges directly.

- Clear the scrub guard from a window pointerup/pointercancel (bits-ui releases
  on document, not the element), so releasing off the thin track can't leave the
  guard stuck open (which re-armed the stutter on replay) or the video paused.
- Intercept arrow/Home/End/Page on the focused seek thumb in the capture phase
  and seek directly, stopping bits-ui from desyncing the thumb (its onValueChange
  fires before a Root-level handler could open the guard).
- Drop the volume-slider guards: volume is always on the step grid (0..1, 0 on
  mute) so bits-ui never re-snaps it — the guard was unnecessary and only broke
  keyboard volume control.
- Make playwright.config resilient to a busy port 5173 (JETPLAY_TEST_PORT).

Tests: no self-seek during playback, keyboard seek on the focused thumb, and
scrub-release-off-track resumes playback; audio mute preserves the volume level.
… to hang

Video transcoding re-encodes to VP9 with libvpx, which defaults to a very slow
deadline — multiples of real time, so converting a normal HD clip looked like
the "Converting for playback…" state was stuck. We only need a watchable
preview, so encode with deadline=realtime, cpu-used=8, row-mt, and all cores.
Measured: a 5s 854x480 clip dropped 4389ms -> 535ms (~8x); HD now transcodes
faster than real time instead of ~5x slower.
Drop the fullscreen button, toggle, F shortcut, fullscreenchange listener, and
the Maximize/Minimize icons.
Copilot AI review requested due to automatic review settings June 7, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

twangodev added 2 commits June 7, 2026 01:22
The transcode/download paths deliver mediaReady asynchronously, but
htmlLoader.load() and the transcode run concurrently — a fast transcode can
call window.jetplayReady before the Svelte app has mounted and defined the
handler, so the `?.` call is a silent no-op. The page then mounts showing the
initial state:'loading' and sticks on "Converting for playback…" at 0% with no
further transition. (The VP9 realtime speedup made this race much easier to
lose.)

Buffer the latest state push on window (__jetplayReadyUrl / __jetplayError /
__jetplayProgress / __jetplayState) and seed the app from it on mount — the
same pattern already used for the waveform and codec-inspector pushes.
Extract named constants for HTTP status codes, audio/bitrate magic
numbers and image signatures; make bitDepth a single-expression when
(<=3 returns); cut loop jump-statements in the waveform and album-art
readers; simplify MediaServer's range condition; and suppress the
inherent TooManyFunctions on the FileEditor impl. ktlint formatting
rules auto-corrected across the touched files. No behavior change.
Copilot AI review requested due to automatic review settings June 7, 2026 09:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- app.css: apply Geist (--font-sans) to body so it actually takes effect
- button: real hover on the anchor render path; make disabled links
  non-interactive (anchors don't match the disabled: pseudo-class)
- audio scratch: evict failed-decode cache entries, disconnect per-slice
  filter nodes, and cancel in-flight momentum on new scrub/reset
- AudioPlayerBody: key the track id and waveform decode to src (guard
  stale async results) so re-used instances swap source correctly
- progress slider: handle pointercancel like pointerup
- waveform: guard divide-by-zero on very narrow containers
- dropdown: forward {checked,indeterminate} to checkbox-item children;
  drop the dead data-inset utility on the label
- waveform-microphone: store and cancel the fade-to-idle RAF on cleanup
- waveform-recording: reset canvas transform before scaling (defensive)
- TranscodeSession: guard cancel()/onReady with a lock so a late
  callback can't run after the editor is disposed
- MediaServerTest: read responseCode before disconnect (JDK flake)
- utils: unknown instead of any (drop dead eslint-disable directives)
@twangodev twangodev merged commit d1e7af7 into main Jun 7, 2026
28 checks passed
@twangodev twangodev deleted the feat/sv11-ui-refresh branch June 7, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants