Last active
April 8, 2025 09:54
-
-
Save kawaz/e178fbac6fa4529b563854eed1482d91 to your computer and use it in GitHub Desktop.
GoogleMeetにカメラとマイクの自動ミュートや自動参加機能を追加するユーザスクリプト
This file contains 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 GoogleMeet: auto mute / auto join | |
// @namespace http://tampermonkey.net/ | |
// @version 2025-04-08 | |
// @description GoogleMeetにカメラとマイクの自動ミュートや自動参加機能を追加する。 | |
// @author Yoshiaki Kawazu ( https://x.com/kawaz ) | |
// @match https://meet.google.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com | |
// @grant none | |
// ==/UserScript== | |
const configs = [ | |
{ id: 'xxx-xxxx-xxx', micMute: true, cameraMute: true, autoJoin: true }, | |
{ id: 'yyy-yyyy-yyy', autoJoin: true }, | |
{ id: 'default', micMute: true, cameraMute: true, autoJoin: false }, | |
]; | |
// ページ読み込み時に1度実行 | |
runAutomation(); | |
// pushState/replaceState で画面遷移された際にも実行させる | |
observeLocationChange(runAutomation); | |
async function runAutomation() { | |
const meetId = location.pathname.replace(/[^\w-]/g, '') | |
if(!/^\w+-\w+-\w+$/.test(meetId)) { | |
return; | |
} | |
const meetConfig = Object.assign( | |
configs.find(v=>v.id === 'default') ?? {}, | |
configs.find(v=>v.id === meetId) ?? {}, | |
); | |
if(!meetConfig?.id) { | |
return; | |
} | |
const { micMute, cameraMute, autoJoin } = meetConfig | |
const messages = []; | |
if(typeof micMute !== 'undefined') { | |
(await waitValue(()=>document.querySelector(`:is(button,[role=button])[aria-label^="マイクを${micMute?'オフ':'オン'}"]`)))?.click(); | |
} | |
if(typeof cameraMute !== 'undefined') { | |
(await waitValue(()=>document.querySelector(`:is(button,[role=button])[aria-label^="カメラを${cameraMute?'オフ':'オン'}"]`)))?.click(); | |
} | |
if(autoJoin) { | |
// 参加ボタンはミュート操作の少し後に押す | |
await sleep(1000); | |
const joinButton = await waitValue(()=>[...document.querySelectorAll(':is(button,[role=button])')].filter(v=>["参加", "今すぐ参加"].includes(v.textContent))[0]) | |
window.joinButton = joinButton; | |
if(joinButton) { | |
joinButton.click(); | |
say("GoogleMeetに自動ジョインします。") | |
} | |
} | |
} | |
/** | |
* 指定されたテキストをブラウザ上で音声読み上げする関数 | |
* @param {string} message - 読み上げるテキスト | |
* @param {Object} options - 読み上げオプション (省略可能) | |
* @param {string} options.lang - 言語コード (例: 'ja-JP', 'en-US') | |
* @param {number} options.rate - 読み上げ速度 (0.1〜10, デフォルト: 1) | |
* @param {number} options.pitch - 音の高さ (0〜2, デフォルト: 1) | |
* @param {SpeechSynthesisVoice} options.voice - 声(デフォルト: null) | |
* @param {number} options.volume - 音量 (0〜1, デフォルト: 1) | |
* @param {boolean} options.enqueue - 既存の読み上げに追加する形にする(デフォルトはcanel) | |
* @returns {Object} 読み上げ制御オブジェクト | |
* @returns {Promise} returns.promise - 読み上げが完了したときに解決されるPromise | |
* @returns {SpeechSynthesisUtterance} returns.utterance - 作成されたSpeechSynthesisUtteranceオブジェクト | |
* @returns {Function} returns.cancel - 読み上げをキャンセルする関数 | |
* @returns {Function} returns.pause - 読み上げを一時停止する関数 | |
* @returns {Function} returns.resume - 一時停止した読み上げを再開する関数 | |
* @returns {Function} returns.speak - 新しいメッセージを読み上げる関数 | |
*/ | |
function say(message, opts = {}) { | |
if (!('speechSynthesis' in window)) { | |
console.error('このブラウザはWeb Speech APIに対応していません'); | |
return; | |
} | |
if(!opts?.enqueue) { | |
window.speechSynthesis.cancel(); | |
} | |
const utterance = new SpeechSynthesisUtterance(message); | |
utterance.lang = opts?.lang ?? ''; | |
utterance.rate = opts?.rate ?? 1; | |
utterance.pitch = opts?.pitch ?? 1; | |
utterance.voice = opts?.voice ?? null; | |
utterance.volume = opts?.volume ?? 1; | |
window.speechSynthesis.speak(utterance); | |
const controler = { | |
promise: new Promise((resolve, reject) => { | |
utterance.onend = resolve; | |
utterance.onerror = reject; | |
}), | |
utterance, | |
cancel() { window.speechSynthesis.cancel() }, | |
pause() { window.speechSynthesis.pause() }, | |
resume() { window.speechSynthesis.resume() }, | |
speak(message) { window.speechSynthesis.speak(message) }, | |
} | |
return controler; | |
} | |
/** | |
* URLの変更を監視し、変更があった場合にコールバック関数を呼び出す | |
* @param {Function} callback - URL変更時に呼び出される関数。引数として旧URLと新URLと監視を停止するための関数が渡される | |
* @returns {Function} 監視を停止するための関数 | |
*/ | |
function observeLocationChange(callback, checkInterval=200) { | |
let oldUrl = window.location.href; | |
const intervalId = setInterval(() => { | |
const newUrl = window.location.href; | |
if (oldUrl !== newUrl) { | |
const controler = {oldUrl, newUrl, stop} | |
callback(controler); | |
oldUrl = newUrl; | |
} | |
}, checkInterval); | |
return function stop() { | |
clearInterval(intervalId); | |
}; | |
} | |
/** | |
* ms ミリ秒待つ | |
*/ | |
async function sleep(ms=500) { | |
return new Promise(resolve=>setTimeout(resolve, ms)) | |
} | |
/** | |
* condFunc() が true になるのを待つ。 | |
* @return タイムアウトした場合は false を返す。 | |
*/ | |
async function waitCondition(condFunc, { interval=500, timeout=5000 }) { | |
return new Promise((resolve)=>{ | |
setIntervalTimeout(async ({stop})=>{ | |
if(await condFunc()) { | |
stop() | |
resolve(true) | |
} | |
}, {immediate: true, interval, timeout, onTimeout: ()=>resolve(false)}) | |
}) | |
} | |
/** | |
* getValue() が値を返すのを待つ。 | |
* @return getValue() の戻り値、またはタイムアウトした場合は getDefaultValue() の値。 | |
*/ | |
async function waitValue(getValue, { interval=500, timeout=5000, getDefaultValue}={}) { | |
return new Promise((resolve)=>{ | |
const tryGet = async ({stop})=>{ | |
const value = await getValue() | |
if(value == null) return | |
stop(); | |
resolve(value); | |
} | |
const onTimeout = async () => { | |
if(typeof getDefaultValue === 'function') { | |
resolve(await getDefaultValue()) | |
} else { | |
resolve(undefined) | |
} | |
} | |
setIntervalTimeout(tryGet, {immediate: true, interval, timeout, onTimeout}) | |
}) | |
} | |
/** | |
* タイムアウト付きの高機能な setInterval | |
*/ | |
function setIntervalTimeout(cb, {immediate=false, interval=500, timeout=5000, onTimeout}={}) { | |
const startAt = Date.now() | |
let stoped = false; | |
let callCount = 0; | |
let intervalId; | |
let timeoutId; | |
if(typeof onTimeout !== 'function') { | |
onTimeout = () => {} | |
} | |
const stop = () => { | |
stoped = true; | |
clearTimeout(timeoutId); | |
clearInterval(intervalId); | |
} | |
const controler = { | |
stop, | |
timeout: onTimeout, | |
get stoped() { return stoped }, | |
get callCount() { return callCount }, | |
get duration() { return Date.now() - startAt }, | |
} | |
const loop = () => { | |
callCount += 1; | |
cb(controler); | |
} | |
if(immediate) { | |
loop(); | |
} | |
intervalId = setInterval(loop, interval) | |
timeoutId = setTimeout(()=>{ | |
clearTimeout(intervalId); | |
onTimeout(); | |
}, timeout) | |
return controler | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment