detached 91bd671

cef: remove reddit-specific video JS injection

commit
91bd67170d4e6ca397743029e204c015a46d7ff5
author
brice · · 2026-04-28 03:43 UTC
parent
bf4a0b5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +0 -229
M internal/infrastructure/cef/content_injector.go +0 -229
@@ -3,8 +3,6 @@ package cef
33 import (
44 "context"
55 "fmt"
6- "net/url"
7- "strings"
86 "sync"
97
108 "github.com/bnema/dumber/internal/infrastructure/webutil"
@@ -164,227 +162,6 @@ const videoDiagnosticJS = `(function(){
164162 'aac:', MediaSource ? MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"') : 'n/a');
165163 })();`
166164
167-// redditDirectVideoJS replaces Reddit's blob/MSE playback path with a direct
168-// HLS source when MP4 MSE buffers are unsupported. This lets the browser issue
169-// a normal media request that our transcoding handler can answer with WebM.
170-const redditDirectVideoJS = `(function(){
171- var tag = '[REDDIT-VIDEO-PATCH]';
172- if (!/(\.|^)reddit\.com$/i.test(location.hostname)) return;
173- if (!window.MediaSource) return;
174- if (MediaSource.isTypeSupported('video/mp4; codecs="avc1.4d401e"') &&
175- MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')) {
176- return;
177- }
178-
179- console.warn(tag, 'active for unsupported Reddit MSE playback');
180-
181- function ensureStyle() {
182- if (document.getElementById('dumber-reddit-direct-video-style')) return;
183- var style = document.createElement('style');
184- style.id = 'dumber-reddit-direct-video-style';
185- style.textContent =
186- 'shreddit-player[data-dumber-direct-video="1"] { position: relative !important; }' +
187- 'shreddit-player[data-dumber-direct-video="1"] > .dumber-direct-video {' +
188- ' position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important;' +
189- ' object-fit: contain !important; background: #000 !important; z-index: 2147483647 !important;' +
190- ' display: block !important; }';
191- document.head.appendChild(style);
192- }
193-
194- function candidateRoots(video) {
195- var roots = [];
196- var seen = [];
197- function add(root) {
198- if (!root) return;
199- if (seen.indexOf(root) !== -1) return;
200- seen.push(root);
201- roots.push(root);
202- }
203-
204- var current = video;
205- while (current) {
206- add(current);
207- if (current.shadowRoot) add(current.shadowRoot);
208- current = current.parentElement;
209- }
210- add(document);
211- return roots;
212- }
213-
214- function findHlsSource(video) {
215- var selectors = [
216- 'source[type="application/vnd.apple.mpegURL"][src]',
217- 'source[type="application/x-mpegURL"][src]',
218- 'source[src*="HLSPlaylist.m3u8"]',
219- '[src*="HLSPlaylist.m3u8"]'
220- ];
221-
222- var roots = candidateRoots(video);
223- for (var i = 0; i < roots.length; i++) {
224- var root = roots[i];
225- if (!root.querySelector) continue;
226- for (var j = 0; j < selectors.length; j++) {
227- var el = root.querySelector(selectors[j]);
228- if (el && el.src) return el.src;
229- }
230- if (root.querySelectorAll) {
231- var all = root.querySelectorAll('*');
232- for (var k = 0; k < all.length; k++) {
233- if (all[k].shadowRoot) {
234- roots.push(all[k].shadowRoot);
235- }
236- }
237- }
238- }
239- return '';
240- }
241-
242- function buildPlaybackURL(hlsURL) {
243- return location.origin + '/__dumber__/transcode.webm?src=' + encodeURIComponent(hlsURL) +
244- '&referer=' + encodeURIComponent(location.href) +
245- '&origin=' + encodeURIComponent(location.origin);
246- }
247-
248- function getPlayerRoot(video) {
249- return video.closest('shreddit-player') || video.parentElement;
250- }
251-
252- function monitorReplacement(video) {
253- if (!video || video._dumberReplacementDiag) return;
254- video._dumberReplacementDiag = true;
255- [
256- 'loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'playing',
257- 'waiting', 'stalled', 'suspend', 'abort', 'emptied', 'error',
258- ].forEach(function(evt) {
259- video.addEventListener(evt, function() {
260- var err = video.error ? ('code=' + video.error.code + ' msg=' + video.error.message) : 'none';
261- console.warn(tag, 'replacement', evt, 'ready:', video.readyState, 'net:', video.networkState,
262- 'paused:', video.paused, 'error:', err, 'src:',
263- (video.currentSrc || video.src || '').substring(0, 120));
264- });
265- });
266- }
267-
268- function ensureReplacement(video, playbackURL) {
269- var root = getPlayerRoot(video);
270- if (!root) return null;
271- ensureStyle();
272-
273- var replacement = root.querySelector(':scope > .dumber-direct-video');
274- if (!replacement) {
275- replacement = document.createElement('video');
276- replacement.className = 'dumber-direct-video';
277- replacement.controls = true;
278- replacement.playsInline = true;
279- replacement.preload = 'auto';
280- replacement.autoplay = true;
281- replacement.muted = true;
282- replacement.loop = video.loop;
283- root.setAttribute('data-dumber-direct-video', '1');
284- root.appendChild(replacement);
285- }
286-
287- replacement.poster = video.poster || replacement.poster || '';
288- monitorReplacement(replacement);
289- if (replacement.src !== playbackURL) {
290- replacement.src = playbackURL;
291- replacement.load();
292- }
293- return replacement;
294- }
295-
296- function shouldForce(video) {
297- var current = video.currentSrc || video.src || '';
298- return current.indexOf('blob:') === 0;
299- }
300-
301- function forceDirectPlayback(video) {
302- if (!video || !shouldForce(video)) return;
303-
304- var hlsURL = findHlsSource(video);
305- if (!hlsURL) {
306- console.warn(tag, 'blob video found without HLS source');
307- return;
308- }
309- var playbackURL = buildPlaybackURL(hlsURL);
310- if (video._dumberPlaybackURL === playbackURL) return;
311- video._dumberPlaybackURL = playbackURL;
312-
313- console.warn(tag, 'forcing replacement video source:', playbackURL.substring(0, 120), 'from:', hlsURL.substring(0, 120));
314-
315- var replacement = ensureReplacement(video, playbackURL);
316- if (!replacement) return;
317-
318- var root = getPlayerRoot(video);
319- if (root && root.querySelectorAll) {
320- root.querySelectorAll('video').forEach(function(candidate) {
321- if (candidate === replacement) return;
322- try { candidate.pause(); } catch (e) {}
323- try { candidate.removeAttribute('src'); } catch (e) {}
324- try { candidate.srcObject = null; } catch (e) {}
325- try { candidate.load(); } catch (e) {}
326- candidate.style.display = 'none';
327- });
328- } else {
329- try { video.pause(); } catch (e) {}
330- try { video.removeAttribute('src'); } catch (e) {}
331- try { video.srcObject = null; } catch (e) {}
332- try { video.load(); } catch (e) {}
333- video.style.display = 'none';
334- }
335-
336- var playPromise = replacement.play();
337- if (playPromise && typeof playPromise.catch === 'function') {
338- playPromise.catch(function(err) {
339- console.warn(tag, 'replacement video.play() rejected:', err && err.message ? err.message : err);
340- });
341- }
342- }
343-
344- function patchTree(root) {
345- if (!root || !root.querySelectorAll) return;
346- root.querySelectorAll('video').forEach(forceDirectPlayback);
347- root.querySelectorAll('*').forEach(function(el) {
348- if (el.shadowRoot) patchTree(el.shadowRoot);
349- });
350- }
351-
352- new MutationObserver(function(mutations) {
353- mutations.forEach(function(mutation) {
354- if (mutation.type === 'attributes' && mutation.target && mutation.target.nodeName === 'VIDEO') {
355- forceDirectPlayback(mutation.target);
356- }
357- mutation.addedNodes.forEach(function(node) {
358- if (node.nodeName === 'VIDEO') forceDirectPlayback(node);
359- patchTree(node);
360- if (node.shadowRoot) patchTree(node.shadowRoot);
361- });
362- });
363- }).observe(document.documentElement, {
364- subtree: true,
365- childList: true,
366- attributes: true,
367- attributeFilter: ['src']
368- });
369-
370- // Chain to existing attachShadow patch (e.g. videoDiagnosticJS) instead of overwriting.
371- var prevAttach = Element.prototype.attachShadow;
372- Element.prototype.attachShadow = function(opts) {
373- var sr = prevAttach.call(this, opts);
374- patchTree(sr);
375- return sr;
376- };
377-
378- patchTree(document);
379- var pollCount = 0;
380- var maxPolls = 30;
381- var pollID = setInterval(function() {
382- patchTree(document);
383- pollCount++;
384- if (pollCount >= maxPolls) clearInterval(pollID);
385- }, 1000);
386-})();`
387-
388165 // contentInjector implements port.ContentInjector for the CEF engine.
389166 // It stores CSS strings and injects them into webviews via ExecuteJavaScript.
390167 // Thread-safe: InjectThemeCSS may be called from the UI thread while OnLoadEnd
@@ -524,12 +301,6 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) {
524301 wv.RunJavaScript(context.Background(), videoDiagnosticJS)
525302 }
526303
527- if parsed, err := url.Parse(uri); err == nil {
528- host := parsed.Hostname()
529- if host == "reddit.com" || strings.HasSuffix(host, ".reddit.com") {
530- wv.RunJavaScript(context.Background(), redditDirectVideoJS)
531- }
532- }
533304 }
534305
535306 // injectCSS injects a CSS string as a <style> element via JavaScript.
@@ -3,8 +3,6 @@ package cef
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6- "net/url"
7- "strings"
8 "sync"6 "sync"
9 7
10 "github.com/bnema/dumber/internal/infrastructure/webutil"8 "github.com/bnema/dumber/internal/infrastructure/webutil"
@@ -164,227 +162,6 @@ const videoDiagnosticJS = `(function(){
164 'aac:', MediaSource ? MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"') : 'n/a');162 'aac:', MediaSource ? MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"') : 'n/a');
165 })();`163 })();`
166 164
167-// redditDirectVideoJS replaces Reddit's blob/MSE playback path with a direct
168-// HLS source when MP4 MSE buffers are unsupported. This lets the browser issue
169-// a normal media request that our transcoding handler can answer with WebM.
170-const redditDirectVideoJS = `(function(){
171- var tag = '[REDDIT-VIDEO-PATCH]';
172- if (!/(\.|^)reddit\.com$/i.test(location.hostname)) return;
173- if (!window.MediaSource) return;
174- if (MediaSource.isTypeSupported('video/mp4; codecs="avc1.4d401e"') &&
175- MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')) {
176- return;
177- }
178-
179- console.warn(tag, 'active for unsupported Reddit MSE playback');
180-
181- function ensureStyle() {
182- if (document.getElementById('dumber-reddit-direct-video-style')) return;
183- var style = document.createElement('style');
184- style.id = 'dumber-reddit-direct-video-style';
185- style.textContent =
186- 'shreddit-player[data-dumber-direct-video="1"] { position: relative !important; }' +
187- 'shreddit-player[data-dumber-direct-video="1"] > .dumber-direct-video {' +
188- ' position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important;' +
189- ' object-fit: contain !important; background: #000 !important; z-index: 2147483647 !important;' +
190- ' display: block !important; }';
191- document.head.appendChild(style);
192- }
193-
194- function candidateRoots(video) {
195- var roots = [];
196- var seen = [];
197- function add(root) {
198- if (!root) return;
199- if (seen.indexOf(root) !== -1) return;
200- seen.push(root);
201- roots.push(root);
202- }
203-
204- var current = video;
205- while (current) {
206- add(current);
207- if (current.shadowRoot) add(current.shadowRoot);
208- current = current.parentElement;
209- }
210- add(document);
211- return roots;
212- }
213-
214- function findHlsSource(video) {
215- var selectors = [
216- 'source[type="application/vnd.apple.mpegURL"][src]',
217- 'source[type="application/x-mpegURL"][src]',
218- 'source[src*="HLSPlaylist.m3u8"]',
219- '[src*="HLSPlaylist.m3u8"]'
220- ];
221-
222- var roots = candidateRoots(video);
223- for (var i = 0; i < roots.length; i++) {
224- var root = roots[i];
225- if (!root.querySelector) continue;
226- for (var j = 0; j < selectors.length; j++) {
227- var el = root.querySelector(selectors[j]);
228- if (el && el.src) return el.src;
229- }
230- if (root.querySelectorAll) {
231- var all = root.querySelectorAll('*');
232- for (var k = 0; k < all.length; k++) {
233- if (all[k].shadowRoot) {
234- roots.push(all[k].shadowRoot);
235- }
236- }
237- }
238- }
239- return '';
240- }
241-
242- function buildPlaybackURL(hlsURL) {
243- return location.origin + '/__dumber__/transcode.webm?src=' + encodeURIComponent(hlsURL) +
244- '&referer=' + encodeURIComponent(location.href) +
245- '&origin=' + encodeURIComponent(location.origin);
246- }
247-
248- function getPlayerRoot(video) {
249- return video.closest('shreddit-player') || video.parentElement;
250- }
251-
252- function monitorReplacement(video) {
253- if (!video || video._dumberReplacementDiag) return;
254- video._dumberReplacementDiag = true;
255- [
256- 'loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'playing',
257- 'waiting', 'stalled', 'suspend', 'abort', 'emptied', 'error',
258- ].forEach(function(evt) {
259- video.addEventListener(evt, function() {
260- var err = video.error ? ('code=' + video.error.code + ' msg=' + video.error.message) : 'none';
261- console.warn(tag, 'replacement', evt, 'ready:', video.readyState, 'net:', video.networkState,
262- 'paused:', video.paused, 'error:', err, 'src:',
263- (video.currentSrc || video.src || '').substring(0, 120));
264- });
265- });
266- }
267-
268- function ensureReplacement(video, playbackURL) {
269- var root = getPlayerRoot(video);
270- if (!root) return null;
271- ensureStyle();
272-
273- var replacement = root.querySelector(':scope > .dumber-direct-video');
274- if (!replacement) {
275- replacement = document.createElement('video');
276- replacement.className = 'dumber-direct-video';
277- replacement.controls = true;
278- replacement.playsInline = true;
279- replacement.preload = 'auto';
280- replacement.autoplay = true;
281- replacement.muted = true;
282- replacement.loop = video.loop;
283- root.setAttribute('data-dumber-direct-video', '1');
284- root.appendChild(replacement);
285- }
286-
287- replacement.poster = video.poster || replacement.poster || '';
288- monitorReplacement(replacement);
289- if (replacement.src !== playbackURL) {
290- replacement.src = playbackURL;
291- replacement.load();
292- }
293- return replacement;
294- }
295-
296- function shouldForce(video) {
297- var current = video.currentSrc || video.src || '';
298- return current.indexOf('blob:') === 0;
299- }
300-
301- function forceDirectPlayback(video) {
302- if (!video || !shouldForce(video)) return;
303-
304- var hlsURL = findHlsSource(video);
305- if (!hlsURL) {
306- console.warn(tag, 'blob video found without HLS source');
307- return;
308- }
309- var playbackURL = buildPlaybackURL(hlsURL);
310- if (video._dumberPlaybackURL === playbackURL) return;
311- video._dumberPlaybackURL = playbackURL;
312-
313- console.warn(tag, 'forcing replacement video source:', playbackURL.substring(0, 120), 'from:', hlsURL.substring(0, 120));
314-
315- var replacement = ensureReplacement(video, playbackURL);
316- if (!replacement) return;
317-
318- var root = getPlayerRoot(video);
319- if (root && root.querySelectorAll) {
320- root.querySelectorAll('video').forEach(function(candidate) {
321- if (candidate === replacement) return;
322- try { candidate.pause(); } catch (e) {}
323- try { candidate.removeAttribute('src'); } catch (e) {}
324- try { candidate.srcObject = null; } catch (e) {}
325- try { candidate.load(); } catch (e) {}
326- candidate.style.display = 'none';
327- });
328- } else {
329- try { video.pause(); } catch (e) {}
330- try { video.removeAttribute('src'); } catch (e) {}
331- try { video.srcObject = null; } catch (e) {}
332- try { video.load(); } catch (e) {}
333- video.style.display = 'none';
334- }
335-
336- var playPromise = replacement.play();
337- if (playPromise && typeof playPromise.catch === 'function') {
338- playPromise.catch(function(err) {
339- console.warn(tag, 'replacement video.play() rejected:', err && err.message ? err.message : err);
340- });
341- }
342- }
343-
344- function patchTree(root) {
345- if (!root || !root.querySelectorAll) return;
346- root.querySelectorAll('video').forEach(forceDirectPlayback);
347- root.querySelectorAll('*').forEach(function(el) {
348- if (el.shadowRoot) patchTree(el.shadowRoot);
349- });
350- }
351-
352- new MutationObserver(function(mutations) {
353- mutations.forEach(function(mutation) {
354- if (mutation.type === 'attributes' && mutation.target && mutation.target.nodeName === 'VIDEO') {
355- forceDirectPlayback(mutation.target);
356- }
357- mutation.addedNodes.forEach(function(node) {
358- if (node.nodeName === 'VIDEO') forceDirectPlayback(node);
359- patchTree(node);
360- if (node.shadowRoot) patchTree(node.shadowRoot);
361- });
362- });
363- }).observe(document.documentElement, {
364- subtree: true,
365- childList: true,
366- attributes: true,
367- attributeFilter: ['src']
368- });
369-
370- // Chain to existing attachShadow patch (e.g. videoDiagnosticJS) instead of overwriting.
371- var prevAttach = Element.prototype.attachShadow;
372- Element.prototype.attachShadow = function(opts) {
373- var sr = prevAttach.call(this, opts);
374- patchTree(sr);
375- return sr;
376- };
377-
378- patchTree(document);
379- var pollCount = 0;
380- var maxPolls = 30;
381- var pollID = setInterval(function() {
382- patchTree(document);
383- pollCount++;
384- if (pollCount >= maxPolls) clearInterval(pollID);
385- }, 1000);
386-})();`
387-
388 // contentInjector implements port.ContentInjector for the CEF engine.165 // contentInjector implements port.ContentInjector for the CEF engine.
389 // It stores CSS strings and injects them into webviews via ExecuteJavaScript.166 // It stores CSS strings and injects them into webviews via ExecuteJavaScript.
390 // Thread-safe: InjectThemeCSS may be called from the UI thread while OnLoadEnd167 // Thread-safe: InjectThemeCSS may be called from the UI thread while OnLoadEnd
@@ -524,12 +301,6 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) {
524 wv.RunJavaScript(context.Background(), videoDiagnosticJS)301 wv.RunJavaScript(context.Background(), videoDiagnosticJS)
525 }302 }
526 303
527- if parsed, err := url.Parse(uri); err == nil {
528- host := parsed.Hostname()
529- if host == "reddit.com" || strings.HasSuffix(host, ".reddit.com") {
530- wv.RunJavaScript(context.Background(), redditDirectVideoJS)
531- }
532- }
533 }304 }
534 305
535 // injectCSS injects a CSS string as a <style> element via JavaScript.306 // injectCSS injects a CSS string as a <style> element via JavaScript.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close