Compare commits

...

11 Commits

Author SHA1 Message Date
LisoUseInAIKyrios
debe93d87a fix: Ignore proguard warnings 2025-09-17 21:03:29 +04:00
LisoUseInAIKyrios
3910d9c9b9 fix: Use okio that works with kotlin 1.9.0 2025-09-17 21:03:28 +04:00
LisoUseInAIKyrios
e0edfdbd97 fix: Add missing repository 2025-09-17 20:56:57 +04:00
oSumAtrIX
95580f84ec fix(YouTube - Spoof video streams): Use webview to generate a PoToken for web client streams 2025-09-17 11:03:14 +04:00
semantic-release-bot
2520129ace chore: Release v5.38.1-dev.2 [skip ci]
## [5.38.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.38.1-dev.1...v5.38.1-dev.2) (2025-09-16)

### Bug Fixes

* **YouTube Music - Spoof video streams:** Remove iPadOS client ([7eeffd3](7eeffd3392))
2025-09-16 21:49:36 +00:00
LisoUseInAIKyrios
7eeffd3392 fix(YouTube Music - Spoof video streams): Remove iPadOS client 2025-09-17 01:44:48 +04:00
LisoUseInAIKyrios
6c3391164e chore: Remove spoof stream data migration since iPadOS can cause 1 minute playback failure for users in some regions 2025-09-16 23:44:01 +04:00
semantic-release-bot
0b8b46c73e chore: Release v5.38.1-dev.1 [skip ci]
## [5.38.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.38.0...v5.38.1-dev.1) (2025-09-16)

### Bug Fixes

