mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
feat: initial subtitle support
This commit is contained in:
@@ -8,10 +8,19 @@ import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import { formatTimeString, secondsToMs, ticksToMs } from "@/utils/time";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {
|
||||
BaseItemDto,
|
||||
type MediaStream,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { Image } from "expo-image";
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Platform,
|
||||
@@ -34,6 +43,8 @@ import { Text } from "../common/Text";
|
||||
import { Loader } from "../Loader";
|
||||
import { VlcPlayerViewRef } from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
||||
import { VideoDebugInfo } from "../vlc/VideoDebugInfo";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
|
||||
interface Props {
|
||||
item: BaseItemDto;
|
||||
@@ -274,6 +285,14 @@ export const Controls: React.FC<Props> = ({
|
||||
setIgnoreSafeAreas((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState<
|
||||
MediaStream | undefined
|
||||
>(undefined);
|
||||
|
||||
const subtitleTracks = useMemo(() => {
|
||||
return item.MediaStreams?.filter((stream) => stream.Type === "Subtitle");
|
||||
}, [item]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -286,6 +305,54 @@ export const Controls: React.FC<Props> = ({
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* <VideoDebugInfo playerRef={videoRef} /> */}
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: insets.top,
|
||||
left: insets.left,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
className="p-4"
|
||||
>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="aspect-square flex flex-col rounded-xl items-center justify-center p-2">
|
||||
<Ionicons
|
||||
name="ellipsis-horizontal-circle-outline"
|
||||
size={32}
|
||||
color={"white"}
|
||||
/>
|
||||
</View>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
loop={true}
|
||||
side="bottom"
|
||||
align="start"
|
||||
alignOffset={0}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Label>Subtitle tracks</DropdownMenu.Label>
|
||||
{subtitleTracks?.map((sub, idx: number) => (
|
||||
<DropdownMenu.Item
|
||||
key={idx.toString()}
|
||||
onSelect={() => {
|
||||
if (!sub.Index !== undefined && sub.Index !== null)
|
||||
videoRef.current?.setSubtitleTrack(sub.Index!);
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>
|
||||
{sub.DisplayTitle}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
@@ -372,7 +439,7 @@ export const Controls: React.FC<Props> = ({
|
||||
animatedTopStyles,
|
||||
]}
|
||||
pointerEvents={showControls ? "auto" : "none"}
|
||||
className={`flex flex-row items-center space-x-2 z-10 p-4`}
|
||||
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={toggleIgnoreSafeAreas}
|
||||
|
||||
@@ -8,19 +8,13 @@ import { useState, useEffect } from "react";
|
||||
import { View, TouchableOpacity, ViewProps } from "react-native";
|
||||
import { Text } from "../common/Text";
|
||||
import React from "react";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
playbackState: PlaybackStatePayload["nativeEvent"] | null;
|
||||
progress: ProgressUpdatePayload["nativeEvent"] | null;
|
||||
playerRef: React.RefObject<VlcPlayerViewRef>;
|
||||
}
|
||||
|
||||
export const VideoDebugInfo: React.FC<Props> = ({
|
||||
playbackState,
|
||||
progress,
|
||||
playerRef,
|
||||
...props
|
||||
}) => {
|
||||
export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
|
||||
const [audioTracks, setAudioTracks] = useState<TrackInfo[] | null>(null);
|
||||
const [subtitleTracks, setSubtitleTracks] = useState<TrackInfo[] | null>(
|
||||
null
|
||||
@@ -39,36 +33,30 @@ export const VideoDebugInfo: React.FC<Props> = ({
|
||||
fetchTracks();
|
||||
}, [playerRef]);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View className="p-2.5 bg-black mt-2.5" {...props}>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: insets.top,
|
||||
left: insets.left + 8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Text className="font-bold">Playback State:</Text>
|
||||
{playbackState && (
|
||||
<>
|
||||
<Text>Type: {playbackState.type}</Text>
|
||||
<Text>Current Time: {playbackState.currentTime}</Text>
|
||||
<Text>Duration: {playbackState.duration}</Text>
|
||||
<Text>Is Buffering: {playbackState.isBuffering ? "Yes" : "No"}</Text>
|
||||
<Text>Target: {playbackState.target}</Text>
|
||||
</>
|
||||
)}
|
||||
<Text className="font-bold mt-2.5">Progress:</Text>
|
||||
{progress && (
|
||||
<>
|
||||
<Text>Current Time: {progress.currentTime}</Text>
|
||||
<Text>Duration: {progress.duration.toFixed(2)}</Text>
|
||||
</>
|
||||
)}
|
||||
<Text className="font-bold mt-2.5">Audio Tracks:</Text>
|
||||
{audioTracks &&
|
||||
audioTracks.map((track) => (
|
||||
<Text key={track.index}>
|
||||
audioTracks.map((track, index) => (
|
||||
<Text key={index}>
|
||||
{track.name} (Index: {track.index})
|
||||
</Text>
|
||||
))}
|
||||
<Text className="font-bold mt-2.5">Subtitle Tracks:</Text>
|
||||
{subtitleTracks &&
|
||||
subtitleTracks.map((track) => (
|
||||
<Text key={track.index}>
|
||||
subtitleTracks.map((track, index) => (
|
||||
<Text key={index}>
|
||||
{track.name} (Index: {track.index})
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -125,13 +125,29 @@ class VlcPlayerView: ExpoView {
|
||||
media = VLCMedia(path: uri)
|
||||
}
|
||||
}
|
||||
// Apply subtitle options
|
||||
let subtitleOptions = self.getSubtitleOptions()
|
||||
media.addOptions(subtitleOptions)
|
||||
print("Debug: Applied subtitle options: \(subtitleOptions)")
|
||||
|
||||
media.delegate = self
|
||||
// Apply any additional media options
|
||||
if let mediaOptions = mediaOptions {
|
||||
media.addOptions(mediaOptions)
|
||||
print("Debug: Applied additional media options: \(mediaOptions)")
|
||||
} else {
|
||||
print("Debug: No additional media options provided")
|
||||
}
|
||||
|
||||
let subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?? -1
|
||||
print("Debug: Subtitle track index from source: \(subtitleTrackIndex)")
|
||||
|
||||
if subtitleTrackIndex >= -1 {
|
||||
self.setSubtitleTrack(subtitleTrackIndex)
|
||||
print("Debug: Set subtitle track to index: \(subtitleTrackIndex)")
|
||||
} else {
|
||||
print("Debug: Subtitle track index is less than -1, not setting")
|
||||
}
|
||||
|
||||
// Set the media without parsing
|
||||
self.mediaPlayer?.media = media
|
||||
|
||||
if autoplay {
|
||||
@@ -142,6 +158,14 @@ class VlcPlayerView: ExpoView {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func loadExternalSubtitle(_ subtitlePath: String) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.mediaPlayer?.addPlaybackSlave(
|
||||
URL(fileURLWithPath: subtitlePath), type: .subtitle, enforce: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setMuted(_ muted: Bool) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.audio?.isMuted = muted
|
||||
@@ -209,16 +233,25 @@ class VlcPlayerView: ExpoView {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
||||
DispatchQueue.main.async {
|
||||
if trackIndex == -1 {
|
||||
print("Debug: Disabling subtitles")
|
||||
// Disable subtitles
|
||||
self.mediaPlayer?.currentVideoSubTitleIndex = -1
|
||||
} else {
|
||||
print("Debug: Setting subtitle track to index: \(trackIndex)")
|
||||
// Set the subtitle track
|
||||
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
||||
}
|
||||
|
||||
// Print the result
|
||||
if let currentIndex = self.mediaPlayer?.currentVideoSubTitleIndex {
|
||||
print("Debug: Current subtitle track index after setting: \(currentIndex)")
|
||||
} else {
|
||||
print("Debug: Unable to retrieve current subtitle track index")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +451,42 @@ class VlcPlayerView: ExpoView {
|
||||
completion?()
|
||||
}
|
||||
|
||||
private func getSubtitleOptions() -> [String: Any] {
|
||||
return [
|
||||
// Text scaling (100 is default, increase for larger text)
|
||||
"sub-text-scale": "105",
|
||||
|
||||
// Text color (RRGGBB format, 16777215 is white)
|
||||
"freetype-color": "16777215",
|
||||
|
||||
// Outline thickness (reduced from 2 to 1 for less border)
|
||||
"freetype-outline-thickness": "1",
|
||||
|
||||
// Outline color (RRGGBB format, 0 is black)
|
||||
"freetype-outline-color": "0",
|
||||
|
||||
// Text opacity (0-255, 255 is fully opaque)
|
||||
"freetype-opacity": "255",
|
||||
|
||||
// Shadow opacity (increased from 128 to 180 for more shadow)
|
||||
"freetype-shadow-opacity": "180",
|
||||
|
||||
// Shadow offset (increased from 2 to 3 for more pronounced shadow)
|
||||
"freetype-shadow-offset": "3",
|
||||
|
||||
// Text alignment (0: center, 1: left, 2: right)
|
||||
"sub-text-alignment": "0",
|
||||
|
||||
// Vertical margin (from bottom of the screen, in pixels)
|
||||
"sub-margin-bottom": "50",
|
||||
|
||||
// Background opacity (0-255, 0 for no background)
|
||||
"freetype-background-opacity": "64",
|
||||
|
||||
// Background color (RRGGBB format)
|
||||
"freetype-background-color": "0",
|
||||
]
|
||||
}
|
||||
// MARK: - Expo Events
|
||||
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
|
||||
Reference in New Issue
Block a user