fix(YouTube): Fix issues related to playback by replace streaming data (#680)

Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
Zain 2024-09-18 05:45:14 +07:00 committed by GitHub
parent ca50665ac8
commit 04682353af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 615 additions and 777 deletions

View File

@ -0,0 +1,79 @@
package app.revanced.integrations.youtube.patches.spoof;
import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1;
import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9;
import android.os.Build;
import androidx.annotation.Nullable;
public enum ClientType {
// https://dumps.tadiphone.dev/dumps/oculus/eureka
IOS(5,
// iPhone 15 supports AV1 hardware decoding.
// Only use if this Android device also has hardware decoding.
allowAV1()
? "iPhone16,2" // 15 Pro Max
: "iPhone11,4", // XS Max
// iOS 14+ forces VP9.
allowVP9()
? "17.5.1.21F90"
: "13.7.17H35",
allowVP9()
? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
: "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
null,
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
"19.10.7"
),
ANDROID_VR(28,
"Quest 3",
"12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
"32", // Android 12.1
"1.56.21"
);
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
public final int id;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String model;
/**
* Device OS version.
*/
public final String osVersion;
/**
* Player user-agent.
*/
public final String userAgent;
/**
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
* Field is null if not applicable.
*/
@Nullable
public final String androidSdkVersion;
/**
* App version.
*/
public final String appVersion;
ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) {
this.id = id;
this.model = model;
this.osVersion = osVersion;
this.userAgent = userAgent;
this.androidSdkVersion = androidSdkVersion;
this.appVersion = appVersion;
}
}

View File

@ -0,0 +1,53 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.os.Build;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.settings.Settings;
public class DeviceHardwareSupport {
public static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
public static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1;
static {
boolean vp9found = false;
boolean av1found = false;
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater
? codecInfo.isHardwareAccelerated()
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
for (String type : codecInfo.getSupportedTypes()) {
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
vp9found = true;
} else if (type.equalsIgnoreCase("video/av01")) {
av1found = true;
}
}
}
}
DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found;
DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found;
Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1
? "Device supports AV1 hardware decoding\n"
: "Device does not support AV1 hardware decoding\n"
+ (DEVICE_HAS_HARDWARE_DECODING_VP9
? "Device supports VP9 hardware decoding"
: "Device does not support VP9 hardware decoding"));
}
public static boolean allowVP9() {
return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
}
public static boolean allowAV1() {
return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
}
}

View File

