mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix(vlc): add audio transcoding mode to fix 7.1 TrueHD playback
This commit is contained in:
@@ -299,7 +299,11 @@ export default function page() {
|
||||
maxStreamingBitrate: bitrateValue,
|
||||
mediaSourceId: mediaSourceId,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: generateDeviceProfile(),
|
||||
deviceProfile: generateDeviceProfile({
|
||||
platform: Platform.OS as "ios" | "android",
|
||||
player: useVlcPlayer ? "vlc" : "ksplayer",
|
||||
audioMode: settings.audioTranscodeMode,
|
||||
}),
|
||||
});
|
||||
if (!res) return;
|
||||
const { mediaSource, sessionId, url } = res;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import { Switch } from "react-native-gesture-handler";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { AudioTranscodeMode, useSettings } from "@/utils/atoms/settings";
|
||||
import { Text } from "../common/Text";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
@@ -54,6 +54,70 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
||||
];
|
||||
}, [cultures, settings?.defaultAudioLanguage, t, updateSettings]);
|
||||
|
||||
const audioTranscodeModeLabels: Record<AudioTranscodeMode, string> = {
|
||||
[AudioTranscodeMode.Auto]: t("home.settings.audio.transcode_mode.auto"),
|
||||
[AudioTranscodeMode.ForceStereo]: t(
|
||||
"home.settings.audio.transcode_mode.stereo",
|
||||
),
|
||||
[AudioTranscodeMode.Allow51]: t("home.settings.audio.transcode_mode.5_1"),
|
||||
[AudioTranscodeMode.AllowAll]: t(
|
||||
"home.settings.audio.transcode_mode.passthrough",
|
||||
),
|
||||
};
|
||||
|
||||
const audioTranscodeModeOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
options: [
|
||||
{
|
||||
type: "radio" as const,
|
||||
label: t("home.settings.audio.transcode_mode.auto"),
|
||||
value: AudioTranscodeMode.Auto,
|
||||
selected:
|
||||
settings?.audioTranscodeMode === AudioTranscodeMode.Auto ||
|
||||
!settings?.audioTranscodeMode,
|
||||
onPress: () =>
|
||||
updateSettings({ audioTranscodeMode: AudioTranscodeMode.Auto }),
|
||||
},
|
||||
{
|
||||
type: "radio" as const,
|
||||
label: t("home.settings.audio.transcode_mode.stereo"),
|
||||
value: AudioTranscodeMode.ForceStereo,
|
||||
selected:
|
||||
settings?.audioTranscodeMode === AudioTranscodeMode.ForceStereo,
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
audioTranscodeMode: AudioTranscodeMode.ForceStereo,
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "radio" as const,
|
||||
label: t("home.settings.audio.transcode_mode.5_1"),
|
||||
value: AudioTranscodeMode.Allow51,
|
||||
selected:
|
||||
settings?.audioTranscodeMode === AudioTranscodeMode.Allow51,
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
audioTranscodeMode: AudioTranscodeMode.Allow51,
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "radio" as const,
|
||||
label: t("home.settings.audio.transcode_mode.passthrough"),
|
||||
value: AudioTranscodeMode.AllowAll,
|
||||
selected:
|
||||
settings?.audioTranscodeMode === AudioTranscodeMode.AllowAll,
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
audioTranscodeMode: AudioTranscodeMode.AllowAll,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[settings?.audioTranscodeMode, t, updateSettings],
|
||||
);
|
||||
|
||||
if (isTv) return null;
|
||||
if (!settings) return null;
|
||||
|
||||
@@ -98,6 +162,31 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
||||
title={t("home.settings.audio.language")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={t("home.settings.audio.transcode_mode.title")}
|
||||
subtitle={t("home.settings.audio.transcode_mode.description")}
|
||||
>
|
||||
<PlatformDropdown
|
||||
groups={audioTranscodeModeOptions}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{
|
||||
audioTranscodeModeLabels[
|
||||
settings?.audioTranscodeMode || AudioTranscodeMode.Auto
|
||||
]
|
||||
}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.audio.transcode_mode.title")}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,9 @@ import type {
|
||||
import { useAtomValue } from "jotai";
|
||||
import type React from "react";
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import type { Bitrate } from "@/components/BitrateSelector";
|
||||
import { settingsAtom } from "@/utils/atoms/settings";
|
||||
import { settingsAtom, VideoPlayerIOS } from "@/utils/atoms/settings";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { generateDeviceProfile } from "@/utils/profiles/native";
|
||||
import { apiAtom, userAtom } from "./JellyfinProvider";
|
||||
@@ -77,7 +78,19 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const native = generateDeviceProfile();
|
||||
// Determine which player is being used:
|
||||
// - Android always uses VLC
|
||||
// - iOS uses user setting (VLC is default)
|
||||
const useVlcPlayer =
|
||||
Platform.OS === "android" ||
|
||||
(Platform.OS === "ios" &&
|
||||
settings.videoPlayerIOS === VideoPlayerIOS.VLC);
|
||||
|
||||
const native = generateDeviceProfile({
|
||||
platform: Platform.OS as "ios" | "android",
|
||||
player: useVlcPlayer ? "vlc" : "ksplayer",
|
||||
audioMode: settings.audioTranscodeMode,
|
||||
});
|
||||
const data = await getStreamUrl({
|
||||
api,
|
||||
deviceProfile: native,
|
||||
|
||||
@@ -132,7 +132,15 @@
|
||||
"audio_language": "Audio Language",
|
||||
"audio_hint": "Choose a default audio language.",
|
||||
"none": "None",
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"transcode_mode": {
|
||||
"title": "Audio Transcoding",
|
||||
"description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
|
||||
"auto": "Auto",
|
||||
"stereo": "Force Stereo",
|
||||
"5_1": "Allow 5.1",
|
||||
"passthrough": "Passthrough"
|
||||
}
|
||||
},
|
||||
"subtitles": {
|
||||
"subtitle_title": "Subtitles",
|
||||
|
||||
@@ -140,6 +140,14 @@ export enum VideoPlayerIOS {
|
||||
VLC = "vlc",
|
||||
}
|
||||
|
||||
// Audio transcoding mode - controls how surround audio is handled
|
||||
export enum AudioTranscodeMode {
|
||||
Auto = "auto", // Platform/player defaults (recommended)
|
||||
ForceStereo = "stereo", // Always transcode to stereo
|
||||
Allow51 = "5.1", // Allow up to 5.1, transcode 7.1+
|
||||
AllowAll = "passthrough", // Direct play all (for external DAC users)
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
home?: Home | null;
|
||||
deviceProfile?: "Expo" | "Native" | "Old";
|
||||
@@ -218,6 +226,8 @@ export type Settings = {
|
||||
audioMaxCacheSizeMB: number;
|
||||
// Music playback
|
||||
preferLocalAudio: boolean;
|
||||
// Audio transcoding mode
|
||||
audioTranscodeMode: AudioTranscodeMode;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -316,6 +326,8 @@ export const defaultValues: Settings = {
|
||||
audioMaxCacheSizeMB: 500,
|
||||
// Music playback
|
||||
preferLocalAudio: true,
|
||||
// Audio transcoding mode
|
||||
audioTranscodeMode: AudioTranscodeMode.Auto,
|
||||
};
|
||||
|
||||
const loadSettings = (): Partial<Settings> => {
|
||||
|
||||
15
utils/profiles/native.d.ts
vendored
15
utils/profiles/native.d.ts
vendored
@@ -4,4 +4,17 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
export function generateDeviceProfile(): any;
|
||||
export type PlatformType = "ios" | "android";
|
||||
export type PlayerType = "vlc" | "ksplayer";
|
||||
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;
|
||||
}
|
||||
|
||||
export function generateDeviceProfile(options?: ProfileOptions): any;
|
||||
|
||||
@@ -7,12 +7,23 @@ import { Platform } from "react-native";
|
||||
import MediaTypes from "../../constants/MediaTypes";
|
||||
import { getSubtitleProfiles } from "./subtitles";
|
||||
|
||||
/**
|
||||
* @typedef {"ios" | "android"} PlatformType
|
||||
* @typedef {"vlc" | "ksplayer"} PlayerType
|
||||
* @typedef {"auto" | "stereo" | "5.1" | "passthrough"} AudioTranscodeModeType
|
||||
*
|
||||
* @typedef {Object} ProfileOptions
|
||||
* @property {PlatformType} [platform] - Target platform
|
||||
* @property {PlayerType} [player] - Video player being used
|
||||
* @property {AudioTranscodeModeType} [audioMode] - Audio transcoding mode
|
||||
*/
|
||||
|
||||
/**
|
||||
* Audio profiles for react-native-track-player based on platform capabilities.
|
||||
* iOS uses AVPlayer, Android uses ExoPlayer - each has different codec support.
|
||||
*/
|
||||
const getAudioDirectPlayProfile = () => {
|
||||
if (Platform.OS === "ios") {
|
||||
const getAudioDirectPlayProfile = (platform) => {
|
||||
if (platform === "ios") {
|
||||
// iOS AVPlayer supported formats
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
@@ -29,8 +40,8 @@ const getAudioDirectPlayProfile = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getAudioCodecProfile = () => {
|
||||
if (Platform.OS === "ios") {
|
||||
const getAudioCodecProfile = (platform) => {
|
||||
if (platform === "ios") {
|
||||
// iOS AVPlayer codec constraints
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
@@ -45,12 +56,99 @@ const getAudioCodecProfile = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const generateDeviceProfile = () => {
|
||||
/**
|
||||
* Gets the video audio codec configuration based on platform, player, and audio mode.
|
||||
*
|
||||
* Key insight: VLC handles AC3/EAC3/DTS downmixing fine.
|
||||
* Only TrueHD and DTS-HD MA (lossless 7.1) cause issues on mobile devices
|
||||
* because VLC's internal downmixing from 7.1 to stereo fails on some Android audio pipelines.
|
||||
*
|
||||
* @param {PlatformType} platform
|
||||
* @param {PlayerType} player
|
||||
* @param {AudioTranscodeModeType} audioMode
|
||||
* @returns {{ directPlayCodec: string, maxAudioChannels: string }}
|
||||
*/
|
||||
const getVideoAudioCodecs = (platform, player, audioMode) => {
|
||||
// Base codecs that work everywhere
|
||||
const baseCodecs = "aac,mp3,flac,opus,vorbis";
|
||||
|
||||
// Surround codecs that VLC handles well (downmixes properly)
|
||||
const surroundCodecs = "ac3,eac3,dts";
|
||||
|
||||
// Lossless HD codecs that cause issues with VLC's downmixing on mobile
|
||||
const losslessHdCodecs = "truehd";
|
||||
|
||||
// Platform-specific codecs
|
||||
const platformCodecs = platform === "ios" ? "alac,wma" : "wma";
|
||||
|
||||
// Handle explicit user settings first
|
||||
switch (audioMode) {
|
||||
case "stereo":
|
||||
// Force stereo transcoding - only allow basic codecs
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${platformCodecs}`,
|
||||
maxAudioChannels: "2",
|
||||
};
|
||||
|
||||
case "5.1":
|
||||
// Allow up to 5.1 - include surround codecs but not lossless HD
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${surroundCodecs},${platformCodecs}`,
|
||||
maxAudioChannels: "6",
|
||||
};
|
||||
|
||||
case "passthrough":
|
||||
// Allow all codecs - for users with external DAC/receiver
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${surroundCodecs},${losslessHdCodecs},${platformCodecs}`,
|
||||
maxAudioChannels: "8",
|
||||
};
|
||||
default:
|
||||
// Auto mode: platform and player-specific defaults
|
||||
break;
|
||||
}
|
||||
|
||||
// Auto mode logic based on platform and player
|
||||
if (player === "ksplayer" && platform === "ios") {
|
||||
// KSPlayer on iOS handles all codecs well, including TrueHD
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${surroundCodecs},${losslessHdCodecs},${platformCodecs}`,
|
||||
maxAudioChannels: "8",
|
||||
};
|
||||
}
|
||||
|
||||
// VLC on Android or iOS - don't include TrueHD (causes 7.1 downmix issues)
|
||||
// DTS core is fine, VLC handles it well. Only lossless 7.1 formats are problematic.
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${surroundCodecs},${platformCodecs}`,
|
||||
maxAudioChannels: "6",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a device profile for Jellyfin playback.
|
||||
*
|
||||
* @param {ProfileOptions} [options] - Profile configuration options
|
||||
* @returns {Object} Jellyfin device profile
|
||||
*/
|
||||
export const generateDeviceProfile = (options = {}) => {
|
||||
const platform = options.platform || Platform.OS;
|
||||
const player = options.player || "vlc";
|
||||
const audioMode = options.audioMode || "auto";
|
||||
|
||||
const { directPlayCodec, maxAudioChannels } = getVideoAudioCodecs(
|
||||
platform,
|
||||
player,
|
||||
audioMode,
|
||||
);
|
||||
|
||||
const playerName = player === "ksplayer" ? "KSPlayer" : "VLC Player";
|
||||
|
||||
/**
|
||||
* Device profile for Native video player
|
||||
*/
|
||||
const profile = {
|
||||
Name: `1. MPV Player`,
|
||||
Name: `1. ${playerName}`,
|
||||
MaxStaticBitrate: 999_999_999,
|
||||
MaxStreamingBitrate: 999_999_999,
|
||||
CodecProfiles: [
|
||||
@@ -76,7 +174,7 @@ export const generateDeviceProfile = () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
getAudioCodecProfile(),
|
||||
getAudioCodecProfile(platform),
|
||||
],
|
||||
DirectPlayProfiles: [
|
||||
{
|
||||
@@ -84,9 +182,9 @@ export const generateDeviceProfile = () => {
|
||||
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: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts,truehd",
|
||||
AudioCodec: directPlayCodec,
|
||||
},
|
||||
getAudioDirectPlayProfile(),
|
||||
getAudioDirectPlayProfile(platform),
|
||||
],
|
||||
TranscodingProfiles: [
|
||||
{
|
||||
@@ -96,6 +194,7 @@ export const generateDeviceProfile = () => {
|
||||
Container: "ts",
|
||||
VideoCodec: "h264, hevc",
|
||||
AudioCodec: "aac,mp3,ac3,dts",
|
||||
MaxAudioChannels: maxAudioChannels,
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
|
||||
Reference in New Issue
Block a user