Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d3837ce
build: replace ExoPlayer 2.18.7 with media3 1.4.1 dependencies
Priveetee Jun 5, 2026
2957c0e
feat(player): vendor exo_icon_/exo_media_action_ drawables for media3-ui
Priveetee Jun 5, 2026
488da05
feat(player): rewrite the media session glue for media3
Priveetee Jun 5, 2026
586f031
feat(player): convert HttpDataSource subclasses to delegation for media3
Priveetee Jun 5, 2026
4151d3e
feat(player): swap exoplayer2 imports to media3 across the player stack
Priveetee Jun 5, 2026
b54c87f
chore(player): bump media3 to 1.10.1 (raises minSdk to 23) and migrat…
Priveetee Jun 6, 2026
a6f3ad4
fix(player): keep legacy subtitle decoding for sideloaded tracks (med…
Priveetee Jun 12, 2026
7cd19c6
feat(player): SABR PO token via headless WebView
Priveetee Jun 5, 2026
cf390d4
feat(player): SABR session store and format selection
Priveetee Jun 5, 2026
14649d3
feat(player): SABR pump and datasource (reader-driven)
Priveetee Jun 5, 2026
aff92e1
feat(player): wire SABR into the player, resolver and load control
Priveetee Jun 5, 2026
ba55d24
fix(sabr): cap cached sessions to 2 to stop the cross-video black screen
Priveetee Jun 5, 2026
151b70a
feat(sabr): per-segment data source for the chunk-based source (tier2)
Priveetee Jun 5, 2026
07128ac
feat(sabr): seekable chunk-based MediaSource core (tier2)
Priveetee Jun 5, 2026
738e317
feat(sabr): wire chunk MediaSource into the resolver (tier2 wip)
Priveetee Jun 5, 2026
830d2e5
feat(sabr): self-contained webm/mp4 chunks, playback works (tier2)
Priveetee Jun 5, 2026
89a7f5a
fix(sabr): track reader position so playback and seek keep feeding (t…
Priveetee Jun 5, 2026
8cdbfad
chore(sabr): drop tier2 debug logging
Priveetee Jun 5, 2026
3a4c596
feat(sabr): respect the user-selected video quality and force AAC audio
Priveetee Jun 6, 2026
79baa90
perf(sabr): persist the PO token on disk and harden the mint timeout/…
Priveetee Jun 6, 2026
7a1bc2f
fix(sabr): ignore ended tracks when reporting the buffered position
Priveetee Jun 6, 2026
ed507f2
fix(sabr): time segment stalls per-request so a throttled pump can't …
Priveetee Jun 6, 2026
b49343d
chore(sabr): remove the dead v1 byte-stream data source
Priveetee Jun 6, 2026
da8d15b
fix(sabr): adapt LoadControl and ChunkSampleStream to the media3 1.10…
Priveetee Jun 6, 2026
130e9ad
docs(sabr): clarify the AAC-over-Opus comment, separate the pump fals…
Priveetee Jun 6, 2026
aa423f6
fix(sabr): rebuild the session when the user changes video quality/codec
Priveetee Jun 6, 2026
0f0a7e6
fix(sabr): keep a 30s back-buffer so short backward seeks land in cache
Priveetee Jun 6, 2026
d5f63d8
fix(sabr): re-fetch evicted segments on a backward seek past the back…
Priveetee Jun 6, 2026
6c6bb11
fix(sabr): fall back to a decodable codec at the chosen resolution, n…
Priveetee Jun 6, 2026
a78daaa
fix(sabr): shrink the back-buffer when the cache is over budget so ev…
Priveetee Jun 6, 2026
f8ec0a0
fix(sabr): serve cold/forward seeks and rewind-after-end in the strea…
Priveetee Jun 11, 2026
04d7b58
fix(sabr): re-enable the video track on the live source when returnin…
Priveetee Jun 11, 2026
1eaf630
fix(player): recover from surface-released decoder failures instead o…
Priveetee Jun 11, 2026
ab44304
fix(sabr): play the original audio track instead of the auto-dub
Priveetee Jun 12, 2026
6a3b38a
fix(player): move top-positioned subtitles back to the bottom
Priveetee Jun 12, 2026
eef0f33
fix(detail): stop reloading related videos on every resume
Priveetee Jun 12, 2026
6104f30
feat(player): SABR multi-language audio track selector (show + switch)
Priveetee Jun 12, 2026
4e44e23
fix(player): keep the playback position when switching the SABR audio…
Priveetee Jun 12, 2026
5cead03
fix(sabr): pre-load init metadata on cold-restore to avoid an audio d…
Priveetee Jun 12, 2026
ae183ec
fix(player): guard NaN aspect ratio in ExpandableSurfaceView
Priveetee Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ android {

defaultConfig {
applicationId "InfinityLoop1309.NewPipeEnhanced"
minSdk 21
// media3 1.9+ requires API 23 (media3-session). Bumped from 21 -> drops Android 5.0/5.1.
minSdk 23
//noinspection ExpiredTargetSdkVersion
targetSdk 33
versionCode 1100
Expand Down Expand Up @@ -137,7 +138,7 @@ ext {
androidxRoomVersion = '2.4.2'
androidxWorkVersion = '2.10.2'

exoPlayerVersion = '2.18.7'
media3Version = '1.10.1'
googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.0'
markwonVersion = '4.6.2'
Expand Down Expand Up @@ -271,8 +272,14 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:3.12.13"

// Media player
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
implementation "androidx.media3:media3-exoplayer:${media3Version}"
implementation "androidx.media3:media3-exoplayer-dash:${media3Version}"
implementation "androidx.media3:media3-exoplayer-hls:${media3Version}"
implementation "androidx.media3:media3-exoplayer-smoothstreaming:${media3Version}"
implementation "androidx.media3:media3-datasource:${media3Version}"
implementation "androidx.media3:media3-ui:${media3Version}"
implementation "androidx.media3:media3-session:${media3Version}"
implementation "androidx.media3:media3-common:${media3Version}"

// Metadata generator for service descriptors
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
android:banner="@mipmap/tv_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:logo="@mipmap/ic_launcher"
android:theme="@style/OpeningTheme"
android:resizeableActivity="true"
Expand Down
301 changes: 301 additions & 0 deletions app/src/main/assets/sabr_potoken_poc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
/*
* SABR PO token POC — WebView pipeline (att mode).
*
* Ported from the local research mint (mint-po-token-browser.mjs), adapted to run inside an Android
* WebView instead of puppeteer. It must be injected AFTER https://www.youtube.com/ has finished
* loading (same-origin is required: att/get and GenerateIT are youtube.com endpoints, and the
* BotGuard interpreter is embedded in the challenge).
*
* Flow: read browser context -> att/get challenge -> run BotGuard VM -> snapshot -> GenerateIT
* integrity token -> mint a videoId-bound PO token -> hand the result back through the
* `SabrPocBridge` JavascriptInterface.
*
* INTERNAL / LOCAL POC ONLY. The minted PO token is session-bound; keep it out of any public log.
* API_KEY / REQUEST_KEY are the well-known public ecosystem constants, not secrets.
*/
(function () {
'use strict';

// YouTube ships a Trusted Types CSP (require-trusted-types-for 'script') that blocks
// new Function()/eval of the BotGuard interpreter. Installing an identity "default" policy makes
// Chromium route those sinks through it, restoring dynamic evaluation. If the CSP forbids
// creating the policy, loadBotGuard() will surface the eval error instead.
try {
if (window.trustedTypes && window.trustedTypes.createPolicy
&& !window.trustedTypes.defaultPolicy) {
window.trustedTypes.createPolicy('default', {
createHTML: function (value) { return value; },
createScript: function (value) { return value; },
createScriptURL: function (value) { return value; }
});
}
} catch (ttError) {
// ignore; surfaced later as an eval failure
}

var API_KEY = 'AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw';
var REQUEST_KEY = 'O43z0dpjhgX20SCx4KAo';

function report(result) {
try {
// eslint-disable-next-line no-undef
SabrPocBridge.onResult(JSON.stringify(result));
} catch (e) {
// Bridge not present (e.g. plain browser run): fall back to console.
try {
console.log('[sabr-poc] ' + JSON.stringify(result));
} catch (ignored) {
// nothing else we can do
}
}
}

function step(message) {
try { console.log('[sabr-poc] ' + message); } catch (e) { /* ignore */ }
}

function readVisitorData() {
var cfg = window.ytcfg;
var fromCfg = cfg && typeof cfg.get === 'function' ? cfg.get('VISITOR_DATA') : null;
if (fromCfg) {
return fromCfg;
}
var html = document.documentElement.innerHTML;
var marker = '"VISITOR_DATA":"';
var start = html.indexOf(marker);
if (start < 0) {
throw new Error('Could not find visitor data');
}
var from = start + marker.length;
var end = html.indexOf('"', from);
if (end < 0) {
throw new Error('Could not find visitor data end');
}
return html.slice(from, end);
}

function readClientVersion() {
var cfg = window.ytcfg;
var fromCfg = cfg && typeof cfg.get === 'function'
? cfg.get('INNERTUBE_CLIENT_VERSION') : null;
return fromCfg || '2.20260114.01.00';
}

function normalizeTrustedUrl(value) {
if (!value) {
throw new Error('Missing interpreter url');
}
return value.indexOf('//') === 0 ? 'https:' + value : value;
}

function fetchChallenge(ctx) {
var context = {
client: {
clientName: 'WEB',
clientVersion: ctx.clientVersion,
hl: 'en',
gl: 'US',
utcOffsetMinutes: 0,
visitorData: ctx.visitorData
}
};
return fetch('https://www.youtube.com/youtubei/v1/att/get?prettyPrint=false&alt=json', {
method: 'POST',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json',
'X-Goog-Visitor-Id': ctx.visitorData,
'X-Youtube-Client-Version': ctx.clientVersion,
'X-Youtube-Client-Name': '1'
},
body: JSON.stringify({
engagementType: 'ENGAGEMENT_TYPE_UNBOUND',
context: context
})
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok || !data.bgChallenge) {
throw new Error('att/get failed status=' + response.status);
}
return data.bgChallenge;
});
});
}

function resolveInterpreter(challenge, userAgent) {
var embedded = challenge.interpreterJavascript
&& challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (embedded) {
return Promise.resolve(embedded);
}
var url = normalizeTrustedUrl(
(challenge.interpreterJavascript
&& challenge.interpreterJavascript
.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue)
|| (challenge.interpreterUrl
&& challenge.interpreterUrl
.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue));
return fetch(url, { headers: { 'User-Agent': userAgent } }).then(function (response) {
return response.text().then(function (js) {
if (!response.ok || !js) {
throw new Error('interpreter fetch failed status=' + response.status);
}
return js;
});
});
}

function loadBotGuard(interpreterJavascript, program, globalName) {
return new Promise(function (resolve, reject) {
try {
new Function(interpreterJavascript)();
} catch (e) {
reject(new Error('interpreter eval failed: ' + e.message));
return;
}
var vm = window[globalName];
if (!vm || typeof vm.a !== 'function') {
reject(new Error('BotGuard VM missing init function'));
return;
}
var timeout = setTimeout(function () {
reject(new Error('BotGuard init timeout'));
}, 10000);
try {
vm.a(program, function (asyncSnapshotFunction) {
clearTimeout(timeout);
resolve({ asyncSnapshotFunction: asyncSnapshotFunction });
}, true, undefined, function () { }, [[], []]);
} catch (e) {
clearTimeout(timeout);
reject(new Error('BotGuard init threw: ' + e.message));
}
});
}

function snapshot(functions, webPoSignalOutput) {
return new Promise(function (resolve, reject) {
var timeout = setTimeout(function () {
reject(new Error('BotGuard snapshot timeout'));
}, 10000);
functions.asyncSnapshotFunction(function (response) {
clearTimeout(timeout);
resolve(response);
}, [undefined, undefined, webPoSignalOutput, undefined]);
});
}

function fetchIntegrityToken(botGuardResponse, userAgent) {
return fetch('https://www.youtube.com/api/jnn/v1/GenerateIT', {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1',
'User-Agent': userAgent
},
body: JSON.stringify([REQUEST_KEY, botGuardResponse])
}).then(function (response) {
return response.json().then(function (data) {
var integrityToken = data[0];
if (typeof integrityToken !== 'string') {
throw new Error('GenerateIT failed status=' + response.status);
}
return integrityToken;
});
});
}

function base64ToU8(value) {
var normalized = value.replace(/-/g, '+').replace(/_/g, '/');
var padded = normalized + '==='.slice((normalized.length + 3) % 4);
var binary = atob(padded);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}

function u8ToBase64Url(value) {
var binary = '';
for (var i = 0; i < value.length; i++) {
binary += String.fromCharCode(value[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function mint(webPoSignalOutput, integrityToken, identifier) {
var getMinter = webPoSignalOutput[0];
if (typeof getMinter !== 'function') {
return Promise.reject(new Error('Missing PO minter factory'));
}
return Promise.resolve(getMinter(base64ToU8(integrityToken))).then(function (mintCallback) {
if (typeof mintCallback !== 'function') {
throw new Error('Missing PO mint callback');
}
return Promise.resolve(mintCallback(new TextEncoder().encode(identifier)))
.then(u8ToBase64Url);
});
}

function run() {
step('run start, readyState=' + document.readyState + ' origin=' + location.origin);
var videoId = window.__SABR_POC_VIDEO_ID || 'aqz-KE-bpKQ';
var ctx = {
visitorData: readVisitorData(),
clientVersion: readClientVersion(),
userAgent: navigator.userAgent
};
step('context ok visitorLen=' + ctx.visitorData.length + ' clientVersion=' + ctx.clientVersion);
var webPoSignalOutput = [];
var integrityTokenLength = -1;
step('fetching att/get challenge...');
return fetchChallenge(ctx).then(function (challenge) {
step('challenge ok embedded='
+ !!(challenge.interpreterJavascript
&& challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue));
return resolveInterpreter(challenge, ctx.userAgent).then(function (interpreterJs) {
step('interpreter resolved len=' + (interpreterJs ? interpreterJs.length : -1));
return loadBotGuard(interpreterJs, challenge.program, challenge.globalName);
});
}).then(function (functions) {
step('botguard loaded, taking snapshot...');
return snapshot(functions, webPoSignalOutput);
}).then(function (botGuardResponse) {
step('snapshot ok, calling GenerateIT...');
return fetchIntegrityToken(botGuardResponse, ctx.userAgent);
}).then(function (integrityToken) {
step('integrity token len=' + integrityToken.length + ', minting...');
integrityTokenLength = integrityToken.length;
return mint(webPoSignalOutput, integrityToken, videoId);
}).then(function (poToken) {
report({
ok: true,
videoId: videoId,
clientVersion: ctx.clientVersion,
visitorDataLength: ctx.visitorData.length,
integrityTokenLength: integrityTokenLength,
poTokenLength: poToken.length,
poToken: poToken,
userAgent: ctx.userAgent
});
});
}

function reportError(e) {
report({
ok: false,
error: (e && e.message) ? e.message : String(e),
errorName: e && e.name ? e.name : '',
stack: e && e.stack ? String(e.stack).slice(0, 400) : '',
userAgent: navigator.userAgent
});
}

try {
run().catch(reportError);
} catch (e) {
reportError(e);
}
})();
2 changes: 1 addition & 1 deletion app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.schabi.newpipe.error

import android.os.Parcelable
import androidx.annotation.StringRes
import com.google.android.exoplayer2.ExoPlaybackException
import androidx.media3.exoplayer.ExoPlaybackException
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;

import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.tabs.TabLayout;
Expand Down Expand Up @@ -396,11 +396,12 @@ public void onResume() {

setupBrightness();

if (currentInfo != null) {
if (tabSettingsChanged) {
tabSettingsChanged = false;
initTabs();
}
// Only rebuild the tabs when the tab settings actually changed. Doing it on every resume
// recreated the related-items (and description) fragments each time, so returning from the
// share sheet / Home reloaded the related videos. A new video goes through handleResult().
if (currentInfo != null && tabSettingsChanged) {
tabSettingsChanged = false;
initTabs();
updateTabs(currentInfo);
}

Expand Down
Loading