mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-17 14:31:58 +01:00
feat: MPV player for both Android and iOS with added HW decoding PiP (with subtitles) (#1332)
Co-authored-by: Alex Kim <alexkim@Alexs-MacBook-Pro.local> Co-authored-by: Alex <111128610+Alexk2309@users.noreply.github.com> Co-authored-by: Simon-Eklundh <simon.eklundh@proton.me>
This commit is contained in:
committed by
GitHub
parent
df2f44e086
commit
f1575ca48b
@@ -3,111 +3,79 @@
|
||||
* 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 MediaTypes from "../../constants/MediaTypes";
|
||||
import { generateDeviceProfile } from "./native";
|
||||
|
||||
/**
|
||||
* Device profile for Native video player
|
||||
* @typedef {"auto" | "stereo" | "5.1" | "passthrough"} AudioTranscodeModeType
|
||||
*/
|
||||
export default {
|
||||
Name: "1. Vlc Player",
|
||||
MaxStaticBitrate: 20_000_000,
|
||||
MaxStreamingBitrate: 20_000_000,
|
||||
CodecProfiles: [
|
||||
{
|
||||
Type: MediaTypes.Video,
|
||||
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,pcm,wma",
|
||||
},
|
||||
],
|
||||
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: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma",
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
Container: "mp3,aac,flac,alac,wav,ogg,wma",
|
||||
AudioCodec:
|
||||
"mp3,aac,flac,alac,opus,vorbis,wma,pcm,mpa,wav,ogg,oga,webma,ape",
|
||||
},
|
||||
],
|
||||
TranscodingProfiles: [
|
||||
{
|
||||
Type: MediaTypes.Video,
|
||||
Context: "Streaming",
|
||||
Protocol: "hls",
|
||||
Container: "ts",
|
||||
VideoCodec: "h264, hevc",
|
||||
AudioCodec: "aac,mp3,ac3",
|
||||
CopyTimestamps: false,
|
||||
EnableSubtitlesInManifest: true,
|
||||
},
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
Context: "Streaming",
|
||||
Protocol: "http",
|
||||
Container: "mp3",
|
||||
AudioCodec: "mp3",
|
||||
MaxAudioChannels: "2",
|
||||
},
|
||||
],
|
||||
SubtitleProfiles: [
|
||||
// Official foramts
|
||||
{ Format: "vtt", Method: "Encode" },
|
||||
|
||||
{ Format: "webvtt", Method: "Encode" },
|
||||
/**
|
||||
* Download-specific subtitle profiles.
|
||||
* These are more permissive than streaming profiles since we can embed subtitles.
|
||||
*/
|
||||
const downloadSubtitleProfiles = [
|
||||
// Official formats
|
||||
{ Format: "vtt", Method: "Encode" },
|
||||
{ Format: "webvtt", Method: "Encode" },
|
||||
{ Format: "srt", Method: "Encode" },
|
||||
{ Format: "subrip", Method: "Encode" },
|
||||
{ Format: "ttml", Method: "Encode" },
|
||||
{ Format: "dvdsub", Method: "Encode" },
|
||||
{ Format: "ass", Method: "Encode" },
|
||||
{ Format: "idx", Method: "Encode" },
|
||||
{ Format: "pgs", Method: "Encode" },
|
||||
{ Format: "pgssub", Method: "Encode" },
|
||||
{ Format: "ssa", Method: "Encode" },
|
||||
// Other formats
|
||||
{ Format: "microdvd", Method: "Encode" },
|
||||
{ Format: "mov_text", Method: "Encode" },
|
||||
{ Format: "mpl2", Method: "Encode" },
|
||||
{ Format: "pjs", Method: "Encode" },
|
||||
{ Format: "realtext", Method: "Encode" },
|
||||
{ Format: "scc", Method: "Encode" },
|
||||
{ Format: "smi", Method: "Encode" },
|
||||
{ Format: "stl", Method: "Encode" },
|
||||
{ Format: "sub", Method: "Encode" },
|
||||
{ Format: "subviewer", Method: "Encode" },
|
||||
{ Format: "teletext", Method: "Encode" },
|
||||
{ Format: "text", Method: "Encode" },
|
||||
{ Format: "vplayer", Method: "Encode" },
|
||||
{ Format: "xsub", Method: "Encode" },
|
||||
];
|
||||
|
||||
{ Format: "srt", Method: "Encode" },
|
||||
/**
|
||||
* Generates a device profile optimized for downloads.
|
||||
* Uses the same audio codec logic as streaming but with download-specific bitrate limits.
|
||||
*
|
||||
* @param {AudioTranscodeModeType} [audioMode="auto"] - Audio transcoding mode
|
||||
* @returns {Object} Jellyfin device profile for downloads
|
||||
*/
|
||||
export const generateDownloadProfile = (audioMode = "auto") => {
|
||||
// Get the base profile with proper audio codec configuration
|
||||
const baseProfile = generateDeviceProfile({ audioMode });
|
||||
|
||||
{ Format: "subrip", Method: "Encode" },
|
||||
|
||||
{ Format: "ttml", Method: "Encode" },
|
||||
|
||||
{ Format: "dvdsub", Method: "Encode" },
|
||||
|
||||
{ Format: "ass", Method: "Encode" },
|
||||
|
||||
{ Format: "idx", Method: "Encode" },
|
||||
|
||||
{ Format: "pgs", Method: "Encode" },
|
||||
|
||||
{ Format: "pgssub", Method: "Encode" },
|
||||
|
||||
{ Format: "ssa", Method: "Encode" },
|
||||
|
||||
// Other formats
|
||||
{ Format: "microdvd", Method: "Encode" },
|
||||
|
||||
{ Format: "mov_text", Method: "Encode" },
|
||||
|
||||
{ Format: "mpl2", Method: "Encode" },
|
||||
|
||||
{ Format: "pjs", Method: "Encode" },
|
||||
|
||||
{ Format: "realtext", Method: "Encode" },
|
||||
|
||||
{ Format: "scc", Method: "Encode" },
|
||||
|
||||
{ Format: "smi", Method: "Encode" },
|
||||
|
||||
{ Format: "stl", Method: "Encode" },
|
||||
|
||||
{ Format: "sub", Method: "Encode" },
|
||||
|
||||
{ Format: "subviewer", Method: "Encode" },
|
||||
|
||||
{ Format: "teletext", Method: "Encode" },
|
||||
|
||||
{ Format: "text", Method: "Encode" },
|
||||
|
||||
{ Format: "vplayer", Method: "Encode" },
|
||||
|
||||
{ Format: "xsub", Method: "Encode" },
|
||||
],
|
||||
// Override with download-specific settings
|
||||
return {
|
||||
...baseProfile,
|
||||
Name: "1. MPV Download",
|
||||
// Limit bitrate for downloads (20 Mbps)
|
||||
MaxStaticBitrate: 20_000_000,
|
||||
MaxStreamingBitrate: 20_000_000,
|
||||
// Use download-specific subtitle profiles
|
||||
SubtitleProfiles: downloadSubtitleProfiles,
|
||||
// Update transcoding profiles with download-specific settings
|
||||
TranscodingProfiles: baseProfile.TranscodingProfiles.map((profile) => {
|
||||
if (profile.Type === "Video") {
|
||||
return {
|
||||
...profile,
|
||||
CopyTimestamps: false,
|
||||
EnableSubtitlesInManifest: true,
|
||||
};
|
||||
}
|
||||
return profile;
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
// Default export for backward compatibility
|
||||
export default generateDownloadProfile();
|
||||
|
||||
5
utils/profiles/native.d.ts
vendored
5
utils/profiles/native.d.ts
vendored
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
export type PlatformType = "ios" | "android";
|
||||
export type PlayerType = "vlc" | "ksplayer";
|
||||
export type PlayerType = "mpv";
|
||||
export type AudioTranscodeModeType = "auto" | "stereo" | "5.1" | "passthrough";
|
||||
|
||||
export interface ProfileOptions {
|
||||
@@ -18,3 +18,6 @@ export interface ProfileOptions {
|
||||
}
|
||||
|
||||
export function generateDeviceProfile(options?: ProfileOptions): any;
|
||||
|
||||
declare const _default: any;
|
||||
export default _default;
|
||||
|
||||
@@ -9,22 +9,22 @@ import { getSubtitleProfiles } from "./subtitles";
|
||||
|
||||
/**
|
||||
* @typedef {"ios" | "android"} PlatformType
|
||||
* @typedef {"vlc" | "ksplayer"} PlayerType
|
||||
* @typedef {"mpv"} PlayerType
|
||||
* @typedef {"auto" | "stereo" | "5.1" | "passthrough"} AudioTranscodeModeType
|
||||
*
|
||||
* @typedef {Object} ProfileOptions
|
||||
* @property {PlatformType} [platform] - Target platform
|
||||
* @property {PlayerType} [player] - Video player being used
|
||||
* @property {PlayerType} [player] - Video player being used (MPV only)
|
||||
* @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.
|
||||
* 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) => {
|
||||
if (platform === "ios") {
|
||||
// iOS AVPlayer supported formats
|
||||
// iOS audio formats supported by MPV
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Container: "mp3,m4a,aac,flac,alac,wav,aiff,caf",
|
||||
@@ -32,7 +32,7 @@ const getAudioDirectPlayProfile = (platform) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Android ExoPlayer supported formats
|
||||
// Android audio formats supported by MPV
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Container: "mp3,m4a,aac,ogg,flac,wav,webm,mka",
|
||||
@@ -40,16 +40,20 @@ const getAudioDirectPlayProfile = (platform) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Audio codec profiles for standalone audio items in MPV player.
|
||||
* These define codec constraints for audio file playback.
|
||||
*/
|
||||
const getAudioCodecProfile = (platform) => {
|
||||
if (platform === "ios") {
|
||||
// iOS AVPlayer codec constraints
|
||||
// iOS audio codec constraints for MPV
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,pcm",
|
||||
};
|
||||
}
|
||||
|
||||
// Android ExoPlayer codec constraints
|
||||
// Android audio codec constraints for MPV
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Codec: "aac,ac3,eac3,mp3,flac,vorbis,opus,pcm",
|
||||
@@ -57,72 +61,61 @@ const getAudioCodecProfile = (platform) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the video audio codec configuration based on platform, player, and audio mode.
|
||||
* Gets the video audio codec configuration based on platform 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.
|
||||
* 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.
|
||||
*
|
||||
* @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 getVideoAudioCodecs = (platform, audioMode) => {
|
||||
// Base codecs
|
||||
const baseCodecs = "aac,mp3,flac,opus,vorbis";
|
||||
|
||||
// Surround codecs that VLC handles well (downmixes properly)
|
||||
// Surround codecs
|
||||
const surroundCodecs = "ac3,eac3,dts";
|
||||
|
||||
// Lossless HD codecs that cause issues with VLC's downmixing on mobile
|
||||
// Lossless HD codecs - MPV decodes these and downmixes as needed
|
||||
const losslessHdCodecs = "truehd";
|
||||
|
||||
// Platform-specific codecs
|
||||
const platformCodecs = platform === "ios" ? "alac,wma" : "wma";
|
||||
|
||||
// Handle explicit user settings first
|
||||
// MPV can decode all codecs - only channel count varies by mode
|
||||
const allCodecs = `${baseCodecs},${surroundCodecs},${losslessHdCodecs},${platformCodecs}`;
|
||||
|
||||
switch (audioMode) {
|
||||
case "stereo":
|
||||
// Force stereo transcoding - only allow basic codecs
|
||||
// Limit to 2 channels - MPV will decode and downmix
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${platformCodecs}`,
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "2",
|
||||
};
|
||||
|
||||
case "5.1":
|
||||
// Allow up to 5.1 - include surround codecs but not lossless HD
|
||||
// Limit to 6 channels
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${surroundCodecs},${platformCodecs}`,
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "6",
|
||||
};
|
||||
|
||||
case "passthrough":
|
||||
// Allow all codecs - for users with external DAC/receiver
|
||||
// Allow up to 8 channels - for external DAC/receiver setups
|
||||
return {
|
||||
directPlayCodec: `${baseCodecs},${surroundCodecs},${losslessHdCodecs},${platformCodecs}`,
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "8",
|
||||
};
|
||||
|
||||
default:
|
||||
// Auto mode: platform and player-specific defaults
|
||||
break;
|
||||
// Auto mode: default to 5.1 (6 channels)
|
||||
return {
|
||||
directPlayCodec: allCodecs,
|
||||
maxAudioChannels: "6",
|
||||
};
|
||||
}
|
||||
|
||||
// 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",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -133,22 +126,18 @@ const getVideoAudioCodecs = (platform, player, audioMode) => {
|
||||
*/
|
||||
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
|
||||
* Device profile for MPV player
|
||||
*/
|
||||
const profile = {
|
||||
Name: `1. ${playerName}`,
|
||||
Name: "1. MPV",
|
||||
MaxStaticBitrate: 999_999_999,
|
||||
MaxStreamingBitrate: 999_999_999,
|
||||
CodecProfiles: [
|
||||
@@ -210,3 +199,6 @@ export const generateDeviceProfile = (options = {}) => {
|
||||
|
||||
return profile;
|
||||
};
|
||||
|
||||
// Default export for backward compatibility
|
||||
export default generateDeviceProfile();
|
||||
|
||||
19
utils/profiles/trackplayer.d.ts
vendored
Normal file
19
utils/profiles/trackplayer.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
export type PlatformType = "ios" | "android";
|
||||
|
||||
export interface TrackPlayerProfileOptions {
|
||||
/** Target platform */
|
||||
platform?: PlatformType;
|
||||
}
|
||||
|
||||
export function generateTrackPlayerProfile(
|
||||
options?: TrackPlayerProfileOptions,
|
||||
): any;
|
||||
|
||||
declare const _default: any;
|
||||
export default _default;
|
||||
95
utils/profiles/trackplayer.js
Normal file
95
utils/profiles/trackplayer.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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 { Platform } from "react-native";
|
||||
import MediaTypes from "../../constants/MediaTypes";
|
||||
|
||||
/**
|
||||
* @typedef {"ios" | "android"} PlatformType
|
||||
*
|
||||
* @typedef {Object} TrackPlayerProfileOptions
|
||||
* @property {PlatformType} [platform] - Target platform
|
||||
*/
|
||||
|
||||
/**
|
||||
* Audio direct play profiles for react-native-track-player.
|
||||
* iOS uses AVPlayer, Android uses ExoPlayer - each has different codec support.
|
||||
*
|
||||
* @param {PlatformType} platform
|
||||
*/
|
||||
const getDirectPlayProfile = (platform) => {
|
||||
if (platform === "ios") {
|
||||
// iOS AVPlayer supported formats
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Container: "mp3,m4a,aac,flac,alac,wav,aiff,caf",
|
||||
AudioCodec: "mp3,aac,alac,flac,opus,pcm",
|
||||
};
|
||||
}
|
||||
|
||||
// Android ExoPlayer supported formats
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Container: "mp3,m4a,aac,ogg,flac,wav,webm,mka",
|
||||
AudioCodec: "mp3,aac,flac,vorbis,opus,pcm",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Audio codec profiles for react-native-track-player.
|
||||
*
|
||||
* @param {PlatformType} platform
|
||||
*/
|
||||
const getCodecProfile = (platform) => {
|
||||
if (platform === "ios") {
|
||||
// iOS AVPlayer codec constraints
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,pcm",
|
||||
};
|
||||
}
|
||||
|
||||
// Android ExoPlayer codec constraints
|
||||
return {
|
||||
Type: MediaTypes.Audio,
|
||||
Codec: "aac,ac3,eac3,mp3,flac,vorbis,opus,pcm",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a device profile for music playback via react-native-track-player.
|
||||
*
|
||||
* This profile is specifically for standalone audio playback using:
|
||||
* - AVPlayer on iOS
|
||||
* - ExoPlayer on Android
|
||||
*
|
||||
* @param {TrackPlayerProfileOptions} [options] - Profile configuration options
|
||||
* @returns {Object} Jellyfin device profile for track player
|
||||
*/
|
||||
export const generateTrackPlayerProfile = (options = {}) => {
|
||||
const platform = options.platform || Platform.OS;
|
||||
|
||||
return {
|
||||
Name: "Track Player",
|
||||
MaxStaticBitrate: 320_000_000,
|
||||
MaxStreamingBitrate: 320_000_000,
|
||||
CodecProfiles: [getCodecProfile(platform)],
|
||||
DirectPlayProfiles: [getDirectPlayProfile(platform)],
|
||||
TranscodingProfiles: [
|
||||
{
|
||||
Type: MediaTypes.Audio,
|
||||
Context: "Streaming",
|
||||
Protocol: "http",
|
||||
Container: "mp3",
|
||||
AudioCodec: "mp3",
|
||||
MaxAudioChannels: "2",
|
||||
},
|
||||
],
|
||||
SubtitleProfiles: [],
|
||||
};
|
||||
};
|
||||
|
||||
// Default export for convenience
|
||||
export default generateTrackPlayerProfile();
|
||||
Reference in New Issue
Block a user