Skip to content

Commit 5062dc3

Browse files
authored
Identify audio stream languages (#847)
1 parent f8183ec commit 5062dc3

File tree

13 files changed

+214
-9
lines changed

13 files changed

+214
-9
lines changed

YoutubeExplode.Converter.Tests/GeneralSpecs.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public async Task I_can_download_a_video_as_a_single_mp4_file_with_multiple_stre
9292
var filePath = Path.Combine(dir.Path, "video.mp4");
9393

9494
// Act
95-
var manifest = await youtube.Videos.Streams.GetManifestAsync("9bZkp7q19f0");
95+
var manifest = await youtube.Videos.Streams.GetManifestAsync("ngqcjXfggHQ");
9696

9797
var audioStreamInfos = manifest
9898
.GetAudioOnlyStreams()
@@ -117,6 +117,20 @@ await youtube.Videos.DownloadAsync(
117117
// Assert
118118
MediaFormat.IsMp4File(filePath).Should().BeTrue();
119119

120+
foreach (var streamInfo in audioStreamInfos)
121+
{
122+
if (streamInfo.AudioLanguage is not null)
123+
{
124+
FileEx
125+
.ContainsBytes(
126+
filePath,
127+
Encoding.ASCII.GetBytes(streamInfo.AudioLanguage.Value.Name)
128+
)
129+
.Should()
130+
.BeTrue();
131+
}
132+
}
133+
120134
foreach (var streamInfo in videoStreamInfos)
121135
{
122136
FileEx
@@ -136,7 +150,7 @@ public async Task I_can_download_a_video_as_a_single_webm_file_with_multiple_str
136150
var filePath = Path.Combine(dir.Path, "video.webm");
137151

138152
// Act
139-
var manifest = await youtube.Videos.Streams.GetManifestAsync("9bZkp7q19f0");
153+
var manifest = await youtube.Videos.Streams.GetManifestAsync("ngqcjXfggHQ");
140154

141155
var audioStreamInfos = manifest
142156
.GetAudioOnlyStreams()
@@ -161,6 +175,20 @@ await youtube.Videos.DownloadAsync(
161175
// Assert
162176
MediaFormat.IsWebMFile(filePath).Should().BeTrue();
163177

178+
foreach (var streamInfo in audioStreamInfos)
179+
{
180+
if (streamInfo.AudioLanguage is not null)
181+
{
182+
FileEx
183+
.ContainsBytes(
184+
filePath,
185+
Encoding.ASCII.GetBytes(streamInfo.AudioLanguage.Value.Name)
186+
)
187+
.Should()
188+
.BeTrue();
189+
}
190+
}
191+
164192
foreach (var streamInfo in videoStreamInfos)
165193
{
166194
FileEx

YoutubeExplode.Converter/Converter.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,43 @@ private async ValueTask ProcessAsync(
116116

117117
if (streamInput.Info is IAudioStreamInfo audioStreamInfo)
118118
{
119-
arguments
120-
.Add($"-metadata:s:a:{lastAudioStreamIndex++}")
121-
.Add($"title={audioStreamInfo.Bitrate}");
119+
// Contains language information
120+
if (audioStreamInfo.AudioLanguage is not null)
121+
{
122+
// Language codes can be stored in any format, but most players expect
123+
// three-letter codes, so we'll try to convert to that first.
124+
var languageCode =
125+
audioStreamInfo.AudioLanguage.Value.TryGetThreeLetterCode()
126+
?? audioStreamInfo.AudioLanguage.Value.Code;
127+
128+
arguments
129+
.Add($"-metadata:s:a:{lastAudioStreamIndex}")
130+
.Add($"language={languageCode}")
131+
.Add($"-metadata:s:a:{lastAudioStreamIndex}")
132+
.Add(
133+
$"title={audioStreamInfo.AudioLanguage.Value.Name} | {audioStreamInfo.Bitrate}"
134+
);
135+
}
136+
// Does not contain language information
137+
else
138+
{
139+
arguments
140+
.Add($"-metadata:s:a:{lastAudioStreamIndex}")
141+
.Add($"title={audioStreamInfo.Bitrate}");
142+
}
143+
144+
lastAudioStreamIndex++;
122145
}
123146

124147
if (streamInput.Info is IVideoStreamInfo videoStreamInfo)
125148
{
126149
arguments
127-
.Add($"-metadata:s:v:{lastVideoStreamIndex++}")
150+
.Add($"-metadata:s:v:{lastVideoStreamIndex}")
128151
.Add(
129152
$"title={videoStreamInfo.VideoQuality.Label} | {videoStreamInfo.Bitrate}"
130153
);
154+
155+
lastVideoStreamIndex++;
131156
}
132157
}
133158
}

YoutubeExplode.Tests/StreamSpecs.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Buffers;
23
using System.IO;
34
using System.Linq;
@@ -57,6 +58,61 @@ public async Task I_can_get_the_list_of_available_streams_of_a_video()
5758
.Contain(s => s.VideoQuality.MaxHeight == 144 && !s.VideoQuality.IsHighDefinition);
5859
}
5960

61+
[Fact]
62+
public async Task I_can_get_the_list_of_available_streams_of_a_video_with_multiple_audio_languages()
63+
{
64+
// Arrange
65+
var youtube = new YoutubeClient();
66+
67+
// Act
68+
var manifest = await youtube.Videos.Streams.GetManifestAsync(
69+
VideoIds.WithMultipleAudioLanguages
70+
);
71+
72+
// Assert
73+
manifest.Streams.Should().NotBeEmpty();
74+
75+
manifest
76+
.GetAudioStreams()
77+
.Should()
78+
.Contain(t =>
79+
t.AudioLanguage != null
80+
&& t.AudioLanguage.Value.Code == "en-US"
81+
&& t.AudioLanguage.Value.Name == "English (United States) original"
82+
&& t.IsAudioLanguageDefault == true
83+
);
84+
85+
manifest
86+
.GetAudioStreams()
87+
.Should()
88+
.Contain(t =>
89+
t.AudioLanguage != null
90+
&& t.AudioLanguage.Value.Code == "fr-FR"
91+
&& t.AudioLanguage.Value.Name == "French (France)"
92+
&& t.IsAudioLanguageDefault == false
93+
);
94+
95+
manifest
96+
.GetAudioStreams()
97+
.Should()
98+
.Contain(t =>
99+
t.AudioLanguage != null
100+
&& t.AudioLanguage.Value.Code == "it"
101+
&& t.AudioLanguage.Value.Name == "Italian"
102+
&& t.IsAudioLanguageDefault == false
103+
);
104+
105+
manifest
106+
.GetAudioStreams()
107+
.Should()
108+
.Contain(t =>
109+
t.AudioLanguage != null
110+
&& t.AudioLanguage.Value.Code == "pt-BR"
111+
&& t.AudioLanguage.Value.Name == "Portuguese (Brazil)"
112+
&& t.IsAudioLanguageDefault == false
113+
);
114+
}
115+
60116
[Theory]
61117
[InlineData(VideoIds.Normal)]
62118
[InlineData(VideoIds.Unlisted)]

YoutubeExplode.Tests/TestData/VideoIds.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ internal static class VideoIds
2121
public const string WithHighDynamicRangeStreams = "vX2vsvdq8nw";
2222
public const string WithClosedCaptions = "YltHGKX80Y8";
2323
public const string WithBrokenClosedCaptions = "1VKIIw05JnE";
24+
public const string WithMultipleAudioLanguages = "ngqcjXfggHQ";
2425
}

YoutubeExplode/Bridge/DashManifest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ public class StreamData(XElement content) : IStreamData
7070
[Lazy]
7171
public string? AudioCodec => IsAudioOnly ? (string?)content.Attribute("codecs") : null;
7272

73+
public string? AudioLanguageCode => null;
74+
75+
public string? AudioLanguageName => null;
76+
77+
public bool? IsAudioLanguageDefault => null;
78+
7379
[Lazy]
7480
public string? VideoCodec => IsAudioOnly ? null : (string?)content.Attribute("codecs");
7581

YoutubeExplode/Bridge/IStreamData.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ internal interface IStreamData
1818

1919
string? AudioCodec { get; }
2020

21+
string? AudioLanguageCode { get; }
22+
23+
string? AudioLanguageName { get; }
24+
25+
bool? IsAudioLanguageDefault { get; }
26+
2127
string? VideoCodec { get; }
2228

2329
string? VideoQualityLabel { get; }

YoutubeExplode/Bridge/PlayerResponse.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,28 @@ public class StreamData(JsonElement content) : IStreamData
246246
public string? AudioCodec =>
247247
IsAudioOnly ? Codecs : Codecs?.SubstringAfter(", ").NullIfWhiteSpace();
248248

249+
[Lazy]
250+
public string? AudioLanguageCode =>
251+
content
252+
.GetPropertyOrNull("audioTrack")
253+
?.GetPropertyOrNull("id")
254+
?.GetStringOrNull()
255+
?.SubstringUntil(".");
256+
257+
[Lazy]
258+
public string? AudioLanguageName =>
259+
content
260+
.GetPropertyOrNull("audioTrack")
261+
?.GetPropertyOrNull("displayName")
262+
?.GetStringOrNull();
263+
264+
[Lazy]
265+
public bool? IsAudioLanguageDefault =>
266+
content
267+
.GetPropertyOrNull("audioTrack")
268+
?.GetPropertyOrNull("audioIsDefault")
269+
?.GetBooleanOrNull();
270+
249271
[Lazy]
250272
public string? VideoCodec
251273
{

YoutubeExplode/Videos/ClosedCaptions/Language.cs renamed to YoutubeExplode/Common/Language.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
33

4+
// TODO: breaking change: update the namespace
5+
// ReSharper disable once CheckNamespace
46
namespace YoutubeExplode.Videos.ClosedCaptions;
57

68
/// <summary>

YoutubeExplode/Utils/Extensions/JsonExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ internal static class JsonExtensions
2525
return null;
2626
}
2727

28+
public static bool? GetBooleanOrNull(this JsonElement element) =>
29+
element.ValueKind switch
30+
{
31+
JsonValueKind.True => true,
32+
JsonValueKind.False => false,
33+
_ => null,
34+
};
35+
2836
public static string? GetStringOrNull(this JsonElement element) =>
2937
element.ValueKind == JsonValueKind.String ? element.GetString() : null;
3038

YoutubeExplode/Videos/Streams/AudioOnlyStreamInfo.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics.CodeAnalysis;
2+
using YoutubeExplode.Videos.ClosedCaptions;
23

34
namespace YoutubeExplode.Videos.Streams;
45

@@ -10,7 +11,9 @@ public class AudioOnlyStreamInfo(
1011
Container container,
1112
FileSize size,
1213
Bitrate bitrate,
13-
string audioCodec
14+
string audioCodec,
15+
Language? audioLanguage,
16+
bool? isAudioLanguageDefault
1417
) : IAudioStreamInfo
1518
{
1619
/// <inheritdoc />
@@ -28,7 +31,16 @@ string audioCodec
2831
/// <inheritdoc />
2932
public string AudioCodec { get; } = audioCodec;
3033

34+
/// <inheritdoc />
35+
public Language? AudioLanguage { get; } = audioLanguage;
36+
37+
/// <inheritdoc />
38+
public bool? IsAudioLanguageDefault { get; } = isAudioLanguageDefault;
39+
3140
/// <inheritdoc />
3241
[ExcludeFromCodeCoverage]
33-
public override string ToString() => $"Audio-only ({Container})";
42+
public override string ToString() =>
43+
AudioLanguage is not null
44+
? $"Audio-only ({Container} | {AudioLanguage})"
45+
: $"Audio-only ({Container})";
3446
}

0 commit comments

Comments
 (0)