MediaStreamSelector.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #pragma warning disable CS1591
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using Jellyfin.Database.Implementations.Enums;
  6. using Jellyfin.Extensions;
  7. using MediaBrowser.Model.Entities;
  8. namespace Emby.Server.Implementations.Library
  9. {
  10. public static class MediaStreamSelector
  11. {
  12. public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
  13. {
  14. var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages).ToList();
  15. if (preferDefaultTrack)
  16. {
  17. var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault);
  18. if (defaultStream is not null)
  19. {
  20. return defaultStream.Index;
  21. }
  22. }
  23. return sortedStreams.FirstOrDefault()?.Index;
  24. }
  25. public static int? GetDefaultSubtitleStreamIndex(
  26. IEnumerable<MediaStream> streams,
  27. IReadOnlyList<string> preferredLanguages,
  28. SubtitlePlaybackMode mode,
  29. string audioTrackLanguage)
  30. {
  31. if (mode == SubtitlePlaybackMode.None)
  32. {
  33. return null;
  34. }
  35. // Sort in the following order: Default > No tag > Forced
  36. var sortedStreams = streams
  37. .Where(i => i.Type == MediaStreamType.Subtitle)
  38. .OrderByDescending(x => x.IsExternal)
  39. .ThenByDescending(x => x.IsDefault)
  40. .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
  41. .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
  42. .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
  43. .ThenByDescending(x => x.IsForced)
  44. .ToList();
  45. MediaStream? stream = null;
  46. if (mode == SubtitlePlaybackMode.Default)
  47. {
  48. // Load subtitles according to external, default and forced flags.
  49. stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
  50. }
  51. else if (mode == SubtitlePlaybackMode.Smart)
  52. {
  53. // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
  54. // If no subtitles of preferred language available, use none.
  55. // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
  56. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
  57. {
  58. stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
  59. }
  60. else
  61. {
  62. stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
  63. }
  64. }
  65. else if (mode == SubtitlePlaybackMode.Always)
  66. {
  67. // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
  68. stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
  69. BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
  70. }
  71. else if (mode == SubtitlePlaybackMode.OnlyForced)
  72. {
  73. // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
  74. stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
  75. }
  76. return stream?.Index;
  77. }
  78. private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, IReadOnlyList<string> languagePreferences)
  79. {
  80. // Give some preference to external text subs for better performance
  81. return streams
  82. .Where(i => i.Type == type)
  83. .OrderByDescending(i => GetStreamScore(i, languagePreferences));
  84. }
  85. public static void SetSubtitleStreamScores(
  86. IReadOnlyList<MediaStream> streams,
  87. IReadOnlyList<string> preferredLanguages,
  88. SubtitlePlaybackMode mode,
  89. string audioTrackLanguage)
  90. {
  91. if (mode == SubtitlePlaybackMode.None)
  92. {
  93. return;
  94. }
  95. var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages).ToList();
  96. List<MediaStream>? filteredStreams = null;
  97. if (mode == SubtitlePlaybackMode.Default)
  98. {
  99. // Prefer embedded metadata over smart logic
  100. // Load subtitles according to external, default, and forced flags.
  101. filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
  102. .ToList();
  103. }
  104. else if (mode == SubtitlePlaybackMode.Smart)
  105. {
  106. // Prefer smart logic over embedded metadata
  107. // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
  108. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
  109. {
  110. filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
  111. .ToList();
  112. }
  113. else
  114. {
  115. filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
  116. }
  117. }
  118. else if (mode == SubtitlePlaybackMode.Always)
  119. {
  120. // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
  121. filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
  122. .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
  123. }
  124. else if (mode == SubtitlePlaybackMode.OnlyForced)
  125. {
  126. // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
  127. filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
  128. }
  129. // If filteredStreams is null, initialize it as an empty list to avoid null reference errors
  130. filteredStreams ??= new List<MediaStream>();
  131. foreach (var stream in filteredStreams)
  132. {
  133. stream.Score = GetStreamScore(stream, preferredLanguages);
  134. }
  135. }
  136. private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
  137. {
  138. // If preferredLanguages is empty, treat it as "any language" (wildcard)
  139. return preferredLanguages.Count == 0 ||
  140. preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
  141. }
  142. private static bool IsLanguageUndefined(string language)
  143. {
  144. // Check for null, empty, or known placeholders
  145. return string.IsNullOrEmpty(language) ||
  146. language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
  147. language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
  148. language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
  149. language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
  150. language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
  151. }
  152. private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
  153. {
  154. return sortedStreams
  155. .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
  156. .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
  157. .ThenByDescending(s => IsLanguageUndefined(s.Language))
  158. .ToList();
  159. }
  160. internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
  161. {
  162. var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
  163. var score = index == -1 ? 1 : 101 - index;
  164. score = (score * 10) + (stream.IsForced ? 2 : 1);
  165. score = (score * 10) + (stream.IsDefault ? 2 : 1);
  166. score = (score * 10) + (stream.SupportsExternalStream ? 2 : 1);
  167. score = (score * 10) + (stream.IsTextSubtitleStream ? 2 : 1);
  168. score = (score * 10) + (stream.IsExternal ? 2 : 1);
  169. return score;
  170. }
  171. }
  172. }