11using System . Net . Http ;
2+ using System . Net . Http . Headers ;
23using System . Threading ;
34using System . Threading . Tasks ;
45using YoutubeExplode . Bridge ;
56using YoutubeExplode . Exceptions ;
67using YoutubeExplode . Utils ;
8+ using YoutubeExplode . Utils . Extensions ;
79
810namespace YoutubeExplode . Videos ;
911
1012internal class VideoController ( HttpClient http )
1113{
14+ private string ? _visitorData ;
15+
1216 protected HttpClient Http { get ; } = http ;
1317
18+ private async ValueTask < string > ResolveVisitorDataAsync (
19+ CancellationToken cancellationToken = default
20+ )
21+ {
22+ if ( ! string . IsNullOrWhiteSpace ( _visitorData ) )
23+ return _visitorData ;
24+
25+ using var request = new HttpRequestMessage (
26+ HttpMethod . Get ,
27+ "https://www.youtube.com/sw.js_data"
28+ ) ;
29+
30+ request . Headers . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
31+
32+ request . Headers . Add (
33+ "User-Agent" ,
34+ "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X; US)"
35+ ) ;
36+
37+ using var response = await Http . SendAsync ( request , cancellationToken ) ;
38+ response . EnsureSuccessStatusCode ( ) ;
39+
40+ // TODO: move this to a bridge wrapper
41+ var jsonString = await response . Content . ReadAsStringAsync ( cancellationToken ) ;
42+ if ( jsonString . StartsWith ( ")]}'" ) )
43+ jsonString = jsonString [ 4 ..] ;
44+
45+ var json = Json . Parse ( jsonString ) ;
46+
47+ // This is just an ordered (but unstructured) blob of data
48+ var value = json [ 0 ] [ 2 ] [ 0 ] [ 0 ] [ 13 ] . GetStringOrNull ( ) ;
49+ if ( string . IsNullOrWhiteSpace ( value ) )
50+ {
51+ throw new YoutubeExplodeException ( "Failed to resolve visitor data." ) ;
52+ }
53+
54+ return _visitorData = value ;
55+ }
56+
1457 public async ValueTask < VideoWatchPage > GetVideoWatchPageAsync (
1558 VideoId videoId ,
1659 CancellationToken cancellationToken = default
@@ -47,6 +90,8 @@ public async ValueTask<PlayerResponse> GetPlayerResponseAsync(
4790 CancellationToken cancellationToken = default
4891 )
4992 {
93+ var visitorData = await ResolveVisitorDataAsync ( cancellationToken ) ;
94+
5095 // The most optimal client to impersonate is any mobile client, because they
5196 // don't require signature deciphering (for both normal and n-parameter signatures).
5297 // However, we can't use the ANDROID client because it has a limitation, preventing it
@@ -76,7 +121,7 @@ public async ValueTask<PlayerResponse> GetPlayerResponseAsync(
76121 "platform": "MOBILE",
77122 "osName": "IOS",
78123 "osVersion": "18.1.0.22B83",
79- "visitorData": {{ Json . Serialize ( await VisitorData . ExtractFromYoutube ( Http ) ) }} ,
124+ "visitorData": {{ Json . Serialize ( visitorData ) }} ,
80125 "hl": "en",
81126 "gl": "US",
82127 "utcOffsetMinutes": 0
@@ -112,6 +157,8 @@ public async ValueTask<PlayerResponse> GetPlayerResponseAsync(
112157 CancellationToken cancellationToken = default
113158 )
114159 {
160+ var visitorData = await ResolveVisitorDataAsync ( cancellationToken ) ;
161+
115162 // The only client that can handle age-restricted videos without authentication is the
116163 // TVHTML5_SIMPLY_EMBEDDED_PLAYER client.
117164 // This client does require signature deciphering, so we only use it as a fallback.
@@ -129,6 +176,7 @@ public async ValueTask<PlayerResponse> GetPlayerResponseAsync(
129176 "client": {
130177 "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
131178 "clientVersion": "2.0",
179+ "visitorData": {{ Json . Serialize ( visitorData ) }} ,
132180 "hl": "en",
133181 "gl": "US",
134182 "utcOffsetMinutes": 0
0 commit comments