Compare commits

...

4 commits

Author SHA1 Message Date
tapframe
eb6fcf639f ksp sub updates 2026-01-11 00:46:30 +05:30
tapframe
a85cc93026 internal sub offset, bg fix android 2026-01-10 23:43:32 +05:30
tapframe
56fd18a8e9 Merge branch 'main' of https://github.com/tapframe/NuvioStreaming 2026-01-10 23:31:47 +05:30
tapframe
82d0ebb714 Fix ExoPlayer subtitle styling and iOS MPV config
- Fix subtitle track selection (only first track worked)
- Fix subtitle styling (background, outline, bottom offset)
- Update iOS MPV to match Wayve settings (Vulkan, HDR, stability options)
- Add patch-package for react-native-video fixes
2026-01-10 23:31:08 +05:30
23 changed files with 98141 additions and 1420 deletions

View file

@ -1,69 +0,0 @@
//
// KSPlayerManager.m
// Nuvio
//
// Created by KSPlayer integration
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE (KSPlayerViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(rate, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL)
RCT_EXPORT_VIEW_PROPERTY(usesExternalPlaybackWhileExternalScreenIsActive, BOOL)
RCT_EXPORT_VIEW_PROPERTY(subtitleBottomOffset, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(subtitleFontSize, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(subtitleTextColor, NSString)
RCT_EXPORT_VIEW_PROPERTY(subtitleBackgroundColor, NSString)
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString)
// Event properties
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBuffering, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBufferingProgress, RCTDirectEventBlock)
RCT_EXTERN_METHOD(seek : (nonnull NSNumber *)node toTime : (nonnull NSNumber *)
time)
RCT_EXTERN_METHOD(setSource : (nonnull NSNumber *)
node source : (nonnull NSDictionary *)source)
RCT_EXTERN_METHOD(setPaused : (nonnull NSNumber *)node paused : (BOOL)paused)
RCT_EXTERN_METHOD(setVolume : (nonnull NSNumber *)
node volume : (nonnull NSNumber *)volume)
RCT_EXTERN_METHOD(setPlaybackRate : (nonnull NSNumber *)
node rate : (nonnull NSNumber *)rate)
RCT_EXTERN_METHOD(setAudioTrack : (nonnull NSNumber *)
node trackId : (nonnull NSNumber *)trackId)
RCT_EXTERN_METHOD(setTextTrack : (nonnull NSNumber *)
node trackId : (nonnull NSNumber *)trackId)
RCT_EXTERN_METHOD(getTracks : (nonnull NSNumber *)node resolve : (
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setAllowsExternalPlayback : (nonnull NSNumber *)
node allows : (BOOL)allows)
RCT_EXTERN_METHOD(setUsesExternalPlaybackWhileExternalScreenIsActive : (
nonnull NSNumber *)node uses : (BOOL)uses)
RCT_EXTERN_METHOD(getAirPlayState : (nonnull NSNumber *)node resolve : (
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(showAirPlayPicker : (nonnull NSNumber *)node)
@end
@interface RCT_EXTERN_MODULE (KSPlayerModule, RCTEventEmitter)
RCT_EXTERN_METHOD(getTracks : (NSNumber *)nodeTag resolve : (
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getAirPlayState : (NSNumber *)nodeTag resolve : (
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(showAirPlayPicker : (NSNumber *)nodeTag)
@end

View file

@ -1,71 +0,0 @@
//
// KSPlayerModule.swift
// Nuvio
//
// Created by KSPlayer integration
//
import Foundation
import KSPlayer
import React
@objc(KSPlayerModule)
class KSPlayerModule: RCTEventEmitter {
override static func requiresMainQueueSetup() -> Bool {
return true
}
override func supportedEvents() -> [String]! {
return [
"KSPlayer-onLoad",
"KSPlayer-onProgress",
"KSPlayer-onBuffering",
"KSPlayer-onEnd",
"KSPlayer-onError"
]
}
@objc func getTracks(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let nodeTag = nodeTag else {
reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil)
return
}
DispatchQueue.main.async {
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
viewManager.getTracks(nodeTag, resolve: resolve, reject: reject)
} else {
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
}
}
}
@objc func getAirPlayState(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let nodeTag = nodeTag else {
reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil)
return
}
DispatchQueue.main.async {
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
viewManager.getAirPlayState(nodeTag, resolve: resolve, reject: reject)
} else {
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
}
}
}
@objc func showAirPlayPicker(_ nodeTag: NSNumber?) {
guard let nodeTag = nodeTag else {
print("[KSPlayerModule] showAirPlayPicker called with nil nodeTag")
return
}
print("[KSPlayerModule] showAirPlayPicker called for nodeTag: \(nodeTag)")
DispatchQueue.main.async {
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
print("[KSPlayerModule] Found KSPlayerViewManager, calling showAirPlayPicker")
viewManager.showAirPlayPicker(nodeTag)
} else {
print("[KSPlayerModule] Could not find KSPlayerViewManager")
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,152 +0,0 @@
//
// KSPlayerViewManager.swift
// Nuvio
//
// Created by KSPlayer integration
//
import Foundation
import KSPlayer
import React
@objc(KSPlayerViewManager)
class KSPlayerViewManager: RCTViewManager {
// Not needed for RCTViewManager-based views; events are exported via Objective-C externs in KSPlayerManager.m
override func view() -> UIView! {
let view = KSPlayerView()
view.viewManager = self
return view
}
override static func requiresMainQueueSetup() -> Bool {
return true
}
override func constantsToExport() -> [AnyHashable : Any]! {
return [
"EventTypes": [
"onLoad": "onLoad",
"onProgress": "onProgress",
"onBuffering": "onBuffering",
"onEnd": "onEnd",
"onError": "onError",
"onBufferingProgress": "onBufferingProgress"
]
]
}
// No-op: events are sent via direct event blocks on the view
@objc func seek(_ node: NSNumber, toTime time: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.seek(to: TimeInterval(truncating: time))
}
}
}
@objc func setSource(_ node: NSNumber, source: NSDictionary) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setSource(source)
}
}
}
@objc func setPaused(_ node: NSNumber, paused: Bool) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setPaused(paused)
}
}
}
@objc func setVolume(_ node: NSNumber, volume: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setVolume(Float(truncating: volume))
}
}
}
@objc func setPlaybackRate(_ node: NSNumber, rate: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setPlaybackRate(Float(truncating: rate))
}
}
}
@objc func setAudioTrack(_ node: NSNumber, trackId: NSNumber) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setAudioTrack(Int(truncating: trackId))
}
}
}
@objc func setTextTrack(_ node: NSNumber, trackId: NSNumber) {
NSLog("[KSPlayerViewManager] setTextTrack called - node: %@, trackId: %@", node, trackId)
DispatchQueue.main.async {
NSLog("[KSPlayerViewManager] setTextTrack on main queue - looking for view with tag: %@", node)
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
NSLog("[KSPlayerViewManager] Found view, calling setTextTrack(%d)", Int(truncating: trackId))
view.setTextTrack(Int(truncating: trackId))
} else {
NSLog("[KSPlayerViewManager] ERROR - Could not find KSPlayerView for tag: %@", node)
}
}
}
@objc func getTracks(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
let tracks = view.getAvailableTracks()
resolve(tracks)
} else {
reject("NO_VIEW", "KSPlayerView not found", nil)
}
}
}
// AirPlay methods
@objc func setAllowsExternalPlayback(_ node: NSNumber, allows: Bool) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setAllowsExternalPlayback(allows)
}
}
}
@objc func setUsesExternalPlaybackWhileExternalScreenIsActive(_ node: NSNumber, uses: Bool) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
view.setUsesExternalPlaybackWhileExternalScreenIsActive(uses)
}
}
}
@objc func getAirPlayState(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
let airPlayState = view.getAirPlayState()
resolve(airPlayState)
} else {
reject("NO_VIEW", "KSPlayerView not found", nil)
}
}
}
@objc func showAirPlayPicker(_ node: NSNumber) {
print("[KSPlayerViewManager] showAirPlayPicker called for node: \(node)")
DispatchQueue.main.async {
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
print("[KSPlayerViewManager] Found KSPlayerView, calling showAirPlayPicker")
view.showAirPlayPicker()
} else {
print("[KSPlayerViewManager] Could not find KSPlayerView for node: \(node)")
}
}
}
}

View file

@ -11,12 +11,12 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */; };
9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; };
9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; };
9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; };
9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
D66ACCC72CB69F1FF14A2585 /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 436A6FCA2C83F29076E121BA /* libPods-Nuvio.a */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
@ -24,16 +24,16 @@
13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = "<group>"; };
15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
436A6FCA2C83F29076E121BA /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
5346BAA9EF8C9C8182D4485C /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = "<group>"; };
819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSPlayerManager.m; sourceTree = "<group>"; };
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerModule.swift; sourceTree = "<group>"; };
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerView.swift; sourceTree = "<group>"; };
9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerViewManager.swift; sourceTree = "<group>"; };
904B4A0A0308D3727268BA5E /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ../KSPlayer/RNBridge/KSPlayerManager.m; sourceTree = "<group>"; };
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerModule.swift; sourceTree = "<group>"; };
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerView.swift; sourceTree = "<group>"; };
9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerViewManager.swift; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */,
D66ACCC72CB69F1FF14A2585 /* libPods-Nuvio.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -76,7 +76,7 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */,
436A6FCA2C83F29076E121BA /* libPods-Nuvio.a */,
);
name = Frameworks;
sourceTree = "<group>";
@ -131,8 +131,8 @@
D90A3959C97EE9926C513293 /* Pods */ = {
isa = PBXGroup;
children = (
819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */,
15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */,
904B4A0A0308D3727268BA5E /* Pods-Nuvio.debug.xcconfig */,
5346BAA9EF8C9C8182D4485C /* Pods-Nuvio.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -152,15 +152,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */;
buildPhases = (
E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */,
3B2D9C1D63379C2F30AC0F2B /* [CP] Check Pods Manifest.lock */,
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */,
B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */,
AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */,
9F740EE07B5F97C85979C145 /* [CP] Embed Pods Frameworks */,
550CD54859274FE505BA4957 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -234,45 +234,29 @@
shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
};
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = {
3B2D9C1D63379C2F30AC0F2B /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/Nuvio/Nuvio.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh",
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[Expo] Configure project";
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift",
"$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Upload Debug Symbols to Sentry";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
};
AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */ = {
550CD54859274FE505BA4957 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -384,7 +368,45 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n";
showEnvVarsInLog = 0;
};
B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */ = {
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/Nuvio/Nuvio.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh",
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n";
};
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Upload Debug Symbols to Sentry";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
};
9F740EE07B5F97C85979C145 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -406,28 +428,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -449,7 +449,7 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */;
baseConfigurationReference = 904B4A0A0308D3727268BA5E /* Pods-Nuvio.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@ -487,7 +487,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */;
baseConfigurationReference = 5346BAA9EF8C9C8182D4485C /* Pods-Nuvio.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;

View file

@ -50,8 +50,9 @@ target 'Nuvio' do
)
# KSPlayer dependencies
pod 'KSPlayer', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main'
pod 'DisplayCriteria', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main', :modular_headers => true
# Use the local checkout so we can patch subtitle rendering (and other behaviors) without forking.
pod 'KSPlayer', :path => '../KSPlayer'
pod 'DisplayCriteria', :path => '../KSPlayer', :modular_headers => true
pod 'FFmpegKit', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main', :modular_headers => true
pod 'Libass', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main'

View file

@ -2760,7 +2760,7 @@ PODS:
- Yoga (0.0.0)
DEPENDENCIES:
- DisplayCriteria (from `https://github.com/kingslay/KSPlayer.git`, branch `main`)
- DisplayCriteria (from `../KSPlayer`)
- EASClient (from `../node_modules/expo-eas-client/ios`)
- EXApplication (from `../node_modules/expo-application/ios`)
- EXConstants (from `../node_modules/expo-constants/ios`)
@ -2800,7 +2800,7 @@ DEPENDENCIES:
- FFmpegKit (from `https://github.com/kingslay/FFmpegKit.git`, branch `main`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- ImageColors (from `../node_modules/react-native-image-colors/ios`)
- KSPlayer (from `https://github.com/kingslay/KSPlayer.git`, branch `main`)
- KSPlayer (from `../KSPlayer`)
- Libass (from `https://github.com/kingslay/FFmpegKit.git`, branch `main`)
- lottie-react-native (from `../node_modules/lottie-react-native`)
- NitroMmkv (from `../node_modules/react-native-mmkv`)
@ -2910,8 +2910,7 @@ SPEC REPOS:
EXTERNAL SOURCES:
DisplayCriteria:
:branch: main
:git: https://github.com/kingslay/KSPlayer.git
:path: "../KSPlayer"
EASClient:
:path: "../node_modules/expo-eas-client/ios"
EXApplication:
@ -2993,8 +2992,7 @@ EXTERNAL SOURCES:
ImageColors:
:path: "../node_modules/react-native-image-colors/ios"
KSPlayer:
:branch: main
:git: https://github.com/kingslay/KSPlayer.git
:path: "../KSPlayer"
Libass:
:branch: main
:git: https://github.com/kingslay/FFmpegKit.git
@ -3174,15 +3172,9 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
CHECKOUT OPTIONS:
DisplayCriteria:
:commit: a7cddd878f557afa6a1f2faad9d756949406adde
:git: https://github.com/kingslay/KSPlayer.git
FFmpegKit:
:commit: d7048037a2eb94a3b08113fbf43aa92bdcb332d9
:git: https://github.com/kingslay/FFmpegKit.git
KSPlayer:
:commit: a7cddd878f557afa6a1f2faad9d756949406adde
:git: https://github.com/kingslay/KSPlayer.git
Libass:
:commit: d7048037a2eb94a3b08113fbf43aa92bdcb332d9
:git: https://github.com/kingslay/FFmpegKit.git
@ -3332,6 +3324,6 @@ SPEC CHECKSUMS:
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
Yoga: 051f086b5ccf465ff2ed38a2cf5a558ae01aaaa1
PODFILE CHECKSUM: 7c74c9cd2c7f3df7ab68b4284d9f324282e54542
PODFILE CHECKSUM: b884d1ff07ac4a43323bce2e2e1342592513858c
COCOAPODS: 1.16.2

View file

@ -2126,6 +2126,16 @@ public class ReactExoplayerView extends FrameLayout implements
if (textRendererIndex != C.INDEX_UNSET) {
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
boolean trackFound = false;
// NOTE:
// RNVideo emits textTracks as a flattened list (Track.index is assigned using textTracks.size()).
// However, previous logic compared the requested "index" against the *trackIndex within a group*,
// which makes any index > 0 either select the wrong subtitle or keep the first one.
// Here we interpret type="index" as the flattened index, matching the JS list order.
int targetFlatIndex = -1;
if ("index".equals(type)) {
targetFlatIndex = ReactBridgeUtils.safeParseInt(value, -1);
}
int flatIndex = 0;
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
TrackGroup group = groups.get(groupIndex);
@ -2138,8 +2148,7 @@ public class ReactExoplayerView extends FrameLayout implements
} else if ("title".equals(type) && format.label != null && format.label.equals(value)) {
isMatch = true;
} else if ("index".equals(type)) {
int targetIndex = ReactBridgeUtils.safeParseInt(value, -1);
if (targetIndex == trackIndex) {
if (targetFlatIndex != -1 && targetFlatIndex == flatIndex) {
isMatch = true;
}
}
@ -2151,6 +2160,7 @@ public class ReactExoplayerView extends FrameLayout implements
trackFound = true;
break;
}
flatIndex++;
}
if (trackFound) break;
}

File diff suppressed because one or more lines are too long

View file

@ -339,7 +339,8 @@ const AndroidVideoPlayer: React.FC = () => {
if (data.audioTracks) {
const formatted = data.audioTracks.map((t: any, i: number) => ({
id: t.index !== undefined ? t.index : i,
// react-native-video selectedAudioTrack {type:'index'} uses 0-based list index.
id: i,
name: t.title || t.name || `Track ${i + 1}`,
language: t.language
}));
@ -347,7 +348,9 @@ const AndroidVideoPlayer: React.FC = () => {
}
if (data.textTracks) {
const formatted = data.textTracks.map((t: any, i: number) => ({
id: t.index !== undefined ? t.index : i,
// react-native-video selectedTextTrack {type:'index'} uses 0-based list index.
// Using `t.index` can be non-unique/misaligned and breaks selection/rendering.
id: i,
name: t.title || t.name || `Track ${i + 1}`,
language: t.language
}));
@ -360,7 +363,7 @@ const AndroidVideoPlayer: React.FC = () => {
// Auto-select audio track based on preferences
if (data.audioTracks && data.audioTracks.length > 0 && settings?.preferredAudioLanguage) {
const formatted = data.audioTracks.map((t: any, i: number) => ({
id: t.index !== undefined ? t.index : i,
id: i,
name: t.title || t.name || `Track ${i + 1}`,
language: t.language
}));
@ -380,7 +383,7 @@ const AndroidVideoPlayer: React.FC = () => {
// Only pre-select internal if preference is internal or any
if (sourcePreference === 'internal' || sourcePreference === 'any') {
const formatted = data.textTracks.map((t: any, i: number) => ({
id: t.index !== undefined ? t.index : i,
id: i,
name: t.title || t.name || `Track ${i + 1}`,
language: t.language
}));
@ -479,11 +482,11 @@ const AndroidVideoPlayer: React.FC = () => {
useEffect(() => {
if (!useCustomSubtitles || customSubtitles.length === 0) return;
const cueNow = customSubtitles.find(
cue => playerState.currentTime >= cue.start && playerState.currentTime <= cue.end
);
// Apply timing offset for custom/addon subtitles (ExoPlayer internal subtitles do not support offset)
const adjustedTime = playerState.currentTime + (subtitleOffsetSec || 0);
const cueNow = customSubtitles.find(cue => adjustedTime >= cue.start && adjustedTime <= cue.end);
setCurrentSubtitle(cueNow ? cueNow.text : '');
}, [playerState.currentTime, useCustomSubtitles, customSubtitles]);
}, [playerState.currentTime, subtitleOffsetSec, useCustomSubtitles, customSubtitles]);
const toggleControls = useCallback(() => {
playerState.setShowControls(prev => {
@ -678,8 +681,8 @@ const AndroidVideoPlayer: React.FC = () => {
mpvPlayerRef.current.setSubtitleTrack(-1);
}
// Set initial subtitle based on current time
const adjustedTime = playerState.currentTime;
// Set initial subtitle based on current time (+ any timing offset)
const adjustedTime = playerState.currentTime + (subtitleOffsetSec || 0);
const cueNow = parsedCues.find(cue => adjustedTime >= cue.start && adjustedTime <= cue.end);
setCurrentSubtitle(cueNow ? cueNow.text : '');
@ -691,7 +694,7 @@ const AndroidVideoPlayer: React.FC = () => {
} finally {
setIsLoadingSubtitles(false);
}
}, [modals, playerState.currentTime, tracksHook]);
}, [modals, playerState.currentTime, subtitleOffsetSec, tracksHook]);
const disableCustomSubtitles = useCallback(() => {
setUseCustomSubtitles(false);

View file

@ -19,6 +19,7 @@ interface KSPlayerViewProps {
subtitleFontSize?: number;
subtitleTextColor?: string;
subtitleBackgroundColor?: string;
subtitleOutlineEnabled?: boolean;
resizeMode?: 'contain' | 'cover' | 'stretch';
onLoad?: (data: any) => void;
onProgress?: (data: any) => void;
@ -60,6 +61,7 @@ export interface KSPlayerProps {
subtitleFontSize?: number;
subtitleTextColor?: string;
subtitleBackgroundColor?: string;
subtitleOutlineEnabled?: boolean;
resizeMode?: 'contain' | 'cover' | 'stretch';
onLoad?: (data: any) => void;
onProgress?: (data: any) => void;
@ -210,6 +212,7 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
subtitleFontSize={props.subtitleFontSize}
subtitleTextColor={props.subtitleTextColor}
subtitleBackgroundColor={props.subtitleBackgroundColor}
subtitleOutlineEnabled={props.subtitleOutlineEnabled}
resizeMode={props.resizeMode}
onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)}
onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)}

View file

@ -616,6 +616,10 @@ const KSPlayerCore: React.FC = () => {
/>
{/* Video Surface & Pinch Zoom */}
{/*
For KSPlayer built-in subtitles (internal text tracks), we intentionally force background OFF.
Background styling is only supported/used for custom (external/addon) subtitles overlay.
*/}
<KSPlayerSurface
ksPlayerRef={ksPlayerRef}
uri={uri}
@ -656,7 +660,20 @@ const KSPlayerCore: React.FC = () => {
screenHeight={screenDimensions.height}
customVideoStyles={{ width: '100%', height: '100%' }}
subtitleTextColor={customSubs.subtitleTextColor}
subtitleBackgroundColor={customSubs.subtitleBackground ? `rgba(0,0,0,${customSubs.subtitleBgOpacity})` : 'transparent'}
subtitleBackgroundColor={
tracks.selectedTextTrack !== null &&
tracks.selectedTextTrack >= 0 &&
!customSubs.useCustomSubtitles
? 'rgba(0,0,0,0)'
: (customSubs.subtitleBackground ? `rgba(0,0,0,${customSubs.subtitleBgOpacity})` : 'transparent')
}
subtitleOutlineEnabled={
tracks.selectedTextTrack !== null &&
tracks.selectedTextTrack >= 0 &&
!customSubs.useCustomSubtitles
? customSubs.subtitleOutline
: false
}
subtitleFontSize={customSubs.subtitleSize}
subtitleBottomOffset={customSubs.subtitleBottomOffset}
/>

View file

@ -173,15 +173,19 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
console.log('[VideoSurface] ExoPlayer textTracks raw:', JSON.stringify(data.textTracks, null, 2));
// Extract track information
// IMPORTANT:
// react-native-video expects selected*Track with { type: 'index', value: <0-based array index> }.
// Some RNVideo/Exo track objects expose `index`, but it is not guaranteed to be unique or
// aligned with the list index. Using it can cause only the first item to render/select.
const audioTracks = data.audioTracks?.map((t: any, i: number) => ({
id: t.index ?? i,
id: i,
name: t.title || t.language || `Track ${i + 1}`,
language: t.language,
})) ?? [];
const subtitleTracks = data.textTracks?.map((t: any, i: number) => {
const track = {
id: t.index ?? i,
id: i,
name: t.title || t.language || `Track ${i + 1}`,
language: t.language,
};
@ -281,6 +285,11 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
}
};
const alphaHex = (opacity01: number) => {
const a = Math.max(0, Math.min(1, opacity01));
return Math.round(a * 255).toString(16).padStart(2, '0').toUpperCase();
};
return (
<View style={[styles.videoContainer, {
width: screenDimensions.width,
@ -314,7 +323,9 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
automaticallyWaitsToMinimizeStalling={true}
useTextureView={true}
// Subtitle Styling for ExoPlayer
// ExoPlayer supports: fontSize, paddingTop/Bottom/Left/Right, opacity, subtitlesFollowVideo
// ExoPlayer (via our patched react-native-video) supports:
// - fontSize, paddingTop/Bottom/Left/Right, opacity, subtitlesFollowVideo
// - PLUS: textColor, backgroundColor, edgeType, edgeColor (outline/shadow)
subtitleStyle={{
// Convert MPV-scaled size back to ExoPlayer scale (~1.5x conversion was applied)
fontSize: subtitleSize ? Math.round(subtitleSize / 1.5) : 18,
@ -328,7 +339,22 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
// Always keep text visible (opacity 1), background control is limited in ExoPlayer
opacity: 1,
subtitlesFollowVideo: false,
}}
// Extended styling (requires our patched RNVideo on Android)
textColor: subtitleColor || '#FFFFFFFF',
// Android Color.parseColor doesn't accept rgba(...). Use #AARRGGBB.
backgroundColor:
subtitleBackgroundOpacity && subtitleBackgroundOpacity > 0
? `#${alphaHex(subtitleBackgroundOpacity)}000000`
: '#00000000',
edgeType:
subtitleBorderSize && subtitleBorderSize > 0
? 'outline'
: (subtitleShadowEnabled ? 'shadow' : 'none'),
edgeColor:
(subtitleBorderSize && subtitleBorderSize > 0 && subtitleBorderColor)
? subtitleBorderColor
: (subtitleShadowEnabled ? '#FF000000' : 'transparent'),
} as any}
/>
) : (
/* MPV Player fallback */

View file

@ -42,6 +42,7 @@ interface KSPlayerSurfaceProps {
subtitleBackgroundColor?: string;
subtitleFontSize?: number;
subtitleBottomOffset?: number;
subtitleOutlineEnabled?: boolean;
}
export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
@ -74,7 +75,8 @@ export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
subtitleTextColor,
subtitleBackgroundColor,
subtitleFontSize,
subtitleBottomOffset
subtitleBottomOffset,
subtitleOutlineEnabled
}) => {
const pinchRef = useRef<PinchGestureHandler>(null);
@ -146,6 +148,7 @@ export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
subtitleBackgroundColor={subtitleBackgroundColor}
subtitleFontSize={subtitleFontSize}
subtitleBottomOffset={subtitleBottomOffset}
subtitleOutlineEnabled={subtitleOutlineEnabled}
onLoad={handleLoad}
onProgress={onProgress}
onBuffering={handleBuffering}

View file

@ -110,7 +110,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
const isCompact = width < 360 || height < 640;
// Internal subtitle is active when a built-in track is selected AND not using custom/addon subtitles
const isUsingInternalSubtitle = selectedTextTrack >= 0 && !useCustomSubtitles;
// ExoPlayer has limited styling support - hide unsupported options when using ExoPlayer with internal subs
// ExoPlayer internal subtitles have limited styling support
const isExoPlayerInternal = useExoPlayer && isUsingInternalSubtitle;
const sectionPad = isCompact ? 12 : 16;
const chipPadH = isCompact ? 8 : 12;
@ -122,7 +122,9 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
const menuMaxHeight = height * 0.95;
React.useEffect(() => {
if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) fetchAvailableSubtitles();
if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) {
fetchAvailableSubtitles();
}
}, [showSubtitleModal]);
const handleClose = () => setShowSubtitleModal(false);
@ -237,7 +239,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<View style={{ height: previewHeight, justifyContent: 'flex-end' }}>
<View style={{ alignItems: subtitleAlign === 'center' ? 'center' : subtitleAlign === 'left' ? 'flex-start' : 'flex-end', marginBottom: Math.min(80, subtitleBottomOffset) }}>
<View style={{
backgroundColor: subtitleBackground ? `rgba(0,0,0,${subtitleBgOpacity})` : 'transparent',
// Built-in (KSPlayer internal) subtitles: force background off in UI preview.
backgroundColor: isUsingInternalSubtitle ? 'transparent' : (subtitleBackground ? `rgba(0,0,0,${subtitleBgOpacity})` : 'transparent'),
borderRadius: 8,
paddingHorizontal: isCompact ? 10 : 12,
paddingVertical: isCompact ? 6 : 8,
@ -259,8 +262,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View>
</View>
{/* Quick Presets - Hidden for ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
{/* Quick Presets - only for CustomSubtitles overlay */}
{!isUsingInternalSubtitle && (
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
<MaterialIcons name="star" size={16} color="rgba(255,255,255,0.7)" />
@ -329,8 +332,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</TouchableOpacity>
</View>
</View>
{/* Show Background - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
{/* Background is only for CustomSubtitles (external/addon). Built-in subtitles force background OFF. */}
{!isUsingInternalSubtitle && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
@ -346,28 +349,27 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
)}
</View>
{/* Advanced controls - Limited for ExoPlayer */}
{/* Advanced controls */}
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad, gap: isCompact ? 10 : 14 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="build" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>{isExoPlayerInternal ? t('player_ui.position') : t('player_ui.advanced')}</Text>
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>{isUsingInternalSubtitle ? t('player_ui.position') : t('player_ui.advanced')}</Text>
</View>
{/* Text Color - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>{t('player_ui.text_color')}</Text>
</View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
))}
</View>
{/* Text Color - supported for MPV built-in, and for CustomSubtitles */}
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>{t('player_ui.text_color')}</Text>
</View>
)}
{/* Align - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
))}
</View>
</View>
{/* Align - only supported for CustomSubtitles overlay */}
{!isUsingInternalSubtitle && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>{t('player_ui.align')}</Text>
<View style={{ flexDirection: 'row', gap: 8 }}>
@ -393,8 +395,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</TouchableOpacity>
</View>
</View>
{/* Background Opacity - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
{/* Background Opacity (CustomSubtitles only) */}
{!isUsingInternalSubtitle && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>{t('player_ui.background_opacity')}</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
@ -418,7 +420,19 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</TouchableOpacity>
</View>
)}
{!isUsingInternalSubtitle && (
{/* Outline controls (now supported for ExoPlayer internal via native patch) */}
{isUsingInternalSubtitle ? (
// KSPlayer built-in subtitles: only expose an Outline on/off toggle (no width control).
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>{t('player_ui.outline')}</Text>
<TouchableOpacity
onPress={() => setSubtitleOutline(!subtitleOutline)}
style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleOutline ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}
>
<Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleOutline ? t('player_ui.on') : t('player_ui.off')}</Text>
</TouchableOpacity>
</View>
) : (
<>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white' }}>{t('player_ui.outline_color')}</Text>

View file

@ -410,6 +410,7 @@
"on": "تشغيل",
"off": "إيقاف",
"outline_color": "لون الإطار",
"outline": "الإطار",
"outline_width": "عرض الإطار",
"letter_spacing": "تباعد الأحرف",
"line_height": "ارتفاع السطر",

View file

@ -410,6 +410,7 @@
"on": "Ein",
"off": "Aus",
"outline_color": "Umrandungsfarbe",
"outline": "Umrandung",
"outline_width": "Umrandungsbreite",
"letter_spacing": "Zeichenabstand",
"line_height": "Zeilenhöhe",

View file

@ -410,6 +410,7 @@
"on": "On",
"off": "Off",
"outline_color": "Outline Color",
"outline": "Outline",
"outline_width": "Outline Width",
"letter_spacing": "Letter Spacing",
"line_height": "Line Height",

View file

@ -410,6 +410,7 @@
"on": "Sí",
"off": "No",
"outline_color": "Color de contorno",
"outline": "Contorno",
"outline_width": "Ancho de contorno",
"letter_spacing": "Espaciado de letras",
"line_height": "Altura de línea",

View file

@ -410,6 +410,7 @@
"on": "Activé",
"off": "Désactivé",
"outline_color": "Couleur du contour",
"outline": "Contour",
"outline_width": "Largeur du contour",
"letter_spacing": "Espacement des lettres",
"line_height": "Hauteur de ligne",

View file

@ -410,6 +410,7 @@
"on": "Attivo",
"off": "Disattivo",
"outline_color": "Colore contorno",
"outline": "Contorno",
"outline_width": "Larghezza contorno",
"letter_spacing": "Spaziatura lettere",
"line_height": "Altezza riga",

View file

@ -410,6 +410,7 @@
"on": "Ligado",
"off": "Desligado",
"outline_color": "Cor do Contorno",
"outline": "Contorno",
"outline_width": "Largura do Contorno",
"letter_spacing": "Espaçamento de Letras",
"line_height": "Altura da Linha",

View file

@ -410,6 +410,7 @@
"on": "Ligado",
"off": "Desligado",
"outline_color": "Cor do Contorno",
"outline": "Contorno",
"outline_width": "Largura do Contorno",
"letter_spacing": "Espaçamento de Letras",
"line_height": "Altura da Linha",