Skip to content

Commit 77bb2f2

Browse files
Copilotargyleink
andauthored
Fix jumpy audio player by setting slider max to track duration in seconds (#38)
* Initial plan * Fix jumpy audio player by setting slider max to track duration in seconds Co-authored-by: argyleink <1134620+argyleink@users.noreply.github.com> * Remove package-lock.json from git tracking (pnpm project) Co-authored-by: argyleink <1134620+argyleink@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: argyleink <1134620+argyleink@users.noreply.github.com>
1 parent 16bbaa2 commit 77bb2f2

File tree

4 files changed

+41
-44
lines changed

4 files changed

+41
-44
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ test-results/
2727

2828
# macOS-specific files
2929
.DS_Store
30+
package-lock.json

src/components/Player.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Slider from './player/Slider';
1111
export default function Player() {
1212
const audioPlayer = useRef<HTMLAudioElement | null>(null);
1313
const progressRef = useRef<number | null>(null);
14-
const [progress, setProgress] = useState(0);
14+
const [currentTime, setCurrentTime] = useState(0);
1515

1616
if (currentEpisode.value === null) {
1717
return;
@@ -21,9 +21,9 @@ export default function Player() {
2121

2222
function whilePlaying() {
2323
if (audioPlayer.current?.duration) {
24-
const percentage =
25-
(audioPlayer.current.currentTime / audioPlayer.current.duration) * 100;
26-
setProgress(percentage);
24+
const time = audioPlayer.current.currentTime;
25+
const percentage = (time / audioPlayer.current.duration) * 100;
26+
setCurrentTime(time);
2727

2828
const slider = document.querySelector('.slider');
2929
const particles = document.querySelector('.ship-particles');
@@ -65,11 +65,12 @@ export default function Player() {
6565
}, [isPlaying.value]);
6666

6767
useEffect(() => {
68-
if (progress >= 99.99) {
68+
const duration = audioPlayer.current?.duration ?? 0;
69+
if (duration > 0 && currentTime >= duration - 0.01) {
6970
isPlaying.value = false;
70-
setProgress(0);
71+
setCurrentTime(0);
7172
}
72-
}, [progress]);
73+
}, [currentTime]);
7374

7475
return (
7576
<div class="player fixed inset-x-0 bottom-0 z-50 lg:left-112 xl:left-120">
@@ -102,7 +103,7 @@ export default function Player() {
102103
</div>
103104
<ForwardButton audioPlayer={audioPlayer} />
104105
</div>
105-
<Slider audioPlayer={audioPlayer} progress={progress} />
106+
<Slider audioPlayer={audioPlayer} currentTime={currentTime} />
106107
<div class="flex items-center gap-4">
107108
<div class="flex items-center">
108109
<PlaybackRateButton audioPlayer={audioPlayer} />

src/components/player/Slider/index.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import './styles.css';
33

44
type Props = {
55
audioPlayer: MutableRef<HTMLAudioElement | null>;
6-
progress: number;
6+
currentTime: number;
77
};
88

99
function parseTime(seconds: number) {
@@ -23,11 +23,10 @@ function formatTime(seconds: Array<number>, totalSeconds = seconds) {
2323
.join(':');
2424
}
2525

26-
export default function Slider({ audioPlayer, progress }: Props) {
27-
let currentTime = parseTime(
28-
Math.floor(audioPlayer.current?.currentTime ?? 0)
29-
);
30-
let totalTime = parseTime(Math.floor(audioPlayer.current?.duration ?? 0));
26+
export default function Slider({ audioPlayer, currentTime }: Props) {
27+
let currentTimeFormatted = parseTime(Math.floor(currentTime));
28+
let duration = Math.floor(audioPlayer.current?.duration ?? 0);
29+
let totalTime = parseTime(duration);
3130

3231
return (
3332
<div class="absolute inset-x-0 bottom-full flex flex-auto touch-none items-center gap-6 md:relative">
@@ -36,20 +35,16 @@ export default function Slider({ audioPlayer, progress }: Props) {
3635
role="slider"
3736
aria-label="audio timeline"
3837
aria-valuemin={0}
39-
aria-valuemax={Math.floor(audioPlayer.current?.duration ?? 0)}
40-
aria-valuenow={Math.floor(audioPlayer.current?.currentTime ?? 0)}
41-
aria-valuetext={`${Math.floor(
42-
audioPlayer.current?.currentTime ?? 0
43-
)} seconds`}
38+
aria-valuemax={duration}
39+
aria-valuenow={Math.floor(currentTime)}
40+
aria-valuetext={`${Math.floor(currentTime)} seconds`}
4441
class="slider group"
4542
type="range"
46-
max="100"
47-
value={progress}
43+
max={duration}
44+
value={Math.floor(currentTime)}
4845
onInput={(e: InputEvent) => {
4946
if (audioPlayer?.current) {
50-
const value =
51-
(Number((e.target as HTMLInputElement).value) / 100) *
52-
audioPlayer.current.duration;
47+
const value = Number((e.target as HTMLInputElement).value);
5348
audioPlayer.current.currentTime = value;
5449
}
5550
}}
@@ -62,7 +57,7 @@ export default function Slider({ audioPlayer, progress }: Props) {
6257
<div className="col-start-1 row-start-1 h-1 w-1 rounded-full"></div>
6358
</div>
6459
<span class="hidden text-sm text-nowrap tabular-nums md:inline-block">
65-
{formatTime(currentTime, totalTime)} / {formatTime(totalTime)}
60+
{formatTime(currentTimeFormatted, totalTime)} / {formatTime(totalTime)}
6661
</span>
6762
</div>
6863
);

tests/unit/Slider.test.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@ describe('Slider', () => {
2020
});
2121

2222
it('renders slider with correct attributes', () => {
23-
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
23+
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);
2424

2525
const slider = screen.getByRole('slider');
2626
expect(slider).toBeInTheDocument();
2727
expect(slider).toHaveAttribute('aria-label', 'audio timeline');
2828
expect(slider).toHaveAttribute('aria-orientation', 'horizontal');
2929
expect(slider).toHaveAttribute('type', 'range');
30-
expect(slider).toHaveAttribute('max', '100');
30+
expect(slider).toHaveAttribute('max', '180');
3131
});
3232

3333
it('displays correct aria values', () => {
34-
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
34+
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);
3535

3636
const slider = screen.getByRole('slider');
3737
expect(slider).toHaveAttribute('aria-valuemin', '0');
@@ -41,12 +41,12 @@ describe('Slider', () => {
4141
});
4242

4343
it('updates current time when slider is moved', () => {
44-
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
44+
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);
4545

4646
const slider = screen.getByRole('slider') as HTMLInputElement;
4747

48-
// Simulate moving slider to 50% (90 seconds into 180 second track)
49-
fireEvent.input(slider, { target: { value: '50' } });
48+
// Simulate moving slider to 90 seconds
49+
fireEvent.input(slider, { target: { value: '90' } });
5050

5151
expect(mockAudioElement.currentTime).toBe(90);
5252
});
@@ -55,7 +55,7 @@ describe('Slider', () => {
5555
const nullRef = createRef();
5656
nullRef.current = null;
5757

58-
render(<Slider audioPlayer={nullRef} progress={0} />);
58+
render(<Slider audioPlayer={nullRef} currentTime={0} />);
5959

6060
const slider = screen.getByRole('slider');
6161
expect(slider).toBeInTheDocument();
@@ -65,7 +65,7 @@ describe('Slider', () => {
6565

6666
it('displays time information on desktop', () => {
6767
const { container } = render(
68-
<Slider audioPlayer={audioPlayerRef} progress={33.33} />
68+
<Slider audioPlayer={audioPlayerRef} currentTime={60} />
6969
);
7070

7171
// Time display should be present (but may be hidden on mobile)
@@ -79,7 +79,7 @@ describe('Slider', () => {
7979
mockAudioElement.duration = 0;
8080
mockAudioElement.currentTime = 0;
8181

82-
render(<Slider audioPlayer={audioPlayerRef} progress={0} />);
82+
render(<Slider audioPlayer={audioPlayerRef} currentTime={0} />);
8383

8484
const slider = screen.getByRole('slider');
8585
expect(slider).toHaveAttribute('aria-valuemax', '0');
@@ -92,7 +92,7 @@ describe('Slider', () => {
9292
mockAudioElement.duration = 7200; // 2:00:00
9393

9494
const { container } = render(
95-
<Slider audioPlayer={audioPlayerRef} progress={50.85} />
95+
<Slider audioPlayer={audioPlayerRef} currentTime={3661} />
9696
);
9797

9898
// Check that time is formatted correctly with hours
@@ -106,7 +106,7 @@ describe('Slider', () => {
106106
mockAudioElement.duration = 600; // 10:00
107107

108108
const { container } = render(
109-
<Slider audioPlayer={audioPlayerRef} progress={10.83} />
109+
<Slider audioPlayer={audioPlayerRef} currentTime={65} />
110110
);
111111

112112
// Check that time is formatted without unnecessary leading zeros
@@ -116,7 +116,7 @@ describe('Slider', () => {
116116
});
117117

118118
it('renders particle animation elements', () => {
119-
render(<Slider audioPlayer={audioPlayerRef} progress={33.33} />);
119+
render(<Slider audioPlayer={audioPlayerRef} currentTime={60} />);
120120

121121
const particles = document.querySelector('.ship-particles');
122122
expect(particles).toBeInTheDocument();
@@ -126,21 +126,21 @@ describe('Slider', () => {
126126
expect(dots).toHaveLength(5);
127127
});
128128

129-
it('calculates seek position correctly for different progress values', () => {
130-
render(<Slider audioPlayer={audioPlayerRef} progress={0} />);
129+
it('calculates seek position correctly for different time values', () => {
130+
render(<Slider audioPlayer={audioPlayerRef} currentTime={0} />);
131131

132132
const slider = screen.getByRole('slider') as HTMLInputElement;
133133

134-
// Test 0% progress
134+
// Test seeking to 0 seconds
135135
fireEvent.input(slider, { target: { value: '0' } });
136136
expect(mockAudioElement.currentTime).toBe(0);
137137

138-
// Test 100% progress
139-
fireEvent.input(slider, { target: { value: '100' } });
138+
// Test seeking to 180 seconds (end of track)
139+
fireEvent.input(slider, { target: { value: '180' } });
140140
expect(mockAudioElement.currentTime).toBe(180);
141141

142-
// Test 25% progress
143-
fireEvent.input(slider, { target: { value: '25' } });
142+
// Test seeking to 45 seconds
143+
fireEvent.input(slider, { target: { value: '45' } });
144144
expect(mockAudioElement.currentTime).toBe(45);
145145
});
146146
});

0 commit comments

Comments
 (0)