mirror of https://github.com/gorhill/uBlock.git
Blocking large media elements also prevents autoplay, regardless of size
Related issue: https://github.com/uBlockOrigin/uBlock-issues/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.
This commit is contained in:
parent
0b02c7ccb6
commit
73ce4e6bcf
|
@ -19,10 +19,6 @@
|
||||||
Home: https://github.com/gorhill/uBlock
|
Home: https://github.com/gorhill/uBlock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* globals browser */
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/1659
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1659
|
||||||
|
@ -90,71 +86,47 @@ vAPI.Tabs = class extends vAPI.Tabs {
|
||||||
['gif','image'],['ico','image'],['jpeg','image'],['jpg','image'],['png','image'],['webp','image']
|
['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/');
|
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 {
|
vAPI.Net = class extends vAPI.Net {
|
||||||
normalizeDetails(details) {
|
normalizeDetails(details) {
|
||||||
// Chromium 63+ supports the `initiator` property, which contains
|
// Chromium 63+ supports the `initiator` property, which contains
|
||||||
// the URL of the origin from which the network request was made.
|
// the URL of the origin from which the network request was made
|
||||||
if (
|
if ( details.initiator && details.initiator !== 'null' ) {
|
||||||
typeof details.initiator === 'string' &&
|
|
||||||
details.initiator !== 'null'
|
|
||||||
) {
|
|
||||||
details.documentUrl = details.initiator;
|
details.documentUrl = details.initiator;
|
||||||
}
|
}
|
||||||
|
const type = details.type;
|
||||||
let type = details.type;
|
|
||||||
|
|
||||||
if ( type === 'imageset' ) {
|
if ( type === 'imageset' ) {
|
||||||
details.type = 'image';
|
details.type = 'image';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The rest of the function code is to normalize type
|
|
||||||
if ( type !== 'other' ) { return; }
|
if ( type !== 'other' ) { return; }
|
||||||
|
// Try to map known "extension" part of URL to request type
|
||||||
// Try to map known "extension" part of URL to request type.
|
if ( details.responseHeaders === undefined ) {
|
||||||
parsedURL.href = details.url;
|
parsedURL.href = details.url;
|
||||||
const path = parsedURL.pathname,
|
const path = parsedURL.pathname;
|
||||||
pos = path.indexOf('.', path.length - 6);
|
const pos = path.indexOf('.', path.length - 6);
|
||||||
if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) {
|
if ( pos !== -1 ) {
|
||||||
details.type = type;
|
details.type = extToTypeMap.get(path.slice(pos + 1)) || type;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Try to extract type from response headers
|
||||||
// Try to extract type from response headers if present.
|
const ctype = this.headerValue(details.responseHeaders, 'content-type');
|
||||||
if ( details.responseHeaders ) {
|
if ( ctype.startsWith('font/') ) {
|
||||||
type = headerValue(details.responseHeaders, 'content-type');
|
|
||||||
if ( type.startsWith('font/') ) {
|
|
||||||
details.type = 'font';
|
details.type = 'font';
|
||||||
return;
|
} else if ( ctype.startsWith('image/') ) {
|
||||||
}
|
|
||||||
if ( type.startsWith('image/') ) {
|
|
||||||
details.type = 'image';
|
details.type = 'image';
|
||||||
return;
|
} else if ( ctype.startsWith('audio/') || ctype.startsWith('video/') ) {
|
||||||
}
|
|
||||||
if ( type.startsWith('audio/') || type.startsWith('video/') ) {
|
|
||||||
details.type = 'media';
|
details.type = 'media';
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://www.reddit.com/r/uBlockOrigin/comments/9vcrk3/
|
// https://www.reddit.com/r/uBlockOrigin/comments/9vcrk3/
|
||||||
// Some types can be mapped from 'other', thus include 'other' if and
|
// 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) {
|
denormalizeTypes(types) {
|
||||||
if ( types.length === 0 ) {
|
if ( types.length === 0 ) {
|
||||||
return Array.from(this.validTypes);
|
return Array.from(this.validTypes);
|
||||||
|
|
|
@ -1382,6 +1382,14 @@ vAPI.Net = class {
|
||||||
if ( this.suspendDepth !== 0 ) { return; }
|
if ( this.suspendDepth !== 0 ) { return; }
|
||||||
this.unsuspendAllRequests(discard);
|
this.unsuspendAllRequests(discard);
|
||||||
}
|
}
|
||||||
|
headerValue(headers, name) {
|
||||||
|
for ( const header of headers ) {
|
||||||
|
if ( header.name.toLowerCase() === name ) {
|
||||||
|
return header.value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
static canSuspend() {
|
static canSuspend() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,19 +110,16 @@ vAPI.Net = class extends vAPI.Net {
|
||||||
details.type = 'image';
|
details.type = 'image';
|
||||||
return;
|
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
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/345
|
||||||
// Re-categorize an embedded object as a `sub_frame` if its
|
// Re-categorize an embedded object as a `sub_frame` if its
|
||||||
// content type is that of a HTML document.
|
// content type is that of a HTML document.
|
||||||
if ( type === 'object' && Array.isArray(details.responseHeaders) ) {
|
if ( ctype === 'text/html' ) {
|
||||||
for ( const header of details.responseHeaders ) {
|
|
||||||
if ( header.name.toLowerCase() === 'content-type' ) {
|
|
||||||
if ( header.value.startsWith('text/html') ) {
|
|
||||||
details.type = 'sub_frame';
|
details.type = 'sub_frame';
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
denormalizeTypes(types) {
|
denormalizeTypes(types) {
|
||||||
|
|
|
@ -1019,10 +1019,29 @@ const PageStore = class {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The caller is responsible to check whether filtering is enabled or not.
|
// The caller is responsible to check whether filtering is enabled or not.
|
||||||
filterLargeMediaElement(fctxt, size) {
|
filterLargeMediaElement(fctxt, headers) {
|
||||||
fctxt.filter = undefined;
|
fctxt.filter = undefined;
|
||||||
|
if ( this.allowLargeMediaElementsUntil === 0 ) { return 0; }
|
||||||
if ( this.allowLargeMediaElementsUntil === 0 ) {
|
if ( sessionSwitches.evaluateZ('no-large-media', fctxt.getTabHostname() ) !== true ) {
|
||||||
|
this.allowLargeMediaElementsUntil = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 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 ) {
|
||||||
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
// Disregard large media elements previously allowed: for example, to
|
// Disregard large media elements previously allowed: for example, to
|
||||||
|
@ -1033,34 +1052,18 @@ const PageStore = class {
|
||||||
) {
|
) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if ( Date.now() < this.allowLargeMediaElementsUntil ) {
|
// Regardless of whether a media is blocked, we want to prevent autoplay
|
||||||
const sources = this.allowLargeMediaElementsRegex instanceof RegExp
|
if ( fctxt.itype === fctxt.MEDIA ) {
|
||||||
? [ this.allowLargeMediaElementsRegex.source ]
|
this.largeMediaTimer.on(500);
|
||||||
: [];
|
|
||||||
sources.push('^' + µb.escapeRegex(fctxt.url));
|
|
||||||
this.allowLargeMediaElementsRegex = new RegExp(sources.join('|'));
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
if (
|
const size = headers.contentLength;
|
||||||
sessionSwitches.evaluateZ(
|
if ( isNaN(size) ) { return 0; }
|
||||||
'no-large-media',
|
if ( (size >>> 10) < µb.userSettings.largeMediaSize ) { return 0; }
|
||||||
fctxt.getTabHostname()
|
|
||||||
) !== true
|
|
||||||
) {
|
|
||||||
this.allowLargeMediaElementsUntil = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if ( (size >>> 10) < µb.userSettings.largeMediaSize ) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.largeMediaCount += 1;
|
this.largeMediaCount += 1;
|
||||||
this.largeMediaTimer.on(500);
|
this.largeMediaTimer.on(500);
|
||||||
|
|
||||||
if ( logger.enabled ) {
|
if ( logger.enabled ) {
|
||||||
fctxt.filter = sessionSwitches.toLogData();
|
fctxt.filter = sessionSwitches.toLogData();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,6 @@ if ( typeof vAPI !== 'object' || vAPI.loadAllLargeMedia instanceof Function ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
|
||||||
|
|
||||||
const largeMediaElementAttribute = 'data-' + vAPI.sessionId;
|
const largeMediaElementAttribute = 'data-' + vAPI.sessionId;
|
||||||
const largeMediaElementSelector =
|
const largeMediaElementSelector =
|
||||||
':root audio[' + largeMediaElementAttribute + '],\n' +
|
':root audio[' + largeMediaElementAttribute + '],\n' +
|
||||||
|
@ -37,25 +35,19 @@ const largeMediaElementSelector =
|
||||||
':root picture[' + largeMediaElementAttribute + '],\n' +
|
':root picture[' + largeMediaElementAttribute + '],\n' +
|
||||||
':root video[' + largeMediaElementAttribute + ']';
|
':root video[' + largeMediaElementAttribute + ']';
|
||||||
|
|
||||||
/******************************************************************************/
|
const isMediaElement = elem =>
|
||||||
|
(/^(?:audio|img|picture|video)$/.test(elem.localName));
|
||||||
|
|
||||||
const isMediaElement = function(elem) {
|
const isPlayableMediaElement = elem =>
|
||||||
return /^(?:audio|img|picture|video)$/.test(elem.localName);
|
(/^(?:audio|video)$/.test(elem.localName));
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const mediaNotLoaded = function(elem) {
|
const mediaNotLoaded = function(elem) {
|
||||||
switch ( elem.localName ) {
|
switch ( elem.localName ) {
|
||||||
case 'audio':
|
case 'audio':
|
||||||
case 'video': {
|
case 'video':
|
||||||
const src = elem.src || '';
|
|
||||||
if ( src.startsWith('blob:') ) {
|
|
||||||
elem.autoplay = false;
|
|
||||||
elem.pause();
|
|
||||||
}
|
|
||||||
return elem.readyState === 0 || elem.error !== null;
|
return elem.readyState === 0 || elem.error !== null;
|
||||||
}
|
|
||||||
case 'img': {
|
case 'img': {
|
||||||
if ( elem.naturalWidth !== 0 || elem.naturalHeight !== 0 ) {
|
if ( elem.naturalWidth !== 0 || elem.naturalHeight !== 0 ) {
|
||||||
break;
|
break;
|
||||||
|
@ -99,10 +91,9 @@ const surveyMissingMediaElements = function() {
|
||||||
return largeMediaElementCount;
|
return largeMediaElementCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( surveyMissingMediaElements() === 0 ) { return; }
|
if ( surveyMissingMediaElements() ) {
|
||||||
|
// Insert CSS to highlight blocked media elements.
|
||||||
// Insert CSS to highlight blocked media elements.
|
if ( vAPI.largeMediaElementStyleSheet === undefined ) {
|
||||||
if ( vAPI.largeMediaElementStyleSheet === undefined ) {
|
|
||||||
vAPI.largeMediaElementStyleSheet = [
|
vAPI.largeMediaElementStyleSheet = [
|
||||||
largeMediaElementSelector + ' {',
|
largeMediaElementSelector + ' {',
|
||||||
'border: 2px dotted red !important;',
|
'border: 2px dotted red !important;',
|
||||||
|
@ -122,6 +113,7 @@ if ( vAPI.largeMediaElementStyleSheet === undefined ) {
|
||||||
].join('\n');
|
].join('\n');
|
||||||
vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet);
|
vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet);
|
||||||
vAPI.userStylesheet.apply();
|
vAPI.userStylesheet.apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -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() {
|
vAPI.loadAllLargeMedia = function() {
|
||||||
document.removeEventListener('click', onMouseClick, true);
|
document.removeEventListener('click', onMouseClick, true);
|
||||||
document.removeEventListener('loadeddata', onLoadedData, true);
|
document.removeEventListener('loadeddata', onLoadedData, true);
|
||||||
|
|
|
@ -488,7 +488,7 @@ const onHeadersReceived = function(details) {
|
||||||
}
|
}
|
||||||
if ( pageStore.getNetFilteringSwitch(fctxt) === false ) { return; }
|
if ( pageStore.getNetFilteringSwitch(fctxt) === false ) { return; }
|
||||||
|
|
||||||
if ( fctxt.itype === fctxt.IMAGE || fctxt.itype === fctxt.MEDIA ) {
|
if ( (fctxt.itype & foilLargeMediaElement.TYPE_BITS) !== 0 ) {
|
||||||
const result = foilLargeMediaElement(details, fctxt, pageStore);
|
const result = foilLargeMediaElement(details, fctxt, pageStore);
|
||||||
if ( result !== undefined ) { return result; }
|
if ( result !== undefined ) { return result; }
|
||||||
}
|
}
|
||||||
|
@ -1124,15 +1124,12 @@ const injectPP = function(fctxt, pageStore, responseHeaders) {
|
||||||
const foilLargeMediaElement = function(details, fctxt, pageStore) {
|
const foilLargeMediaElement = function(details, fctxt, pageStore) {
|
||||||
if ( details.fromCache === true ) { return; }
|
if ( details.fromCache === true ) { return; }
|
||||||
|
|
||||||
let size = 0;
|
onDemandHeaders.setHeaders(details.responseHeaders);
|
||||||
if ( µb.userSettings.largeMediaSize !== 0 ) {
|
|
||||||
const headers = details.responseHeaders;
|
const result = pageStore.filterLargeMediaElement(fctxt, onDemandHeaders);
|
||||||
const i = headerIndexFromName('content-length', headers);
|
|
||||||
if ( i === -1 ) { return; }
|
onDemandHeaders.reset();
|
||||||
size = parseInt(headers[i].value, 10) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = pageStore.filterLargeMediaElement(fctxt, size);
|
|
||||||
if ( result === 0 ) { return; }
|
if ( result === 0 ) { return; }
|
||||||
|
|
||||||
if ( logger.enabled ) {
|
if ( logger.enabled ) {
|
||||||
|
@ -1142,16 +1139,15 @@ const foilLargeMediaElement = function(details, fctxt, pageStore) {
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
foilLargeMediaElement.TYPE_BITS = fc.IMAGE | fc.MEDIA | fc.XMLHTTPREQUEST;
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// Caller must ensure headerName is normalized to lower case.
|
// Caller must ensure headerName is normalized to lower case.
|
||||||
|
|
||||||
const headerIndexFromName = function(headerName, headers) {
|
const headerIndexFromName = function(headerName, headers) {
|
||||||
let i = headers.length;
|
for ( let i = 0, n = headers.length; i < n; i++ ) {
|
||||||
while ( i-- ) {
|
if ( headers[i].name.toLowerCase() === headerName ) { return i; }
|
||||||
if ( headers[i].name.toLowerCase() === headerName ) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
@ -1161,6 +1157,24 @@ const headerValueFromName = function(headerName, headers) {
|
||||||
return i !== -1 ? headers[i].value : '';
|
return i !== -1 ? headers[i].value : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDemandHeaders = {
|
||||||
|
headers: [],
|
||||||
|
get contentLength() {
|
||||||
|
const contentLength = headerValueFromName('content-length', this.headers);
|
||||||
|
if ( contentLength === '' ) { return Number.NaN; }
|
||||||
|
return parseInt(contentLength, 10) || 0;
|
||||||
|
},
|
||||||
|
get contentType() {
|
||||||
|
return headerValueFromName('content-type', this.headers);
|
||||||
|
},
|
||||||
|
setHeaders(headers) {
|
||||||
|
this.headers = headers;
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.headers = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const strictBlockBypasser = {
|
const strictBlockBypasser = {
|
||||||
|
|
Loading…
Reference in New Issue