背景:
使用了某在线教育的课程,发现没有倍速功能很难受。

content.js
javascript
(function() {
'use strict';
let speedControlPanel = null;
let isPanelVisible = false;
let currentSpeed = 1.0;
let hasInitialized = false;
// 延迟初始化,等待页面完全加载
function delayedInit() {
if (hasInitialized) return;
hasInitialized = true;
console.log(' Video Speed Controller 开始初始化...');
// 创建控制面板
createSpeedControlPanel();
// 绑定键盘事件
document.addEventListener('keydown', handleKeyboardShortcuts);
// 持续监听视频元素
observeVideoElements();
// 尝试恢复速度
setTimeout(() => {
restoreLastSpeed();
}, 2000);
console.log('✅ Video Speed Controller 初始化完成');
console.log('💡 按 Ctrl+Shift+V 或 Cmd+Shift+V 打开控制面板');
}
// 立即执行 + DOMContentLoaded 双重保障
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(delayedInit, 1000);
});
} else {
setTimeout(delayedInit, 1000);
}
// 额外的 MutationObserver 确保在动态内容加载后也能工作
const mainObserver = new MutationObserver(() => {
if (!hasInitialized && document.body) {
delayedInit();
}
});
if (document.documentElement) {
mainObserver.observe(document.documentElement, {
childList: true,
subtree: true
});
}
function createSpeedControlPanel() {
if (speedControlPanel) return;
console.log(' 创建速度控制面板...');
const panel = document.createElement('div');
panel.id = 'video-speed-control-panel';
panel.className = 'video-speed-panel';
panel.innerHTML = `
<div class="panel-header">
<span class="panel-title">⚡ 倍速控制</span>
<button class="panel-close">×</button>
</div>
<div class="panel-content">
<div class="current-speed-display">
<span>当前速度:</span>
<strong id="panel-current-speed">1.0x</strong>
</div>
<div class="speed-buttons">
<button class="speed-btn" data-speed="0.5">0.5x</button>
<button class="speed-btn" data-speed="0.75">0.75x</button>
<button class="speed-btn active" data-speed="1">1x</button>
<button class="speed-btn" data-speed="1.25">1.25x</button>
<button class="speed-btn" data-speed="1.5">1.5x</button>
<button class="speed-btn" data-speed="2">2x</button>
<button class="speed-btn" data-speed="2.5">2.5x</button>
<button class="speed-btn" data-speed="3">3x</button>
</div>
<div class="custom-speed">
<input type="number" id="custom-speed-input" placeholder="自定义速度" min="0.1" max="16" step="0.1">
<button id="custom-speed-btn">应用</button>
</div>
<div class="keyboard-hint">
💡 快捷键: ←减速 | →加速 | R重置 | Ctrl+Shift+V开关
</div>
</div>
`;
// 使用最高优先级插入到 body
if (document.body) {
document.body.appendChild(panel);
console.log('✅ 控制面板已添加到页面');
} else {
console.error('❌ document.body 不存在,无法添加控制面板');
return;
}
speedControlPanel = panel;
bindPanelEvents();
}
function bindPanelEvents() {
if (!speedControlPanel) return;
const closeBtn = speedControlPanel.querySelector('.panel-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
hidePanel();
});
}
const speedButtons = speedControlPanel.querySelectorAll('.speed-btn');
speedButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const speed = parseFloat(btn.dataset.speed);
setAllVideosSpeed(speed);
updateActiveButton(btn);
});
});
const customInput = speedControlPanel.querySelector('#custom-speed-input');
const customBtn = speedControlPanel.querySelector('#custom-speed-btn');
if (customBtn) {
customBtn.addEventListener('click', (e) => {
e.stopPropagation();
const speed = parseFloat(customInput.value);
if (speed && speed >= 0.1 && speed <= 16) {
setAllVideosSpeed(speed);
customInput.value = '';
} else {
alert('请输入 0.1 到 16 之间的有效数字');
}
});
}
if (customInput) {
customInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
customBtn.click();
}
});
}
}
function showPanel() {
if (!speedControlPanel) {
createSpeedControlPanel();
}
if (speedControlPanel) {
speedControlPanel.classList.add('visible');
isPanelVisible = true;
console.log(' 控制面板已显示');
}
}
function hidePanel() {
if (speedControlPanel) {
speedControlPanel.classList.remove('visible');
isPanelVisible = false;
console.log('📱 控制面板已隐藏');
}
}
function togglePanel() {
console.log('🔄 切换控制面板状态');
if (isPanelVisible) {
hidePanel();
} else {
showPanel();
}
}
function updateActiveButton(activeBtn) {
if (!speedControlPanel) return;
const buttons = speedControlPanel.querySelectorAll('.speed-btn');
buttons.forEach(btn => btn.classList.remove('active'));
if (activeBtn) {
activeBtn.classList.add('active');
}
}
function updatePanelSpeedDisplay(speed) {
if (!speedControlPanel) return;
const display = speedControlPanel.querySelector('#panel-current-speed');
if (display) {
display.textContent = speed.toFixed(2) + 'x';
}
}
function setAllVideosSpeed(speed) {
currentSpeed = speed;
let videoCount = 0;
console.log(`🎯 尝试设置视频速度为: ${speed}x`);
// 方法1: 标准 video 标签
const videos = document.querySelectorAll('video');
console.log(`📹 找到 ${videos.length} 个 video 元素`);
videos.forEach((video, index) => {
try {
console.log(` [${index}] video 元素:`, video);
video.playbackRate = speed;
videoCount++;
console.log(` ✅ 已设置 video[${index}] 速度为 ${speed}x`);
} catch (e) {
console.warn(` ❌ 设置 video[${index}] 失败:`, e);
}
});
// 方法2: 查找 shadow DOM 中的视频
const allElements = document.querySelectorAll('*');
allElements.forEach(el => {
if (el.shadowRoot) {
const shadowVideos = el.shadowRoot.querySelectorAll('video');
shadowVideos.forEach(video => {
try {
video.playbackRate = speed;
videoCount++;
console.log('✅ 已设置 shadow DOM 中的 video 速度');
} catch (e) {}
});
}
});
// 保存到本地存储
try {
localStorage.setItem('preferredPlaybackSpeed', speed);
} catch (e) {}
updatePanelSpeedDisplay(speed);
if (videoCount === 0) {
console.warn('⚠️ 未找到可控制的视频元素');
showNoVideoMessage();
} else {
console.log(`✅ 成功设置 ${videoCount} 个视频的速度为 ${speed}x`);
}
return videoCount;
}
function showNoVideoMessage() {
const existingMsg = document.querySelector('.no-video-message');
if (existingMsg) return;
const message = document.createElement('div');
message.className = 'no-video-message';
message.textContent = '⚠️ 未检测到视频元素';
message.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 87, 34, 0.9);
color: white;
padding: 20px 30px;
border-radius: 8px;
z-index: 9999999;
font-size: 16px;
font-weight: bold;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
`;
document.body.appendChild(message);
setTimeout(() => {
if (message.parentNode) {
message.remove();
}
}, 3000);
}
function restoreLastSpeed() {
try {
const savedSpeed = localStorage.getItem('preferredPlaybackSpeed');
if (savedSpeed) {
const speed = parseFloat(savedSpeed);
if (!isNaN(speed)) {
console.log(`💾 恢复上次保存的速度: ${speed}x`);
setAllVideosSpeed(speed);
if (speedControlPanel) {
const buttons = speedControlPanel.querySelectorAll('.speed-btn');
buttons.forEach(btn => {
if (Math.abs(parseFloat(btn.dataset.speed) - speed) < 0.01) {
updateActiveButton(btn);
}
});
}
}
}
} catch (e) {
console.warn('恢复速度失败:', e);
}
}
function handleKeyboardShortcuts(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
// Ctrl/Cmd + Shift + V
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'V' || e.key === 'v')) {
e.preventDefault();
e.stopPropagation();
console.log('⌨️ 检测到快捷键: Ctrl+Shift+V');
togglePanel();
return;
}
if (isPanelVisible) {
return;
}
// 左箭头
if (e.key === 'ArrowLeft') {
e.preventDefault();
adjustSpeed(-0.25);
}
// 右箭头
else if (e.key === 'ArrowRight') {
e.preventDefault();
adjustSpeed(0.25);
}
// R键
else if (e.key === 'r' || e.key === 'R') {
e.preventDefault();
setAllVideosSpeed(1.0);
}
}
function adjustSpeed(delta) {
const newSpeed = Math.max(0.25, Math.min(16, currentSpeed + delta));
console.log(`⚡ 调整速度: ${currentSpeed}x → ${newSpeed}x`);
setAllVideosSpeed(newSpeed);
}
function observeVideoElements() {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeName === 'VIDEO' ||
(node.querySelectorAll && node.querySelectorAll('video').length > 0)) {
console.log('🎥 检测到新视频元素添加');
setTimeout(() => {
restoreLastSpeed();
}, 500);
}
});
}
});
});
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log(' 已开始监听视频元素变化');
}
}
})();
manifest.json
html
{
"manifest_version": 3,
"name": "Video Speed Controller",
"version": "1.0.0",
"description": "为网页视频添加倍速播放控制功能",
"permissions": [
"activeTab",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_idle"
}
]
}
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Speed Controller</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 300px;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
}
h1 {
font-size: 18px;
margin-bottom: 15px;
color: #333;
text-align: center;
}
.control-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: #666;
}
input[type="range"] {
width: 100%;
margin-bottom: 5px;
cursor: pointer;
}
.speed-display {
text-align: center;
font-size: 28px;
font-weight: bold;
color: #667eea;
margin: 15px 0;
padding: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
transition: all 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button.secondary {
background: #e0e0e0;
color: #333;
}
button.secondary:hover {
background: #d0d0d0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.preset-speeds {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 10px;
}
.preset-btn {
padding: 10px;
font-size: 13px;
}
.info {
margin-top: 15px;
padding: 10px;
background: #fff3cd;
border-left: 3px solid #ffc107;
border-radius: 4px;
font-size: 12px;
color: #856404;
}
</style>
</head>
<body>
<h1>🎬 视频倍速控制器</h1>
<div class="control-group">
<div class="speed-display" id="currentSpeed">1.0x</div>
</div>
<div class="control-group">
<label>调整速度:</label>
<input type="range" id="speedSlider" min="0.25" max="4" step="0.25" value="1">
</div>
<button id="applyBtn">应用速度</button>
<div class="preset-speeds">
<button class="preset-btn secondary" data-speed="0.5">0.5x</button>
<button class="preset-btn secondary" data-speed="0.75">0.75x</button>
<button class="preset-btn secondary" data-speed="1">1x</button>
<button class="preset-btn secondary" data-speed="1.25">1.25x</button>
<button class="preset-btn secondary" data-speed="1.5">1.5x</button>
<button class="preset-btn secondary" data-speed="2">2x</button>
<button class="preset-btn secondary" data-speed="2.5">2.5x</button>
<button class="preset-btn secondary" data-speed="3">3x</button>
<button class="preset-btn secondary" data-speed="4">4x</button>
</div>
<button id="resetBtn" class="secondary" style="margin-top: 10px;">重置为默认</button>
<div class="info">
💡 提示:也可以在页面按 Ctrl+Shift+V 打开控制面板
</div>
<script src="popup.js"></script>
</body>
</html>
popup.js
javascript
document.addEventListener('DOMContentLoaded', function() {
const speedSlider = document.getElementById('speedSlider');
const currentSpeedDisplay = document.getElementById('currentSpeed');
const applyBtn = document.getElementById('applyBtn');
const resetBtn = document.getElementById('resetBtn');
const presetButtons = document.querySelectorAll('.preset-btn');
// 获取当前标签页的视频元素并获取其播放速度
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
if (tabs[0]) {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
func: () => {
const videos = document.querySelectorAll('video');
return videos.length > 0 ? videos[0].playbackRate : 1.0;
}
}, function(results) {
if (results && results[0] && results[0].result !== undefined) {
const currentSpeed = results[0].result;
speedSlider.value = currentSpeed;
currentSpeedDisplay.textContent = currentSpeed.toFixed(2) + 'x';
}
});
}
});
// 滑块变化时更新显示
speedSlider.addEventListener('input', function() {
currentSpeedDisplay.textContent = parseFloat(this.value).toFixed(2) + 'x';
});
// 应用速度按钮
applyBtn.addEventListener('click', function() {
const speed = parseFloat(speedSlider.value);
setVideoSpeed(speed);
});
// 预设速度按钮
presetButtons.forEach(btn => {
btn.addEventListener('click', function() {
const speed = parseFloat(this.dataset.speed);
speedSlider.value = speed;
currentSpeedDisplay.textContent = speed.toFixed(2) + 'x';
setVideoSpeed(speed);
});
});
// 重置按钮
resetBtn.addEventListener('click', function() {
speedSlider.value = 1.0;
currentSpeedDisplay.textContent = '1.00x';
setVideoSpeed(1.0);
});
// 设置视频播放速度
function setVideoSpeed(speed) {
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
if (tabs[0]) {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
func: (speed) => {
const videos = document.querySelectorAll('video');
videos.forEach(video => {
video.playbackRate = speed;
});
try {
localStorage.setItem('preferredPlaybackSpeed', speed);
} catch (e) {}
return videos.length;
},
args: [speed]
}, function(results) {
if (results && results[0] && results[0].result > 0) {
console.log(`已将 ${results[0].result} 个视频的速度设置为 ${speed}x`);
} else {
alert('未找到视频元素');
}
});
}
});
}
});
styles.css
css
.video-speed-panel {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
z-index: 999999;
opacity: 0;
visibility: hidden;
transform: translateY(-20px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.video-speed-panel.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px 12px 0 0;
color: white;
}
.panel-title {
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
}
.panel-close {
background: none;
border: none;
color: white;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
line-height: 1;
border-radius: 50%;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.panel-close:hover {
background: rgba(255, 255, 255, 0.2);
transform: rotate(90deg);
}
.panel-content {
padding: 20px;
}
.current-speed-display {
text-align: center;
margin-bottom: 20px;
padding: 12px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
font-size: 14px;
color: #333;
}
.current-speed-display strong {
font-size: 24px;
color: #667eea;
margin-left: 8px;
}
.speed-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.speed-btn {
padding: 12px 8px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
color: #333;
position: relative;
overflow: hidden;
}
.speed-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(102, 126, 234, 0.1);
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s;
}
.speed-btn:hover::before {
width: 200px;
height: 200px;
}
.speed-btn:hover {
border-color: #667eea;
background: #f8f9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.speed-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-color: transparent;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
transform: scale(1.05);
}
.custom-speed {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.custom-speed input {
flex: 1;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.custom-speed input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.custom-speed button {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
white-space: nowrap;
}
.custom-speed button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.keyboard-hint {
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
font-size: 12px;
color: #666;
text-align: center;
line-height: 1.6;
border-left: 3px solid #667eea;
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
visibility: hidden;
}
}
@media (max-width: 480px) {
.video-speed-panel {
width: calc(100% - 40px);
right: 20px;
left: 20px;
}
.speed-buttons {
grid-template-columns: repeat(3, 1fr);
}
}
Video Speed Controller - Chrome 倍速播放扩展
一个强大的 Chrome 浏览器扩展,为任意网页上的视频添加倍速播放控制功能。
✨ 功能特性
-
🚀 **快速倍速调整**: 支持 0.5x - 3x 常用速度
-
🎯 **精确控制**: 自定义速度范围 0.1x - 16x
-
⌨️ **键盘快捷键**:
-
`Ctrl/Cmd + Shift + V`: 打开/关闭控制面板
-
`←`: 减速 0.25x
-
`→`: 加速 0.25x
-
`R`: 重置为 1x
-
💾 **记忆功能**: 自动保存上次使用的速度
-
**美观界面**: 现代化设计,流畅动画
-
📱 **响应式**: 适配各种屏幕尺寸
📦 安装方法
方法一:开发者模式安装
-
下载本项目到本地
-
打开 Chrome 浏览器,访问 `chrome://extensions/`
-
开启右上角的"开发者模式"
-
点击"加载已解压的扩展程序"
-
选择本项目的文件夹
-
安装完成!
方法二:从 Chrome Web Store 安装(待发布)
即将上线...
🎮 使用方法
方式一:使用弹出窗口
-
点击浏览器工具栏中的扩展图标
-
拖动滑块或点击预设速度按钮
-
点击"应用速度"按钮
方式二:使用页面控制面板(推荐)
-
在任何包含视频的页面
-
按下 `Ctrl + Shift + V`(Windows/Linux)或 `Cmd + Shift + V`(Mac)
-
在弹出的控制面板中选择速度
-
或直接使用键盘快捷键调整
方式三:键盘快捷键
-
直接按 `←` / `→` 调整速度(±0.25x)
-
按 `R` 重置为正常速度
️ 技术栈
-
**Manifest V3**: 最新的 Chrome 扩展规范
-
**Vanilla JavaScript**: 无依赖,轻量高效
-
**CSS3**: 现代样式和动画效果
-
**Chrome Extension API**: 完整的浏览器集成
📁 项目结构
video-speed-controller/
├── manifest.json
├── popup.html
├── popup.js
├── content.js
├── styles.css
├── README.md
