feat(Spoof video streams): Add Android Reel client to fix playback issues

Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
This commit is contained in:
oSumAtrIX 2026-03-16 23:35:39 +01:00
parent df1c3a4a70
commit 2841e408dc
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
6 changed files with 83 additions and 93 deletions

View file

@ -1,7 +1,7 @@
package app.revanced.extension.music.patches.spoof;
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
@ -19,7 +19,7 @@ public class SpoofVideoStreamsPatch {
public static void setClientOrderToUse() {
List<ClientType> availableClients = List.of(
ANDROID_VR_1_43_32,
ANDROID_NO_SDK,
ANDROID_REEL,
VISIONOS,
ANDROID_VR_1_61_48
);

View file

@ -9,9 +9,35 @@ import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@SuppressWarnings("ConstantLocale")
public enum ClientType {
/**
* Video not playable: Paid, Movie, Private, Age-restricted.
* Uses non-adaptive bitrate.
* AV1 codec available.
*/
ANDROID_REEL(
3,
"ANDROID",
"com.google.android.youtube",
Build.MANUFACTURER,
Build.MODEL,
"Android",
Build.VERSION.RELEASE,
String.valueOf(Build.VERSION.SDK_INT),
Build.ID,
"20.26.46",
// This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay.
// This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from Morphe.
// This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
// For this reason, ANDROID_REEL is used as a logout client.
false,
true,
false,
"Android Reel"
),
/**
* Video not playable: Kids / Paid / Movie / Private / Age-restricted.
* This client can only be used when logged out.
@ -28,10 +54,10 @@ public enum ClientType {
// Android 12.1
"32",
"SQ3A.220605.009.A1",
"132.0.6808.3",
"1.61.48",
false,
false,
true,
"Android VR 1.61"
),
/**
@ -48,39 +74,12 @@ public enum ClientType {
ANDROID_VR_1_61_48.osVersion,
Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
"107.0.5284.2",
"1.43.32",
ANDROID_VR_1_61_48.useAuth,
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
ANDROID_VR_1_61_48.usePlayerEndpoint,
"Android VR 1.43"
),
/**
* Video not playable: Paid / Movie / Private / Age-restricted.
* Note: The 'Authorization' key must be excluded from the header.
*
* According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing,
* the GVS did not return a valid response:
* [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550).
*
* According to the latest commit in yt-dlp, the GVS returns a valid response
* even if the 'androidSdkVersion' field is missing:
* [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693).
*
* For some reason, PoToken is not required.
*/
ANDROID_NO_SDK(
3,
"ANDROID",
"",
"",
"",
Build.VERSION.RELEASE,
"20.05.46",
"com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip",
false,
true,
"Android No SDK"
),
/**
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
@ -95,10 +94,10 @@ public enum ClientType {
"15",
"35",
"AP3A.241005.015.A2",
"132.0.6779.0",
"23.47.101",
true,
false,
true,
"Android Studio"
),
/**
@ -114,32 +113,8 @@ public enum ClientType {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
false,
false,
"visionOS"
),
/**
* The device machine id for the iPad 6th Gen (iPad7,6).
* AV1 hardware decoding is not supported.
* See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information.
*
* Based on Google's actions to date, PoToken may not be required on devices with very low specs.
* For example, suppose the User-Agent for a PlayStation 3 (with 256MB of RAM) is used.
* Accessing 'Web' (https://www.youtube.com) will redirect to 'TV' (https://www.youtube.com/tv).
* 'TV' target devices with very low specs, such as embedded devices, game consoles, and blu-ray players, so PoToken is not required.
*
* For this reason, the device machine id for the iPad 6th Gen (with 2GB of RAM),
* the lowest spec device capable of running iPadOS 17, was used.
*/
IPADOS(5,
"IOS",
"Apple",
"iPad7,6",
"iPadOS",
"17.7.10.21H450",
"19.22.3",
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
false,
true,
"iPadOS"
"visionOS"
);
/**
@ -195,13 +170,6 @@ public enum ClientType {
@Nullable
private final String buildId;
/**
* Cronet release version, as found in decompiled client apk.
* Field is null if not applicable.
*/
@Nullable
private final String cronetVersion;
/**
* App version.
*/
@ -217,6 +185,11 @@ public enum ClientType {
*/
public final boolean supportsMultiAudioTracks;
/**
* If the client should use the player endpoint for stream extraction.
*/
public final boolean usePlayerEndpoint;
/**
* Friendly name displayed in stats for nerds.
*/
@ -234,10 +207,10 @@ public enum ClientType {
String osVersion,
@NonNull String androidSdkVersion,
@NonNull String buildId,
@NonNull String cronetVersion,
String clientVersion,
boolean useAuth,
boolean supportsMultiAudioTracks,
boolean usePlayerEndpoint,
String friendlyName) {
this.id = id;
this.clientName = clientName;
@ -248,21 +221,20 @@ public enum ClientType {
this.osVersion = osVersion;
this.androidSdkVersion = androidSdkVersion;
this.buildId = buildId;
this.cronetVersion = cronetVersion;
this.clientVersion = clientVersion;
this.useAuth = useAuth;
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
this.usePlayerEndpoint = usePlayerEndpoint;
this.friendlyName = friendlyName;
Locale defaultLocale = Locale.getDefault();
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s)",
packageName,
clientVersion,
osVersion,
defaultLocale,
deviceModel,
Objects.requireNonNull(buildId),
Objects.requireNonNull(cronetVersion)
buildId
);
Logger.printDebug(() -> "userAgent: " + this.userAgent);
}
@ -278,6 +250,7 @@ public enum ClientType {
String userAgent,
boolean useAuth,
boolean supportsMultiAudioTracks,
boolean usePlayerEndpoint,
String friendlyName) {
this.id = id;
this.clientName = clientName;
@ -289,10 +262,10 @@ public enum ClientType {
this.userAgent = userAgent;
this.useAuth = useAuth;
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
this.usePlayerEndpoint = usePlayerEndpoint;
this.friendlyName = friendlyName;
this.packageName = null;
this.androidSdkVersion = null;
this.buildId = null;
this.cronetVersion = null;
}
}

View file

@ -15,13 +15,20 @@ import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
final class PlayerRoutes {
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
static final Route.CompiledRoute GET_PLAYER_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile();
static final Route.CompiledRoute GET_REEL_STREAMING_DATA = new Route(
Route.Method.POST,
"reel/reel_item_watch" +
"?fields=playerResponse.playabilityStatus,playerResponse.streamingData" +
"&alt=proto"
).compile();
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
/**
@ -47,6 +54,7 @@ final class PlayerRoutes {
Locale streamLocale = language.getLocale();
JSONObject client = new JSONObject();
client.put("deviceMake", clientType.deviceMake);
client.put("deviceModel", clientType.deviceModel);
client.put("clientName", clientType.clientName);
@ -61,9 +69,19 @@ final class PlayerRoutes {
context.put("client", client);
innerTubeBody.put("context", context);
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
if (clientType.usePlayerEndpoint) {
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
} else {
JSONObject playerRequest = new JSONObject();
playerRequest.put("contentCheckOk", true);
playerRequest.put("racyCheckOk", true);
playerRequest.put("videoId", videoId);
innerTubeBody.put("playerRequest", playerRequest);
innerTubeBody.put("disablePlayerResponse", false);
}
} catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e);
}

View file

@ -1,7 +1,8 @@
package app.revanced.extension.shared.spoof.requests;
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_PLAYER_STREAMING_DATA;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_REEL_STREAMING_DATA;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -27,6 +28,7 @@ import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType;
@ -146,7 +148,10 @@ public class StreamingDataRequest {
final long startTime = System.currentTimeMillis();
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
Route.CompiledRoute route = clientType.usePlayerEndpoint ?
GET_PLAYER_STREAMING_DATA : GET_REEL_STREAMING_DATA;
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(route, clientType);
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);

View file

@ -1,10 +1,9 @@
package app.revanced.extension.youtube.patches.spoof;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_CREATOR;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
import static app.revanced.extension.shared.spoof.ClientType.IPADOS;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL;
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
import java.util.List;
@ -44,11 +43,11 @@ public class SpoofVideoStreamsPatch {
}
List<ClientType> availableClients = List.of(
VISIONOS,
ANDROID_CREATOR,
ANDROID_VR_1_43_32,
ANDROID_NO_SDK,
IPADOS);
ANDROID_REEL,
VISIONOS,
ANDROID_CREATOR
);
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
availableClients, client);

View file

@ -88,27 +88,22 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1")
+ '\n' + str("revanced_spoof_video_streams_about_no_force_original_audio");
case ANDROID_REEL ->
summary = str("revanced_spoof_video_streams_about_playback_failure");
// VR 1.61 is not exposed in the UI and should never be reached here.
case ANDROID_VR_1_43_32, ANDROID_VR_1_61_48 ->
summary = str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume");
case ANDROID_NO_SDK ->
summary = str("revanced_spoof_video_streams_about_playback_failure");
case IPADOS ->
summary = str("revanced_spoof_video_streams_about_playback_failure")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
case VISIONOS ->
case VISIONOS ->
summary = str("revanced_spoof_video_streams_about_experimental")
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
default -> Logger.printException(() -> "Unknown client: " + clientType);
}
// Only iPadOS can play children videos in incognito, but it commonly fails at 1 minute
// or doesn't start playback at all. List the side effect for other clients
// since they will fall over to iPadOS.
if (clientType != ClientType.IPADOS && clientType != ClientType.ANDROID_NO_SDK) {
summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos");
// Only Android Reel and Android VR supports 360° VR immersive mode.
if (!clientType.name().startsWith("ANDROID_VR") && clientType != ClientType.ANDROID_REEL) {
summary += '\n' + str("revanced_spoof_video_streams_about_no_immersive_mode");
}
// Use better formatting for bullet points.