* **YouTube - Spoof video streams:** Do not use Android Creator for livestreams ([cbe576b](cbe576bc38))
2025-09-16 19:27:03 +00:00
LisoUseInAIKyrios
cbe576bc38 fix(YouTube - Spoof video streams): Do not use Android Creator for livestreams 2025-09-16 23:24:07 +04:00
github-actions[bot]
3a29f2a805 chore: Sync translations (#5909) 2025-09-16 23:21:01 +04:00
LisoUseInAIKyrios
50069c7e05 chore: Fix merge typo 2025-09-16 17:26:48 +04:00
37 changed files with 907 additions and 155 deletions

View File

@@ -1,3 +1,17 @@
## [5.38.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.38.1-dev.1...v5.38.1-dev.2) (2025-09-16)
### Bug Fixes
* **YouTube Music - Spoof video streams:** Remove iPadOS client ([7eeffd3](https://github.com/ReVanced/revanced-patches/commit/7eeffd3392c57555342173103d3a417c038d0970))
## [5.38.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.38.0...v5.38.1-dev.1) (2025-09-16)
### Bug Fixes
* **YouTube - Spoof video streams:** Do not use Android Creator for livestreams ([cbe576b](https://github.com/ReVanced/revanced-patches/commit/cbe576bc384ef5f5ee2fa341147925ed0dff568b))
# [5.38.0](https://github.com/ReVanced/revanced-patches/compare/v5.37.0...v5.38.0) (2025-09-16)

View File

@@ -1,5 +1,6 @@
package app.revanced.extension.music.patches.spoof;
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
@@ -22,6 +23,8 @@ public class SpoofVideoStreamsPatch {
VISIONOS
);
StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32);
ClientType client = SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setPreferredClient(client);
StreamingDataRequest.setClientOrderToUse(availableClients, client);
}
}

View File

@@ -3,8 +3,12 @@ package app.revanced.extension.music.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.spoof.ClientType;
public class Settings extends BaseSettings {
@@ -18,4 +22,8 @@ public class Settings extends BaseSettings {
// Player
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
// Miscellaneous
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));
}

View File

@@ -7,3 +7,8 @@
-keep class com.google.** {
*;
}
-keep class org.mozilla.javascript.** { *; }
-dontwarn org.mozilla.javascript.tools.**
-dontwarn java.beans.**
-dontwarn jdk.dynalink.**
-dontwarn javax.script.**

View File

@@ -1,6 +1,4 @@
package app.revanced.extension.youtube;
import androidx.annotation.NonNull;
package app.revanced.extension.shared;
import java.nio.charset.StandardCharsets;
@@ -39,7 +37,7 @@ public final class ByteTrieSearch extends TrieSearch<byte[]> {
return replacement;
}
public ByteTrieSearch(@NonNull byte[]... patterns) {
public ByteTrieSearch(byte[]... patterns) {
super(new ByteTrieNode(), patterns);
}
}

View File

@@ -1,6 +1,4 @@
package app.revanced.extension.youtube;
import androidx.annotation.NonNull;
package app.revanced.extension.shared;
/**
* Text pattern searching using a prefix tree (trie).
@@ -28,7 +26,7 @@ public final class StringTrieSearch extends TrieSearch<String> {
}
}
public StringTrieSearch(@NonNull String... patterns) {
public StringTrieSearch(String... patterns) {
super(new StringTrieNode(), patterns);
}
}

View File

@@ -1,6 +1,5 @@
package app.revanced.extension.youtube;
package app.revanced.extension.shared;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
@@ -8,8 +7,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Searches for a group of different patterns using a trie (prefix tree).
/**Searches for a group of different patterns using a trie (prefix tree).
* Can significantly speed up searching for multiple patterns.
*/
public abstract class TrieSearch<T> {
@@ -136,7 +134,7 @@ public abstract class TrieSearch<T> {
* @param patternLength Length of the pattern.
* @param callback Callback, where a value of NULL indicates to always accept a pattern match.
*/
private void addPattern(@NonNull T pattern, int patternIndex, int patternLength,
private void addPattern(T pattern, int patternIndex, int patternLength,
@Nullable TriePatternMatchedCallback<T> callback) {
if (patternIndex == patternLength) { // Reached the end of the pattern.
if (endOfPatternCallback == null) {
@@ -308,13 +306,13 @@ public abstract class TrieSearch<T> {
private final List<T> patterns = new ArrayList<>();
@SafeVarargs
TrieSearch(@NonNull TrieNode<T> root, @NonNull T... patterns) {
TrieSearch(TrieNode<T> root, T... patterns) {
this.root = Objects.requireNonNull(root);
addPatterns(patterns);
}
@SafeVarargs
public final void addPatterns(@NonNull T... patterns) {
public final void addPatterns(T... patterns) {
for (T pattern : patterns) {
addPattern(pattern);
}
@@ -325,7 +323,7 @@ public abstract class TrieSearch<T> {
*
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
*/
public void addPattern(@NonNull T pattern) {
public void addPattern(T pattern) {
addPattern(pattern, root.getTextLength(pattern), null);
}
@@ -333,31 +331,31 @@ public abstract class TrieSearch<T> {
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
* @param callback Callback to determine if searching should halt when a match is found.
*/
public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback<T> callback) {
public void addPattern(T pattern, TriePatternMatchedCallback<T> callback) {
addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
}
void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
void addPattern(T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
if (patternLength == 0) return; // Nothing to match
patterns.add(pattern);
root.addPattern(pattern, 0, patternLength, callback);
}
public final boolean matches(@NonNull T textToSearch) {
public final boolean matches(T textToSearch) {
return matches(textToSearch, 0);
}
public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) {
public boolean matches(T textToSearch, Object callbackParameter) {
return matches(textToSearch, 0, root.getTextLength(textToSearch),
Objects.requireNonNull(callbackParameter));
}
public boolean matches(@NonNull T textToSearch, int startIndex) {
public boolean matches(T textToSearch, int startIndex) {
return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
}
public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) {
public final boolean matches(T textToSearch, int startIndex, int endIndex) {
return matches(textToSearch, startIndex, endIndex, null);
}
@@ -370,11 +368,11 @@ public abstract class TrieSearch<T> {
* @param callbackParameter Optional parameter passed to the callbacks.
* @return If any pattern matched, and it's callback halted searching.
*/
public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
public boolean matches(T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
}
private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex,
private boolean matches(T textToSearch, int textToSearchLength, int startIndex, int endIndex,
@Nullable Object callbackParameter) {
if (endIndex > textToSearchLength) {
throw new IllegalArgumentException("endIndex: " + endIndex

View File

@@ -5,9 +5,6 @@ import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.spoof.ClientType;
/**
* Settings shared across multiple apps.
* <p>
@@ -31,13 +28,4 @@ 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 EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_61_48, true, parent(SPOOF_VIDEO_STREAMS));
static {
if (SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED) {
Logger.printInfo(() -> "Migrating from iOS Unplugged to iPadOS");
SPOOF_VIDEO_STREAMS_CLIENT_TYPE.save(ClientType.IPADOS);
}
}
}

View File

@@ -31,7 +31,6 @@ public enum ClientType {
"132.0.6808.3",
"1.61.48",
false,
false,
"Android VR 1.61"
),
/**
@@ -50,7 +49,6 @@ public enum ClientType {
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
"107.0.5284.2",
"1.43.32",
ANDROID_VR_1_61_48.requiresAuth,
ANDROID_VR_1_61_48.useAuth,
"Android VR 1.43"
),
@@ -71,7 +69,6 @@ public enum ClientType {
"132.0.6779.0",
"23.47.101",
true,
true,
"Android Creator"
),
/**
@@ -86,7 +83,6 @@ public enum ClientType {
"0.1",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
false,
false,
"visionOS"
),
/**
@@ -111,25 +107,7 @@ public enum ClientType {
"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,
false,
"iPadOS"
),
/**
* Obsolete and broken client. Here only to migrate data.
*/
@Deprecated
IOS_UNPLUGGED(
33,
"IOS_UNPLUGGED",
"Apple",
"iPhone16,2",
"iOS",
"18.2.22C152",
"8.49",
"dummy user-agent",
true,
true,
"iOS TV"
);
/**
@@ -197,12 +175,6 @@ public enum ClientType {
*/
public final String clientVersion;
/**
* If this client requires authentication and does not work
* if logged out or in incognito mode.
*/
public final boolean requiresAuth;
/**
* If the client should use authentication if available.
*/
@@ -227,7 +199,6 @@ public enum ClientType {
@NonNull String buildId,
@NonNull String cronetVersion,
String clientVersion,
boolean requiresAuth,
boolean useAuth,
String friendlyName) {
this.id = id;
@@ -241,7 +212,6 @@ public enum ClientType {
this.buildId = buildId;
this.cronetVersion = cronetVersion;
this.clientVersion = clientVersion;
this.requiresAuth = requiresAuth;
this.useAuth = useAuth;
this.friendlyName = friendlyName;
@@ -267,7 +237,6 @@ public enum ClientType {
String osVersion,
String clientVersion,
String userAgent,
boolean requiresAuth,
boolean useAuth,
String friendlyName) {
this.id = id;
@@ -278,7 +247,6 @@ public enum ClientType {
this.osVersion = osVersion;
this.clientVersion = clientVersion;
this.userAgent = userAgent;
this.requiresAuth = requiresAuth;
this.useAuth = useAuth;
this.friendlyName = friendlyName;
this.packageName = null;

View File

@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@@ -17,14 +18,6 @@ import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
@SuppressWarnings("unused")
public class SpoofVideoStreamsPatch {
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.VISIONOS;
@Nullable
private static volatile AppLanguage languageOverride;
/**
* Domain used for internet connectivity verification.
* It has an empty response body and is only used to check for a 204 response code.
@@ -40,6 +33,13 @@ public class SpoofVideoStreamsPatch {
private static final String INTERNET_CONNECTION_CHECK_URI_STRING = "https://www.google.com/gen_204";
private static final Uri INTERNET_CONNECTION_CHECK_URI = Uri.parse(INTERNET_CONNECTION_CHECK_URI_STRING);
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
@Nullable
private static volatile AppLanguage languageOverride;
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_61_48;
/**
* @return If this patch was included during patching.
*/
@@ -47,8 +47,9 @@ public class SpoofVideoStreamsPatch {
return false; // Modified during patching.
}
public static boolean spoofingToClientWithNoMultiAudioStreams() {
return isPatchIncluded() && BaseSettings.SPOOF_VIDEO_STREAMS.get();
@Nullable
public static AppLanguage getLanguageOverride() {
return languageOverride;
}
/**
@@ -59,9 +60,14 @@ public class SpoofVideoStreamsPatch {
languageOverride = language;
}
@Nullable
public static AppLanguage getLanguageOverride() {
return languageOverride;
public static void setPreferredClient(ClientType client) {
preferredClient = Objects.requireNonNull(client);
}
public static boolean spoofingToClientWithNoMultiAudioStreams() {
return isPatchIncluded()
&& SPOOF_STREAMING_DATA
&& preferredClient != ClientType.IPADOS;
}
/**
@@ -276,8 +282,7 @@ public class SpoofVideoStreamsPatch {
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
// Since all current clients are un-authenticated, this works for all spoof clients.
return BaseSettings.SPOOF_VIDEO_STREAMS.get();
return BaseSettings.SPOOF_VIDEO_STREAMS.get() && !preferredClient.useAuth;
}
}
}

View File

@@ -42,10 +42,10 @@ final class PlayerRoutes {
JSONObject context = new JSONObject();
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
if (language == null || BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32) {
if (language == null || clientType == ANDROID_VR_1_43_32) {
// Force original audio has not overrode the language.
// Or if YT has fallen over to the very last client (VR 1.43), then always
// use the app language because forcing an audio stream of specific languages
// Or if YT has fallen over to the last unauthenticated client (VR 1.43), then
// always use the app language because forcing an audio stream of specific languages
// can sometimes fail so it's better to try and load something rather than nothing.
language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get();
}

View File

@@ -1,5 +1,6 @@
package app.revanced.extension.shared.spoof.requests;
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import androidx.annotation.NonNull;
@@ -13,12 +14,18 @@ import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
@@ -93,6 +100,16 @@ public class StreamingDataRequest {
}
});
/**
* Strings found in the response if the video is a livestream.
*/
private static final ByteTrieSearch liveStreamBufferSearch = new ByteTrieSearch(
convertStringsToBytes(
"yt_live_broadcast",
"yt_premiere_broadcast"
)
);
private static volatile ClientType lastSpoofedClientType;
public static String getLastSpoofedClientName() {
@@ -160,7 +177,7 @@ public class StreamingDataRequest {
}
}
if (!authHeadersIncludes && clientType.requiresAuth) {
if (!authHeadersIncludes && clientType.useAuth) {
Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType
+ " videoId: " + videoId);
return null;
@@ -221,9 +238,13 @@ public class StreamingDataRequest {
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
lastSpoofedClientType = clientType;
if (clientType == ClientType.ANDROID_CREATOR && liveStreamBufferSearch.matches(buffer)) {
Logger.printDebug(() -> "Skipping Android Studio as video is a livestream: " + videoId);
} else {
lastSpoofedClientType = clientType;
return ByteBuffer.wrap(baos.toByteArray());
return ByteBuffer.wrap(baos.toByteArray());
}
}
}
} catch (IOException ex) {

View File

@@ -2,6 +2,19 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:youtube:stub"))
compileOnly(libs.annotation)
implementation("com.github.teamnewpipe:NewPipeExtractor:0.24.8")
implementation("io.reactivex.rxjava3:rxjava:3.1.8")
implementation("com.squareup.okio:okio:3.7.0") // Newer okio use Kotlin 2.0 which Patches does not yet use.
implementation("com.github.TeamNewPipe:nanojson:e9d656ddb49a412a5a0a5d5ef20ca7ef09549996")
implementation("io.reactivex.rxjava3:rxandroid:3.0.2")
}
repositories {
mavenCentral()
google()
maven {
url = uri("https://jitpack.io")
}
}
android {

View File

@@ -10,7 +10,7 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")

View File

@@ -14,7 +14,7 @@ import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ByteTrieSearch;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
/**

View File

@@ -1,6 +1,6 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;

View File

@@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.youtube.ByteTrieSearch;
import app.revanced.extension.shared.ByteTrieSearch;
abstract class FilterGroup<T> {
final static class FilterGroupResult {

View File

@@ -5,9 +5,9 @@ import androidx.annotation.NonNull;
import java.util.*;
import java.util.function.Consumer;
import app.revanced.extension.youtube.ByteTrieSearch;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.youtube.TrieSearch;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.TrieSearch;
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {

View File

@@ -14,9 +14,9 @@ import java.util.concurrent.atomic.AtomicReference;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ByteTrieSearch;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.youtube.TrieSearch;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
import app.revanced.extension.youtube.shared.PlayerType;

View File

@@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;

View File

@@ -8,7 +8,7 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")

View File

@@ -12,7 +12,7 @@ import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.TrieSearch;
import app.revanced.extension.shared.TrieSearch;
/**
* Searches for video id's in the proto buffer of Shorts dislike.

View File

@@ -0,0 +1,695 @@
package app.revanced.extension.youtube.patches.spoof
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebView
import app.revanced.extension.shared.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.core.SingleEmitter
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo
import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import java.io.Closeable
import java.time.Instant
import com.grack.nanojson.JsonObject
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonWriter
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.toByteString
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
import org.schabi.newpipe.extractor.stream.StreamInfo
import java.net.HttpURLConnection
import java.net.URL
// TODO: Remove
fun test(){
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl);
with(StreamInfo.getInfo("https://youtube.com/watch?v=dQw4w9WgXcQ")) {
this.videoStreams.first().content
this.videoOnlyStreams.first().content
this.audioStreams.first().content
this.subtitles.first().isAutoGenerated
}
}
object BuildConfig {
const val DEBUG = true
}
fun parseChallengeData(rawChallengeData: String): String {
val scrambled = JsonParser.array().from(rawChallengeData)
val challengeData = if (scrambled.size > 1 && scrambled.isString(1)) {
val descrambled = descramble(scrambled.getString(1))
JsonParser.array().from(descrambled)
} else {
scrambled.getArray(0)
}
val messageId = challengeData.getString(0)
val interpreterHash = challengeData.getString(3)
val program = challengeData.getString(4)
val globalName = challengeData.getString(5)
val clientExperimentsStateBlob = challengeData.getString(7)
val privateDoNotAccessOrElseSafeScriptWrappedValue = challengeData.getArray(1, null)?.find { it is String }
val privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = challengeData.getArray(2, null)?.find { it is String }
return JsonWriter.string(
JsonObject.builder()
.value("messageId", messageId)
.`object`("interpreterJavascript")
.value("privateDoNotAccessOrElseSafeScriptWrappedValue", privateDoNotAccessOrElseSafeScriptWrappedValue)
.value("privateDoNotAccessOrElseTrustedResourceUrlWrappedValue", privateDoNotAccessOrElseTrustedResourceUrlWrappedValue)
.end()
.value("interpreterHash", interpreterHash)
.value("program", program)
.value("globalName", globalName)
.value("clientExperimentsStateBlob", clientExperimentsStateBlob)
.done()
)
}
/**
* Parses the raw integrity token data obtained from the GenerateIT endpoint to a JavaScript
* `Uint8Array` that can be embedded directly in JavaScript code, and an [Int] representing the
* duration of this token in seconds.
*/
fun parseIntegrityTokenData(rawIntegrityTokenData: String): Pair<String, Long> {
val integrityTokenData = JsonParser.array().from(rawIntegrityTokenData)
return base64ToU8(integrityTokenData.getString(0)) to integrityTokenData.getLong(1)
}
/**
* Converts a string (usually the identifier used as input to `obtainPoToken`) to a JavaScript
* `Uint8Array` that can be embedded directly in JavaScript code.
*/
fun stringToU8(identifier: String): String {
return newUint8Array(identifier.toByteArray())
}
/**
* Takes a poToken encoded as a sequence of bytes represented as integers separated by commas
* (e.g. "97,98,99" would be "abc"), which is the output of `Uint8Array::toString()` in JavaScript,
* and converts it to the specific base64 representation for poTokens.
*/
fun u8ToBase64(poToken: String): String {
return poToken.split(",")
.map { it.toUByte().toByte() }
.toByteArray()
.toByteString()
.base64()
.replace("+", "-")
.replace("/", "_")
}
/**
* Takes the scrambled challenge, decodes it from base64, adds 97 to each byte.
*/
private fun descramble(scrambledChallenge: String): String {
return base64ToByteString(scrambledChallenge)
.map { (it + 97).toByte() }
.toByteArray()
.decodeToString()
}
/**
* Decodes a base64 string encoded in the specific base64 representation used by YouTube, and
* returns a JavaScript `Uint8Array` that can be embedded directly in JavaScript code.
*/
private fun base64ToU8(base64: String): String {
return newUint8Array(base64ToByteString(base64))
}
private fun newUint8Array(contents: ByteArray): String {
return "new Uint8Array([" + contents.joinToString(separator = ",") { it.toUByte().toString() } + "])"
}
/**
* Decodes a base64 string encoded in the specific base64 representation used by YouTube.
*/
private fun base64ToByteString(base64: String): ByteArray {
val base64Mod = base64
.replace('-', '+')
.replace('_', '/')
.replace('.', '=')
return (base64Mod.decodeBase64() ?: throw PoTokenException("Cannot base64 decode"))
.toByteArray()
}
class PoTokenException(message: String) : Exception(message)
// to be thrown if the WebView provided by the system is broken
class BadWebViewException(message: String) : Exception(message)
fun buildExceptionForJsError(error: String): Exception {
return if (error.contains("SyntaxError"))
BadWebViewException(error)
else
PoTokenException(error)
}
/**
* This interface was created to allow for multiple methods to generate poTokens in the future (e.g.
* via WebView and via a local DOM implementation)
*/
interface PoTokenGenerator : Closeable {
/**
* Generates a poToken for the provided identifier, using the `integrityToken` and
* `webPoSignalOutput` previously obtained in the initialization of [PoTokenWebView]. Can be
* called multiple times.
*/
fun generatePoToken(identifier: String): Single<String>
/**
* @return whether the `integrityToken` is expired, in which case all tokens generated by
* [generatePoToken] will be invalid
*/
fun isExpired(): Boolean
interface Factory {
/**
* Initializes a [PoTokenGenerator] by loading the BotGuard VM, running it, and obtaining
* an `integrityToken`. Can then be used multiple times to generate multiple poTokens with
* [generatePoToken].
*
* @param context used e.g. to load the HTML asset or to instantiate a WebView
*/
fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator>
}
}
object PoTokenProviderImpl : PoTokenProvider {
val TAG = PoTokenProviderImpl::class.simpleName
private var webViewBadImpl = false // whether the system has a bad WebView implementation
private object WebPoTokenGenLock
private var webPoTokenVisitorData: String? = null
private var webPoTokenStreamingPot: String? = null
private var webPoTokenGenerator: PoTokenGenerator? = null
override fun getWebClientPoToken(videoId: String): PoTokenResult? {
if (!webViewBadImpl) {
return null
}
try {
return getWebClientPoToken(videoId = videoId, forceRecreate = false)
} catch (e: RuntimeException) {
// RxJava's Single wraps exceptions into RuntimeErrors, so we need to unwrap them here
when (val cause = e.cause) {
is BadWebViewException -> {
Log.e(TAG, "Could not obtain poToken because WebView is broken", e)
webViewBadImpl = true
return null
}
null -> throw e
else -> throw cause // includes PoTokenException
}
}
}
/**
* @param forceRecreate whether to force the recreation of [webPoTokenGenerator], to be used in
* case the current [webPoTokenGenerator] threw an error last time
* [PoTokenGenerator.generatePoToken] was called
*/
private fun getWebClientPoToken(videoId: String, forceRecreate: Boolean): PoTokenResult {
// just a helper class since Kotlin does not have builtin support for 4-tuples
data class Quadruple<T1, T2, T3, T4>(val t1: T1, val t2: T2, val t3: T3, val t4: T4)
val (poTokenGenerator, visitorData, streamingPot, hasBeenRecreated) =
synchronized(WebPoTokenGenLock) {
val shouldRecreate = webPoTokenGenerator == null || forceRecreate ||
webPoTokenGenerator!!.isExpired()
if (shouldRecreate) {
val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient()
innertubeClientRequestInfo.clientInfo.clientVersion =
YoutubeParsingHelper.getClientVersion()
webPoTokenVisitorData = YoutubeParsingHelper.getVisitorDataFromInnertube(
innertubeClientRequestInfo,
NewPipe.getPreferredLocalization(),
NewPipe.getPreferredContentCountry(),
YoutubeParsingHelper.getYouTubeHeaders(),
YoutubeParsingHelper.YOUTUBEI_V1_URL,
null,
false
)
// close the current webPoTokenGenerator on the main thread
webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } }
// create a new webPoTokenGenerator
webPoTokenGenerator = PoTokenWebView
.newPoTokenGenerator(Utils.getContext()).blockingGet()
// The streaming poToken needs to be generated exactly once before generating
// any other (player) tokens.
webPoTokenStreamingPot = webPoTokenGenerator!!
.generatePoToken(webPoTokenVisitorData!!).blockingGet()
}
return@synchronized Quadruple(
webPoTokenGenerator!!,
webPoTokenVisitorData!!,
webPoTokenStreamingPot!!,
shouldRecreate
)
}
val playerPot = try {
// Not using synchronized here, since poTokenGenerator would be able to generate
// multiple poTokens in parallel if needed. The only important thing is for exactly one
// visitorData/streaming poToken to be generated before anything else.
poTokenGenerator.generatePoToken(videoId).blockingGet()
} catch (throwable: Throwable) {
if (hasBeenRecreated) {
// the poTokenGenerator has just been recreated (and possibly this is already the
// second time we try), so there is likely nothing we can do
throw throwable
} else {
// retry, this time recreating the [webPoTokenGenerator] from scratch;
// this might happen for example if NewPipe goes in the background and the WebView
// content is lost
Log.e(TAG, "Failed to obtain poToken, retrying", throwable)
return getWebClientPoToken(videoId = videoId, forceRecreate = true)
}
}
if (BuildConfig.DEBUG) {
Log.d(
TAG,
"poToken for $videoId: playerPot=$playerPot, " +
"streamingPot=$streamingPot, visitor_data=$visitorData"
)
}
return PoTokenResult(visitorData, playerPot, streamingPot)
}
override fun getWebEmbedClientPoToken(videoId: String): PoTokenResult? = null
override fun getAndroidClientPoToken(videoId: String): PoTokenResult? = null
override fun getIosClientPoToken(videoId: String): PoTokenResult? = null
}
class PoTokenWebView private constructor(
context: Context,
// to be used exactly once only during initialization!
private val generatorEmitter: SingleEmitter<PoTokenGenerator>,
) : PoTokenGenerator {
private val webView = WebView(context)
private val disposables = CompositeDisposable() // used only during initialization
private val poTokenEmitters = mutableListOf<Pair<String, SingleEmitter<String>>>()
private lateinit var expirationInstant: Instant
//region Initialization
init {
val webViewSettings = webView.settings
//noinspection SetJavaScriptEnabled we want to use JavaScript!
webViewSettings.javaScriptEnabled = true
webViewSettings.safeBrowsingEnabled = false
webViewSettings.userAgentString = USER_AGENT
webViewSettings.blockNetworkLoads = true // the WebView does not need internet access
// so that we can run async functions and get back the result
webView.addJavascriptInterface(this, JS_INTERFACE)
webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(m: ConsoleMessage): Boolean {
if (m.message().contains("Uncaught")) {
// There should not be any uncaught errors while executing the code, because
// everything that can fail is guarded by try-catch. Therefore, this likely
// indicates that there was a syntax error in the code, i.e. the WebView only
// supports a really old version of JS.
val fmt = "\"${m.message()}\", source: ${m.sourceId()} (${m.lineNumber()})"
val exception = BadWebViewException(fmt)
Log.e(TAG, "This WebView implementation is broken: $fmt")
onInitializationErrorCloseAndCancel(exception)
popAllPoTokenEmitters().forEach { (_, emitter) -> emitter.onError(exception) }
}
return super.onConsoleMessage(m)
}
}
}
/**
* Must be called right after instantiating [PoTokenWebView] to perform the actual
* initialization. This will asynchronously go through all the steps needed to load BotGuard,
* run it, and obtain an `integrityToken`.
*/
private fun loadHtmlAndObtainBotguard(context: Context) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "loadHtmlAndObtainBotguard() called")
}
disposables.add(
Single.fromCallable {
val html = context.assets.open("po_token.html").bufferedReader()
.use { it.readText() }
return@fromCallable html
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ html ->
webView.loadDataWithBaseURL(
"https://www.youtube.com",
html.replaceFirst(
"</script>",
// calls downloadAndRunBotguard() when the page has finished loading
"\n$JS_INTERFACE.downloadAndRunBotguard()</script>"
),
"text/html",
"utf-8",
null,
)
},
this::onInitializationErrorCloseAndCancel
)
)
}
/**
* Called during initialization by the JavaScript snippet appended to the HTML page content in
* [loadHtmlAndObtainBotguard] after the WebView content has been loaded.
*/
@JavascriptInterface
fun downloadAndRunBotguard() {
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadAndRunBotguard() called")
}
makeBotguardServiceRequest(
"https://www.youtube.com/api/jnn/v1/Create",
"[ \"$REQUEST_KEY\" ]",
) { responseBody ->
val parsedChallengeData = parseChallengeData(responseBody)
webView.evaluateJavascript(
"""try {
data = $parsedChallengeData
runBotGuard(data).then(function (result) {
this.webPoSignalOutput = result.webPoSignalOutput
$JS_INTERFACE.onRunBotguardResult(result.botguardResponse)
}, function (error) {
$JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack)
})
} catch (error) {
$JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack)
}""",
null
)
}
}
/**
* Called during initialization by the JavaScript snippets from either
* [downloadAndRunBotguard] or [onRunBotguardResult].
*/
@JavascriptInterface
fun onJsInitializationError(error: String) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Initialization error from JavaScript: $error")
}
onInitializationErrorCloseAndCancel(buildExceptionForJsError(error))
}
/**
* Called during initialization by the JavaScript snippet from [downloadAndRunBotguard] after
* obtaining the BotGuard execution output [botguardResponse].
*/
@JavascriptInterface
fun onRunBotguardResult(botguardResponse: String) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "botguardResponse: $botguardResponse")
}
makeBotguardServiceRequest(
"https://www.youtube.com/api/jnn/v1/GenerateIT",
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]",
) { responseBody ->
if (BuildConfig.DEBUG) {
Log.d(TAG, "GenerateIT response: $responseBody")
}
val (integrityToken, expirationTimeInSeconds) = parseIntegrityTokenData(responseBody)
// leave 10 minutes of margin just to be sure
expirationInstant = Instant.now().plusSeconds(expirationTimeInSeconds - 600)
webView.evaluateJavascript(
"this.integrityToken = $integrityToken"
) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "initialization finished, expiration=${expirationTimeInSeconds}s")
}
generatorEmitter.onSuccess(this)
}
}
}
//endregion
//region Obtaining poTokens
override fun generatePoToken(identifier: String): Single<String> =
Single.create { emitter ->
if (BuildConfig.DEBUG) {
Log.d(TAG, "generatePoToken() called with identifier $identifier")
}
runOnMainThread(emitter) {
addPoTokenEmitter(identifier, emitter)
val u8Identifier = stringToU8(identifier)
webView.evaluateJavascript(
"""try {
identifier = "$identifier"
u8Identifier = $u8Identifier
poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier)
poTokenU8String = ""
for (i = 0; i < poTokenU8.length; i++) {
if (i != 0) poTokenU8String += ","
poTokenU8String += poTokenU8[i]
}
$JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String)
} catch (error) {
$JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack)
}""",
) {}
}
}
/**
* Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the
* JavaScript `obtainPoToken()` function.
*/
@JavascriptInterface
fun onObtainPoTokenError(identifier: String, error: String) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "obtainPoToken error from JavaScript: $error")
}
popPoTokenEmitter(identifier)?.onError(buildExceptionForJsError(error))
}
/**
* Called by the JavaScript snippet from [generatePoToken] with the original identifier and the
* result of the JavaScript `obtainPoToken()` function.
*/
@JavascriptInterface
fun onObtainPoTokenResult(identifier: String, poTokenU8: String) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Generated poToken (before decoding): identifier=$identifier poTokenU8=$poTokenU8")
}
val poToken = try {
u8ToBase64(poTokenU8)
} catch (t: Throwable) {
popPoTokenEmitter(identifier)?.onError(t)
return
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "Generated poToken: identifier=$identifier poToken=$poToken")
}
popPoTokenEmitter(identifier)?.onSuccess(poToken)
}
override fun isExpired(): Boolean {
return Instant.now().isAfter(expirationInstant)
}
//endregion
//region Handling multiple emitters
/**
* Adds the ([identifier], [emitter]) pair to the [poTokenEmitters] list. This makes it so that
* multiple poToken requests can be generated invparallel, and the results will be notified to
* the right emitters.
*/
private fun addPoTokenEmitter(identifier: String, emitter: SingleEmitter<String>) {
synchronized(poTokenEmitters) {
poTokenEmitters.add(Pair(identifier, emitter))
}
}
/**
* Extracts and removes from the [poTokenEmitters] list a [SingleEmitter] based on its
* [identifier]. The emitter is supposed to be used immediately after to either signal a success
* or an error.
*/
private fun popPoTokenEmitter(identifier: String): SingleEmitter<String>? {
return synchronized(poTokenEmitters) {
poTokenEmitters.indexOfFirst { it.first == identifier }.takeIf { it >= 0 }?.let {
poTokenEmitters.removeAt(it).second
}
}
}
/**
* Clears [poTokenEmitters] and returns its previous contents. The emitters are supposed to be
* used immediately after to either signal a success or an error.
*/
private fun popAllPoTokenEmitters(): List<Pair<String, SingleEmitter<String>>> {
return synchronized(poTokenEmitters) {
val result = poTokenEmitters.toList()
poTokenEmitters.clear()
result
}
}
//endregion
//region Utils
/**
* Makes a POST request to [url] with the given [data] by setting the correct headers. Calls
* [onInitializationErrorCloseAndCancel] in case of any network errors and also if the response
* does not have HTTP code 200, therefore this is supposed to be used only during
* initialization. Calls [handleResponseBody] with the response body if the response is
* successful. The request is performed in the background and a disposable is added to
* [disposables].
*/
private fun makeBotguardServiceRequest(
url: String,
data: String,
handleResponseBody: (String) -> Unit,
) {
disposables.add(
Single.fromCallable {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
// headers
connection.setRequestProperty("User-Agent", USER_AGENT)
connection.setRequestProperty("Accept", "application/json")
connection.setRequestProperty("Content-Type", "application/json+protobuf")
connection.setRequestProperty("x-goog-api-key", GOOGLE_API_KEY)
connection.setRequestProperty("x-user-agent", "grpc-web-javascript/0.1")
// body
connection.outputStream.use { os ->
os.writer().write(data);
}
// response
return@fromCallable connection
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ connection ->
val httpCode = connection.responseCode
if (httpCode != 200) {
onInitializationErrorCloseAndCancel(
PoTokenException("Invalid response code: $httpCode")
)
return@subscribe
}
val responseBody = connection.inputStream.bufferedReader().use { it.readText() }
connection.disconnect()
handleResponseBody(responseBody)
},
this::onInitializationErrorCloseAndCancel
)
)
}
/**
* Handles any error happening during initialization, releasing resources and sending the error
* to [generatorEmitter].
*/
private fun onInitializationErrorCloseAndCancel(error: Throwable) {
runOnMainThread(generatorEmitter) {
close()
generatorEmitter.onError(error)
}
}
/**
* Releases all [webView] and [disposables] resources.
*/
override fun close() {
disposables.dispose()
webView.clearHistory()
// clears RAM cache and disk cache (globally for all WebViews)
webView.clearCache(true)
// ensures that the WebView isn't doing anything when destroying it
webView.loadUrl("about:blank")
webView.onPause()
webView.removeAllViews()
webView.destroy()
}
//endregion
companion object : PoTokenGenerator.Factory {
private val TAG = PoTokenWebView::class.simpleName
// Public API key used by BotGuard, which has been got by looking at BotGuard requests
private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" // NOSONAR
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3"
private const val JS_INTERFACE = "PoTokenWebView"
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> =
Single.create { emitter ->
runOnMainThread(emitter) {
val potWv = PoTokenWebView(context, emitter)
potWv.loadHtmlAndObtainBotguard(context)
emitter.setDisposable(potWv.disposables)
}
}
/**
* Runs [runnable] on the main thread using `Handler(Looper.getMainLooper()).post()`, and
* if the `post` fails emits an error on [emitterIfPostFails].
*/
private fun runOnMainThread(
emitterIfPostFails: SingleEmitter<out Any>,
runnable: Runnable,
) {
if (!Handler(Looper.getMainLooper()).post(runnable)) {
emitterIfPostFails.onError(PoTokenException("Could not run on main thread"))
}
}
}
}

View File

@@ -8,9 +8,9 @@ import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
import java.util.List;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofVideoStreamsPatch {
@@ -22,14 +22,13 @@ public class SpoofVideoStreamsPatch {
List<ClientType> availableClients = List.of(
ANDROID_VR_1_61_48,
VISIONOS,
IPADOS,
// Creator must be next to last, because livestreams fetch successfully but don't playback.
ANDROID_CREATOR,
// VR 1.43 must be last as spoof streaming data handles it slightly differently.
ANDROID_VR_1_43_32
ANDROID_VR_1_43_32,
IPADOS
);
StreamingDataRequest.setClientOrderToUse(availableClients,
BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
ClientType client = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setPreferredClient(client);
StreamingDataRequest.setClientOrderToUse(availableClients, client);
}
}

View File

@@ -41,6 +41,7 @@ import app.revanced.extension.shared.settings.LongSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
@@ -356,6 +357,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
"revanced_spoof_device_dimensions_user_dialog_message");
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_61_48, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));

