background.js
// 监听标签页更新
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
// 检查是否启用了嗅探
chrome.storage.local.get(['snifferEnabled'], function(result) {
if (result.snifferEnabled) {
// 通知内容脚本开始嗅探
chrome.tabs.sendMessage(tabId, {
action: 'updateSniffer',
enabled: true
});
}
});
}
});
// 安装或更新插件时初始化设置
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.set({
snifferEnabled: false,
playbackSpeed: 1.0,
detectedVideos: []
});
});
content.js
// 全局变量
let snifferEnabled = false;
let playbackSpeed = 1.0;
let detectedVideos = [];
let selectedVideoIndex = -1;
let observer = null;
// 初始化
function initialize() {
// 从存储中获取设置
chrome.storage.local.get(['snifferEnabled', 'playbackSpeed'], function(result) {
snifferEnabled = result.snifferEnabled || false;
playbackSpeed = result.playbackSpeed || 1.0;
if (snifferEnabled) {
startVideoSniffer();
}
});
// 监听来自popup的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.action === 'updateSniffer') {
snifferEnabled = request.enabled;
if (snifferEnabled) {
startVideoSniffer();
} else {
stopVideoSniffer();
}
} else if (request.action === 'updateSpeed') {
playbackSpeed = request.speed;
applyPlaybackSpeed();
} else if (request.action === 'selectVideo') {
selectedVideoIndex = request.index;
applyPlaybackSpeed();
}
});
}
// 开始视频嗅探
function startVideoSniffer() {
// 立即扫描当前页面上的视频
scanForVideos();
// 设置MutationObserver监听DOM变化
if (!observer) {
observer = new MutationObserver(function(mutations) {
scanForVideos();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// 停止视频嗅探
function stopVideoSniffer() {
if (observer) {
observer.disconnect();
observer = null;
}
}
// 扫描页面上的视频
function scanForVideos() {
if (!snifferEnabled) return;
const videos = Array.from(document.querySelectorAll('video'));
const iframes = Array.from(document.querySelectorAll('iframe'));
// 处理直接的video标签
detectedVideos = videos.map(video => ({
element: video,
src: video.src || (video.querySelector('source') ? video.querySelector('source').src : '未知源')
}));
// 尝试处理iframe中的视频(受同源策略限制,可能无法访问)
iframes.forEach(iframe => {
try {
const iframeVideos = Array.from(iframe.contentDocument.querySelectorAll('video'));
detectedVideos = detectedVideos.concat(iframeVideos.map(video => ({
element: video,
src: video.src || (video.querySelector('source') ? video.querySelector('source').src : '未知源')
})));
} catch (e) {
// 跨域iframe无法访问,忽略错误
}
});
// 通知popup更新视频列表
chrome.runtime.sendMessage({
action: 'updateVideoList',
videos: detectedVideos.map(v => ({src: v.src}))
});
// 应用播放速度
applyPlaybackSpeed();
}
// 应用播放速度
function applyPlaybackSpeed() {
if (detectedVideos.length === 0) return;
if (selectedVideoIndex >= 0 && selectedVideoIndex < detectedVideos.length) {
// 只调整选中的视频
const video = detectedVideos[selectedVideoIndex].element;
video.playbackRate = playbackSpeed;
} else {
// 调整所有检测到的视频
detectedVideos.forEach(video => {
video.element.playbackRate = playbackSpeed;
});
}
}
// 初始化插件
initialize();
manifest.json
{
"manifest_version": 3,
"name": "视频嗅探与倍速控制",
"version": "1.0",
"description": "自动嗅探网站视频并控制播放速度(0.033x-30x)",
"permissions": ["activeTab", "storage", "scripting"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
}
},
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js"
}
}
popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>视频嗅探与倍速控制</title>
<style>
body {
width: 300px;
padding: 10px;
font-family: Arial, sans-serif;
}
.container {
display: flex;
flex-direction: column;
gap: 15px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.control-group {
display: flex;
align-items: center;
justify-content: space-between;
}
.video-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
}
.video-item {
padding: 5px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.video-item:hover {
background-color: #f5f5f5;
}
.speed-control {
display: flex;
align-items: center;
gap: 10px;
}
.speed-value {
width: 50px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h2>视频嗅探与倍速控制</h2>
<div class="control-group">
<span>视频嗅探</span>
<label class="switch">
<input type="checkbox" id="sniffer-toggle">
<span class="slider"></span>
</label>
</div>
<div class="speed-control">
<span>播放速度:</span>
<input type="range" id="speed-slider" min="0.033" max="30" step="0.1" value="1">
<span id="speed-value" class="speed-value">1.0x</span>
</div>
<div>
<h3>检测到的视频:</h3>
<div id="video-list" class="video-list">
<div class="video-item">暂无检测到视频</div>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
popup.js
document.addEventListener('DOMContentLoaded', function() {
const snifferToggle = document.getElementById('sniffer-toggle');
const speedSlider = document.getElementById('speed-slider');
const speedValue = document.getElementById('speed-value');
const videoList = document.getElementById('video-list');
// 加载保存的设置
chrome.storage.local.get(['snifferEnabled', 'playbackSpeed', 'detectedVideos'], function(result) {
snifferToggle.checked = result.snifferEnabled || false;
const speed = result.playbackSpeed || 1;
speedSlider.value = speed;
speedValue.textContent = speed.toFixed(1) + 'x';
updateVideoList(result.detectedVideos || []);
});
// 监听嗅探开关变化
snifferToggle.addEventListener('change', function() {
const enabled = snifferToggle.checked;
chrome.storage.local.set({snifferEnabled: enabled});
// 通知内容脚本更新嗅探状态
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
action: 'updateSniffer',
enabled: enabled
});
});
});
// 监听速度滑块变化
speedSlider.addEventListener('input', function() {
const speed = parseFloat(speedSlider.value);
speedValue.textContent = speed.toFixed(1) + 'x';
chrome.storage.local.set({playbackSpeed: speed});
// 通知内容脚本更新播放速度
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
action: 'updateSpeed',
speed: speed
});
});
});
// 更新视频列表
function updateVideoList(videos) {
if (!videos || videos.length === 0) {
videoList.innerHTML = '<div class="video-item">暂无检测到视频</div>';
return;
}
videoList.innerHTML = '';
videos.forEach((video, index) => {
const videoItem = document.createElement('div');
videoItem.className = 'video-item';
videoItem.textContent = `视频 ${index + 1}: ${video.src.substring(0, 50)}...`;
videoItem.addEventListener('click', function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
action: 'selectVideo',
index: index
});
});
});
videoList.appendChild(videoItem);
});
}
// 监听来自内容脚本的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.action === 'updateVideoList') {
chrome.storage.local.set({detectedVideos: request.videos});
updateVideoList(request.videos);
}
});
});