Skip to content

Commit

Permalink
Blocking large media elements also prevents autoplay, regardless of size
Browse files Browse the repository at this point in the history
Related issue:
uBlockOrigin/uBlock-issues#3394

When the "No large media elements" per-site switch is toggled on,
it will also act to prevent autoplay of video/audio media, regardless
of their size. This also works for xhr-based media streaming.

If blocking by size is not desirable while blocking autoplay is
desired, one can toggle on "No large media elements" switch while
setting "Block media elements larger than ..." to a very high value.
  • Loading branch information
gorhill committed Oct 2, 2024
1 parent 0b02c7c commit 73ce4e6
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 132 deletions.
70 changes: 21 additions & 49 deletions platform/chromium/vapi-background-ext.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@
Home: https://github.com/gorhill/uBlock
*/

/* globals browser */

'use strict';

/******************************************************************************/

// https://github.com/uBlockOrigin/uBlock-issues/issues/1659
Expand Down Expand Up @@ -90,71 +86,47 @@ vAPI.Tabs = class extends vAPI.Tabs {
['gif','image'],['ico','image'],['jpeg','image'],['jpg','image'],['png','image'],['webp','image']
]);

const headerValue = (headers, name) => {
let i = headers.length;
while ( i-- ) {
if ( headers[i].name.toLowerCase() === name ) {
return headers[i].value.trim();
}
}
return '';
};

const parsedURL = new URL('https://www.example.org/');

// Extend base class to normalize as per platform.
// Extend base class to normalize as per platform

