Skip to content

Instantly share code, notes, and snippets.

@kawaz
Last active April 8, 2025 09:54
Show Gist options
  • Save kawaz/e178fbac6fa4529b563854eed1482d91 to your computer and use it in GitHub Desktop.
Save kawaz/e178fbac6fa4529b563854eed1482d91 to your computer and use it in GitHub Desktop.
GoogleMeetにカメラとマイクの自動ミュートや自動参加機能を追加するユーザスクリプト
// ==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