diff --git a/src/libs/subsonic/test/TranscodeDecision.cpp b/src/libs/subsonic/test/TranscodeDecision.cpp index 691f4fe4..ae7272c2 100644 --- a/src/libs/subsonic/test/TranscodeDecision.cpp +++ b/src/libs/subsonic/test/TranscodeDecision.cpp @@ -123,6 +123,65 @@ namespace lms::api::subsonic .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 { .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 } } }, }, + // 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 { .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 } } }, }, + // 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 { .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 } } }, }, + }; + processTests(testCases); + } + + TEST(TranscodeDecision, codecFallbackSelection) + { + const TestCase testCases[]{ // needs transcode because codec not handled { .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 } } }, }, - // 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 { .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 } } }, }, + }; + processTests(testCases); + } + + TEST(TranscodeDecision, profileMatching) + { + const TestCase testCases[]{ // no protocol specified { .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 } } }, }, + // 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) { .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 } } }, }, + + // 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);