@ -1,279 +0,0 @@
package app.revanced.integrations.youtube.patches.spoof;
import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowAV1;
import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.DeviceHardwareSupport.allowVP9;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.net.Uri;
import android.os.Build;
import org.chromium.net.ExperimentalUrlRequest;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofClientPatch {
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get();
private static final boolean SPOOF_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS;
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_CLIENT_ENABLED) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_CLIENT_ENABLED) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
return UNREACHABLE_HOST_URI_STRING;
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/**
* Injection point.
*/
public static int getClientTypeId(int originalClientTypeId) {
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.id : originalClientTypeId;
}
/**
* Injection point.
*/
public static String getClientVersion(String originalClientVersion) {
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.appVersion : originalClientVersion;
}
/**
* Injection point.
*/
public static String getClientModel(String originalClientModel) {
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.model : originalClientModel;
}
/**
* Injection point.
* Fix video qualities missing, if spoofing to iOS by using the correct client OS version.
*/
public static String getOsVersion(String originalOsVersion) {
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.osVersion : originalOsVersion;
}
/**
* Injection point.
*/
public static boolean enablePlayerGesture(boolean original) {
return SPOOF_CLIENT_ENABLED || original;
}
/**
* Injection point.
*/
public static boolean isClientSpoofingEnabled() {
return SPOOF_CLIENT_ENABLED;
}
/**
* Injection point.
* When spoofing the client to iOS, the playback speed menu is missing from the player response.
* Return true to force create the playback speed menu.
*/
public static boolean forceCreatePlaybackSpeedMenu(boolean original) {
return SPOOF_IOS || original;
}
/**
* Injection point.
* When spoofing the client to iOS, background audio only playback of livestreams fails.
* Return true to force enable audio background play.
*/
public static boolean overrideBackgroundAudioPlayback() {
return SPOOF_IOS && BackgroundPlaybackPatch.playbackIsNotShort();
}
/**
* Injection point.
* Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent.
*/
public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url) {
if (SPOOF_CLIENT_ENABLED) {
String path = Uri.parse(url).getPath();
if (path != null && path.contains("player")) {
return builder.addHeader("User-Agent", SPOOF_CLIENT_TYPE.userAgent).build();
}
}
return builder.build();
}
// Must check for device features in a separate class and cannot place this code inside
// the Patch or ClientType enum due to cyclic Setting references.
static class DeviceHardwareSupport {
private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9 = deviceHasVP9HardwareDecoding();
private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1 = deviceHasAV1HardwareDecoding();
private static boolean deviceHasVP9HardwareDecoding() {
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
final boolean isHardwareAccelerated = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
? codecInfo.isHardwareAccelerated()
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
for (String type : codecInfo.getSupportedTypes()) {
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
Logger.printDebug(() -> "Device supports VP9 hardware decoding.");
return true;
}
}
}
}
Logger.printDebug(() -> "Device does not support VP9 hardware decoding.");
return false;
}
private static boolean deviceHasAV1HardwareDecoding() {
// It appears all devices with hardware AV1 are also Android 10 or newer.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) {
for (String type : codecInfo.getSupportedTypes()) {
if (type.equalsIgnoreCase("video/av01")) {
Logger.printDebug(() -> "Device supports AV1 hardware decoding.");
return true;
}
}
}
}
}
Logger.printDebug(() -> "Device does not support AV1 hardware decoding.");
return false;
}
static boolean allowVP9() {
return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_CLIENT_IOS_FORCE_AVC.get();
}
static boolean allowAV1() {
return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
}
}
public enum ClientType {
// https://dumps.tadiphone.dev/dumps/oculus/eureka
IOS(5,
// iPhone 15 supports AV1 hardware decoding.
// Only use if this Android device also has hardware decoding.
allowAV1()
? "iPhone16,2" // 15 Pro Max
: "iPhone11,4", // XS Max
// iOS 14+ forces VP9.
allowVP9()
? "17.5.1.21F90"
: "13.7.17H35",
allowVP9()
? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
: "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
"19.10.7"
),
ANDROID_VR(28,
"Quest 3",
"12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
"1.56.21"
);
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
final int id;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
final String model;
/**
* Device OS version.
*/
final String osVersion;
/**
* Player user-agent.
*/
final String userAgent;
/**
* App version.
*/
final String appVersion;
ClientType(int id, String model, String osVersion, String userAgent, String appVersion) {
this.id = id;
this.model = model;
this.osVersion = osVersion;
this.userAgent = userAgent;
this.appVersion = appVersion;
}
}
public static final class ForceiOSAVCAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return Settings.SPOOF_CLIENT.get() && Settings.SPOOF_CLIENT_TYPE.get() == ClientType.IOS;
}
}
}

View File

