feat(casting): add Chromecast device profile builder

This commit is contained in:
Uruk
2026-05-21 02:16:29 +02:00
parent bcfa8c6d63
commit e3f105691b
2 changed files with 161 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { describe, expect, test } from "bun:test";
import { buildChromecastProfile } from "./buildProfile";
import { CONSERVATIVE_CAPABILITIES } from "./capabilities";
describe("buildChromecastProfile", () => {
test("conservative caps produce an H.264-only video codec list", () => {
const profile = buildChromecastProfile(CONSERVATIVE_CAPABILITIES);
const videoCodecProfile = profile.CodecProfiles?.find(
(c) => c.Type === "Video",
);
expect(videoCodecProfile?.Codec).toBe("h264");
});
test("HEVC-capable caps include hevc in the video codec list", () => {
const profile = buildChromecastProfile({
...CONSERVATIVE_CAPABILITIES,
hevc: true,
});
const videoCodecProfile = profile.CodecProfiles?.find(
(c) => c.Type === "Video",
);
expect(videoCodecProfile?.Codec).toContain("hevc");
});
test("maxVideoBitrate drives MaxStreamingBitrate", () => {
const profile = buildChromecastProfile({
...CONSERVATIVE_CAPABILITIES,
maxVideoBitrate: 5_000_000,
});
expect(profile.MaxStreamingBitrate).toBe(5_000_000);
});
test("maxAudioChannels constrains transcoding profiles", () => {
const profile = buildChromecastProfile(CONSERVATIVE_CAPABILITIES);
const videoTranscode = profile.TranscodingProfiles?.find(
(p) => p.Type === "Video",
);
expect(videoTranscode?.MaxAudioChannels).toBe("2");
});
test("non-10bit HEVC caps add a video bit-depth condition", () => {
const profile = buildChromecastProfile({
...CONSERVATIVE_CAPABILITIES,
hevc: true,
hevc10bit: false,
});
const videoCodecProfile = profile.CodecProfiles?.find(
(c) => c.Type === "Video",
);
const bitDepthCondition = videoCodecProfile?.Conditions?.find(
(cond) => cond.Property === "VideoBitDepth",
);
expect(bitDepthCondition).toBeDefined();
});
});

View File

@@ -0,0 +1,106 @@
import type {
DeviceProfile,
ProfileCondition,
} from "@jellyfin/sdk/lib/generated-client/models";
import type { ChromecastCapabilities } from "./capabilities";
/**
* Build a Jellyfin `DeviceProfile` for a Chromecast from its detected capabilities.
* Replaces the former static `chromecast.ts` / `chromecasth265.ts` profiles.
*/
export const buildChromecastProfile = (
caps: ChromecastCapabilities,
): DeviceProfile => {
const videoCodecs = caps.hevc ? "hevc,h264" : "h264";
const maxHeight = caps.maxResolution === 2160 ? "2160" : "1080";
const maxChannels = String(caps.maxAudioChannels);
const videoConditions: ProfileCondition[] = [
{
Condition: "LessThanEqual",
Property: "Height",
Value: maxHeight,
IsRequired: false,
},
];
// When HEVC is allowed but 10-bit is not, force the server to transcode
// 10-bit sources down to 8-bit.
if (caps.hevc && !caps.hevc10bit) {
videoConditions.push({
Condition: "LessThanEqual",
Property: "VideoBitDepth",
Value: "8",
IsRequired: false,
});
}
return {
Name: "Chromecast Video Profile",
MaxStreamingBitrate: caps.maxVideoBitrate,
MaxStaticBitrate: caps.maxVideoBitrate,
MusicStreamingTranscodingBitrate: 384000,
CodecProfiles: [
{
Type: "Video",
Codec: videoCodecs,
Conditions: videoConditions,
},
{
Type: "Audio",
Codec: "aac,mp3,flac,opus,vorbis",
// Force transcode of multichannel audio the receiver cannot output.
Conditions: [
{
Condition: "LessThanEqual",
Property: "AudioChannels",
Value: maxChannels,
IsRequired: false,
},
],
},
],
ContainerProfiles: [],
DirectPlayProfiles: [
{
Container: caps.hevc ? "mp4,mkv" : "mp4",
Type: "Video",
VideoCodec: videoCodecs,
AudioCodec: "aac,mp3,opus,vorbis",
},
{ Container: "mp3", Type: "Audio" },
{ Container: "aac", Type: "Audio" },
{ Container: "flac", Type: "Audio" },
{ Container: "wav", Type: "Audio" },
],
TranscodingProfiles: [
{
Container: "ts",
Type: "Video",
VideoCodec: videoCodecs,
AudioCodec: "aac,mp3",
Protocol: "hls",
Context: "Streaming",
MaxAudioChannels: maxChannels,
MinSegments: 2,
BreakOnNonKeyFrames: true,
},
{
Container: "mp3",
Type: "Audio",
AudioCodec: "mp3",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: maxChannels,
},
{
Container: "aac",
Type: "Audio",
AudioCodec: "aac",
Protocol: "http",
Context: "Streaming",
MaxAudioChannels: maxChannels,
},
],
SubtitleProfiles: [{ Format: "vtt", Method: "Encode" }],
};
};