Skip to content

Commit d4af0f1

Browse files
committed
feat(YouTube Music): Add Spoof player parameter patch inotia00/ReVanced_Extended#2832
1 parent 806976b commit d4af0f1

File tree

11 files changed

+469
-17
lines changed

11 files changed

+469
-17
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package app.revanced.extension.music.patches.misc;
2+
3+
import static app.revanced.extension.music.shared.VideoInformation.parameterIsAgeRestricted;
4+
import static app.revanced.extension.music.shared.VideoInformation.parameterIsSample;
5+
6+
import androidx.annotation.GuardedBy;
7+
import androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
10+
import org.apache.commons.lang3.BooleanUtils;
11+
12+
import java.util.Arrays;
13+
import java.util.LinkedHashMap;
14+
import java.util.Map;
15+
16+
import app.revanced.extension.music.settings.Settings;
17+
import app.revanced.extension.music.shared.VideoInformation;
18+
import app.revanced.extension.shared.utils.Logger;
19+
20+
@SuppressWarnings("unused")
21+
public class SpoofPlayerParameterPatch {
22+
/**
23+
* Used in YouTube Music.
24+
*/
25+
private static final boolean SPOOF_PLAYER_PARAMETER = Settings.SPOOF_PLAYER_PARAMETER.get();
26+
27+
/**
28+
* Parameter to fix playback issues.
29+
* Used in YouTube Music Samples.
30+
*/
31+
private static final String PLAYER_PARAMETER_SAMPLES =
32+
"8AEB2AUBogYVAUY4C8W9wrM-FdhjSW4MnCgH44uhkAcI";
33+
34+
/**
35+
* Parameter to fix playback issues.
36+
* Used in YouTube Shorts.
37+
*/
38+
private static final String PLAYER_PARAMETER_SHORTS =
39+
"8AEByAMkuAQ0ogYVAePzwRN3uesV1sPI2x4-GkDYlvqUkAcC";
40+
41+
/**
42+
* On app first start, the first video played usually contains a single non-default window setting value
43+
* and all other subtitle settings for the video are (incorrect) default Samples window settings.
44+
* For this situation, the Samples settings must be replaced.
45+
* <p>
46+
* But some videos use multiple text positions on screen (such as youtu.be/3hW1rMNC89o),
47+
* and by chance many of the subtitles uses window positions that match a default Samples position.
48+
* To handle these videos, selectively allowing the Samples specific window settings to 'pass thru' unchanged,
49+
* but only if the video contains multiple non-default subtitle window positions.
50+
* <p>
51+
* Do not enable 'pass thru mode' until this many non default subtitle settings are observed for a single video.
52+
*/
53+
private static final int NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU = 2;
54+
55+
/**
56+
* The number of non default subtitle settings encountered for the current video.
57+
*/
58+
private static int numberOfNonDefaultSettingsObserved;
59+
60+
@GuardedBy("itself")
61+
private static final Map<String, Boolean> lastVideoIds = new LinkedHashMap<>() {
62+
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5;
63+
64+
@Override
65+
protected boolean removeEldestEntry(Entry eldest) {
66+
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
67+
}
68+
};
69+
70+
/**
71+
* Injection point.
72+
*/
73+
public static String spoofParameter(@NonNull String videoId, @Nullable String parameter) {
74+
if (SPOOF_PLAYER_PARAMETER) {
75+
synchronized (lastVideoIds) {
76+
Boolean isSamples = parameterIsSample(parameter);
77+
if (lastVideoIds.put(videoId, isSamples) == null) {
78+
Logger.printDebug(() -> "New video loaded (videoId: " + videoId + ", isSamples: " + isSamples + ")");
79+
}
80+
}
81+
return parameterIsAgeRestricted(parameter)
82+
? PLAYER_PARAMETER_SHORTS
83+
: PLAYER_PARAMETER_SAMPLES;
84+
}
85+
return parameter;
86+
}
87+
88+
/**
89+
* Injection point. Overrides values passed into SubtitleWindowSettings constructor.
90+
*
91+
* @param ap anchor position. A bitmask with 6 bit fields, that appears to indicate the layout position on screen
92+
* @param ah anchor horizontal. A percentage [0, 100], that appears to be a horizontal text anchor point
93+
* @param av anchor vertical. A percentage [0, 100], that appears to be a vertical text anchor point
94+
* @param vs appears to indicate if subtitles exist, and the value is always true.
95+
* @param sd function is not entirely clear
96+
*/
97+
public static int[] fixSubtitleWindowPosition(int ap, int ah, int av, boolean vs, boolean sd) {
98+
// Videos with custom captions that specify screen positions appear to always have correct screen positions (even with spoofing).
99+
// But for auto generated and most other captions, the spoof incorrectly gives various default Samples caption settings.
100+
// Check for these known default Samples captions parameters, and replace with the known correct values.
101+
//
102+
// If a regular video uses a custom subtitle setting that match a default Samples setting,
103+
// then this will incorrectly replace the setting.
104+
// But, if the video uses multiple subtitles in different screen locations, then detect the non-default values
105+
// and do not replace any window settings for the video (regardless if they match a Samples default).
106+
if (SPOOF_PLAYER_PARAMETER &&
107+
numberOfNonDefaultSettingsObserved < NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU) {
108+
synchronized (lastVideoIds) {
109+
String videoId = VideoInformation.getVideoId();
110+
Boolean isSample = lastVideoIds.get(videoId);
111+
if (BooleanUtils.isFalse(isSample)) {
112+
for (SubtitleWindowReplacementSettings setting : SubtitleWindowReplacementSettings.values()) {
113+
if (setting.match(ap, ah, av, vs, sd)) {
114+
return setting.replacementSetting();
115+
}
116+
}
117+
118+
numberOfNonDefaultSettingsObserved++;
119+
}
120+
}
121+
}
122+
123+
return new int[]{ap, ah, av};
124+
}
125+
126+
/**
127+
* Injection point.
128+
* <p>
129+
* Return false to force disable age restricted playback feature flag.
130+
*/
131+
public static boolean forceDisableAgeRestrictedPlaybackFeatureFlag(boolean original) {
132+
if (SPOOF_PLAYER_PARAMETER) {
133+
return false;
134+
}
135+
return original;
136+
}
137+
138+
/**
139+
* Known incorrect default Samples subtitle parameters, and the corresponding correct (non-Samples) values.
140+
*/
141+
private enum SubtitleWindowReplacementSettings {
142+
DEFAULT_SAMPLES_PARAMETERS_1(10, 50, 0, true, false,
143+
34, 50, 95),
144+
DEFAULT_SAMPLES_PARAMETERS_2(9, 20, 0, true, false,
145+
34, 50, 90),
146+
DEFAULT_SAMPLES_PARAMETERS_3(9, 20, 0, true, true,
147+
33, 20, 100);
148+
149+
// original values
150+
final int ap, ah, av;
151+
final boolean vs, sd;
152+
153+
// replacement int values
154+
final int[] replacement;
155+
156+
SubtitleWindowReplacementSettings(int ap, int ah, int av, boolean vs, boolean sd,
157+
int replacementAp, int replacementAh, int replacementAv) {
158+
this.ap = ap;
159+
this.ah = ah;
160+
this.av = av;
161+
this.vs = vs;
162+
this.sd = sd;
163+
this.replacement = new int[]{replacementAp, replacementAh, replacementAv};
164+
}
165+
166+
boolean match(int ap, int ah, int av, boolean vs, boolean sd) {
167+
return this.ap == ap && this.ah == ah && this.av == av && this.vs == vs && this.sd == sd;
168+
}
169+
170+
int[] replacementSetting() {
171+
return replacement;
172+
}
173+
}
174+
}

extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ public class Settings extends BaseSettings {
190190
public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true);
191191
public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true);
192192
public static final EnumSetting<RedirectType> DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true);
193+
public static final BooleanSetting SPOOF_PLAYER_PARAMETER = new BooleanSetting("revanced_spoof_player_parameter", TRUE, true);
193194
public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false);
194195

195196
// PreferenceScreen: Return YouTube Dislike

extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,25 @@ public final class VideoInformation {
2222
private static final float DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED = 1.0f;
2323
private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2;
2424
private static final String DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING = getString("quality_auto");
25+
/**
26+
* Prefix present in all Age-restricted music player parameters signature.
27+
*/
28+
private static final String AGE_RESTRICTED_PLAYER_PARAMETER = "ygYQ";
29+
/**
30+
* Prefix present in all Sample player parameters signature.
31+
*/
32+
private static final String SAMPLES_PLAYER_PARAMETERS = "8AEB";
33+
2534
@NonNull
2635
private static String videoId = "";
2736

2837
private static long videoLength = 0;
2938
private static long videoTime = -1;
3039

40+
@NonNull
41+
private static volatile String playerResponseVideoId = "";
42+
private static volatile boolean playerResponseVideoIdIsSample;
43+
3144
/**
3245
* The current playback speed
3346
*/
@@ -85,6 +98,65 @@ public static void setVideoId(@NonNull String newlyLoadedVideoId) {
8598
videoId = newlyLoadedVideoId;
8699
}
87100

101+
/**
102+
* Differs from {@link #videoId} as this is the video id for the
103+
* last player response received, which may not be the last video opened.
104+
* <p>
105+
* If Shorts are loading the background, this commonly will be
106+
* different from the Short that is currently on screen.
107+
* <p>
108+
* For most use cases, you should instead use {@link #getVideoId()}.
109+
*
110+
* @return The id of the last video loaded, or an empty string if no videos have been loaded yet.
111+
*/
112+
@NonNull
113+
public static String getPlayerResponseVideoId() {
114+
return playerResponseVideoId;
115+
}
116+
117+
/**
118+
* @return If the last player response video id was a Sample.
119+
*/
120+
public static boolean lastPlayerResponseIsSample() {
121+
return playerResponseVideoIdIsSample;
122+
}
123+
124+
/**
125+
* Injection point. Called off the main thread.
126+
*
127+
* @param videoId The id of the last video loaded.
128+
*/
129+
public static void setPlayerResponseVideoId(@NonNull String videoId) {
130+
if (!playerResponseVideoId.equals(videoId)) {
131+
playerResponseVideoId = videoId;
132+
}
133+
}
134+
135+
/**
136+
* @return If the player parameter is for a Age-restricted video.
137+
*/
138+
public static boolean parameterIsAgeRestricted(@Nullable String parameter) {
139+
return parameter != null && parameter.startsWith(AGE_RESTRICTED_PLAYER_PARAMETER);
140+
}
141+
142+
/**
143+
* @return If the player parameter is for a Sample.
144+
*/
145+
public static boolean parameterIsSample(@Nullable String parameter) {
146+
return parameter != null && parameter.startsWith(SAMPLES_PLAYER_PARAMETERS);
147+
}
148+
149+
/**
150+
* Injection point.
151+
*/
152+
@Nullable
153+
public static String newPlayerResponseParameter(@NonNull String videoId, @Nullable String playerParameter) {
154+
playerResponseVideoIdIsSample = parameterIsSample(playerParameter);
155+
Logger.printDebug(() -> "videoId: " + videoId + ", playerParameter: " + playerParameter);
156+
157+
return playerParameter; // Return the original value since we are observing and not modifying.
158+
}
159+
88160
/**
89161
* Seek on the current video.
90162
* Does not function for playback of Shorts.

patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import app.revanced.patches.music.utils.settings.addSwitchPreference
1414
import app.revanced.patches.music.utils.settings.settingsPatch
1515
import app.revanced.patches.music.video.information.videoIdHook
1616
import app.revanced.patches.music.video.information.videoInformationPatch
17-
import app.revanced.patches.music.video.playerresponse.hookPlayerResponse
17+
import app.revanced.patches.music.video.playerresponse.Hook
18+
import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
1819
import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
1920
import app.revanced.util.findMethodOrThrow
2021
import app.revanced.util.fingerprint.methodOrThrow
@@ -46,7 +47,11 @@ val albumMusicVideoPatch = bytecodePatch(
4647

4748
// region hook player response
4849

49-
hookPlayerResponse("$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V")
50+
addPlayerResponseMethodHook(
51+
Hook.VideoIdAndPlaylistId(
52+
"$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V"
53+
),
54+
)
5055

5156
// endregion
5257

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package app.revanced.patches.music.utils.fix.parameter
2+
3+
import app.revanced.util.fingerprint.legacyFingerprint
4+
import app.revanced.util.or
5+
import com.android.tools.smali.dexlib2.AccessFlags
6+
7+
internal val subtitleWindowFingerprint = legacyFingerprint(
8+
name = "subtitleWindowFingerprint",
9+
returnType = "V",
10+
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
11+
parameters = listOf("I", "I", "I", "Z", "Z"),
12+
strings = listOf("invalid anchorHorizontalPos: %s"),
13+
)
14+
15+
/**
16+
* If this flag is activated, a playback issue occurs in age-restricted videos.
17+
*/
18+
internal const val AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG = 45651506L
19+
20+
internal val ageRestrictedPlaybackFeatureFlagFingerprint = legacyFingerprint(
21+
name = "ageRestrictedPlaybackFeatureFlagFingerprint",
22+
returnType = "Z",
23+
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
24+
parameters = emptyList(),
25+
literals = listOf(AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG),
26+
)

0 commit comments

Comments
 (0)