Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ae347a7
Initial plan
Copilot Apr 21, 2026
9977318
Add IsUpscaled property to VideoQuality to detect AI-upscaled streams
Copilot Apr 21, 2026
16096f9
asd
Tyrrrz Apr 21, 2026
5f7bbee
zxc
Tyrrrz Apr 21, 2026
0e150f2
Fix IsUpscaled detection: use xtags protobuf "sr" key instead of qual…
Copilot Apr 21, 2026
fa0fe12
Address review: specific FormatException catch, add XML doc to 3-para…
Copilot Apr 21, 2026
d83b7ad
Move IsUpscaled from VideoQuality to IVideoStreamInfo and its impleme…
Copilot Apr 21, 2026
33d426b
Move IsVideoUpscaled to after VideoFramerate in IStreamData and imple…
Copilot Apr 21, 2026
8852ed6
Refactor IsVideoUpscaled: use proper protobuf map parser that validat…
Copilot Apr 21, 2026
22d6ef4
Extract protobuf parsing into Utils.Protobuf helper class with Deseri…
Copilot Apr 21, 2026
71f0907
Simplify Protobuf.Deserialize to string-only (map<string,string> is t…
Copilot Apr 21, 2026
5ec2327
refactor: rename Deserialize to TryDeserialize, add base64 overload, …
Copilot Apr 22, 2026
d346d11
refactor: add IsLenField/TryReadString helpers, rename IsUpscaled to …
Copilot Apr 22, 2026
d4bcbc1
revert: restore VideoQuality.FromItag to use 2-param constructor (no …
Copilot Apr 22, 2026
f4ec180
Update Protobuf.cs
Tyrrrz Apr 22, 2026
95a43d5
reorder: move IsVideoUpscaled after VideoWidth/VideoHeight in IStream…
Copilot Apr 22, 2026
9ed7b5c
Update PlayerResponse.cs
Tyrrrz Apr 22, 2026
aa6032f
refactor: rename TryDeserialize→TryDeserializeMap; use nullable retur…
Copilot Apr 22, 2026
3a76e4e
refactor: use var everywhere in Protobuf.cs
Copilot Apr 22, 2026
ff35cf0
Update Protobuf.cs
Tyrrrz Apr 22, 2026
94539a8
Add backwards-compatible constructor overloads to VideoOnlyStreamInfo…
Copilot Apr 22, 2026
53207b7
Update YoutubeExplode/Utils/Protobuf.cs
Tyrrrz Apr 22, 2026
bccda04
Update Protobuf.cs
Tyrrrz Apr 22, 2026
f55b58a
asd
Tyrrrz Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions YoutubeExplode.Tests/StreamSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ public async Task I_can_get_the_list_of_available_streams_of_a_video_with_multip
);
}

[Fact]
public async Task I_can_get_the_list_of_available_streams_of_a_video_with_upscaled_streams()
{
// Arrange
using var youtube = new YoutubeClient();

// Act
var manifest = await youtube.Videos.Streams.GetManifestAsync(VideoIds.WithUpscaledStreams);

// Assert
manifest.Streams.Should().NotBeEmpty();
manifest.GetVideoStreams().Should().Contain(s => s.IsUpscaled);
manifest.GetVideoStreams().Should().Contain(s => !s.IsUpscaled);
}

[Theory]
[InlineData(VideoIds.Normal)]
[InlineData(VideoIds.Unlisted)]
Expand Down
1 change: 1 addition & 0 deletions YoutubeExplode.Tests/TestData/VideoIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ internal static class VideoIds
public const string WithClosedCaptions = "YltHGKX80Y8";
public const string WithBrokenClosedCaptions = "1VKIIw05JnE";
public const string WithMultipleAudioLanguages = "ngqcjXfggHQ";
public const string WithUpscaledStreams = "IFACrIx5SZ0";
}
2 changes: 2 additions & 0 deletions YoutubeExplode/Bridge/DashManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class StreamData(XElement content) : IStreamData

public string? VideoQualityLabel => null;

public bool IsVideoUpscaled => false;
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated

[Lazy]
public int? VideoWidth => (int?)content.Attribute("width");

Expand Down
2 changes: 2 additions & 0 deletions YoutubeExplode/Bridge/IStreamData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ internal interface IStreamData

string? VideoQualityLabel { get; }

bool IsVideoUpscaled { get; }

int? VideoWidth { get; }

int? VideoHeight { get; }
Expand Down
36 changes: 36 additions & 0 deletions YoutubeExplode/Bridge/PlayerResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,42 @@ public string? VideoCodec
public string? VideoQualityLabel =>
content.GetPropertyOrNull("qualityLabel")?.GetStringOrNull();

