feat(audio): introduce playback speed

This commit is contained in:
Elian Doran 2026-03-11 19:17:14 +02:00
parent e86d84c463
commit 365d0f0aac
No known key found for this signature in database
5 changed files with 75 additions and 71 deletions

View File

@ -3,7 +3,7 @@ import { useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import { getUrlForDownload } from "../../../services/open";
import { LoopButton, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
import { LoopButton, PlaybackSpeed, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
export default function AudioPreview({ note }: { note: FNote }) {
const [playing, setPlaying] = useState(false);
@ -22,7 +22,9 @@ export default function AudioPreview({ note }: { note: FNote }) {
<SeekBar mediaRef={audioRef} />
<div class="media-buttons-row">
<div className="left" />
<div className="left">
<PlaybackSpeed mediaRef={audioRef} />
</div>
<div className="center">
<div className="spacer" />

View File

@ -66,4 +66,23 @@
cursor: pointer;
}
}
.speed-dropdown {
position: relative;
.tn-icon {
transform: translateY(-10%);
}
.media-speed-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
transform: translateY(15%);
text-align: center;
font-size: 0.6rem;
font-variant-numeric: tabular-nums;
}
}
}

View File

@ -5,6 +5,8 @@ import { useEffect, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import ActionButton from "../../react/ActionButton";
import Dropdown from "../../react/Dropdown";
import Icon from "../../react/Icon";
export function SeekBar({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement> }) {
const [currentTime, setCurrentTime] = useState(0);
@ -176,3 +178,51 @@ export function LoopButton({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement
/>
);
}
const PLAYBACK_SPEEDS = [0.5, 1, 1.25, 1.5, 2];
export function PlaybackSpeed({ mediaRef }: { mediaRef: RefObject<HTMLVideoElement | HTMLAudioElement> }) {
const [speed, setSpeed] = useState(() => mediaRef.current?.playbackRate ?? 1);
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
setSpeed(media.playbackRate);
const onRateChange = () => setSpeed(media.playbackRate);
media.addEventListener("ratechange", onRateChange);
return () => media.removeEventListener("ratechange", onRateChange);
}, []);
const selectSpeed = (rate: number) => {
const media = mediaRef.current;
if (!media) return;
media.playbackRate = rate;
setSpeed(rate);
};
return (
<Dropdown
iconAction
hideToggleArrow
buttonClassName="speed-dropdown"
text={<>
<Icon icon="bx bx-tachometer" />
<span class="media-speed-label">{speed}x</span>
</>}
title={t("video.playback-speed")}
>
{PLAYBACK_SPEEDS.map((rate) => (
<li key={rate}>
<button
class={`dropdown-item ${rate === speed ? "active" : ""}`}
onClick={() => selectSpeed(rate)}
>
{rate}x
</button>
</li>
))}
</Dropdown>
);
}

View File

@ -25,23 +25,4 @@
opacity: 1;
transition: opacity 300ms ease;
}
.speed-dropdown {
position: relative;
.tn-icon {
transform: translateY(-10%);
}
.video-speed-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
transform: translateY(15%);
text-align: center;
font-size: 0.6rem;
font-variant-numeric: tabular-nums;
}
}
}

View File

@ -10,7 +10,7 @@ import ActionButton from "../../react/ActionButton";
import Dropdown from "../../react/Dropdown";
import Icon from "../../react/Icon";
import NoItems from "../../react/NoItems";
import { LoopButton, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
import { LoopButton, PlaybackSpeed, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
const AUTO_HIDE_DELAY = 3000;
@ -117,7 +117,7 @@ export default function VideoPreview({ note }: { note: FNote }) {
<SeekBar mediaRef={videoRef} />
<div class="media-buttons-row">
<div className="left">
<PlaybackSpeed videoRef={videoRef} />
<PlaybackSpeed mediaRef={videoRef} />
<RotateButton videoRef={videoRef} />
</div>
<div className="center">
@ -169,54 +169,6 @@ function useAutoHideControls(videoRef: RefObject<HTMLVideoElement>, playing: boo
return { visible, onMouseMove, flash: onMouseMove };
}
const PLAYBACK_SPEEDS = [0.5, 1, 1.25, 1.5, 2];
function PlaybackSpeed({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
const [speed, setSpeed] = useState(() => videoRef.current?.playbackRate ?? 1);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
setSpeed(video.playbackRate);
const onRateChange = () => setSpeed(video.playbackRate);
video.addEventListener("ratechange", onRateChange);
return () => video.removeEventListener("ratechange", onRateChange);
}, []);
const selectSpeed = (rate: number) => {
const video = videoRef.current;
if (!video) return;
video.playbackRate = rate;
setSpeed(rate);
};
return (
<Dropdown
iconAction
hideToggleArrow
buttonClassName="speed-dropdown"
text={<>
<Icon icon="bx bx-tachometer" />
<span class="video-speed-label">{speed}x</span>
</>}
title={t("video.playback-speed")}
>
{PLAYBACK_SPEEDS.map((rate) => (
<li key={rate}>
<button
class={`dropdown-item ${rate === speed ? "active" : ""}`}
onClick={() => selectSpeed(rate)}
>
{rate}x
</button>
</li>
))}
</Dropdown>
);
}
function RotateButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
const [rotation, setRotation] = useState(0);