@@ -3,6 +3,7 @@ import { usePlayerStore } from "../../stores/playerStore";
33import { useMediaQuery } from "@/hooks/useMediaQuery" ;
44import { useShadowingStore } from "../../stores/shadowingStore" ;
55import {
6+ CachedWaveformData ,
67 getCachedWaveform ,
78 retrieveMediaFile ,
89 setCachedWaveform ,
@@ -34,6 +35,7 @@ import {
3435 createPlaceholderWaveform ,
3536 shouldUseAdaptiveWaveform ,
3637 shouldUseDetailedWaveform ,
38+ shouldUseProgressiveWaveform ,
3739} from "../../utils/waveformAnalysis" ;
3840
3941// Constant toast ID to ensure only one bookmark notification is shown at a time
@@ -44,6 +46,20 @@ const BOOKMARK_TOAST_ID = "bookmark-action-toast";
4446const EMPTY_BOOKMARKS : readonly any [ ] = Object . freeze ( [ ] ) ;
4547const EMPTY_SEGMENTS : readonly any [ ] = Object . freeze ( [ ] ) ;
4648
49+ const normalizeCachedWaveform = (
50+ waveform : CachedWaveformData
51+ ) : CachedWaveformData => ( {
52+ ...waveform ,
53+ status :
54+ waveform . status ?? ( waveform . strategy === "placeholder" ? "placeholder" : "ready" ) ,
55+ progress :
56+ typeof waveform . progress === "number"
57+ ? Math . max ( 0 , Math . min ( 100 , waveform . progress ) )
58+ : waveform . strategy === "placeholder"
59+ ? 0
60+ : 100 ,
61+ } ) ;
62+
4763type ShadowWaveform = {
4864 start : number ;
4965 data : Float32Array ;
@@ -79,6 +95,10 @@ export const WaveformVisualizer = () => {
7995 { id : string ; x1 : number ; x2 : number ; y1 : number ; y2 : number } [ ]
8096 > ( [ ] ) ;
8197 const [ waveformData , setWaveformData ] = useState < Float32Array | null > ( null ) ;
98+ const [ waveformLoadState , setWaveformLoadState ] = useState < {
99+ status : "idle" | "placeholder" | "analyzing" | "ready" | "error" ;
100+ progress : number ;
101+ } > ( { status : "idle" , progress : 0 } ) ;
82102 const [ isDragging , setIsDragging ] = useState ( false ) ;
83103 const [ dragStart , setDragStart ] = useState < number | null > ( null ) ;
84104 const [ dragEnd , setDragEnd ] = useState < number | null > ( null ) ;
@@ -324,18 +344,43 @@ export const WaveformVisualizer = () => {
324344 ! currentFile . type . includes ( "video" ) )
325345 ) {
326346 setWaveformData ( null ) ;
347+ setWaveformLoadState ( { status : "idle" , progress : 0 } ) ;
327348 return ;
328349 }
329350
330351 let cancelled = false ;
352+ let idleId : number | null = null ;
353+ let timeoutId : ReturnType < typeof setTimeout > | null = null ;
354+
355+ const setWaveformPreview = ( waveform : CachedWaveformData ) => {
356+ if ( cancelled ) return ;
357+
358+ const normalized = normalizeCachedWaveform ( waveform ) ;
359+ setWaveformData ( Float32Array . from ( normalized . peaks ) ) ;
360+ setWaveformLoadState ( {
361+ status : normalized . status ?? "ready" ,
362+ progress : normalized . progress ?? 0 ,
363+ } ) ;
364+ } ;
365+
366+ const scheduleBackgroundAnalysis = ( task : ( ) => void ) => {
367+ if ( typeof window !== "undefined" && "requestIdleCallback" in window ) {
368+ idleId = (
369+ window as Window & {
370+ requestIdleCallback : ( callback : IdleRequestCallback ) => number ;
371+ }
372+ ) . requestIdleCallback ( ( ) => task ( ) ) ;
373+ return ;
374+ }
375+
376+ timeoutId = globalThis . setTimeout ( task , 0 ) ;
377+ } ;
331378
332379 const loadAudio = async ( ) => {
333380 try {
334381 if ( currentYouTube ) {
335382 if ( ! cancelled ) {
336- setWaveformData (
337- Float32Array . from ( createPlaceholderWaveform ( duration || 0 , 1200 ) . peaks )
338- ) ;
383+ setWaveformPreview ( createPlaceholderWaveform ( duration || 0 , 1200 ) ) ;
339384 }
340385 return ;
341386 }
@@ -346,17 +391,15 @@ export const WaveformVisualizer = () => {
346391
347392 const mediaKey = buildWaveformMediaKey ( currentFile ) ;
348393 const cached = await getCachedWaveform ( mediaKey ) ;
349- if ( cached && ! cancelled ) {
350- setWaveformData ( Float32Array . from ( cached . peaks ) ) ;
351- return ;
352- }
353394
354395 if ( currentFile . type . includes ( "video" ) ) {
355- const placeholder = createPlaceholderWaveform ( duration || 0 , 1200 ) ;
396+ const placeholder : CachedWaveformData = {
397+ ...createPlaceholderWaveform ( duration || 0 , 1200 ) ,
398+ status : "ready" ,
399+ progress : 100 ,
400+ } ;
356401 await setCachedWaveform ( mediaKey , placeholder ) ;
357- if ( ! cancelled ) {
358- setWaveformData ( Float32Array . from ( placeholder . peaks ) ) ;
359- }
402+ setWaveformPreview ( placeholder ) ;
360403 return ;
361404 }
362405
@@ -371,22 +414,105 @@ export const WaveformVisualizer = () => {
371414 throw new Error ( "Unable to load file for waveform analysis" ) ;
372415 }
373416
374- const analysis =
375- shouldUseDetailedWaveform ( file ) || shouldUseAdaptiveWaveform ( file )
376- ? await analyzeAudioFileWaveform ( file )
377- : createPlaceholderWaveform ( duration || 0 , 800 ) ;
417+ const normalizedCached = cached ? normalizeCachedWaveform ( cached ) : null ;
418+ const isDetailed = shouldUseDetailedWaveform ( file ) ;
419+ const isAdaptive = shouldUseAdaptiveWaveform ( file ) ;
420+ const isProgressive = shouldUseProgressiveWaveform ( file ) ;
421+ const canAnalyze = isDetailed || isAdaptive ;
378422
379- await setCachedWaveform ( mediaKey , analysis ) ;
423+ if ( normalizedCached ) {
424+ setWaveformPreview ( normalizedCached ) ;
425+ if ( normalizedCached . status === "ready" || ! canAnalyze ) {
426+ return ;
427+ }
428+ }
380429
381- if ( ! cancelled ) {
382- setWaveformData ( Float32Array . from ( analysis . peaks ) ) ;
430+ if ( ! canAnalyze ) {
431+ const placeholder : CachedWaveformData = {
432+ ...( normalizedCached ?? createPlaceholderWaveform ( duration || 0 , 800 ) ) ,
433+ status : "placeholder" ,
434+ progress : 0 ,
435+ updatedAt : Date . now ( ) ,
436+ } ;
437+ await setCachedWaveform ( mediaKey , placeholder ) ;
438+ setWaveformPreview ( placeholder ) ;
439+ return ;
383440 }
441+
442+ if ( isProgressive ) {
443+ const placeholder : CachedWaveformData = {
444+ ...( normalizedCached ?? createPlaceholderWaveform ( duration || 0 , 1000 ) ) ,
445+ status : "analyzing" ,
446+ progress : Math . max ( 5 , normalizedCached ?. progress ?? 0 ) ,
447+ updatedAt : Date . now ( ) ,
448+ } ;
449+
450+ await setCachedWaveform ( mediaKey , placeholder ) ;
451+ setWaveformPreview ( placeholder ) ;
452+
453+ scheduleBackgroundAnalysis ( ( ) => {
454+ void ( async ( ) => {
455+ try {
456+ const analysis = await analyzeAudioFileWaveform ( file , ( update ) => {
457+ if ( cancelled ) return ;
458+
459+ const previewWaveform : CachedWaveformData = {
460+ peaks : placeholder . peaks ,
461+ resolution : placeholder . resolution ,
462+ duration : duration || placeholder . duration ,
463+ strategy : placeholder . strategy ,
464+ status : update . status ,
465+ progress : update . progress ,
466+ updatedAt : Date . now ( ) ,
467+ } ;
468+
469+ setWaveformLoadState ( {
470+ status : update . status ,
471+ progress : update . progress ,
472+ } ) ;
473+ void setCachedWaveform ( mediaKey , previewWaveform ) ;
474+ } ) ;
475+
476+ await setCachedWaveform ( mediaKey , analysis ) ;
477+ setWaveformPreview ( analysis ) ;
478+ } catch ( error ) {
479+ console . error ( "Error analyzing waveform in background:" , error ) ;
480+ const failedWaveform : CachedWaveformData = {
481+ peaks : placeholder . peaks ,
482+ resolution : placeholder . resolution ,
483+ duration : duration || placeholder . duration ,
484+ strategy : placeholder . strategy ,
485+ status : "error" ,
486+ progress : 0 ,
487+ updatedAt : Date . now ( ) ,
488+ } ;
489+ await setCachedWaveform ( mediaKey , failedWaveform ) ;
490+ setWaveformPreview ( failedWaveform ) ;
491+ }
492+ } ) ( ) ;
493+ } ) ;
494+
495+ return ;
496+ }
497+
498+ const analysis = await analyzeAudioFileWaveform ( file , ( update ) => {
499+ if ( cancelled ) return ;
500+ setWaveformLoadState ( {
501+ status : update . status ,
502+ progress : update . progress ,
503+ } ) ;
504+ } ) ;
505+
506+ await setCachedWaveform ( mediaKey , analysis ) ;
507+ setWaveformPreview ( analysis ) ;
384508 } catch ( error ) {
385509 console . error ( "Error loading audio for waveform:" , error ) ;
386510 if ( ! cancelled ) {
387- setWaveformData (
388- Float32Array . from ( createPlaceholderWaveform ( duration || 0 , 800 ) . peaks )
389- ) ;
511+ setWaveformPreview ( {
512+ ...createPlaceholderWaveform ( duration || 0 , 800 ) ,
513+ status : "error" ,
514+ progress : 0 ,
515+ } ) ;
390516 }
391517 }
392518 } ;
@@ -395,6 +521,16 @@ export const WaveformVisualizer = () => {
395521
396522 return ( ) => {
397523 cancelled = true ;
524+ if ( idleId !== null && typeof window !== "undefined" && "cancelIdleCallback" in window ) {
525+ (
526+ window as Window & {
527+ cancelIdleCallback : ( id : number ) => void ;
528+ }
529+ ) . cancelIdleCallback ( idleId ) ;
530+ }
531+ if ( timeoutId !== null ) {
532+ globalThis . clearTimeout ( timeoutId ) ;
533+ }
398534 } ;
399535 } , [ currentFile , currentYouTube , duration ] ) ;
400536
@@ -1766,6 +1902,18 @@ export const WaveformVisualizer = () => {
17661902 </ div >
17671903 ) }
17681904
1905+ { ! currentYouTube &&
1906+ ( waveformLoadState . status === "analyzing" ||
1907+ waveformLoadState . status === "error" ) && (
1908+ < div className = "absolute top-2 right-2 z-20 flex items-center gap-2 px-3 py-1.5 bg-black/55 backdrop-blur-sm rounded border border-white/10 shadow-sm" >
1909+ < span className = "text-white/80 text-xs font-medium" >
1910+ { waveformLoadState . status === "error"
1911+ ? t ( "waveform.analysisError" )
1912+ : `${ t ( "waveform.analyzing" ) } ${ waveformLoadState . progress } %` }
1913+ </ span >
1914+ </ div >
1915+ ) }
1916+
17691917 { /* Tooltip for current hover/touch position */ }
17701918 { hoverTime !== null && (
17711919 < div
0 commit comments