Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
57555ec
feat(player): SABR PO token via headless WebView
Priveetee Jun 5, 2026
f06bdce
feat(player): SABR session store and format selection
Priveetee Jun 5, 2026
aa9d5c8
feat(player): SABR pump and datasource (reader-driven)
Priveetee Jun 5, 2026
fb1a2c3
feat(player): wire SABR into the player, resolver and load control
Priveetee Jun 5, 2026
0c2a3ed
fix(sabr): cap cached sessions to 2 to stop the cross-video black screen
Priveetee Jun 5, 2026
2a99b2d
feat(sabr): per-segment data source for the chunk-based source (tier2)
Priveetee Jun 5, 2026
6b43898
feat(sabr): seekable chunk-based MediaSource core (tier2)
Priveetee Jun 5, 2026
c4870b9
feat(sabr): wire chunk MediaSource into the resolver (tier2 wip)
Priveetee Jun 5, 2026
8ae69db
feat(sabr): self-contained webm/mp4 chunks, playback works (tier2)
Priveetee Jun 5, 2026
fb6007a
fix(sabr): track reader position so playback and seek keep feeding (t…
Priveetee Jun 5, 2026
ffe6123
chore(sabr): drop tier2 debug logging
Priveetee Jun 5, 2026
a7e9c77
feat(sabr): respect the user-selected video quality and force AAC audio
Priveetee Jun 6, 2026
8b94111
perf(sabr): persist the PO token on disk and harden the mint timeout/…
Priveetee Jun 6, 2026
5a7df94
fix(sabr): ignore ended tracks when reporting the buffered position
Priveetee Jun 6, 2026
06926b2
fix(sabr): time segment stalls per-request so a throttled pump can't …
Priveetee Jun 6, 2026
9d9f5c9
chore(sabr): remove the dead v1 byte-stream data source
Priveetee Jun 6, 2026
8e63765
fix(sabr): adapt LoadControl and ChunkSampleStream to the media3 1.10…
Priveetee Jun 6, 2026
7672d42
docs(sabr): clarify the AAC-over-Opus comment, separate the pump fals…
Priveetee Jun 6, 2026
17f6c51
fix(sabr): rebuild the session when the user changes video quality/codec
Priveetee Jun 6, 2026
b8bcb0c
fix(sabr): keep a 30s back-buffer so short backward seeks land in cache
Priveetee Jun 6, 2026
b8234b7
fix(sabr): re-fetch evicted segments on a backward seek past the back…
Priveetee Jun 6, 2026
77229b8
fix(sabr): fall back to a decodable codec at the chosen resolution, n…
Priveetee Jun 6, 2026
d58473a
fix(sabr): shrink the back-buffer when the cache is over budget so ev…
Priveetee Jun 6, 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackException;
import androidx.media3.common.C;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.common.PlaybackException;

import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
Expand All @@ -29,9 +29,9 @@
import java.util.Map;
import java.util.function.Supplier;

import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
import static androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
import static androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED;
import static androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED;

/**
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
Expand Down
Loading