@ -1,242 +0,0 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.patches.VideoInformation;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.PlayerType;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static app.revanced.integrations.shared.Utils.containsAny;
import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
/** @noinspection unused*/
@Deprecated
public class SpoofSignaturePatch {
/**
* Parameter (also used by
* <a href="https://github.com/yt-dlp/yt-dlp/blob/81ca451480051d7ce1a31c017e005358345a9149/yt_dlp/extractor/youtube.py#L3602">yt-dlp</a>)
* to fix playback issues.
*/
private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
/**
* Parameters used when playing clips.
*/
private static final String CLIPS_PARAMETERS = "kAIB";
/**
* Parameters causing playback issues.
*/
private static final String[] AUTOPLAY_PARAMETERS = {
"YAHI", // Autoplay in feed.
"SAFg" // Autoplay in scrim.
};
/**
* Parameter used for autoplay in scrim.
* Prepend this parameter to mute video playback (for autoplay in feed).
*/
private static final String SCRIM_PARAMETER = "SAFgAXgB";
/**
* Last video id loaded. Used to prevent reloading the same spec multiple times.
*/
@Nullable
private static volatile String lastPlayerResponseVideoId;
@Nullable
private static volatile Future<StoryboardRenderer> rendererFuture;
private static volatile boolean useOriginalStoryboardRenderer;
private static volatile boolean isPlayingShorts;
@Nullable
private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
Future<StoryboardRenderer> future = rendererFuture;
if (future != null) {
try {
if (waitForCompletion || future.isDone()) {
return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
} // else, return null.
} catch (TimeoutException ex) {
Logger.printDebug(() -> "Could not get renderer (get timed out)");
} catch (ExecutionException | InterruptedException ex) {
// Should never happen.
Logger.printException(() -> "Could not get renderer", ex);
}
}
return null;
}
/**
* Injection point.
*
* Called off the main thread, and called multiple times for each video.
*
* @param parameters Original protobuf parameter value.
*/
public static String spoofParameter(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) {
try {
Logger.printDebug(() -> "Original protobuf parameter value: " + parameters);
if (parameters == null || !Settings.SPOOF_SIGNATURE.get()) {
return parameters;
}
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
// Clips are 60 seconds or less in length, so no spoofing.
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) {
return parameters;
}
// Shorts do not need to be spoofed.
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
isPlayingShorts = true;
return parameters;
}
isPlayingShorts = false;
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
if (isPlayingFeed) {
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = !Settings.SPOOF_SIGNATURE_IN_FEED.get()) {
// Don't spoof the feed video playback. This will cause video playback issues,
// but only if user continues watching for more than 1 minute.
return parameters;
}
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
fetchStoryboardRenderer();
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
}
fetchStoryboardRenderer();
} catch (Exception ex) {
Logger.printException(() -> "spoofParameter failure", ex);
}
return INCOGNITO_PARAMETERS;
}
private static void fetchStoryboardRenderer() {
if (!Settings.SPOOF_STORYBOARD_RENDERER.get()) {
lastPlayerResponseVideoId = null;
rendererFuture = null;
return;
}
String videoId = VideoInformation.getPlayerResponseVideoId();
if (!videoId.equals(lastPlayerResponseVideoId)) {
rendererFuture = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
lastPlayerResponseVideoId = videoId;
}
// Block until the renderer fetch completes.
// This is desired because if this returns without finishing the fetch
// then video will start playback but the storyboard is not ready yet.
getRenderer(true);
}
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
boolean returnNullIfLiveStream) {
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream) {
return null;
}
if (renderer.spec != null) {
return renderer.spec;
}
}
}
return originalStoryboardRendererSpec;
}
/**
* Injection point.
* Called from background threads and from the main thread.
*/
@Nullable
public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
return getStoryboardRendererSpec(originalStoryboardRendererSpec, false);
}
/**
* Injection point.
* Uses additional check to handle live streams.
* Called from background threads and from the main thread.
*/
@Nullable
public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) {
return getStoryboardRendererSpec(originalStoryboardRendererSpec, true);
}
/**
* Injection point.
*/
public static int getRecommendedLevel(int originalLevel) {
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
if (renderer.recommendedLevel != null) {
return renderer.recommendedLevel;
}
}
}
return originalLevel;
}
/**
* Injection point. Forces seekbar to be shown for paid videos or
* if {@link Settings#SPOOF_STORYBOARD_RENDERER} is not enabled.
*/
public static boolean getSeekbarThumbnailOverrideValue() {
if (!Settings.SPOOF_SIGNATURE.get()) {
return false;
}
StoryboardRenderer renderer = getRenderer(false);
if (renderer == null) {
// Spoof storyboard renderer is turned off,
// video is paid, or the storyboard fetch timed out.
// Show empty thumbnails so the seek time and chapters still show up.
return true;
}
return renderer.spec != null;
}
/**
* Injection point.
*
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
*/
public static void seekbarImageViewCreated(ImageView view) {
try {
if (!Settings.SPOOF_SIGNATURE.get()
|| Settings.SPOOF_STORYBOARD_RENDERER.get()) {
return;
}
if (isPlayingShorts) return;
view.setVisibility(View.GONE);
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
ViewGroup parentLayout = (ViewGroup) view.getParent();
parentLayout.setPadding(0, 0, 0, 0);
} catch (Exception ex) {
Logger.printException(() -> "seekbarImageViewCreated failure", ex);
}
}
}

View File

@ -0,0 +1,170 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofVideoStreamsPatch {
public static final class ForceiOSAVCAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return Settings.SPOOF_VIDEO_STREAMS.get() && Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS;
}
}
private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_VIDEO_STREAMS.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_STREAMING_DATA) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_STREAMING_DATA) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
return UNREACHABLE_HOST_URI_STRING;
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/**
* Injection point.
*/
public static boolean isSpoofingEnabled() {
return SPOOF_STREAMING_DATA;
}
/**
* Injection point.
*/
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
if (SPOOF_STREAMING_DATA) {
try {
Uri uri = Uri.parse(url);
String path = uri.getPath();
// 'heartbeat' has no video id and appears to be only after playback has started.
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
}
} catch (Exception ex) {
Logger.printException(() -> "buildRequest failure", ex);
}
}
}
/**
* Injection point.
* Fix playback by replace the streaming data.
* Called after {@link #fetchStreams(String, Map)}.
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
if (SPOOF_STREAMING_DATA) {
try {
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
if (request != null) {
// This hook is always called off the main thread,
// but this can later be called for the same video id from the main thread.
// This is not a concern, since the fetch will always be finished
// and never block the main thread.
// But if debugging, then still verify this is the situation.
if (BaseSettings.DEBUG.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
Logger.printException(() -> "Error: Blocking main thread");
}
var stream = request.getStream();
if (stream != null) {
Logger.printDebug(() -> "Overriding video stream: " + videoId);
return stream;
}
}
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
} catch (Exception ex) {
Logger.printException(() -> "getStreamingData failure", ex);
}
}
return null;
}
/**
* Injection point.
* Called after {@link #getStreamingData(String)}.
*/
@Nullable
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
if (SPOOF_STREAMING_DATA) {
try {
final int methodPost = 2;
if (method == methodPost) {
String path = uri.getPath();
String clientNameQueryKey = "c";
final boolean iosClient = "IOS".equals(uri.getQueryParameter(clientNameQueryKey));
if (iosClient && path != null && path.contains("videoplayback")) {
return null;
}
}
} catch (Exception ex) {
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
}
}
return postData;
}
}

View File

@ -1,36 +0,0 @@
package app.revanced.integrations.youtube.patches.spoof;
import androidx.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
@Deprecated
public final class StoryboardRenderer {
public final String videoId;
@Nullable
public final String spec;
public final boolean isLiveStream;
/**
* Recommended image quality level, or NULL if no recommendation exists.
*/
@Nullable
public final Integer recommendedLevel;
public StoryboardRenderer(String videoId, @Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
this.videoId = videoId;
this.spec = spec;
this.isLiveStream = isLiveStream;
this.recommendedLevel = recommendedLevel;
}
@NotNull
@Override
public String toString() {
return "StoryboardRenderer{" +
"videoId=" + videoId +
", isLiveStream=" + isLiveStream +
", spec='" + spec + '\'' +
", recommendedLevel=" + recommendedLevel +
'}';
}
}

View File

@ -1,94 +1,68 @@
package app.revanced.integrations.youtube.patches.spoof.requests;
import app.revanced.integrations.youtube.requests.Requester;
import app.revanced.integrations.youtube.requests.Route;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.patches.spoof.ClientType;
import app.revanced.integrations.youtube.requests.Requester;
import app.revanced.integrations.youtube.requests.Route;
final class PlayerRoutes {
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=storyboards.playerStoryboardSpecRenderer," +
"storyboards.playerLiveStoryboardSpecRenderer," +
"playabilityStatus.status"
"?fields=streamingData" +
"&alt=proto"
).compile();
static final String ANDROID_INNER_TUBE_BODY;
static final String TV_EMBED_INNER_TUBE_BODY;
/**
* TCP connection and HTTP read timeout
*/
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
static {
JSONObject innerTubeBody = new JSONObject();
private PlayerRoutes() {
}
static String createInnertubeBody(ClientType clientType) {
JSONObject innerTubeBody = new JSONObject();
try {
JSONObject context = new JSONObject();
JSONObject client = new JSONObject();
client.put("clientName", "ANDROID");
client.put("clientVersion", Utils.getAppVersionName());
client.put("androidSdkVersion", 34);
client.put("clientName", clientType.name());
client.put("clientVersion", clientType.appVersion);
client.put("deviceModel", clientType.model);
client.put("osVersion", clientType.osVersion);
if (clientType.androidSdkVersion != null) {
client.put("androidSdkVersion", clientType.androidSdkVersion);
}
context.put("client", client);
innerTubeBody.put("context", context);
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", "%s");
} catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e);
}
ANDROID_INNER_TUBE_BODY = innerTubeBody.toString();
JSONObject tvEmbedInnerTubeBody = new JSONObject();
try {
JSONObject context = new JSONObject();
JSONObject client = new JSONObject();
client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER");
client.put("clientVersion", "2.0");
client.put("platform", "TV");
client.put("clientScreen", "EMBED");
JSONObject thirdParty = new JSONObject();
thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s");
context.put("thirdParty", thirdParty);
context.put("client", client);
tvEmbedInnerTubeBody.put("context", context);
tvEmbedInnerTubeBody.put("videoId", "%s");
} catch (JSONException e) {
Logger.printException(() -> "Failed to create tvEmbedInnerTubeBody", e);
}
TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString();
}
private PlayerRoutes() {
return innerTubeBody.toString();
}
/** @noinspection SameParameterValue*/
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException {
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
connection.setRequestProperty(
"User-Agent", "com.google.android.youtube/" +
Utils.getAppVersionName() +
" (Linux; U; Android 12; GB) gzip"
);
connection.setRequestProperty("X-Goog-Api-Format-Version", "2");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", clientType.userAgent);
connection.setUseCaches(false);
connection.setDoOutput(true);

View File

@ -1,153 +0,0 @@
package app.revanced.integrations.youtube.patches.spoof.requests;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.youtube.patches.spoof.StoryboardRenderer;
import app.revanced.integrations.youtube.requests.Requester;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*;
public class StoryboardRendererRequester {
private StoryboardRendererRequester() {
}
private static void randomlyWaitIfLocallyDebugging() {
final boolean randomlyWait = false; // Enable to simulate slow connection responses.
if (randomlyWait) {
final long maximumTimeToRandomlyWait = 10000;
Utils.doNothingForDuration(maximumTimeToRandomlyWait);
}
}
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex,
boolean showToastOnIOException) {
if (showToastOnIOException) Utils.showToastShort(toastMessage);
Logger.printInfo(() -> toastMessage, ex);
}
@Nullable
private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) {
final long startTime = System.currentTimeMillis();
try {
Utils.verifyOffMainThread();
Objects.requireNonNull(requestBody);
final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8);
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER);
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
final int responseCode = connection.getResponseCode();
randomlyWaitIfLocallyDebugging();
if (responseCode == 200) return Requester.parseJSONObject(connection);
// Always show a toast for this, as a non 200 response means something is broken.
// Not a normal code path and should not be reached, so no translations are needed.
handleConnectionError("Spoof storyboard not available: " + responseCode,
null, showToastOnIOException || BaseSettings.DEBUG_TOAST_ON_ERROR.get());
connection.disconnect();
} catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_spoof_client_storyboard_timeout"), ex, showToastOnIOException);
} catch (IOException ex) {
handleConnectionError(str("revanced_spoof_client_storyboard_io_exception", ex.getMessage()),
ex, showToastOnIOException);
} catch (Exception ex) {
Logger.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
} finally {
Logger.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
}
return null;
}
private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) {
try {
return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK");
} catch (JSONException e) {
Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse);
}
return false;
}
/**
* Fetches the storyboardRenderer from the innerTubeBody.
* @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer.
* @return StoryboardRenderer or null if playabilityStatus is not OK.
*/
@Nullable
private static StoryboardRenderer getStoryboardRendererUsingBody(String videoId,
@NonNull String innerTubeBody,
boolean showToastOnIOException) {
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException);
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
return getStoryboardRendererUsingResponse(videoId, playerResponse);
return null;
}
@Nullable
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull String videoId, @NonNull JSONObject playerResponse) {
try {
Logger.printDebug(() -> "Parsing response: " + playerResponse);
if (!playerResponse.has("storyboards")) {
Logger.printDebug(() -> "Using empty storyboard");
return new StoryboardRenderer(videoId, null, false, null);
}
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer");
final String storyboardsRendererTag = isLiveStream
? "playerLiveStoryboardSpecRenderer"
: "playerStoryboardSpecRenderer";
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
StoryboardRenderer renderer = new StoryboardRenderer(
videoId,
rendererElement.getString("spec"),
isLiveStream,
rendererElement.has("recommendedLevel")
? rendererElement.getInt("recommendedLevel")
: null
);
Logger.printDebug(() -> "Fetched: " + renderer);
return renderer;
} catch (JSONException e) {
Logger.printException(() -> "Failed to get storyboardRenderer", e);
}
return null;
}
@Nullable
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
Objects.requireNonNull(videoId);
var renderer = getStoryboardRendererUsingBody(videoId,
String.format(ANDROID_INNER_TUBE_BODY, videoId), false);
if (renderer == null) {
Logger.printDebug(() -> videoId + " not available using Android client");
renderer = getStoryboardRendererUsingBody(videoId,
String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true);
if (renderer == null) {
Logger.printDebug(() -> videoId + " not available using TV embedded client");
}
}
return renderer;
}
}

