Last active
October 29, 2025 21:48
-
-
Save camjac251/8dcb2603d516da572a57bc0be0a5ab7c to your computer and use it in GitHub Desktop.
Sora Post Auto Unmute userscript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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