From 2becba47c3d68a6a9af8d49031434b6e12b2a41a Mon Sep 17 00:00:00 2001 From: Ian Schwartz <168640+studiozeroseven@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:35:01 -0500 Subject: [PATCH] fix(frontend): display audio/video bitrate in correct SI units MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1164 — the bitrate value from yt-dlp is in bits per second, but the frontend was formatting it via bitsToBytes() then humanFileSize() with a '/s' suffix, producing labels like '984 KiB/s' for what is actually a per-second *bit* rate. Add a dedicated humanBitrate() helper that formats a bps value with SI decimal units (bps, kbps, Mbps, Gbps, Tbps) — the standard for network bitrates and the convention yt-dlp itself uses. Update VideoListItemTable and the stream-info block on Video.tsx to use the new helper, and drop the now-unused bitsToBytes import from Video.tsx. --- .../src/components/VideoListItemTable.tsx | 6 +-- frontend/src/functions/humanBitrate.ts | 44 +++++++++++++++++++ frontend/src/pages/Video.tsx | 4 +- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 frontend/src/functions/humanBitrate.ts diff --git a/frontend/src/components/VideoListItemTable.tsx b/frontend/src/components/VideoListItemTable.tsx index dc09c585..b21ae246 100644 --- a/frontend/src/components/VideoListItemTable.tsx +++ b/frontend/src/components/VideoListItemTable.tsx @@ -3,12 +3,12 @@ import Routes from '../configuration/routes/RouteList'; import { VideoType } from '../pages/Home'; import { ViewStylesType } from '../configuration/constants/ViewStyle'; import humanFileSize from '../functions/humanFileSize'; +import humanBitrate from '../functions/humanBitrate'; import { FileSizeUnits } from '../api/actions/updateUserConfig'; import { useUserConfigStore } from '../stores/UserConfigStore'; import { useVideoSelectionStore } from '../stores/VideoSelectionStore'; import iconChecked from '/img/icon-seen.svg'; import iconUnchecked from '/img/icon-unseen.svg'; -import bitsToBytes from '../functions/bitsToBytes'; const StreamsTypeEmun = { Video: 'video', @@ -98,9 +98,9 @@ const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => { {`${videoStream?.width || '-'}x${videoStream?.height || '-'}`} {humanFileSize(media_size, useSiUnits)} {videoStream?.codec || '-'} - {humanFileSize(bitsToBytes(videoStream?.bitrate || 0), useSiUnits)} + {humanBitrate(videoStream?.bitrate || 0)} {audioStream?.codec || '-'} - {humanFileSize(bitsToBytes(audioStream?.bitrate || 0), useSiUnits)} + {humanBitrate(audioStream?.bitrate || 0)} ); })} diff --git a/frontend/src/functions/humanBitrate.ts b/frontend/src/functions/humanBitrate.ts new file mode 100644 index 00000000..3f64df19 --- /dev/null +++ b/frontend/src/functions/humanBitrate.ts @@ -0,0 +1,44 @@ +/** + * Format a bitrate (in bits per second) as human-readable text. + * + * Bitrate values from yt-dlp come in bits per second. This helper formats + * them using SI powers of 1000 (kbps, Mbps, Gbps) — the standard for + * bit-rate display, where 1 kbps = 1000 bits per second. + * + * Note: this intentionally does NOT use the IEC "Kib/s" unit, since + * network bitrates (and yt-dlp's own `--format-sort bitrate` values) + * are universally expressed in SI decimal units in the wild. + * + * @param bitsPerSecond Bitrate in bits per second. + * @param dp Number of decimal places to display. Default 1. + * + * @return Formatted string ending in the unit (e.g. "1.5 Mbps"). + */ +function humanBitrate(bitsPerSecond: number, dp = 1): string { + if (!isFinite(bitsPerSecond) || bitsPerSecond < 0) { + return '-'; + } + if (bitsPerSecond === 0) { + return '0 bps'; + } + + const thresh = 1000; + const units = ['bps', 'kbps', 'Mbps', 'Gbps', 'Tbps']; + + if (Math.abs(bitsPerSecond) < thresh) { + return bitsPerSecond + ' bps'; + } + + let value = bitsPerSecond; + let u = 0; + const r = 10 ** dp; + + do { + value /= thresh; + ++u; + } while (Math.round(Math.abs(value) * r) / r >= thresh && u < units.length - 1); + + return value.toFixed(dp) + ' ' + units[u]; +} + +export default humanBitrate; diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx index 2a4e39f7..3d4a3ac1 100644 --- a/frontend/src/pages/Video.tsx +++ b/frontend/src/pages/Video.tsx @@ -16,6 +16,7 @@ import loadSimilarVideosById from '../api/loader/loadSimilarVideosById'; import VideoList from '../components/VideoList'; import updateWatchedState from '../api/actions/updateWatchedState'; import humanFileSize from '../functions/humanFileSize'; +import humanBitrate from '../functions/humanBitrate'; import ScrollToTopOnNavigate from '../components/ScrollToTop'; import ChannelOverview from '../components/ChannelOverview'; import deleteVideo from '../api/actions/deleteVideo'; @@ -45,7 +46,6 @@ import NotFound from './NotFound'; import { ApiResponseType } from '../functions/APIClient'; import VideoThumbnail from '../components/VideoThumbail'; import { ViewStylesEnum, ViewStylesType } from '../configuration/constants/ViewStyle'; -import bitsToBytes from '../functions/bitsToBytes'; const isInPlaylist = (videoId: string, playlist: PlaylistType) => { return playlist.playlist_entries.some(entry => { @@ -461,7 +461,7 @@ const Video = () => { return (

{capitalizeFirstLetter(stream.type)}: {stream.codec}{' '} - {humanFileSize(bitsToBytes(stream.bitrate), useSiUnits)}/s + {humanBitrate(stream.bitrate)} {stream.width && ( <> | {stream.width}x{stream.height}