@waveform-playlist/browser 11.2.0 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -41,6 +41,15 @@ interface TrackState$1 {
41
41
  volume: number;
42
42
  pan: number;
43
43
  }
44
+ /** Per-frame data passed to registered animation callbacks. */
45
+ interface FrameData {
46
+ /** Raw engine time (for state/logic — NOT for visual positioning). */
47
+ readonly time: number;
48
+ /** time - outputLatency (for DOM positioning — matches speaker output). */
49
+ readonly visualTime: number;
50
+ readonly sampleRate: number;
51
+ readonly samplesPerPixel: number;
52
+ }
44
53
  interface PlaybackAnimationContextValue {
45
54
  isPlaying: boolean;
46
55
  currentTime: number;
@@ -49,6 +58,10 @@ interface PlaybackAnimationContextValue {
49
58
  audioStartPositionRef: React__default.RefObject<number>;
50
59
  /** Returns current playback time from engine (auto-wraps at loop boundaries). */
51
60
  getPlaybackTime: () => number;
61
+ /** Register a per-frame callback driven by the single animation loop. */
62
+ registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;
63
+ /** Unregister a per-frame callback. */
64
+ unregisterFrameCallback: (id: string) => void;
52
65
  }
53
66
  interface PlaylistStateContextValue {
54
67
  continuousPlay: boolean;
@@ -1174,7 +1187,7 @@ declare const TimeFormatSelect: React__default.FC<{
1174
1187
  }>;
1175
1188
  /**
1176
1189
  * Audio position display that uses the playlist context.
1177
- * Uses requestAnimationFrame for smooth 60fps updates during playback.
1190
+ * Updates via the shared animation frame registry — no own rAF loop.
1178
1191
  * Direct DOM manipulation avoids React re-renders.
1179
1192
  */
1180
1193
  declare const AudioPosition: React__default.FC<{
@@ -1811,4 +1824,4 @@ declare function getWaveformDataMetadata(src: string): Promise<{
1811
1824
  bits: 8 | 16;
1812
1825
  }>;
1813
1826
 
1814
- export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
1827
+ export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type FrameData, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,15 @@ interface TrackState$1 {
41
41
  volume: number;
42
42
  pan: number;
43
43
  }
44
+ /** Per-frame data passed to registered animation callbacks. */
45
+ interface FrameData {
46
+ /** Raw engine time (for state/logic — NOT for visual positioning). */
47
+ readonly time: number;
48
+ /** time - outputLatency (for DOM positioning — matches speaker output). */
49
+ readonly visualTime: number;
50
+ readonly sampleRate: number;
51
+ readonly samplesPerPixel: number;
52
+ }
44
53
  interface PlaybackAnimationContextValue {
45
54
  isPlaying: boolean;
46
55
  currentTime: number;
@@ -49,6 +58,10 @@ interface PlaybackAnimationContextValue {
49
58
  audioStartPositionRef: React__default.RefObject<number>;
50
59
  /** Returns current playback time from engine (auto-wraps at loop boundaries). */
51
60
  getPlaybackTime: () => number;
61
+ /** Register a per-frame callback driven by the single animation loop. */
62
+ registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;
63
+ /** Unregister a per-frame callback. */
64
+ unregisterFrameCallback: (id: string) => void;
52
65
  }
53
66
  interface PlaylistStateContextValue {
54
67
  continuousPlay: boolean;
@@ -1174,7 +1187,7 @@ declare const TimeFormatSelect: React__default.FC<{
1174
1187
  }>;
1175
1188
  /**
1176
1189
  * Audio position display that uses the playlist context.
1177
- * Uses requestAnimationFrame for smooth 60fps updates during playback.
1190
+ * Updates via the shared animation frame registry — no own rAF loop.
1178
1191
  * Direct DOM manipulation avoids React re-renders.
1179
1192
  */
1180
1193
  declare const AudioPosition: React__default.FC<{
@@ -1811,4 +1824,4 @@ declare function getWaveformDataMetadata(src: string): Promise<{
1811
1824
  bits: 8 | 16;
1812
1825
  }>;
1813
1826
 
1814
- export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
1827
+ export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type FrameData, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
package/dist/index.js CHANGED
@@ -3822,6 +3822,7 @@ var WaveformPlaylistProvider = ({
3822
3822
  const playbackEndTimeRef = (0, import_react24.useRef)(null);
3823
3823
  const scrollContainerRef = (0, import_react24.useRef)(null);
3824
3824
  const isAutomaticScrollRef = (0, import_react24.useRef)(false);
3825
+ const frameCallbacksRef = (0, import_react24.useRef)(/* @__PURE__ */ new Map());
3825
3826
  const continuousPlayRef = (0, import_react24.useRef)((_d = annotationList == null ? void 0 : annotationList.isContinuousPlay) != null ? _d : false);
3826
3827
  const activeAnnotationIdRef = (0, import_react24.useRef)(null);
3827
3828
  const engineTracksRef = (0, import_react24.useRef)(null);
@@ -4282,10 +4283,30 @@ var WaveformPlaylistProvider = ({
4282
4283
  const elapsed = (0, import_tone4.getContext)().currentTime - ((_a2 = playbackStartTimeRef.current) != null ? _a2 : 0);
4283
4284
  return ((_b2 = audioStartPositionRef.current) != null ? _b2 : 0) + elapsed;
4284
4285
  }, []);
4286
+ const registerFrameCallback = (0, import_react24.useCallback)((id, cb) => {
4287
+ frameCallbacksRef.current.set(id, cb);
4288
+ }, []);
4289
+ const unregisterFrameCallback = (0, import_react24.useCallback)((id) => {
4290
+ frameCallbacksRef.current.delete(id);
4291
+ }, []);
4285
4292
  const startAnimationLoop = (0, import_react24.useCallback)(() => {
4293
+ const audioCtx = (0, import_playout5.getGlobalAudioContext)();
4286
4294
  const updateTime = () => {
4287
4295
  const time = getPlaybackTime();
4288
4296
  currentTimeRef.current = time;
4297
+ const latency = "outputLatency" in audioCtx ? audioCtx.outputLatency : 0;
4298
+ const visualTime = Math.max(0, time - latency);
4299
+ const sr = sampleRateRef.current;
4300
+ const spp = samplesPerPixelRef.current;
4301
+ const frameData = {
4302
+ time,
4303
+ visualTime,
4304
+ sampleRate: sr,
4305
+ samplesPerPixel: spp
4306
+ };
4307
+ for (const cb of frameCallbacksRef.current.values()) {
4308
+ cb(frameData);
4309
+ }
4289
4310
  const currentAnnotations = annotationsRef.current;
4290
4311
  if (currentAnnotations.length > 0) {
4291
4312
  const currentAnnotation = currentAnnotations.find(
@@ -4320,10 +4341,9 @@ var WaveformPlaylistProvider = ({
4320
4341
  }
4321
4342
  if (isAutomaticScrollRef.current && scrollContainerRef.current && duration > 0) {
4322
4343
  const container = scrollContainerRef.current;
4323
- const sr = sampleRateRef.current;
4324
- const pixelPosition = time * sr / samplesPerPixelRef.current;
4344
+ const pixelPosition = visualTime * sr / spp;
4325
4345
  const containerWidth = container.clientWidth;
4326
- const targetScrollLeft = Math.max(0, pixelPosition - containerWidth / 2);
4346
+ const targetScrollLeft = Math.round(Math.max(0, pixelPosition - containerWidth / 2));
4327
4347
  container.scrollLeft = targetScrollLeft;
4328
4348
  }
4329
4349
  if (playbackEndTimeRef.current !== null && time >= playbackEndTimeRef.current) {
@@ -4559,7 +4579,9 @@ var WaveformPlaylistProvider = ({
4559
4579
  currentTimeRef,
4560
4580
  playbackStartTimeRef,
4561
4581
  audioStartPositionRef,
4562
- getPlaybackTime
4582
+ getPlaybackTime,
4583
+ registerFrameCallback,
4584
+ unregisterFrameCallback
4563
4585
  }),
4564
4586
  [
4565
4587
  isPlaying,
@@ -4567,7 +4589,9 @@ var WaveformPlaylistProvider = ({
4567
4589
  currentTimeRef,
4568
4590
  playbackStartTimeRef,
4569
4591
  audioStartPositionRef,
4570
- getPlaybackTime
4592
+ getPlaybackTime,
4593
+ registerFrameCallback,
4594
+ unregisterFrameCallback
4571
4595
  ]
4572
4596
  );
4573
4597
  const stateValue = (0, import_react24.useMemo)(
@@ -5348,32 +5372,19 @@ var PositionDisplay = import_styled_components3.default.span`
5348
5372
  var AudioPosition = ({ className }) => {
5349
5373
  var _a;
5350
5374
  const timeRef = (0, import_react27.useRef)(null);
5351
- const animationFrameRef = (0, import_react27.useRef)(null);
5352
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5375
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5353
5376
  const { timeFormat: format } = usePlaylistData();
5354
5377
  (0, import_react27.useEffect)(() => {
5355
- const updateTime = () => {
5356
- var _a2;
5357
- if (timeRef.current) {
5358
- const time = isPlaying ? getPlaybackTime() : (_a2 = currentTimeRef.current) != null ? _a2 : 0;
5359
- timeRef.current.textContent = (0, import_ui_components6.formatTime)(time, format);
5360
- }
5361
- if (isPlaying) {
5362
- animationFrameRef.current = requestAnimationFrame(updateTime);
5363
- }
5364
- };
5378
+ const id = "audio-position";
5365
5379
  if (isPlaying) {
5366
- animationFrameRef.current = requestAnimationFrame(updateTime);
5367
- } else {
5368
- updateTime();
5380
+ registerFrameCallback(id, ({ time }) => {
5381
+ if (timeRef.current) {
5382
+ timeRef.current.textContent = (0, import_ui_components6.formatTime)(time, format);
5383
+ }
5384
+ });
5369
5385
  }
5370
- return () => {
5371
- if (animationFrameRef.current) {
5372
- cancelAnimationFrame(animationFrameRef.current);
5373
- animationFrameRef.current = null;
5374
- }
5375
- };
5376
- }, [isPlaying, format, currentTimeRef, getPlaybackTime]);
5386
+ return () => unregisterFrameCallback(id);
5387
+ }, [isPlaying, format, registerFrameCallback, unregisterFrameCallback]);
5377
5388
  (0, import_react27.useEffect)(() => {
5378
5389
  var _a2;
5379
5390
  if (!isPlaying && timeRef.current) {
@@ -5533,33 +5544,20 @@ var PlayheadLine = import_styled_components4.default.div.attrs((props) => ({
5533
5544
  `;
5534
5545
  var AnimatedPlayhead = ({ color = "#ff0000" }) => {
5535
5546
  const playheadRef = (0, import_react30.useRef)(null);
5536
- const animationFrameRef = (0, import_react30.useRef)(null);
5537
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5547
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5538
5548
  const { samplesPerPixel, sampleRate, progressBarWidth } = usePlaylistData();
5539
5549
  (0, import_react30.useEffect)(() => {
5540
- const updatePosition = () => {
5541
- var _a;
5542
- if (playheadRef.current) {
5543
- const time = isPlaying ? getPlaybackTime() : (_a = currentTimeRef.current) != null ? _a : 0;
5544
- const position = time * sampleRate / samplesPerPixel;
5545
- playheadRef.current.style.transform = `translate3d(${position}px, 0, 0)`;
5546
- }
5547
- if (isPlaying) {
5548
- animationFrameRef.current = requestAnimationFrame(updatePosition);
5549
- }
5550
- };
5550
+ const id = "playhead";
5551
5551
  if (isPlaying) {
5552
- animationFrameRef.current = requestAnimationFrame(updatePosition);
5553
- } else {
5554
- updatePosition();
5552
+ registerFrameCallback(id, ({ visualTime, sampleRate: sr, samplesPerPixel: spp }) => {
5553
+ if (playheadRef.current) {
5554
+ const px = visualTime * sr / spp;
5555
+ playheadRef.current.style.transform = `translate3d(${px}px, 0, 0)`;
5556
+ }
5557
+ });
5555
5558
  }
5556
- return () => {
5557
- if (animationFrameRef.current) {
5558
- cancelAnimationFrame(animationFrameRef.current);
5559
- animationFrameRef.current = null;
5560
- }
5561
- };
5562
- }, [isPlaying, sampleRate, samplesPerPixel, currentTimeRef, getPlaybackTime]);
5559
+ return () => unregisterFrameCallback(id);
5560
+ }, [isPlaying, registerFrameCallback, unregisterFrameCallback]);
5563
5561
  (0, import_react30.useEffect)(() => {
5564
5562
  var _a;
5565
5563
  if (!isPlaying && playheadRef.current) {
@@ -5631,10 +5629,10 @@ var ChannelWithProgress = (_a) => {
5631
5629
  "clipOffsetSeconds"
5632
5630
  ]);
5633
5631
  const progressRef = (0, import_react31.useRef)(null);
5634
- const animationFrameRef = (0, import_react31.useRef)(null);
5632
+ const callbackId = (0, import_react31.useId)();
5635
5633
  const theme = (0, import_ui_components8.useTheme)();
5636
5634
  const { waveHeight } = (0, import_ui_components8.usePlaylistInfo)();
5637
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5635
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5638
5636
  const { samplesPerPixel, sampleRate } = usePlaylistData();
5639
5637
  const progressColor = (theme == null ? void 0 : theme.waveProgressColor) || "rgba(0, 0, 0, 0.1)";
5640
5638
  const clipPixelWidth = (0, import_core6.clipPixelWidth)(
@@ -5643,46 +5641,32 @@ var ChannelWithProgress = (_a) => {
5643
5641
  samplesPerPixel
5644
5642
  );
5645
5643
  (0, import_react31.useEffect)(() => {
5646
- const updateProgress = () => {
5647
- var _a2;
5648
- if (progressRef.current) {
5649
- const currentTime = isPlaying ? getPlaybackTime() : (_a2 = currentTimeRef.current) != null ? _a2 : 0;
5650
- const currentSample = currentTime * sampleRate;
5651
- const clipEndSample = clipStartSample + clipDurationSamples;
5652
- let ratio = 0;
5653
- if (currentSample <= clipStartSample) {
5654
- ratio = 0;
5655
- } else if (currentSample >= clipEndSample) {
5656
- ratio = 1;
5657
- } else {
5658
- const playedSamples = currentSample - clipStartSample;
5659
- ratio = playedSamples / clipDurationSamples;
5660
- }
5661
- progressRef.current.style.transform = `scaleX(${ratio})`;
5662
- }
5663
- if (isPlaying) {
5664
- animationFrameRef.current = requestAnimationFrame(updateProgress);
5665
- }
5666
- };
5667
5644
  if (isPlaying) {
5668
- animationFrameRef.current = requestAnimationFrame(updateProgress);
5669
- } else {
5670
- updateProgress();
5645
+ registerFrameCallback(callbackId, ({ visualTime, sampleRate: sr }) => {
5646
+ if (progressRef.current) {
5647
+ const currentSample = visualTime * sr;
5648
+ const clipEndSample = clipStartSample + clipDurationSamples;
5649
+ let ratio = 0;
5650
+ if (currentSample <= clipStartSample) {
5651
+ ratio = 0;
5652
+ } else if (currentSample >= clipEndSample) {
5653
+ ratio = 1;
5654
+ } else {
5655
+ const playedSamples = currentSample - clipStartSample;
5656
+ ratio = playedSamples / clipDurationSamples;
5657
+ }
5658
+ progressRef.current.style.transform = `scaleX(${ratio})`;
5659
+ }
5660
+ });
5671
5661
  }
5672
- return () => {
5673
- if (animationFrameRef.current) {
5674
- cancelAnimationFrame(animationFrameRef.current);
5675
- animationFrameRef.current = null;
5676
- }
5677
- };
5662
+ return () => unregisterFrameCallback(callbackId);
5678
5663
  }, [
5679
5664
  isPlaying,
5680
- sampleRate,
5681
5665
  clipStartSample,
5682
5666
  clipDurationSamples,
5683
- clipPixelWidth,
5684
- currentTimeRef,
5685
- getPlaybackTime
5667
+ callbackId,
5668
+ registerFrameCallback,
5669
+ unregisterFrameCallback
5686
5670
  ]);
5687
5671
  (0, import_react31.useEffect)(() => {
5688
5672
  var _a2;