Skip to content

Commit 323b4ce

Browse files
committed
Add mpegdash-prune scriptlet
Related issue: uBlockOrigin/uAssets#34277
1 parent df8556a commit 323b4ce

2 files changed

Lines changed: 180 additions & 1 deletion

File tree

src/js/resources/scriptlets.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import './prevent-navigation.js';
3838
import './prevent-settimeout.js';
3939
import './replace-argument.js';
4040
import './spoof-css.js';
41+
import './vod.js';
4142

4243
import {
4344
collateFetchArgumentsFn,
@@ -48,7 +49,10 @@ import {
4849
onIdleFn,
4950
parsePropertiesToMatchFn,
5051
} from './utils.js';
51-
import { runAt, runAtHtmlElementFn } from './run-at.js';
52+
import {
53+
runAt,
54+
runAtHtmlElementFn,
55+
} from './run-at.js';
5256

5357
import { getAllCookiesFn } from './cookie.js';
5458
import { getAllLocalStorageFn } from './localstorage.js';

src/js/resources/vod.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*******************************************************************************
2+
3+
uBlock Origin - a comprehensive, efficient content blocker
4+
Copyright (C) 2026-present Raymond Hill
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see {http://www.gnu.org/licenses/}.
18+
19+
Home: https://github.com/gorhill/uBlock
20+
21+
*/
22+
23+
import { modifyXhrResponseFn } from './prevent-xhr.js';
24+
import { registerScriptlet } from './base.js';
25+
import { safeSelf } from './safe-self.js';
26+
27+
export function mpegdashPrune(
28+
selector = '',
29+
propsToMatch = ''
30+
) {
31+
if ( typeof selector !== 'string' ) { return; }
32+
if ( selector === '' ) { return; }
33+
const safe = safeSelf();
34+
const logPrefix = safe.makeLogPrefix('mpegdash-prune', selector, propsToMatch);
35+
const queryAll = (xmlDoc, selector) => {
36+
if ( selector.startsWith('xpath:') === false ) {
37+
return Array.from(xmlDoc.querySelectorAll(selector));
38+
}
39+
const xpr = xmlDoc.evaluate(
40+
selector.slice(6),
41+
xmlDoc,
42+
null,
43+
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
44+
null
45+
);
46+
const out = [];
47+
for ( let i = 0; i < xpr.snapshotLength; i++ ) {
48+
const node = xpr.snapshotItem(i);
49+
out.push(node);
50+
}
51+
return out;
52+
};
53+
const rePTparse = /^PT(\d+D)?(\d+H)?(\d+M)?([\d.]+S)?$/;
54+
const secondsPerDay = 24 * 60 * 60;
55+
const secondsPerHour = 60 * 60;
56+
const secondsPerMinute = 60;
57+
const secondsFromPT = pt => {
58+
const match = rePTparse.exec(pt);
59+
if ( match === null ) { return; }
60+
let seconds = 0;
61+
if ( match[1] ) {
62+
const d = parseFloat(match[1].slice(0, -1));
63+
if ( isNaN(d) ) { return; }
64+
seconds += d * secondsPerDay;
65+
}
66+
if ( match[2] ) {
67+
const h = parseFloat(match[2].slice(0, -1));
68+
if ( isNaN(h) ) { return; }
69+
seconds += h * secondsPerHour;
70+
}
71+
if ( match[3] ) {
72+
const m = parseFloat(match[3].slice(0, -1));
73+
if ( isNaN(m) ) { return; }
74+
seconds += m * secondsPerMinute;
75+
}
76+
if ( match[4] ) {
77+
const s = parseFloat(match[4].slice(0, -1));
78+
if ( isNaN(s) ) { return; }
79+
seconds += s;
80+
}
81+
return seconds;
82+
};
83+
const ptFromSeconds = seconds => {
84+
const parts = [ 'PT' ];
85+
const d = Math.floor(seconds / secondsPerDay);
86+
if ( d ) {
87+
parts.push(`${d}D`);
88+
seconds -= d * secondsPerDay;
89+
}
90+
const h = Math.floor(seconds / secondsPerHour);
91+
if ( h ) {
92+
parts.push(`${h}H`);
93+
seconds -= h * secondsPerHour;
94+
}
95+
const m = Math.floor(seconds / secondsPerMinute);
96+
if ( m ) {
97+
parts.push(`${m}M`);
98+
seconds -= m * secondsPerMinute;
99+
}
100+
parts.push(`${seconds}S`);
101+
return parts.join('');
102+
};
103+
const fixTimeAttributes = xmlDoc => {
104+
try {
105+
const periods = queryAll(xmlDoc, 'MPD > Period');
106+
if ( periods.length === 0 ) { return; }
107+
let seconds = 0;
108+
for ( const period of periods ) {
109+
const startAttrBefore = period.getAttribute('start');
110+
const durAttr = period.getAttribute('duration');
111+
if ( startAttrBefore === null || durAttr === null ) { continue; }
112+
const startAttrAfter = ptFromSeconds(seconds);
113+
period.setAttribute('start', startAttrAfter);
114+
if ( period.hasAttribute('id') ) {
115+
const idAttr = period.getAttribute('id');
116+
period.setAttribute('id', idAttr.replace(startAttrBefore, startAttrAfter));
117+
}
118+
seconds += secondsFromPT(durAttr);
119+
}
120+
const mpds = queryAll(xmlDoc, 'MPD[mediaPresentationDuration]');
121+
if ( mpds.length !== 1 ) { return; }
122+
mpds[0].setAttribute('mediaPresentationDuration', ptFromSeconds(seconds));
123+
} catch {
124+
}
125+
};
126+
const pruneFromDoc = xmlDoc => {
127+
try {
128+
if ( selector === '' ) {
129+
const serializer = new XMLSerializer();
130+
safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`);
131+
}
132+
const items = queryAll(xmlDoc, selector);
133+
if ( items.length === 0 ) { return xmlDoc; }
134+
safe.uboLog(logPrefix, `Patching ${items.length} items`);
135+
for ( const item of items ) {
136+
if ( item.nodeType !== 1 ) { continue; }
137+
item.setAttribute('duration', 'PT0S');
138+
}
139+
fixTimeAttributes(xmlDoc);
140+
} catch(ex) {
141+
safe.uboErr(logPrefix, `Error: ${ex}`);
142+
}
143+
return xmlDoc;
144+
};
145+
const pruneFromText = text => {
146+
if ( (/^\s*</.test(text) && />\s*$/.test(text)) === false ) {
147+
return text;
148+
}
149+
try {
150+
const xmlParser = new DOMParser();
151+
const xmlDoc = xmlParser.parseFromString(text, 'text/xml');
152+
pruneFromDoc(xmlDoc);
153+
const serializer = new XMLSerializer();
154+
text = serializer.serializeToString(xmlDoc);
155+
} catch {
156+
}
157+
return text;
158+
};
159+
modifyXhrResponseFn(propsToMatch, (xhr, before) => {
160+
if ( before instanceof XMLDocument ) {
161+
return pruneFromDoc(before);
162+
}
163+
if ( typeof before === 'string' ) {
164+
return pruneFromText(before);
165+
}
166+
return before;
167+
});
168+
}
169+
registerScriptlet(mpegdashPrune, {
170+
name: 'mpegdash-prune.js',
171+
dependencies: [
172+
modifyXhrResponseFn,
173+
safeSelf,
174+
],
175+
});

0 commit comments

Comments
 (0)