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