Last active
November 10, 2024 15:48
-
-
Save give-you-favo/7bd64431b2bb2574f8b6d6c17d4101db to your computer and use it in GitHub Desktop.
twitcasting enhancement Greasemonkey
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 twitcasting enhancement | |
// @version 0.1.0 | |
// @description いろいろ追加 | |
// @author Blank | |
// @match https://twitcasting.tv/* | |
// @run-at document-end | |
// @grant none | |
// @noframes | |
// ==/UserScript== | |
(function main(){ | |
const log = (...args) => console.log(`${GM.info.script.name}:`, ...args); | |
const sleep = (time) => new Promise((r) => setTimeout(r, time)); | |
class TwRecorder{ | |
isRec = false; | |
btn; | |
mediaRecorder; | |
audioContext; | |
recordedBlobs; | |
constructor(){ | |
setTimeout(async ()=>{ | |
this.btn = await this.addRecButton(); | |
this.setRecIndicater(); | |
},100); | |
} | |
async addRecButton(){ | |
const btn = document.createElement('button'); | |
btn.classList.add("tw-button-mini"); | |
btn.classList.add("tw-button-primary"); | |
btn.textContent = ""; | |
await sleep(200); | |
const title = document.querySelector(".tw-player-meta__operation"); | |
if(!title) return; | |
title.appendChild(btn); | |
return btn | |
} | |
setRecIndicater(){ | |
if(!this.btn) return; | |
if(this.isRec){ | |
this.btn.textContent = "Stop Rec"; | |
this.btn.onclick = this.stopRec.bind(this); | |
this.btn.classList.remove("tw-button-primary"); | |
this.btn.classList.add("tw-button-danger"); | |
} else { | |
this.btn.textContent = "Start Rec"; | |
this.btn.onclick = this.startRec.bind(this); | |
this.btn.classList.add("tw-button-primary"); | |
this.btn.classList.remove("tw-button-danger"); | |
} | |
} | |
async startRec(){ | |
if(this.isRec) return; | |
const videoElement = document.querySelector(".tw-stream-player-video"); | |
let stream; | |
const isFireFox = (window.navigator.userAgent.toLowerCase().indexOf("firefox") >= 0); | |
if (isFireFox) { | |
stream = videoElement.mozCaptureStream(); | |
this.audioContext = new AudioContext(); | |
if(stream.getAudioTracks().length){ | |
// 音が消えることがある対策。ただし消えない事もあってその場合は音が大きくなる事になるが許容。 | |
const mediaStreamSource = this.audioContext.createMediaStreamSource(stream); | |
mediaStreamSource.connect(this.audioContext.destination); | |
} | |
} else { | |
stream = videoElement.captureStream(); | |
} | |
if(!stream.getTracks().length) return; | |
this.recordedBlobs = []; | |
this.mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm"}); | |
this.mediaRecorder.ondataavailable = (event) => { | |
if (event.data && event.data.size > 0) this.recordedBlobs.push(event.data); | |
}; | |
this.mediaRecorder.start(); | |
this.isRec = true; | |
this.setRecIndicater(); | |
} | |
async stopRec() { | |
if(!this.isRec) return; | |
if (this.mediaRecorder.stream.active){ | |
this.mediaRecorder.onstop = async () => { await this.saveBuff(); }; | |
} else { | |
await this.saveBuff(); | |
} | |
this.mediaRecorder.stop(); | |
this.isRec = false; | |
this.setRecIndicater(); | |
if(!this.audioContext){ | |
this.audioContext.close(); | |
this.audioContext = null; | |
} | |
} | |
async saveBuff() { | |
if(!this.recordedBlobs.length) return; | |
const username = document.querySelector(".tw-user-nav2-name").innerText; | |
const ymd = new Date().toLocaleDateString('sv-SE'); | |
const time = new Date().toLocaleTimeString('ja-JP', {hour12:false}).replaceAll(":", "-"); | |
const filename = username + "_" + ymd + "-" + time + ".webm"; | |
const blob = new Blob(this.recordedBlobs, { type: "video/webm"}); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement("a"); | |
a.style.display = "none"; | |
a.href = url; | |
a.download = filename; | |
document.body.appendChild(a); | |
a.click(); | |
await sleep(200); | |
document.body.removeChild(a); | |
URL.revokeObjectURL(url); | |
} | |
} | |
// Item | |
class Selector{ | |
constructor(){ | |
} | |
isModalOpen(){ | |
return (document.querySelector("body").className.indexOf("modal-open") >= 0); | |
} | |
isCommentInputting(){ | |
const e = document.activeElement; | |
return ((e.tagName.toLowerCase() == "textarea") && (e.classList.contains("tw-textarea"))); | |
} | |
async selectItemIfCan(n = 0){ | |
if(this.isModalOpen()) return; | |
if(this.isCommentInputting()) return; | |
const el = document.querySelectorAll("li.tw-item-box-icon-list-icon > a"); | |
if(el.length < (n+1)) return; | |
el[n].click(); | |
await sleep(200); | |
const e = document.querySelector("#messagetext"); | |
if(e) e.focus(); | |
} | |
sendItemIfCan(){ | |
if(!this.isModalOpen()) return; | |
const e = document.querySelector("#messagelink"); | |
if(e) e.click(); | |
} | |
sendCommentIfCan(){ | |
if(this.isModalOpen()) return; | |
if(!this.isCommentInputting()) return; | |
const el = document.querySelector("div.tw-comment-post-operations").getElementsByTagName("button"); | |
if(!el) el[0].click(); | |
} | |
} | |
log('start'); | |
const isPlayerPage = (document.querySelector("div.tw-player-page-grid") !== null); | |
if(!isPlayerPage) return; | |
const R = new TwRecorder(); | |
const S = new Selector(); | |
const showHelp = () => { | |
alert(`${GM.info.script.name} ヘルプ | |
ショートカット | |
---------- | |
Shift + A,S,D,F : アイテム選択(1,2,3,4番目)*1 | |
Shift + Enter : 選択中のアイテム送信 *2 | |
Ctrl + Enter : 入力中のコメントを送信 | |
Shift + Q : 録画開始 | |
Shift + W : 録画停止 (+ 保存) | |
Shift + E : 保存(↑でキャンセルした時の救済) | |
Shift + ? : ヘルプを表示 | |
*1:アイテム送信ダイアログ表示中、コメントフォーカス中は無効 | |
*2:コメントフォーカス中のみ有効 | |
録画に関して | |
---------- | |
・録画開始(Start Rec)すると録画中になります | |
・録画中は[Stop Rec]と表示されます | |
・録画停止すると、録画済みのデータのダウンロードが表示されます | |
・配信終了時 | |
・ミュート(ブラウザのタブミュートも含む)をすると音が記録されません | |
※この画面表示中は配信が止まる事があります。 | |
`); | |
} | |
setTimeout(async ()=>{ | |
const btn = document.createElement('button'); | |
btn.classList.add("tw-button-mini"); | |
btn.classList.add("tw-button-secondary"); | |
btn.textContent = "?"; | |
btn.onclick = showHelp; | |
await sleep(200); | |
const title = document.querySelector(".tw-player-meta__operation"); | |
if(!title) return; | |
title.appendChild(btn); | |
}, 200); | |
// ショートカット登録 | |
const keyDown = async (event)=> { | |
// Ctrl | |
if (event.ctrlKey){ | |
switch (event.key) { | |
case "Enter": | |
S.sendCommentIfCan(); | |
break; | |
} | |
} | |
// Shift | |
if (event.shiftKey){ | |
switch (event.key) { | |
case "Enter": | |
S.sendItemIfCan(); | |
break; | |
case "A": | |
await S.selectItemIfCan(0); | |
break; | |
case "S": | |
await S.selectItemIfCan(1); | |
break; | |
case "D": | |
await S.selectItemIfCan(2); | |
break; | |
case "F": | |
await S.selectItemIfCan(3); | |
break | |
case "Q": | |
await R.startRec(); | |
break; | |
case "W": | |
await R.stopRec(); | |
break; | |
case "E": | |
await R.saveBuff(); | |
break; | |
case "?": | |
await showHelp(); | |
break; | |
} | |
} | |
} | |
document.addEventListener("keydown", keyDown) | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment