diff --git a/.gitignore b/.gitignore
index d337f31..0cc11f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.idea
.DS_Store
Archive.zip
-images/store
-package.zip
\ No newline at end of file
+**/images/store
+web-ext-artifacts
+node_modules
\ No newline at end of file
diff --git a/README.md b/README.md
index e3dc0a5..ffcf90e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-This is a Chrome extension that pauses Youtube videos when losing the tab/window focus by
+This is a Chrome & Firefox extension that pauses Youtube videos when losing the tab/window focus by
sending events to the player. Resumes the playback once the Youtube tab/window is back in focus.
Also listens for computer lock events and when the video goes out of viewport
@@ -10,18 +10,28 @@ Features some useful keyboard shortcuts to control videos in the window.
## Installing
-**From chrome web store**
+**From web store**
-https://chrome.google.com/webstore/detail/pbehcnkdmffkllmlfjpblpjhflnafioo/
+Chrome: https://chrome.google.com/webstore/detail/pbehcnkdmffkllmlfjpblpjhflnafioo/
+Firefox: https://addons.mozilla.org/en-US/firefox/addon/youtube-auto-pause/
-**Manually**
+**Manually (chrome)**
1. Clone the repository
2. Start chrome
-3. Go to chrome://extensions
+3. Go to `chrome://extensions`
4. Enable developer mode
5. Click on "Load unpacked"
-6. Select the cloned folder
+6. Select the `chrome` directory in the repository
+
+**Manually (firefox)**
+
+1. Clone the repository
+2. Start firefox
+3. Go to `about:debugging`
+4. Click on "This Firefox"
+5. Click on "Load Temporary Add-on"
+6. Select the `firefox` directory in the repository
## Supported services
@@ -37,6 +47,15 @@ Please feel free to contribute with pull requests or by creating issues. In case
the extension does not work, please also list all other extensions you have
enabled as this might conflict with other extensions.
-## TODO
+### Running and developing
+
+To run the extension, either use `npm run start:chrome` or `npm run start:firefox` to start
+specific browser. This will open a new browser window with the extension enabled.
+
+If you want to debug the extension further, use `npm run debug:chrome` and `npm run debug:firefox`
+respectively. This will open the browser's developer tools that you can use to debug the extension.
+
+### Building
-- Allow selecting video services
+Run `npm install` to fetch necessary dependencies. Then run `npm run build` to build
+the extension packages under `web-ext-artifacts`.
diff --git a/images/icon_1024.png b/chrome/images/icon_1024.png
similarity index 100%
rename from images/icon_1024.png
rename to chrome/images/icon_1024.png
diff --git a/images/icon_128.png b/chrome/images/icon_128.png
similarity index 100%
rename from images/icon_128.png
rename to chrome/images/icon_128.png
diff --git a/images/icon_16.png b/chrome/images/icon_16.png
similarity index 100%
rename from images/icon_16.png
rename to chrome/images/icon_16.png
diff --git a/images/icon_32.png b/chrome/images/icon_32.png
similarity index 100%
rename from images/icon_32.png
rename to chrome/images/icon_32.png
diff --git a/images/icon_64.png b/chrome/images/icon_64.png
similarity index 100%
rename from images/icon_64.png
rename to chrome/images/icon_64.png
diff --git a/manifest.json b/chrome/manifest.json
similarity index 100%
rename from manifest.json
rename to chrome/manifest.json
diff --git a/options.html b/chrome/options.html
similarity index 100%
rename from options.html
rename to chrome/options.html
diff --git a/options.js b/chrome/options.js
similarity index 96%
rename from options.js
rename to chrome/options.js
index 8498209..397b42c 100644
--- a/options.js
+++ b/chrome/options.js
@@ -76,7 +76,7 @@ for (host of hosts) {
label.appendChild(checkbox);
const span = document.createElement("span");
span.className = "label-text";
- span.innerHTML = formatHostName(host);
+ span.textContent = formatHostName(host);
label.appendChild(span);
hostsDiv.appendChild(label);
checkbox.addEventListener("change", save_options);
@@ -84,7 +84,7 @@ for (host of hosts) {
// Show version in the options window
const version = document.getElementById("version");
-version.innerHTML = "v" + chrome.runtime.getManifest().version;
+version.textContent = "v" + chrome.runtime.getManifest().version;
// Restore options on load and when they change in the store
document.addEventListener("DOMContentLoaded", restore_options);
diff --git a/yt.js b/chrome/yt.js
similarity index 95%
rename from yt.js
rename to chrome/yt.js
index 8759ca7..333f4f3 100644
--- a/yt.js
+++ b/chrome/yt.js
@@ -20,8 +20,8 @@ let options = {
debugMode: false,
};
-var hosts = chrome.runtime.getManifest().host_permissions;
-for (var host of hosts) {
+const hosts = chrome.runtime.getManifest().host_permissions;
+for (const host of hosts) {
options[host] = true;
}
@@ -75,7 +75,7 @@ function isEnabledForTab(tab) {
});
if (optionKey) {
- return options[optionKey];
+ return !!options[optionKey];
}
return false;
@@ -144,7 +144,7 @@ function toggle_mute(tab) {
}
// Listen options changes
-chrome.storage.onChanged.addListener(async function (changes, namespace) {
+chrome.storage.onChanged.addListener(async function (changes) {
enabledTabs = [];
for (const key in changes) {
debugLog(
@@ -154,14 +154,17 @@ chrome.storage.onChanged.addListener(async function (changes, namespace) {
}
if ("disabled" in changes) {
- refresh_settings();
- const tabs = await chrome.tabs.query({ active: true });
+ const tabs = await browser.tabs.query({ active: true });
+ if (!options.disabled) {
+ debugLog(`Extension enabled, resuming active tabs`);
+ } else {
+ debugLog(`Extension disabled, stopping active tabs`);
+ }
+
for (let i = 0; i < tabs.length; i++) {
- if (options.disabled === true) {
- debugLog(`Extension enabled, resuming active tabs`);
+ if (!options.disabled) {
resume(tabs[i]);
} else {
- debugLog(`Extension disabled, stopping active tabs`);
stop(tabs[i]);
}
}
diff --git a/yt_auto_pause.js b/chrome/yt_auto_pause.js
similarity index 100%
rename from yt_auto_pause.js
rename to chrome/yt_auto_pause.js
diff --git a/create_package.sh b/create_package.sh
deleted file mode 100755
index 95e8def..0000000
--- a/create_package.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-zip -r package.zip . \
- -x ".*" \
- -x "*/.*" \
- -x "create_package.sh" \
- -x "package.zip" \
- -x ".DS_Store" \
- -x "README.md" \
- -x "LICENSE" \
- -x "yt_auto_pause.png"
\ No newline at end of file
diff --git a/firefox/images/icon_1024.png b/firefox/images/icon_1024.png
new file mode 100644
index 0000000..a9ff636
Binary files /dev/null and b/firefox/images/icon_1024.png differ
diff --git a/firefox/images/icon_128.png b/firefox/images/icon_128.png
new file mode 100644
index 0000000..426e92a
Binary files /dev/null and b/firefox/images/icon_128.png differ
diff --git a/firefox/images/icon_16.png b/firefox/images/icon_16.png
new file mode 100644
index 0000000..e070f36
Binary files /dev/null and b/firefox/images/icon_16.png differ
diff --git a/firefox/images/icon_32.png b/firefox/images/icon_32.png
new file mode 100644
index 0000000..292c3fb
Binary files /dev/null and b/firefox/images/icon_32.png differ
diff --git a/firefox/images/icon_64.png b/firefox/images/icon_64.png
new file mode 100644
index 0000000..0e92765
Binary files /dev/null and b/firefox/images/icon_64.png differ
diff --git a/firefox/manifest.json b/firefox/manifest.json
new file mode 100644
index 0000000..62ff043
--- /dev/null
+++ b/firefox/manifest.json
@@ -0,0 +1,84 @@
+{
+ "name": "YouTube Auto Pause",
+ "version": "1.7.0",
+ "description": "Stops YouTube (+ other video services) on tab unfocus and continues on focus",
+ "permissions": ["tabs", "storage", "activeTab", "scripting", "idle"],
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "drodil@youtube_auto_pause",
+ "strict_min_version": "127.0"
+ }
+ },
+ "host_permissions": [
+ "https://*.youtube.com/*",
+ "https://*.vimeo.com/*",
+ "https://*.netflix.com/watch/*",
+ "https://*.youtubekids.com/*",
+ "https://*.primevideo.com/*",
+ "https://*.hbomax.com/*",
+ "https://*.disneyplus.com/*",
+ "https://*.twitch.tv/*",
+ "https://*.udacity.com/*"
+ ],
+ "homepage_url": "https://github.com/drodil/youtube_auto_pause",
+ "options_ui": {
+ "page": "options.html",
+ "open_in_tab": false
+ },
+ "icons": {
+ "16": "images/icon_16.png",
+ "32": "images/icon_32.png",
+ "64": "images/icon_64.png",
+ "128": "images/icon_128.png"
+ },
+ "action": {
+ "default_popup": "options.html",
+ "default_icon": {
+ "16": "images/icon_16.png",
+ "32": "images/icon_32.png",
+ "64": "images/icon_64.png",
+ "128": "images/icon_128.png"
+ }
+ },
+ "background": {
+ "scripts": ["yt.js"]
+ },
+ "externally_connectable": {
+ "ids": ["*"],
+ "matches": [
+ "https://*.youtube.com/*",
+ "https://*.youtubekids.com/*",
+ "https://*.vimeo.com/*",
+ "https://*.netflix.com/watch/*",
+ "https://*.primevideo.com/*",
+ "https://*.hbomax.com/*",
+ "https://*.disneyplus.com/*",
+ "https://*.twitch.tv/*",
+ "https://*.udacity.com/*"
+ ]
+ },
+ "commands": {
+ "toggle-extension": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+K",
+ "mac": "Command+Shift+K"
+ },
+ "description": "Toggle auto pause/resume"
+ },
+ "toggle-play": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+Space",
+ "mac": "Command+Shift+Space"
+ },
+ "description": "Toggle play/pause"
+ },
+ "toggle_mute": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+O",
+ "mac": "Command+Shift+O"
+ },
+ "description": "Toggle mute"
+ }
+ },
+ "manifest_version": 3
+}
diff --git a/firefox/options.html b/firefox/options.html
new file mode 100644
index 0000000..033328a
--- /dev/null
+++ b/firefox/options.html
@@ -0,0 +1,139 @@
+
+
+
+
+ Youtube Auto Pause options
+
+
+
+
+
+
+
+
+
diff --git a/firefox/options.js b/firefox/options.js
new file mode 100644
index 0000000..a8a334a
--- /dev/null
+++ b/firefox/options.js
@@ -0,0 +1,118 @@
+const options = {
+ autopause: true,
+ autoresume: true,
+ scrollpause: false,
+ lockpause: true,
+ lockresume: true,
+ focuspause: false,
+ focusresume: false,
+ disabled: false,
+ cursorTracking: false,
+ manualPause: true,
+ debugMode: false,
+};
+
+const hosts = browser.runtime.getManifest().host_permissions;
+for (const host of hosts) {
+ options[host] = true;
+}
+
+// Saves options to browser.storage
+function save_options() {
+ const storage = {};
+
+ for (const option in options) {
+ storage[option] = document.getElementById(option).checked;
+ }
+
+ browser.storage.sync.set(storage, function () {});
+}
+
+// Restores options from browser.storage
+function restore_options() {
+ browser.storage.sync.get(options, function (items) {
+ for (const opt in items) {
+ document.getElementById(opt).checked = items[opt];
+ }
+
+ for (const option in options) {
+ document.getElementById(option).disabled = items.disabled;
+ if (items.disabled) {
+ document.getElementById("disabled").disabled = false;
+ }
+ }
+ });
+}
+
+// Show shortcuts in the options window
+browser.commands.getAll(function (commands) {
+ const hotkeysDiv = document.getElementById("hotkeys");
+ for (let i = 0; i < commands.length; i++) {
+ if (
+ commands[i].shortcut.length === 0 ||
+ commands[i].description.length === 0
+ ) {
+ continue;
+ }
+ const tag = document.createElement("p");
+ const text = document.createTextNode(
+ commands[i].shortcut + " - " + commands[i].description
+ );
+ tag.appendChild(text);
+ hotkeysDiv.appendChild(tag);
+ }
+});
+
+function formatHostName(hostname) {
+ return hostname.replace("https://", "").split("/")[0].replaceAll("*.", "");
+}
+
+const hostsDiv = document.getElementById("hosts");
+for (host of hosts) {
+ const label = document.createElement("label");
+ const checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.id = host;
+ label.appendChild(checkbox);
+ const span = document.createElement("span");
+ span.className = "label-text";
+ span.textContent = formatHostName(host);
+ label.appendChild(span);
+ hostsDiv.appendChild(label);
+ checkbox.addEventListener("change", save_options);
+}
+
+// Show version in the options window
+const version = document.getElementById("version");
+version.textContent = "v" + browser.runtime.getManifest().version;
+
+// Restore options on load and when they change in the store
+document.addEventListener("DOMContentLoaded", restore_options);
+browser.storage.onChanged.addListener(function (_changes, _namespace) {
+ restore_options();
+});
+
+// Listen to changes of options
+for (const option in options) {
+ document.getElementById(option).addEventListener("change", save_options);
+}
+
+const coll = document.getElementsByClassName("collapsible_button");
+let i;
+
+for (i = 0; i < coll.length; i++) {
+ coll[i].addEventListener("click", function () {
+ this.classList.toggle("active");
+ const content = this.nextElementSibling;
+ if (content.style.maxHeight) {
+ content.style.maxHeight = null;
+ } else {
+ content.style.maxHeight = content.scrollHeight + "px";
+ }
+ });
+}
+
+// Add event listener for the cursor tracking option
+document
+ .getElementById("cursorTracking")
+ .addEventListener("change", save_options);
diff --git a/firefox/yt.js b/firefox/yt.js
new file mode 100644
index 0000000..fbcf2da
--- /dev/null
+++ b/firefox/yt.js
@@ -0,0 +1,393 @@
+// Previous tab and window numbers
+let previous_tab = -1;
+let previous_window = browser.windows.WINDOW_ID_NONE;
+let executedTabs = [];
+let enabledTabs = [];
+
+// Computer state
+let state = "active";
+// Default options
+let options = {
+ autopause: true,
+ autoresume: true,
+ scrollpause: false,
+ lockpause: true,
+ lockresume: true,
+ focuspause: false,
+ focusresume: false,
+ disabled: false,
+ cursorTracking: false,
+ debugMode: false,
+};
+
+const hosts = browser.runtime.getManifest().host_permissions;
+for (const host of hosts) {
+ options[host] = true;
+}
+
+// Initialize settings from storage
+setTimeout(() => {
+ refresh_settings();
+}, 100);
+
+function refresh_settings() {
+ browser.storage.sync.get(Object.keys(options), function (result) {
+ options = Object.assign(options, result);
+ if (options.disabled === true) {
+ options.autopause = false;
+ options.autoresume = false;
+ options.scrollpause = false;
+ options.lockpause = false;
+ options.lockresume = false;
+ options.focuspause = false;
+ options.focusresume = false;
+ options.cursorTracking = false;
+ options.debugMode = false;
+ for (var host of hosts) {
+ options[host] = false;
+ }
+ }
+ enabledTabs = [];
+ });
+}
+
+function debugLog(message) {
+ if (options.debugMode) {
+ console.log(`YouTube auto pause: ${message}`);
+ }
+}
+
+function isEnabledForTab(tab) {
+ if (!tab || !tab.url) {
+ return false;
+ }
+
+ if (enabledTabs.includes(tab.id)) {
+ return true;
+ }
+
+ const optionKey = Object.keys(options).find((option) => {
+ if (!option.startsWith("http")) {
+ return false;
+ }
+ const reg = option
+ .replace(/[.+?^${}()|/[\]\\]/g, "\\$&")
+ .replace("*", ".*");
+ return new RegExp(reg).test(tab.url);
+ });
+
+ if (optionKey) {
+ return !!options[optionKey];
+ }
+
+ return false;
+}
+
+async function injectScript(tab) {
+ if (executedTabs.includes(tab.id) || !isEnabledForTab(tab)) {
+ return;
+ }
+
+ debugLog(`Injecting script into tab ${tab.id} with url ${tab.url}`);
+ try {
+ await browser.scripting.executeScript({
+ target: { tabId: tab.id },
+ files: ["yt_auto_pause.js"],
+ });
+ executedTabs.push(tab.id);
+ } catch (e) {
+ debugLog(e);
+ }
+}
+
+// Functionality to send messages to tabs
+function sendMessage(tab, message) {
+ if (!browser.runtime?.id || !isEnabledForTab(tab)) {
+ debugLog(`Not sending message`);
+ return;
+ }
+
+ if (browser.runtime.lastError) {
+ console.error(
+ `YouTube Autopause error: ${browser.runtime.lastError.toString()}`
+ );
+ return;
+ }
+
+ debugLog(`Sending message ${JSON.stringify(message)} to tab ${tab.id}`);
+
+ browser.tabs.sendMessage(tab.id, message, {}, function () {
+ void browser.runtime.lastError;
+ });
+}
+
+// Media conrol functions
+function stop(tab) {
+ sendMessage(tab, { action: "stop" });
+}
+
+function resume(tab) {
+ sendMessage(tab, { action: "resume" });
+}
+
+function toggle(tab) {
+ sendMessage(tab, { action: "toggle" });
+}
+
+function mute(tab) {
+ sendMessage(tab, { action: "mute" });
+}
+
+function unmute(tab) {
+ sendMessage(tab, { action: "unmute" });
+}
+
+function toggle_mute(tab) {
+ sendMessage(tab, { action: "toggle_mute" });
+}
+
+// Listen options changes
+browser.storage.onChanged.addListener(async function (changes) {
+ enabledTabs = [];
+ for (const key in changes) {
+ debugLog(
+ `Settings changed for key ${key} from ${changes[key].oldValue} to ${changes[key].newValue}`
+ );
+ options[key] = changes[key].newValue;
+ }
+
+ if ("disabled" in changes) {
+ const tabs = await browser.tabs.query({ active: true });
+ if (!options.disabled) {
+ debugLog(`Extension enabled, resuming active tabs`);
+ } else {
+ debugLog(`Extension disabled, stopping active tabs`);
+ }
+
+ for (let i = 0; i < tabs.length; i++) {
+ if (!options.disabled) {
+ resume(tabs[i]);
+ } else {
+ stop(tabs[i]);
+ }
+ }
+ }
+});
+
+// Tab change listener
+browser.tabs.onActivated.addListener(function (info) {
+ if (previous_window !== info.windowId) {
+ return;
+ }
+
+ browser.tabs.get(info.tabId, async function (tab) {
+ if (!isEnabledForTab(tab) || previous_tab === info.tabId) {
+ return;
+ }
+
+ await injectScript(tab);
+
+ if (options.autopause && previous_tab !== -1) {
+ debugLog(`Tab changed, stopping video from tab ${previous_tab}`);
+ browser.tabs.get(previous_tab, function (prev) {
+ stop(prev);
+ });
+ }
+
+ if (options.autoresume) {
+ debugLog(`Tab changed, resuming video from tab ${info.tabId}`);
+ resume(tab);
+ }
+
+ previous_tab = info.tabId;
+ });
+});
+
+// Tab update listener
+browser.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) {
+ if (!isEnabledForTab(tab)) {
+ return;
+ }
+
+ await injectScript(tab);
+
+ if (
+ "status" in changeInfo &&
+ changeInfo.status === "complete" &&
+ !tab.active
+ ) {
+ debugLog(
+ `Tab updated, stopping video in tab ${tabId} with status ${changeInfo.status}, active ${tab.active}`
+ );
+ stop(tab);
+ }
+});
+
+browser.tabs.onRemoved.addListener(function (tabId) {
+ if (enabledTabs.includes(tabId)) {
+ debugLog(`Tab removed, removing tab ${tabId} from enabled tabs`);
+ enabledTabs = enabledTabs.filter((e) => e !== tabId);
+ }
+ if (executedTabs.includes(tabId)) {
+ debugLog(`Tab removed, removing tab ${tabId} from executed tabs`);
+ executedTabs = executedTabs.filter((e) => e !== tabId);
+ }
+});
+
+// Window focus listener
+browser.windows.onFocusChanged.addListener(async function (window) {
+ if (window !== previous_window) {
+ if (options.focuspause && state !== "locked") {
+ const tabsStop = await browser.tabs.query({ windowId: previous_window });
+ debugLog(`Window changed, stopping videos in window ${window}`);
+ for (let i = 0; i < tabsStop.length; i++) {
+ if (!isEnabledForTab(tabsStop[i])) {
+ continue;
+ }
+ stop(tabsStop[i]);
+ }
+ }
+
+ if (options.focusresume && window !== browser.windows.WINDOW_ID_NONE) {
+ const tabsResume = await browser.tabs.query({ windowId: window });
+ debugLog(`Window changed, resuming videos in window ${window}`);
+ for (let i = 0; i < tabsResume.length; i++) {
+ if (!isEnabledForTab(tabsResume[i])) {
+ continue;
+ }
+ if (!tabsResume[i].active && options.autopause) {
+ continue;
+ }
+ resume(tabsResume[i]);
+ }
+ }
+
+ previous_window = window;
+ }
+});
+
+// Message listener for messages from tabs
+browser.runtime.onMessage.addListener(async function (
+ request,
+ sender,
+ sendResponse
+) {
+ if (!isEnabledForTab(sender.tab) || browser.runtime.lastError) {
+ return true;
+ }
+
+ if ("minimized" in request) {
+ if (request.minimized && options.autopause) {
+ debugLog(`Window minimized, stopping videos in tab ${sender.tab.id}`);
+ stop(sender.tab);
+ } else if (!request.minimized && options.autoresume) {
+ debugLog(`Window returned, resuming videos in tab ${sender.tab.id}`);
+ resume(sender.tab);
+ }
+ }
+
+ if ("visible" in request && options.scrollpause) {
+ if (!request.visible) {
+ debugLog(
+ `Window is not visible, stopping videos in tab ${sender.tab.id}`
+ );
+ stop(sender.tab);
+ } else {
+ debugLog(`Window is visible, resuming videos in tab ${sender.tab.id}`);
+ resume(sender.tab);
+ }
+ }
+
+ await browser.storage.sync.get("cursorTracking", function (result) {
+ if (result.cursorTracking && "cursorNearEdge" in request) {
+ // Handle cursor near edge changes
+ if (request.cursorNearEdge && options.autopause) {
+ debugLog(
+ `Nearing window edge, stopping videos in tab ${sender.tab.id}`
+ );
+ stop(sender.tab);
+ } else if (!request.cursorNearEdge && options.autoresume) {
+ debugLog(`Returned to window, resuming videos in tab ${sender.tab.id}`);
+ resume(sender.tab);
+ }
+ }
+ });
+
+ sendResponse({});
+ return true;
+});
+
+// Listener for keyboard shortcuts
+browser.commands.onCommand.addListener(async (command) => {
+ if (command === "toggle-extension") {
+ options.disabled = !options.disabled;
+ debugLog(
+ `Toggle extension command received, extension state ${options.disabled}`
+ );
+ browser.storage.sync.set({ disabled: options.disabled });
+ refresh_settings();
+ } else if (command === "toggle-play") {
+ debugLog(
+ `Toggle play command received, toggling play for all tabs in current window`
+ );
+ const tabs = await browser.tabs.query({ currentWindow: true });
+ for (let i = 0; i < tabs.length; i++) {
+ if (!isEnabledForTab(tabs[i])) {
+ continue;
+ }
+ toggle(tabs[i]);
+ }
+ } else if (command === "toggle_mute") {
+ debugLog(
+ `Toggle mute command received, toggling mute for all tabs in current window`
+ );
+ const tabs = await browser.tabs.query({ currentWindow: true });
+ for (let i = 0; i < tabs.length; i++) {
+ if (!isEnabledForTab(tabs[i])) {
+ continue;
+ }
+ toggle_mute(tabs[i]);
+ }
+ }
+});
+
+// Listener for computer idle/locked/active
+browser.idle.onStateChanged.addListener(async function (s) {
+ state = s;
+ const tabs = await browser.tabs.query({ active: true });
+
+ for (let i = 0; i < tabs.length; i++) {
+ if (!isEnabledForTab(tabs[i])) {
+ continue;
+ }
+
+ if (state === "locked" && options.lockpause) {
+ debugLog(`Computer locked, stopping all videos`);
+ stop(tabs[i]);
+ } else if (state !== "locked" && options.lockresume) {
+ if (!tabs[i].active && options.autopause) {
+ continue;
+ }
+ debugLog(`Computer unlocked, resuming videos`);
+ resume(tabs[i]);
+ }
+ }
+});
+
+browser.windows.onCreated.addListener(async function (window) {
+ const tabs = await browser.tabs.query({ windowId: window.id });
+ debugLog(`Window created, stopping all non active videos`);
+ for (let i = 0; i < tabs.length; i++) {
+ if (!isEnabledForTab(tabs[i])) {
+ continue;
+ }
+
+ await injectScript(tabs[i]);
+
+ if (tabs[i].active && options.autoresume) {
+ resume(tabs[i]);
+ } else if (options.autopause) {
+ stop(tabs[i]);
+ }
+ }
+});
diff --git a/firefox/yt_auto_pause.js b/firefox/yt_auto_pause.js
new file mode 100644
index 0000000..3d5e907
--- /dev/null
+++ b/firefox/yt_auto_pause.js
@@ -0,0 +1,245 @@
+if (window.ytAutoPauseInjected !== true) {
+ window.ytAutoPauseInjected = true;
+ let manuallyPaused = false;
+ let automaticallyPaused = false;
+
+ let options = {
+ autopause: true,
+ autoresume: true,
+ scrollpause: false,
+ lockpause: true,
+ lockresume: true,
+ focuspause: false,
+ focusresume: false,
+ disabled: false,
+ cursorTracking: false,
+ manualPause: true,
+ debugMode: false,
+ };
+
+ function debugLog(message) {
+ if (options.debugMode) {
+ console.log(`YouTube auto pause: ${message}`);
+ }
+ }
+
+ // Initialize settings from storage
+ refresh_settings();
+
+ function refresh_settings() {
+ browser.storage.sync.get(Object.keys(options), function (result) {
+ options = Object.assign(options, result);
+ if (options.disabled === true) {
+ options.autopause = false;
+ options.autoresume = false;
+ options.scrollpause = false;
+ options.lockpause = false;
+ options.lockresume = false;
+ options.focuspause = false;
+ options.focusresume = false;
+ options.cursorTracking = false;
+ options.debugMode = false;
+ for (var host of hosts) {
+ options[host] = false;
+ }
+ }
+ });
+ }
+
+ browser.storage.onChanged.addListener(async function (changes, namespace) {
+ for (const key in changes) {
+ debugLog(
+ `Settings changed for key ${key} from ${changes[key].oldValue} to ${changes[key].newValue}`
+ );
+ options[key] = changes[key].newValue;
+ }
+
+ if (!options.manualPause) {
+ manuallyPaused = false;
+ automaticallyPaused = true;
+ }
+
+ if ("disabled" in changes) {
+ refresh_settings();
+ }
+ });
+
+ // Function to check if the cursor is near the edge of the window
+ function isCursorNearEdge(event) {
+ const threshold = 50; // pixels from the edge
+ return (
+ event.clientX < threshold ||
+ event.clientX > window.innerWidth - threshold ||
+ event.clientY < threshold ||
+ event.clientY > window.innerHeight - threshold
+ );
+ }
+
+ let cursorNearEdgeTimeout;
+
+ // Listen for mousemove events
+ window.addEventListener("mousemove", async function (event) {
+ if (!options.cursorTracking) {
+ return;
+ }
+ if (isCursorNearEdge(event)) {
+ // If the cursor is near the edge, set a timeout
+ if (!cursorNearEdgeTimeout) {
+ cursorNearEdgeTimeout = setTimeout(function () {
+ debugLog(`Cursor near window edge, sending message`);
+ sendMessage({ cursorNearEdge: true });
+ cursorNearEdgeTimeout = null;
+ }, 200); // Wait for 1 second to infer user intention
+ }
+ } else {
+ // If the cursor moves away from the edge, clear the timeout
+ clearTimeout(cursorNearEdgeTimeout);
+ cursorNearEdgeTimeout = null;
+ debugLog(`Cursor not near window edge, sending message`);
+ sendMessage({ cursorNearEdge: false });
+ }
+ });
+
+ // Existing code...
+
+ // Send message to service worker
+ function sendMessage(message) {
+ if (!browser.runtime?.id) {
+ return;
+ }
+
+ if (browser.runtime.lastError) {
+ console.error(
+ `Youtube Autopause error: ${browser.runtime.lastError.toString()}`
+ );
+ return;
+ }
+
+ debugLog(`Sending message ${JSON.stringify(message)}`);
+
+ browser.runtime.sendMessage(message, function () {
+ void browser.runtime.lastError;
+ });
+ }
+
+ // Listen to visibilitychange event of the page
+ document.addEventListener(
+ "visibilitychange",
+ function () {
+ if (document.hidden !== undefined) {
+ debugLog(`Document hidden, sending minimized ${document.hidden}`);
+ sendMessage({ minimized: document.hidden });
+ }
+ },
+ false
+ );
+
+ // Listen media commands from the service worker
+ browser.runtime.onMessage.addListener(async function (
+ request,
+ sender,
+ sendResponse
+ ) {
+ if (!("action" in request)) {
+ return false;
+ }
+ debugLog(`Received message: ${JSON.stringify(request)}`);
+
+ const videoElements = document.getElementsByTagName("video");
+ const iframeElements = document.getElementsByTagName("iframe");
+
+ for (let i = 0; i < iframeElements.length; i++) {
+ const iframe = iframeElements[i];
+ try {
+ if (request.action === "stop") {
+ iframe.contentWindow.postMessage(
+ JSON.stringify({ event: "command", func: "pauseVideo" }),
+ "*"
+ );
+ } else if (request.action === "resume") {
+ iframe.contentWindow.postMessage(
+ JSON.stringify({ event: "command", func: "playVideo" }),
+ "*"
+ );
+ }
+ } catch (e) {
+ debugLog(e);
+ }
+ }
+
+ for (let i = 0; i < videoElements.length; i++) {
+ try {
+ if (request.action === "stop" && !manuallyPaused) {
+ automaticallyPaused = true;
+ videoElements[i].pause();
+ } else if (
+ request.action === "resume" &&
+ videoElements[i].paused &&
+ !manuallyPaused
+ ) {
+ automaticallyPaused = false;
+ if (!videoElements[i].ended) {
+ await videoElements[i].play();
+ }
+ } else if (request.action === "toggle_mute") {
+ videoElements[i].muted = !videoElements[i].muted;
+ } else if (request.action === "mute") {
+ videoElements[i].muted = true;
+ } else if (request.action === "unmute") {
+ videoElements[i].muted = false;
+ } else if (request.action === "toggle") {
+ if (videoElements[i].paused && !manuallyPaused) {
+ if (!videoElements[i].ended) {
+ await videoElements[i].play();
+ }
+ automaticallyPaused = false;
+ } else if (!manuallyPaused) {
+ videoElements[i].pause();
+ automaticallyPaused = true;
+ }
+ }
+ } catch (e) {
+ debugLog(e);
+ }
+ }
+ sendResponse({});
+ return true;
+ });
+
+ // Intersection observer for the video elements in page
+ // can be used to determine when video goes out of viewport
+ const intersection_observer = new IntersectionObserver(
+ function (entries) {
+ if (!options.scrollpause) {
+ return;
+ }
+ if (entries[0].isIntersecting === true) {
+ debugLog(`Video not anymore in viewport`);
+ sendMessage({ visible: true });
+ } else {
+ debugLog(`Video in viewport`);
+ sendMessage({ visible: false });
+ }
+ },
+ { threshold: [0] }
+ );
+
+ // Start observing video elements
+ let videoElements = document.getElementsByTagName("video");
+ for (let i = 0; i < videoElements.length; i++) {
+ intersection_observer.observe(videoElements[i]);
+ videoElements[i].addEventListener("pause", async (_e) => {
+ if (!automaticallyPaused && options.manualPause) {
+ debugLog(`Manually paused video`);
+ manuallyPaused = true;
+ automaticallyPaused = false;
+ }
+ });
+ videoElements[i].addEventListener("play", (_e) => {
+ if (options.manualPause) {
+ debugLog(`Manually resumed video`);
+ manuallyPaused = false;
+ }
+ });
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..855152a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@drodil/youtube_auto_pause",
+ "version": "1.7.0",
+ "homepage": "https://github.com/drodil/youtube_auto_pause",
+ "bugs": {
+ "url": "https://github.com/drodil/youtube_auto_pause/issues",
+ "email": "heiccih@gmail.com"
+ },
+ "private": true,
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/drodil/youtube_auto_pause.git"
+ },
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/drodil"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/drodil"
+ }
+ ],
+ "scripts": {
+ "build": "npm run build:chrome && npm run build:firefox",
+ "lint": "web-ext lint --source-dir ./firefox/",
+ "start:chrome": "web-ext run --source-dir ./chrome/ --target chromium",
+ "debug:chrome": "web-ext run --devtools --source-dir ./chrome/ --target chromium",
+ "build:chrome": "web-ext build --overwrite-dest --source-dir ./chrome/ --filename chrome.zip",
+ "start:firefox": "web-ext run --source-dir ./firefox/",
+ "debug:firefox": "web-ext run --devtools --source-dir ./firefox/",
+ "build:firefox": "web-ext build --overwrite-dest --source-dir ./firefox/ --filename firefox.zip"
+ },
+ "devDependencies": {
+ "web-ext": "^8.2.0"
+ }
+}