diff --git a/build.js b/build.js
index cfdb0a6..817372c 100644
--- a/build.js
+++ b/build.js
@@ -111,6 +111,9 @@ const buildOptions = {
},
},
],
+ define: {
+ "process.env.NODE_ENV": JSON.stringify(env),
+ },
mainFields: ["browser", "module", "main"],
conditions: ["production", "default"],
};
@@ -119,9 +122,6 @@ if (env === "prod") {
Object.assign(buildOptions, {
minify: true,
pure: ["console.debug"], // Remove all `console.debug()` calls in production builds.
- define: {
- "process.env.NODE_ENV": '"production"',
- },
});
}
diff --git a/package-lock.json b/package-lock.json
index c7cf360..b54985b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@number-flow/react": "^0.5.10",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slider": "^1.2.0",
+ "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -1648,6 +1649,35 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
diff --git a/package.json b/package.json
index fdc9eef..ec746a8 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@number-flow/react": "^0.5.10",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slider": "^1.2.0",
+ "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/src/components/Switch.jsx b/src/components/Switch.jsx
new file mode 100644
index 0000000..667c266
--- /dev/null
+++ b/src/components/Switch.jsx
@@ -0,0 +1,15 @@
+import React from "react";
+import * as SwitchPrimitive from "@radix-ui/react-switch";
+
+export default function Switch({ checked, onCheckedChange, ...props }) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Switch.scss b/src/components/Switch.scss
new file mode 100644
index 0000000..fa445bb
--- /dev/null
+++ b/src/components/Switch.scss
@@ -0,0 +1,51 @@
+@use "~@destinygg/libstiny" as dgg;
+@use "@destinygg/libstiny/lib/utils/transitions" as transitions;
+
+.switch {
+ &__slider {
+ @include transitions.create-transition(all, default);
+ position: relative;
+ display: inline-block;
+ width: dgg.$switch-width;
+ height: dgg.$switch-height;
+ flex-shrink: 0;
+ cursor: pointer;
+ background-color: dgg.$switch-background-default-rest;
+ border-radius: dgg.$semantic-radii-pill;
+ border: none;
+ padding: 0;
+
+ &:hover {
+ background-color: dgg.$switch-background-default-hover;
+ }
+
+ &[data-state="checked"] {
+ background-color: dgg.$switch-background-active-rest;
+
+ &:hover {
+ background-color: dgg.$switch-background-active-hover;
+ }
+ }
+
+ // Override libstiny's ::before thumb in favor of the Radix UI Switch thumb element.
+ &::before {
+ content: none;
+ }
+ }
+
+ &__thumb {
+ @include transitions.create-transition(all, movement);
+ display: block;
+ position: absolute;
+ height: dgg.$switch-toggle-size;
+ width: dgg.$switch-toggle-size;
+ left: dgg.$space-1;
+ bottom: dgg.$space-1;
+ background-color: dgg.$switch-toggle-background;
+ border-radius: 50%;
+
+ &[data-state="checked"] {
+ transform: translateX(dgg.$switch-toggle-size);
+ }
+ }
+}
diff --git a/src/controls/settings/MainMenu.jsx b/src/controls/settings/MainMenu.jsx
index 7280bfe..76c59d6 100644
--- a/src/controls/settings/MainMenu.jsx
+++ b/src/controls/settings/MainMenu.jsx
@@ -1,21 +1,41 @@
import React from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { ChevronRight } from "lucide-react";
+import Switch from "../../components/Switch.jsx";
-export default function MainMenu({ onNavigateQuality, selectedQuality }) {
+export default function MainMenu({
+ onNavigateQuality,
+ selectedQuality,
+ isIvsDebug,
+ onIvsDebugChange,
+}) {
return (
- {
- e.preventDefault();
- onNavigateQuality();
- }}
- >
- Quality
-
- {selectedQuality.label}
-
-
-
+ <>
+ {
+ e.preventDefault();
+ onNavigateQuality();
+ }}
+ >
+ Quality
+
+ {selectedQuality.label}
+
+
+
+
+ {process.env.NODE_ENV === "dev" && (
+ {
+ e.preventDefault();
+ }}
+ >
+ IVS Debug
+
+
+ )}
+ >
);
}
diff --git a/src/controls/settings/SettingsButton.jsx b/src/controls/settings/SettingsButton.jsx
index bb1ad95..383eb76 100644
--- a/src/controls/settings/SettingsButton.jsx
+++ b/src/controls/settings/SettingsButton.jsx
@@ -8,6 +8,7 @@ import MainMenu from "./MainMenu.jsx";
import QualityMenu from "./QualityMenu.jsx";
import { useSettings } from "./useSettings.js";
import { useQualitySelector } from "./useQualitySelector.js";
+import { useIvsDebug } from "./useIvsDebug.js";
export default function SettingsButton({ core, container, shouldShow }) {
const {
@@ -22,6 +23,8 @@ export default function SettingsButton({ core, container, shouldShow }) {
const { selectedQuality, qualityOptions, handleQualityChange } =
useQualitySelector(core);
+ const { isIvsDebug, setIsIvsDebug } = useIvsDebug(core);
+
return (
)}
diff --git a/src/controls/settings/useIvsDebug.js b/src/controls/settings/useIvsDebug.js
new file mode 100644
index 0000000..4351a63
--- /dev/null
+++ b/src/controls/settings/useIvsDebug.js
@@ -0,0 +1,36 @@
+import { useEffect } from "react";
+import { usePreferences } from "../usePreferences";
+
+export function useIvsDebug(core) {
+ const { isIvsDebug, setIsIvsDebug } = usePreferences();
+
+ useEffect(() => {
+ if (!isIvsDebug) {
+ return;
+ }
+
+ // Sample IVS messages:
+ // { type: 12, arg: { key: "bufferedPosition", value: 2.334009 } }
+ // { type: 12, arg: { key: "statistics", value: { bitrate: 3109311, ... } } }
+ // { type: "PlayerQualityChanged", arg: { name: "720p60", ... } }
+ const handler = (event) => {
+ const data = event.data;
+ const type = data.type;
+ const detail = data.arg?.key || data.arg?.name;
+
+ if (detail === "bufferedPosition") {
+ return;
+ }
+
+ const label = detail ? `${type} (${detail})` : type;
+ console.debug(`[Kickstiny] IVS Message: ${label}`, data);
+ };
+
+ core.worker.addEventListener("message", handler);
+ return () => {
+ core.worker.removeEventListener("message", handler);
+ };
+ }, [isIvsDebug, core]);
+
+ return { isIvsDebug, setIsIvsDebug };
+}
diff --git a/src/controls/usePreferences.js b/src/controls/usePreferences.js
index 0f15294..67741fc 100644
--- a/src/controls/usePreferences.js
+++ b/src/controls/usePreferences.js
@@ -2,6 +2,7 @@ import { useState, useCallback } from "react";
const QUALITY_STORAGE_KEY = "kickstiny.preference.quality";
const VOLUME_STORAGE_KEY = "kickstiny.preference.volume";
+const IVS_DEBUG_STORAGE_KEY = "kickstiny.preference.ivsDebug";
const DEFAULT_VOLUME = 100;
@@ -25,6 +26,16 @@ export function usePreferences() {
}
});
+ const [isIvsDebug, setIsIvsDebugState] = useState(() => {
+ try {
+ const stored = window.localStorage.getItem(IVS_DEBUG_STORAGE_KEY);
+ return stored === "true";
+ } catch (err) {
+ console.log("[Kickstiny] Unable to read ivs debug preference", err);
+ return false;
+ }
+ });
+
const setSavedQuality = useCallback((value) => {
try {
window.localStorage.setItem(QUALITY_STORAGE_KEY, value);
@@ -43,10 +54,21 @@ export function usePreferences() {
}
}, []);
+ const setIsIvsDebug = useCallback((value) => {
+ try {
+ window.localStorage.setItem(IVS_DEBUG_STORAGE_KEY, value.toString());
+ setIsIvsDebugState(value);
+ } catch (err) {
+ console.log("[Kickstiny] Unable to persist ivs debug preference", err);
+ }
+ }, []);
+
return {
savedQuality,
savedVolume,
+ isIvsDebug,
setSavedQuality,
setSavedVolume,
+ setIsIvsDebug,
};
}
diff --git a/src/main.scss b/src/main.scss
index c5c3e74..a4e1b64 100644
--- a/src/main.scss
+++ b/src/main.scss
@@ -3,6 +3,7 @@
@use "./components/Tooltip.scss";
@use "./components/Dropdown.scss";
@use "./components/Slider.scss";
+@use "./components/Switch.scss";
@use "./controls/container/Container.scss";
@use "./controls/bar/ControlsBar.scss";