// ==UserScript== // @name 12345 Video Track Probe // @namespace http://tampermonkey.net/ // @version 1.0 // @description Probe page/WebRTC video input/output tracks and show active RTP stats. // @author You // @match *://yhy-yw.22sj.cn:*/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; const originalGetUserMedia = navigator.mediaDevices?.getUserMedia?.bind(navigator.mediaDevices); const originalAddEventListener = RTCPeerConnection.prototype.addEventListener; const originalAddTrack = RTCPeerConnection.prototype.addTrack; const originalAddTransceiver = RTCPeerConnection.prototype.addTransceiver; const originalSetRemoteDescription = RTCPeerConnection.prototype.setRemoteDescription; const mediaSrcObjectDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'srcObject'); const candidates = new Map(); const senderCandidates = new Map(); const receivers = new Set(); const senders = new Set(); const localCaptureTrackIds = new Set(); const inboundStatMemory = new Map(); const outboundStatMemory = new Map(); let selectedTrackId = null; let selectedSenderId = null; let lastPanelRenderAt = 0; function nowText() { return new Date().toLocaleTimeString([], { hour12: false }); } function isLiveVideoTrack(track) { return track?.kind === 'video' && track.readyState === 'live'; } function shortId(id) { if (!id) return 'none'; if (id.length <= 18) return id; return `${id.slice(0, 8)}...${id.slice(-6)}`; } function trackSettings(track) { try { return track?.getSettings?.() || {}; } catch { return {}; } } function log(message) { console.log(`[Video Track Probe] ${message}`); const box = document.getElementById('video-track-probe-log'); if (!box) return; const row = document.createElement('div'); row.textContent = `[${nowText()}] ${message}`; box.appendChild(row); while (box.children.length > 80) box.removeChild(box.firstChild); box.scrollTop = box.scrollHeight; } function ensureCandidate(track, source) { if (!isLiveVideoTrack(track)) return null; let candidate = candidates.get(track.id); if (!candidate) { candidate = { track, receiver: null, sources: new Set(), elements: new Set(), isLocalCapture: localCaptureTrackIds.has(track.id), firstSeenAt: Date.now(), lastSeenAt: Date.now(), lastPacketAt: 0, bytesReceived: 0, packetsReceived: 0, bytesDelta: 0, packetsDelta: 0, framesReceived: 0, framesDecoded: 0, framesDropped: 0, framesDelta: 0, frameWidth: 0, frameHeight: 0, framesPerSecond: 0, score: 0, }; candidates.set(track.id, candidate); log(`found video track ${track.id} (${source})`); } candidate.track = track; candidate.sources.add(source); candidate.lastSeenAt = Date.now(); candidate.isLocalCapture = localCaptureTrackIds.has(track.id); return candidate; } function ensureSenderCandidate(sender, track, source) { if (!sender) return null; const senderTrack = track || sender.track; if (senderTrack && senderTrack.kind !== 'video') return null; const id = sender.__videoTrackProbeId || `sender-${senderCandidates.size + 1}`; sender.__videoTrackProbeId = id; let candidate = senderCandidates.get(id); if (!candidate) { candidate = { id, sender, track: senderTrack || null, sources: new Set(), isLocalCapture: Boolean(senderTrack && localCaptureTrackIds.has(senderTrack.id)), firstSeenAt: Date.now(), lastSeenAt: Date.now(), bytesSent: 0, packetsSent: 0, bytesDelta: 0, packetsDelta: 0, framesEncoded: 0, framesSent: 0, framesDelta: 0, frameWidth: 0, frameHeight: 0, score: 0, }; senderCandidates.set(id, candidate); log(`found video sender ${id} (${source}) track=${senderTrack?.id || 'null'}`); } candidate.sender = sender; candidate.track = senderTrack || sender.track || candidate.track; candidate.sources.add(source); candidate.lastSeenAt = Date.now(); candidate.isLocalCapture = Boolean(candidate.track && localCaptureTrackIds.has(candidate.track.id)); senders.add(sender); wrapSender(sender); return candidate; } function wrapSender(sender) { if (!sender || sender.__videoTrackProbeWrapped) return; const originalReplaceTrack = sender.replaceTrack?.bind(sender); if (!originalReplaceTrack) return; sender.replaceTrack = async function (track) { const result = await originalReplaceTrack(track); if (!track || track.kind === 'video') { const candidate = ensureSenderCandidate(sender, track, 'replaceTrack'); if (candidate) { candidate.track = track; log(`sender ${candidate.id} replaceTrack -> ${track?.id || 'null'}`); } } return result; }; sender.__videoTrackProbeWrapped = true; } function rememberReceiver(receiver, source) { if (!receiver?.track || !isLiveVideoTrack(receiver.track)) return; receivers.add(receiver); const candidate = ensureCandidate(receiver.track, source); if (candidate) candidate.receiver = receiver; } function rememberStream(stream, source, element) { if (!stream || !(stream instanceof MediaStream)) return; for (const track of stream.getVideoTracks()) { const candidate = ensureCandidate(track, source); if (candidate && element) candidate.elements.add(element); } } function scanMediaElements() { document.querySelectorAll('video').forEach((element, index) => { rememberStream(element.srcObject, `element-${index}:video`, element); }); } function applyInboundVideoStat(stat, candidate) { const prev = inboundStatMemory.get(stat.id); const bytes = stat.bytesReceived || 0; const packets = stat.packetsReceived || 0; const framesReceived = stat.framesReceived || 0; const framesDecoded = stat.framesDecoded || 0; const framesDropped = stat.framesDropped || 0; candidate.bytesDelta = prev ? Math.max(0, bytes - prev.bytes) : 0; candidate.packetsDelta = prev ? Math.max(0, packets - prev.packets) : 0; candidate.framesDelta = prev ? Math.max(0, framesDecoded - prev.framesDecoded) : 0; candidate.bytesReceived = bytes; candidate.packetsReceived = packets; candidate.framesReceived = framesReceived; candidate.framesDecoded = framesDecoded; candidate.framesDropped = framesDropped; candidate.frameWidth = stat.frameWidth || candidate.frameWidth; candidate.frameHeight = stat.frameHeight || candidate.frameHeight; candidate.framesPerSecond = stat.framesPerSecond || candidate.framesPerSecond; if (candidate.bytesDelta > 0 || candidate.packetsDelta > 0 || candidate.framesDelta > 0) { candidate.lastPacketAt = Date.now(); } inboundStatMemory.set(stat.id, { bytes, packets, framesDecoded }); } function applyOutboundVideoStat(stat, candidate) { const prev = outboundStatMemory.get(stat.id); const bytes = stat.bytesSent || 0; const packets = stat.packetsSent || 0; const framesEncoded = stat.framesEncoded || 0; const framesSent = stat.framesSent || 0; candidate.bytesDelta = prev ? Math.max(0, bytes - prev.bytes) : 0; candidate.packetsDelta = prev ? Math.max(0, packets - prev.packets) : 0; candidate.framesDelta = prev ? Math.max(0, framesSent - prev.framesSent) : 0; candidate.bytesSent = bytes; candidate.packetsSent = packets; candidate.framesEncoded = framesEncoded; candidate.framesSent = framesSent; candidate.frameWidth = stat.frameWidth || candidate.frameWidth; candidate.frameHeight = stat.frameHeight || candidate.frameHeight; outboundStatMemory.set(stat.id, { bytes, packets, framesSent }); } async function refreshStats() { for (const receiver of Array.from(receivers)) { if (!receiver.track || receiver.track.readyState !== 'live') { receivers.delete(receiver); continue; } const candidate = ensureCandidate(receiver.track, 'receiver-stats'); if (!candidate || !receiver.getStats) continue; candidate.receiver = receiver; try { const report = await receiver.getStats(); report.forEach((stat) => { const kind = stat.kind || stat.mediaType; if (stat.type !== 'inbound-rtp' || kind !== 'video') return; applyInboundVideoStat(stat, candidate); }); } catch (error) { candidate.sources.add(`stats-error:${error.message || error}`); } } chooseActiveTrack(); await refreshSenderStats(); chooseActiveSender(); } async function refreshSenderStats() { for (const sender of Array.from(senders)) { if (!sender) { senders.delete(sender); continue; } const candidate = ensureSenderCandidate(sender, sender.track, 'sender-stats'); if (!candidate || !sender.getStats) continue; try { const report = await sender.getStats(); report.forEach((stat) => { const kind = stat.kind || stat.mediaType; if (stat.type !== 'outbound-rtp' || kind !== 'video') return; applyOutboundVideoStat(stat, candidate); }); } catch (error) { candidate.sources.add(`sender-stats-error:${error.message || error}`); } } } function scoreCandidate(candidate) { if (!candidate || !isLiveVideoTrack(candidate.track)) return -1; let score = 0; if (!candidate.track.muted) score += 1000; if (candidate.receiver) score += 300; if (candidate.bytesDelta > 0) score += 4000 + Math.min(1500, candidate.bytesDelta); if (candidate.packetsDelta > 0) score += 2000 + Math.min(800, candidate.packetsDelta * 8); if (candidate.framesDelta > 0) score += 3000 + Math.min(1200, candidate.framesDelta * 40); if (Date.now() - candidate.lastPacketAt < 3000) score += 1200; if (candidate.frameWidth > 0 && candidate.frameHeight > 0) score += 400; if (candidate.elements.size > 0) score += 200; if (candidate.isLocalCapture) score -= 2500; return score; } function chooseActiveTrack() { let best = null; for (const candidate of candidates.values()) { candidate.score = scoreCandidate(candidate); if (!best || candidate.score > best.score) best = candidate; } if (!best || best.score < 0) return; if (best.track.id !== selectedTrackId) { selectedTrackId = best.track.id; log(`active video looks like ${best.track.id}, score=${Math.round(best.score)}, ${best.frameWidth}x${best.frameHeight}`); } } function scoreSenderCandidate(candidate) { if (!candidate) return -1; let score = 0; if (candidate.track?.readyState === 'live') score += 1000; if (candidate.track && !candidate.track.muted) score += 400; if (candidate.bytesSent > 0) score += 300; if (candidate.bytesDelta > 0) score += 4500 + Math.min(1800, candidate.bytesDelta); if (candidate.packetsDelta > 0) score += 1800 + Math.min(900, candidate.packetsDelta * 10); if (candidate.framesDelta > 0) score += 2200 + Math.min(1000, candidate.framesDelta * 30); if (candidate.frameWidth > 0 && candidate.frameHeight > 0) score += 300; if (candidate.isLocalCapture) score += 2500; if (candidate.sources.has('pc-addTrack')) score += 1500; return score; } function chooseActiveSender() { let best = null; for (const candidate of senderCandidates.values()) { candidate.score = scoreSenderCandidate(candidate); if (!best || candidate.score > best.score) best = candidate; } if (!best || best.score < 0) return; if (best.id !== selectedSenderId) { selectedSenderId = best.id; log(`active output sender looks like ${best.id}, track=${best.track?.id || 'null'}, score=${Math.round(best.score)}`); } } function dumpInputs() { return Array.from(candidates.values()) .sort((a, b) => b.score - a.score) .map((candidate) => { const settings = trackSettings(candidate.track); return { selected: candidate.track.id === selectedTrackId, id: candidate.track.id, label: candidate.track.label, enabled: candidate.track.enabled, muted: candidate.track.muted, readyState: candidate.track.readyState, isLocalCapture: candidate.isLocalCapture, hasReceiver: Boolean(candidate.receiver), bytesReceived: candidate.bytesReceived, packetsReceived: candidate.packetsReceived, bytesDelta: candidate.bytesDelta, packetsDelta: candidate.packetsDelta, framesReceived: candidate.framesReceived, framesDecoded: candidate.framesDecoded, framesDropped: candidate.framesDropped, framesDelta: candidate.framesDelta, frameWidth: candidate.frameWidth || settings.width || 0, frameHeight: candidate.frameHeight || settings.height || 0, framesPerSecond: candidate.framesPerSecond || settings.frameRate || 0, score: Math.round(candidate.score), sources: Array.from(candidate.sources), elements: Array.from(candidate.elements).map((element) => ({ tag: element.tagName, paused: element.paused, muted: element.muted, videoWidth: element.videoWidth, videoHeight: element.videoHeight, })), }; }); } function dumpOutputs() { return Array.from(senderCandidates.values()) .sort((a, b) => b.score - a.score) .map((candidate) => { const settings = trackSettings(candidate.track); return { selected: candidate.id === selectedSenderId, id: candidate.id, trackId: candidate.track?.id || null, trackLabel: candidate.track?.label || null, trackEnabled: candidate.track?.enabled ?? null, trackMuted: candidate.track?.muted ?? null, trackReadyState: candidate.track?.readyState || null, isLocalCapture: candidate.isLocalCapture, bytesSent: candidate.bytesSent, packetsSent: candidate.packetsSent, bytesDelta: candidate.bytesDelta, packetsDelta: candidate.packetsDelta, framesEncoded: candidate.framesEncoded, framesSent: candidate.framesSent, framesDelta: candidate.framesDelta, frameWidth: candidate.frameWidth || settings.width || 0, frameHeight: candidate.frameHeight || settings.height || 0, score: Math.round(candidate.score), sources: Array.from(candidate.sources), }; }); } function dump() { return { selectedInputTrackId: selectedTrackId, selectedOutputSenderId: selectedSenderId, inputs: dumpInputs(), outputs: dumpOutputs(), }; } function activityPct(row) { return Math.min(100, Math.max( row.framesDelta * 8, row.bytesDelta / 40, row.packetsDelta * 2 )); } function renderPanel() { if (!document.body) return; let panel = document.getElementById('video-track-probe-panel'); if (!panel) { panel = document.createElement('div'); panel.id = 'video-track-probe-panel'; panel.style.cssText = [ 'position:fixed', 'top:20px', 'left:20px', 'width:480px', 'max-height:70vh', 'z-index:999998', 'box-sizing:border-box', 'padding:12px', 'border-radius:8px', 'background:rgba(3,7,18,.96)', 'color:#e5e7eb', 'font:12px ui-monospace,Menlo,monospace', 'box-shadow:0 12px 32px rgba(0,0,0,.35)', 'overflow:auto', ].join(';'); panel.innerHTML = `