vAPI.Net = class extends vAPI.Net {
normalizeDetails(details) {
// Chromium 63+ supports the `initiator` property, which contains
// the URL of the origin from which the network request was made.
if (
typeof details.initiator === 'string' &&
details.initiator !== 'null'
) {
// the URL of the origin from which the network request was made
if ( details.initiator && details.initiator !== 'null' ) {
details.documentUrl = details.initiator;
}

let type = details.type;

const type = details.type;
if ( type === 'imageset' ) {
details.type = 'image';
return;
}

// The rest of the function code is to normalize type
if ( type !== 'other' ) { return; }

// Try to map known "extension" part of URL to request type.
parsedURL.href = details.url;
const path = parsedURL.pathname,
pos = path.indexOf('.', path.length - 6);
if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) {
details.type = type;
// Try to map known "extension" part of URL to request type
if ( details.responseHeaders === undefined ) {
parsedURL.href = details.url;
const path = parsedURL.pathname;
const pos = path.indexOf('.', path.length - 6);
if ( pos !== -1 ) {
details.type = extToTypeMap.get(path.slice(pos + 1)) || type;
}
return;
}

// Try to extract type from response headers if present.
if ( details.responseHeaders ) {
type = headerValue(details.responseHeaders, 'content-type');
if ( type.startsWith('font/') ) {
details.type = 'font';
return;
}
if ( type.startsWith('image/') ) {
details.type = 'image';
return;
}
if ( type.startsWith('audio/') || type.startsWith('video/') ) {
details.type = 'media';
return;
}
// Try to extract type from response headers
const ctype = this.headerValue(details.responseHeaders, 'content-type');
if ( ctype.startsWith('font/') ) {
details.type = 'font';
} else if ( ctype.startsWith('image/') ) {
details.type = 'image';
} else if ( ctype.startsWith('audio/') || ctype.startsWith('video/') ) {
details.type = 'media';
}
}

// https://www.reddit.com/r/uBlockOrigin/comments/9vcrk3/
// Some types can be mapped from 'other', thus include 'other' if and
// only if the caller is interested in at least one of those types.
// only if the caller is interested in at least one of those types
denormalizeTypes(types) {
if ( types.length === 0 ) {
return Array.from(this.validTypes);
Expand Down
8 changes: 8 additions & 0 deletions platform/common/vapi-background.js
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,14 @@ vAPI.Net = class {
if ( this.suspendDepth !== 0 ) { return; }
this.unsuspendAllRequests(discard);
}
headerValue(headers, name) {
for ( const header of headers ) {
if ( header.name.toLowerCase() === name ) {
return header.value.trim();
}
}
return '';
}
static canSuspend() {
return false;
}
Expand Down
15 changes: 6 additions & 9 deletions platform/firefox/vapi-background-ext.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,15 @@ vAPI.Net = class extends vAPI.Net {
details.type = 'image';
return;
}
if ( type !== 'object' ) { return; }
// Try to extract type from response headers if present.
if ( details.responseHeaders === undefined ) { return; }
const ctype = this.headerValue(details.responseHeaders, 'content-type');
// https://github.com/uBlockOrigin/uBlock-issues/issues/345
// Re-categorize an embedded object as a `sub_frame` if its
// content type is that of a HTML document.
if ( type === 'object' && Array.isArray(details.responseHeaders) ) {
for ( const header of details.responseHeaders ) {
if ( header.name.toLowerCase() === 'content-type' ) {
if ( header.value.startsWith('text/html') ) {
details.type = 'sub_frame';
}
break;
}
}
if ( ctype === 'text/html' ) {
details.type = 'sub_frame';
}
}

Expand Down
51 changes: 27 additions & 24 deletions src/js/pagestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -1019,48 +1019,51 @@ const PageStore = class {
}

// The caller is responsible to check whether filtering is enabled or not.
filterLargeMediaElement(fctxt, size) {
filterLargeMediaElement(fctxt, headers) {
fctxt.filter = undefined;

if ( this.allowLargeMediaElementsUntil === 0 ) {
if ( this.allowLargeMediaElementsUntil === 0 ) { return 0; }
if ( sessionSwitches.evaluateZ('no-large-media', fctxt.getTabHostname() ) !== true ) {
this.allowLargeMediaElementsUntil = 0;
return 0;
}
// Disregard large media elements previously allowed: for example, to
// seek inside a previously allowed audio/video.
if (
this.allowLargeMediaElementsRegex instanceof RegExp &&
this.allowLargeMediaElementsRegex.test(fctxt.url)
) {
// XHR-based streaming is never blocked but we want to prevent autoplay
if ( fctxt.itype === fctxt.XMLHTTPREQUEST ) {
const ctype = headers.contentType;
if ( ctype.startsWith('audio/') || ctype.startsWith('video/') ) {
this.largeMediaTimer.on(500);
}
return 0;
}
if ( Date.now() < this.allowLargeMediaElementsUntil ) {
const sources = this.allowLargeMediaElementsRegex instanceof RegExp
? [ this.allowLargeMediaElementsRegex.source ]
: [];
sources.push('^' + µb.escapeRegex(fctxt.url));
this.allowLargeMediaElementsRegex = new RegExp(sources.join('|'));
if ( fctxt.itype === fctxt.MEDIA ) {
const sources = this.allowLargeMediaElementsRegex instanceof RegExp
? [ this.allowLargeMediaElementsRegex.source ]
: [];
sources.push('^' + µb.escapeRegex(fctxt.url));
this.allowLargeMediaElementsRegex = new RegExp(sources.join('|'));
}
return 0;
}
// Disregard large media elements previously allowed: for example, to
// seek inside a previously allowed audio/video.
if (
sessionSwitches.evaluateZ(
'no-large-media',
fctxt.getTabHostname()
) !== true
this.allowLargeMediaElementsRegex instanceof RegExp &&
this.allowLargeMediaElementsRegex.test(fctxt.url)
) {
this.allowLargeMediaElementsUntil = 0;
return 0;
}
if ( (size >>> 10) < µb.userSettings.largeMediaSize ) {
return 0;
// Regardless of whether a media is blocked, we want to prevent autoplay
if ( fctxt.itype === fctxt.MEDIA ) {
this.largeMediaTimer.on(500);
}

const size = headers.contentLength;
if ( isNaN(size) ) { return 0; }
if ( (size >>> 10) < µb.userSettings.largeMediaSize ) { return 0; }
this.largeMediaCount += 1;
this.largeMediaTimer.on(500);

if ( logger.enabled ) {
fctxt.filter = sessionSwitches.toLogData();
}

return 1;
}

Expand Down
85 changes: 49 additions & 36 deletions src/js/scriptlets/load-large-media-interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,26 @@ if ( typeof vAPI !== 'object' || vAPI.loadAllLargeMedia instanceof Function ) {
return;
}

/******************************************************************************/

const largeMediaElementAttribute = 'data-' + vAPI.sessionId;
const largeMediaElementSelector =
':root audio[' + largeMediaElementAttribute + '],\n' +
':root img[' + largeMediaElementAttribute + '],\n' +
':root picture[' + largeMediaElementAttribute + '],\n' +
':root video[' + largeMediaElementAttribute + ']';

/******************************************************************************/
const isMediaElement = elem =>
(/^(?:audio|img|picture|video)$/.test(elem.localName));

const isMediaElement = function(elem) {
return /^(?:audio|img|picture|video)$/.test(elem.localName);
};
const isPlayableMediaElement = elem =>
(/^(?:audio|video)$/.test(elem.localName));

/******************************************************************************/

const mediaNotLoaded = function(elem) {
switch ( elem.localName ) {
case 'audio':
case 'video': {
const src = elem.src || '';
if ( src.startsWith('blob:') ) {
elem.autoplay = false;
elem.pause();
}
case 'video':
return elem.readyState === 0 || elem.error !== null;
}
case 'img': {
if ( elem.naturalWidth !== 0 || elem.naturalHeight !== 0 ) {
break;
Expand Down Expand Up @@ -99,29 +91,29 @@ const surveyMissingMediaElements = function() {
return largeMediaElementCount;
};

if ( surveyMissingMediaElements() === 0 ) { return; }

// Insert CSS to highlight blocked media elements.
if ( vAPI.largeMediaElementStyleSheet === undefined ) {
vAPI.largeMediaElementStyleSheet = [
largeMediaElementSelector + ' {',
'border: 2px dotted red !important;',
'box-sizing: border-box !important;',
'cursor: zoom-in !important;',
'display: inline-block;',
'filter: none !important;',
'font-size: 1rem !important;',
'min-height: 1em !important;',
'min-width: 1em !important;',
'opacity: 1 !important;',
'outline: none !important;',
'transform: none !important;',
'visibility: visible !important;',
'z-index: 2147483647',
'}',
].join('\n');
vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet);
vAPI.userStylesheet.apply();
if ( surveyMissingMediaElements() ) {
// Insert CSS to highlight blocked media elements.
if ( vAPI.largeMediaElementStyleSheet === undefined ) {
vAPI.largeMediaElementStyleSheet = [
largeMediaElementSelector + ' {',
'border: 2px dotted red !important;',
'box-sizing: border-box !important;',
'cursor: zoom-in !important;',
'display: inline-block;',
'filter: none !important;',
'font-size: 1rem !important;',
'min-height: 1em !important;',
'min-width: 1em !important;',
'opacity: 1 !important;',
'outline: none !important;',
'transform: none !important;',
'visibility: visible !important;',
'z-index: 2147483647',
'}',
].join('\n');
vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet);
vAPI.userStylesheet.apply();
}
}

/******************************************************************************/
Expand Down Expand Up @@ -258,6 +250,27 @@ document.addEventListener('error', onLoadError, true);

/******************************************************************************/

const autoPausedMedia = new WeakMap();

for ( const elem of document.querySelectorAll('audio,video') ) {
elem.setAttribute('autoplay', 'false');
}

const preventAutoplay = function(ev) {
const elem = ev.target;
if ( isPlayableMediaElement(elem) === false ) { return; }
const currentSrc = elem.getAttribute('src') || '';
const pausedSrc = autoPausedMedia.get(elem);
if ( pausedSrc === currentSrc ) { return; }
autoPausedMedia.set(elem, currentSrc);
elem.setAttribute('autoplay', 'false');
elem.pause();
};

document.addEventListener('timeupdate', preventAutoplay, true);

/******************************************************************************/

vAPI.loadAllLargeMedia = function() {
document.removeEventListener('click', onMouseClick, true);
document.removeEventListener('loadeddata', onLoadedData, true);
Expand Down
Loading

0 comments on commit 73ce4e6

Please sign in to comment.