[Lazy]
public bool IsVideoUpscaled
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
{
get
{
var xtags = content.GetPropertyOrNull("xtags")?.GetStringOrNull();
if (string.IsNullOrEmpty(xtags))
return false;

try
{
// xtags is a base64-encoded protobuf map<string, string>.
// Streams upscaled with YouTube's Super Resolution feature have
// the map entry {"sr": "1"}, whose key is encoded in the binary as:
// field 1 LEN (0x0a), length 2 (0x02), 's' (0x73), 'r' (0x72)
var bytes = Convert.FromBase64String(xtags);
for (var i = 0; i <= bytes.Length - 4; i++)
{
if (
bytes[i] == 0x0a
&& bytes[i + 1] == 0x02
&& bytes[i + 2] == 0x73
&& bytes[i + 3] == 0x72
)
return true;
}
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
}
catch (FormatException)
{
// Ignore invalid base64 encoding
}

return false;
}
}

[Lazy]
public int? VideoWidth => content.GetPropertyOrNull("width")?.GetInt32OrNull();

Expand Down
5 changes: 5 additions & 0 deletions YoutubeExplode/Videos/Streams/IVideoStreamInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public interface IVideoStreamInfo : IStreamInfo
/// Video resolution.
/// </summary>
Resolution VideoResolution { get; }

/// <summary>
/// Whether this stream was produced by YouTube's Super Resolution (AI upscaling) feature.
/// </summary>
bool IsUpscaled { get; }
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
}

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion YoutubeExplode/Videos/Streams/MuxedStreamInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public class MuxedStreamInfo(
bool? isAudioLanguageDefault,
string videoCodec,
VideoQuality videoQuality,
Resolution videoResolution
Resolution videoResolution,
bool isUpscaled

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename all instances to IsVideoUpscaled @copilot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d346d11. Renamed IsUpscaledIsVideoUpscaled in IVideoStreamInfo, VideoOnlyStreamInfo, MuxedStreamInfo (both the constructor parameter and the public property), and the test assertions.

) : IAudioStreamInfo, IVideoStreamInfo
{

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding isUpscaled as a new required parameter changes the public constructor signature and is a breaking change for consumers that instantiate MuxedStreamInfo directly. If you want to preserve compatibility, consider a default value (= false) and/or an overload without this parameter.

Suggested change
{
{
public MuxedStreamInfo(
string url,
Container container,
FileSize size,
Bitrate bitrate,
string audioCodec,
Language? audioLanguage,
bool? isAudioLanguageDefault,
string videoCodec,
VideoQuality videoQuality,
Resolution videoResolution
)
: this(
url,
container,
size,
bitrate,
audioCodec,
audioLanguage,
isAudioLanguageDefault,
videoCodec,
videoQuality,
videoResolution,
false
) { }

Copilot uses AI. Check for mistakes.
/// <inheritdoc />
Expand Down Expand Up @@ -50,6 +51,9 @@ Resolution videoResolution
/// <inheritdoc />
public Resolution VideoResolution { get; } = videoResolution;

/// <inheritdoc />
public bool IsUpscaled { get; } = isUpscaled;

/// <inheritdoc />
[ExcludeFromCodeCoverage]
public override string ToString() => $"Muxed ({VideoQuality} | {Container})";
Expand Down
6 changes: 4 additions & 2 deletions YoutubeExplode/Videos/Streams/StreamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null
streamData.IsAudioLanguageDefault,
streamData.VideoCodec,
videoQuality,
videoResolution
videoResolution,
streamData.IsVideoUpscaled
);

yield return streamInfo;
Expand All @@ -173,7 +174,8 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null
bitrate,
streamData.VideoCodec,
videoQuality,
videoResolution
videoResolution,
streamData.IsVideoUpscaled
);

yield return streamInfo;
Expand Down
6 changes: 5 additions & 1 deletion YoutubeExplode/Videos/Streams/VideoOnlyStreamInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public class VideoOnlyStreamInfo(
Bitrate bitrate,
string videoCodec,
VideoQuality videoQuality,
Resolution videoResolution
Resolution videoResolution,
bool isUpscaled

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding isUpscaled as a new required parameter changes the public constructor signature and is a breaking change for consumers that instantiate VideoOnlyStreamInfo directly. If backwards compatibility matters, consider giving the parameter a default (= false) and/or adding an overload that omits it.

Suggested change
bool isUpscaled
bool isUpscaled = false

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot let's add a constructor overload that sets this to false for backwars compat.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 94539a8. Added backwards-compatible overloads to both VideoOnlyStreamInfo and MuxedStreamInfo that omit isVideoUpscaled and delegate to the primary constructor with false.

) : IVideoStreamInfo
{
/// <inheritdoc />
Expand All @@ -37,6 +38,9 @@ Resolution videoResolution
/// <inheritdoc />
public Resolution VideoResolution { get; } = videoResolution;

/// <inheritdoc />
public bool IsUpscaled { get; } = isUpscaled;

/// <inheritdoc />
[ExcludeFromCodeCoverage]
public override string ToString() => $"Video-only ({VideoQuality} | {Container})";
Expand Down
2 changes: 1 addition & 1 deletion YoutubeExplode/Videos/Streams/VideoQuality.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ internal static VideoQuality FromItag(int itag, int framerate)
_ => throw new ArgumentException($"Unrecognized itag '{itag}'.", nameof(itag)),
};

return new VideoQuality(maxHeight, framerate);
return new VideoQuality(FormatLabel(maxHeight, framerate), maxHeight, framerate);
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
}
}

Expand Down