feat(Spoof video streams): Add Android Reel client to fix playback issues (#6830)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de> Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
This commit is contained in:
parent
14ea61355d
commit
4b6c3e3123
19 changed files with 319 additions and 173 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
package app.revanced.extension.music.patches.spoof;
|
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.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_43_32;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||||
|
|
@ -18,8 +18,8 @@ public class SpoofVideoStreamsPatch {
|
||||||
*/
|
*/
|
||||||
public static void setClientOrderToUse() {
|
public static void setClientOrderToUse() {
|
||||||
List<ClientType> availableClients = List.of(
|
List<ClientType> availableClients = List.of(
|
||||||
|
ANDROID_REEL,
|
||||||
ANDROID_VR_1_43_32,
|
ANDROID_VR_1_43_32,
|
||||||
ANDROID_NO_SDK,
|
|
||||||
VISIONOS,
|
VISIONOS,
|
||||||
ANDROID_VR_1_61_48
|
ANDROID_VR_1_61_48
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public class Settings extends YouTubeAndMusicSettings {
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
||||||
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
|
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,6 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
compileOnly(libs.okhttp)
|
compileOnly(libs.okhttp)
|
||||||
|
compileOnly(libs.protobuf.javalite)
|
||||||
|
implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class BaseSettings {
|
||||||
//
|
//
|
||||||
|
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_video_streams_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
||||||
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,34 @@ import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantLocale")
|
@SuppressWarnings("ConstantLocale")
|
||||||
public enum ClientType {
|
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.44.38",
|
||||||
|
// 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 ReVanced.
|
||||||
|
// This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
"Android Reel"
|
||||||
|
),
|
||||||
/**
|
/**
|
||||||
* Video not playable: Kids / Paid / Movie / Private / Age-restricted.
|
* Video not playable: Kids / Paid / Movie / Private / Age-restricted.
|
||||||
* This client can only be used when logged out.
|
* This client can only be used when logged out.
|
||||||
|
|
@ -28,10 +53,10 @@ public enum ClientType {
|
||||||
// Android 12.1
|
// Android 12.1
|
||||||
"32",
|
"32",
|
||||||
"SQ3A.220605.009.A1",
|
"SQ3A.220605.009.A1",
|
||||||
"132.0.6808.3",
|
|
||||||
"1.61.48",
|
"1.61.48",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
"Android VR 1.61"
|
"Android VR 1.61"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,39 +73,12 @@ public enum ClientType {
|
||||||
ANDROID_VR_1_61_48.osVersion,
|
ANDROID_VR_1_61_48.osVersion,
|
||||||
Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
|
Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
|
||||||
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
|
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
|
||||||
"107.0.5284.2",
|
|
||||||
"1.43.32",
|
"1.43.32",
|
||||||
ANDROID_VR_1_61_48.useAuth,
|
ANDROID_VR_1_61_48.useAuth,
|
||||||
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
|
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
|
||||||
|
ANDROID_VR_1_61_48.usePlayerEndpoint,
|
||||||
"Android VR 1.43"
|
"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".
|
* 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>
|
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
|
||||||
|
|
@ -95,10 +93,10 @@ public enum ClientType {
|
||||||
"15",
|
"15",
|
||||||
"35",
|
"35",
|
||||||
"AP3A.241005.015.A2",
|
"AP3A.241005.015.A2",
|
||||||
"132.0.6779.0",
|
|
||||||
"23.47.101",
|
"23.47.101",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
"Android Studio"
|
"Android Studio"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
|
|
@ -114,32 +112,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",
|
"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,
|
||||||
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,
|
true,
|
||||||
"iPadOS"
|
"visionOS"
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -195,13 +169,6 @@ public enum ClientType {
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String buildId;
|
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.
|
* App version.
|
||||||
*/
|
*/
|
||||||
|
|
@ -217,6 +184,11 @@ public enum ClientType {
|
||||||
*/
|
*/
|
||||||
public final boolean supportsMultiAudioTracks;
|
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.
|
* Friendly name displayed in stats for nerds.
|
||||||
*/
|
*/
|
||||||
|
|
@ -234,10 +206,10 @@ public enum ClientType {
|
||||||
String osVersion,
|
String osVersion,
|
||||||
@NonNull String androidSdkVersion,
|
@NonNull String androidSdkVersion,
|
||||||
@NonNull String buildId,
|
@NonNull String buildId,
|
||||||
@NonNull String cronetVersion,
|
|
||||||
String clientVersion,
|
String clientVersion,
|
||||||
boolean useAuth,
|
boolean useAuth,
|
||||||
boolean supportsMultiAudioTracks,
|
boolean supportsMultiAudioTracks,
|
||||||
|
boolean usePlayerEndpoint,
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
|
|
@ -248,21 +220,20 @@ public enum ClientType {
|
||||||
this.osVersion = osVersion;
|
this.osVersion = osVersion;
|
||||||
this.androidSdkVersion = androidSdkVersion;
|
this.androidSdkVersion = androidSdkVersion;
|
||||||
this.buildId = buildId;
|
this.buildId = buildId;
|
||||||
this.cronetVersion = cronetVersion;
|
|
||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.useAuth = useAuth;
|
this.useAuth = useAuth;
|
||||||
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
||||||
|
this.usePlayerEndpoint = usePlayerEndpoint;
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
|
|
||||||
Locale defaultLocale = Locale.getDefault();
|
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,
|
packageName,
|
||||||
clientVersion,
|
clientVersion,
|
||||||
osVersion,
|
osVersion,
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
deviceModel,
|
deviceModel,
|
||||||
Objects.requireNonNull(buildId),
|
buildId
|
||||||
Objects.requireNonNull(cronetVersion)
|
|
||||||
);
|
);
|
||||||
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
Logger.printDebug(() -> "userAgent: " + this.userAgent);
|
||||||
}
|
}
|
||||||
|
|
@ -278,6 +249,7 @@ public enum ClientType {
|
||||||
String userAgent,
|
String userAgent,
|
||||||
boolean useAuth,
|
boolean useAuth,
|
||||||
boolean supportsMultiAudioTracks,
|
boolean supportsMultiAudioTracks,
|
||||||
|
boolean usePlayerEndpoint,
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
|
|
@ -289,10 +261,10 @@ public enum ClientType {
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.useAuth = useAuth;
|
this.useAuth = useAuth;
|
||||||
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
||||||
|
this.usePlayerEndpoint = usePlayerEndpoint;
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
this.packageName = null;
|
this.packageName = null;
|
||||||
this.androidSdkVersion = null;
|
this.androidSdkVersion = null;
|
||||||
this.buildId = null;
|
this.buildId = null;
|
||||||
this.cronetVersion = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -39,7 +38,7 @@ public class SpoofVideoStreamsPatch {
|
||||||
@Nullable
|
@Nullable
|
||||||
private static volatile AppLanguage languageOverride;
|
private static volatile AppLanguage languageOverride;
|
||||||
|
|
||||||
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_43_32;
|
private static volatile ClientType preferredClient = ClientType.ANDROID_REEL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If this patch was included during patching.
|
* @return If this patch was included during patching.
|
||||||
|
|
@ -250,7 +249,7 @@ public class SpoofVideoStreamsPatch {
|
||||||
* Called after {@link #fetchStreams(String, Map)}.
|
* Called after {@link #fetchStreams(String, Map)}.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static ByteBuffer getStreamingData(String videoId) {
|
public static byte[] getStreamingData(String videoId) {
|
||||||
if (SPOOF_STREAMING_DATA) {
|
if (SPOOF_STREAMING_DATA) {
|
||||||
try {
|
try {
|
||||||
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,20 @@ import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
|
|
||||||
final class PlayerRoutes {
|
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,
|
Route.Method.POST,
|
||||||
"player" +
|
"player" +
|
||||||
"?fields=streamingData" +
|
"?fields=streamingData" +
|
||||||
"&alt=proto"
|
"&alt=proto"
|
||||||
).compile();
|
).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/";
|
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,6 +54,7 @@ final class PlayerRoutes {
|
||||||
Locale streamLocale = language.getLocale();
|
Locale streamLocale = language.getLocale();
|
||||||
|
|
||||||
JSONObject client = new JSONObject();
|
JSONObject client = new JSONObject();
|
||||||
|
|
||||||
client.put("deviceMake", clientType.deviceMake);
|
client.put("deviceMake", clientType.deviceMake);
|
||||||
client.put("deviceModel", clientType.deviceModel);
|
client.put("deviceModel", clientType.deviceModel);
|
||||||
client.put("clientName", clientType.clientName);
|
client.put("clientName", clientType.clientName);
|
||||||
|
|
@ -61,9 +69,19 @@ final class PlayerRoutes {
|
||||||
context.put("client", client);
|
context.put("client", client);
|
||||||
|
|
||||||
innerTubeBody.put("context", context);
|
innerTubeBody.put("context", context);
|
||||||
|
|
||||||
|
if (clientType.usePlayerEndpoint) {
|
||||||
innerTubeBody.put("contentCheckOk", true);
|
innerTubeBody.put("contentCheckOk", true);
|
||||||
innerTubeBody.put("racyCheckOk", true);
|
innerTubeBody.put("racyCheckOk", true);
|
||||||
innerTubeBody.put("videoId", videoId);
|
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) {
|
} catch (JSONException e) {
|
||||||
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
package app.revanced.extension.shared.spoof.requests;
|
package app.revanced.extension.shared.spoof.requests;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
|
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.Utils.isNotEmpty;
|
||||||
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
@ -27,6 +26,11 @@ import java.util.concurrent.TimeoutException;
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
import app.revanced.extension.shared.ByteTrieSearch;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass;
|
||||||
|
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.PlayerResponse;
|
||||||
|
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.StreamingData;
|
||||||
|
import app.revanced.extension.shared.innertube.ReelItemWatchResponseOuterClass.ReelItemWatchResponse;
|
||||||
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
|
|
@ -111,7 +115,7 @@ public class StreamingDataRequest {
|
||||||
|
|
||||||
private final String videoId;
|
private final String videoId;
|
||||||
|
|
||||||
private final Future<ByteBuffer> future;
|
private final Future<byte[]> future;
|
||||||
|
|
||||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||||
Objects.requireNonNull(playerHeaders);
|
Objects.requireNonNull(playerHeaders);
|
||||||
|
|
@ -134,6 +138,12 @@ public class StreamingDataRequest {
|
||||||
Logger.printInfo(() -> toastMessage, ex);
|
Logger.printInfo(() -> toastMessage, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void handleDebugToast(String toastMessage, ClientType clientType) {
|
||||||
|
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
|
||||||
|
Utils.showToastShort(String.format(toastMessage, clientType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static HttpURLConnection send(ClientType clientType,
|
private static HttpURLConnection send(ClientType clientType,
|
||||||
String videoId,
|
String videoId,
|
||||||
|
|
@ -146,7 +156,10 @@ public class StreamingDataRequest {
|
||||||
final long startTime = System.currentTimeMillis();
|
final long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
try {
|
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.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
|
|
||||||
|
|
@ -203,7 +216,7 @@ public class StreamingDataRequest {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
|
private static byte[] fetch(String videoId, Map<String, String> playerHeaders) {
|
||||||
final boolean debugEnabled = BaseSettings.DEBUG.get();
|
final boolean debugEnabled = BaseSettings.DEBUG.get();
|
||||||
|
|
||||||
// Retry with different client if empty response body is received.
|
// Retry with different client if empty response body is received.
|
||||||
|
|
@ -214,33 +227,11 @@ public class StreamingDataRequest {
|
||||||
|
|
||||||
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
try {
|
byte[] playerResponseBuffer = buildPlayerResponseBuffer(clientType, connection);
|
||||||
// gzip encoding doesn't response with content length (-1),
|
if (playerResponseBuffer != null) {
|
||||||
// but empty response body does.
|
|
||||||
if (connection.getContentLength() == 0) {
|
|
||||||
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
|
|
||||||
Utils.showToastShort("Debug: Ignoring empty spoof stream client " + clientType);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
|
||||||
|
|
||||||
byte[] buffer = new byte[2048];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
|
||||||
baos.write(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
if (clientType == ClientType.ANDROID_CREATOR && liveStreamBufferSearch.matches(buffer)) {
|
|
||||||
Logger.printDebug(() -> "Skipping Android Studio as video is a livestream: " + videoId);
|
|
||||||
} else {
|
|
||||||
lastSpoofedClientType = clientType;
|
lastSpoofedClientType = clientType;
|
||||||
|
|
||||||
return ByteBuffer.wrap(baos.toByteArray());
|
return playerResponseBuffer;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Logger.printException(() -> "Fetch failed while processing response data", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,12 +241,61 @@ public class StreamingDataRequest {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static byte[] buildPlayerResponseBuffer(ClientType clientType,
|
||||||
|
HttpURLConnection connection) {
|
||||||
|
// gzip encoding doesn't response with content length (-1),
|
||||||
|
// but empty response body does.
|
||||||
|
if (connection.getContentLength() == 0) {
|
||||||
|
handleDebugToast("Debug: Ignoring empty spoof stream client (%s)", clientType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream inputStream = connection.getInputStream()) {
|
||||||
|
PlayerResponse playerResponse = clientType.usePlayerEndpoint
|
||||||
|
? PlayerResponse.parseFrom(inputStream)
|
||||||
|
: ReelItemWatchResponse.parseFrom(inputStream).getPlayerResponse();
|
||||||
|
|
||||||
|
var playabilityStatus = playerResponse.getPlayabilityStatus();
|
||||||
|
if (playabilityStatus.getStatus() != PlayerResponseOuterClass.Status.OK) {
|
||||||
|
handleDebugToast("Debug: Ignoring unplayable video (%s)", clientType);
|
||||||
|
String reason = playabilityStatus.getReason();
|
||||||
|
if (isNotEmpty(reason)) {
|
||||||
|
Logger.printDebug(() -> String.format("Debug: Ignoring unplayable video (%s), reason: %s", clientType, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerResponse.Builder responseBuilder = playerResponse.toBuilder();
|
||||||
|
if (!playerResponse.hasStreamingData()) {
|
||||||
|
handleDebugToast("Debug: Ignoring empty streaming data (%s)", clientType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android Studio only supports the HLS protocol for live streams.
|
||||||
|
// HLS protocol can theoretically be played with ExoPlayer,
|
||||||
|
// but the related code has not yet been implemented.
|
||||||
|
// If DASH protocol is not available, the client will be skipped.
|
||||||
|
StreamingData streamingData = playerResponse.getStreamingData();
|
||||||
|
if (streamingData.getAdaptiveFormatsCount() == 0) {
|
||||||
|
handleDebugToast("Debug: Ignoring empty adaptiveFormat (%s)", clientType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseBuilder.build().toByteArray();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.printException(() -> "Failed to write player response to buffer array", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean fetchCompleted() {
|
public boolean fetchCompleted() {
|
||||||
return future.isDone();
|
return future.isDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public ByteBuffer getStream() {
|
public byte[] getStream() {
|
||||||
try {
|
try {
|
||||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||||
} catch (TimeoutException ex) {
|
} catch (TimeoutException ex) {
|
||||||
|
|
|
||||||
55
extensions/shared/protobuf/build.gradle.kts
Normal file
55
extensions/shared/protobuf/build.gradle.kts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
alias(libs.plugins.protobuf)
|
||||||
|
alias(libs.plugins.shadow)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shade: Configuration by configurations.creating {
|
||||||
|
configurations.getByName("compileClasspath").extendsFrom(this)
|
||||||
|
configurations.getByName("runtimeClasspath").extendsFrom(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.annotation)
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
|
shade(libs.protobuf.javalite)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
// Make sure generated proto sources are compiled and end up in the shaded jar
|
||||||
|
main {
|
||||||
|
java.srcDir("$buildDir/generated/source/proto/main/java")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = libs.protobuf.protoc.get().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach { task ->
|
||||||
|
task.builtins {
|
||||||
|
named("java") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
|
||||||
|
configurations = listOf(shade)
|
||||||
|
relocate("com.google.protobuf", "app.revanced.com.google.protobuf")
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.named("runtimeElements") {
|
||||||
|
isCanBeConsumed = true
|
||||||
|
isCanBeResolved = false
|
||||||
|
|
||||||
|
outgoing.artifacts.clear()
|
||||||
|
outgoing.artifact(shadowJar)
|
||||||
|
}!!.let { artifacts { add(it.name, shadowJar) } }
|
||||||
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package app.revanced.extension.shared.innertube;
|
||||||
|
|
||||||
|
option optimize_for = LITE_RUNTIME;
|
||||||
|
option java_package = "app.revanced.extension.shared.innertube";
|
||||||
|
|
||||||
|
message PlayerResponse {
|
||||||
|
oneof data {
|
||||||
|
PlayabilityStatus playability_status = 2;
|
||||||
|
StreamingData streaming_data = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayabilityStatus {
|
||||||
|
Status status = 1;
|
||||||
|
string reason = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
OK = 0;
|
||||||
|
ERROR = 1;
|
||||||
|
UNPLAYABLE = 2;
|
||||||
|
LOGIN_REQUIRED = 3;
|
||||||
|
CONTENT_CHECK_REQUIRED = 4;
|
||||||
|
AGE_CHECK_REQUIRED = 5;
|
||||||
|
LIVE_STREAM_OFFLINE = 6;
|
||||||
|
FULLSCREEN_ONLY = 7;
|
||||||
|
GL_PLAYBACK_REQUIRED = 8;
|
||||||
|
AGE_VERIFICATION_REQUIRED = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StreamingData {
|
||||||
|
repeated Format formats = 2;
|
||||||
|
repeated Format adaptiveFormats = 3;
|
||||||
|
string serverAbrStreamingUrl = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Format {
|
||||||
|
string url = 2;
|
||||||
|
string signatureCipher = 48;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
import "app/revanced/extension/shared/innertube/player_response.proto";
|
||||||
|
|
||||||
|
package app.revanced.extension.shared.innertube;
|
||||||
|
|
||||||
|
option optimize_for = LITE_RUNTIME;
|
||||||
|
option java_package = "app.revanced.extension.shared.innertube";
|
||||||
|
|
||||||
|
message ReelItemWatchResponse {
|
||||||
|
oneof data {
|
||||||
|
PlayerResponse player_response = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package app.revanced.extension.youtube.patches.spoof;
|
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_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_43_32;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
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 static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -44,11 +43,11 @@ public class SpoofVideoStreamsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ClientType> availableClients = List.of(
|
List<ClientType> availableClients = List.of(
|
||||||
VISIONOS,
|
ANDROID_REEL,
|
||||||
ANDROID_CREATOR,
|
|
||||||
ANDROID_VR_1_43_32,
|
ANDROID_VR_1_43_32,
|
||||||
ANDROID_NO_SDK,
|
VISIONOS,
|
||||||
IPADOS);
|
ANDROID_CREATOR
|
||||||
|
);
|
||||||
|
|
||||||
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
||||||
availableClients, client);
|
availableClients, client);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "unused"})
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
public class SpoofVideoStreamsSideEffectsPreference extends Preference {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private ClientType currentClientType;
|
private ClientType currentClientType;
|
||||||
|
|
@ -33,19 +33,19 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
||||||
Utils.runOnMainThread(this::updateUI);
|
Utils.runOnMainThread(this::updateUI);
|
||||||
};
|
};
|
||||||
|
|
||||||
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) {
|
public SpoofVideoStreamsSideEffectsPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpoofStreamingDataSideEffectsPreference(Context context) {
|
public SpoofVideoStreamsSideEffectsPreference(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,27 +88,23 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
||||||
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume")
|
+ '\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_av1")
|
||||||
+ '\n' + str("revanced_spoof_video_streams_about_no_force_original_audio");
|
+ '\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.
|
// 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 ->
|
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")
|
summary = str("revanced_spoof_video_streams_about_playback_failure")
|
||||||
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
|
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
|
||||||
case VISIONOS ->
|
+ '\n' + str("revanced_spoof_video_streams_about_no_stable_volume");
|
||||||
summary = str("revanced_spoof_video_streams_about_experimental")
|
case VISIONOS -> summary = str("revanced_spoof_video_streams_about_experimental")
|
||||||
|
+ '\n' + str("revanced_spoof_video_streams_about_playback_failure")
|
||||||
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
|
+ '\n' + str("revanced_spoof_video_streams_about_no_audio_tracks")
|
||||||
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
|
+ '\n' + str("revanced_spoof_video_streams_about_no_av1");
|
||||||
default -> Logger.printException(() -> "Unknown client: " + clientType);
|
default -> Logger.printException(() -> "Unknown client: " + clientType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only iPadOS can play children videos in incognito, but it commonly fails at 1 minute
|
// Only Android Reel and Android VR supports 360° VR immersive mode.
|
||||||
// or doesn't start playback at all. List the side effect for other clients
|
if (!clientType.name().startsWith("ANDROID_VR") && clientType != ClientType.ANDROID_REEL) {
|
||||||
// since they will fall over to iPadOS.
|
summary += '\n' + str("revanced_spoof_video_streams_about_no_immersive_mode");
|
||||||
if (clientType != ClientType.IPADOS && clientType != ClientType.ANDROID_NO_SDK) {
|
|
||||||
summary += '\n' + str("revanced_spoof_video_streams_about_kids_videos");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use better formatting for bullet points.
|
// Use better formatting for bullet points.
|
||||||
|
|
@ -10,6 +10,10 @@ okhttp = "5.3.2"
|
||||||
retrofit = "3.0.0"
|
retrofit = "3.0.0"
|
||||||
guava = "33.5.0-jre"
|
guava = "33.5.0-jre"
|
||||||
apksig = "9.0.1"
|
apksig = "9.0.1"
|
||||||
|
# TODO: Adjust once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged.
|
||||||
|
protobuf = "master-SNAPSHOT"
|
||||||
|
protoc = "4.34.0"
|
||||||
|
shadow = "9.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
||||||
|
|
@ -18,6 +22,10 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||||
apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" }
|
apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" }
|
||||||
|
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protoc" }
|
||||||
|
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-library = { id = "com.android.library" }
|
android-library = { id = "com.android.library" }
|
||||||
|
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
|
||||||
|
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
|
||||||
|
|
|
||||||
|
|
@ -169,13 +169,13 @@ internal fun spoofVideoStreamsPatch(
|
||||||
if-eqz v2, :disabled
|
if-eqz v2, :disabled
|
||||||
|
|
||||||
# Get streaming data.
|
# Get streaming data.
|
||||||
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)[B
|
||||||
move-result-object v3
|
move-result-object v3
|
||||||
if-eqz v3, :disabled
|
if-eqz v3, :disabled
|
||||||
|
|
||||||
# Parse streaming data.
|
# Parse streaming data.
|
||||||
sget-object v4, $playerProtoClass->a:$playerProtoClass
|
sget-object v4, $playerProtoClass->a:$playerProtoClass
|
||||||
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}[B)$protobufClass
|
||||||
move-result-object v5
|
move-result-object v5
|
||||||
check-cast v5, $playerProtoClass
|
check-cast v5, $playerProtoClass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
|
||||||
// Requires a key and title but the actual text is chosen at runtime.
|
// Requires a key and title but the actual text is chosen at runtime.
|
||||||
key = "revanced_spoof_video_streams_about",
|
key = "revanced_spoof_video_streams_about",
|
||||||
summaryKey = null,
|
summaryKey = null,
|
||||||
tag = "app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference",
|
tag = "app.revanced.extension.youtube.settings.preference.SpoofVideoStreamsSideEffectsPreference",
|
||||||
),
|
),
|
||||||
SwitchPreference("revanced_spoof_video_streams_av1"),
|
SwitchPreference("revanced_spoof_video_streams_av1"),
|
||||||
SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"),
|
SwitchPreference("revanced_spoof_video_streams_stats_for_nerds"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -220,14 +220,14 @@
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||||
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
||||||
|
<item>Android Reel</item>
|
||||||
<item>Android VR</item>
|
<item>Android VR</item>
|
||||||
<item>visionOS</item>
|
<item>visionOS</item>
|
||||||
<item>Android No SDK</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
|
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
|
||||||
|
<item>ANDROID_REEL</item>
|
||||||
<item>ANDROID_VR_1_43_32</item>
|
<item>ANDROID_VR_1_43_32</item>
|
||||||
<item>VISIONOS</item>
|
<item>VISIONOS</item>
|
||||||
<item>ANDROID_NO_SDK</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
</patch>
|
</patch>
|
||||||
</app>
|
</app>
|
||||||
|
|
@ -264,18 +264,16 @@
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||||
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
||||||
|
<item>Android Reel</item>
|
||||||
<item>Android VR</item>
|
<item>Android VR</item>
|
||||||
<item>Android Studio</item>
|
<item>Android Studio</item>
|
||||||
<item>Android No SDK</item>
|
|
||||||
<item>visionOS</item>
|
<item>visionOS</item>
|
||||||
<item>iPadOS</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
|
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
|
||||||
|
<item>ANDROID_REEL</item>
|
||||||
<item>ANDROID_VR_1_43_32</item>
|
<item>ANDROID_VR_1_43_32</item>
|
||||||
<item>ANDROID_CREATOR</item>
|
<item>ANDROID_CREATOR</item>
|
||||||
<item>ANDROID_NO_SDK</item>
|
|
||||||
<item>VISIONOS</item>
|
<item>VISIONOS</item>
|
||||||
<item>IPADOS</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="interaction.swipecontrols.swipeControlsResourcePatch">
|
<patch id="interaction.swipecontrols.swipeControlsResourcePatch">
|
||||||
|
|
@ -663,8 +661,7 @@
|
||||||
<item>cross-out</item>
|
<item>cross-out</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="chat.autoclaim.autoClaimChannelPointsPatch">
|
<patch id="chat.autoclaim.autoClaimChannelPointsPatch"></patch>
|
||||||
</patch>
|
|
||||||
<patch id="ad.embedded.embeddedAdsPatch">
|
<patch id="ad.embedded.embeddedAdsPatch">
|
||||||
<string-array name="revanced_block_embedded_ads_entries">
|
<string-array name="revanced_block_embedded_ads_entries">
|
||||||
<item>@string/revanced_block_embedded_ads_entry_1</item>
|
<item>@string/revanced_block_embedded_ads_entry_1</item>
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,16 @@ If you are a YouTube Premium user, this setting may not be required"</string>
|
||||||
Playback may not work"</string>
|
Playback may not work"</string>
|
||||||
<string name="revanced_spoof_video_streams_user_dialog_message">Turning off this setting may cause playback issues.</string>
|
<string name="revanced_spoof_video_streams_user_dialog_message">Turning off this setting may cause playback issues.</string>
|
||||||
<string name="revanced_spoof_video_streams_client_type_title">Default client</string>
|
<string name="revanced_spoof_video_streams_client_type_title">Default client</string>
|
||||||
|
|
||||||
|
<string name="revanced_spoof_video_streams_about_experimental">• Experimental client and may stop working anytime</string>
|
||||||
|
<string name="revanced_spoof_video_streams_about_playback_failure">• Video may stop at 1:00, or may not be available in some regions</string>
|
||||||
|
<string name="revanced_spoof_video_streams_about_no_audio_tracks">• Audio track menu is missing</string>
|
||||||
|
<string name="revanced_spoof_video_streams_about_no_av1">• No AV1 video codec</string>
|
||||||
|
<string name="revanced_spoof_video_streams_about_no_immersive_mode">• 360° VR immersive mode is not available</string>
|
||||||
|
<string name="revanced_spoof_video_streams_about_no_stable_volume">• Stable volume is not available</string>
|
||||||
|
<string name="revanced_spoof_video_streams_about_title">Spoofing side effects</string>
|
||||||
|
<!-- "Force original audio" should use the same text as revanced_force_original_audio_title -->
|
||||||
|
<string name="revanced_spoof_video_streams_about_no_force_original_audio">• Force original audio is not available</string>
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="misc.audio.forceOriginalAudioPatch">
|
<patch id="misc.audio.forceOriginalAudioPatch">
|
||||||
<string name="revanced_force_original_audio_title">Force original audio language</string>
|
<string name="revanced_force_original_audio_title">Force original audio language</string>
|
||||||
|
|
@ -900,10 +910,10 @@ Adjust volume by swiping vertically on the right side of the screen"</string>
|
||||||
<string name="revanced_hide_player_flyout_audio_track_summary_on">Audio track menu is hidden</string>
|
<string name="revanced_hide_player_flyout_audio_track_summary_on">Audio track menu is hidden</string>
|
||||||
<string name="revanced_hide_player_flyout_audio_track_summary_off">Audio track menu is shown</string>
|
<string name="revanced_hide_player_flyout_audio_track_summary_off">Audio track menu is shown</string>
|
||||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'.
|
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'.
|
||||||
'Android No SDK' must be kept untranslated. -->
|
'Android Reel' must be kept untranslated. -->
|
||||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Audio track menu is hidden
|
<string name="revanced_hide_player_flyout_audio_track_not_available">"Audio track menu is hidden
|
||||||
|
|
||||||
To show the Audio track menu, change \'Spoof video streams\' to \'Android No SDK\'"</string>
|
To show the Audio track menu, change \'Spoof video streams\' to \'Android >Reel\'"</string>
|
||||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||||
<string name="revanced_hide_player_flyout_watch_in_vr_title">Hide Watch in VR</string>
|
<string name="revanced_hide_player_flyout_watch_in_vr_title">Hide Watch in VR</string>
|
||||||
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Watch in VR menu is hidden</string>
|
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Watch in VR menu is hidden</string>
|
||||||
|
|
@ -1786,18 +1796,9 @@ Playback may stutter or drop frames"</string>
|
||||||
<string name="revanced_spoof_video_streams_av1_user_dialog_message">"Enabling this setting may use software AV1 decoding.
|
<string name="revanced_spoof_video_streams_av1_user_dialog_message">"Enabling this setting may use software AV1 decoding.
|
||||||
|
|
||||||
Video playback with AV1 may stutter or drop frames."</string>
|
Video playback with AV1 may stutter or drop frames."</string>
|
||||||
<string name="revanced_spoof_video_streams_about_title">Spoofing side effects</string>
|
<string name="revanced_spoof_video_streams_stats_for_nerds_title">Show in Stats for nerds</string>
|
||||||
<string name="revanced_spoof_video_streams_about_experimental">• Experimental client and may stop working anytime</string>
|
<string name="revanced_spoof_video_streams_stats_for_nerds_summary_on">Client type is shown in Stats for nerds</string>
|
||||||
<string name="revanced_spoof_video_streams_about_playback_failure">• Video may stop at 1:00, or may not be available in some regions</string>
|
<string name="revanced_spoof_video_streams_stats_for_nerds_summary_off">Client is hidden in Stats for nerds</string>
|
||||||
<string name="revanced_spoof_video_streams_about_no_audio_tracks">• Audio track menu is missing</string>
|
|
||||||
<string name="revanced_spoof_video_streams_about_no_av1">• No AV1 video codec</string>
|
|
||||||
<string name="revanced_spoof_video_streams_about_no_stable_volume">• Stable volume is not available</string>
|
|
||||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Kids videos may not play when logged out or in incognito mode</string>
|
|
||||||
<!-- "Force original audio" should use the same text as revanced_force_original_audio_title -->
|
|
||||||
<string name="revanced_spoof_video_streams_about_no_force_original_audio">• Force original audio is not available</string>
|
|
||||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Show in Stats for nerds</string>
|
|
||||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Client type is shown in Stats for nerds</string>
|
|
||||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Client is hidden in Stats for nerds</string>
|
|
||||||
</patch>
|
</patch>
|
||||||
</app>
|
</app>
|
||||||
<app id="music">
|
<app id="music">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ rootProject.name = "revanced-patches"
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
maven {
|
maven {
|
||||||
|
|
@ -10,12 +9,16 @@ pluginManagement {
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patches-gradle-plugin")
|
url = uri("https://maven.pkg.github.com/revanced/revanced-patches-gradle-plugin")
|
||||||
credentials(PasswordCredentials::class)
|
credentials(PasswordCredentials::class)
|
||||||
}
|
}
|
||||||
|
// TODO: Remove once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged.
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
}
|
||||||
|
// TODO: Remove once https://github.com/google/protobuf-gradle-plugin/pull/797 is merged.
|
||||||
|
resolutionStrategy {
|
||||||
|
eachPlugin {
|
||||||
|
if (requested.id.id == "com.google.protobuf") {
|
||||||
|
useModule("com.github.ReVanced:protobuf-gradle-plugin:${requested.version}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue