Skip to content

Commit

Permalink
Identify audio stream languages (#847)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyrrrz authored Dec 2, 2024
1 parent f8183ec commit 5062dc3
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 9 deletions.
32 changes: 30 additions & 2 deletions YoutubeExplode.Converter.Tests/GeneralSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public async Task I_can_download_a_video_as_a_single_mp4_file_with_multiple_stre
var filePath = Path.Combine(dir.Path, "video.mp4");

// Act
var manifest = await youtube.Videos.Streams.GetManifestAsync("9bZkp7q19f0");
var manifest = await youtube.Videos.Streams.GetManifestAsync("ngqcjXfggHQ");

var audioStreamInfos = manifest
.GetAudioOnlyStreams()
Expand All @@ -117,6 +117,20 @@ await youtube.Videos.DownloadAsync(
// Assert
MediaFormat.IsMp4File(filePath).Should().BeTrue();

foreach (var streamInfo in audioStreamInfos)
{
if (streamInfo.AudioLanguage is not null)
{
FileEx
.ContainsBytes(
filePath,
Encoding.ASCII.GetBytes(streamInfo.AudioLanguage.Value.Name)
)
.Should()
.BeTrue();
}
}

foreach (var streamInfo in videoStreamInfos)
{
FileEx
Expand All @@ -136,7 +150,7 @@ public async Task I_can_download_a_video_as_a_single_webm_file_with_multiple_str
var filePath = Path.Combine(dir.Path, "video.webm");

// Act
var manifest = await youtube.Videos.Streams.GetManifestAsync("9bZkp7q19f0");
var manifest = await youtube.Videos.Streams.GetManifestAsync("ngqcjXfggHQ");

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

foreach (var streamInfo in audioStreamInfos)
{
if (streamInfo.AudioLanguage is not null)
{
FileEx
.ContainsBytes(
filePath,
Encoding.ASCII.GetBytes(streamInfo.AudioLanguage.Value.Name)
)
.Should()
.BeTrue();
}
}

foreach (var streamInfo in videoStreamInfos)
{
FileEx
Expand Down
33 changes: 29 additions & 4 deletions YoutubeExplode.Converter/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,43 @@ private async ValueTask ProcessAsync(

if (streamInput.Info is IAudioStreamInfo audioStreamInfo)
{
arguments
.Add($"-metadata:s:a:{lastAudioStreamIndex++}")
.Add($"title={audioStreamInfo.Bitrate}");
// Contains language information
if (audioStreamInfo.AudioLanguage is not null)
{
// Language codes can be stored in any format, but most players expect
// three-letter codes, so we'll try to convert to that first.
var languageCode =
audioStreamInfo.AudioLanguage.Value.TryGetThreeLetterCode()
?? audioStreamInfo.AudioLanguage.Value.Code;

arguments
.Add($"-metadata:s:a:{lastAudioStreamIndex}")
.Add($"language={languageCode}")
.Add($"-metadata:s:a:{lastAudioStreamIndex}")
.Add(
$"title={audioStreamInfo.AudioLanguage.Value.Name} | {audioStreamInfo.Bitrate}"
);
}
// Does not contain language information
else
{
arguments
.Add($"-metadata:s:a:{lastAudioStreamIndex}")
.Add($"title={audioStreamInfo.Bitrate}");
}

lastAudioStreamIndex++;
}

if (streamInput.Info is IVideoStreamInfo videoStreamInfo)
{
arguments
.Add($"-metadata:s:v:{lastVideoStreamIndex++}")
.Add($"-metadata:s:v:{lastVideoStreamIndex}")
.Add(
$"title={videoStreamInfo.VideoQuality.Label} | {videoStreamInfo.Bitrate}"
);

lastVideoStreamIndex++;
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions YoutubeExplode.Tests/StreamSpecs.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Buffers;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -57,6 +58,61 @@ public async Task I_can_get_the_list_of_available_streams_of_a_video()
.Contain(s => s.VideoQuality.MaxHeight == 144 && !s.VideoQuality.IsHighDefinition);
}

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

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

// Assert
manifest.Streams.Should().NotBeEmpty();

manifest
.GetAudioStreams()
.Should()
.Contain(t =>
t.AudioLanguage != null
&& t.AudioLanguage.Value.Code == "en-US"
&& t.AudioLanguage.Value.Name == "English (United States) original"
&& t.IsAudioLanguageDefault == true
);

manifest
.GetAudioStreams()
.Should()
.Contain(t =>
t.AudioLanguage != null
&& t.AudioLanguage.Value.Code == "fr-FR"
&& t.AudioLanguage.Value.Name == "French (France)"
&& t.IsAudioLanguageDefault == false
);

manifest
.GetAudioStreams()
.Should()
.Contain(t =>
t.AudioLanguage != null
&& t.AudioLanguage.Value.Code == "it"
&& t.AudioLanguage.Value.Name == "Italian"
&& t.IsAudioLanguageDefault == false
);

manifest
.GetAudioStreams()
.Should()
.Contain(t =>
t.AudioLanguage != null
&& t.AudioLanguage.Value.Code == "pt-BR"
&& t.AudioLanguage.Value.Name == "Portuguese (Brazil)"
&& t.IsAudioLanguageDefault == false
);
}

[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 @@ -21,4 +21,5 @@ internal static class VideoIds
public const string WithHighDynamicRangeStreams = "vX2vsvdq8nw";
public const string WithClosedCaptions = "YltHGKX80Y8";
public const string WithBrokenClosedCaptions = "1VKIIw05JnE";
public const string WithMultipleAudioLanguages = "ngqcjXfggHQ";
}
6 changes: 6 additions & 0 deletions YoutubeExplode/Bridge/DashManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public class StreamData(XElement content) : IStreamData
[Lazy]
public string? AudioCodec => IsAudioOnly ? (string?)content.Attribute("codecs") : null;

public string? AudioLanguageCode => null;

public string? AudioLanguageName => null;

public bool? IsAudioLanguageDefault => null;

[Lazy]
public string? VideoCodec => IsAudioOnly ? null : (string?)content.Attribute("codecs");

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

string? AudioCodec { get; }

string? AudioLanguageCode { get; }

string? AudioLanguageName { get; }

bool? IsAudioLanguageDefault { get; }

string? VideoCodec { get; }

string? VideoQualityLabel { get; }
Expand Down
22 changes: 22 additions & 0 deletions YoutubeExplode/Bridge/PlayerResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,28 @@ public class StreamData(JsonElement content) : IStreamData
public string? AudioCodec =>
IsAudioOnly ? Codecs : Codecs?.SubstringAfter(", ").NullIfWhiteSpace();

[Lazy]
public string? AudioLanguageCode =>
content
.GetPropertyOrNull("audioTrack")
?.GetPropertyOrNull("id")
?.GetStringOrNull()
?.SubstringUntil(".");

[Lazy]
public string? AudioLanguageName =>
content
.GetPropertyOrNull("audioTrack")
?.GetPropertyOrNull("displayName")
?.GetStringOrNull();

[Lazy]
public bool? IsAudioLanguageDefault =>
content
.GetPropertyOrNull("audioTrack")
?.GetPropertyOrNull("audioIsDefault")
?.GetBooleanOrNull();

[Lazy]
public string? VideoCodec
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;

// TODO: breaking change: update the namespace
// ReSharper disable once CheckNamespace
namespace YoutubeExplode.Videos.ClosedCaptions;

/// <summary>
Expand Down
8 changes: 8 additions & 0 deletions YoutubeExplode/Utils/Extensions/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ internal static class JsonExtensions
return null;
}

public static bool? GetBooleanOrNull(this JsonElement element) =>
element.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => null,
};

public static string? GetStringOrNull(this JsonElement element) =>
element.ValueKind == JsonValueKind.String ? element.GetString() : null;

Expand Down
16 changes: 14 additions & 2 deletions YoutubeExplode/Videos/Streams/AudioOnlyStreamInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using YoutubeExplode.Videos.ClosedCaptions;

namespace YoutubeExplode.Videos.Streams;

Expand All @@ -10,7 +11,9 @@ public class AudioOnlyStreamInfo(
Container container,
FileSize size,
Bitrate bitrate,
string audioCodec
string audioCodec,
Language? audioLanguage,
bool? isAudioLanguageDefault
) : IAudioStreamInfo
{
/// <inheritdoc />
Expand All @@ -28,7 +31,16 @@ string audioCodec
/// <inheritdoc />
public string AudioCodec { get; } = audioCodec;

/// <inheritdoc />
public Language? AudioLanguage { get; } = audioLanguage;

/// <inheritdoc />
public bool? IsAudioLanguageDefault { get; } = isAudioLanguageDefault;

/// <inheritdoc />
[ExcludeFromCodeCoverage]
public override string ToString() => $"Audio-only ({Container})";
public override string ToString() =>
AudioLanguage is not null
? $"Audio-only ({Container} | {AudioLanguage})"
: $"Audio-only ({Container})";
}
18 changes: 18 additions & 0 deletions YoutubeExplode/Videos/Streams/IAudioStreamInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using YoutubeExplode.Videos.ClosedCaptions;

namespace YoutubeExplode.Videos.Streams;

/// <summary>
Expand All @@ -9,4 +11,20 @@ public interface IAudioStreamInfo : IStreamInfo
/// Audio codec.
/// </summary>
string AudioCodec { get; }

/// <summary>
/// Audio language.
/// </summary>
/// <remarks>
/// May be null if the audio stream does not contain language information.
/// </remarks>
Language? AudioLanguage { get; }

/// <summary>
/// Whether the audio stream's language corresponds to the default language of the video.
/// </summary>
/// <remarks>
/// May be null if the audio stream does not contain language information.
/// </remarks>
bool? IsAudioLanguageDefault { get; }
}
9 changes: 9 additions & 0 deletions YoutubeExplode/Videos/Streams/MuxedStreamInfo.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using YoutubeExplode.Common;
using YoutubeExplode.Videos.ClosedCaptions;

namespace YoutubeExplode.Videos.Streams;

Expand All @@ -12,6 +13,8 @@ public class MuxedStreamInfo(
FileSize size,
Bitrate bitrate,
string audioCodec,
Language? audioLanguage,
bool? isAudioLanguageDefault,
string videoCodec,
VideoQuality videoQuality,
Resolution videoResolution
Expand All @@ -32,6 +35,12 @@ Resolution videoResolution
/// <inheritdoc />
public string AudioCodec { get; } = audioCodec;

/// <inheritdoc />
public Language? AudioLanguage { get; } = audioLanguage;

/// <inheritdoc />
public bool? IsAudioLanguageDefault { get; } = isAudioLanguageDefault;

/// <inheritdoc />
public string VideoCodec { get; } = videoCodec;

Expand Down
14 changes: 13 additions & 1 deletion YoutubeExplode/Videos/Streams/StreamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using YoutubeExplode.Exceptions;
using YoutubeExplode.Utils;
using YoutubeExplode.Utils.Extensions;
using YoutubeExplode.Videos.ClosedCaptions;

namespace YoutubeExplode.Videos.Streams;

Expand Down Expand Up @@ -123,6 +124,13 @@ private async IAsyncEnumerable<IStreamInfo> GetStreamInfosAsync(
streamData.Bitrate?.Pipe(s => new Bitrate(s))
?? throw new YoutubeExplodeException("Failed to extract the stream bitrate.");

var audioLanguage = !string.IsNullOrWhiteSpace(streamData.AudioLanguageCode)
? new Language(
streamData.AudioLanguageCode,
streamData.AudioLanguageName ?? streamData.AudioLanguageCode
)
: (Language?)null;

// Muxed or video-only stream
if (!string.IsNullOrWhiteSpace(streamData.VideoCodec))
{
Expand All @@ -146,6 +154,8 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null
new FileSize(contentLength.Value),
bitrate,
streamData.AudioCodec,
audioLanguage,
streamData.IsAudioLanguageDefault,
streamData.VideoCodec,
videoQuality,
videoResolution
Expand Down Expand Up @@ -177,7 +187,9 @@ streamData.VideoWidth is not null && streamData.VideoHeight is not null
container,
new FileSize(contentLength.Value),
bitrate,
streamData.AudioCodec
streamData.AudioCodec,
audioLanguage,
streamData.IsAudioLanguageDefault
);

yield return streamInfo;
Expand Down

0 comments on commit 5062dc3

Please sign in to comment.