mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-01 18:12:51 +01:00
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>
302 lines
9.2 KiB
TypeScript
302 lines
9.2 KiB
TypeScript
/**
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { Platform } from "react-native";
|
|
import MediaTypes from "../../constants/MediaTypes";
|
|
import { getSubtitleProfiles } from "./subtitles";
|
|
|
|
export type PlatformType = "ios" | "android";
|
|
export type PlayerType = "mpv" | "exoplayer";
|
|
export type AudioTranscodeModeType = "auto" | "stereo" | "5.1" | "passthrough";
|
|
|
|
export interface ProfileOptions {
|
|
/** Target platform */
|
|
platform?: PlatformType;
|
|
/** Video player being used */
|
|
player?: PlayerType;
|
|
/** Audio transcoding mode */
|
|
audioMode?: AudioTranscodeModeType;
|
|
}
|
|
|
|
/**
|
|
* Audio direct play profiles for standalone audio items in MPV player.
|
|
* These define which audio file formats can be played directly without transcoding.
|
|
*/
|
|
const getAudioDirectPlayProfile = (platform: PlatformType) => {
|
|
if (platform === "ios") {
|
|
// iOS audio formats supported by MPV
|
|
return {
|
|
Type: MediaTypes.Audio,
|
|
Container: "mp3,m4a,aac,flac,alac,wav,aiff,caf",
|
|
AudioCodec: "mp3,aac,alac,flac,opus,pcm",
|
|
};
|
|
}
|
|
|
|
// Android audio formats supported by MPV
|
|
return {
|
|
Type: MediaTypes.Audio,
|
|
Container: "mp3,m4a,aac,ogg,flac,wav,webm,mka",
|
|
AudioCodec: "mp3,aac,flac,vorbis,opus,pcm",
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Audio codec profiles for standalone audio items in MPV player.
|
|
* These define codec constraints for audio file playback.
|
|
*/
|
|
const getAudioCodecProfile = (platform: PlatformType) => {
|
|
if (platform === "ios") {
|
|
// iOS audio codec constraints for MPV
|
|
return {
|
|
Type: MediaTypes.Audio,
|
|
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,pcm",
|
|
};
|
|
}
|
|
|
|
// Android audio codec constraints for MPV
|
|
return {
|
|
Type: MediaTypes.Audio,
|
|
Codec: "aac,ac3,eac3,mp3,flac,vorbis,opus,pcm",
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* MPV (via FFmpeg) can decode all audio codecs including TrueHD and DTS-HD MA.
|
|
* The audioMode setting only controls the maximum channel count - MPV will
|
|
* decode and downmix as needed.
|
|
*/
|
|
const getVideoAudioCodecs = (
|
|
platform: PlatformType,
|
|
audioMode: AudioTranscodeModeType,
|
|
): { directPlayCodec: string; maxAudioChannels: string } => {
|
|
// Base codecs
|
|
const baseCodecs = "aac,mp3,flac,opus,vorbis";
|
|
|
|
// Surround codecs
|
|
const surroundCodecs = "ac3,eac3,dts";
|
|
|
|
// Lossless HD codecs - MPV decodes these and downmixes as needed
|
|
const losslessHdCodecs = "truehd";
|
|
|
|
// Platform-specific codecs
|
|
const platformCodecs = platform === "ios" ? "alac,wma" : "wma";
|
|
|
|
// MPV can decode all codecs - only channel count varies by mode
|
|
const allCodecs = `${baseCodecs},${surroundCodecs},${losslessHdCodecs},${platformCodecs}`;
|
|
|
|
return {
|
|
directPlayCodec: allCodecs,
|
|
maxAudioChannels: maxChannelsForMode(audioMode),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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";
|
|
|
|
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",
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Generates a device profile for Jellyfin playback.
|
|
*/
|
|
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,
|
|
audioMode,
|
|
);
|
|
|
|
/**
|
|
* Device profile for MPV player
|
|
*/
|
|
const profile = {
|
|
Name: "1. MPV",
|
|
MaxStaticBitrate: 999_999_999,
|
|
MaxStreamingBitrate: 999_999_999,
|
|
CodecProfiles: [
|
|
{
|
|
Type: MediaTypes.Video,
|
|
Codec: "h264,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
|
},
|
|
{
|
|
Type: MediaTypes.Video,
|
|
Codec: "hevc,h265",
|
|
Conditions: [
|
|
{
|
|
Condition: "LessThanEqual",
|
|
Property: "VideoLevel",
|
|
Value: "153",
|
|
IsRequired: false,
|
|
},
|
|
{
|
|
Condition: "NotEquals",
|
|
Property: "VideoRangeType",
|
|
Value: "DOVI", //no dolby vision at all
|
|
IsRequired: true,
|
|
},
|
|
],
|
|
},
|
|
getAudioCodecProfile(platform),
|
|
],
|
|
DirectPlayProfiles: [
|
|
{
|
|
Type: MediaTypes.Video,
|
|
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
|
|
VideoCodec:
|
|
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
|
|
AudioCodec: directPlayCodec,
|
|
},
|
|
getAudioDirectPlayProfile(platform),
|
|
],
|
|
TranscodingProfiles: [
|
|
{
|
|
Type: MediaTypes.Video,
|
|
Context: "Streaming",
|
|
Protocol: "hls",
|
|
Container: "ts",
|
|
VideoCodec: "h264, hevc",
|
|
AudioCodec: "aac,mp3,ac3,dts",
|
|
MaxAudioChannels: maxAudioChannels,
|
|
},
|
|
{
|
|
Type: MediaTypes.Audio,
|
|
Context: "Streaming",
|
|
Protocol: "http",
|
|
Container: "mp3",
|
|
AudioCodec: "mp3",
|
|
MaxAudioChannels: "2",
|
|
},
|
|
],
|
|
SubtitleProfiles: getSubtitleProfiles(),
|
|
} satisfies DeviceProfile;
|
|
|
|
return profile;
|
|
};
|