View File

@@ -15,6 +15,7 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
public class SpoofStreamingDataSideEffectsPreference extends Preference {
@@ -69,7 +70,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
}
private void updateUI() {
ClientType clientType = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
ClientType clientType = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
if (currentClientType == clientType) {
return;
}

View File

@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
version = 5.38.0
version = 5.38.1-dev.2

View File

@@ -38,7 +38,7 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
)
},
executeBlock = {
addResources("shared", "misc.spoof.spoofVideoStreamsPatch")
addResources("music", "misc.fix.playback.spoofVideoStreamsPatch")
PreferenceScreen.MISC.addPreferences(
PreferenceScreenPreference(

View File

@@ -89,10 +89,16 @@ Davam et düyməsinə toxun və optimallaşdırma dəyişikliklərin qəbul et."
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string name="revanced_spoof_video_streams_screen_title">Video yayımları saxtalaşdır</string>
<string name="revanced_spoof_video_streams_screen_summary">Oynatma problemlərin önləmək üçün qəbuledici video yayımların saxtalaşdır</string>
<string name="revanced_spoof_video_streams_screen_title">Video yayımların saxtalaşdır</string>
<string name="revanced_spoof_video_streams_screen_summary">Oynatma problemlərin önləmək üçün qəbuledici video yayımların saxtalaşdır</string>
<string name="revanced_spoof_video_streams_title">Video yayımları saxtalaşdır</string>
<string name="revanced_spoof_video_streams_summary_on">"Video yayımları saxtalaşdırılıb
Əgər YouTube Premium istifadəçisisinizsə, bu tənzimlənmə tələb olunmaya bilər"</string>
<string name="revanced_spoof_video_streams_summary_off">"Video yayımları saxtalaşmayıb
Oynatma işləməyə bilər"</string>
<string name="revanced_spoof_video_streams_user_dialog_message">Bu tənzimləməni qapatmaq oynatma problemlərinə səbəb ola bilər.</string>
<string name="revanced_spoof_video_streams_client_type_title">İlkin qəbuledici</string>
</patch>
<patch id="misc.debugging.enableDebuggingPatch">
@@ -713,6 +719,9 @@ Bu seçimi dəyişdirmə işə düşmürsə, Gizli rejimə keçməyə çalışı
<string name="revanced_hide_player_flyout_audio_track_summary_on">Səs axını menyusu gizlidir</string>
<string name="revanced_hide_player_flyout_audio_track_summary_off">Səs axını menyusu göstərilir</string>
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
<string name="revanced_hide_player_flyout_audio_track_not_available">"Səs trek menyusu gizlidir
Səs treki menyusunu göstərmək üçün \"Video yayımları saxtalaşdır\"ı iPadOS-a dəyiş"</string>
<!-- '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">\"VR-da İzləni\" gizlət</string>
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">VR menyusunda izləmə gizlidir</string>
@@ -1541,6 +1550,7 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
<string name="revanced_spoof_video_streams_about_android_title">Android saxtalaşdırma yan təsirləri</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• Səs treki menyusu əlçatmazdır
• Sabit səs səviyyəsi yoxdur"</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video 01:00-da dayana bilər və ya bəzi bölgələrdə mövcud olmaya bilər</string>
<string name="revanced_spoof_video_streams_about_experimental">• Təcrübi qəbuledici və hər vaxt işləməyi dayandıra bilər</string>
<string name="revanced_spoof_video_streams_about_no_av1">• AV1 video kodlayıcı yoxdur</string>
<string name="revanced_spoof_video_streams_about_kids_videos">• Giriş edilməyəndə və ya gizli rejimdə uşaq videoları oynadıla bilməz</string>
@@ -1549,6 +1559,7 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Qəbuledici nerd üçün Statistikada gizlidir</string>
<string name="revanced_spoof_video_streams_language_title">Səs yayım dili</string>
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_language_not_available">Xüsusi səs dilini seçmək üçün \"Orijinal səs dilini zorlanı\" qapat</string>
</patch>
</app>
<app id="music">
@@ -1556,18 +1567,31 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
<string name="revanced_settings_music_screen_0_about_title">Haqqında</string>
<string name="revanced_settings_music_screen_1_ads_title">Reklamlar</string>
<string name="revanced_settings_music_screen_2_general_title">Ümumi</string>
<string name="revanced_settings_music_screen_3_player_title">Oyunçu</string>
<string name="revanced_settings_music_screen_3_player_title">Oynadıcı</string>
<string name="revanced_settings_music_screen_4_misc_title">Çoxvariantlı</string>
</patch>
<patch id="ad.video.hideVideoAdsPatch">
<string name="revanced_music_hide_video_ads_title">Video reklamlarını gizlət</string>
<string name="revanced_music_hide_video_ads_summary_on">Video reklamları gizlidir</string>
<string name="revanced_music_hide_video_ads_summary_off">Video reklamları görünür</string>
</patch>
<patch id="interaction.permanentrepeat.permanentRepeatPatch">
<string name="revanced_music_play_permanent_repeat_title">Kəsintisiz təkrarlamanı aktivləşdir</string>
<string name="revanced_music_play_permanent_repeat_summary_on">Kəsintisiz təkrarlama aktivdir</string>
<string name="revanced_music_play_permanent_repeat_summary_off">Kəsintisiz təkrarlama qapalıdır</string>
</patch>
<patch id="layout.compactheader.hideCategoryBar">
<string name="revanced_music_hide_category_bar_title">Kateqoriya cizgisin gizlət</string>
<string name="revanced_music_hide_category_bar_summary_on">Kateqoriya cizgisi gizlidir</string>
<string name="revanced_music_hide_category_bar_summary_off">Kateqoriya cizgisi görünür</string>
</patch>
<patch id="layout.premium.hideGetPremiumPatch">
<string name="revanced_music_hide_get_premium_label_title"> \'Musiqi Premiumu Əldə et\' etiketini gizlət</string>
<string name="revanced_music_hide_get_premium_label_summary_on">Etiket gizlidir</string>
<string name="revanced_music_hide_get_premium_label_summary_off">Etiket görünür</string>
</patch>
<patch id="layout.upgradebutton.hideUpgradeButtonPatch">
<string name="revanced_music_hide_upgrade_button_title">Təkmilləşdirmə düyməsini gizlət</string>
<string name="revanced_music_hide_upgrade_button_summary_on">Düymə gizlidir</string>
<string name="revanced_music_hide_upgrade_button_summary_off">Düymə görünür</string>
</patch>

View File

@@ -721,7 +721,7 @@ Second \"item\" text"</string>
<string name="revanced_hide_player_flyout_audio_track_summary_on">Κρυμμένο</string>
<string name="revanced_hide_player_flyout_audio_track_summary_off">Εμφανίζεται</string>
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
<string name="revanced_hide_player_flyout_audio_track_not_available">"Το μενού «Κομμάτι ήχου» είναι κρυμμένο
<string name="revanced_hide_player_flyout_audio_track_not_available">"Κρυμμένο
Για να εμφανίζεται το μενού κομματιού ήχου, αλλάξτε την «Παραποίηση ροών βίντεο» σε iPadOS"</string>
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
@@ -1550,7 +1550,7 @@ Second \"item\" text"</string>
<string name="revanced_spoof_video_streams_about_android_title">Παρενέργειες παραποίησης σε Android</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• Το μενού «Κομμάτι ήχου» λείπει
Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη"</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">Το βίντεο ενδέχεται να σταματήσει στο 1:00 ή ενδέχεται να μην είναι διαθέσιμο σε ορισμένες περιοχές</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">Το βίντεο ενδέχεται να σταματήσει στο 1:00 ή να μην είναι διαθέσιμο σε ορισμένες περιοχές</string>
<string name="revanced_spoof_video_streams_about_experimental">• Πειραματικός πελάτης και μπορεί να σταματήσει να λειτουργεί ανά πάσα στιγμή</string>
<string name="revanced_spoof_video_streams_about_no_av1">• Δεν υπάρχει ο κωδικοποιητής βίντεο AV1</string>
<string name="revanced_spoof_video_streams_about_kids_videos">Τα βίντεο για παιδιά ενδέχεται να μην αναπαράγονται αν είστε αποσυνδεδεμένοι ή σε λειτουργία ανώνυμης περιήγησης</string>
@@ -1559,12 +1559,12 @@ Second \"item\" text"</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Το πρόγραμμα πελάτη δεν εμφανίζεται στο μενού «Στατιστικά για σπασίκλες»</string>
<string name="revanced_spoof_video_streams_language_title">Γλώσσα ροής ήχου</string>
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_language_not_available">Για να επιλέξετε μια συγκεκριμένη γλώσσα ήχου, απενεργοποιήστε το \'Επιβολή αρχικής γλώσσας ήχου\'</string>
<string name="revanced_spoof_video_streams_language_not_available">Για να επιλέξετε μια συγκεκριμένη γλώσσα ήχου, απενεργοποιήστε το «Εξαναγκασμός αρχικής γλώσσας ήχου»</string>
</patch>
</app>
<app id="music">
<patch id="misc.settings.settingsPatch">
<string name="revanced_settings_music_screen_0_about_title">Ομάδα προγραμματιστών</string>
<string name="revanced_settings_music_screen_0_about_title">Σχετικά με</string>
<string name="revanced_settings_music_screen_1_ads_title">Διαφημίσεις</string>
<string name="revanced_settings_music_screen_2_general_title">Γενικά</string>
<string name="revanced_settings_music_screen_3_player_title">Οθόνη αναπαραγωγής</string>
@@ -1581,17 +1581,17 @@ Second \"item\" text"</string>
<string name="revanced_music_play_permanent_repeat_summary_off">Η μόνιμη επανάληψη είναι απενεργοποιημένη</string>
</patch>
<patch id="layout.compactheader.hideCategoryBar">
<string name="revanced_music_hide_category_bar_title">Απόκρυψη γραμμής κατηγοριών</string>
<string name="revanced_music_hide_category_bar_summary_on">Η γραμμή κατηγοριών είναι κρυμμένη</string>
<string name="revanced_music_hide_category_bar_summary_off">Η γραμμή κατηγοριών εμφανίζεται</string>
<string name="revanced_music_hide_category_bar_title">Γραμμή κατηγοριών</string>
<string name="revanced_music_hide_category_bar_summary_on">Κρυμμένη</string>
<string name="revanced_music_hide_category_bar_summary_off">Εμφανίζεται</string>
</patch>
<patch id="layout.premium.hideGetPremiumPatch">
<string name="revanced_music_hide_get_premium_label_title">Απόκρυψη ετικέτας «Απόκτηση Music Premium»</string>
<string name="revanced_music_hide_get_premium_label_summary_on">Η ετικέτα είναι κρυμμένη</string>
<string name="revanced_music_hide_get_premium_label_summary_off">Η ετικέτα εμφανίζεται</string>
<string name="revanced_music_hide_get_premium_label_title">Ετικέτα «Αποκτήστε το Music Premium»</string>
<string name="revanced_music_hide_get_premium_label_summary_on">Κρυμμένη</string>
<string name="revanced_music_hide_get_premium_label_summary_off">Εμφανίζεται</string>
</patch>
<patch id="layout.upgradebutton.hideUpgradeButtonPatch">
<string name="revanced_music_hide_upgrade_button_title">Απόκρυψη κουμπιού αναβάθμισης</string>
<string name="revanced_music_hide_upgrade_button_title">Κουμπί «Αναβάθμιση»</string>
<string name="revanced_music_hide_upgrade_button_summary_on">Κρυμμένο</string>
<string name="revanced_music_hide_upgrade_button_summary_off">Εμφανίζεται</string>
</patch>

View File

@@ -94,10 +94,10 @@ Appuyez sur le bouton Continuer et autorisez les modifications."</string>
<string name="revanced_spoof_video_streams_title">Falsifier les flux vidéo</string>
<string name="revanced_spoof_video_streams_summary_on">"Les flux vidéo sont falsifiés
Si vous avez YouTube Premium, il est possible que ce paramètre ne soit pas nécessaire"</string>
Si vous êtes abonné à YouTube Premium, ce paramètre n'est peut-être pas nécessaire"</string>
<string name="revanced_spoof_video_streams_summary_off">"Les flux vidéo ne sont pas falsifiés
La lecture est susceptible de ne pas fonctionner"</string>
Il est possible que la lecture ne fonctionne pas"</string>
<string name="revanced_spoof_video_streams_user_dialog_message">La désactivation de ce paramètre peut entraîner des problèmes de lecture.</string>
<string name="revanced_spoof_video_streams_client_type_title">Client par défaut</string>
</patch>
@@ -1063,9 +1063,9 @@ Cette fonctionnalité fonctionne de manière optimale avec une qualité vidéo d
<string name="revanced_sb_general_min_duration">Durée minimale d\'un segment</string>
<string name="revanced_sb_general_min_duration_sum">Les segments dont la durée est inférieure à cette valeur (en secondes) ne seront ni affichés ni ignorés</string>
<string name="revanced_sb_general_min_duration_invalid">Durée invalide</string>
<string name="revanced_sb_general_uuid">Votre ID privé d\'utilisateur</string>
<string name="revanced_sb_general_uuid">Votre ID d\'utilisateur privé</string>
<string name="revanced_sb_general_uuid_sum">Cet identifiant doit rester confidentiel. Cet identifiant est comme un mot de passe et ne doit être partagé avec qui que ce soit. Si quelqu\'un venait à mettre la main dessus, il pourrait usurper votre identité.</string>
<string name="revanced_sb_general_uuid_invalid">L\'ID privé d\'utilisateur doit comporter au moins 30 caractères</string>
<string name="revanced_sb_general_uuid_invalid">ID d\'utilisateur privé trop court (30+ caractères)</string>
<string name="revanced_sb_general_api_url">Modifier l\'URL de l\'API</string>
<string name="revanced_sb_general_api_url_sum">L\'adresse utilisée par SponsorBlock pour contacter son serveur</string>
<string name="revanced_sb_api_url_reset">URL de l\'API réinitialisée</string>
@@ -1074,11 +1074,11 @@ Cette fonctionnalité fonctionne de manière optimale avec une qualité vidéo d
<string name="revanced_sb_settings_ie">Importer/Exporter les paramètres</string>
<string name="revanced_sb_settings_copy">Copier</string>
<string name="revanced_sb_settings_ie_sum">Votre configuration SponsorBlock au format JSON qui peut être importée/exportée vers ReVanced et les autres plateformes SponsorBlock</string>
<string name="revanced_sb_settings_ie_sum_warning">Votre configuration SponsorBlock au format JSON qui peut être importée/exportée vers ReVanced et d\'autres plateformes SponsorBlock. Votre ID privé d\'utilisateur y est présent, partagez-la avec prudence.</string>
<string name="revanced_sb_settings_ie_sum_warning">Votre configuration SponsorBlock au format JSON qui peut être importée/exportée vers ReVanced et d\'autres plateformes SponsorBlock. Votre ID d\'utilisateur privé y est présent, partagez-la avec prudence.</string>
<string name="revanced_sb_settings_import_successful">Paramètres importés avec succès</string>
<string name="revanced_sb_settings_import_failed">Échec de l\'importation : %s</string>
<string name="revanced_sb_settings_export_failed">Échec de l\'exportation : %s</string>
<string name="revanced_sb_settings_revanced_export_user_id_warning">"Vos paramètres contiennent un ID privé d'utilisateur.
<string name="revanced_sb_settings_revanced_export_user_id_warning">"Vos paramètres contiennent un ID d'utilisateur SponsorBlock privé.
Votre ID d'utilisateur est comme un mot de passe et ne doit jamais être partagé."</string>
<string name="revanced_sb_settings_revanced_export_user_id_warning_dismiss">Ne plus afficher</string>

View File

@@ -721,7 +721,7 @@ Jika mengubah setelan ini tidak berpengaruh, coba beralih ke mode Penyamaran."</
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
<string name="revanced_hide_player_flyout_audio_track_not_available">"Menu trek audio disembunyikan
Untuk menampilkan menu trek audio, ubah 'Spoof stream video' ke iPadOS"</string>
Untuk menampilkan menu trek audio, ubah 'Palsukan aliran video' ke iPadOS"</string>
<!-- '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">Sembunyikan Tonton di VR</string>
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Menu tonton di VR disembunyikan</string>
@@ -1548,9 +1548,9 @@ Mengaktifkan ini dapat membuka kualitas video yang lebih tinggi"</string>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string name="revanced_spoof_video_streams_about_title">Efek samping pemalsuan</string>
<string name="revanced_spoof_video_streams_about_android_title">Efek samping pemalsuan Android</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• Menu trek audio tidak ada
<string name="revanced_spoof_video_streams_about_android_summary">"• Menu trek audio hilang
• Volume stabil tidak tersedia"</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video mungkin berhenti pada pukul 1:00, atau mungkin tidak tersedia di beberapa wilayah</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video mungkin berhenti pada menit 1:00, atau mungkin tidak tersedia di beberapa wilayah</string>
<string name="revanced_spoof_video_streams_about_experimental">• Klien eksperimental dan dapat berhenti berfungsi kapan saja</string>
<string name="revanced_spoof_video_streams_about_no_av1">• Tidak ada codec video AV1</string>
<string name="revanced_spoof_video_streams_about_kids_videos">• Video anak-anak mungkin tidak dapat diputar saat keluar atau dalam mode penyamaran</string>

View File

@@ -1558,7 +1558,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 탭하세요"</string>
<string name="revanced_spoof_video_streams_about_android_title">Android 변경에 따른 부작용</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• 오디오 트랙 메뉴가 표시되지 않습니다
• 안정적인 볼륨을 사용할 수 없습니다"</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• 동영상이 1:00에서 중지되거나 일부 지역에서 제공되지 않을 수 있습니다</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• 동영상이 1:00에 멈출 수 있으며, 일부 지역에서는 이용이 불가능할 수 있습니다</string>
<string name="revanced_spoof_video_streams_about_experimental">• 실험용 클라이언트이며 언제든지 작동이 중단될 수 있습니다</string>
<string name="revanced_spoof_video_streams_about_no_av1">• AV1 코덱이 지원되지 않습니다</string>
<string name="revanced_spoof_video_streams_about_kids_videos">• Kids 동영상은 로그인을 하지 않았거나 시크릿 모드에서는 재생되지 않을 수 있습니다</string>
@@ -1567,7 +1567,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 탭하세요"</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">동영상 스트림을 가져오는 데 사용되는 클라이언트가 전문 통계에서 표시되지 않습니다</string>
<string name="revanced_spoof_video_streams_language_title">오디오 스트림 언어</string>
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_language_not_available">특정 오디오 언어를 선택하려면 \"원본 오디오 언어 강제 적용\"을 끄십시오.</string>
<string name="revanced_spoof_video_streams_language_not_available">특정 오디오 스트림 언어를 선택하려면, \'원본 오디오 스트림 언어 강제로 활성화하기\'를 끄세요</string>
</patch>
</app>
<app id="music">

View File

@@ -89,13 +89,13 @@ Dodirnite dugme „Nastavi” i dozvolite promene optimizacije."</string>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string name="revanced_spoof_video_streams_screen_title">Lažirani video strimovi</string>
<string name="revanced_spoof_video_streams_screen_summary">Lažiranje klijenta video strimova da bi se sprečili problemi sa reprodukcijom</string>
<string name="revanced_spoof_video_streams_screen_title">Lažiranje video tokova</string>
<string name="revanced_spoof_video_streams_screen_summary">Lažirajte video tokove klijenta da biste sprečili probleme sa reprodukcijom.</string>
<string name="revanced_spoof_video_streams_screen_title">Lažirani video strimovi</string>
<string name="revanced_spoof_video_streams_screen_summary">Lažiranje video strimova klijenta da bi se sprečili problemi sa reprodukcijom</string>
<string name="revanced_spoof_video_streams_title">Lažirani video strimovi</string>
<string name="revanced_spoof_video_streams_summary_on">"Video strimovi su lažirani
Ako ste korisnik YouTube Premiuma, ovo podešavanje možda neće biti potrebno"</string>
<string name="revanced_spoof_video_streams_summary_off">"Video tokovi nisu lažirani
<string name="revanced_spoof_video_streams_summary_off">"Video strimovi nisu lažirani
Reprodukcija možda neće raditi"</string>
<string name="revanced_spoof_video_streams_user_dialog_message">Isključivanje ovog podešavanja može izazvati probleme sa reprodukcijom.</string>
@@ -721,7 +721,7 @@ Ako se promena ove opcije ne primeni, pokušajte da pređete u režim bez arhivi
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
<string name="revanced_hide_player_flyout_audio_track_not_available">"Meni „Audio snimak” je skriven
Da biste prikazali meni „Audio snimak”, promenite „Lažirani video strimovi” u iPadOS"</string>
Da biste prikazali meni „Audio snimak”, promenite opciju „Lažirani video strimovi” na iPadOS"</string>
<!-- '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">Sakrij dugme „Gledaj u VR”</string>
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Dugme „Gledaj u VR” je skriveno</string>
@@ -1550,7 +1550,7 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"</string>
<string name="revanced_spoof_video_streams_about_android_title">Neželjeni efekti lažiranja na Android</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• Meni „Audio snimak” nedostaje
• Opcija „Ujednačena jačina zvuka” nije dostupna"</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video se može zaustaviti u 1:00, ili možda neće biti dostupan u nekim regionima</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video se može zaustaviti u 1:00 ili možda neće biti dostupan u nekim regionima</string>
<string name="revanced_spoof_video_streams_about_experimental">• Eksperimentalni klijent i može prestati da radi bilo kada</string>
<string name="revanced_spoof_video_streams_about_no_av1">• Nema video kodeka AV1</string>
<string name="revanced_spoof_video_streams_about_kids_videos">• Videi za decu se možda neće puštati kada ste odjavljeni ili u režimu bez arhiviranja</string>
@@ -1559,12 +1559,12 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Tip klijenta je skriven u „Statistici za znalce”</string>
<string name="revanced_spoof_video_streams_language_title">Jezik audio strima</string>
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_language_not_available">Da biste odabrali određeni audio jezik, isključite \'Prisiljavanje originalnog audio jezika\'</string>
<string name="revanced_spoof_video_streams_language_not_available">Da biste izabrali određeni jezik zvuka, isključite opciju „Prisili originalni jezik zvuka”</string>
</patch>
</app>
<app id="music">
<patch id="misc.settings.settingsPatch">
<string name="revanced_settings_music_screen_0_about_title">O</string>
<string name="revanced_settings_music_screen_0_about_title">O programu</string>
<string name="revanced_settings_music_screen_1_ads_title">Oglasi</string>
<string name="revanced_settings_music_screen_2_general_title">Opšte</string>
<string name="revanced_settings_music_screen_3_player_title">Plejer</string>
@@ -1586,7 +1586,7 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"</string>
<string name="revanced_music_hide_category_bar_summary_off">Traka kategorija je prikazana</string>
</patch>
<patch id="layout.premium.hideGetPremiumPatch">
<string name="revanced_music_hide_get_premium_label_title">Sakrij oznaku „Preuzmi Music Premium”</string>
<string name="revanced_music_hide_get_premium_label_title">Sakrij oznaku „Nabavite Music Premium”</string>
<string name="revanced_music_hide_get_premium_label_summary_on">Oznaka je skrivena</string>
<string name="revanced_music_hide_get_premium_label_summary_off">Oznaka je prikazana</string>
</patch>

View File

@@ -89,13 +89,13 @@ Second \"item\" text"</string>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string name="revanced_spoof_video_streams_screen_title">Лажирани видео стримови</string>
<string name="revanced_spoof_video_streams_screen_summary">Лажирање клијента видео стримова да би се спречили проблеми са репродукцијом</string>
<string name="revanced_spoof_video_streams_screen_title">Фалсификујте видео стримове</string>
<string name="revanced_spoof_video_streams_screen_summary">Фалсификујте клијентске видео стримове да бисте спречили проблеме са репродукцијом</string>
<string name="revanced_spoof_video_streams_screen_title">Лажирани видео стримови</string>
<string name="revanced_spoof_video_streams_screen_summary">Лажирање видео стримова клијента да би се спречили проблеми са репродукцијом</string>
<string name="revanced_spoof_video_streams_title">Лажирани видео стримови</string>
<string name="revanced_spoof_video_streams_summary_on">"Видео стримови су лажирани
Ако сте корисник YouTube Premium-а, ово подешавање можда неће бити потребно"</string>
<string name="revanced_spoof_video_streams_summary_off">"Видео стримови нису фалсификовани
<string name="revanced_spoof_video_streams_summary_off">"Видео стримови нису лажирани
Репродукција можда неће радити"</string>
<string name="revanced_spoof_video_streams_user_dialog_message">Искључивање овог подешавања може изазвати проблеме са репродукцијом.</string>
@@ -721,7 +721,7 @@ Second \"item\" text"</string>
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
<string name="revanced_hide_player_flyout_audio_track_not_available">"Мени „Аудио снимак” је скривен
Да бисте приказали мени „Аудио снимак”, промените „Лажирани видео стримови” на iPadOS"</string>
Да бисте приказали мени „Аудио снимак”, промените опцију „Лажирани видео стримови” на iPadOS"</string>
<!-- '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">Сакриј дугме „Гледај у ВР”</string>
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Дугме „Гледај у ВР” је скривено</string>
@@ -1553,7 +1553,7 @@ Second \"item\" text"</string>
<string name="revanced_spoof_video_streams_about_android_title">Нежељени ефекти лажирања на Android</string>
<string name="revanced_spoof_video_streams_about_android_summary">"• Мени „Аудио снимак” недостаје
• Опција „Уједначена јачина звука” није доступна"</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Видео се може зауставити на 1:00, или можда неће бити доступан у неким регионима</string>
<string name="revanced_spoof_video_streams_about_ipados_summary">• Видео се може зауставити на 1:00 или можда неће бити доступан у неким регионима</string>
<string name="revanced_spoof_video_streams_about_experimental">• Експериментални клијент и може престати да ради било када</string>
<string name="revanced_spoof_video_streams_about_no_av1">• Нема видео кодека AV1</string>
<string name="revanced_spoof_video_streams_about_kids_videos">• Видеи за децу се можда неће пуштати када сте одјављени или у режиму без архивирања</string>
@@ -1562,7 +1562,7 @@ Second \"item\" text"</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Тип клијента је скривен у „Статистици за зналце”</string>
<string name="revanced_spoof_video_streams_language_title">Језик аудио стрима</string>
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_language_not_available">Да бисте изабрали одређени аудио језик, искључите „Принудно коришћење оригиналног аудио језика“</string>
<string name="revanced_spoof_video_streams_language_not_available">Да бисте изабрали одређени језик звука, искључите опцију „Присили оригинални језик звука”</string>
</patch>
</app>
<app id="music">
@@ -1589,7 +1589,7 @@ Second \"item\" text"</string>
<string name="revanced_music_hide_category_bar_summary_off">Трака категорија је приказана</string>
</patch>
<patch id="layout.premium.hideGetPremiumPatch">
<string name="revanced_music_hide_get_premium_label_title">Сакриј ознаку „Преузми Music Premium”</string>
<string name="revanced_music_hide_get_premium_label_title">Сакриј ознаку „Набавите Music Premium”</string>
<string name="revanced_music_hide_get_premium_label_summary_on">Ознака је скривена</string>
<string name="revanced_music_hide_get_premium_label_summary_off">Ознака је приказана</string>
</patch>

View File

@@ -1559,7 +1559,7 @@ Second \"item\" text"</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Тип клієнта приховано у вікні \"Статистика для сисадмінів\"</string>
<string name="revanced_spoof_video_streams_language_title">Мова звукової доріжки</string>
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
<string name="revanced_spoof_video_streams_language_not_available">Щоб вибрати певну аудіомову, вимкніть \"Примусово використовувати оригінальну аудіомову\"</string>
<string name="revanced_spoof_video_streams_language_not_available">Щоб вибрати певну звукову доріжку, вимкніть \"Примусово оригінальна мова звукової доріжки\"</string>
</patch>
</app>
<app id="music">

View File

@@ -121,6 +121,20 @@
<item>ZH</item>
</string-array>
</patch>
</app>
<app id="music">
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string-array name="revanced_spoof_video_streams_client_type_entries">
<item>Android VR</item>
<item>visionOS</item>
</string-array>
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
<item>ANDROID_VR_1_43_32</item>
<item>VISIONOS</item>
</string-array>
</patch>
</app>
<app id="youtube">
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string-array name="revanced_spoof_video_streams_client_type_entries">
<item>Android VR</item>
@@ -133,8 +147,6 @@
<item>IPADOS</item>
</string-array>
</patch>
</app>
<app id="youtube">
<patch id="interaction.swipecontrols.swipeControlsResourcePatch">
<string-array name="revanced_swipe_overlay_style_entries">
<item>@string/revanced_swipe_overlay_style_entry_1</item>