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}