fix(twitch): remove codec interference preventing theater mode and fullscreen
- commit
- 27b965a67f45536f3ebc8be3dff179a33d85fdc3
- author
- bamen · · 2025-09-12 10:39 UTC
- parent
- cf74e06
- signature
- unsigned
6 files · +51 -148
M CHANGELOG.md +4 -0
| @@ -19,6 +19,10 @@ All notable changes to this project will be documented in this file. | |||
| 19 | 19 | - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache) | |
| 20 | 20 | - **F5**: Alternative reload key | |
| 21 | 21 | - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls | |
| 22 | + | - **Twitch theater mode and fullscreen stability fix**: | |
| 23 | + | - **Complete removal of codec interference on Twitch**: Eliminates aggressive codec control that caused theater mode and fullscreen freezing | |
| 24 | + | - **Native Twitch codec selection**: Allows Twitch to handle codec negotiation without browser intervention | |
| 25 | + | - Fixes theater mode freeze and fullscreen black screen issues on Twitch | |
| 22 | 26 | ||
| 23 | 27 | ### Fixed | |
| 24 | 28 | - Zoom level toast no longer shows on every page navigation within the same domain: | |
| @@ -19,6 +19,10 @@ All notable changes to this project will be documented in this file. | |||||
| 19 | - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache) | 19 | - **Ctrl+Shift+R** / **Cmd+Shift+R**: Hard reload (bypass cache) | ||
| 20 | - **F5**: Alternative reload key | 20 | - **F5**: Alternative reload key | ||
| 21 | - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls | 21 | - WebKit integration with proper `webkit_web_view_reload()` and `webkit_web_view_reload_bypass_cache()` calls | ||
| 22 | + | - **Twitch theater mode and fullscreen stability fix**: | |||
| 23 | + | - **Complete removal of codec interference on Twitch**: Eliminates aggressive codec control that caused theater mode and fullscreen freezing | |||
| 24 | + | - **Native Twitch codec selection**: Allows Twitch to handle codec negotiation without browser intervention | |||
| 25 | + | - Fixes theater mode freeze and fullscreen black screen issues on Twitch | |||
| 22 | 26 | ||||
| 23 | ### Fixed | 27 | ### Fixed | ||
| 24 | - Zoom level toast no longer shows on every page navigation within the same domain: | 28 | - Zoom level toast no longer shows on every page navigation within the same domain: | ||
M internal/config/config.go +15 -10
| @@ -146,6 +146,9 @@ type CodecConfig struct { | |||
| 146 | 146 | ||
| 147 | 147 | // Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited) | |
| 148 | 148 | AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"` | |
| 149 | + | ||
| 150 | + | // Site-specific codec control settings | |
| 151 | + | DisableTwitchCodecControl bool `mapstructure:"disable_twitch_codec_control" yaml:"disable_twitch_codec_control"` | |
| 149 | 152 | } | |
| 150 | 153 | ||
| 151 | 154 | // Manager handles configuration loading, watching, and reloading. | |
| @@ -234,16 +237,17 @@ func NewManager() (*Manager, error) { | |||
| 234 | 237 | ||
| 235 | 238 | // Codec preferences environment variable bindings | |
| 236 | 239 | codecEnvBindings := map[string]string{ | |
| 237 | - | "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS", | |
| 238 | - | "codec_preferences.force_av1": "DUMBER_FORCE_AV1", | |
| 239 | - | "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9", | |
| 240 | - | "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8", | |
| 241 | - | "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY", | |
| 242 | - | "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW", | |
| 243 | - | "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB", | |
| 244 | - | "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC", | |
| 245 | - | "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA", | |
| 246 | - | "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES", | |
| 240 | + | "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS", | |
| 241 | + | "codec_preferences.force_av1": "DUMBER_FORCE_AV1", | |
| 242 | + | "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9", | |
| 243 | + | "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8", | |
| 244 | + | "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY", | |
| 245 | + | "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW", | |
| 246 | + | "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB", | |
| 247 | + | "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC", | |
| 248 | + | "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA", | |
| 249 | + | "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES", | |
| 250 | + | "codec_preferences.disable_twitch_codec_control": "DUMBER_DISABLE_TWITCH_CODEC", | |
| 247 | 251 | } | |
| 248 | 252 | ||
| 249 | 253 | for key, env := range codecEnvBindings { | |
| @@ -493,6 +497,7 @@ func (m *Manager) setDefaults() { | |||
| 493 | 497 | m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec) | |
| 494 | 498 | m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent) | |
| 495 | 499 | m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution) | |
| 500 | + | m.viper.SetDefault("codec_preferences.disable_twitch_codec_control", defaults.CodecPreferences.DisableTwitchCodecControl) | |
| 496 | 501 | ||
| 497 | 502 | // Rendering defaults | |
| 498 | 503 | m.viper.SetDefault("rendering_mode", string(RenderingModeAuto)) | |
| @@ -146,6 +146,9 @@ type CodecConfig struct { | |||||
| 146 | 146 | ||||
| 147 | // Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited) | 147 | // Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited) | ||
| 148 | AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"` | 148 | AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"` | ||
| 149 | + | ||||
| 150 | + | // Site-specific codec control settings | |||
| 151 | + | DisableTwitchCodecControl bool `mapstructure:"disable_twitch_codec_control" yaml:"disable_twitch_codec_control"` | |||
| 149 | } | 152 | } | ||
| 150 | 153 | ||||
| 151 | // Manager handles configuration loading, watching, and reloading. | 154 | // Manager handles configuration loading, watching, and reloading. | ||
| @@ -234,16 +237,17 @@ func NewManager() (*Manager, error) { | |||||
| 234 | 237 | ||||
| 235 | // Codec preferences environment variable bindings | 238 | // Codec preferences environment variable bindings | ||
| 236 | codecEnvBindings := map[string]string{ | 239 | codecEnvBindings := map[string]string{ | ||
| 237 | - | "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS", | 240 | + | "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS", |
| 238 | - | "codec_preferences.force_av1": "DUMBER_FORCE_AV1", | 241 | + | "codec_preferences.force_av1": "DUMBER_FORCE_AV1", |
| 239 | - | "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9", | 242 | + | "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9", |
| 240 | - | "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8", | 243 | + | "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8", |
| 241 | - | "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY", | 244 | + | "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY", |
| 242 | - | "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW", | 245 | + | "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW", |
| 243 | - | "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB", | 246 | + | "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB", |
| 244 | - | "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC", | 247 | + | "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC", |
| 245 | - | "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA", | 248 | + | "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA", |
| 246 | - | "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES", | 249 | + | "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES", |
| 250 | + | "codec_preferences.disable_twitch_codec_control": "DUMBER_DISABLE_TWITCH_CODEC", | |||
| 247 | } | 251 | } | ||
| 248 | 252 | ||||
| 249 | for key, env := range codecEnvBindings { | 253 | for key, env := range codecEnvBindings { | ||
| @@ -493,6 +497,7 @@ func (m *Manager) setDefaults() { | |||||
| 493 | m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec) | 497 | m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec) | ||
| 494 | m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent) | 498 | m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent) | ||
| 495 | m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution) | 499 | m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution) | ||
| 500 | + | m.viper.SetDefault("codec_preferences.disable_twitch_codec_control", defaults.CodecPreferences.DisableTwitchCodecControl) | |||
| 496 | 501 | ||||
| 497 | // Rendering defaults | 502 | // Rendering defaults | ||
| 498 | m.viper.SetDefault("rendering_mode", string(RenderingModeAuto)) | 503 | m.viper.SetDefault("rendering_mode", string(RenderingModeAuto)) | ||
M internal/config/defaults.go +11 -10
| @@ -107,16 +107,17 @@ func DefaultConfig() *Config { | |||
| 107 | 107 | LegacyVAAPI: false, | |
| 108 | 108 | }, | |
| 109 | 109 | CodecPreferences: CodecConfig{ | |
| 110 | - | PreferredCodecs: "av1,h264", // AV1 first, but allow VP9 fallback for higher resolutions | |
| 111 | - | ForceAV1: false, // Use smart AV1 negotiation instead of forcing | |
| 112 | - | BlockVP9: false, // Allow VP9 for higher resolutions | |
| 113 | - | BlockVP8: true, // Still block VP8 as it's outdated | |
| 114 | - | AV1HardwareOnly: false, // Allow software AV1 fallback | |
| 115 | - | DisableVP9Hardware: false, // Allow VP9 hardware for high res content | |
| 116 | - | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams | |
| 117 | - | QueueBufferTimeSec: 10, // More buffering time for smooth playback | |
| 118 | - | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", // Chrome UA with AV1 support | |
| 119 | - | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res | |
| 110 | + | PreferredCodecs: "av1,h264", // AV1 first, but allow VP9 fallback for higher resolutions | |
| 111 | + | ForceAV1: false, // Use smart AV1 negotiation instead of forcing | |
| 112 | + | BlockVP9: false, // Allow VP9 for higher resolutions | |
| 113 | + | BlockVP8: true, // Still block VP8 as it's outdated | |
| 114 | + | AV1HardwareOnly: false, // Allow software AV1 fallback | |
| 115 | + | DisableVP9Hardware: false, // Allow VP9 hardware for high res content | |
| 116 | + | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams | |
| 117 | + | QueueBufferTimeSec: 10, // More buffering time for smooth playback | |
| 118 | + | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", // Chrome UA with AV1 support | |
| 119 | + | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res | |
| 120 | + | DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing) | |
| 120 | 121 | }, | |
| 121 | 122 | RenderingMode: RenderingModeAuto, | |
| 122 | 123 | } | |
| @@ -107,16 +107,17 @@ func DefaultConfig() *Config { | |||||
| 107 | LegacyVAAPI: false, | 107 | LegacyVAAPI: false, | ||
| 108 | }, | 108 | }, | ||
| 109 | CodecPreferences: CodecConfig{ | 109 | CodecPreferences: CodecConfig{ | ||
| 110 | - | PreferredCodecs: "av1,h264", // AV1 first, but allow VP9 fallback for higher resolutions | 110 | + | PreferredCodecs: "av1,h264", // AV1 first, but allow VP9 fallback for higher resolutions |
| 111 | - | ForceAV1: false, // Use smart AV1 negotiation instead of forcing | 111 | + | ForceAV1: false, // Use smart AV1 negotiation instead of forcing |
| 112 | - | BlockVP9: false, // Allow VP9 for higher resolutions | 112 | + | BlockVP9: false, // Allow VP9 for higher resolutions |
| 113 | - | BlockVP8: true, // Still block VP8 as it's outdated | 113 | + | BlockVP8: true, // Still block VP8 as it's outdated |
| 114 | - | AV1HardwareOnly: false, // Allow software AV1 fallback | 114 | + | AV1HardwareOnly: false, // Allow software AV1 fallback |
| 115 | - | DisableVP9Hardware: false, // Allow VP9 hardware for high res content | 115 | + | DisableVP9Hardware: false, // Allow VP9 hardware for high res content |
| 116 | - | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams | 116 | + | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams |
| 117 | - | QueueBufferTimeSec: 10, // More buffering time for smooth playback | 117 | + | QueueBufferTimeSec: 10, // More buffering time for smooth playback |
| 118 | - | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", // Chrome UA with AV1 support | 118 | + | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", // Chrome UA with AV1 support |
| 119 | - | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res | 119 | + | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res |
| 120 | + | DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing) | |||
| 120 | }, | 121 | }, | ||
| 121 | RenderingMode: RenderingModeAuto, | 122 | RenderingMode: RenderingModeAuto, | ||
| 122 | } | 123 | } | ||
M main.go +5 -4
| @@ -455,10 +455,11 @@ func runBrowser() { | |||
| 455 | 455 | }, | |
| 456 | 456 | Memory: readMemoryConfigFromEnv(), | |
| 457 | 457 | CodecPreferences: webkit.CodecPreferencesConfig{ | |
| 458 | - | PreferredCodecs: strings.Split(cfg.CodecPreferences.PreferredCodecs, ","), | |
| 459 | - | BlockedCodecs: buildBlockedCodecsList(cfg.CodecPreferences), | |
| 460 | - | ForceAV1: cfg.CodecPreferences.ForceAV1, | |
| 461 | - | CustomUserAgent: cfg.CodecPreferences.CustomUserAgent, | |
| 458 | + | PreferredCodecs: strings.Split(cfg.CodecPreferences.PreferredCodecs, ","), | |
| 459 | + | BlockedCodecs: buildBlockedCodecsList(cfg.CodecPreferences), | |
| 460 | + | ForceAV1: cfg.CodecPreferences.ForceAV1, | |
| 461 | + | CustomUserAgent: cfg.CodecPreferences.CustomUserAgent, | |
| 462 | + | DisableTwitchCodecControl: cfg.CodecPreferences.DisableTwitchCodecControl, | |
| 462 | 463 | }, | |
| 463 | 464 | }) | |
| 464 | 465 | if err != nil { | |
| @@ -455,10 +455,11 @@ func runBrowser() { | |||||
| 455 | }, | 455 | }, | ||
| 456 | Memory: readMemoryConfigFromEnv(), | 456 | Memory: readMemoryConfigFromEnv(), | ||
| 457 | CodecPreferences: webkit.CodecPreferencesConfig{ | 457 | CodecPreferences: webkit.CodecPreferencesConfig{ | ||
| 458 | - | PreferredCodecs: strings.Split(cfg.CodecPreferences.PreferredCodecs, ","), | 458 | + | PreferredCodecs: strings.Split(cfg.CodecPreferences.PreferredCodecs, ","), |
| 459 | - | BlockedCodecs: buildBlockedCodecsList(cfg.CodecPreferences), | 459 | + | BlockedCodecs: buildBlockedCodecsList(cfg.CodecPreferences), |
| 460 | - | ForceAV1: cfg.CodecPreferences.ForceAV1, | 460 | + | ForceAV1: cfg.CodecPreferences.ForceAV1, |
| 461 | - | CustomUserAgent: cfg.CodecPreferences.CustomUserAgent, | 461 | + | CustomUserAgent: cfg.CodecPreferences.CustomUserAgent, |
| 462 | + | DisableTwitchCodecControl: cfg.CodecPreferences.DisableTwitchCodecControl, | |||
| 462 | }, | 463 | }, | ||
| 463 | }) | 464 | }) | ||
| 464 | if err != nil { | 465 | if err != nil { | ||
M pkg/webkit/codec_control.go +11 -120
| @@ -44,10 +44,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 44 | 44 | blockedPattern := strings.Join(blockedCodecs, "|") | |
| 45 | 45 | hasBlockedCodecs := len(blockedCodecs) > 0 | |
| 46 | 46 | hasPreferredCodecs := len(preferredCodecs) > 0 | |
| 47 | - | firstPreferredCodec := "" | |
| 48 | - | if len(preferredCodecs) > 0 { | |
| 49 | - | firstPreferredCodec = preferredCodecs[0] | |
| 50 | - | } | |
| 47 | + | ||
| 51 | 48 | ||
| 52 | 49 | // Convert preferredCodecs slice to proper JavaScript array syntax | |
| 53 | 50 | preferredCodecsJS := "[" + strings.Join(func() []string { | |
| @@ -121,6 +118,13 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 121 | 118 | ||
| 122 | 119 | console.log('[dumber-codec] WebKit fullscreen transition detected'); | |
| 123 | 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 | + | ||
| 124 | 128 | const videos = document.querySelectorAll('video'); | |
| 125 | 129 | const isEntering = !!(document.fullscreenElement || document.webkitFullscreenElement); | |
| 126 | 130 | ||
| @@ -468,114 +472,10 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 468 | 472 | }; | |
| 469 | 473 | } | |
| 470 | 474 | ||
| 471 | - | // Twitch-specific codec control | |
| 475 | + | // Twitch: No codec interference - let Twitch handle codec selection natively | |
| 472 | 476 | if (location.hostname.includes('twitch.tv')) { | |
| 473 | - | console.log('[dumber-codec] Applying Twitch codec preferences'); | |
| 474 | - | ||
| 475 | - | // Intercept Twitch Enhanced Broadcasting API calls | |
| 476 | - | const originalFetch = window.fetch; | |
| 477 | - | window.fetch = function(input, init) { | |
| 478 | - | const url = typeof input === 'string' ? input : input.url; | |
| 479 | - | ||
| 480 | - | // Intercept Enhanced Broadcasting API calls | |
| 481 | - | if (url && url.includes('/api/channel/hls') || url.includes('/enhanced_broadcasting')) { | |
| 482 | - | console.log('[dumber-codec] Twitch: Intercepting Enhanced Broadcasting API call'); | |
| 483 | - | ||
| 484 | - | // Modify request to prefer AV1 if available | |
| 485 | - | if (init && init.body) { | |
| 486 | - | try { | |
| 487 | - | const body = JSON.parse(init.body); | |
| 488 | - | if (body.supported_codecs) {`) | |
| 489 | - | ||
| 490 | - | if prefs.ForceAV1 || hasPreferredCodecs { | |
| 491 | - | js.WriteString(` | |
| 492 | - | // Reorder supported codecs to prioritize AV1 | |
| 493 | - | const preferredOrder = ['av01', 'avc1', 'vp9', 'vp8']; | |
| 494 | - | body.supported_codecs.sort((a, b) => { | |
| 495 | - | const aIdx = preferredOrder.indexOf(a.toLowerCase()) !== -1 ? | |
| 496 | - | preferredOrder.indexOf(a.toLowerCase()) : 999; | |
| 497 | - | const bIdx = preferredOrder.indexOf(b.toLowerCase()) !== -1 ? | |
| 498 | - | preferredOrder.indexOf(b.toLowerCase()) : 999; | |
| 499 | - | return aIdx - bIdx; | |
| 500 | - | }); | |
| 501 | - | console.log('[dumber-codec] Twitch: Reordered supported codecs:', body.supported_codecs);`) | |
| 502 | - | } | |
| 503 | - | ||
| 504 | - | js.WriteString(` | |
| 505 | - | } | |
| 506 | - | init.body = JSON.stringify(body); | |
| 507 | - | } catch (e) { | |
| 508 | - | // Non-JSON body, ignore | |
| 509 | - | } | |
| 510 | - | } | |
| 511 | - | } | |
| 512 | - | ||
| 513 | - | // Intercept HLS manifest requests for codec modification | |
| 514 | - | if (url && (url.includes('.m3u8') || url.includes('/playlist/'))) { | |
| 515 | - | return originalFetch.call(this, input, init).then(response => { | |
| 516 | - | if (response.ok && response.headers.get('content-type')?.includes('application/vnd.apple.mpegurl')) { | |
| 517 | - | return response.text().then(text => {`) | |
| 518 | - | ||
| 519 | - | if hasBlockedCodecs { | |
| 520 | - | js.WriteString(` | |
| 521 | - | // Remove blocked codecs from HLS manifest | |
| 522 | - | const blockedCodecs = ` + preferredCodecsJS + `.join('|'); | |
| 523 | - | const blockedRegex = new RegExp('CODECS="[^"]*(' + blockedCodecs + ')[^"]*"', 'gi'); | |
| 524 | - | text = text.replace(blockedRegex, '');`) | |
| 525 | - | } | |
| 526 | - | ||
| 527 | - | js.WriteString(` | |
| 528 | - | ||
| 529 | - | console.log('[dumber-codec] Twitch: Modified HLS manifest'); | |
| 530 | - | return new Response(text, { | |
| 531 | - | status: response.status, | |
| 532 | - | statusText: response.statusText, | |
| 533 | - | headers: response.headers | |
| 534 | - | }); | |
| 535 | - | }); | |
| 536 | - | } | |
| 537 | - | return response; | |
| 538 | - | }); | |
| 539 | - | } | |
| 540 | - | ||
| 541 | - | return originalFetch.call(this, input, init); | |
| 542 | - | }; | |
| 543 | - | ||
| 544 | - | // Override Twitch player codec selection | |
| 545 | - | const originalDefineProperty = Object.defineProperty; | |
| 546 | - | Object.defineProperty = function(obj, prop, descriptor) { | |
| 547 | - | // Intercept video codec settings | |
| 548 | - | if (prop === 'videoCodec' && descriptor.value) {`) | |
| 549 | - | ||
| 550 | - | if prefs.ForceAV1 && hasPreferredCodecs { | |
| 551 | - | js.WriteString(` | |
| 552 | - | if (true) { | |
| 553 | - | descriptor.value = 'av01'; | |
| 554 | - | console.log('[dumber-codec] Twitch: Forced AV1 codec'); | |
| 555 | - | }`) | |
| 556 | - | } else if hasPreferredCodecs && firstPreferredCodec != "" { | |
| 557 | - | twitchCodec := firstPreferredCodec | |
| 558 | - | switch strings.ToLower(firstPreferredCodec) { | |
| 559 | - | case "av1", "av01": | |
| 560 | - | twitchCodec = "av01" | |
| 561 | - | case "h264", "avc1": | |
| 562 | - | twitchCodec = "avc1" | |
| 563 | - | default: | |
| 564 | - | twitchCodec = firstPreferredCodec | |
| 565 | - | } | |
| 566 | - | js.WriteString(` | |
| 567 | - | else if (true && "` + firstPreferredCodec + `" !== "") { | |
| 568 | - | const twitchCodec = "` + twitchCodec + `"; | |
| 569 | - | descriptor.value = twitchCodec; | |
| 570 | - | console.log('[dumber-codec] Twitch: Set preferred codec:', twitchCodec); | |
| 571 | - | }`) | |
| 572 | - | } | |
| 573 | - | ||
| 574 | - | js.WriteString(` | |
| 575 | - | } | |
| 576 | - | ||
| 577 | - | return originalDefineProperty.call(this, obj, prop, descriptor); | |
| 578 | - | }; | |
| 477 | + | console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability'); | |
| 478 | + | // Twitch codec control completely removed to prevent theater/fullscreen freezing | |
| 579 | 479 | } | |
| 580 | 480 | ||
| 581 | 481 | // Passive video monitoring without interfering with element creation | |
| @@ -605,12 +505,3 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||
| 605 | 505 | return js.String() | |
| 606 | 506 | } | |
| 607 | 507 | ||
| 608 | - | // convertConfigCodecsToWebKitCodecs converts config codec preferences to WebKit format | |
| 609 | - | func convertConfigCodecsToWebKitCodecs(codecPrefs interface{}) CodecPreferencesConfig { | |
| 610 | - | // This function will be called from the main browser service to convert | |
| 611 | - | // the config.CodecConfig to webkit.CodecPreferencesConfig | |
| 612 | - | ||
| 613 | - | // We'll need to import the config package for this conversion | |
| 614 | - | // For now, return empty config and implement the conversion later | |
| 615 | - | return CodecPreferencesConfig{} | |
| 616 | - | } | |
| @@ -44,10 +44,7 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 44 | blockedPattern := strings.Join(blockedCodecs, "|") | 44 | blockedPattern := strings.Join(blockedCodecs, "|") | ||
| 45 | hasBlockedCodecs := len(blockedCodecs) > 0 | 45 | hasBlockedCodecs := len(blockedCodecs) > 0 | ||
| 46 | hasPreferredCodecs := len(preferredCodecs) > 0 | 46 | hasPreferredCodecs := len(preferredCodecs) > 0 | ||
| 47 | - | firstPreferredCodec := "" | 47 | + | |
| 48 | - | if len(preferredCodecs) > 0 { | |||
| 49 | - | firstPreferredCodec = preferredCodecs[0] | |||
| 50 | - | } | |||
| 51 | 48 | ||||
| 52 | // Convert preferredCodecs slice to proper JavaScript array syntax | 49 | // Convert preferredCodecs slice to proper JavaScript array syntax | ||
| 53 | preferredCodecsJS := "[" + strings.Join(func() []string { | 50 | preferredCodecsJS := "[" + strings.Join(func() []string { | ||
| @@ -121,6 +118,13 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 121 | 118 | ||||
| 122 | console.log('[dumber-codec] WebKit fullscreen transition detected'); | 119 | console.log('[dumber-codec] WebKit fullscreen transition detected'); | ||
| 123 | 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 | + | ||||
| 124 | const videos = document.querySelectorAll('video'); | 128 | const videos = document.querySelectorAll('video'); | ||
| 125 | const isEntering = !!(document.fullscreenElement || document.webkitFullscreenElement); | 129 | const isEntering = !!(document.fullscreenElement || document.webkitFullscreenElement); | ||
| 126 | 130 | ||||
| @@ -468,114 +472,10 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 468 | }; | 472 | }; | ||
| 469 | } | 473 | } | ||
| 470 | 474 | ||||
| 471 | - | // Twitch-specific codec control | 475 | + | // Twitch: No codec interference - let Twitch handle codec selection natively |
| 472 | if (location.hostname.includes('twitch.tv')) { | 476 | if (location.hostname.includes('twitch.tv')) { | ||
| 473 | - | console.log('[dumber-codec] Applying Twitch codec preferences'); | 477 | + | console.log('[dumber-codec] Detected Twitch domain - no codec interference for stability'); |
| 474 | - | 478 | + | // Twitch codec control completely removed to prevent theater/fullscreen freezing | |
| 475 | - | // Intercept Twitch Enhanced Broadcasting API calls | |||
| 476 | - | const originalFetch = window.fetch; | |||
| 477 | - | window.fetch = function(input, init) { | |||
| 478 | - | const url = typeof input === 'string' ? input : input.url; | |||
| 479 | - | ||||
| 480 | - | // Intercept Enhanced Broadcasting API calls | |||
| 481 | - | if (url && url.includes('/api/channel/hls') || url.includes('/enhanced_broadcasting')) { | |||
| 482 | - | console.log('[dumber-codec] Twitch: Intercepting Enhanced Broadcasting API call'); | |||
| 483 | - | ||||
| 484 | - | // Modify request to prefer AV1 if available | |||
| 485 | - | if (init && init.body) { | |||
| 486 | - | try { | |||
| 487 | - | const body = JSON.parse(init.body); | |||
| 488 | - | if (body.supported_codecs) {`) | |||
| 489 | - | ||||
| 490 | - | if prefs.ForceAV1 || hasPreferredCodecs { | |||
| 491 | - | js.WriteString(` | |||
| 492 | - | // Reorder supported codecs to prioritize AV1 | |||
| 493 | - | const preferredOrder = ['av01', 'avc1', 'vp9', 'vp8']; | |||
| 494 | - | body.supported_codecs.sort((a, b) => { | |||
| 495 | - | const aIdx = preferredOrder.indexOf(a.toLowerCase()) !== -1 ? | |||
| 496 | - | preferredOrder.indexOf(a.toLowerCase()) : 999; | |||
| 497 | - | const bIdx = preferredOrder.indexOf(b.toLowerCase()) !== -1 ? | |||
| 498 | - | preferredOrder.indexOf(b.toLowerCase()) : 999; | |||
| 499 | - | return aIdx - bIdx; | |||
| 500 | - | }); | |||
| 501 | - | console.log('[dumber-codec] Twitch: Reordered supported codecs:', body.supported_codecs);`) | |||
| 502 | - | } | |||
| 503 | - | ||||
| 504 | - | js.WriteString(` | |||
| 505 | - | } | |||
| 506 | - | init.body = JSON.stringify(body); | |||
| 507 | - | } catch (e) { | |||
| 508 | - | // Non-JSON body, ignore | |||
| 509 | - | } | |||
| 510 | - | } | |||
| 511 | - | } | |||
| 512 | - | ||||
| 513 | - | // Intercept HLS manifest requests for codec modification | |||
| 514 | - | if (url && (url.includes('.m3u8') || url.includes('/playlist/'))) { | |||
| 515 | - | return originalFetch.call(this, input, init).then(response => { | |||
| 516 | - | if (response.ok && response.headers.get('content-type')?.includes('application/vnd.apple.mpegurl')) { | |||
| 517 | - | return response.text().then(text => {`) | |||
| 518 | - | ||||
| 519 | - | if hasBlockedCodecs { | |||
| 520 | - | js.WriteString(` | |||
| 521 | - | // Remove blocked codecs from HLS manifest | |||
| 522 | - | const blockedCodecs = ` + preferredCodecsJS + `.join('|'); | |||
| 523 | - | const blockedRegex = new RegExp('CODECS="[^"]*(' + blockedCodecs + ')[^"]*"', 'gi'); | |||
| 524 | - | text = text.replace(blockedRegex, '');`) | |||
| 525 | - | } | |||
| 526 | - | ||||
| 527 | - | js.WriteString(` | |||
| 528 | - | ||||
| 529 | - | console.log('[dumber-codec] Twitch: Modified HLS manifest'); | |||
| 530 | - | return new Response(text, { | |||
| 531 | - | status: response.status, | |||
| 532 | - | statusText: response.statusText, | |||
| 533 | - | headers: response.headers | |||
| 534 | - | }); | |||
| 535 | - | }); | |||
| 536 | - | } | |||
| 537 | - | return response; | |||
| 538 | - | }); | |||
| 539 | - | } | |||
| 540 | - | ||||
| 541 | - | return originalFetch.call(this, input, init); | |||
| 542 | - | }; | |||
| 543 | - | ||||
| 544 | - | // Override Twitch player codec selection | |||
| 545 | - | const originalDefineProperty = Object.defineProperty; | |||
| 546 | - | Object.defineProperty = function(obj, prop, descriptor) { | |||
| 547 | - | // Intercept video codec settings | |||
| 548 | - | if (prop === 'videoCodec' && descriptor.value) {`) | |||
| 549 | - | ||||
| 550 | - | if prefs.ForceAV1 && hasPreferredCodecs { | |||
| 551 | - | js.WriteString(` | |||
| 552 | - | if (true) { | |||
| 553 | - | descriptor.value = 'av01'; | |||
| 554 | - | console.log('[dumber-codec] Twitch: Forced AV1 codec'); | |||
| 555 | - | }`) | |||
| 556 | - | } else if hasPreferredCodecs && firstPreferredCodec != "" { | |||
| 557 | - | twitchCodec := firstPreferredCodec | |||
| 558 | - | switch strings.ToLower(firstPreferredCodec) { | |||
| 559 | - | case "av1", "av01": | |||
| 560 | - | twitchCodec = "av01" | |||
| 561 | - | case "h264", "avc1": | |||
| 562 | - | twitchCodec = "avc1" | |||
| 563 | - | default: | |||
| 564 | - | twitchCodec = firstPreferredCodec | |||
| 565 | - | } | |||
| 566 | - | js.WriteString(` | |||
| 567 | - | else if (true && "` + firstPreferredCodec + `" !== "") { | |||
| 568 | - | const twitchCodec = "` + twitchCodec + `"; | |||
| 569 | - | descriptor.value = twitchCodec; | |||
| 570 | - | console.log('[dumber-codec] Twitch: Set preferred codec:', twitchCodec); | |||
| 571 | - | }`) | |||
| 572 | - | } | |||
| 573 | - | ||||
| 574 | - | js.WriteString(` | |||
| 575 | - | } | |||
| 576 | - | ||||
| 577 | - | return originalDefineProperty.call(this, obj, prop, descriptor); | |||
| 578 | - | }; | |||
| 579 | } | 479 | } | ||
| 580 | 480 | ||||
| 581 | // Passive video monitoring without interfering with element creation | 481 | // Passive video monitoring without interfering with element creation | ||
| @@ -605,12 +505,3 @@ func GenerateCodecControlScript(prefs CodecPreferencesConfig) string { | |||||
| 605 | return js.String() | 505 | return js.String() | ||
| 606 | } | 506 | } | ||
| 607 | 507 | ||||
| 608 | - | // convertConfigCodecsToWebKitCodecs converts config codec preferences to WebKit format | |||
| 609 | - | func convertConfigCodecsToWebKitCodecs(codecPrefs interface{}) CodecPreferencesConfig { | |||
| 610 | - | // This function will be called from the main browser service to convert | |||
| 611 | - | // the config.CodecConfig to webkit.CodecPreferencesConfig | |||
| 612 | - | ||||
| 613 | - | // We'll need to import the config package for this conversion | |||
| 614 | - | // For now, return empty config and implement the conversion later | |||
| 615 | - | return CodecPreferencesConfig{} | |||
| 616 | - | } | |||
M pkg/webkit/settings.go +5 -4
| @@ -41,10 +41,11 @@ type Config struct { | |||
| 41 | 41 | ||
| 42 | 42 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | |
| 43 | 43 | type CodecPreferencesConfig struct { | |
| 44 | - | PreferredCodecs []string | |
| 45 | - | BlockedCodecs []string | |
| 46 | - | ForceAV1 bool | |
| 47 | - | CustomUserAgent string | |
| 44 | + | PreferredCodecs []string | |
| 45 | + | BlockedCodecs []string | |
| 46 | + | ForceAV1 bool | |
| 47 | + | CustomUserAgent string | |
| 48 | + | DisableTwitchCodecControl bool | |
| 48 | 49 | } | |
| 49 | 50 | ||
| 50 | 51 | // RenderingConfig controls hardware acceleration preferences. | |
| @@ -41,10 +41,11 @@ type Config struct { | |||||
| 41 | 41 | ||||
| 42 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | 42 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | ||
| 43 | type CodecPreferencesConfig struct { | 43 | type CodecPreferencesConfig struct { | ||
| 44 | - | PreferredCodecs []string | 44 | + | PreferredCodecs []string |
| 45 | - | BlockedCodecs []string | 45 | + | BlockedCodecs []string |
| 46 | - | ForceAV1 bool | 46 | + | ForceAV1 bool |
| 47 | - | CustomUserAgent string | 47 | + | CustomUserAgent string |
| 48 | + | DisableTwitchCodecControl bool | |||
| 48 | } | 49 | } | ||
| 49 | 50 | ||||
| 50 | // RenderingConfig controls hardware acceleration preferences. | 51 | // RenderingConfig controls hardware acceleration preferences. | ||