mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-01 18:12:51 +01:00
feat: adding exoplayer for HDR playback
Currently MPV doesn't support HDR via external displays. giving people the choice of HDR/limited ass sub support/SDR full sub support Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import MediaTypes from "../../constants/MediaTypes";
|
||||
import { getSubtitleProfiles } from "./subtitles";
|
||||
|
||||
export type PlatformType = "ios" | "android";
|
||||
export type PlayerType = "mpv";
|
||||
export type PlayerType = "mpv" | "exoplayer";
|
||||
export type AudioTranscodeModeType = "auto" | "stereo" | "5.1" | "passthrough";
|
||||
|
||||
export interface ProfileOptions {
|
||||
@@ -63,6 +63,26 @@ const getAudioCodecProfile = (platform: PlatformType) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves the MaxAudioChannels string for a given audio transcoding mode.
|
||||
* Used by both the MPV and ExoPlayer profile branches — the channel-cap
|
||||
* rule is player-agnostic (the player decodes; the cap just tells the
|
||||
* server when to transcode down).
|
||||
*/
|
||||
const maxChannelsForMode = (audioMode: AudioTranscodeModeType): string => {
|
||||
switch (audioMode) {
|
||||
case "stereo":
|
||||
return "2";
|
||||
case "5.1":
|
||||
return "6";
|
||||
case "passthrough":
|
||||
return "8";
|
||||
default:
|
||||
// Auto: default to 5.1 (6 channels)
|
||||
return "6";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the video audio codec configuration based on platform and audio mode.
|
||||
*
|
||||
@@ -89,35 +109,59 @@ const getVideoAudioCodecs = (
|
||||
// MPV can decode all codecs - only channel count varies by mode
|
||||
const allCodecs = `${baseCodecs},${surroundCodecs},${losslessHdCodecs},${platformCodecs}`;
|
||||
|
||||
switch (audioMode) {
|
||||
case "stereo":
|
||||
// Limit to 2 channels - MPV will decode and downmix
|
||||
return {
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "2",
|
||||
};
|
||||
return {
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: maxChannelsForMode(audioMode),
|
||||
};
|
||||
};
|
||||
|
||||
case "5.1":
|
||||
// Limit to 6 channels
|
||||
return {
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "6",
|
||||
};
|
||||
/**
|
||||
* ExoPlayer (Media3 1.10.1) direct-play profile for Android TV.
|
||||
*
|
||||
* Codec set aligned with Media3's documented supported-formats list:
|
||||
* - Video: H.263, H.264, H.265, VP8, VP9, AV1
|
||||
* - Audio: Vorbis, Opus, FLAC, ALAC, PCM, MP3, AAC, AC-3, E-AC-3, DTS,
|
||||
* DTS-HD, TrueHD
|
||||
*
|
||||
* Hardware decode (MediaCodec) handles whatever the device ships with;
|
||||
* the rest fall through to FFmpeg software decode via the Jellyfin-published
|
||||
* `org.jellyfin.media3:media3-ffmpeg-decoder` extension wired up with
|
||||
* `DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER` (see
|
||||
* ExoPlayerView.kt:ensurePlayer).
|
||||
*
|
||||
* Cross-checked against the reference-device probe in
|
||||
* docs/research/hdr-dv-atmos-tv-plan.md (Amlogic Android 14 TV; HDMI sink
|
||||
* accepts AC3/EAC3 as bitstream and multichannel PCM up to 7.1 @ 192 kHz,
|
||||
* so software-decoded DTS/DTS-HD/TrueHD reach the sink as PCM).
|
||||
*
|
||||
* Dolby Vision: the CodecProfile below uses `NotEquals VideoRangeType
|
||||
* DOVI`, which in Jellyfin's semantics blocks ONLY pure Profile 5
|
||||
* (IPTPQc2 — the stream that renders purple/green without a DV-aware
|
||||
* decoder). DV Profiles 7/8 with HDR10 or SDR base layers (Jellyfin
|
||||
* reports these as `DOVIWithHDR10`, `DOVIWithHDR10Plus`, `DOVIWithEL`)
|
||||
* are NOT blocked — Media3 1.9.1+ correctly falls back to the AVC/HEVC
|
||||
* base layer.
|
||||
*
|
||||
* Containers limited to Media3's bundled extractors. FLV is intentionally
|
||||
* absent — Media3 has no FLV extractor (MPV claims it via FFmpeg).
|
||||
*/
|
||||
const getExoPlayerDirectPlayProfile = () => {
|
||||
const audioCodecs =
|
||||
"vorbis,opus,flac,alac,pcm,mp3,aac,ac3,eac3,dts,dtshd,truehd";
|
||||
|
||||
case "passthrough":
|
||||
// Allow up to 8 channels - for external DAC/receiver setups
|
||||
return {
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "8",
|
||||
};
|
||||
|
||||
default:
|
||||
// Auto mode: default to 5.1 (6 channels)
|
||||
return {
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "6",
|
||||
};
|
||||
}
|
||||
return {
|
||||
video: {
|
||||
Type: MediaTypes.Video,
|
||||
Container: "mp4,mkv,webm,ts,mpegts,mov",
|
||||
VideoCodec: "h263,h264,hevc,vp8,vp9,av1",
|
||||
AudioCodec: audioCodecs,
|
||||
},
|
||||
audio: {
|
||||
Type: MediaTypes.Audio,
|
||||
Container: "mp3,m4a,aac,ogg,flac,wav,webm,mka",
|
||||
AudioCodec: "vorbis,opus,flac,alac,pcm,mp3,aac",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -126,6 +170,63 @@ const getVideoAudioCodecs = (
|
||||
export const generateDeviceProfile = (options: ProfileOptions = {}) => {
|
||||
const platform = (options.platform || Platform.OS) as PlatformType;
|
||||
const audioMode = options.audioMode || "auto";
|
||||
const player = options.player || "mpv";
|
||||
|
||||
// ExoPlayer branch — Media3 capabilities on Android TV.
|
||||
if (player === "exoplayer" && platform === "android") {
|
||||
const exoDirect = getExoPlayerDirectPlayProfile();
|
||||
|
||||
return {
|
||||
Name: "1. ExoPlayer",
|
||||
MaxStaticBitrate: 999_999_999,
|
||||
MaxStreamingBitrate: 999_999_999,
|
||||
CodecProfiles: [
|
||||
{
|
||||
Type: MediaTypes.Video,
|
||||
Codec: "h263,h264,hevc,vp8,vp9,av1",
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Video,
|
||||
Codec: "hevc,h265",
|
||||
Conditions: [
|
||||
{
|
||||
Condition: "NotEquals",
|
||||
Property: "VideoRangeType",
|
||||
// Blocks ONLY pure DV Profile 5 (IPTPQc2). Profiles 7/8 with
|
||||
// HDR10/SDR base layers fall through to Media3's HEVC fallback
|
||||
// (1.9.1+). See getExoPlayerDirectPlayProfile doc above.
|
||||
Value: "DOVI",
|
||||
IsRequired: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
Codec: "vorbis,opus,flac,alac,pcm,mp3,aac,ac3,eac3,dts,dtshd,truehd",
|
||||
},
|
||||
],
|
||||
DirectPlayProfiles: [exoDirect.video, exoDirect.audio],
|
||||
TranscodingProfiles: [
|
||||
{
|
||||
Type: MediaTypes.Video,
|
||||
Context: "Streaming",
|
||||
Protocol: "hls",
|
||||
Container: "ts",
|
||||
VideoCodec: "h264,hevc",
|
||||
AudioCodec: "aac,mp3,ac3",
|
||||
MaxAudioChannels: maxChannelsForMode(audioMode),
|
||||
},
|
||||
],
|
||||
// Text-only subtitles for direct play. PGS delivered as Encode
|
||||
// (burn-in) because Media3's PGS support is inconsistent.
|
||||
SubtitleProfiles: [
|
||||
{ Format: "srt", Method: "External" },
|
||||
{ Format: "vtt", Method: "External" },
|
||||
{ Format: "ttml", Method: "External" },
|
||||
{ Format: "pgssub", Method: "Encode" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const { directPlayCodec, maxAudioChannels } = getVideoAudioCodecs(
|
||||
platform,
|
||||
@@ -198,6 +299,3 @@ export const generateDeviceProfile = (options: ProfileOptions = {}) => {
|
||||
|
||||
return profile;
|
||||
};
|
||||
|
||||
// Default export for backward compatibility
|
||||
export default generateDeviceProfile();
|
||||
|
||||
Reference in New Issue
Block a user