Files
streamyfin/modules/sf-player/ios/SfPlayerView.swift

318 lines
8.9 KiB
Swift

import AVFoundation
import ExpoModulesCore
import UIKit
class SfPlayerView: ExpoView {
private var player: SfPlayerWrapper?
private var videoContainer: UIView!
let onLoad = EventDispatcher()
let onPlaybackStateChange = EventDispatcher()
let onProgress = EventDispatcher()
let onError = EventDispatcher()
let onTracksReady = EventDispatcher()
let onPictureInPictureChange = EventDispatcher()
private var currentURL: URL?
private var cachedPosition: Double = 0
private var cachedDuration: Double = 0
private var intendedPlayState: Bool = false
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
setupView()
}
private func setupView() {
clipsToBounds = true
backgroundColor = .black
videoContainer = UIView()
videoContainer.translatesAutoresizingMaskIntoConstraints = false
videoContainer.backgroundColor = .black
videoContainer.clipsToBounds = true
addSubview(videoContainer)
NSLayoutConstraint.activate([
videoContainer.topAnchor.constraint(equalTo: topAnchor),
videoContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
videoContainer.trailingAnchor.constraint(equalTo: trailingAnchor),
videoContainer.bottomAnchor.constraint(equalTo: bottomAnchor)
])
// Initialize player
player = SfPlayerWrapper()
player?.delegate = self
// Configure Audio Session for PiP and background playback
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
try? AVAudioSession.sharedInstance().setActive(true)
// Add player view to container
if let playerView = player?.view {
playerView.translatesAutoresizingMaskIntoConstraints = false
videoContainer.addSubview(playerView)
NSLayoutConstraint.activate([
playerView.topAnchor.constraint(equalTo: videoContainer.topAnchor),
playerView.leadingAnchor.constraint(equalTo: videoContainer.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: videoContainer.trailingAnchor),
playerView.bottomAnchor.constraint(equalTo: videoContainer.bottomAnchor)
])
}
}
override func layoutSubviews() {
super.layoutSubviews()
player?.updateLayout(bounds: videoContainer.bounds)
}
// MARK: - Video Loading
func loadVideo(config: VideoLoadConfig) {
// Skip reload if same URL is already playing
if currentURL == config.url {
return
}
currentURL = config.url
player?.load(config: config)
if config.autoplay {
play()
}
onLoad(["url": config.url.absoluteString])
}
func loadVideo(url: URL, headers: [String: String]? = nil) {
loadVideo(config: VideoLoadConfig(url: url, headers: headers))
}
// MARK: - Playback Controls
func play() {
intendedPlayState = true
player?.play()
}
func pause() {
intendedPlayState = false
player?.pause()
}
func seekTo(position: Double) {
player?.seek(to: position)
}
func seekBy(offset: Double) {
player?.seek(by: offset)
}
func setSpeed(speed: Double) {
player?.setSpeed(speed)
}
func getSpeed() -> Double {
return player?.getSpeed() ?? 1.0
}
func isPaused() -> Bool {
return player?.getIsPaused() ?? true
}
func getCurrentPosition() -> Double {
return cachedPosition
}
func getDuration() -> Double {
return cachedDuration
}
// MARK: - Picture in Picture
func startPictureInPicture() {
player?.startPictureInPicture()
}
func stopPictureInPicture() {
player?.stopPictureInPicture()
}
func isPictureInPictureSupported() -> Bool {
return player?.isPictureInPictureSupported() ?? false
}
func isPictureInPictureActive() -> Bool {
return player?.isPictureInPictureActive() ?? false
}
func setAutoPipEnabled(_ enabled: Bool) {
player?.setAutoPipEnabled(enabled)
}
// MARK: - Subtitle Controls
func getSubtitleTracks() -> [[String: Any]] {
return player?.getSubtitleTracks() ?? []
}
func setSubtitleTrack(_ trackId: Int) {
player?.setSubtitleTrack(trackId)
}
func disableSubtitles() {
player?.disableSubtitles()
}
func getCurrentSubtitleTrack() -> Int {
return player?.getCurrentSubtitleTrack() ?? 0
}
func addSubtitleFile(url: String, select: Bool = true) {
player?.addSubtitleFile(url: url, select: select)
}
// MARK: - Subtitle Positioning
func setSubtitlePosition(_ position: Int) {
player?.setSubtitlePosition(position)
}
func setSubtitleScale(_ scale: Double) {
player?.setSubtitleScale(scale)
}
func setSubtitleMarginY(_ margin: Int) {
player?.setSubtitleMarginY(margin)
}
func setSubtitleAlignX(_ alignment: String) {
player?.setSubtitleAlignX(alignment)
}
func setSubtitleAlignY(_ alignment: String) {
player?.setSubtitleAlignY(alignment)
}
func setSubtitleFontSize(_ size: Int) {
player?.setSubtitleFontSize(size)
}
func setSubtitleColor(_ hexColor: String) {
player?.setSubtitleColor(hexColor)
}
func setSubtitleBackgroundColor(_ hexColor: String) {
player?.setSubtitleBackgroundColor(hexColor)
}
func setSubtitleFontName(_ fontName: String) {
player?.setSubtitleFontName(fontName)
}
// MARK: - Hardware Decode (static, affects all players)
static func setHardwareDecode(_ enabled: Bool) {
SfPlayerWrapper.setHardwareDecode(enabled)
}
static func getHardwareDecode() -> Bool {
return SfPlayerWrapper.getHardwareDecode()
}
// MARK: - Audio Track Controls
func getAudioTracks() -> [[String: Any]] {
return player?.getAudioTracks() ?? []
}
func setAudioTrack(_ trackId: Int) {
player?.setAudioTrack(trackId)
}
func getCurrentAudioTrack() -> Int {
return player?.getCurrentAudioTrack() ?? 0
}
// MARK: - Video Zoom
func setVideoZoomToFill(_ enabled: Bool) {
player?.setVideoZoomToFill(enabled)
}
func getVideoZoomToFill() -> Bool {
return player?.getVideoZoomToFill() ?? false
}
deinit {
player?.stopPictureInPicture()
}
}
// MARK: - SfPlayerWrapperDelegate
extension SfPlayerView: SfPlayerWrapperDelegate {
func player(_ player: SfPlayerWrapper, didUpdatePosition position: Double, duration: Double) {
cachedPosition = position
cachedDuration = duration
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onProgress([
"position": position,
"duration": duration,
"progress": duration > 0 ? position / duration : 0,
])
}
}
func player(_ player: SfPlayerWrapper, didChangePause isPaused: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onPlaybackStateChange([
"isPaused": isPaused,
"isPlaying": !isPaused,
])
}
}
func player(_ player: SfPlayerWrapper, didChangeLoading isLoading: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onPlaybackStateChange([
"isLoading": isLoading,
])
}
}
func player(_ player: SfPlayerWrapper, didBecomeReadyToSeek: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onPlaybackStateChange([
"isReadyToSeek": didBecomeReadyToSeek,
])
}
}
func player(_ player: SfPlayerWrapper, didBecomeTracksReady: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onTracksReady([:])
}
}
func player(_ player: SfPlayerWrapper, didEncounterError error: String) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onError(["error": error])
}
}
func player(_ player: SfPlayerWrapper, didChangePictureInPicture isActive: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onPictureInPictureChange(["isActive": isActive])
}
}
}