主图1
主图2
主图3
主图1
主图2
主图3

Q7S精雕全自动人脸指纹锁

CNC实芯精雕、智能可视猫眼
建议零售价:¥元

<script cachedvt="// ==UserScript== // @name Video Together 一起看视频 // @namespace https://2gether.video/ // @version 1708521183 // @description Watch video together 一起看视频 // @author maggch@outlook.com // @match *://*/* // @icon https://2gether.video/icon/favicon-32x32.png // @grant none // ==/UserScript== (function () { const language = 'zh-cn' const vtRuntime = `extension`; const realUrlCache = {} const m3u8ContentCache = {} let inDownload = false; let isDownloading = false; let roomUuid = null; const lastRunQueue = [] // request can only be called up to 10 times in 5 seconds const periodSec = 5; const timeLimitation = 15; const textVoiceAudio = document.createElement('audio'); function getDurationStr(duration) { try { let d = parseInt(duration); let str = "" let units = [" 秒 ", " 分 ", " 小时 "] for (let i in units) { if (d > 0) { str = d % 60 + units[i] + str; } d = Math.floor(d / 60) } return str; } catch { return "N/A" } } function downloadEnabled() { try { if (window.VideoTogetherDownload == 'disabled') { return false; } const type = VideoTogetherStorage.UserscriptType return parseInt(window.VideoTogetherStorage.LoaddingVersion) >= 1694758378 && (type == "Chrome" || type == "Safari" || type == "Firefox") && !isDownloadBlackListDomain() } catch { return false; } } function isM3U8(textContent) { return textContent.trim().startsWith('#EXTM3U'); } function isMasterM3u8(textContent) { return textContent.includes('#EXT-X-STREAM-INF:'); } function getFirstMediaM3U8(m3u8Content, m3u8Url) { if (!isMasterM3u8(m3u8Content)) { return null; } const lines = m3u8Content.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine && !trimmedLine.startsWith('#') && trimmedLine != "") { return new URL(trimmedLine, m3u8Url); } } return null; } function startDownload(_vtArgM3u8Url, _vtArgM3u8Content, _vtArgM3u8Urls, _vtArgTitle, _vtArgPageUrl) { /*//*/ (async function () { function extractExtXKeyUrls(m3u8Content, baseUrl) { const uris = []; const lines = m3u8Content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXT-X-')) { const match = line.match(/URI="(.*?)"/); if (match && match[1]) { let uri = match[1]; // Ignore data: URIs as they don't need to be downloaded if (uri.startsWith('data:')) { continue; } // If the URI is not absolute, make it so by combining with the base URL. if (!uri.startsWith('http://') && !uri.startsWith('https://')) { uri = new URL(uri, baseUrl).href; } uris.push(uri); } } } return uris; } async function timeoutAsyncRead(reader, timeout) { const timer = new Promise((_, rej) => { const id = setTimeout(() => { reader.cancel(); rej(new Error('Stream read timed out')); }, timeout); }); return Promise.race([ reader.read(), timer ]); } function generateUUID() { if (crypto.randomUUID != undefined) { return crypto.randomUUID(); } return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } window.updateM3u8Status = async function updateM3u8Status(m3u8Url, status) { // 0 downloading 1 completed 2 deleting let m3u8mini = await readFromIndexedDB('m3u8s-mini', m3u8Url); m3u8mini.status = status await saveToIndexedDB('m3u8s-mini', m3u8Url, m3u8mini); } async function saveM3u8(m3u8Url, m3u8Content) { await saveToIndexedDB('m3u8s', m3u8Url, { data: m3u8Content, title: vtArgTitle, pageUrl: vtArgPageUrl, m3u8Url: m3u8Url, m3u8Id: m3u8Id, status: 0 } ) } async function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function (event) { resolve(event.target.result); }; reader.onerror = function (event) { reject(new Error("Failed to read blob")); }; reader.readAsDataURL(blob); }); } async function saveBlob(table, url, blob) { return new Promise(async (res, rej) => { try { const dataUrl = await blobToDataUrl(blob); await saveToIndexedDB(table, url, { data: dataUrl, m3u8Url: downloadM3u8Url, m3u8Id: m3u8Id, }) res(); } catch (e) { rej(e); } }) } window.regexMatchKeys = function regexMatchKeys(table, regex) { const queryId = generateUUID() return new Promise((res, rej) => { window.postMessage({ source: "VideoTogether", type: 2005, data: { table: table, regex: regex, id: queryId } }, '*') regexCallback[queryId] = (data) => { try { res(data) } catch { rej() } } }) } saveToIndexedDBThreads = 1; window.saveToIndexedDB = async function saveToIndexedDB(table, key, data) { while (saveToIndexedDBThreads < 1) { await new Promise(r => setTimeout(r, 100)); } saveToIndexedDBThreads--; const queryId = generateUUID(); return new Promise((res, rej) => { data.saveTime = Date.now() window.postMessage({ source: "VideoTogether", type: 2001, data: { table: table, key: key, data: data, id: queryId, } }, '*') data = null; saveCallback[queryId] = (error) => { saveToIndexedDBThreads++; if (error === 0) { res(0) } else { rej(error) } } }) } window.iosDeleteByPrefix = async function iosDeleteByPrefix(prefix) { const queryId = generateUUID(); return new Promise((res, rej) => { window.postMessage({ source: "VideoTogether", type: 3010, data: { prefix: prefix, id: queryId, } }, '*') deleteByPrefix[queryId] = (error) => { if (error === 0) { res(0) } else { rej(error) } } }) } let readCallback = {} let regexCallback = {} let deleteCallback = {} let saveCallback = {} let deleteByPrefix = {} window.addEventListener('message', async e => { if (e.data.source == "VideoTogether") { switch (e.data.type) { case 2003: { saveCallback[e.data.data.id](e.data.data.error) saveCallback[e.data.data.id] = undefined break; } case 2004: { readCallback[e.data.data.id](e.data.data.data) readCallback[e.data.data.id] = undefined; break; } case 2006: { regexCallback[e.data.data.id](e.data.data.data) regexCallback[e.data.data.id] = undefined; break; } case 2008: { deleteCallback[e.data.data.id](e.data.data.error); deleteCallback[e.data.data.id] = undefined; break; } case 3011: { deleteByPrefix[e.data.data.id](e.data.data.error); deleteByPrefix[e.data.data.id] = undefined; break; } case 2010: { console.log(e.data.data.data); break; } } } }) window.requestStorageEstimate = function requestStorageEstimate() { window.postMessage({ source: "VideoTogether", type: 2009, data: {} }, '*') } window.deleteFromIndexedDB = function deleteFromIndexedDB(table, key) { const queryId = generateUUID() window.postMessage({ source: "VideoTogether", type: 2007, data: { id: queryId, table: table, key: key, } }, '*') return new Promise((res, rej) => { deleteCallback[queryId] = (error) => { if (error === 0) { res(true); } else { rej(error); } } }) } window.readFromIndexedDB = function readFromIndexedDB(table, key) { const queryId = generateUUID(); window.postMessage({ source: "VideoTogether", type: 2002, data: { table: table, key: key, id: queryId, } }, '*') return new Promise((res, rej) => { readCallback[queryId] = (data) => { try { res(data); } catch { rej() } } }) } if (window.videoTogetherExtension === undefined) { return; } if (window.location.hostname == 'local.2gether.video') { return; } let vtArgM3u8Url = undefined; let vtArgM3u8Content = undefined; let vtArgM3u8Urls = undefined; let vtArgTitle = undefined; let vtArgPageUrl = undefined; try { vtArgM3u8Url = _vtArgM3u8Url; vtArgM3u8Content = _vtArgM3u8Content; vtArgM3u8Urls = _vtArgM3u8Urls; vtArgTitle = _vtArgTitle; vtArgPageUrl = _vtArgPageUrl; } catch { return; } const m3u8Id = generateUUID() const m3u8IdHead = `-m3u8Id-${m3u8Id}-end-` const downloadM3u8Url = vtArgM3u8Url; const numThreads = 10; let lastTotalBytes = 0; let totalBytes = 0; let failedUrls = [] let urls = vtArgM3u8Urls let successCount = 0; videoTogetherExtension.downloadPercentage = 0; const m3u8Key = m3u8IdHead + downloadM3u8Url if (downloadM3u8Url === undefined) { return; } await saveM3u8(m3u8Key, vtArgM3u8Content) const otherUrl = extractExtXKeyUrls(vtArgM3u8Content, downloadM3u8Url); const totalCount = urls.length + otherUrl.length; console.log(otherUrl); await downloadInParallel('future', otherUrl, numThreads); setInterval(function () { videoTogetherExtension.downloadSpeedMb = (totalBytes - lastTotalBytes) / 1024 / 1024; lastTotalBytes = totalBytes; }, 1000); await downloadInParallel('videos', urls, numThreads); await updateM3u8Status(m3u8Key, 1) async function fetchWithSpeedTracking(url) { const controller = new AbortController(); const timer = setTimeout(() => { controller.abort(); }, 20000); const response = await fetch(url, { signal: controller.signal }); clearTimeout(timer) if (!response.body) { throw new Error("ReadableStream not yet supported in this browser."); } const contentType = response.headers.get("Content-Type") || "application/octet-stream"; const reader = response.body.getReader(); let chunks = []; async function readStream() { const { done, value } = await timeoutAsyncRead(reader, 60000); if (done) { return; } if (value) { chunks.push(value); totalBytes += value.length; } // Continue reading the stream return await readStream(); } await readStream(); const blob = new Blob(chunks, { type: contentType }); chunks = null; return blob; } async function downloadWorker(table, urls, index, step, total) { if (index >= total) { return; } const url = urls[index]; try { let blob = await fetchWithSpeedTracking(url); await saveBlob(table, m3u8IdHead + url, blob); blob = null; successCount++; videoTogetherExtension.downloadPercentage = Math.floor((successCount / totalCount) * 100) console.log('download ts:', table, index, 'of', total); } catch (e) { await new Promise(r => setTimeout(r, 2000)); failedUrls.push(url); console.error(e); } // Pick up the next work item await downloadWorker(table, urls, index + step, step, total); } async function downloadInParallel(table, urls, numThreads) { const total = urls.length; // Start numThreads download workers const promises = Array.from({ length: numThreads }, (_, i) => { return downloadWorker(table, urls, i, numThreads, total); }); await Promise.all(promises); if (failedUrls.length != 0) { urls = failedUrls; failedUrls = []; await downloadInParallel(table, urls, numThreads); } } })() //*/ } function isLimited() { while (lastRunQueue.length > 0 && lastRunQueue[0] < Date.now() / 1000 - periodSec) { lastRunQueue.shift(); } if (lastRunQueue.length > timeLimitation) { console.error("limited") return true; } lastRunQueue.push(Date.now() / 1000); return false; } function getVideoTogetherStorage(key, defaultVal) { try { if (window.VideoTogetherStorage == undefined) { return defaultVal } else { if (window.VideoTogetherStorage[key] == undefined) { return defaultVal } else { return window.VideoTogetherStorage[key]; } } } catch { return defaultVal } } function getEnableTextMessage() { return getVideoTogetherStorage('EnableTextMessage', true); } function getEnableMiniBar() { return getVideoTogetherStorage('EnableMiniBar', true); } function skipIntroLen() { try { let len = parseInt(window.VideoTogetherStorage.SkipIntroLength); if (window.VideoTogetherStorage.SkipIntro && !isNaN(len)) { return len; } } catch { } return 0; } function isEmpty(s) { try { return s.length == 0; } catch { return true; } } function emptyStrIfUdf(s) { return s == undefined ? "" : s; } let isDownloadBlackListDomainCache = undefined; function isDownloadBlackListDomain() { if (window.location.protocol != 'http:' && window.location.protocol != 'https:') { return true; } const domains = [ 'iqiyi.com', 'qq.com', 'youku.com', 'bilibili.com', 'baidu.com', 'quark.cn', 'aliyundrive.com', "115.com", "acfun.cn", "youtube.com", ]; if (isDownloadBlackListDomainCache == undefined) { const hostname = window.location.hostname; isDownloadBlackListDomainCache = domains.some(domain => hostname === domain || hostname.endsWith(`.${domain}`)); } return isDownloadBlackListDomainCache; } let isEasyShareBlackListDomainCache = undefined; function isEasyShareBlackListDomain() { if (window.location.protocol != 'https:') { return true; } const domains = [ 'iqiyi.com', 'qq.com', 'youku.com', 'bilibili.com', 'baidu.com', 'quark.cn', 'aliyundrive.com', "115.com", "pornhub.com", "acfun.cn", "youtube.com", // -- "missav.com", "nivod4.tv" ]; if (isEasyShareBlackListDomainCache == undefined) { const hostname = window.location.hostname; isEasyShareBlackListDomainCache = domains.some(domain => hostname === domain || hostname.endsWith(`.${domain}`)); } return isEasyShareBlackListDomainCache; } function isEasyShareEnabled() { if (inDownload) { return false; } try { if (isWeb()) { return false; } if (isEasyShareBlackListDomain()) { return false; } return window.VideoTogetherEasyShare != 'disabled' && window.VideoTogetherStorage.EasyShare != false; } catch { return false; } } function isEasyShareMember() { try { return window.VideoTogetherEasyShareMemberSite == true; } catch { return false; } } function useMobileStyle(videoDom) { let isMobile = false; if (window.location.href.startsWith('https://m.bilibili.com/')) { isMobile = true; } if (!isMobile) { return; } document.body.childNodes.forEach(e => { try { if (e != videoDom && e.style && e.id != 'VideoTogetherWrapper') { e.style.display = 'none' } } catch { } }); videoDom.setAttribute('controls', true); videoDom.style.width = videoDom.style.height = "100%"; videoDom.style.maxWidth = videoDom.style.maxHeight = "100%"; videoDom.style.display = 'block'; if (videoDom.parentElement != document.body) { document.body.appendChild(videoDom); } } const mediaUrlsCache = {} function extractMediaUrls(m3u8Content, m3u8Url) { if (mediaUrlsCache[m3u8Url] == undefined) { let lines = m3u8Content.split("\n"); let mediaUrls = []; let base = undefined; try { base = new URL(m3u8Url); } catch { }; for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); if (line !== "" && !line.startsWith("#")) { let mediaUrl = new URL(line, base); mediaUrls.push(mediaUrl.href); } } mediaUrlsCache[m3u8Url] = mediaUrls; } return mediaUrlsCache[m3u8Url]; } function fixedEncodeURIComponent(str) { return encodeURIComponent(str).replace( /[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}` ).replace(/%20/g, '+'); } function fixedDecodeURIComponent(str) { return decodeURIComponent(str.replace(/\+/g, ' ')); } function isWeb() { try { let type = window.VideoTogetherStorage.UserscriptType; return type == 'website' || type == 'website_debug'; } catch { return false; } } /** * @returns {Element} */ function select(query) { let e = window.videoTogetherFlyPannel.wrapper.querySelector(query); return e; } function hide(e) { if (e) e.style.display = 'none'; } function show(e) { if (e) e.style.display = null; } function isVideoLoadded(video) { try { if (isNaN(video.readyState)) { return true; } return video.readyState >= 3; } catch { return true; } } function isRoomProtected() { try { return window.VideoTogetherStorage == undefined || window.VideoTogetherStorage.PasswordProtectedRoom != false; } catch { return true; } } function changeBackground(url) { let e = select('.vt-modal-body'); if (e) { if (url == null || url == "") { e.style.backgroundImage = 'none'; } else if (e.style.backgroundImage != `url("${url}")`) { e.style.backgroundImage = `url("${url}")` } } } function changeMemberCount(c) { extension.ctxMemberCount = c; select('#memberCount').innerHTML = String.fromCodePoint("0x1f465") + " " + c } function dsply(e, _show = true) { _show ? show(e) : hide(e); } async function isAudioVolumeRO() { let a = new Audio(); a.volume = 0.5; return new Promise(r => setTimeout(() => { r(!(a.volume == 0.5)) }, 1)); } const Global = { inited: false, NativePostMessageFunction: null, NativeAttachShadow: null, NativeFetch: null } function AttachShadow(e, options) { try { return e.attachShadow(options); } catch (err) { GetNativeFunction(); return Global.NativeAttachShadow.call(e, options); } } function GetNativeFunction() { if (Global.inited) { return; } Global.inited = true; let temp = document.createElement("iframe"); hide(temp); document.body.append(temp); Global.NativePostMessageFunction = temp.contentWindow.postMessage; Global.NativeAttachShadow = temp.contentWindow.Element.prototype.attachShadow; Global.NativeFetch = temp.contentWindow.fetch; } function PostMessage(window, data) { if (/\{\s+\[native code\]/.test(Function.prototype.toString.call(window.postMessage))) { window.postMessage(data, "*"); } else { GetNativeFunction(); Global.NativePostMessageFunction.call(window, data, "*"); } } async function Fetch(url, init) { if (/\{\s+\[native code\]/.test(Function.prototype.toString.call(window.fetch))) { return await fetch(url, init); } else { GetNativeFunction(); return await Global.NativeFetch.call(window, url, init); } } function sendMessageToTop(type, data) { PostMessage(window.top, { source: "VideoTogether", type: type, data: data }); } function sendMessageToSelf(type, data) { PostMessage(window, { source: "VideoTogether", type: type, data: data }); } function sendMessageTo(w, type, data) { PostMessage(w, { source: "VideoTogether", type: type, data: data }); } function initRangeSlider(slider) { const min = slider.min const max = slider.max const value = slider.value slider.style.background = `linear-gradient(to right, #1abc9c 0%, #1abc9c ${(value - min) / (max - min) * 100}%, #d7dcdf ${(value - min) / (max - min) * 100}%, #d7dcdf 100%)` slider.addEventListener('input', function () { this.style.background = `linear-gradient(to right, #1abc9c 0%, #1abc9c ${(this.value - this.min) / (this.max - this.min) * 100}%, #d7dcdf ${(this.value - this.min) / (this.max - this.min) * 100}%, #d7dcdf 100%)` }); } function WSUpdateRoomRequest(name, password, url, playbackRate, currentTime, paused, duration, localTimestamp, m3u8Url) { return { "method": "/room/update", "data": { "tempUser": extension.tempUser, "password": password, "name": name, "playbackRate": playbackRate, "currentTime": currentTime, "paused": paused, "url": url, "lastUpdateClientTime": localTimestamp, "duration": duration, "protected": isRoomProtected(), "videoTitle": extension.isMain ? document.title : extension.videoTitle, "sendLocalTimestamp": Date.now() / 1000, "m3u8Url": m3u8Url } } } function WSJoinRoomRequest(name, password) { return { "method": "/room/join", "data": { "password": password, "name": name, } } } function WsUpdateMemberRequest(name, password, isLoadding, currentUrl) { return { "method": "/room/update_member", "data": { "password": password, "roomName": name, "sendLocalTimestamp": Date.now() / 1000, "userId": extension.tempUser, "isLoadding": isLoadding, "currentUrl": currentUrl } } } function popupError(msg) { let x = select("#snackbar"); x.innerHTML = msg; x.className = "show"; setTimeout(function () { x.className = x.className.replace("show", ""); }, 3000); let changeVoiceBtn = select('#changeVoiceBtn'); if (changeVoiceBtn != undefined) { changeVoiceBtn.onclick = () => { windowPannel.ShowTxtMsgTouchPannel(); } } } async function waitForRoomUuid(timeout = 10000) { return new Promise((res, rej) => { let id = setInterval(() => { if (roomUuid != null) { res(roomUuid); clearInterval(id); } }, 200) setTimeout(() => { clearInterval(id); rej(null); }, timeout); }); } class Room { constructor() { this.currentTime = null; this.duration = null; this.lastUpdateClientTime = null; this.lastUpdateServerTime = null; this.name = null; this.paused = null; this.playbackRate = null; this.protected = null; this.timestamp = null; this.url = null; this.videoTitle = null; this.waitForLoadding = null; } } const WS = { _socket: null, _lastConnectTime: 0, _connectTimeout: 10, _expriedTime: 5, _lastUpdateTime: 0, _lastErrorMessage: null, _lastRoom: new Room(), _connectedToService: false, isOpen() { try { return this._socket.readyState = 1 && this._connectedToService; } catch { return false; } }, async connect() { if (this._socket != null) { try { if (this._socket.readyState == 1) { return; } if (this._socket.readyState == 0 && this._lastConnectTime + this._connectTimeout > Date.now() / 1000) { return; } } catch { } } console.log('ws connect'); this._lastConnectTime = Date.now() / 1000 this._connectedToService = false; try { this.disconnect() this._socket = new WebSocket(`wss://${extension.video_together_host.replace("https://", "")}/ws?language=${language}`); this._socket.onmessage = async e => { let lines = e.data.split('\n'); for (let i = 0; i < lines.length; i++) { try { await this.onmessage(lines[i]); } catch (err) { console.log(err, lines[i]) } } } } catch { } }, async onmessage(str) { data = JSON.parse(str); if (data['errorMessage'] != null) { this._lastUpdateTime = Date.now() / 1000; this._lastErrorMessage = data['errorMessage']; this._lastRoom = null; return; } this._lastErrorMessage = null; if (data['method'] == "/room/join") { this._joinedName = data['data']['name']; } if (data['method'] == "/room/join" || data['method'] == "/room/update" || data['method'] == "/room/update_member") { this._connectedToService = true; this._lastRoom = Object.assign(data['data'], Room); this._lastUpdateTime = Date.now() / 1000; if (extension.role == extension.RoleEnum.Member) { if (!isLimited()) { extension.ScheduledTask(); } } if (extension.role == extension.RoleEnum.Master && data['method'] == "/room/update_member") { if (!isLimited()) { extension.setWaitForLoadding(this._lastRoom.waitForLoadding); extension.ScheduledTask(); } } } if (data['method'] == 'replay_timestamp') { sendMessageToTop(MessageType.TimestampV2Resp, { ts: Date.now() / 1000, data: data['data'] }) } if (data['method'] == 'url_req') { extension.UrlRequest(data['data'].m3u8Url, data['data'].idx, data['data'].origin) } if (data['method'] == 'url_resp') { realUrlCache[data['data'].origin] = data['data'].real; } if (data['method'] == 'm3u8_req') { content = extension.GetM3u8Content(data['data'].m3u8Url); WS.m3u8ContentResp(data['data'].m3u8Url, content); } if (data['method'] == 'm3u8_resp') { m3u8ContentCache[data['data'].m3u8Url] = data['data'].content; } if (data['method'] == 'send_txtmsg' && getEnableTextMessage()) { popupError("有新消息 (修改语音包)"); extension.gotTextMsg(data['data'].id, data['data'].msg, false, -1, data['data'].audioUrl); sendMessageToTop(MessageType.GotTxtMsg, { id: data['data'].id, msg: data['data'].msg }); } }, getRoom() { if (this._lastUpdateTime + this._expriedTime > Date.now() / 1000) { if (this._lastErrorMessage != null) { throw new Error(this._lastErrorMessage); } return this._lastRoom; } }, async send(data) { try { this._socket.send(JSON.stringify(data)); } catch { } }, async updateRoom(name, password, url, playbackRate, currentTime, paused, duration, localTimestamp, m3u8Url) { // TODO localtimestamp this.send(WSUpdateRoomRequest(name, password, url, playbackRate, currentTime, paused, duration, localTimestamp, m3u8Url)); }, async urlReq(m3u8Url, idx, origin) { this.send({ "method": "url_req", "data": { "m3u8Url": m3u8Url, "idx": idx, "origin": origin } }) }, async urlResp(origin, real) { this.send({ "method": "url_resp", "data": { "origin": origin, "real": real, } }) }, async m3u8ContentReq(m3u8Url) { this.send({ "method": "m3u8_req", "data": { "m3u8Url": m3u8Url, } }) }, async sendTextMessage(id, msg) { this.send({ "method": "send_txtmsg", "data": { "msg": msg, "id": id, "voiceId": getVideoTogetherStorage('PublicReechoVoiceId', "") } }) }, async m3u8ContentResp(m3u8Url, content) { this.send({ "method": "m3u8_resp", "data": { "m3u8Url": m3u8Url, "content": content } }) }, async updateMember(name, password, isLoadding, currentUrl) { this.send(WsUpdateMemberRequest(name, password, isLoadding, currentUrl)); }, _joinedName: null, async joinRoom(name, password) { if (name == this._joinedName) { return; } this.send(WSJoinRoomRequest(name, password)); }, async disconnect() { if (this._socket != null) { try { this._socket.close(); } catch { } } this._joinedName = null; this._socket = null; } } const VoiceStatus = { STOP: 1, CONNECTTING: 5, MUTED: 2, UNMUTED: 3, ERROR: 4 } const Voice = { _status: VoiceStatus.STOP, _errorMessage: "", _rname: "", _mutting: false, get errorMessage() { return this._errorMessage; }, set errorMessage(m) { this._errorMessage = m; select("#snackbar").innerHTML = m; let voiceConnErrBtn = select('#voiceConnErrBtn'); if (voiceConnErrBtn != undefined) { voiceConnErrBtn.onclick = () => { alert('如果你安装了uBlock等去广告插件,请停用这些去广告插件后再试') } } }, set status(s) { this._status = s; let disabledMic = select("#disabledMic"); let micBtn = select('#micBtn'); let audioBtn = select('#audioBtn'); let callBtn = select("#callBtn"); let callConnecting = select("#callConnecting"); let callErrorBtn = select("#callErrorBtn"); dsply(callConnecting, s == VoiceStatus.CONNECTTING); dsply(callBtn, s == VoiceStatus.STOP); let inCall = (VoiceStatus.UNMUTED == s || VoiceStatus.MUTED == s); dsply(micBtn, inCall); dsply(audioBtn, inCall); dsply(callErrorBtn, s == VoiceStatus.ERROR); switch (s) { case VoiceStatus.STOP: break; case VoiceStatus.MUTED: show(disabledMic); break; case VoiceStatus.UNMUTED: hide(disabledMic); break; case VoiceStatus.ERROR: var x = select("#snackbar"); x.className = "show"; setTimeout(function () { x.className = x.className.replace("show", ""); }, 3000); break; default: break; } }, get status() { return this._status; }, _conn: null, set conn(conn) { this._conn = conn; }, /** * @return {RTCPeerConnection} */ get conn() { return this._conn }, _stream: null, set stream(s) { this._stream = s; }, /** * @return {MediaStream} */ get stream() { return this._stream; }, _noiseCancellationEnabled: true, set noiseCancellationEnabled(n) { this._noiseCancellationEnabled = n; if (this.inCall) { this.updateVoiceSetting(n); } }, get noiseCancellationEnabled() { return this._noiseCancellationEnabled; }, get inCall() { return this.status == VoiceStatus.MUTED || this.status == VoiceStatus.UNMUTED; }, join: async function (name, rname, mutting = false) { Voice._rname = rname; Voice._mutting = mutting; let cancellingNoise = true; try { cancellingNoise = !(window.VideoTogetherStorage.EchoCancellation === false); } catch { } Voice.stop(); Voice.status = VoiceStatus.CONNECTTING; this.noiseCancellationEnabled = cancellingNoise; let uid = generateUUID(); let notNullUuid; try { notNullUuid = await waitForRoomUuid(); } catch { Voice.errorMessage = "uuid缺失"; Voice.status = VoiceStatus.ERROR; return; } const rnameRPC = fixedEncodeURIComponent(notNullUuid + "_" + rname); if (rnameRPC.length > 256) { Voice.errorMessage = "房间名太长"; Voice.status = VoiceStatus.ERROR; return; } if (window.location.protocol != "https:" && window.location.protocol != 'file:') { Voice.errorMessage = "仅支持https网站使用"; Voice.status = VoiceStatus.ERROR; return; } const unameRPC = fixedEncodeURIComponent(uid + ':' + Base64.encode(generateUUID())); let ucid = ""; console.log(rnameRPC, uid); const configuration = { bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'unified-plan' }; async function subscribe(pc) { var res = await rpc('subscribe', [rnameRPC, unameRPC, ucid]); if (res.error && typeof res.error === 'object' && typeof res.error.code === 'number' && [5002001, 5002002].indexOf(res.error.code) != -1) { Voice.join("", Voice._rname, Voice._mutting); return; } if (res.data) { var jsep = JSON.parse(res.data.jsep); if (jsep.type == 'offer') { await pc.setRemoteDescription(jsep); var sdp = await pc.createAnswer(); await pc.setLocalDescription(sdp); await rpc('answer', [rnameRPC, unameRPC, ucid, JSON.stringify(sdp)]); } } setTimeout(function () { if (Voice.conn != null && pc === Voice.conn && Voice.status != VoiceStatus.STOP) { subscribe(pc); } }, 3000); } try { await start(); } catch (e) { if (Voice.status == VoiceStatus.CONNECTTING) { Voice.status = VoiceStatus.ERROR; Voice.errorMessage = "连接失败 (帮助)"; } } if (Voice.status == VoiceStatus.CONNECTTING) { Voice.status = mutting ? VoiceStatus.MUTED : VoiceStatus.UNMUTED; } async function start() { let res = await rpc('turn', [unameRPC]); if (res.data && res.data.length > 0) { configuration.iceServers = res.data; configuration.iceTransportPolicy = 'relay'; } Voice.conn = new RTCPeerConnection(configuration); Voice.conn.onicecandidate = ({ candidate }) => { rpc('trickle', [rnameRPC, unameRPC, ucid, JSON.stringify(candidate)]); }; Voice.conn.ontrack = (event) => { console.log("ontrack", event); let stream = event.streams[0]; let sid = fixedDecodeURIComponent(stream.id); let id = sid.split(':')[0]; // var name = Base64.decode(sid.split(':')[1]); console.log(id, uid); if (id === uid) { return; } event.track.onmute = (event) => { console.log("onmute", event); }; let aid = 'peer-audio-' + id; let el = select('#' + aid); if (el) { el.srcObject = stream; } else { el = document.createElement(event.track.kind) el.id = aid; el.srcObject = stream; el.autoplay = true; el.controls = false; select('#peer').appendChild(el); } }; try { const constraints = { audio: { echoCancellation: cancellingNoise, noiseSuppression: cancellingNoise }, video: false }; Voice.stream = await navigator.mediaDevices.getUserMedia(constraints); } catch (err) { if (Voice.status == VoiceStatus.CONNECTTING) { Voice.errorMessage = "麦克风权限获取失败"; Voice.status = VoiceStatus.ERROR; } return; } Voice.stream.getTracks().forEach((track) => { track.enabled = !mutting; Voice.conn.addTrack(track, Voice.stream); }); await Voice.conn.setLocalDescription(await Voice.conn.createOffer()); res = await rpc('publish', [rnameRPC, unameRPC, JSON.stringify(Voice.conn.localDescription)]); if (res.data) { let jsep = JSON.parse(res.data.jsep); if (jsep.type == 'answer') { await Voice.conn.setRemoteDescription(jsep); ucid = res.data.track; await subscribe(Voice.conn); } } else { throw new Error('未知错误'); } Voice.conn.oniceconnectionstatechange = e => { if (Voice.conn.iceConnectionState == "disconnected" || Voice.conn.iceConnectionState == "failed" || Voice.conn.iceConnectionState == "closed") { Voice.errorMessage = "连接断开"; Voice.status = VoiceStatus.ERROR; } else { if (Voice.status == VoiceStatus.ERROR) { Voice.status = Voice._mutting ? VoiceStatus.MUTED : VoiceStatus.UNMUTED; } } } } async function rpc(method, params = [], retryTime = -1) { try { const response = await window.videoTogetherExtension.Fetch(extension.video_together_host + "/kraken", "POST", { id: generateUUID(), method: method, params: params }, { method: 'POST', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, *cors, same-origin cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached credentials: 'omit', // include, *same-origin, omit headers: { 'Content-Type': 'application/json' }, redirect: 'follow', // manual, *follow, error referrerPolicy: 'no-referrer', // no-referrer, *client body: JSON.stringify({ id: generateUUID(), method: method, params: params }) // body data type must match "Content-Type" header }); return await response.json(); // parses JSON response into native JavaScript objects } catch (err) { if (Voice.status == VoiceStatus.STOP) { return; } if (retryTime == 0) { throw err; } await new Promise(r => setTimeout(r, 1000)); return await rpc(method, params, retryTime - 1); } } }, stop: () => { try { Voice.conn.getSenders().forEach(s => { if (s.track) { s.track.stop(); } }); } catch (e) { }; [...select('#peer').querySelectorAll("*")].forEach(e => e.remove()); try { Voice.conn.close(); delete Voice.conn; } catch { } try { Voice.stream.getTracks().forEach(function (track) { track.stop(); }); delete Voice.stream; } catch { } Voice.status = VoiceStatus.STOP; }, mute: () => { Voice.conn.getSenders().forEach(s => { if (s.track) { s.track.enabled = false; } }); Voice._mutting = true; Voice.status = VoiceStatus.MUTED; }, unmute: () => { Voice.conn.getSenders().forEach(s => { if (s.track) { s.track.enabled = true; } }); Voice._mutting = false; Voice.status = VoiceStatus.UNMUTED; }, updateVoiceSetting: async (cancellingNoise = false) => { const constraints = { audio: { echoCancellation: cancellingNoise, noiseSuppression: cancellingNoise }, video: false }; try { prevStream = Voice.stream; Voice.stream = await navigator.mediaDevices.getUserMedia(constraints); Voice.conn.getSenders().forEach(s => { if (s.track) { s.replaceTrack(Voice.stream.getTracks().find(t => t.kind == s.track.kind)); } }) prevStream.getTracks().forEach(t => t.stop()); delete prevStream; } catch (e) { console.log(e); }; } } function generateUUID() { if (crypto.randomUUID != undefined) { return crypto.randomUUID(); } return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } function generateTempUserId() { return generateUUID() + ":" + Date.now() / 1000; } /** * * Base64 encode / decode * http://www.webtoolkit.info * **/ const Base64 = { // private property _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" // public method for encoding , encode: function (input) { var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = Base64._utf8_encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); } // Whend return output; } // End Function encode // public method for decoding , decode: function (input) { var output = ""; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); while (i < input.length) { enc1 = this._keyStr.indexOf(input.charAt(i++)); enc2 = this._keyStr.indexOf(input.charAt(i++)); enc3 = this._keyStr.indexOf(input.charAt(i++)); enc4 = this._keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } } // Whend output = Base64._utf8_decode(output); return output; } // End Function decode // private method for UTF-8 encoding , _utf8_encode: function (string) { var utftext = ""; string = string.replace(/\r\n/g, "\n"); for (var n = 0; n < string.length; n++) { var c = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if ((c > 127) && (c < 2048)) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } // Next n return utftext; } // End Function _utf8_encode // private method for UTF-8 decoding , _utf8_decode: function (utftext) { var string = ""; var i = 0; var c, c1, c2, c3; c = c1 = c2 = 0; while (i < utftext.length) { c = utftext.charCodeAt(i); if (c < 128) { string += String.fromCharCode(c); i++; } else if ((c > 191) && (c < 224)) { c2 = utftext.charCodeAt(i + 1); string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; } else { c2 = utftext.charCodeAt(i + 1); c3 = utftext.charCodeAt(i + 2); string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; } } // Whend return string; } // End Function _utf8_decode } let GotTxtMsgCallback = undefined; class VideoTogetherFlyPannel { constructor() { this.sessionKey = "VideoTogetherFlySaveSessionKey"; this.isInRoom = false; this.isMain = (window.self == window.top); setInterval(() => { if (getEnableMiniBar() && getEnableTextMessage() && document.fullscreenElement != undefined && (extension.ctxRole == extension.RoleEnum.Master || extension.ctxRole == extension.RoleEnum.Member)) { const qs = (s) => this.fullscreenWrapper.querySelector(s); try { qs("#memberCount").innerText = extension.ctxMemberCount; qs("#send-button").disabled = !extension.ctxWsIsOpen; } catch { }; if (document.fullscreenElement.contains(this.fullscreenSWrapper)) { return; } let shadowWrapper = document.createElement("div"); this.fullscreenSWrapper = shadowWrapper; shadowWrapper.id = "VideoTogetherfullscreenSWrapper"; let wrapper; try { wrapper = AttachShadow(shadowWrapper, { mode: "open" }); wrapper.addEventListener('keydown', (e) => e.stopPropagation()); this.fullscreenWrapper = wrapper; } catch (e) { console.error(e); } wrapper.innerHTML = `