Added more unit tests for the subsonic transcoding decision engine

This commit is contained in:
emeric
2026-07-12 23:16:14 +02:00
parent 43c3105a16
commit 78ff9494bb
+474 -151
View File
@@ -123,6 +123,65 @@ namespace lms::api::subsonic
.expected = { detail::DirectPlayResult{} }, .expected = { detail::DirectPlayResult{} },
}, },
// check protocol * and codec * are properly handled
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = std::nullopt,
},
.expected = { detail::DirectPlayResult{} },
},
// check container * is properly handled
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = std::nullopt,
},
.expected = { detail::DirectPlayResult{} },
},
};
processTests(testCases);
}
TEST(TranscodeDecision, bitrateLimitation)
{
const TestCase testCases[]{
// Needs transcode due to codec limitation // Needs transcode due to codec limitation
{ {
.clientInfo = { .clientInfo = {
@@ -213,6 +272,39 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
}, },
// want flac but bitrate too high
{
.clientInfo = {
.name = "LocalDevice",
.platform = "Android",
.maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 },
},
.transcodingProfiles = { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } },
.codecProfiles = {},
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 1'000'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = 16,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
};
processTests(testCases);
}
TEST(TranscodeDecision, sampleRateLimitation)
{
const TestCase testCases[]{
// Needs transcode due to max audio sample rate not handle by codec limitation // Needs transcode due to max audio sample rate not handle by codec limitation
{ {
.clientInfo = { .clientInfo = {
@@ -243,6 +335,86 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
}, },
// want flac but source sample rate is too high
{
.clientInfo = {
.name = "SONOS",
.platform = "UPnP",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
},
.codecProfiles = {
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 950'000,
.channelCount = 2,
.sampleRate = 96'000,
.bitsPerSample = 24,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
},
// want flac but source sample rate is too high, no max bitrate
{
.clientInfo = {
.name = "SONOS",
.platform = "UPnP",
.maxAudioBitrate = std::nullopt,
.maxTranscodingAudioBitrate = std::nullopt,
.directPlayProfiles = {
{ .containers = { "opus", "ogg", "oga", "aac", "webma", "webm", "wav", "flac", "mka" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp4", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
},
.codecProfiles = {
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 950'000,
.channelCount = 2,
.sampleRate = 96'000,
.bitsPerSample = 24,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
},
};
processTests(testCases);
}
TEST(TranscodeDecision, channelLimitation)
{
const TestCase testCases[]{
// Needs transcode due to max nb channels not handle by profile // Needs transcode due to max nb channels not handle by profile
{ {
.clientInfo = { .clientInfo = {
@@ -294,7 +466,14 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
}, },
};
processTests(testCases);
}
TEST(TranscodeDecision, codecFallbackSelection)
{
const TestCase testCases[]{
// needs transcode because codec not handled // needs transcode because codec not handled
{ {
.clientInfo = { .clientInfo = {
@@ -373,157 +552,6 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
}, },
// check protocol * and codec * are properly handled
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = std::nullopt,
},
.expected = { detail::DirectPlayResult{} },
},
// check container * is properly handled
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = std::nullopt,
},
.expected = { detail::DirectPlayResult{} },
},
// want flac but bitrate too high
{
.clientInfo = {
.name = "LocalDevice",
.platform = "Android",
.maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 },
},
.transcodingProfiles = { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } },
.codecProfiles = {},
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 1'000'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = 16,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
// want flac but source sample rate is too high
{
.clientInfo = {
.name = "SONOS",
.platform = "UPnP",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
},
.codecProfiles = {
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 950'000,
.channelCount = 2,
.sampleRate = 96'000,
.bitsPerSample = 24,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
},
// want flac but source sample rate is too high, no max bitrate
{
.clientInfo = {
.name = "SONOS",
.platform = "UPnP",
.maxAudioBitrate = std::nullopt,
.maxTranscodingAudioBitrate = std::nullopt,
.directPlayProfiles = {
{ .containers = { "opus", "ogg", "oga", "aac", "webma", "webm", "wav", "flac", "mka" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp4", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
},
.codecProfiles = {
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 950'000,
.channelCount = 2,
.sampleRate = 96'000,
.bitsPerSample = 24,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
},
// wants a lossy codec not handled -> transcode to lossy // wants a lossy codec not handled -> transcode to lossy
{ {
.clientInfo = { .clientInfo = {
@@ -595,7 +623,14 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
}, },
};
processTests(testCases);
}
TEST(TranscodeDecision, profileMatching)
{
const TestCase testCases[]{
// no protocol specified // no protocol specified
{ {
.clientInfo = { .clientInfo = {
@@ -626,6 +661,69 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
}, },
// needs transcode because audio codec not supported by the direct play profile (container matches, codec does not)
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "aac" }, .protocols = { "http" }, .maxAudioChannels = 2 },
},
.transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 44'100,
.bitsPerSample = std::nullopt,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioCodecNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
// needs transcode because the direct play profile does not support the http protocol
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "hls" }, .maxAudioChannels = 2 },
},
.transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 44'100,
.bitsPerSample = std::nullopt,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ProtocolNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
};
processTests(testCases);
}
TEST(TranscodeDecision, bitDepth)
{
const TestCase testCases[]{
// transcoding to a lossy codec must not report a bit depth, even if a bit depth limitation is set on that codec (lossy codecs have no PCM bit depth) // transcoding to a lossy codec must not report a bit depth, even if a bit depth limitation is set on that codec (lossy codecs have no PCM bit depth)
{ {
.clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { { .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { {
@@ -673,6 +771,231 @@ namespace lms::api::subsonic
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = 16 } } }, .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = 16 } } },
}, },
// needs transcode because the codec profile bit depth limitation rejects direct play
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt },
},
.codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = {
{ .name = Limitation::Type::AudioBitdepth, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "16" }, .required = true },
} } },
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 750'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = 24,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitdepthNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = 16 } } },
},
// a required bit depth limitation cannot be evaluated against a lossy source (no bit depth to check), so it is always treated as incompatible on both the direct play and transcoding target sides
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt },
{ .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt },
},
.codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {
{ .name = Limitation::Type::AudioBitdepth, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "24" }, .required = true },
} } },
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 44'100,
.bitsPerSample = std::nullopt,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitdepthNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
};
processTests(testCases);
}
TEST(TranscodeDecision, comparisonOperators)
{
const TestCase testCases[]{
// Equals comparison (single value): mismatch cannot be adjusted -> rejects direct play, falls back to a transcoding profile not covered by that codec profile
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt },
},
.codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {
{ .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::Equals, .values = { "44100" }, .required = true },
} } },
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 48'000,
.bitsPerSample = std::nullopt,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
// Equals comparison (multiple values): no exact match -> adjusted down to the closest allowed value below the source's
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt },
},
.codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = {
{ .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::Equals, .values = { "44100", "48000", "96000" }, .required = true },
} } },
},
.source = {
.container = core::media::Container::FLAC,
.codec = core::media::Codec::FLAC,
.duration = std::chrono::seconds{ 60 },
.bitrate = 750'000,
.channelCount = 2,
.sampleRate = 60'000,
.bitsPerSample = 16,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } },
},
// NotEquals comparison: source value is in the forbidden list and cannot be adjusted -> rejects direct play, falls back to a transcoding profile not covered by that codec profile
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt },
},
.codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {
{ .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::NotEquals, .values = { "2" }, .required = true },
} } },
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 44'100,
.bitsPerSample = std::nullopt,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
// GreaterThanEqual comparison: source bitrate is below the required minimum and cannot be upscaled -> that transcoding profile is rejected entirely, falls back to the next one
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt },
{ .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt },
},
.codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {
{ .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::GreaterThanEqual, .values = { "192000" }, .required = true },
} } },
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 44'100,
.bitsPerSample = std::nullopt,
},
.expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } },
},
};
processTests(testCases);
}
TEST(TranscodeDecision, failure)
{
const TestCase testCases[]{
// no compatible direct play or transcoding profile at all: transcoding profiles are skipped (unsupported output format, then non-http protocol) -> failure
{
.clientInfo = {
.name = "TestClient",
.platform = "TestPlatform",
.maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
},
.transcodingProfiles = {
{ .container = "wma", .audioCodec = "wma", .protocol = "http", .maxAudioChannels = std::nullopt },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "hls", .maxAudioChannels = std::nullopt },
},
.codecProfiles = {},
},
.source = {
.container = core::media::Container::MPEG,
.codec = core::media::Codec::MP3,
.duration = std::chrono::seconds{ 60 },
.bitrate = 128'000,
.channelCount = 2,
.sampleRate = 44'100,
.bitsPerSample = std::nullopt,
},
.expected = { detail::FailureResult{ "No compatible direct play or transcoding profile found" } },
},
}; };
processTests(testCases); processTests(testCases);