diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 951d523f3e..bcab10bcb6 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -1,7 +1,10 @@
#pragma warning disable CA1819 // XML serialization handles collections improperly, so we need to use arrays
#nullable disable
+using System;
using System.ComponentModel;
+using System.Text.Json.Serialization;
+using System.Xml.Serialization;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Configuration;
@@ -234,8 +237,27 @@ public class EncodingOptions
///
/// Gets or sets the encoder preset.
///
+ [XmlIgnore]
public EncoderPreset EncoderPreset { get; set; }
+ ///
+ /// Gets or sets the encoder preset as it is stored in the configuration file. Files written before the preset
+ /// list was validated can carry an empty element, which has to read back as the default rather than throw and
+ /// take the rest of the configuration down with it.
+ ///
+ [JsonIgnore]
+ [XmlElement(nameof(EncoderPreset))]
+ public string EncoderPresetXml
+ {
+ get => EncoderPreset.ToString();
+
+ // TryParse also accepts a number, which would yield a preset value nothing maps to, so the result has to be
+ // a declared one.
+ set => EncoderPreset = Enum.TryParse(value, true, out var encoderPreset) && Enum.IsDefined(encoderPreset)
+ ? encoderPreset
+ : EncoderPreset.auto;
+ }
+
///
/// Gets or sets a value indicating whether the framerate is doubled when deinterlacing.
///
diff --git a/tests/Jellyfin.Model.Tests/Configuration/EncodingOptionsTests.cs b/tests/Jellyfin.Model.Tests/Configuration/EncodingOptionsTests.cs
new file mode 100644
index 0000000000..afd7a18be3
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Configuration/EncodingOptionsTests.cs
@@ -0,0 +1,52 @@
+using System.IO;
+using System.Xml.Serialization;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Configuration;
+
+public class EncodingOptionsTests
+{
+ [Theory]
+ [InlineData("")]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("notapreset")]
+ [InlineData("42")]
+ [InlineData("-1")]
+ public void Deserialize_UnreadableEncoderPreset_FallsBackToDefault(string encoderPresetElement)
+ {
+ var options = Deserialize($"{encoderPresetElement}21");
+
+ Assert.Equal(EncoderPreset.auto, options.EncoderPreset);
+
+ // The rest of the file has to survive: a throwing preset used to discard every other encoding setting.
+ Assert.Equal(21, options.H264Crf);
+ }
+
+ [Fact]
+ public void Deserialize_KnownEncoderPreset_IsKept()
+ {
+ var options = Deserialize("veryfast");
+
+ Assert.Equal(EncoderPreset.veryfast, options.EncoderPreset);
+ }
+
+ [Fact]
+ public void Serialize_WritesTheEncoderPresetElement()
+ {
+ var serializer = new XmlSerializer(typeof(EncodingOptions));
+ using var writer = new StringWriter();
+ serializer.Serialize(writer, new EncodingOptions { EncoderPreset = EncoderPreset.slow });
+
+ Assert.Contains("slow", writer.ToString(), System.StringComparison.Ordinal);
+ }
+
+ private static EncodingOptions Deserialize(string xml)
+ {
+ var serializer = new XmlSerializer(typeof(EncodingOptions));
+ using var reader = new StringReader(xml);
+ return (EncodingOptions)serializer.Deserialize(reader)!;
+ }
+}