View File

@ -0,0 +1,215 @@
package app.revanced.integrations.youtube.patches.spoof.requests;
import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.youtube.patches.spoof.ClientType;
import app.revanced.integrations.youtube.settings.Settings;
/**
* Video streaming data. Fetching is tied to the behavior YT uses,
* where this class fetches the streams only when YT fetches.
*
* Effectively the cache expiration of these fetches is the same as the stock app,
* since the stock app would not use expired streams and therefor
* the integrations replace stream hook is called only if YT
* would have used it's own client streams.
*/
public class StreamingDataRequest {
private static final ClientType[] CLIENT_ORDER_TO_USE;
static {
ClientType[] allClientTypes = ClientType.values();
ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
CLIENT_ORDER_TO_USE[0] = preferredClient;
int i = 1;
for (ClientType c : allClientTypes) {
if (c != preferredClient) {
CLIENT_ORDER_TO_USE[i++] = c;
}
}
}
/**
* TCP connection and HTTP read timeout.
*/
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
/**
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
*/
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
new LinkedHashMap<>(100) {
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final int CACHE_LIMIT = 50;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
public static void fetchRequest(String videoId, Map<String, String> fetchHeaders) {
// Always fetch, even if there is a existing request for the same video.
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
}
@Nullable
public static StreamingDataRequest getRequestForVideoId(String videoId) {
return cache.get(videoId);
}
private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) {
if (showToast) Utils.showToastShort(toastMessage);
Logger.printInfo(() -> toastMessage, ex);
}
@Nullable
private static HttpURLConnection send(ClientType clientType, String videoId,
Map<String, String> playerHeaders,
boolean showErrorToasts) {
Objects.requireNonNull(clientType);
Objects.requireNonNull(videoId);
Objects.requireNonNull(playerHeaders);
final long startTime = System.currentTimeMillis();
String clientTypeName = clientType.name();
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
try {
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
String authHeader = playerHeaders.get("Authorization");
String visitorId = playerHeaders.get("X-Goog-Visitor-Id");
connection.setRequestProperty("Authorization", authHeader);
connection.setRequestProperty("X-Goog-Visitor-Id", visitorId);
String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
connection.setFixedLengthStreamingMode(requestBody.length);
connection.getOutputStream().write(requestBody);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) return connection;
handleConnectionError(clientTypeName + " not available with response code: "
+ responseCode + " message: " + connection.getResponseMessage(),
null, showErrorToasts);
} catch (SocketTimeoutException ex) {
handleConnectionError("Connection timeout", ex, showErrorToasts);
} catch (IOException ex) {
handleConnectionError("Network error", ex, showErrorToasts);
} catch (Exception ex) {
Logger.printException(() -> "send failed", ex);
} finally {
Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
}
return null;
}
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
final boolean debugEnabled = BaseSettings.DEBUG.get();
// Retry with different client if empty response body is received.
int i = 0;
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
// Show an error if the last client type fails, or if the debug is enabled then show for all attempts.
final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled;
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
if (connection != null) {
try {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() != 0) {
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
return ByteBuffer.wrap(baos.toByteArray());
}
}
}
} catch (IOException ex) {
Logger.printException(() -> "Fetch failed while processing response data", ex);
}
}
}
handleConnectionError("Could not fetch any client streams", null, debugEnabled);
return null;
}
private final String videoId;
private final Future<ByteBuffer> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
this.videoId = videoId;
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
}
public boolean fetchCompleted() {
return future.isDone();
}
@Nullable
public ByteBuffer getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printInfo(() -> "getStream timed out", ex);
} catch (InterruptedException ex) {
Logger.printException(() -> "getStream interrupted", ex);
Thread.currentThread().interrupt(); // Restore interrupt status flag.
} catch (ExecutionException ex) {
Logger.printException(() -> "getStream failure", ex);
}
return null;
}
@NonNull
@Override
public String toString() {
return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
}
}

