fix(frontend): display audio/video bitrate in correct SI units

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.
This commit is contained in:
Ian Schwartz 2026-06-20 22:35:01 -05:00
parent 5477ca97f1
commit 2becba47c3
3 changed files with 49 additions and 5 deletions

View File

@ -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) => {
<td>{`${videoStream?.width || '-'}x${videoStream?.height || '-'}`}</td>
<td>{humanFileSize(media_size, useSiUnits)}</td>
<td>{videoStream?.codec || '-'}</td>
<td>{humanFileSize(bitsToBytes(videoStream?.bitrate || 0), useSiUnits)}</td>
<td>{humanBitrate(videoStream?.bitrate || 0)}</td>
<td>{audioStream?.codec || '-'}</td>
<td>{humanFileSize(bitsToBytes(audioStream?.bitrate || 0), useSiUnits)}</td>
<td>{humanBitrate(audioStream?.bitrate || 0)}</td>
</tr>
);
})}

View File

@ -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;

View File

@ -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 (
<p key={stream.index}>
{capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
{humanFileSize(bitsToBytes(stream.bitrate), useSiUnits)}/s
{humanBitrate(stream.bitrate)}
{stream.width && (
<>
<span className="space-carrot">|</span> {stream.width}x{stream.height}