officer-jellyfin owns the whole Jellyfin contract: the instance URL, the access token, the Jellyfin user it belongs to and the DeviceId its sessions are keyed by. The platform side is a 17-line proxy holding no credentials. Servers are a registry, not a single row — this machine runs four instances and the owner switches between them. The password is never stored: it is traded once for an access token through AuthenticateByName, and only that token is persisted, encrypted. Two doors. /_officer/* is a hand-written JSON façade for the things the browser should not have to know — the user id in the path, the Fields lists that decide whether a grid has posters, the PlaybackInfo negotiation. /_jf/* is a GET-only, allow-listed byte pass-through for images, video, HLS and subtitles; it keeps Jellyfin's own paths because a master playlist references its segments relatively, so any renaming would mean rewriting m3u8 bodies. TranscodingUrl arrives with api_key=<access token> in its query string and would otherwise be handed straight to a video element. It is stripped before anything is returned; the pass-through re-adds the credential as a header. Video only — Officer's own player owns audio, so music collections are filtered out of the library list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
3.2 KiB
TypeScript
77 lines
3.2 KiB
TypeScript
// The device profile sent with every PlaybackInfo request.
|
|
//
|
|
// This is the single most consequential object in the whole Jellyfin integration and the least obvious: it is
|
|
// how the SERVER decides whether to hand back the original file or spin up an ffmpeg transcode. Describe the
|
|
// browser too generously and playback dies silently on an unsupported codec; too conservatively and every
|
|
// file is transcoded, which on this machine means CPU-only ffmpeg (no /dev/dri is passed into the container).
|
|
//
|
|
// It deliberately describes what a modern Chromium/WebKit `<video>` plus hls.js can actually play, which is
|
|
// narrower than the file formats a personal library contains — Matroska in particular is NOT playable in any
|
|
// browser, so mkv is the common case that has to go out as HLS even when its streams are already h264/aac.
|
|
//
|
|
// Kept as a plain literal rather than probed from the client: it is server-side knowledge about a fixed
|
|
// target (our own web player), and a profile assembled from feature detection in the browser is the classic
|
|
// way to end up with a per-machine playback bug nobody can reproduce.
|
|
|
|
const VIDEO_CONTAINERS = 'mp4,m4v,webm';
|
|
const VIDEO_CODECS = 'h264,vp8,vp9,av1';
|
|
const AUDIO_CODECS = 'aac,mp3,opus,flac,vorbis';
|
|
|
|
export const DEVICE_PROFILE = {
|
|
MaxStreamingBitrate: 120_000_000,
|
|
MaxStaticBitrate: 100_000_000,
|
|
MusicStreamingTranscodingBitrate: 384_000,
|
|
|
|
DirectPlayProfiles: [
|
|
{ Container: VIDEO_CONTAINERS, Type: 'Video', VideoCodec: VIDEO_CODECS, AudioCodec: AUDIO_CODECS },
|
|
// Present so a trailer or a stray extra plays; the library itself is video and audio lives in /music.
|
|
{ Container: 'mp3,aac,flac,opus,webm', Type: 'Audio' },
|
|
],
|
|
|
|
TranscodingProfiles: [
|
|
{
|
|
// fMP4 segments rather than MPEG-TS: h264 in fMP4 is what jellyfin-web itself defaults to on 10.11,
|
|
// and it is the only path that can stream-copy an h264 track into HLS instead of re-encoding it.
|
|
Container: 'mp4',
|
|
Type: 'Video',
|
|
AudioCodec: 'aac,mp3,opus,flac',
|
|
VideoCodec: 'h264',
|
|
Context: 'Streaming',
|
|
Protocol: 'hls',
|
|
MaxAudioChannels: '2',
|
|
MinSegments: 1,
|
|
BreakOnNonKeyFrames: true,
|
|
},
|
|
{
|
|
Container: 'mp4',
|
|
Type: 'Video',
|
|
AudioCodec: 'aac',
|
|
VideoCodec: 'h264',
|
|
Context: 'Static',
|
|
Protocol: 'http',
|
|
},
|
|
],
|
|
|
|
CodecProfiles: [
|
|
{
|
|
Type: 'Video',
|
|
Codec: 'h264',
|
|
Conditions: [
|
|
// High 10 (10-bit h264) decodes in almost no browser, and hitting it is a black screen with audio
|
|
// rather than an error, so it is excluded explicitly instead of being left to chance.
|
|
{ Condition: 'NotEquals', Property: 'VideoProfile', Value: 'high 10', IsRequired: false },
|
|
{ Condition: 'LessThanEqual', Property: 'VideoLevel', Value: '52', IsRequired: false },
|
|
],
|
|
},
|
|
],
|
|
|
|
SubtitleProfiles: [
|
|
// Text subtitles are fetched separately and rendered by the player. Image-based ones (PGS, VOBSUB) have
|
|
// no such path in a browser and are left out, which makes Jellyfin burn them into the video instead.
|
|
{ Format: 'vtt', Method: 'External' },
|
|
{ Format: 'srt', Method: 'External' },
|
|
{ Format: 'ass', Method: 'External' },
|
|
{ Format: 'ssa', Method: 'External' },
|
|
],
|
|
} as const;
|