feat: add WaveAudioPlayer with waveform visualization and authenticated audio fetch#10158
feat: add WaveAudioPlayer with waveform visualization and authenticated audio fetch#10158viva-jinyi wants to merge 8 commits intomainfrom
Conversation
Amp-Thread-ID: https://ampcode.com/threads/T-019cfad1-e43f-743f-90dc-6fa5c5770753 Co-authored-by: Amp <amp@ampcode.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a WaveAudioPlayer Vue component with compact/expanded views, a composable useWaveAudioPlayer (plus formatTime), Storybook stories, unit tests, replaces native audio elements in two components with the new player, centralizes time formatting, and adds four English i18n keys. Changes
Sequence DiagramsequenceDiagram
participant User
participant WaveAudioPlayer
participant useWaveAudioPlayer
participant FetchAPI as Fetch API
participant AudioContext
participant HTMLAudio as HTMLAudioElement
User->>WaveAudioPlayer: mount with `src` prop
WaveAudioPlayer->>useWaveAudioPlayer: init(src, barCount)
useWaveAudioPlayer->>FetchAPI: fetch(src)
FetchAPI-->>useWaveAudioPlayer: audio blob
useWaveAudioPlayer->>useWaveAudioPlayer: create blob URL
useWaveAudioPlayer->>HTMLAudio: set src (blob URL)
useWaveAudioPlayer->>AudioContext: decode audio buffer
AudioContext-->>useWaveAudioPlayer: decoded buffer
useWaveAudioPlayer->>useWaveAudioPlayer: generate waveform bars
useWaveAudioPlayer-->>WaveAudioPlayer: update bars & state
User->>WaveAudioPlayer: click play/pause or waveform
WaveAudioPlayer->>useWaveAudioPlayer: togglePlayPause / handleWaveformClick
useWaveAudioPlayer->>HTMLAudio: play/pause or set currentTime
HTMLAudio-->>useWaveAudioPlayer: playback updates
useWaveAudioPlayer-->>WaveAudioPlayer: update playedBarIndex & progress
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error, 1 warning)
✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 632 passed, 0 failed · 5 flaky📊 Browser Reports
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/composables/useWaveAudioPlayer.test.ts (1)
91-102: RestoreglobalThis.AudioContextafter the test.Line 98 permanently overrides a global. This can create cross-test coupling/flakiness.
As per coding guidelines "For mocking in Vitest, leverage Vitest's utilities where possible; keep module mocks contained without global mutable state".💡 Proposed fix
-import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' @@ const mockFetchApi = vi.fn() +const originalAudioContext = globalThis.AudioContext + +afterEach(() => { + globalThis.AudioContext = originalAudioContext + mockFetchApi.mockReset() +})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/useWaveAudioPlayer.test.ts` around lines 91 - 102, The test overrides the globalThis.AudioContext (in the "fetches and decodes audio when src changes" test) which can leak into other tests; save the original AudioContext before mocking (e.g., const originalAudioContext = globalThis.AudioContext) and restore it after the test using a finally block or an afterEach hook (set globalThis.AudioContext = originalAudioContext), ensuring any mockClose or decodeAudioData mocks are cleaned up (or use vi.restoreAllMocks()) so globals are returned to their prior state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/common/WaveAudioPlayer.vue`:
- Around line 3-9: The container div that currently uses `@pointerdown.stop` (the
compact variant div with :class="cn('flex w-full gap-2', align === 'center' ?
'items-center' : 'items-end')") still allows click events to bubble up; update
that element to also stop click propagation (add `@click.stop`) and apply the same
change to the other occurrence near the waveform/progress (the element using
`@pointerdown.stop` around the progress/waveform controls) so clicks on the player
do not trigger parent clickable card actions.
In `@src/composables/useWaveAudioPlayer.ts`:
- Around line 43-60: The generateBarsFromBuffer function can compute
samplesPerBar = 0 for very short or empty AudioBuffers causing NaN heights;
guard against that by checking channelData.length and samplesPerBar before the
inner loop and early-returning or filling bars.value with a safe default (e.g.,
barCount entries with height 8) when samplesPerBar <= 0 or channelData.length
=== 0; otherwise clamp index access (i * samplesPerBar + j) to be inside
channelData.length and compute averages as currently done, then proceed to
compute peak and set bars.value as before. Ensure you reference
generateBarsFromBuffer, samplesPerBar, channelData, barCount, and bars.value
when making the change.
- Around line 62-87: In decodeAudioSource, validate the fetch response and
ensure AudioContext and blob URLs are cleaned up: check response.ok and that
response.headers.get('content-type') exists (or matches audio/*) before creating
the Blob and setting blobUrl.value; move new AudioContext() creation to a scope
so it exists before the try and always call ctx.close() in a finally block to
avoid leaking the AudioContext; when creating a new blobUrl revoke any previous
blobUrl.value first and if decoding fails revoke the newly created blobUrl.value
in the catch before setting bars.value = generatePlaceholderBars(); keep
generateBarsFromBuffer and generatePlaceholderBars usage unchanged but only call
decodeAudioData when response validation passes.
---
Nitpick comments:
In `@src/composables/useWaveAudioPlayer.test.ts`:
- Around line 91-102: The test overrides the globalThis.AudioContext (in the
"fetches and decodes audio when src changes" test) which can leak into other
tests; save the original AudioContext before mocking (e.g., const
originalAudioContext = globalThis.AudioContext) and restore it after the test
using a finally block or an afterEach hook (set globalThis.AudioContext =
originalAudioContext), ensuring any mockClose or decodeAudioData mocks are
cleaned up (or use vi.restoreAllMocks()) so globals are returned to their prior
state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 277a5dd3-48ce-466d-b99f-f8b1eb312512
📒 Files selected for processing (7)
src/components/common/WaveAudioPlayer.stories.tssrc/components/common/WaveAudioPlayer.vuesrc/components/sidebar/tabs/queue/ResultAudio.vuesrc/composables/useWaveAudioPlayer.test.tssrc/composables/useWaveAudioPlayer.tssrc/locales/en/main.jsonsrc/platform/assets/components/MediaAudioTop.vue
… AudioContext leak Amp-Thread-ID: https://ampcode.com/threads/T-019cfee8-bad9-7131-8e7c-d2444c3fc456 Co-authored-by: Amp <amp@ampcode.com>
📦 Bundle: 5.01 MB gzip 🔴 +8.75 kBDetailsSummary
Category Glance App Entry Points — 22.8 kB (baseline 22.7 kB) • 🔴 +75 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.1 MB (baseline 1.1 MB) • 🔴 +62 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 76.1 kB (baseline 75.9 kB) • 🔴 +240 BTop-level views, pages, and routed surfaces
Status: 11 added / 11 removed Panels & Settings — 461 kB (baseline 461 kB) • 🔴 +341 BConfiguration panels, inspectors, and settings screens
Status: 22 added / 22 removed User & Accounts — 17 kB (baseline 16.9 kB) • 🔴 +81 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed Editors & Dialogs — 82.1 kB (baseline 82 kB) • 🔴 +80 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 59.5 kB (baseline 59.4 kB) • 🔴 +120 BReusable component library chunks
Status: 13 added / 13 removed Data & Services — 2.91 MB (baseline 2.91 MB) • 🟢 -150 BStores, services, APIs, and repositories
Status: 17 added / 18 removed Utilities & Hooks — 322 kB (baseline 321 kB) • 🔴 +830 BHelpers, composables, and utility bundles
Status: 20 added / 19 removed / 2 unchanged Vendor & Third-Party — 9.8 MB (baseline 9.78 MB) • 🔴 +10.8 kBExternal libraries and shared vendor chunks
Status: 6 added / 6 removed / 10 unchanged Other — 8.25 MB (baseline 8.24 MB) • 🔴 +15 kBBundles that do not match a named category
Status: 127 added / 126 removed / 6 unchanged ⚡ Performance Report
All metrics
Historical variance (last 10 runs)
Trend (last 10 commits on main)
Raw data{
"timestamp": "2026-03-18T04:28:45.983Z",
"gitSha": "1a6d3cad6fce3bf4397bc076244540bc436313d6",
"branch": "feature/new-wave-audio",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2026.8389999999954,
"styleRecalcs": 12,
"styleRecalcDurationMs": 10.595,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 362.25700000000006,
"heapDeltaBytes": 1685400,
"domNodes": 23,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 20.85,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "canvas-idle",
"durationMs": 2036.3189999999918,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.74,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 362.80199999999996,
"heapDeltaBytes": 1137308,
"domNodes": 22,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 24.779000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "canvas-idle",
"durationMs": 2026.6879999999219,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.740000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 351.51899999999995,
"heapDeltaBytes": 1392244,
"domNodes": 22,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 20.566000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.660000000000036
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1918.5559999999953,
"styleRecalcs": 78,
"styleRecalcDurationMs": 44.774,
"layouts": 12,
"layoutDurationMs": 4.13,
"taskDurationMs": 820.336,
"heapDeltaBytes": -2048516,
"domNodes": 65,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 135.02599999999998,
"eventListeners": 30,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.660000000000036
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1801.3449999999693,
"styleRecalcs": 74,
"styleRecalcDurationMs": 34.54500000000001,
"layouts": 12,
"layoutDurationMs": 3.2489999999999997,
"taskDurationMs": 750.6870000000001,
"heapDeltaBytes": -2555512,
"domNodes": 57,
"jsHeapTotalBytes": 17563648,
"scriptDurationMs": 133.38,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1793.7789999999723,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.017999999999994,
"layouts": 12,
"layoutDurationMs": 3.384,
"taskDurationMs": 735.253,
"heapDeltaBytes": -2673956,
"domNodes": 58,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 130.525,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1748.5130000000026,
"styleRecalcs": 31,
"styleRecalcDurationMs": 17.311,
"layouts": 6,
"layoutDurationMs": 0.704,
"taskDurationMs": 301.29200000000003,
"heapDeltaBytes": 5881440,
"domNodes": 79,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 25.961000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1750.4190000000222,
"styleRecalcs": 30,
"styleRecalcDurationMs": 17.250999999999998,
"layouts": 6,
"layoutDurationMs": 0.7499999999999998,
"taskDurationMs": 296.506,
"heapDeltaBytes": 5876588,
"domNodes": 78,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 25.465999999999994,
"eventListeners": 21,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.659999999999947
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1752.5819999999612,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.491,
"layouts": 6,
"layoutDurationMs": 0.562,
"taskDurationMs": 295.049,
"heapDeltaBytes": 5861056,
"domNodes": 79,
"jsHeapTotalBytes": 17039360,
"scriptDurationMs": 23.659,
"eventListeners": 21,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "dom-widget-clipping",
"durationMs": 607.0099999999741,
"styleRecalcs": 15,
"styleRecalcDurationMs": 11.043000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 390.24399999999997,
"heapDeltaBytes": 18891168,
"domNodes": 24,
"jsHeapTotalBytes": 18612224,
"scriptDurationMs": 89.643,
"eventListeners": 26,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "dom-widget-clipping",
"durationMs": 551.2729999999806,
"styleRecalcs": 14,
"styleRecalcDurationMs": 10.982000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 339.923,
"heapDeltaBytes": 13785524,
"domNodes": 25,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 64.965,
"eventListeners": 26,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.65999999999999
},
{
"name": "dom-widget-clipping",
"durationMs": 575.0500000000329,
"styleRecalcs": 13,
"styleRecalcDurationMs": 11.313,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 349.577,
"heapDeltaBytes": 13541468,
"domNodes": 25,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 65.08500000000001,
"eventListeners": 26,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.65999999999999
},
{
"name": "large-graph-idle",
"durationMs": 2025.2319999999884,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.141000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 521.577,
"heapDeltaBytes": -9619556,
"domNodes": 23,
"jsHeapTotalBytes": 8531968,
"scriptDurationMs": 101.14500000000001,
"eventListeners": 28,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000073
},
{
"name": "large-graph-idle",
"durationMs": 1999.3390000000204,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.425,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 477.944,
"heapDeltaBytes": -10055220,
"domNodes": 22,
"jsHeapTotalBytes": 9318400,
"scriptDurationMs": 90.06900000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "large-graph-idle",
"durationMs": 1994.2920000000868,
"styleRecalcs": 11,
"styleRecalcDurationMs": 13.308,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 519.0839999999998,
"heapDeltaBytes": -10559708,
"domNodes": 25,
"jsHeapTotalBytes": 6934528,
"scriptDurationMs": 102.498,
"eventListeners": 30,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.660000000000036
},
{
"name": "large-graph-pan",
"durationMs": 2107.568999999984,
"styleRecalcs": 70,
"styleRecalcDurationMs": 16.187,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1022.2600000000002,
"heapDeltaBytes": 5429280,
"domNodes": 20,
"jsHeapTotalBytes": 15790080,
"scriptDurationMs": 385.41200000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.660000000000036
},
{
"name": "large-graph-pan",
"durationMs": 2109.3140000000403,
"styleRecalcs": 70,
"styleRecalcDurationMs": 16.634,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1051.719,
"heapDeltaBytes": 6381692,
"domNodes": 20,
"jsHeapTotalBytes": 9236480,
"scriptDurationMs": 405.80899999999997,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "large-graph-pan",
"durationMs": 2106.273999999985,
"styleRecalcs": 68,
"styleRecalcDurationMs": 15.441,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1018.4150000000001,
"heapDeltaBytes": 4483960,
"domNodes": 16,
"jsHeapTotalBytes": 8450048,
"scriptDurationMs": 388.077,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "minimap-idle",
"durationMs": 2017.0119999999656,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.641,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 473.71700000000004,
"heapDeltaBytes": 13558472,
"domNodes": 20,
"jsHeapTotalBytes": 14561280,
"scriptDurationMs": 94.136,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.659999999999947
},
{
"name": "minimap-idle",
"durationMs": 2003.5520000000133,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.839999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 471.60999999999996,
"heapDeltaBytes": -10783068,
"domNodes": 22,
"jsHeapTotalBytes": 8769536,
"scriptDurationMs": 87.188,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "minimap-idle",
"durationMs": 1990.1820000000043,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.304,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 470.82099999999997,
"heapDeltaBytes": -10805280,
"domNodes": 22,
"jsHeapTotalBytes": 8269824,
"scriptDurationMs": 88.76100000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 580.1829999999768,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.452,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 355.9,
"heapDeltaBytes": 12989840,
"domNodes": 22,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 123.74600000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 571.1069999999836,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.418,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 354.539,
"heapDeltaBytes": 13032132,
"domNodes": 22,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 121.36300000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.65999999999999
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 595.6180000000586,
"styleRecalcs": 50,
"styleRecalcDurationMs": 13.924,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 382.82199999999995,
"heapDeltaBytes": 12677156,
"domNodes": 26,
"jsHeapTotalBytes": 16252928,
"scriptDurationMs": 128.67199999999997,
"eventListeners": 32,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "subgraph-idle",
"durationMs": 1992.7529999999933,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.12,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 339.71799999999996,
"heapDeltaBytes": 799952,
"domNodes": 22,
"jsHeapTotalBytes": 17563648,
"scriptDurationMs": 17.386,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.659999999999947
},
{
"name": "subgraph-idle",
"durationMs": 1997.5979999999822,
"styleRecalcs": 12,
"styleRecalcDurationMs": 11.850000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 348.271,
"heapDeltaBytes": 1003000,
"domNodes": 25,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 18.005000000000003,
"eventListeners": 30,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "subgraph-idle",
"durationMs": 2002.2429999999076,
"styleRecalcs": 12,
"styleRecalcDurationMs": 12.095,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 350.188,
"heapDeltaBytes": 1732332,
"domNodes": 27,
"jsHeapTotalBytes": 17825792,
"scriptDurationMs": 17.167,
"eventListeners": 30,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1979.343,
"styleRecalcs": 87,
"styleRecalcDurationMs": 44.745,
"layouts": 16,
"layoutDurationMs": 4.375,
"taskDurationMs": 910.6020000000001,
"heapDeltaBytes": -7141808,
"domNodes": 74,
"jsHeapTotalBytes": 17563648,
"scriptDurationMs": 97.86500000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1715.4439999999909,
"styleRecalcs": 77,
"styleRecalcDurationMs": 39.778999999999996,
"layouts": 16,
"layoutDurationMs": 6.9430000000000005,
"taskDurationMs": 680.237,
"heapDeltaBytes": -6457480,
"domNodes": 64,
"jsHeapTotalBytes": 17563648,
"scriptDurationMs": 94.271,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.680000000000017
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1690.2689999999438,
"styleRecalcs": 78,
"styleRecalcDurationMs": 38.419000000000004,
"layouts": 16,
"layoutDurationMs": 4.56,
"taskDurationMs": 651.664,
"heapDeltaBytes": -7090760,
"domNodes": 66,
"jsHeapTotalBytes": 18087936,
"scriptDurationMs": 94.24499999999999,
"eventListeners": 28,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.660000000000036
},
{
"name": "vue-large-graph-idle",
"durationMs": 13300.543000000005,
"styleRecalcs": 2,
"styleRecalcDurationMs": 26.943999999999996,
"layouts": 2,
"layoutDurationMs": 9.235999999999994,
"taskDurationMs": 13285.117000000002,
"heapDeltaBytes": 3957808,
"domNodes": -1,
"jsHeapTotalBytes": 36438016,
"scriptDurationMs": 621.46,
"eventListeners": 3534,
"totalBlockingTimeMs": 871,
"frameDurationMs": 18.33000000000029
},
{
"name": "vue-large-graph-idle",
"durationMs": 12139.696000000014,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12127.838,
"heapDeltaBytes": -28808216,
"domNodes": -3338,
"jsHeapTotalBytes": 21757952,
"scriptDurationMs": 596.596,
"eventListeners": -16486,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000073
},
{
"name": "vue-large-graph-idle",
"durationMs": 12265.201999999932,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12245.646,
"heapDeltaBytes": -13647808,
"domNodes": -3340,
"jsHeapTotalBytes": 18350080,
"scriptDurationMs": 610.649,
"eventListeners": -16486,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.33000000000029
},
{
"name": "vue-large-graph-pan",
"durationMs": 14183.144000000028,
"styleRecalcs": 66,
"styleRecalcDurationMs": 12.513999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14164.026,
"heapDeltaBytes": -36840988,
"domNodes": -3338,
"jsHeapTotalBytes": 19136512,
"scriptDurationMs": 877.213,
"eventListeners": -16482,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.339999999999783
},
{
"name": "vue-large-graph-pan",
"durationMs": 14206.833000000017,
"styleRecalcs": 66,
"styleRecalcDurationMs": 14.746999999999982,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14187.756,
"heapDeltaBytes": -4025876,
"domNodes": -3336,
"jsHeapTotalBytes": 15380480,
"scriptDurationMs": 860.265,
"eventListeners": -16484,
"totalBlockingTimeMs": 26,
"frameDurationMs": 18.340000000000146
},
{
"name": "vue-large-graph-pan",
"durationMs": 14156.072999999993,
"styleRecalcs": 65,
"styleRecalcDurationMs": 12.619999999999992,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14136.394000000002,
"heapDeltaBytes": -10700876,
"domNodes": -3340,
"jsHeapTotalBytes": 16691200,
"scriptDurationMs": 866.0989999999999,
"eventListeners": -16482,
"totalBlockingTimeMs": 2,
"frameDurationMs": 18.339999999999783
},
{
"name": "workflow-execution",
"durationMs": 459.0170000000171,
"styleRecalcs": 22,
"styleRecalcDurationMs": 23.624,
"layouts": 6,
"layoutDurationMs": 1.588,
"taskDurationMs": 123.586,
"heapDeltaBytes": 4795492,
"domNodes": 192,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 27.088999999999995,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000027
},
{
"name": "workflow-execution",
"durationMs": 458.1100000000333,
"styleRecalcs": 24,
"styleRecalcDurationMs": 24.702,
"layouts": 5,
"layoutDurationMs": 1.5999999999999999,
"taskDurationMs": 120.40899999999999,
"heapDeltaBytes": 4532096,
"domNodes": 181,
"jsHeapTotalBytes": 4456448,
"scriptDurationMs": 24.990999999999993,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000027
},
{
"name": "workflow-execution",
"durationMs": 448.9019999999755,
"styleRecalcs": 19,
"styleRecalcDurationMs": 26.163,
"layouts": 5,
"layoutDurationMs": 1.436,
"taskDurationMs": 131.24699999999999,
"heapDeltaBytes": 4513228,
"domNodes": 157,
"jsHeapTotalBytes": 4718592,
"scriptDurationMs": 33.242000000000004,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998
}
]
} |
Amp-Thread-ID: https://ampcode.com/threads/T-019cfee8-bad9-7131-8e7c-d2444c3fc456 Co-authored-by: Amp <amp@ampcode.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/composables/useWaveAudioPlayer.ts (1)
71-99:⚠️ Potential issue | 🟠 MajorFailed loads can leave stale
blobUrl, causing wrong audio playback and URL leaks.When decode/fetch fails,
blobUrlis not cleared/revoked incatch. SinceaudioSrcprefersblobUrl(Line 36), the player can keep using an old track aftersrcchanges.💡 Proposed cleanup fix
async function decodeAudioSource(url: string) { loading.value = true let ctx: AudioContext | undefined + let nextBlobUrl: string | undefined try { @@ - const blob = new Blob([arrayBuffer.slice(0)], { + const blob = new Blob([arrayBuffer], { type: response.headers.get('content-type') ?? 'audio/wav' }) - if (blobUrl.value) URL.revokeObjectURL(blobUrl.value) - blobUrl.value = URL.createObjectURL(blob) + nextBlobUrl = URL.createObjectURL(blob) ctx = new AudioContext() const audioBuffer = await ctx.decodeAudioData(arrayBuffer) + if (blobUrl.value) URL.revokeObjectURL(blobUrl.value) + blobUrl.value = nextBlobUrl + nextBlobUrl = undefined generateBarsFromBuffer(audioBuffer) - } catch { + } catch { + if (nextBlobUrl) URL.revokeObjectURL(nextBlobUrl) + if (blobUrl.value) { + URL.revokeObjectURL(blobUrl.value) + blobUrl.value = undefined + } bars.value = generatePlaceholderBars() } finally { - await ctx?.close() + await ctx?.close().catch(() => undefined) loading.value = false } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/useWaveAudioPlayer.ts` around lines 71 - 99, decodeAudioSource can leave a stale blobUrl when fetch/decode fails because the catch block only sets placeholder bars; update the catch block to revoke and clear any existing blobUrl (check blobUrl.value, call URL.revokeObjectURL and set blobUrl.value = undefined) so audioSrc (which prefers blobUrl) won't continue to point at an old track, then set bars to generatePlaceholderBars(); keep the finally behavior (ctx?.close() and loading.value = false) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/composables/useWaveAudioPlayer.test.ts`:
- Around line 72-98: The test's mocked fetch response is missing success
indicators so the composable may take the error path without decodeAudioData
running; update the mockFetchApi resolved value to include ok: true (and
optionally status: 200) so the response is treated as successful, then assert
that the AudioContext.decodeAudioData mock (or decodeAudioData on the created
AudioContext) was called with the ArrayBuffer to ensure decoding actually
happened; locate this in the test around useWaveAudioPlayer, mockFetchApi,
globalThis.AudioContext, decodeAudioData, src and bars.
In `@src/composables/useWaveAudioPlayer.ts`:
- Around line 71-100: decodeAudioSource is vulnerable to race conditions because
multiple async decodes can complete out of order and overwrite bars/blobUrl; add
a per-invocation request token and only commit state when the token matches the
latest. Concretely: introduce a module-scoped incrementing counter (e.g.,
decodeRequestCounter) and inside decodeAudioSource capture const requestId =
++decodeRequestCounter; after each await (before mutating blobUrl, calling
URL.revokeObjectURL, setting bars, loading, or calling
generateBarsFromBuffer/generatePlaceholderBars) check that requestId ===
decodeRequestCounter and skip updates if not; keep ctx local and still close it
in finally but avoid revoking/setting blobUrl or updating bars unless the token
matches. Apply the same request-token guard pattern to the similar async decode
block referenced at 149-159.
- Around line 94-96: The empty catch in useWaveAudioPlayer swallows errors;
update the catch to capture the thrown error and surface it via a dedicated
error ref (e.g., audioError or playbackError) instead of silently discarding
it—set bars.value = generatePlaceholderBars() for UI fallback and assign
audioError.value = { message: 'Failed to generate audio bars', cause: err } (or
similar), and ensure the hook returns that error ref so callers/UI can react;
alternatively, if callers should handle it, rethrow the captured error from the
catch after setting the placeholder.
---
Duplicate comments:
In `@src/composables/useWaveAudioPlayer.ts`:
- Around line 71-99: decodeAudioSource can leave a stale blobUrl when
fetch/decode fails because the catch block only sets placeholder bars; update
the catch block to revoke and clear any existing blobUrl (check blobUrl.value,
call URL.revokeObjectURL and set blobUrl.value = undefined) so audioSrc (which
prefers blobUrl) won't continue to point at an old track, then set bars to
generatePlaceholderBars(); keep the finally behavior (ctx?.close() and
loading.value = false) unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c7a3a430-37ee-4ad6-89c9-c89c9b9fe687
📒 Files selected for processing (3)
src/components/common/WaveAudioPlayer.vuesrc/composables/useWaveAudioPlayer.test.tssrc/composables/useWaveAudioPlayer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/common/WaveAudioPlayer.vue
Amp-Thread-ID: https://ampcode.com/threads/T-019cfee8-bad9-7131-8e7c-d2444c3fc456 Co-authored-by: Amp <amp@ampcode.com>
…o decoding Amp-Thread-ID: https://ampcode.com/threads/T-019cfee8-bad9-7131-8e7c-d2444c3fc456 Co-authored-by: Amp <amp@ampcode.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/composables/useWaveAudioPlayer.ts (1)
24-25:⚠️ Potential issue | 🟠 MajorInvalidate pending decode work on unmount to avoid object-URL leaks.
A request that resolves after unmount can still create a new blob URL. The
current unmount cleanup won’t revoke URLs created afterward.💡 Suggested fix
onUnmounted(() => { + decodeRequestId += 1 audioRef.value?.pause() - if (blobUrl.value) URL.revokeObjectURL(blobUrl.value) + if (blobUrl.value) { + URL.revokeObjectURL(blobUrl.value) + blobUrl.value = undefined + } })Also applies to: 172-175
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/useWaveAudioPlayer.ts` around lines 24 - 25, The decode flow can create blob URLs after the component unmounts causing leaks; update the decode logic that uses decodeRequestId (and any handler that generates blob URLs for bars) so you increment decodeRequestId before each decode, capture the current id in the async task, and when the task resolves compare the captured id against the latest decodeRequestId and skip creating/replacing blob URLs or updating bars if they differ; also ensure the unmount cleanup increments/invalidates decodeRequestId (or sets a cancelled flag) and revokes any existing object URLs stored in bars (and revokes newly created URLs immediately if the component is already unmounted) to prevent leaks.
🧹 Nitpick comments (1)
src/composables/useWaveAudioPlayer.test.ts (1)
72-102: Add regression tests for source-switch failure and empty-srccleanup.Given the new request-token logic, it would help to lock in behavior when a new
source fails to decode (and whensrcbecomes empty), so stale audio URLs
cannot regress silently.As per coding guidelines "Do not write tests that just test the mocks; ensure tests fail when code behaves unexpectedly."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/useWaveAudioPlayer.test.ts` around lines 72 - 102, Add two regression tests in useWaveAudioPlayer.test.ts to cover (1) when src switches to a new URL whose fetch decodes fail: simulate mockFetchApi resolving for the new URL and mockDecodeAudioData rejecting, then assert that the previous decoded data/bars remain unchanged (no silent replacement) and loading resolves appropriately; and (2) when src becomes empty: set src.value = '' and assert the player cleans up by calling AudioContext.close (mockClose), clearing bars and stopping any loading state. Use the existing test helpers/mocks (mockFetchApi, mockDecodeAudioData, mockClose) and the same useWaveAudioPlayer invocation to locate relevant logic in useWaveAudioPlayer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/composables/useWaveAudioPlayer.ts`:
- Line 38: audioSrc currently prefers blobUrl over src which can keep a previous
blob playing when src becomes empty or a decode fails; change the computed
audioSrc to return an empty/explicit src when src.value is falsy (e.g.,
computed(() => src.value ? (blobUrl.value ?? src.value) : src.value)) so a
cleared src wins over an existing blob, and ensure any decode error paths that
previously left blobUrl set (the function that creates/assigns blobUrl after
decoding) explicitly clear blobUrl on failure so no stale blob remains.
---
Duplicate comments:
In `@src/composables/useWaveAudioPlayer.ts`:
- Around line 24-25: The decode flow can create blob URLs after the component
unmounts causing leaks; update the decode logic that uses decodeRequestId (and
any handler that generates blob URLs for bars) so you increment decodeRequestId
before each decode, capture the current id in the async task, and when the task
resolves compare the captured id against the latest decodeRequestId and skip
creating/replacing blob URLs or updating bars if they differ; also ensure the
unmount cleanup increments/invalidates decodeRequestId (or sets a cancelled
flag) and revokes any existing object URLs stored in bars (and revokes newly
created URLs immediately if the component is already unmounted) to prevent
leaks.
---
Nitpick comments:
In `@src/composables/useWaveAudioPlayer.test.ts`:
- Around line 72-102: Add two regression tests in useWaveAudioPlayer.test.ts to
cover (1) when src switches to a new URL whose fetch decodes fail: simulate
mockFetchApi resolving for the new URL and mockDecodeAudioData rejecting, then
assert that the previous decoded data/bars remain unchanged (no silent
replacement) and loading resolves appropriately; and (2) when src becomes empty:
set src.value = '' and assert the player cleans up by calling AudioContext.close
(mockClose), clearing bars and stopping any loading state. Use the existing test
helpers/mocks (mockFetchApi, mockDecodeAudioData, mockClose) and the same
useWaveAudioPlayer invocation to locate relevant logic in useWaveAudioPlayer.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5377abfe-767e-4c7c-a54c-d94f3e976405
📒 Files selected for processing (2)
src/composables/useWaveAudioPlayer.test.tssrc/composables/useWaveAudioPlayer.ts
| const formattedCurrentTime = computed(() => formatTime(currentTime.value)) | ||
| const formattedDuration = computed(() => formatTime(duration.value)) | ||
|
|
||
| const audioSrc = computed(() => blobUrl.value ?? src.value) |
There was a problem hiding this comment.
Prevent stale audio playback when decode fails or src becomes empty.
Because audioSrc prefers blobUrl, the previous blob can stay active after a
failed decode or an empty src, so users may hear the old clip unexpectedly.
💡 Suggested fix
watch(
src,
(url) => {
+ if (blobUrl.value) {
+ URL.revokeObjectURL(blobUrl.value)
+ blobUrl.value = undefined
+ }
if (url) {
playing.value = false
currentTime.value = 0
void decodeAudioSource(url)
+ } else {
+ bars.value = generatePlaceholderBars()
}
},
{ immediate: true }
)
@@
- } catch {
+ } catch {
if (requestId === decodeRequestId) {
+ if (blobUrl.value) {
+ URL.revokeObjectURL(blobUrl.value)
+ blobUrl.value = undefined
+ }
bars.value = generatePlaceholderBars()
}
} finally {Also applies to: 101-104, 160-170
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/composables/useWaveAudioPlayer.ts` at line 38, audioSrc currently prefers
blobUrl over src which can keep a previous blob playing when src becomes empty
or a decode fails; change the computed audioSrc to return an empty/explicit src
when src.value is falsy (e.g., computed(() => src.value ? (blobUrl.value ??
src.value) : src.value)) so a cleared src wins over an existing blob, and ensure
any decode error paths that previously left blobUrl set (the function that
creates/assigns blobUrl after decoding) explicitly clear blobUrl on failure so
no stale blob remains.
| export function formatTime(seconds: number): string { | ||
| if (isNaN(seconds) || seconds === 0) return '0:00' | ||
| const mins = Math.floor(seconds / 60) | ||
| const secs = Math.floor(seconds % 60) | ||
| return `${mins}:${secs.toString().padStart(2, '0')}` | ||
| } |
There was a problem hiding this comment.
Can we reuse the one in audioUtils?
| audioSrc, | ||
| bars, | ||
| loading, | ||
| isPlaying: playing, |
There was a problem hiding this comment.
The only one that has to be renamed...
| watch( | ||
| src, | ||
| (url) => { | ||
| if (url) { |
There was a problem hiding this comment.
When the whole watch body is truthy gated, you can use whenever
| const secs = Math.floor(seconds % 60) | ||
| return `${mins}:${secs.toString().padStart(2, '0')}` | ||
| } | ||
| export { formatTime } from '@/utils/formatUtil' |
There was a problem hiding this comment.
Instead of re-exporting, can the consumers all just import from formatUtil?
| align?: 'center' | 'bottom' | ||
| variant?: 'compact' | 'expanded' |
| " | ||
| :style="{ | ||
| height: (bar.height / 100) * height + 'px', | ||
| minHeight: '2px' |
There was a problem hiding this comment.
This one could be baked into the classlist, since it's constant.
| align === 'center' ? 'items-center' : 'items-end' | ||
| ) | ||
| " | ||
| :style="{ height: height + 'px' }" |
There was a problem hiding this comment.
For all of the styles derived from height, one option is to use a CSS variable instead that you can set once at a common root and use something like h-(--wave-bar-height) so that you're not mutating styles on as many DOM elements.
Summary
Add a waveform-based audio player component (
WaveAudioPlayer) replacing the native<audio>element, with authenticated API fetch for cloud audio playback.Changes
useWaveAudioPlayercomposable with waveform visualization from audio data (Web Audio APIdecodeAudioData), playback controls, and seek supportWaveAudioPlayer.vuecomponent with compact (inline waveform + time) and expanded (full transport controls) variants<audio>inMediaAudioTop.vueandResultAudio.vuewithWaveAudioPlayerapi.fetchApi()instead of barefetch()to include Firebase JWT auth headers, fixing 401 errors in cloud environmentsReview Focus
api.fetchApi()with auth headers, converted to a Blob URL, then passed to the native<audio>element. This avoids 401 Unauthorized in cloud environments where/api/viewrequires authentication.url.includes(apiBase)) handles both full API URLs and relative paths.screen-capture.webm
┆Issue is synchronized with this Notion page by Unito