Skip to content

Instantly share code, notes, and snippets.

@camjac251
Last active October 29, 2025 21:48
Show Gist options
  • Save camjac251/8dcb2603d516da572a57bc0be0a5ab7c to your computer and use it in GitHub Desktop.
Save camjac251/8dcb2603d516da572a57bc0be0a5ab7c to your computer and use it in GitHub Desktop.
Sora Post Auto Unmute userscript
// ==UserScript==
// @name Sora Post Auto Unmute
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Auto-unmutes Sora posts on load and pauses videos when tab is unfocused.
// @match https://sora.chatgpt.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
if (!location.pathname.startsWith("/p/") && !location.pathname.startsWith("/d/")) {
return;
}
// CSS selectors that match the global mute toggle rendered on Sora post pages.
const BUTTON_SELECTORS = [
"button.absolute.right-5.top-5",
'button svg path[d^="M3.983"]', // Muted state SVG
'button svg path[d^="M9.751"]', // Unmuted state SVG
];
// Full pointer/mouse sequence so the click looks like genuine user input.
const CLICK_SEQUENCE = [
"pointerdown",
"mousedown",
"pointerup",
"mouseup",
"click",
];
// Flip once per navigation; Sora keeps the state within the tab afterwards.
let toggled = false;
let lastPath = location.pathname;
const debug = false;
const log = debug
? (...args) => console.log("[Sora Auto Unmute]", ...args)
: () => {};
const resolveToggleButton = () => {
for (const pattern of BUTTON_SELECTORS) {
const match = document.querySelector(pattern);
if (!match) continue;
return match.tagName === "BUTTON" ? match : match.closest("button");
}
return null;
};
// Returns true if at least one video is muted or volume-suppressed; null while waiting for first video.
const needsUnmute = () => {
const videos = Array.from(document.querySelectorAll("video"));
if (!videos.length) return null;
return videos.some(
(video) =>
video.muted || video.volume === 0 || video.hasAttribute("muted")
);
};
const performClick = (button) => {
if (!button) return;
CLICK_SEQUENCE.forEach((type) => {
button.dispatchEvent(
new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
buttons: 1,
})
);
});
log("Triggered global mute toggle");
};
const getVideos = () => Array.from(document.querySelectorAll("video"));
const pauseVideos = () => {
const videos = getVideos();
videos.forEach((video) => {
if (!video.paused) {
video.pause();
log("Paused video");
}
});
};
const resumeVideos = () => {
const videos = getVideos();
videos.forEach((video) => {
if (video.paused) {
video.play().catch((err) => log("Resume failed:", err));
log("Resumed video");
}
});
};
const tryUnmute = () => {
// Reset on SPA navigation
if (location.pathname !== lastPath) {
toggled = false;
lastPath = location.pathname;
log("Path changed, reset toggled flag");
}
if (toggled) return;
const videoNeedsUnmute = needsUnmute();
if (videoNeedsUnmute === null) {
log("Waiting for video element");
return;
}
if (!videoNeedsUnmute) {
toggled = true;
log("Video already unmuted");
return;
}
const button = resolveToggleButton();
if (!button) {
log("Mute button not yet present");
return;
}
performClick(button);
toggled = true;
};
const observer = new MutationObserver(() => {
if (document.hidden) return;
tryUnmute();
if (toggled) observer.disconnect();
});
const start = () => {
if (!document.body) return;
observer.observe(document.body, { childList: true, subtree: true });
tryUnmute();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start, { once: true });
} else {
start();
}
window.addEventListener("pageshow", tryUnmute, { passive: true });
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
pauseVideos();
} else {
tryUnmute();
resumeVideos();
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment