1package webkit
2
3import (
4 "strings"
5)
6
7// GenerateCodecControlScript creates JavaScript code to control codec preferences
8// This script overrides MediaCapabilities API and platform-specific player configurations
9func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
10 // Build blocked codecs regex pattern
11 var blockedCodecs []string
12 for _, codec := range prefs.BlockedCodecs {
13 switch strings.ToLower(codec) {
14 case "vp9":
15 blockedCodecs = append(blockedCodecs, "vp9", "vp09")
16 case "vp8":
17 blockedCodecs = append(blockedCodecs, "vp8", "vp08")
18 case "h264":
19 blockedCodecs = append(blockedCodecs, "h264", "avc1")
20 case "h265", "hevc":
21 blockedCodecs = append(blockedCodecs, "h265", "hevc", "hev1", "hvc1")
22 default:
23 blockedCodecs = append(blockedCodecs, strings.ToLower(codec))
24 }
25 }
26
27 // Build preferred codecs list
28 var preferredCodecs []string
29 for _, codec := range prefs.PreferredCodecs {
30 switch strings.ToLower(codec) {
31 case "av1":
32 preferredCodecs = append(preferredCodecs, "av01", "av1")
33 case "h264":
34 preferredCodecs = append(preferredCodecs, "h264", "avc1")
35 case "vp9":
36 preferredCodecs = append(preferredCodecs, "vp9", "vp09")
37 case "vp8":
38 preferredCodecs = append(preferredCodecs, "vp8", "vp08")
39 default:
40 preferredCodecs = append(preferredCodecs, strings.ToLower(codec))
41 }
42 }
43
44 blockedPattern := strings.Join(blockedCodecs, "|")
45 hasBlockedCodecs := len(blockedCodecs) > 0
46 hasPreferredCodecs := len(preferredCodecs) > 0
47
48 // Convert preferredCodecs slice to proper JavaScript array syntax
49 preferredCodecsJS := "[" + strings.Join(func() []string {
50 var quoted []string
51 for _, codec := range preferredCodecs {
52 quoted = append(quoted, `"`+codec+`"`)
53 }
54 return quoted
55 }(), ", ") + "]"
56
57 // Build JavaScript directly without format specifiers
58 var js strings.Builder
59
60 js.WriteString(`(() => {
61 'use strict';
62
63 console.log('[dumber-codec] Initializing codec control');
64
65 // Smart fullscreen handling that works with WebKit's native behavior
66 const videoFullscreenStates = new WeakMap();
67 let fullscreenTransitionInProgress = false;
68
69 // Minimal state tracking - only what's essential
70 function trackVideoForFullscreen(video) {
71 if (!video || video.tagName !== 'VIDEO') return;
72
73 // Only track if video has meaningful content
74 if (video.readyState < 2 || !video.duration) return;
75
76 const state = {
77 wasPlaying: !video.paused && !video.ended,
78 currentTime: video.currentTime,
79 playbackRate: video.playbackRate
80 };
81
82 videoFullscreenStates.set(video, state);
83 console.log('[dumber-codec] Tracking video for fullscreen - playing:', state.wasPlaying, 'time:', state.currentTime);
84 }
85
86 // Gentle recovery without forcing reloads
87 function recoverVideoAfterFullscreen(video) {
88 const state = videoFullscreenStates.get(video);
89 if (!state) return;
90
91 console.log('[dumber-codec] Recovering video after fullscreen transition');
92
93 // Only intervene if video is stuck in a loading state
94 const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready
95 const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2;
96
97 if (isStuck || hasLostTime) {
98 console.log('[dumber-codec] Video appears stuck, applying gentle recovery');
99
100 // Gentle nudge - just try to resume if it was playing
101 if (state.wasPlaying && video.paused) {
102 setTimeout(() => {
103 if (video.readyState >= 2) {
104 video.play().catch(e => console.log('[dumber-codec] Play failed:', e));
105 }
106 }, 200);
107 }
108 }
109
110 videoFullscreenStates.delete(video);
111 }
112
113 // WebKit-aware fullscreen change detection
114 function handleWebKitFullscreenTransition() {
115 if (fullscreenTransitionInProgress) return;
116 fullscreenTransitionInProgress = true;
117
118 console.log('[dumber-codec] WebKit fullscreen transition detected');
119
120 // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing
121 if (location.hostname.includes('twitch.tv')) {
122 console.log('[dumber-codec] Skipping fullscreen recovery on Twitch for stability');
123 fullscreenTransitionInProgress = false;
124 return;
125 }
126
127 const videos = document.querySelectorAll('video');
128 const isEntering = !!(document.fullscreenElement || document.webkitFullscreenElement);
129
130 videos.forEach(video => {
131 if (isEntering) {
132 trackVideoForFullscreen(video);
133 } else {
134 // Give WebKit time to complete its native fullscreen handling
135 setTimeout(() => recoverVideoAfterFullscreen(video), 500);
136 }
137 });
138
139 setTimeout(() => {
140 fullscreenTransitionInProgress = false;
141 }, 1000);
142 }
143
144 // Use passive listeners to avoid interfering with WebKit's native handling
145 document.addEventListener('fullscreenchange', handleWebKitFullscreenTransition, { passive: true });
146 document.addEventListener('webkitfullscreenchange', handleWebKitFullscreenTransition, { passive: true });
147
148 // Override canPlayType for HTMLMediaElement (primary codec detection)
149 const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
150 HTMLMediaElement.prototype.canPlayType = function(type) {
151 console.log('[dumber-codec] canPlayType called with:', type);
152
153 // Block unwanted codecs`)
154
155 if hasBlockedCodecs {
156 js.WriteString(`
157 const blockedRegex = /` + blockedPattern + `/i;
158 if (blockedRegex.test(type)) {
159 console.log('[dumber-codec] Blocking codec via canPlayType:', type);
160 return '';
161 }`)
162 }
163
164 if hasPreferredCodecs {
165 js.WriteString(`
166
167 // Boost preferred codecs
168 const preferredCodecs = ` + preferredCodecsJS + `;
169 for (const preferred of preferredCodecs) {
170 if (type.toLowerCase().includes(preferred)) {
171 console.log('[dumber-codec] Boosting preferred codec via canPlayType:', type);
172 return 'probably';
173 }
174 }`)
175 }
176
177 js.WriteString(`
178
179 // Call original for other codecs
180 return originalCanPlayType.call(this, type);
181 };
182
183 // Override MediaSource.isTypeSupported for more aggressive codec blocking
184 if (window.MediaSource && MediaSource.isTypeSupported) {
185 const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource);
186 MediaSource.isTypeSupported = function(type) {
187 console.log('[dumber-codec] MediaSource.isTypeSupported called with:', type);`)
188
189 if hasBlockedCodecs {
190 js.WriteString(`
191
192 // Block unwanted codecs
193 const blockedRegex = /` + blockedPattern + `/i;
194 if (blockedRegex.test(type)) {
195 console.log('[dumber-codec] Blocking codec via MediaSource.isTypeSupported:', type);
196 return false;
197 }`)
198 }
199
200 if hasPreferredCodecs {
201 js.WriteString(`
202
203 // Boost preferred codecs
204 const preferredCodecs = ` + preferredCodecsJS + `;
205 for (const preferred of preferredCodecs) {
206 if (type.toLowerCase().includes(preferred)) {
207 console.log('[dumber-codec] Boosting preferred codec via MediaSource.isTypeSupported:', type);
208 return true;
209 }
210 }`)
211 }
212
213 js.WriteString(`
214
215 // Call original for other codecs
216 return originalIsTypeSupported(type);
217 };
218 }
219
220 // Override MediaCapabilities API
221 if (navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo) {
222 const originalDecodingInfo = navigator.mediaCapabilities.decodingInfo.bind(navigator.mediaCapabilities);
223
224 navigator.mediaCapabilities.decodingInfo = async function(config) {
225 const contentType = config.video?.contentType || config.type || '';
226
227 // Parse resolution from video config for resolution-aware decisions
228 let width = 0, height = 0;
229 if (config.video) {
230 width = config.video.width || 0;
231 height = config.video.height || 0;
232 }
233
234 // Determine if this is a high resolution request
235 const isHighRes = height >= 1440 || width >= 2560; // 1440p/1080p ultrawide+
236 const is4K = height >= 2160 || width >= 3840;
237 const is1080p60 = (height >= 1080 || width >= 1920) &&
238 (config.video?.framerate >= 50 || config.video?.frameRate >= 50);
239
240 console.log('[dumber-codec] MediaCapabilities query - contentType:', contentType,
241 'resolution:', width + 'x' + height, 'highRes:', isHighRes, '4K:', is4K, '1080p60:', is1080p60);
242
243 // Block unwanted codecs`)
244
245 if hasBlockedCodecs {
246 js.WriteString(`
247 const blockedRegex = /` + blockedPattern + `/i;
248 if (blockedRegex.test(contentType)) {
249 console.log('[dumber-codec] Blocking codec:', contentType);
250 return {
251 supported: false,
252 smooth: false,
253 powerEfficient: false
254 };
255 }`)
256 }
257
258 js.WriteString(`
259
260 // Resolution-aware preferred codec handling`)
261
262 if hasPreferredCodecs {
263 js.WriteString(`
264 const preferredCodecs = ` + preferredCodecsJS + `;
265 for (const preferred of preferredCodecs) {
266 if (contentType.toLowerCase().includes(preferred)) {
267 console.log('[dumber-codec] Boosting preferred codec:', contentType);
268
269 // Resolution-aware capability reporting for AV1
270 if (preferred === 'av01' || preferred === 'av1') {
271 // AV1 handling based on resolution
272 if (is4K) {
273 // 4K AV1: supported but not smooth (encourage VP9 fallback)
274 return {
275 supported: true,
276 smooth: false,
277 powerEfficient: false
278 };
279 } else if (is1080p60) {
280 // 1080p60 AV1: supported but not power efficient
281 return {
282 supported: true,
283 smooth: true,
284 powerEfficient: false
285 };
286 } else if (isHighRes) {
287 // 1440p AV1: supported but not optimal
288 return {
289 supported: true,
290 smooth: false,
291 powerEfficient: true
292 };
293 } else {
294 // <= 1080p30 AV1: fully supported
295 return {
296 supported: true,
297 smooth: true,
298 powerEfficient: true
299 };
300 }
301 } else {
302 // Other preferred codecs (VP9, H264) - always report as capable
303 return {
304 supported: true,
305 smooth: true,
306 powerEfficient: true
307 };
308 }
309 }
310 }`)
311 }
312
313 js.WriteString(`
314
315 // Call original for other codecs
316 try {
317 return await originalDecodingInfo(config);
318 } catch (e) {
319 console.warn('[dumber-codec] MediaCapabilities error:', e);
320 return {
321 supported: false,
322 smooth: false,
323 powerEfficient: false
324 };
325 }
326 };
327 }
328
329 // YouTube-specific codec forcing
330 if (location.hostname.includes('youtube.com')) {
331 console.log('[dumber-codec] Applying YouTube codec preferences');
332
333 // Set YouTube's localStorage preferences for smart AV1 usage
334 // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res
335 // This allows YouTube to make smarter codec decisions based on our MediaCapabilities
336 try {
337 localStorage.setItem('yt-player-av1-pref', '2048');
338 Object.defineProperty(localStorage, 'yt-player-av1-pref', {
339 value: '2048',
340 writable: false,
341 configurable: false
342 });
343 console.log('[dumber-codec] YouTube: Set smart AV1 localStorage preference (2048)');
344 } catch (e) {
345 console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e);
346 }
347
348 // Override ytInitialPlayerResponse parsing
349 Object.defineProperty(window, 'ytplayer', {
350 configurable: true,
351 get() {
352 return this._ytplayer || null;
353 },
354 set(player) {
355 if (player && player.config && player.config.args) {`)
356
357 if prefs.ForceAV1 {
358 js.WriteString(`
359 // Force AV1 if enabled
360 player.config.args.preferred_codecs = 'av01';
361 console.log('[dumber-codec] YouTube: Forced AV1 codec');`)
362 }
363
364 if hasPreferredCodecs {
365 js.WriteString(`
366 // Set codec preference order
367 const preferredCodecs = ` + preferredCodecsJS + `;
368 const codecOrder = preferredCodecs.map(c => {
369 switch(c.toLowerCase()) {
370 case 'av1': case 'av01': return 'av01';
371 case 'h264': case 'avc1': return 'avc1';
372 case 'vp9': case 'vp09': return 'vp9';
373 case 'vp8': case 'vp08': return 'vp8';
374 default: return c;
375 }
376 }).join(',');
377 player.config.args.preferred_codecs = codecOrder;
378 console.log('[dumber-codec] YouTube: Set codec order:', codecOrder);`)
379 }
380
381 js.WriteString(`
382 }
383 this._ytplayer = player;
384 }
385 });
386
387 // Intercept YouTube's format selection
388 const originalFetch = window.fetch;
389 window.fetch = function(input, init) {
390 if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {
391 console.log('[dumber-codec] YouTube: Intercepting player request');`)
392
393 if hasPreferredCodecs {
394 js.WriteString(`
395
396 // Modify request to prefer our codecs
397 if (init && init.body) {
398 try {
399 const body = JSON.parse(init.body);
400 if (body.videoId) {
401 // Add codec preferences to the request
402 body.codecPreferences = ` + preferredCodecsJS + `;
403 init.body = JSON.stringify(body);
404 }
405 } catch (e) {
406 // Non-JSON body, ignore
407 }
408 }`)
409 }
410
411 js.WriteString(`
412 }
413
414 // Process YouTube player API responses for format manipulation
415 if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {
416 return originalFetch.call(this, input, init).then(response => {
417 if (!response.ok) return response;
418
419 return response.text().then(text => {
420 try {
421 const data = JSON.parse(text);
422
423 // Manipulate streaming data if present
424 if (data.streamingData && data.streamingData.formats) {
425 console.log('[dumber-codec] YouTube: Processing format manifest');
426
427 // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher
428 data.streamingData.formats.sort((a, b) => {
429 const aHeight = a.height || 0;
430 const bHeight = b.height || 0;
431 const aIsAV1 = a.mimeType && a.mimeType.includes('av01');
432 const bIsAV1 = b.mimeType && b.mimeType.includes('av01');
433 const aIsVP9 = a.mimeType && a.mimeType.includes('vp9');
434 const bIsVP9 = b.mimeType && b.mimeType.includes('vp9');
435
436 // For resolutions <= 1080p, prioritize AV1
437 if (Math.max(aHeight, bHeight) <= 1080) {
438 if (aIsAV1 && !bIsAV1) return -1;
439 if (!aIsAV1 && bIsAV1) return 1;
440 }
441 // For resolutions > 1080p, prioritize VP9
442 else {
443 if (aIsVP9 && !bIsVP9) return -1;
444 if (!aIsVP9 && bIsVP9) return 1;
445 }
446
447 return 0;
448 });
449
450 console.log('[dumber-codec] YouTube: Reordered formats for optimal codec selection');
451 }
452
453 return new Response(JSON.stringify(data), {
454 status: response.status,
455 statusText: response.statusText,
456 headers: response.headers
457 });
458 } catch (e) {
459 console.warn('[dumber-codec] YouTube: Failed to process response:', e);
460 return new Response(text, {
461 status: response.status,
462 statusText: response.statusText,
463 headers: response.headers
464 });
465 }
466 });
467 });
468 }
469
470 return originalFetch.call(this, input, init);
471 };
472 }
473
474 // Twitch: No codec interference - let Twitch handle codec selection natively
475 if (location.hostname.includes('twitch.tv')) {
476 console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability');
477 // Twitch codec control completely removed to prevent theater/fullscreen freezing
478 }
479
480 // Passive video monitoring without interfering with element creation
481 const monitorExistingVideos = () => {
482 const videos = document.querySelectorAll('video');
483 videos.forEach(video => {
484 if (!video.__codecMonitored) {
485 video.__codecMonitored = true;
486 console.log('[dumber-codec] Found video element, adding passive monitoring');
487
488 // Passive codec detection only
489 video.addEventListener('canplay', function() {
490 console.log('[dumber-codec] Video can play - resolution:', this.videoWidth + 'x' + this.videoHeight);
491 }, { passive: true, once: true });
492 }
493 });
494 };
495
496 // Monitor for video elements periodically without overriding createElement
497 const videoObserver = new MutationObserver(monitorExistingVideos);
498 videoObserver.observe(document.body, { childList: true, subtree: true });
499 monitorExistingVideos(); // Check existing videos
500
501 console.log('[dumber-codec] Codec control initialization complete');
502})();`)
503
504 return js.String()
505}