View File

@ -7,8 +7,9 @@ import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.DeAr
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.integrations.youtube.patches.spoof.ClientType;
import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch;
import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch;
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
import java.util.Arrays;
@ -19,7 +20,6 @@ import static app.revanced.integrations.shared.settings.Setting.*;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType;
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
@ -256,10 +256,10 @@ public class Settings extends BaseSettings {
"revanced_spoof_device_dimensions_user_dialog_message");
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true,"revanced_spoof_client_user_dialog_message");
public static final BooleanSetting SPOOF_CLIENT_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_client_ios_force_avc", FALSE, true,
"revanced_spoof_client_ios_force_avc_user_dialog_message", new SpoofClientPatch.ForceiOSAVCAvailability());
public static final EnumSetting<ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS, true, parent(SPOOF_CLIENT));
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_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofVideoStreamsPatch.ForceiOSAVCAvailability());
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.IOS, true, parent(SPOOF_VIDEO_STREAMS));
@Deprecated
public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);

View File

@ -0,0 +1,61 @@
package app.revanced.integrations.youtube.settings.preference;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.DEVICE_HAS_HARDWARE_DECODING_VP9;
import android.content.Context;
import android.preference.SwitchPreference;
import android.util.AttributeSet;
@SuppressWarnings({"unused", "deprecation"})
public class ForceAVCSpoofingPreference extends SwitchPreference {
{
if (!DEVICE_HAS_HARDWARE_DECODING_VP9) {
setSummaryOn(str("revanced_spoof_video_streams_ios_force_avc_no_hardware_vp9_summary_on"));
}
}
public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ForceAVCSpoofingPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ForceAVCSpoofingPreference(Context context) {
super(context);
}
private void updateUI() {
if (DEVICE_HAS_HARDWARE_DECODING_VP9) {
return;
}
// Temporarily remove the preference key to allow changing this preference without
// causing the settings UI listeners from showing reboot dialogs by the changes made here.
String key = getKey();
setKey(null);
// This setting cannot be changed by the user.
super.setEnabled(false);
super.setChecked(true);
setKey(key);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
updateUI();
}
@Override
public void setChecked(boolean checked) {
super.setChecked(checked);
updateUI();
}
}

View File

@ -1,8 +0,0 @@
package org.chromium.net;
public abstract class ExperimentalUrlRequest {
public abstract class Builder {
public abstract ExperimentalUrlRequest.Builder addHeader(String name, String value);
public abstract ExperimentalUrlRequest build();
}
}

View File

@ -1,4 +1,8 @@
package org.chromium.net;
public abstract class UrlRequest {
public abstract class Builder {
public abstract Builder addHeader(String name, String value);
public abstract UrlRequest build();
}
}