Skip to content

Commit fe168ab

Browse files
authored
Merge pull request #1272 from AudricV/yt_clients_changes_and_potokens_support
[YouTube] Refactor player clients, add support for poTokens, extract visitor data from the service and more
2 parents 186e32c + 96911ae commit fe168ab

8 files changed

Lines changed: 1313 additions & 549 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.schabi.newpipe.extractor.services.youtube;
2+
3+
final class ClientsConstants {
4+
private ClientsConstants() {
5+
}
6+
7+
// Common client fields
8+
9+
static final String DESKTOP_CLIENT_PLATFORM = "DESKTOP";
10+
static final String MOBILE_CLIENT_PLATFORM = "MOBILE";
11+
static final String WATCH_CLIENT_SCREEN = "WATCH";
12+
static final String EMBED_CLIENT_SCREEN = "EMBED";
13+
14+
// WEB (YouTube desktop) client fields
15+
16+
static final String WEB_CLIENT_ID = "1";
17+
static final String WEB_CLIENT_NAME = "WEB";
18+
/**
19+
* The client version for InnerTube requests with the {@code WEB} client, used as the last
20+
* fallback if the extraction of the real one failed.
21+
*/
22+
static final String WEB_HARDCODED_CLIENT_VERSION = "2.20250122.04.00";
23+
24+
// WEB_REMIX (YouTube Music) client fields
25+
26+
static final String WEB_REMIX_CLIENT_ID = "67";
27+
static final String WEB_REMIX_CLIENT_NAME = "WEB_REMIX";
28+
static final String WEB_REMIX_HARDCODED_CLIENT_VERSION = "1.20250122.01.00";
29+
30+
// TVHTML5 (YouTube on TVs and consoles using HTML5) client fields
31+
static final String TVHTML5_CLIENT_ID = "7";
32+
static final String TVHTML5_CLIENT_NAME = "TVHTML5";
33+
static final String TVHTML5_CLIENT_VERSION = "7.20250122.15.00";
34+
static final String TVHTML5_CLIENT_PLATFORM = "GAME_CONSOLE";
35+
static final String TVHTML5_DEVICE_MAKE = "Sony";
36+
static final String TVHTML5_DEVICE_MODEL_AND_OS_NAME = "PlayStation 4";
37+
// CHECKSTYLE:OFF
38+
static final String TVHTML5_USER_AGENT =
39+
"Mozilla/5.0 (PlayStation; PlayStation 4/12.00) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15";
40+
// CHECKSTYLE:ON
41+
42+
// WEB_EMBEDDED_PLAYER (YouTube embeds)
43+
44+
static final String WEB_EMBEDDED_CLIENT_ID = "56";
45+
static final String WEB_EMBEDDED_CLIENT_NAME = "WEB_EMBEDDED_PLAYER";
46+
static final String WEB_EMBEDDED_CLIENT_VERSION = "1.20250121.00.00";
47+
48+
// IOS (iOS YouTube app) client fields
49+
50+
static final String IOS_CLIENT_ID = "5";
51+
static final String IOS_CLIENT_NAME = "IOS";
52+
53+
/**
54+
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
55+
*
56+
* <p>
57+
* It can be extracted by getting the latest release version of the app on
58+
* <a href="https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/">the App
59+
* Store page of the YouTube app</a>, in the {@code What’s New} section.
60+
* </p>
61+
*/
62+
static final String IOS_CLIENT_VERSION = "20.03.02";
63+
64+
/**
65+
* The device machine id for the iPhone 15 Pro Max, used to get 60fps with the {@code iOS}
66+
* client.
67+
*
68+
* <p>
69+
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
70+
* information.
71+
* </p>
72+
*/
73+
static final String IOS_DEVICE_MODEL = "iPhone16,2";
74+
75+
/**
76+
* The iOS version to be used in JSON POST requests, the one of an iPhone 15 Pro Max running
77+
* iOS 18.2.1 with the hardcoded version of the iOS app (for the {@code "osVersion"} field).
78+
*
79+
* <p>
80+
* The value of this field seems to use the following structure:
81+
* "iOS major version.minor version.patch version.build version", where
82+
* "patch version" is equal to 0 if it isn't set
83+
* The build version corresponding to the iOS version used can be found on
84+
* <a href="https://theapplewiki.com/wiki/Firmware/iPhone/18.x#iPhone_15_Pro_Max">
85+
* https://theapplewiki.com/wiki/Firmware/iPhone/18.x#iPhone_15_Pro_Max</a>
86+
* </p>
87+
*
88+
* @see #IOS_USER_AGENT_VERSION
89+
*/
90+
static final String IOS_OS_VERSION = "18.2.1.22C161";
91+
92+
/**
93+
* The iOS version to be used in the HTTP user agent for requests.
94+
*
95+
* <p>
96+
* This should be the same of as {@link #IOS_OS_VERSION}.
97+
* </p>
98+
*
99+
* @see #IOS_OS_VERSION
100+
*/
101+
static final String IOS_USER_AGENT_VERSION = "18_2_1";
102+
103+
// ANDROID (Android YouTube app) client fields
104+
105+
static final String ANDROID_CLIENT_ID = "3";
106+
static final String ANDROID_CLIENT_NAME = "ANDROID";
107+
108+
/**
109+
* The hardcoded client version of the Android app used for InnerTube requests with this
110+
* client.
111+
*
112+
* <p>
113+
* It can be extracted by getting the latest release version of the app in an APK repository
114+
* such as <a href="https://www.apkmirror.com/apk/google-inc/youtube/">APKMirror</a>.
115+
* </p>
116+
*/
117+
static final String ANDROID_CLIENT_VERSION = "19.28.35";
118+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package org.schabi.newpipe.extractor.services.youtube;
2+
3+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_ID;
4+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_NAME;
5+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.ANDROID_CLIENT_VERSION;
6+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.DESKTOP_CLIENT_PLATFORM;
7+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.EMBED_CLIENT_SCREEN;
8+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_CLIENT_ID;
9+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_CLIENT_NAME;
10+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_CLIENT_VERSION;
11+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_DEVICE_MODEL;
12+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.IOS_OS_VERSION;
13+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.MOBILE_CLIENT_PLATFORM;
14+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_CLIENT_ID;
15+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_CLIENT_NAME;
16+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_CLIENT_PLATFORM;
17+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_CLIENT_VERSION;
18+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_DEVICE_MAKE;
19+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.TVHTML5_DEVICE_MODEL_AND_OS_NAME;
20+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WATCH_CLIENT_SCREEN;
21+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_ID;
22+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_CLIENT_NAME;
23+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_EMBEDDED_CLIENT_ID;
24+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_EMBEDDED_CLIENT_NAME;
25+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_HARDCODED_CLIENT_VERSION;
26+
import static org.schabi.newpipe.extractor.services.youtube.ClientsConstants.WEB_REMIX_HARDCODED_CLIENT_VERSION;
27+
28+
import javax.annotation.Nonnull;
29+
import javax.annotation.Nullable;
30+
31+
// TODO: add docs
32+
33+
public final class InnertubeClientRequestInfo {
34+
35+
@Nonnull
36+
public ClientInfo clientInfo;
37+
@Nonnull
38+
public DeviceInfo deviceInfo;
39+
40+
public static final class ClientInfo {
41+
42+
@Nonnull
43+
public String clientName;
44+
@Nonnull
45+
public String clientVersion;
46+
@Nonnull
47+
public String clientScreen;
48+
@Nullable
49+
public String clientId;
50+
@Nullable
51+
public String visitorData;
52+
53+
private ClientInfo(@Nonnull final String clientName,
54+
@Nonnull final String clientVersion,
55+
@Nonnull final String clientScreen,
56+
@Nullable final String clientId,
57+
@Nullable final String visitorData) {
58+
this.clientName = clientName;
59+
this.clientVersion = clientVersion;
60+
this.clientScreen = clientScreen;
61+
this.clientId = clientId;
62+
this.visitorData = visitorData;
63+
}
64+
}
65+
66+
public static final class DeviceInfo {
67+
68+
@Nonnull
69+
public String platform;
70+
@Nullable
71+
public String deviceMake;
72+
@Nullable
73+
public String deviceModel;
74+
@Nullable
75+
public String osName;
76+
@Nullable
77+
public String osVersion;
78+
public int androidSdkVersion;
79+
80+
private DeviceInfo(@Nonnull final String platform,
81+
@Nullable final String deviceMake,
82+
@Nullable final String deviceModel,
83+
@Nullable final String osName,
84+
@Nullable final String osVersion,
85+
final int androidSdkVersion) {
86+
this.platform = platform;
87+
this.deviceMake = deviceMake;
88+
this.deviceModel = deviceModel;
89+
this.osName = osName;
90+
this.osVersion = osVersion;
91+
this.androidSdkVersion = androidSdkVersion;
92+
}
93+
}
94+
95+
private InnertubeClientRequestInfo(@Nonnull final ClientInfo clientInfo,
96+
@Nonnull final DeviceInfo deviceInfo) {
97+
this.clientInfo = clientInfo;
98+
this.deviceInfo = deviceInfo;
99+
}
100+
101+
@Nonnull
102+
public static InnertubeClientRequestInfo ofWebClient() {
103+
return new InnertubeClientRequestInfo(
104+
new InnertubeClientRequestInfo.ClientInfo(
105+
WEB_CLIENT_NAME, WEB_HARDCODED_CLIENT_VERSION, WATCH_CLIENT_SCREEN,
106+
WEB_CLIENT_ID, null),
107+
new InnertubeClientRequestInfo.DeviceInfo(DESKTOP_CLIENT_PLATFORM, null, null,
108+
null, null, -1));
109+
}
110+
111+
@Nonnull
112+
public static InnertubeClientRequestInfo ofWebEmbeddedPlayerClient() {
113+
return new InnertubeClientRequestInfo(
114+
new InnertubeClientRequestInfo.ClientInfo(WEB_EMBEDDED_CLIENT_NAME,
115+
WEB_REMIX_HARDCODED_CLIENT_VERSION, EMBED_CLIENT_SCREEN,
116+
WEB_EMBEDDED_CLIENT_ID, null),
117+
new InnertubeClientRequestInfo.DeviceInfo(DESKTOP_CLIENT_PLATFORM, null, null,
118+
null, null, -1));
119+
}
120+
121+
@Nonnull
122+
public static InnertubeClientRequestInfo ofTvHtml5Client() {
123+
return new InnertubeClientRequestInfo(
124+
new InnertubeClientRequestInfo.ClientInfo(TVHTML5_CLIENT_NAME,
125+
TVHTML5_CLIENT_VERSION, WATCH_CLIENT_SCREEN, TVHTML5_CLIENT_ID, null),
126+
new InnertubeClientRequestInfo.DeviceInfo(TVHTML5_CLIENT_PLATFORM,
127+
TVHTML5_DEVICE_MAKE, TVHTML5_DEVICE_MODEL_AND_OS_NAME,
128+
TVHTML5_DEVICE_MODEL_AND_OS_NAME, "", -1));
129+
}
130+
131+
@Nonnull
132+
public static InnertubeClientRequestInfo ofAndroidClient() {
133+
return new InnertubeClientRequestInfo(
134+
new InnertubeClientRequestInfo.ClientInfo(ANDROID_CLIENT_NAME,
135+
ANDROID_CLIENT_VERSION, WATCH_CLIENT_SCREEN, ANDROID_CLIENT_ID, null),
136+
new InnertubeClientRequestInfo.DeviceInfo(MOBILE_CLIENT_PLATFORM, null, null,
137+
"Android", "15", 35));
138+
}
139+
140+
@Nonnull
141+
public static InnertubeClientRequestInfo ofIosClient() {
142+
return new InnertubeClientRequestInfo(
143+
new InnertubeClientRequestInfo.ClientInfo(IOS_CLIENT_NAME, IOS_CLIENT_VERSION,
144+
WATCH_CLIENT_SCREEN, IOS_CLIENT_ID, null),
145+
new InnertubeClientRequestInfo.DeviceInfo(MOBILE_CLIENT_PLATFORM, "Apple",
146+
IOS_DEVICE_MODEL, "iOS", IOS_OS_VERSION, -1));
147+
}
148+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.schabi.newpipe.extractor.services.youtube;
2+
3+
import javax.annotation.Nullable;
4+
5+
/**
6+
* Interface to provide {@code poToken}s to YouTube player requests.
7+
*
8+
* <p>
9+
* On some major clients, YouTube requires that the integrity of the device passes some checks to
10+
* allow playback.
11+
* </p>
12+
*
13+
* <p>
14+
* These checks involve running codes to verify the integrity and using their result to generate
15+
* one or multiple {@code poToken}(s) (which stands for proof of origin token(s)).
16+
* </p>
17+
*
18+
* <p>
19+
* These tokens may have a role in triggering the sign in requirement.
20+
* </p>
21+
*
22+
* <p>
23+
* If an implementation does not want to return a {@code poToken} for a specific client, it <b>must
24+
* return {@code null}</b>.
25+
* </p>
26+
*
27+
* <p>
28+
* <b>Implementations of this interface are expected to be thread-safe, as they may be accessed by
29+
* multiple threads.</b>
30+
* </p>
31+
*/
32+
public interface PoTokenProvider {
33+
34+
/**
35+
* Get a {@link PoTokenResult} specific to the desktop website, a.k.a. the WEB InnerTube client.
36+
*
37+
* <p>
38+
* To be generated and valid, {@code poToken}s from this client must be generated using Google's
39+
* BotGuard machine, which requires a JavaScript engine with a good DOM implementation. They
40+
* must be added to adaptive/DASH streaming URLs with the {@code pot} parameter.
41+
* </p>
42+
*
43+
* <p>
44+
* Note that YouTube desktop website generates two {@code poToken}s:
45+
* - one for the player requests {@code poToken}s, using the videoId as the minter value;
46+
* - one for the streaming URLs, using a visitor data for logged-out users as the minter value.
47+
* </p>
48+
*
49+
* @return a {@link PoTokenResult} specific to the WEB InnerTube client
50+
*/
51+
@Nullable
52+
PoTokenResult getWebClientPoToken(String videoId);
53+
54+
/**
55+
* Get a {@link PoTokenResult} specific to the web embeds, a.k.a. the WEB_EMBEDDED_PLAYER
56+
* InnerTube client.
57+
*
58+
* <p>
59+
* To be generated and valid, {@code poToken}s from this client must be generated using Google's
60+
* BotGuard machine, which requires a JavaScript engine with a good DOM implementation. They
61+
* should be added to adaptive/DASH streaming URLs with the {@code pot} parameter.
62+
* </p>
63+
*
64+
* <p>
65+
* As of writing, like the YouTube desktop website previously did, it generates only one
66+
* {@code poToken}, sent in player requests and streaming URLs, using a visitor data for
67+
* logged-out users. {@code poToken}s do not seem to be mandatory for now on this client.
68+
* </p>
69+
*
70+
* @return a {@link PoTokenResult} specific to the WEB_EMBEDDED_PLAYER InnerTube client
71+
*/
72+
@Nullable
73+
PoTokenResult getWebEmbedClientPoToken(String videoId);
74+
75+
/**
76+
* Get a {@link PoTokenResult} specific to the Android app, a.k.a. the ANDROID InnerTube client.
77+
*
78+
* <p>
79+
* Implementation details are not known, the app uses DroidGuard, a downloaded native virtual
80+
* machine ran by Google Play Services for which its code is updated pretty frequently.
81+
* </p>
82+
*
83+
* <p>
84+
* As of writing, DroidGuard seem to check for the Android app signature and package ID, as
85+
* non-rooted YouTube patched with reVanced doesn't work without spoofing another InnerTube
86+
* client while the rooted version works without any client spoofing.
87+
* </p>
88+
*
89+
* <p>
90+
* There should be only one {@code poToken} needed for the player requests, it shouldn't be
91+
* required for regular adaptive URLs (i.e. not server adaptive bitrate (SABR) URLs). HLS
92+
* formats returned (only for premieres and running and post-live livestreams) in the client's
93+
* HLS manifest URL should work without {@code poToken}s.
94+
* </p>
95+
*
96+
* @return a {@link PoTokenResult} specific to the ANDROID InnerTube client
97+
*/
98+
@Nullable
99+
PoTokenResult getAndroidClientPoToken(String videoId);
100+
101+
/**
102+
* Get a {@link PoTokenResult} specific to the iOS app, a.k.a. the IOS InnerTube client.
103+
*
104+
* <p>
105+
* Implementation details are not known, the app seem to use something called iosGuard which
106+
* should be similar to Android's DroidGuard. It may rely on Apple's attestation APIs.
107+
* </p>
108+
*
109+
* <p>
110+
* As of writing, there should be only one {@code poToken} needed for the player requests, it
111+
* shouldn't be required for regular adaptive URLs (i.e. not server adaptive bitrate (SABR)
112+
* URLs). HLS formats returned in the client's HLS manifest URL should also work without a
113+
* {@code poToken}.
114+
* </p>
115+
*
116+
* @return a {@link PoTokenResult} specific to the IOS InnerTube client
117+
*/
118+
@Nullable
119+
PoTokenResult getIosClientPoToken(String videoId);
120+
}

0 commit comments

Comments
 (0)