Control video playback speed (2x, 3x, 5x, 10x) and auto-forward when videos complete
Size
12.5 KB
Version
1.1.1
Created
Dec 4, 2025
Updated
4 days ago
1// ==UserScript==
2// @name Edgenuity Video Speed Controller & Auto-Forward
3// @description Control video playback speed (2x, 3x, 5x, 10x) and auto-forward when videos complete
4// @version 1.1.1
5// @match https://*.r03.core.learn.edgenuity.com/*
6// @icon https://r03.core.learn.edgenuity.com/Player/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Edgenuity Video Controller: Extension loaded');
12
13 let speedCheckInterval = null;
14 let autoForwardCheckInterval = null;
15
16 // Debounce function to prevent excessive calls
17 function debounce(func, wait) {
18 let timeout;
19 return function executedFunction(...args) {
20 const later = () => {
21 clearTimeout(timeout);
22 func(...args);
23 };
24 clearTimeout(timeout);
25 timeout = setTimeout(later, wait);
26 };
27 }
28
29 // Create control panel
30 function createControlPanel() {
31 // Check if panel already exists
32 if (document.getElementById('edgenuity-control-panel')) {
33 console.log('Control panel already exists');
34 return;
35 }
36
37 const panel = document.createElement('div');
38 panel.id = 'edgenuity-control-panel';
39 panel.innerHTML = `
40 <div style="display: flex; align-items: center; gap: 15px;">
41 <div style="display: flex; align-items: center; gap: 8px;">
42 <label style="color: white; font-weight: bold; font-size: 14px;">
43 <input type="checkbox" id="auto-forward-toggle" style="margin-right: 5px; cursor: pointer;">
44 Auto-Forward
45 </label>
46 </div>
47 <div style="border-left: 2px solid rgba(255,255,255,0.3); height: 30px;"></div>
48 <div style="display: flex; align-items: center; gap: 8px;">
49 <span style="color: white; font-weight: bold; font-size: 14px;">Speed:</span>
50 <button class="speed-btn" data-speed="1">1x</button>
51 <button class="speed-btn" data-speed="2">2x</button>
52 <button class="speed-btn" data-speed="3">3x</button>
53 <button class="speed-btn" data-speed="5">5x</button>
54 <button class="speed-btn" data-speed="10">10x</button>
55 </div>
56 </div>
57 `;
58
59 // Add styles
60 const style = document.createElement('style');
61 style.textContent = `
62 #edgenuity-control-panel {
63 position: fixed;
64 top: 10px;
65 left: 50%;
66 transform: translateX(-50%);
67 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
68 padding: 12px 20px;
69 border-radius: 10px;
70 box-shadow: 0 4px 15px rgba(0,0,0,0.3);
71 z-index: 999999;
72 font-family: Arial, sans-serif;
73 }
74
75 .speed-btn {
76 background: white;
77 border: 2px solid transparent;
78 padding: 6px 14px;
79 border-radius: 6px;
80 cursor: pointer;
81 font-weight: bold;
82 font-size: 13px;
83 color: #667eea;
84 transition: all 0.2s ease;
85 }
86
87 .speed-btn:hover {
88 background: #f0f0f0;
89 transform: translateY(-2px);
90 box-shadow: 0 2px 8px rgba(0,0,0,0.2);
91 }
92
93 .speed-btn.active {
94 background: #4CAF50;
95 color: white;
96 border-color: #45a049;
97 }
98
99 #auto-forward-toggle {
100 width: 16px;
101 height: 16px;
102 }
103 `;
104 document.head.appendChild(style);
105 document.body.appendChild(panel);
106
107 console.log('Control panel created');
108 setupEventListeners();
109 }
110
111 // Setup event listeners for the control panel
112 async function setupEventListeners() {
113 // Load saved settings
114 const autoForwardEnabled = await GM.getValue('autoForwardEnabled', false);
115 const savedSpeed = await GM.getValue('videoSpeed', 1);
116
117 const autoForwardToggle = document.getElementById('auto-forward-toggle');
118 if (autoForwardToggle) {
119 autoForwardToggle.checked = autoForwardEnabled;
120 }
121
122 // Highlight active speed button
123 document.querySelectorAll('.speed-btn').forEach(btn => {
124 if (parseFloat(btn.dataset.speed) === savedSpeed) {
125 btn.classList.add('active');
126 }
127 });
128
129 // Auto-forward toggle
130 if (autoForwardToggle) {
131 autoForwardToggle.addEventListener('change', async (e) => {
132 const enabled = e.target.checked;
133 await GM.setValue('autoForwardEnabled', enabled);
134 console.log('Auto-forward:', enabled ? 'enabled' : 'disabled');
135
136 if (enabled) {
137 startAutoForwardMonitoring();
138 } else {
139 stopAutoForwardMonitoring();
140 }
141 });
142 }
143
144 // Speed buttons
145 document.querySelectorAll('.speed-btn').forEach(btn => {
146 btn.addEventListener('click', async (e) => {
147 const speed = parseFloat(e.target.dataset.speed);
148 await GM.setValue('videoSpeed', speed);
149
150 // Update active state
151 document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
152 e.target.classList.add('active');
153
154 console.log('Video speed set to:', speed + 'x');
155 });
156 });
157
158 // Start continuous speed monitoring
159 startSpeedMonitoring();
160
161 // Start auto-forward if enabled
162 if (autoForwardEnabled) {
163 startAutoForwardMonitoring();
164 }
165 }
166
167 // Continuously monitor and apply speed to all videos
168 function startSpeedMonitoring() {
169 if (speedCheckInterval) {
170 clearInterval(speedCheckInterval);
171 }
172
173 speedCheckInterval = setInterval(async () => {
174 const savedSpeed = await GM.getValue('videoSpeed', 1);
175
176 // Check main document videos
177 const videos = document.querySelectorAll('video');
178 videos.forEach(video => {
179 if (video.playbackRate !== savedSpeed) {
180 video.playbackRate = savedSpeed;
181 console.log('Applied speed', savedSpeed + 'x to video in main document');
182 }
183 });
184
185 // Check iframe videos
186 const iframe = document.getElementById('stageFrame');
187 if (iframe) {
188 try {
189 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
190 const iframeVideos = iframeDoc.querySelectorAll('video');
191 iframeVideos.forEach(video => {
192 if (video.playbackRate !== savedSpeed) {
193 video.playbackRate = savedSpeed;
194 console.log('Applied speed', savedSpeed + 'x to video in iframe');
195 }
196 });
197 } catch (e) {
198 // Cross-origin iframe, can't access
199 }
200 }
201 }, 500); // Check every 500ms to keep speed consistent
202 }
203
204 // Monitor for video completion and auto-forward
205 function startAutoForwardMonitoring() {
206 console.log('Starting auto-forward monitoring');
207
208 if (autoForwardCheckInterval) {
209 clearInterval(autoForwardCheckInterval);
210 }
211
212 autoForwardCheckInterval = setInterval(async () => {
213 const autoForwardEnabled = await GM.getValue('autoForwardEnabled', false);
214 if (!autoForwardEnabled) {
215 stopAutoForwardMonitoring();
216 return;
217 }
218
219 // Check main document videos
220 const videos = document.querySelectorAll('video');
221 videos.forEach(video => {
222 checkVideoEnded(video);
223 });
224
225 // Check iframe videos
226 const iframe = document.getElementById('stageFrame');
227 if (iframe) {
228 try {
229 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
230 const iframeVideos = iframeDoc.querySelectorAll('video');
231 iframeVideos.forEach(video => {
232 checkVideoEnded(video);
233 });
234 } catch (e) {
235 // Cross-origin iframe, can't access
236 }
237 }
238 }, 1000); // Check every second
239 }
240
241 function stopAutoForwardMonitoring() {
242 if (autoForwardCheckInterval) {
243 clearInterval(autoForwardCheckInterval);
244 autoForwardCheckInterval = null;
245 console.log('Stopped auto-forward monitoring');
246 }
247 }
248
249 // Check if video has ended and trigger forward
250 function checkVideoEnded(video) {
251 if (!video) return;
252
253 // Check if video is ended or very close to the end (within 1 second)
254 if (video.ended || (video.duration > 0 && video.currentTime >= video.duration - 1)) {
255 if (!video.hasAttribute('data-forwarded')) {
256 video.setAttribute('data-forwarded', 'true');
257 console.log('Video ended, attempting to auto-forward');
258
259 setTimeout(() => {
260 clickNextButton();
261 // Reset the forwarded flag after a delay
262 setTimeout(() => {
263 video.removeAttribute('data-forwarded');
264 }, 5000);
265 }, 1000);
266 }
267 } else {
268 // Reset flag if video is playing
269 if (video.hasAttribute('data-forwarded') && video.currentTime < video.duration - 2) {
270 video.removeAttribute('data-forwarded');
271 }
272 }
273 }
274
275 // Click the next/continue button
276 function clickNextButton() {
277 // Look for next/continue button in main document
278 const mainButtons = Array.from(document.querySelectorAll('button, a')).filter(el => {
279 const text = el.textContent.toLowerCase();
280 const dataBindAttr = el.getAttribute('data-bind') || '';
281 return /next|continue|forward/i.test(text) ||
282 /next|continue/i.test(dataBindAttr);
283 });
284
285 // Try iframe buttons
286 const iframe = document.getElementById('stageFrame');
287 let iframeButtons = [];
288 if (iframe) {
289 try {
290 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
291 iframeButtons = Array.from(iframeDoc.querySelectorAll('button, a')).filter(el => {
292 const text = el.textContent.toLowerCase();
293 const dataBindAttr = el.getAttribute('data-bind') || '';
294 return /next|continue|forward/i.test(text) ||
295 /next|continue/i.test(dataBindAttr);
296 });
297 } catch (e) {
298 // Cross-origin iframe
299 }
300 }
301
302 const allButtons = [...mainButtons, ...iframeButtons];
303 const nextButton = allButtons.find(btn => btn && btn.offsetParent !== null);
304
305 if (nextButton) {
306 console.log('Clicking next button:', nextButton.textContent.trim());
307 nextButton.click();
308 } else {
309 console.log('No next button found');
310 }
311 }
312
313 // Initialize when DOM is ready
314 function init() {
315 if (document.readyState === 'loading') {
316 document.addEventListener('DOMContentLoaded', createControlPanel);
317 } else {
318 createControlPanel();
319 }
320
321 // Monitor for DOM changes with debouncing
322 const debouncedSpeedCheck = debounce(async () => {
323 const savedSpeed = await GM.getValue('videoSpeed', 1);
324 const videos = document.querySelectorAll('video');
325 videos.forEach(video => {
326 if (video.playbackRate !== savedSpeed) {
327 video.playbackRate = savedSpeed;
328 }
329 });
330 }, 300);
331
332 const observer = new MutationObserver(debouncedSpeedCheck);
333 observer.observe(document.body, {
334 childList: true,
335 subtree: true
336 });
337 }
338
339 init();
340})();