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:
parent
df1c3a4a70
commit
2841e408dc
6 changed files with 83 additions and 93 deletions
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue