diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java new file mode 100644 index 000000000..d5e5b014b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java @@ -0,0 +1,174 @@ +package app.revanced.extension.music.patches.misc; + +import static app.revanced.extension.music.shared.VideoInformation.parameterIsAgeRestricted; +import static app.revanced.extension.music.shared.VideoInformation.parameterIsSample; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class SpoofPlayerParameterPatch { + /** + * Used in YouTube Music. + */ + private static final boolean SPOOF_PLAYER_PARAMETER = Settings.SPOOF_PLAYER_PARAMETER.get(); + + /** + * Parameter to fix playback issues. + * Used in YouTube Music Samples. + */ + private static final String PLAYER_PARAMETER_SAMPLES = + "8AEB2AUBogYVAUY4C8W9wrM-FdhjSW4MnCgH44uhkAcI"; + + /** + * Parameter to fix playback issues. + * Used in YouTube Shorts. + */ + private static final String PLAYER_PARAMETER_SHORTS = + "8AEByAMkuAQ0ogYVAePzwRN3uesV1sPI2x4-GkDYlvqUkAcC"; + + /** + * On app first start, the first video played usually contains a single non-default window setting value + * and all other subtitle settings for the video are (incorrect) default Samples window settings. + * For this situation, the Samples settings must be replaced. + *
+ * But some videos use multiple text positions on screen (such as youtu.be/3hW1rMNC89o), + * and by chance many of the subtitles uses window positions that match a default Samples position. + * To handle these videos, selectively allowing the Samples specific window settings to 'pass thru' unchanged, + * but only if the video contains multiple non-default subtitle window positions. + *
+ * Do not enable 'pass thru mode' until this many non default subtitle settings are observed for a single video.
+ */
+ private static final int NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU = 2;
+
+ /**
+ * The number of non default subtitle settings encountered for the current video.
+ */
+ private static int numberOfNonDefaultSettingsObserved;
+
+ @GuardedBy("itself")
+ private static final Map
+ * Return false to force disable age restricted playback feature flag.
+ */
+ public static boolean forceDisableAgeRestrictedPlaybackFeatureFlag(boolean original) {
+ if (SPOOF_PLAYER_PARAMETER) {
+ return false;
+ }
+ return original;
+ }
+
+ /**
+ * Known incorrect default Samples subtitle parameters, and the corresponding correct (non-Samples) values.
+ */
+ private enum SubtitleWindowReplacementSettings {
+ DEFAULT_SAMPLES_PARAMETERS_1(10, 50, 0, true, false,
+ 34, 50, 95),
+ DEFAULT_SAMPLES_PARAMETERS_2(9, 20, 0, true, false,
+ 34, 50, 90),
+ DEFAULT_SAMPLES_PARAMETERS_3(9, 20, 0, true, true,
+ 33, 20, 100);
+
+ // original values
+ final int ap, ah, av;
+ final boolean vs, sd;
+
+ // replacement int values
+ final int[] replacement;
+
+ SubtitleWindowReplacementSettings(int ap, int ah, int av, boolean vs, boolean sd,
+ int replacementAp, int replacementAh, int replacementAv) {
+ this.ap = ap;
+ this.ah = ah;
+ this.av = av;
+ this.vs = vs;
+ this.sd = sd;
+ this.replacement = new int[]{replacementAp, replacementAh, replacementAv};
+ }
+
+ boolean match(int ap, int ah, int av, boolean vs, boolean sd) {
+ return this.ap == ap && this.ah == ah && this.av == av && this.vs == vs && this.sd == sd;
+ }
+
+ int[] replacementSetting() {
+ return replacement;
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java
index b0b531d8b..75b8a6c61 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java
@@ -190,6 +190,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true);
public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true);
public static final EnumSetting
+ * If Shorts are loading the background, this commonly will be
+ * different from the Short that is currently on screen.
+ *
+ * For most use cases, you should instead use {@link #getVideoId()}.
+ *
+ * @return The id of the last video loaded, or an empty string if no videos have been loaded yet.
+ */
+ @NonNull
+ public static String getPlayerResponseVideoId() {
+ return playerResponseVideoId;
+ }
+
+ /**
+ * @return If the last player response video id was a Sample.
+ */
+ public static boolean lastPlayerResponseIsSample() {
+ return playerResponseVideoIdIsSample;
+ }
+
+ /**
+ * Injection point. Called off the main thread.
+ *
+ * @param videoId The id of the last video loaded.
+ */
+ public static void setPlayerResponseVideoId(@NonNull String videoId) {
+ if (!playerResponseVideoId.equals(videoId)) {
+ playerResponseVideoId = videoId;
+ }
+ }
+
+ /**
+ * @return If the player parameter is for a Age-restricted video.
+ */
+ public static boolean parameterIsAgeRestricted(@Nullable String parameter) {
+ return parameter != null && parameter.startsWith(AGE_RESTRICTED_PLAYER_PARAMETER);
+ }
+
+ /**
+ * @return If the player parameter is for a Sample.
+ */
+ public static boolean parameterIsSample(@Nullable String parameter) {
+ return parameter != null && parameter.startsWith(SAMPLES_PLAYER_PARAMETERS);
+ }
+
+ /**
+ * Injection point.
+ */
+ @Nullable
+ public static String newPlayerResponseParameter(@NonNull String videoId, @Nullable String playerParameter) {
+ playerResponseVideoIdIsSample = parameterIsSample(playerParameter);
+ Logger.printDebug(() -> "videoId: " + videoId + ", playerParameter: " + playerParameter);
+
+ return playerParameter; // Return the original value since we are observing and not modifying.
+ }
+
/**
* Seek on the current video.
* Does not function for playback of Shorts.
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
index f72a914ee..5944b88cd 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
@@ -14,7 +14,8 @@ import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.music.video.information.videoIdHook
import app.revanced.patches.music.video.information.videoInformationPatch
-import app.revanced.patches.music.video.playerresponse.hookPlayerResponse
+import app.revanced.patches.music.video.playerresponse.Hook
+import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.methodOrThrow
@@ -46,7 +47,11 @@ val albumMusicVideoPatch = bytecodePatch(
// region hook player response
- hookPlayerResponse("$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V")
+ addPlayerResponseMethodHook(
+ Hook.VideoIdAndPlaylistId(
+ "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V"
+ ),
+ )
// endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt
new file mode 100644
index 000000000..727df6df9
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt
@@ -0,0 +1,26 @@
+package app.revanced.patches.music.utils.fix.parameter
+
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+
+internal val subtitleWindowFingerprint = legacyFingerprint(
+ name = "subtitleWindowFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
+ parameters = listOf("I", "I", "I", "Z", "Z"),
+ strings = listOf("invalid anchorHorizontalPos: %s"),
+)
+
+/**
+ * If this flag is activated, a playback issue occurs in age-restricted videos.
+ */
+internal const val AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG = 45651506L
+
+internal val ageRestrictedPlaybackFeatureFlagFingerprint = legacyFingerprint(
+ name = "ageRestrictedPlaybackFeatureFlagFingerprint",
+ returnType = "Z",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = emptyList(),
+ literals = listOf(AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG),
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt
new file mode 100644
index 000000000..5e71c60cd
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt
@@ -0,0 +1,82 @@
+package app.revanced.patches.music.utils.fix.parameter
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.music.utils.extension.Constants.MISC_PATH
+import app.revanced.patches.music.utils.patch.PatchList.SPOOF_PLAYER_PARAMETER
+import app.revanced.patches.music.utils.settings.CategoryType
+import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
+import app.revanced.patches.music.utils.settings.addSwitchPreference
+import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.music.video.information.videoInformationPatch
+import app.revanced.patches.music.video.playerresponse.Hook
+import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
+import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
+import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.fingerprint.resolvable
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "$MISC_PATH/SpoofPlayerParameterPatch;"
+
+@Suppress("unused")
+val spoofPlayerParameterPatch = bytecodePatch(
+ SPOOF_PLAYER_PARAMETER.title,
+ SPOOF_PLAYER_PARAMETER.summary
+) {
+ compatibleWith(COMPATIBLE_PACKAGE)
+
+ dependsOn(
+ settingsPatch,
+ videoInformationPatch,
+ playerResponseMethodHookPatch,
+ )
+
+ execute {
+
+ addPlayerResponseMethodHook(
+ Hook.PlayerParameter(
+ "$EXTENSION_CLASS_DESCRIPTOR->spoofParameter(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
+ ),
+ )
+
+ // region fix for subtitles position
+
+ subtitleWindowFingerprint.methodOrThrow().addInstructions(
+ 0,
+ """
+ invoke-static {p1, p2, p3, p4, p5}, $EXTENSION_CLASS_DESCRIPTOR->fixSubtitleWindowPosition(IIIZZ)[I
+ move-result-object v0
+ const/4 v1, 0x0
+ aget p1, v0, v1 # ap, anchor position
+ const/4 v1, 0x1
+ aget p2, v0, v1 # ah, horizontal anchor
+ const/4 v1, 0x2
+ aget p3, v0, v1 # av, vertical anchor
+ """
+ )
+
+ // endregion
+
+ // region fix for feature flags
+
+ if (ageRestrictedPlaybackFeatureFlagFingerprint.resolvable()) {
+ ageRestrictedPlaybackFeatureFlagFingerprint.injectLiteralInstructionBooleanCall(
+ AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG,
+ "$EXTENSION_CLASS_DESCRIPTOR->forceDisableAgeRestrictedPlaybackFeatureFlag(Z)Z"
+ )
+ }
+
+ // endregion
+
+ addSwitchPreference(
+ CategoryType.MISC,
+ "revanced_spoof_player_parameter",
+ "true"
+ )
+
+ updatePatchStatus(SPOOF_PLAYER_PARAMETER)
+
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
index 1b9c330e0..7fb107952 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
@@ -157,6 +157,10 @@ internal enum class PatchList(
"Spoof client",
"Adds options to spoof the client to allow playback."
),
+ SPOOF_PLAYER_PARAMETER(
+ "Spoof player parameter",
+ "Adds options to spoof player parameter to allow playback."
+ ),
TRANSLATIONS_FOR_YOUTUBE_MUSIC(
"Translations for YouTube Music",
"Add translations or remove string resources."
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt
index 275e96b4c..bc4b331aa 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt
@@ -13,6 +13,9 @@ import app.revanced.patches.music.utils.extension.Constants.SHARED_PATH
import app.revanced.patches.music.utils.playbackSpeedFingerprint
import app.revanced.patches.music.utils.playbackSpeedParentFingerprint
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.music.video.playerresponse.Hook
+import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
+import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
import app.revanced.patches.shared.mdxPlayerDirectorSetVideoStageFingerprint
import app.revanced.patches.shared.videoLengthFingerprint
import app.revanced.util.addStaticFieldToExtension
@@ -71,7 +74,10 @@ private var videoTimeConstructorInsertIndex = 2
val videoInformationPatch = bytecodePatch(
description = "videoInformationPatch",
) {
- dependsOn(sharedResourceIdPatch)
+ dependsOn(
+ playerResponseMethodHookPatch,
+ sharedResourceIdPatch
+ )
execute {
fun addSeekInterfaceMethods(
@@ -241,7 +247,18 @@ val videoInformationPatch = bytecodePatch(
* Set current video id
*/
videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V")
-
+ addPlayerResponseMethodHook(
+ Hook.VideoId(
+ "$EXTENSION_CLASS_DESCRIPTOR->setPlayerResponseVideoId(Ljava/lang/String;)V"
+ ),
+ )
+ // Call before any other video id hooks,
+ // so they can use VideoInformation and check if the video id is for a Short.
+ addPlayerResponseMethodHook(
+ Hook.PlayerParameterBeforeVideoId(
+ "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponseParameter(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
+ )
+ )
/**
* Hook current playback speed
*/
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
index 450ce37ea..1208c5e2b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
@@ -11,8 +11,7 @@ private val PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST = listOf(
"[B",
"Ljava/lang/String;", // Player parameters proto buffer.
"Ljava/lang/String;", // PlaylistId.
- "I", // PlaylistIndex.
- "I"
+ "I" // PlaylistIndex.
)
/**
@@ -30,7 +29,7 @@ internal val playerParameterBuilderFingerprint = legacyFingerprint(
return@custom false
}
- val startsWithMethodParameterList = parameterTypes.slice(0..5)
+ val startsWithMethodParameterList = parameterTypes.slice(0..4)
parametersEqual(
PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST,
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
index 8570097d0..85e51a62b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
@@ -1,22 +1,35 @@
package app.revanced.patches.music.video.playerresponse
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
+import app.revanced.patches.music.utils.extension.sharedExtensionPatch
import app.revanced.patches.music.utils.playservice.is_7_03_or_greater
import app.revanced.patches.music.utils.playservice.versionCheckPatch
import app.revanced.util.fingerprint.methodOrThrow
+private val hooks = mutableSetOf