added back ksplayer files
This commit is contained in:
parent
88a1399c3b
commit
838f74caa2
5 changed files with 738 additions and 17 deletions
42
ios/KSPlayerManager.m
Normal file
42
ios/KSPlayerManager.m
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// KSPlayerManager.m
|
||||
// Nuvio
|
||||
//
|
||||
// Created by KSPlayer integration
|
||||
//
|
||||
|
||||
#import <React/RCTViewManager.h>
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.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(audioTrack, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
|
||||
|
||||
// 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(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)
|
||||
|
||||
@end
|
||||
|
||||
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
||||
|
||||
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
37
ios/KSPlayerModule.swift
Normal file
37
ios/KSPlayerModule.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
519
ios/KSPlayerView.swift
Normal file
519
ios/KSPlayerView.swift
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
//
|
||||
// KSPlayerView.swift
|
||||
// Nuvio
|
||||
//
|
||||
// Created by KSPlayer integration
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KSPlayer
|
||||
import React
|
||||
|
||||
@objc(KSPlayerView)
|
||||
class KSPlayerView: UIView {
|
||||
private var playerView: IOSVideoPlayerView!
|
||||
private var currentSource: NSDictionary?
|
||||
private var isPaused = false
|
||||
private var currentVolume: Float = 1.0
|
||||
weak var viewManager: KSPlayerViewManager?
|
||||
|
||||
// Event blocks for Fabric
|
||||
@objc var onLoad: RCTDirectEventBlock?
|
||||
@objc var onProgress: RCTDirectEventBlock?
|
||||
@objc var onBuffering: RCTDirectEventBlock?
|
||||
@objc var onEnd: RCTDirectEventBlock?
|
||||
@objc var onError: RCTDirectEventBlock?
|
||||
@objc var onBufferingProgress: RCTDirectEventBlock?
|
||||
|
||||
// Property setters that React Native will call
|
||||
@objc var source: NSDictionary? {
|
||||
didSet {
|
||||
if let source = source {
|
||||
setSource(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc var paused: Bool = false {
|
||||
didSet {
|
||||
setPaused(paused)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var volume: NSNumber = 1.0 {
|
||||
didSet {
|
||||
setVolume(volume.floatValue)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var audioTrack: NSNumber = -1 {
|
||||
didSet {
|
||||
setAudioTrack(audioTrack.intValue)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var textTrack: NSNumber = -1 {
|
||||
didSet {
|
||||
setTextTrack(textTrack.intValue)
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupPlayerView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupPlayerView()
|
||||
}
|
||||
|
||||
private func setupPlayerView() {
|
||||
playerView = IOSVideoPlayerView()
|
||||
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// Hide native controls - we use custom React Native controls
|
||||
playerView.isUserInteractionEnabled = false
|
||||
// Hide KSPlayer's built-in overlay/controls
|
||||
playerView.controllerView.isHidden = true
|
||||
playerView.contentOverlayView.isHidden = true
|
||||
playerView.controllerView.alpha = 0
|
||||
playerView.contentOverlayView.alpha = 0
|
||||
playerView.controllerView.gestureRecognizers?.forEach { $0.isEnabled = false }
|
||||
addSubview(playerView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
playerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
playerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
playerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
playerView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
// Set up player delegates and callbacks
|
||||
setupPlayerCallbacks()
|
||||
}
|
||||
|
||||
private func setupPlayerCallbacks() {
|
||||
// Configure KSOptions (use static defaults where required)
|
||||
KSOptions.isAutoPlay = false
|
||||
#if targetEnvironment(simulator)
|
||||
// Simulator: disable hardware decode and MEPlayer to avoid VT/Vulkan issues
|
||||
KSOptions.hardwareDecode = false
|
||||
KSOptions.asynchronousDecompression = false
|
||||
KSOptions.secondPlayerType = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
func setSource(_ source: NSDictionary) {
|
||||
currentSource = source
|
||||
|
||||
guard let uri = source["uri"] as? String else {
|
||||
print("KSPlayerView: No URI provided")
|
||||
sendEvent("onError", ["error": "No URI provided in source"])
|
||||
return
|
||||
}
|
||||
|
||||
// Validate URL before proceeding
|
||||
guard let url = URL(string: uri), url.scheme != nil else {
|
||||
print("KSPlayerView: Invalid URL format: \(uri)")
|
||||
sendEvent("onError", ["error": "Invalid URL format: \(uri)"])
|
||||
return
|
||||
}
|
||||
|
||||
var headers: [String: String] = [:]
|
||||
if let headersDict = source["headers"] as? [String: String] {
|
||||
headers = headersDict
|
||||
}
|
||||
|
||||
// Choose player pipeline based on format
|
||||
let isMKV = uri.lowercased().contains(".mkv")
|
||||
#if targetEnvironment(simulator)
|
||||
if isMKV {
|
||||
// MKV not supported on AVPlayer in Simulator and MEPlayer is disabled
|
||||
sendEvent("onError", ["error": "MKV playback is not supported in the iOS Simulator. Test on a real device."])
|
||||
}
|
||||
#else
|
||||
if isMKV {
|
||||
// Prefer MEPlayer (FFmpeg) for MKV on device
|
||||
KSOptions.firstPlayerType = KSMEPlayer.self
|
||||
KSOptions.secondPlayerType = nil
|
||||
} else {
|
||||
KSOptions.firstPlayerType = KSAVPlayer.self
|
||||
KSOptions.secondPlayerType = KSMEPlayer.self
|
||||
}
|
||||
#endif
|
||||
|
||||
// Create KSPlayerResource with validated URL
|
||||
let resource = KSPlayerResource(url: url, options: createOptions(with: headers), name: "Video")
|
||||
|
||||
print("KSPlayerView: Setting source: \(uri)")
|
||||
print("KSPlayerView: URL scheme: \(url.scheme ?? "unknown"), host: \(url.host ?? "unknown")")
|
||||
|
||||
playerView.set(resource: resource)
|
||||
|
||||
// Set up delegate after setting the resource
|
||||
playerView.playerLayer?.delegate = self
|
||||
|
||||
// Apply current state
|
||||
if isPaused {
|
||||
playerView.pause()
|
||||
} else {
|
||||
playerView.play()
|
||||
}
|
||||
|
||||
setVolume(currentVolume)
|
||||
}
|
||||
|
||||
private func createOptions(with headers: [String: String]) -> KSOptions {
|
||||
let options = KSOptions()
|
||||
// Disable native player remote control center integration; use RN controls
|
||||
options.registerRemoteControll = false
|
||||
|
||||
// Configure audio for proper dialogue mixing using FFmpeg's pan filter
|
||||
// This approach uses standard audio engineering practices for multi-channel downmixing
|
||||
|
||||
// Use conservative center channel mixing that preserves spatial audio
|
||||
// c0 (Left) = 70% original left + 30% center (dialogue) + 20% rear left
|
||||
// c1 (Right) = 70% original right + 30% center (dialogue) + 20% rear right
|
||||
// This creates natural dialogue presence without the "playing on both ears" effect
|
||||
options.audioFilters.append("pan=stereo|c0=0.7*c0+0.3*c2+0.2*c4|c1=0.7*c1+0.3*c2+0.2*c5")
|
||||
|
||||
// Alternative: Use FFmpeg's surround filter for more sophisticated downmixing
|
||||
// This provides better spatial audio processing and natural dialogue mixing
|
||||
// options.audioFilters.append("surround=ang=45")
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
options.hardwareDecode = false
|
||||
options.asynchronousDecompression = false
|
||||
#else
|
||||
options.hardwareDecode = KSOptions.hardwareDecode
|
||||
#endif
|
||||
if !headers.isEmpty {
|
||||
// Clean and validate headers before adding
|
||||
var cleanHeaders: [String: String] = [:]
|
||||
for (key, value) in headers {
|
||||
// Remove any null or empty values
|
||||
if !value.isEmpty && value != "null" {
|
||||
cleanHeaders[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if !cleanHeaders.isEmpty {
|
||||
options.appendHeader(cleanHeaders)
|
||||
print("KSPlayerView: Added headers: \(cleanHeaders.keys.joined(separator: ", "))")
|
||||
|
||||
if let referer = cleanHeaders["Referer"] ?? cleanHeaders["referer"] {
|
||||
options.referer = referer
|
||||
print("KSPlayerView: Set referer: \(referer)")
|
||||
}
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func setPaused(_ paused: Bool) {
|
||||
isPaused = paused
|
||||
if paused {
|
||||
playerView.pause()
|
||||
} else {
|
||||
playerView.play()
|
||||
}
|
||||
}
|
||||
|
||||
func setVolume(_ volume: Float) {
|
||||
currentVolume = volume
|
||||
playerView.playerLayer?.player.playbackVolume = volume
|
||||
}
|
||||
|
||||
func seek(to time: TimeInterval) {
|
||||
guard let playerLayer = playerView.playerLayer,
|
||||
playerLayer.player.isReadyToPlay,
|
||||
playerLayer.player.seekable else {
|
||||
print("KSPlayerView: Cannot seek - player not ready or not seekable")
|
||||
return
|
||||
}
|
||||
|
||||
playerView.seek(time: time) { success in
|
||||
if success {
|
||||
print("KSPlayerView: Seek successful to \(time)")
|
||||
} else {
|
||||
print("KSPlayerView: Seek failed to \(time)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setAudioTrack(_ trackId: Int) {
|
||||
if let player = playerView.playerLayer?.player {
|
||||
let audioTracks = player.tracks(mediaType: .audio)
|
||||
print("KSPlayerView: Available audio tracks count: \(audioTracks.count)")
|
||||
print("KSPlayerView: Requested track ID: \(trackId)")
|
||||
|
||||
// Debug: Print all track information
|
||||
for (index, track) in audioTracks.enumerated() {
|
||||
print("KSPlayerView: Track \(index) - ID: \(track.trackID), Name: '\(track.name)', Language: '\(track.language ?? "nil")', isEnabled: \(track.isEnabled)")
|
||||
}
|
||||
|
||||
// First try to find track by trackID (proper way)
|
||||
var selectedTrack: MediaPlayerTrack? = nil
|
||||
var trackIndex: Int = -1
|
||||
|
||||
// Try to find by exact trackID match
|
||||
if let track = audioTracks.first(where: { Int($0.trackID) == trackId }) {
|
||||
selectedTrack = track
|
||||
trackIndex = audioTracks.firstIndex(where: { $0.trackID == track.trackID }) ?? -1
|
||||
print("KSPlayerView: Found track by trackID \(trackId) at index \(trackIndex)")
|
||||
}
|
||||
// Fallback: treat trackId as array index
|
||||
else if trackId >= 0 && trackId < audioTracks.count {
|
||||
selectedTrack = audioTracks[trackId]
|
||||
trackIndex = trackId
|
||||
print("KSPlayerView: Found track by array index \(trackId) (fallback)")
|
||||
}
|
||||
|
||||
if let track = selectedTrack {
|
||||
print("KSPlayerView: Selecting track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))")
|
||||
|
||||
// Use KSPlayer's select method which properly handles track selection
|
||||
player.select(track: track)
|
||||
|
||||
print("KSPlayerView: Successfully selected audio track \(trackId)")
|
||||
|
||||
// Verify the selection worked
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
let tracksAfter = player.tracks(mediaType: .audio)
|
||||
for (index, track) in tracksAfter.enumerated() {
|
||||
print("KSPlayerView: After selection - Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure audio downmixing for multi-channel tracks
|
||||
configureAudioDownmixing(for: track)
|
||||
} else if trackId == -1 {
|
||||
// Disable all audio tracks (mute)
|
||||
for track in audioTracks { track.isEnabled = false }
|
||||
print("KSPlayerView: Disabled all audio tracks")
|
||||
} else {
|
||||
print("KSPlayerView: Track \(trackId) not found. Available track IDs: \(audioTracks.map { Int($0.trackID) }), array indices: 0..\(audioTracks.count - 1)")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: No player available for audio track selection")
|
||||
}
|
||||
}
|
||||
|
||||
private func configureAudioDownmixing(for track: MediaPlayerTrack) {
|
||||
// Check if this is a multi-channel audio track that needs downmixing
|
||||
// This is a simplified check - in practice, you might want to check the actual channel layout
|
||||
let trackName = track.name.lowercased()
|
||||
let isMultiChannel = trackName.contains("5.1") || trackName.contains("7.1") ||
|
||||
trackName.contains("truehd") || trackName.contains("dts") ||
|
||||
trackName.contains("dolby") || trackName.contains("atmos")
|
||||
|
||||
if isMultiChannel {
|
||||
print("KSPlayerView: Detected multi-channel audio track '\(track.name)', ensuring proper dialogue mixing")
|
||||
print("KSPlayerView: Using FFmpeg pan filter for natural stereo downmixing")
|
||||
} else {
|
||||
print("KSPlayerView: Stereo or mono audio track '\(track.name)', no additional downmixing needed")
|
||||
}
|
||||
}
|
||||
|
||||
func setTextTrack(_ trackId: Int) {
|
||||
if let player = playerView.playerLayer?.player {
|
||||
let textTracks = player.tracks(mediaType: .subtitle)
|
||||
print("KSPlayerView: Available text tracks count: \(textTracks.count)")
|
||||
print("KSPlayerView: Requested text track ID: \(trackId)")
|
||||
|
||||
// First try to find track by trackID (proper way)
|
||||
var selectedTrack: MediaPlayerTrack? = nil
|
||||
var trackIndex: Int = -1
|
||||
|
||||
// Try to find by exact trackID match
|
||||
if let track = textTracks.first(where: { Int($0.trackID) == trackId }) {
|
||||
selectedTrack = track
|
||||
trackIndex = textTracks.firstIndex(where: { $0.trackID == track.trackID }) ?? -1
|
||||
print("KSPlayerView: Found text track by trackID \(trackId) at index \(trackIndex)")
|
||||
}
|
||||
// Fallback: treat trackId as array index
|
||||
else if trackId >= 0 && trackId < textTracks.count {
|
||||
selectedTrack = textTracks[trackId]
|
||||
trackIndex = trackId
|
||||
print("KSPlayerView: Found text track by array index \(trackId) (fallback)")
|
||||
}
|
||||
|
||||
if let track = selectedTrack {
|
||||
print("KSPlayerView: Selecting text track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))")
|
||||
|
||||
// Use KSPlayer's select method which properly handles track selection
|
||||
player.select(track: track)
|
||||
|
||||
print("KSPlayerView: Successfully selected text track \(trackId)")
|
||||
} else if trackId == -1 {
|
||||
// Disable all subtitles
|
||||
for track in textTracks { track.isEnabled = false }
|
||||
print("KSPlayerView: Disabled all text tracks")
|
||||
} else {
|
||||
print("KSPlayerView: Text track \(trackId) not found. Available track IDs: \(textTracks.map { Int($0.trackID) }), array indices: 0..\(textTracks.count - 1)")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: No player available for text track selection")
|
||||
}
|
||||
}
|
||||
|
||||
// Get available tracks for React Native
|
||||
func getAvailableTracks() -> [String: Any] {
|
||||
guard let player = playerView.playerLayer?.player else {
|
||||
return ["audioTracks": [], "textTracks": []]
|
||||
}
|
||||
|
||||
let audioTracks = player.tracks(mediaType: .audio).enumerated().map { index, track in
|
||||
return [
|
||||
"id": Int(track.trackID), // Use actual track ID, not array index
|
||||
"index": index, // Keep index for backward compatibility
|
||||
"name": track.name,
|
||||
"language": track.language ?? "Unknown",
|
||||
"languageCode": track.languageCode ?? "",
|
||||
"isEnabled": track.isEnabled,
|
||||
"bitRate": track.bitRate,
|
||||
"bitDepth": track.bitDepth
|
||||
]
|
||||
}
|
||||
|
||||
let textTracks = player.tracks(mediaType: .subtitle).enumerated().map { index, track in
|
||||
return [
|
||||
"id": Int(track.trackID), // Use actual track ID, not array index
|
||||
"index": index, // Keep index for backward compatibility
|
||||
"name": track.name,
|
||||
"language": track.language ?? "Unknown",
|
||||
"languageCode": track.languageCode ?? "",
|
||||
"isEnabled": track.isEnabled,
|
||||
"isImageSubtitle": track.isImageSubtitle
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
"audioTracks": audioTracks,
|
||||
"textTracks": textTracks
|
||||
]
|
||||
}
|
||||
|
||||
// Get current player state for React Native
|
||||
func getCurrentState() -> [String: Any] {
|
||||
guard let player = playerView.playerLayer?.player else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
return [
|
||||
"currentTime": player.currentPlaybackTime,
|
||||
"duration": player.duration,
|
||||
"buffered": player.playableTime,
|
||||
"isPlaying": !isPaused,
|
||||
"volume": currentVolume
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension KSPlayerView: KSPlayerLayerDelegate {
|
||||
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||
switch state {
|
||||
case .readyToPlay:
|
||||
// Send onLoad event to React Native with track information
|
||||
let p = layer.player
|
||||
let tracks = getAvailableTracks()
|
||||
sendEvent("onLoad", [
|
||||
"duration": p.duration,
|
||||
"currentTime": p.currentPlaybackTime,
|
||||
"naturalSize": [
|
||||
"width": p.naturalSize.width,
|
||||
"height": p.naturalSize.height
|
||||
],
|
||||
"audioTracks": tracks["audioTracks"] ?? [],
|
||||
"textTracks": tracks["textTracks"] ?? []
|
||||
])
|
||||
case .buffering:
|
||||
sendEvent("onBuffering", ["isBuffering": true])
|
||||
case .bufferFinished:
|
||||
sendEvent("onBuffering", ["isBuffering": false])
|
||||
case .playedToTheEnd:
|
||||
sendEvent("onEnd", [:])
|
||||
case .error:
|
||||
// Error will be handled by the finish delegate method
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
||||
let p = layer.player
|
||||
// Ensure we have valid duration before sending progress updates
|
||||
if totalTime > 0 {
|
||||
sendEvent("onProgress", [
|
||||
"currentTime": currentTime,
|
||||
"duration": totalTime,
|
||||
"bufferTime": p.playableTime
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func player(layer: KSPlayerLayer, finish error: Error?) {
|
||||
if let error = error {
|
||||
let errorMessage = error.localizedDescription
|
||||
print("KSPlayerView: Player finished with error: \(errorMessage)")
|
||||
|
||||
// Provide more specific error messages for common issues
|
||||
var detailedError = errorMessage
|
||||
if errorMessage.contains("avformat can't open input") {
|
||||
detailedError = "Unable to open video stream. This could be due to:\n• Invalid or malformed URL\n• Network connectivity issues\n• Server blocking the request\n• Unsupported video format\n• Missing required headers"
|
||||
} else if errorMessage.contains("timeout") {
|
||||
detailedError = "Stream connection timed out. The server may be slow or unreachable."
|
||||
} else if errorMessage.contains("404") || errorMessage.contains("Not Found") {
|
||||
detailedError = "Video stream not found. The URL may be expired or incorrect."
|
||||
} else if errorMessage.contains("403") || errorMessage.contains("Forbidden") {
|
||||
detailedError = "Access denied. The server may be blocking requests or require authentication."
|
||||
}
|
||||
|
||||
sendEvent("onError", ["error": detailedError])
|
||||
}
|
||||
}
|
||||
|
||||
func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval) {
|
||||
// Handle buffering progress if needed
|
||||
sendEvent("onBufferingProgress", [
|
||||
"bufferedCount": bufferedCount,
|
||||
"consumeTime": consumeTime
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension KSPlayerView {
|
||||
private func sendEvent(_ eventName: String, _ body: [String: Any]) {
|
||||
DispatchQueue.main.async {
|
||||
switch eventName {
|
||||
case "onLoad":
|
||||
self.onLoad?(body)
|
||||
case "onProgress":
|
||||
self.onProgress?(body)
|
||||
case "onBuffering":
|
||||
self.onBuffering?(body)
|
||||
case "onEnd":
|
||||
self.onEnd?([:])
|
||||
case "onError":
|
||||
self.onError?(body)
|
||||
case "onBufferingProgress":
|
||||
self.onBufferingProgress?(body)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Renamed to avoid clashing with React's UIView category method
|
||||
private func findHostViewController() -> UIViewController? {
|
||||
var responder: UIResponder? = self
|
||||
while let nextResponder = responder?.next {
|
||||
if let viewController = nextResponder as? UIViewController {
|
||||
return viewController
|
||||
}
|
||||
responder = nextResponder
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
99
ios/KSPlayerViewManager.swift
Normal file
99
ios/KSPlayerViewManager.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// 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 RCT_EXPORT_VIEW_PROPERTY
|
||||
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 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) {
|
||||
DispatchQueue.main.async {
|
||||
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||
view.setTextTrack(Int(truncating: trackId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
|
@ -13,6 +13,10 @@
|
|||
2F6C4443E26F4184A8EA68F1 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4B16751559433B951A993C /* noop-file.swift */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
96905EF65AED1B983A6B3ABC /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* 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 */; };
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
E660DA8F5C7B39AACB568229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8F20890D58E6A611113A359A /* PrivacyInfo.xcprivacy */; };
|
||||
|
|
@ -30,7 +34,12 @@
|
|||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6C2E3173556A471DD304B334 /* 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>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* 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>"; };
|
||||
8F20890D58E6A611113A359A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
8F20890D58E6A611113A359A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
9F9D45D12E85C42200C88FAD /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; 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>"; };
|
||||
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; };
|
||||
|
|
@ -52,6 +61,7 @@
|
|||
13B07FAE1A68108700A75B9A /* Nuvio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F9D45D12E85C42200C88FAD /* NuvioRelease.entitlements */,
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
||||
|
|
@ -85,6 +95,10 @@
|
|||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */,
|
||||
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */,
|
||||
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */,
|
||||
9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */,
|
||||
13B07FAE1A68108700A75B9A /* Nuvio */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
|
|
@ -373,6 +387,10 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */,
|
||||
9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */,
|
||||
9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */,
|
||||
9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||
2F6C4443E26F4184A8EA68F1 /* noop-file.swift in Sources */,
|
||||
|
|
@ -400,7 +418,10 @@
|
|||
);
|
||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -409,7 +430,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
PRODUCT_NAME = Nuvio;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -424,14 +445,17 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
|
||||
CODE_SIGN_ENTITLEMENTS = Nuvio/NuvioRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = NLXTHANK2N;
|
||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -440,7 +464,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
PRODUCT_NAME = Nuvio;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -496,14 +520,14 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
|
|
@ -552,13 +576,13 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue