mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-23 11:38:03 +00:00
done
This commit is contained in:
@@ -539,11 +539,6 @@ export default function page() {
|
||||
[playbackManager, item?.Id, progress],
|
||||
);
|
||||
|
||||
const _allSubs =
|
||||
stream?.mediaSource.MediaStreams?.filter(
|
||||
(sub) => sub.Type === "Subtitle",
|
||||
).sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)) || [];
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// Add useEffect to handle mounting
|
||||
@@ -582,6 +577,14 @@ export default function page() {
|
||||
videoRef.current?.addSubtitleFile?.(url);
|
||||
}, []);
|
||||
|
||||
const getAudioTracks = useCallback(async () => {
|
||||
return videoRef.current?.getAudioTracks?.() || null;
|
||||
}, []);
|
||||
|
||||
const setAudioTrack = useCallback((index: number) => {
|
||||
videoRef.current?.setAudioTrack?.(index);
|
||||
}, []);
|
||||
|
||||
// Apply MPV subtitle settings when video loads
|
||||
useEffect(() => {
|
||||
if (!isVideoLoaded || !videoRef.current) return;
|
||||
@@ -702,8 +705,10 @@ export default function page() {
|
||||
seek={seek}
|
||||
enableTrickplay={true}
|
||||
getSubtitleTracks={getSubtitleTracks}
|
||||
getAudioTracks={getAudioTracks}
|
||||
offline={offline}
|
||||
setSubtitleTrack={setSubtitleTrack}
|
||||
setAudioTrack={setAudioTrack}
|
||||
setSubtitleURL={setSubtitleURL}
|
||||
aspectRatio={aspectRatio}
|
||||
scaleFactor={scaleFactor}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||
import type { MpvPlayerViewRef, SubtitleTrack } from "@/modules";
|
||||
import type { AudioTrack, MpvPlayerViewRef, SubtitleTrack } from "@/modules";
|
||||
import { DownloadedItem } from "@/providers/Downloads/types";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||
@@ -71,8 +71,10 @@ interface Props {
|
||||
getSubtitleTracks?:
|
||||
| (() => Promise<SubtitleTrack[] | null>)
|
||||
| (() => SubtitleTrack[]);
|
||||
getAudioTracks?: (() => Promise<AudioTrack[] | null>) | (() => AudioTrack[]);
|
||||
setSubtitleURL?: (url: string, customName: string) => void;
|
||||
setSubtitleTrack?: (index: number) => void;
|
||||
setAudioTrack?: (index: number) => void;
|
||||
setVideoAspectRatio?: (aspectRatio: string | null) => Promise<void>;
|
||||
setVideoScaleFactor?: (scaleFactor: number) => Promise<void>;
|
||||
aspectRatio?: AspectRatio;
|
||||
@@ -100,8 +102,10 @@ export const Controls: FC<Props> = ({
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getSubtitleTracks,
|
||||
getAudioTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
setVideoAspectRatio,
|
||||
setVideoScaleFactor,
|
||||
aspectRatio = "default",
|
||||
@@ -501,7 +505,9 @@ export const Controls: FC<Props> = ({
|
||||
previousItem={previousItem}
|
||||
nextItem={nextItem}
|
||||
getSubtitleTracks={getSubtitleTracks}
|
||||
getAudioTracks={getAudioTracks}
|
||||
setSubtitleTrack={setSubtitleTrack}
|
||||
setAudioTrack={setAudioTrack}
|
||||
setSubtitleURL={setSubtitleURL}
|
||||
aspectRatio={aspectRatio}
|
||||
scaleFactor={scaleFactor}
|
||||
|
||||
@@ -35,7 +35,9 @@ interface HeaderControlsProps {
|
||||
previousItem?: BaseItemDto | null;
|
||||
nextItem?: BaseItemDto | null;
|
||||
getSubtitleTracks?: (() => Promise<any[] | null>) | (() => any[]);
|
||||
getAudioTracks?: (() => Promise<any[] | null>) | (() => any[]);
|
||||
setSubtitleTrack?: (index: number) => void;
|
||||
setAudioTrack?: (index: number) => void;
|
||||
setSubtitleURL?: (url: string, customName: string) => void;
|
||||
aspectRatio?: AspectRatio;
|
||||
scaleFactor?: ScaleFactor;
|
||||
@@ -57,7 +59,9 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
||||
previousItem,
|
||||
nextItem,
|
||||
getSubtitleTracks,
|
||||
getAudioTracks,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
setSubtitleURL,
|
||||
aspectRatio = "default",
|
||||
scaleFactor = 1.0,
|
||||
@@ -111,7 +115,9 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
||||
{!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
|
||||
<VideoProvider
|
||||
getSubtitleTracks={getSubtitleTracks}
|
||||
getAudioTracks={getAudioTracks}
|
||||
setSubtitleTrack={setSubtitleTrack}
|
||||
setAudioTrack={setAudioTrack}
|
||||
setSubtitleURL={setSubtitleURL}
|
||||
>
|
||||
<View pointerEvents='auto'>
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { SubtitleTrack } from "@/modules";
|
||||
import type { AudioTrack, SubtitleTrack } from "@/modules";
|
||||
import type { Track } from "../types";
|
||||
import { useControlContext } from "./ControlContext";
|
||||
|
||||
interface VideoContextProps {
|
||||
subtitleTracks: Track[] | null;
|
||||
audioTracks: Track[] | null;
|
||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||
}
|
||||
@@ -27,21 +28,29 @@ interface VideoProviderProps {
|
||||
| (() => Promise<SubtitleTrack[] | null>)
|
||||
| (() => SubtitleTrack[])
|
||||
| undefined;
|
||||
getAudioTracks:
|
||||
| (() => Promise<AudioTrack[] | null>)
|
||||
| (() => AudioTrack[])
|
||||
| undefined;
|
||||
setSubtitleTrack: ((index: number) => void) | undefined;
|
||||
setAudioTrack: ((index: number) => void) | undefined;
|
||||
setSubtitleURL: ((url: string, customName: string) => void) | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Video context provider for managing subtitle tracks.
|
||||
s * Video context provider for managing subtitle and audio tracks.
|
||||
* MPV player is used for all playback.
|
||||
*/
|
||||
export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
children,
|
||||
getSubtitleTracks,
|
||||
getAudioTracks,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
setSubtitleURL,
|
||||
}) => {
|
||||
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
||||
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
||||
|
||||
const ControlContext = useControlContext();
|
||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||
@@ -122,6 +131,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
let subtitleData: SubtitleTrack[] | null = null;
|
||||
try {
|
||||
subtitleData = await getSubtitleTracks();
|
||||
console.log("subtitleData", subtitleData);
|
||||
} catch (error) {
|
||||
console.log("[VideoContext] Failed to get subtitle tracks:", error);
|
||||
return;
|
||||
@@ -169,10 +179,49 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
fetchTracks();
|
||||
}, [isVideoLoaded, getSubtitleTracks]);
|
||||
|
||||
// Fetch audio tracks
|
||||
useEffect(() => {
|
||||
const fetchAudioTracks = async () => {
|
||||
if (getAudioTracks) {
|
||||
let audioData: AudioTrack[] | null = null;
|
||||
try {
|
||||
audioData = await getAudioTracks();
|
||||
console.log("audioData", audioData);
|
||||
} catch (error) {
|
||||
console.log("[VideoContext] Failed to get audio tracks:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const allAudio =
|
||||
mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || [];
|
||||
|
||||
let embedAudioIndex = 0;
|
||||
const processedAudio: Track[] = allAudio?.map((audio) => {
|
||||
const mpvIndex = audioData?.at(embedAudioIndex)?.id ?? 1;
|
||||
embedAudioIndex++;
|
||||
return {
|
||||
name: audio.DisplayTitle || "Undefined Audio",
|
||||
index: audio.Index ?? -1,
|
||||
setTrack: () => {
|
||||
setAudioTrack?.(mpvIndex);
|
||||
router.setParams({
|
||||
audioIndex: audio.Index?.toString() ?? "0",
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
setAudioTracks(processedAudio);
|
||||
}
|
||||
};
|
||||
fetchAudioTracks();
|
||||
}, [isVideoLoaded, getAudioTracks]);
|
||||
|
||||
return (
|
||||
<VideoContext.Provider
|
||||
value={{
|
||||
subtitleTracks,
|
||||
audioTracks,
|
||||
setSubtitleTrack,
|
||||
setSubtitleURL,
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ export { default as BackgroundDownloader } from "./background-downloader";
|
||||
// Type aliases for backward compatibility during migration
|
||||
// These map old VLC type names to new MPV equivalents
|
||||
export type {
|
||||
AudioTrack,
|
||||
MpvPlayerViewProps,
|
||||
MpvPlayerViewRef,
|
||||
OnErrorEventPayload,
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//
|
||||
// MPVSoftwareRenderer.swift
|
||||
// test
|
||||
//
|
||||
// Created by Francesco on 28/09/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Libmpv
|
||||
import CoreMedia
|
||||
@@ -115,9 +108,8 @@ final class MPVSoftwareRenderer {
|
||||
setOption(name: "demuxer-readahead-secs", value: "20")
|
||||
|
||||
// Subtitle options - blend into video for software renderer
|
||||
setOption(name: "blend-subtitles", value: "video")
|
||||
setOption(name: "sub-visibility", value: "yes")
|
||||
setOption(name: "osd-level", value: "0")
|
||||
setOption(name: "sub-auto", value: "yes")
|
||||
setOption(name: "subs-fallback", value: "yes")
|
||||
|
||||
let initStatus = mpv_initialize(handle)
|
||||
guard initStatus >= 0 else {
|
||||
@@ -979,11 +971,15 @@ final class MPVSoftwareRenderer {
|
||||
// MARK: - Subtitle Controls
|
||||
|
||||
func getSubtitleTracks() -> [[String: Any]] {
|
||||
guard let handle = mpv else { return [] }
|
||||
guard let handle = mpv else {
|
||||
Logger.shared.log("getSubtitleTracks: mpv handle is nil", type: "Warn")
|
||||
return []
|
||||
}
|
||||
var tracks: [[String: Any]] = []
|
||||
|
||||
var trackCount: Int64 = 0
|
||||
getProperty(handle: handle, name: "track-list/count", format: MPV_FORMAT_INT64, value: &trackCount)
|
||||
Logger.shared.log("getSubtitleTracks: total track count = \(trackCount)", type: "Info")
|
||||
|
||||
for i in 0..<trackCount {
|
||||
var trackType: String?
|
||||
@@ -1010,13 +1006,16 @@ final class MPVSoftwareRenderer {
|
||||
getProperty(handle: handle, name: "track-list/\(i)/selected", format: MPV_FORMAT_FLAG, value: &selected)
|
||||
track["selected"] = selected != 0
|
||||
|
||||
Logger.shared.log("getSubtitleTracks: found sub track id=\(trackId), title=\(track["title"] ?? "none"), lang=\(track["lang"] ?? "none")", type: "Info")
|
||||
tracks.append(track)
|
||||
}
|
||||
|
||||
Logger.shared.log("getSubtitleTracks: returning \(tracks.count) subtitle tracks", type: "Info")
|
||||
return tracks
|
||||
}
|
||||
|
||||
func setSubtitleTrack(_ trackId: Int) {
|
||||
Logger.shared.log("setSubtitleTrack: setting sid to \(trackId)", type: "Info")
|
||||
setProperty(name: "sid", value: String(trackId))
|
||||
}
|
||||
|
||||
@@ -1061,4 +1060,71 @@ final class MPVSoftwareRenderer {
|
||||
func setSubtitleFontSize(_ size: Int) {
|
||||
setProperty(name: "sub-font-size", value: String(size))
|
||||
}
|
||||
|
||||
// MARK: - Audio Track Controls
|
||||
|
||||
func getAudioTracks() -> [[String: Any]] {
|
||||
guard let handle = mpv else {
|
||||
Logger.shared.log("getAudioTracks: mpv handle is nil", type: "Warn")
|
||||
return []
|
||||
}
|
||||
var tracks: [[String: Any]] = []
|
||||
|
||||
var trackCount: Int64 = 0
|
||||
getProperty(handle: handle, name: "track-list/count", format: MPV_FORMAT_INT64, value: &trackCount)
|
||||
|
||||
for i in 0..<trackCount {
|
||||
var trackType: String?
|
||||
if let typeStr = getStringProperty(handle: handle, name: "track-list/\(i)/type") {
|
||||
trackType = typeStr
|
||||
}
|
||||
|
||||
guard trackType == "audio" else { continue }
|
||||
|
||||
var trackId: Int64 = 0
|
||||
getProperty(handle: handle, name: "track-list/\(i)/id", format: MPV_FORMAT_INT64, value: &trackId)
|
||||
|
||||
var track: [String: Any] = ["id": Int(trackId)]
|
||||
|
||||
if let title = getStringProperty(handle: handle, name: "track-list/\(i)/title") {
|
||||
track["title"] = title
|
||||
}
|
||||
|
||||
if let lang = getStringProperty(handle: handle, name: "track-list/\(i)/lang") {
|
||||
track["lang"] = lang
|
||||
}
|
||||
|
||||
if let codec = getStringProperty(handle: handle, name: "track-list/\(i)/codec") {
|
||||
track["codec"] = codec
|
||||
}
|
||||
|
||||
var channels: Int64 = 0
|
||||
getProperty(handle: handle, name: "track-list/\(i)/audio-channels", format: MPV_FORMAT_INT64, value: &channels)
|
||||
if channels > 0 {
|
||||
track["channels"] = Int(channels)
|
||||
}
|
||||
|
||||
var selected: Int32 = 0
|
||||
getProperty(handle: handle, name: "track-list/\(i)/selected", format: MPV_FORMAT_FLAG, value: &selected)
|
||||
track["selected"] = selected != 0
|
||||
|
||||
Logger.shared.log("getAudioTracks: found audio track id=\(trackId), title=\(track["title"] ?? "none"), lang=\(track["lang"] ?? "none")", type: "Info")
|
||||
tracks.append(track)
|
||||
}
|
||||
|
||||
Logger.shared.log("getAudioTracks: returning \(tracks.count) audio tracks", type: "Info")
|
||||
return tracks
|
||||
}
|
||||
|
||||
func setAudioTrack(_ trackId: Int) {
|
||||
Logger.shared.log("setAudioTrack: setting aid to \(trackId)", type: "Info")
|
||||
setProperty(name: "aid", value: String(trackId))
|
||||
}
|
||||
|
||||
func getCurrentAudioTrack() -> Int {
|
||||
guard let handle = mpv else { return 0 }
|
||||
var aid: Int64 = 0
|
||||
getProperty(handle: handle, name: "aid", format: MPV_FORMAT_INT64, value: &aid)
|
||||
return Int(aid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,19 @@ public class MpvPlayerModule: Module {
|
||||
AsyncFunction("setSubtitleFontSize") { (view: MpvPlayerView, size: Int) in
|
||||
view.setSubtitleFontSize(size)
|
||||
}
|
||||
|
||||
// Audio track functions
|
||||
AsyncFunction("getAudioTracks") { (view: MpvPlayerView) -> [[String: Any]] in
|
||||
return view.getAudioTracks()
|
||||
}
|
||||
|
||||
AsyncFunction("setAudioTrack") { (view: MpvPlayerView, trackId: Int) in
|
||||
view.setAudioTrack(trackId)
|
||||
}
|
||||
|
||||
AsyncFunction("getCurrentAudioTrack") { (view: MpvPlayerView) -> Int in
|
||||
return view.getCurrentAudioTrack()
|
||||
}
|
||||
|
||||
// Defines events that the view can send to JavaScript
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError")
|
||||
|
||||
@@ -168,6 +168,20 @@ class MpvPlayerView: ExpoView {
|
||||
renderer?.addSubtitleFile(url: url)
|
||||
}
|
||||
|
||||
// MARK: - Audio Track Controls
|
||||
|
||||
func getAudioTracks() -> [[String: Any]] {
|
||||
return renderer?.getAudioTracks() ?? []
|
||||
}
|
||||
|
||||
func setAudioTrack(_ trackId: Int) {
|
||||
renderer?.setAudioTrack(trackId)
|
||||
}
|
||||
|
||||
func getCurrentAudioTrack() -> Int {
|
||||
return renderer?.getCurrentAudioTrack() ?? 0
|
||||
}
|
||||
|
||||
// MARK: - Subtitle Positioning
|
||||
|
||||
func setSubtitlePosition(_ position: Int) {
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface MpvPlayerViewRef {
|
||||
stopPictureInPicture: () => Promise<void>;
|
||||
isPictureInPictureSupported: () => Promise<boolean>;
|
||||
isPictureInPictureActive: () => Promise<boolean>;
|
||||
// Subtitle controls
|
||||
getSubtitleTracks: () => Promise<SubtitleTrack[]>;
|
||||
setSubtitleTrack: (trackId: number) => Promise<void>;
|
||||
disableSubtitles: () => Promise<void>;
|
||||
@@ -68,10 +69,24 @@ export interface MpvPlayerViewRef {
|
||||
setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise<void>;
|
||||
setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => Promise<void>;
|
||||
setSubtitleFontSize: (size: number) => Promise<void>;
|
||||
// Audio controls
|
||||
getAudioTracks: () => Promise<AudioTrack[]>;
|
||||
setAudioTrack: (trackId: number) => Promise<void>;
|
||||
getCurrentAudioTrack: () => Promise<number>;
|
||||
}
|
||||
|
||||
export type SubtitleTrack = {
|
||||
id: number;
|
||||
title?: string;
|
||||
lang?: string;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
export type AudioTrack = {
|
||||
id: number;
|
||||
title?: string;
|
||||
lang?: string;
|
||||
codec?: string;
|
||||
channels?: number;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
@@ -84,6 +84,16 @@ export default React.forwardRef<MpvPlayerViewRef, MpvPlayerViewProps>(
|
||||
setSubtitleFontSize: async (size: number) => {
|
||||
await nativeRef.current?.setSubtitleFontSize(size);
|
||||
},
|
||||
// Audio controls
|
||||
getAudioTracks: async () => {
|
||||
return await nativeRef.current?.getAudioTracks();
|
||||
},
|
||||
setAudioTrack: async (trackId: number) => {
|
||||
await nativeRef.current?.setAudioTrack(trackId);
|
||||
},
|
||||
getCurrentAudioTrack: async () => {
|
||||
return await nativeRef.current?.getCurrentAudioTrack();
|
||||
},
|
||||
}));
|
||||
|
||||
return <NativeView ref={nativeRef} {...props} />;
|
||||
|
||||
Reference in New Issue
Block a user