您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Monitor and detect streaming media loading on web pages
// ==UserScript== // @name StreamWatch - 流媒体监控 // @name:zh-CN StreamWatch - 流媒体监控 // @description:zh-CN 监控和检测网页中的流媒体加载情况 // @author MissChina // @match *://*/* // @grant none // @namespace https://github.com/MissChina/StreamWatch // @version 3.2.0 // @icon https://github.com/MissChina/StreamWatch/raw/main/streamwatch.png // @license Custom License - No Commercial Use, Attribution Required // @homepageURL https://github.com/MissChina/StreamWatch // @supportURL https://github.com/MissChina/StreamWatch/issues // @description Monitor and detect streaming media loading on web pages // ==/UserScript== (function(){ 'use strict'; const CONFIG={VERSION:'3.2.0',SCAN_INTERVAL:3000,MAX_STREAMS:100,TOAST_DURATION:2000,AUTO_HIDE_DELAY:3000}; class StreamWatch{ constructor(){ this.streams=new Map(); this.isActive=false; this.scanTimer=null; this.hideTimer=null; this.initUI(); this.bindUIEvents(); this.interceptNetwork(); this.hookHls(); setTimeout(()=>this.toggleMonitoring(),1000); console.log(`🎬 StreamWatch v${CONFIG.VERSION} 已启动`); } analyzeUrl(url){ if(!url || this.streams.has(url)) return; if(url.includes('.m3u8')) this.addStream(url,'m3u8'); else if(url.match(/\.(mp4|webm|mov|mkv|mpd)([?#].*)?$/i)) this.addStream(url,'video'); } addStream(url,type){ if(this.streams.size>=CONFIG.MAX_STREAMS) return; const s={url,type,title:this.getTitle(url)}; this.streams.set(url,s); this.renderStream(s); this.showToast(`检测到 ${type.toUpperCase()} 流`); } getTitle(url){ try{return new URL(url).pathname.split('/').pop()||new URL(url).hostname;}catch{return url.slice(0,30);} } interceptNetwork(){ const origFetch=window.fetch; window.fetch=async(...args)=>{ const resp=await origFetch(...args); try{ const ct=resp.headers.get('content-type')||''; const clone=resp.clone(); if(ct.includes('mpegurl')) this.addStream(resp.url,'m3u8'); else clone.text().then(t=>{if(t.startsWith('#EXTM3U')) this.addStream(resp.url,'m3u8');}).catch(()=>{}); }catch{} return resp; }; const origOpen=XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open=function(m,url,...r){ this._sw_url=url; return origOpen.call(this,m,url,...r); }; const origSend=XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send=function(...r){ this.addEventListener('load',()=>{ try{ const ct=this.getResponseHeader('content-type')||''; if(ct.includes('mpegurl')||(this.responseText&&this.responseText.startsWith('#EXTM3U'))) window.streamWatch?.addStream(this._sw_url,'m3u8'); }catch{} }); return origSend.apply(this,r); }; } hookHls(){ setInterval(()=>{ if(window.Hls&&window.Hls.isSupported&&!window.Hls._patched){ const origLoad=window.Hls.prototype.loadSource; window.Hls.prototype.loadSource=function(url){ window.streamWatch?.addStream(url,'m3u8'); return origLoad.call(this,url); }; window.Hls._patched=true; console.log('[StreamWatch] Hls.js 已挂钩'); } },2000); } initUI(){ const style=document.createElement('style'); style.innerHTML=` /* iOS 26 Liquid Glass 风格 */ #sw-panel{ position:fixed;top:60px;right:20px;width:300px;background:rgba(255,255,255,0.15); color:#fff;font-size:13px;border-radius:16px; box-shadow:0 12px 28px rgba(0,0,0,0.4); display:none;z-index:999999;overflow:hidden;resize:both; backdrop-filter:blur(16px) saturate(180%) contrast(120%); transition:opacity 0.3s, transform 0.2s; } #sw-panel header{ background:linear-gradient(135deg, rgba(255,255,255,0.25), rgba(255,255,255,0.05)); padding:8px 12px;display:flex;justify-content:space-between;align-items:center; font-weight:bold;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.4); border-bottom:1px solid rgba(255,255,255,0.2); border-top-left-radius:16px;border-top-right-radius:16px; } #sw-list{padding:8px;max-height:220px;overflow:auto;} #sw-list div{ padding:6px;margin-bottom:6px;background:rgba(255,255,255,0.1); border-left:4px solid #4caf50;border-radius:8px; box-shadow:0 4px 10px rgba(0,0,0,0.2);transition:background 0.2s, transform 0.2s; } #sw-list div:hover{ background:rgba(255,255,255,0.2); transform:translateX(2px); } #sw-panel footer{ padding:6px;text-align:right;border-top:1px solid rgba(255,255,255,0.2); } #sw-panel button{ background:linear-gradient(135deg,#4caf50,#81c784); color:#fff;border:none;padding:4px 8px;margin-left:4px; border-radius:8px;cursor:pointer;font-size:12px;transition:background 0.2s, transform 0.2s; } #sw-panel button:hover{ background:linear-gradient(135deg,#81c784,#66bb6a); transform:scale(1.05); } #sw-fab{ position:fixed;bottom:20px;right:20px;width:52px;height:52px; border-radius:50%;background:linear-gradient(135deg,#ff8a65,#ff7043); color:#fff;display:flex;align-items:center;justify-content:center;font-size:26px; cursor:pointer;box-shadow:0 8px 20px rgba(0,0,0,0.6);transition:transform 0.2s, box-shadow 0.2s; } #sw-fab:hover{ transform:scale(1.2); box-shadow:0 10px 28px rgba(0,0,0,0.8); } `; document.head.appendChild(style); const panel=document.createElement('div'); panel.id='sw-panel'; panel.innerHTML=` <header> StreamWatch v${CONFIG.VERSION} <div> <button id="sw-min">—</button> <button id="sw-close">✖</button> </div> </header> <div id="sw-list"><em>等待检测流媒体...</em></div> <footer> <button id="sw-export">导出</button> <button id="sw-clear">清空</button> </footer> `; document.body.appendChild(panel); const fab=document.createElement('div'); fab.id='sw-fab'; fab.textContent='🎬'; document.body.appendChild(fab); // 拖拽 let isDragging=false,offsetX=0,offsetY=0; const header=panel.querySelector('header'); header.style.cursor='move'; header.addEventListener('mousedown',e=>{isDragging=true; offsetX=e.clientX-panel.offsetLeft; offsetY=e.clientY-panel.offsetTop;}); document.addEventListener('mousemove',e=>{if(isDragging){panel.style.left=e.clientX-offsetX+'px';panel.style.top=e.clientY-offsetY+'px';panel.style.right='auto';}}); document.addEventListener('mouseup',()=>{isDragging=false;}); // 自动半透明隐藏 const resetHideTimer=()=>{ panel.style.opacity='1'; if(this.hideTimer) clearTimeout(this.hideTimer); this.hideTimer=setTimeout(()=>{panel.style.opacity='0.3';},CONFIG.AUTO_HIDE_DELAY); }; panel.addEventListener('mousemove',resetHideTimer); panel.addEventListener('mouseenter',()=>panel.style.opacity='1'); panel.addEventListener('mouseleave',resetHideTimer); } bindUIEvents(){ const panel=document.getElementById('sw-panel'),fab=document.getElementById('sw-fab'); fab.addEventListener('click',()=>{panel.style.display='block'; fab.style.display='none';}); document.getElementById('sw-min').addEventListener('click',()=>{panel.style.display='none'; fab.style.display='flex';}); document.getElementById('sw-close').addEventListener('click',()=>{panel.style.display='none'; fab.style.display='flex';}); document.getElementById('sw-clear').addEventListener('click',()=>this.clearStreams()); document.getElementById('sw-export').addEventListener('click',()=>this.exportData()); } renderStream(s){ const list=document.getElementById('sw-list'),item=document.createElement('div'); item.innerHTML=`<div><strong>${s.title}</strong> <small>[${s.type}]</small></div> <div style="word-break:break-all;color:#eee;">${s.url}</div> <button onclick="navigator.clipboard.writeText('${s.url}')">复制</button> <button onclick="navigator.clipboard.writeText('${this.getFFmpeg(s.url,s.type)}')">FFmpeg</button>`; if(list.querySelector('em')) list.innerHTML=''; list.appendChild(item); } clearStreams(){ this.streams.clear(); document.getElementById('sw-list').innerHTML='<em>等待检测流媒体...</em>'; this.showToast('已清空'); } exportData(){ if(this.streams.size===0) return this.showToast('没有可导出数据'); const blob=new Blob([JSON.stringify([...this.streams.values()],null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`streamwatch_${Date.now()}.json`; a.click(); this.showToast('导出成功'); } showToast(msg){ const t=document.createElement('div'); t.textContent=msg; t.style=`position:fixed;bottom:80px;right:20px;background:rgba(0,0,0,0.7);padding:6px 12px;border-radius:8px;color:#fff;z-index:100000;opacity:0.95;font-weight:bold;backdrop-filter:blur(4px);`; document.body.appendChild(t); setTimeout(()=>t.remove(),CONFIG.TOAST_DURATION); } toggleMonitoring(){ this.isActive=!this.isActive; this.isActive?this.startScan():this.stopScan(); this.showToast(this.isActive?'开始监控':'停止监控'); } startScan(){ this.scanTimer=setInterval(()=>{ document.querySelectorAll('video, audio, source').forEach(el=>{ if(el.src)this.analyzeUrl(el.src); if(el.currentSrc)this.analyzeUrl(el.currentSrc); }); },CONFIG.SCAN_INTERVAL); } stopScan(){ if(this.scanTimer) clearInterval(this.scanTimer); } getFFmpeg(url,type){ return type==='m3u8'?`ffmpeg -i "${url}" -c copy -bsf:a aac_adtstoasc output.mp4`:`ffmpeg -i "${url}" -c copy output.mp4`; } } if(!window.streamWatch) window.streamWatch=new StreamWatch(); })();