From cc4c75f88af2c28bc97f6314efd50a96e303d57d Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:06:42 +0200 Subject: [PATCH] let's text @bshar1865 code Co-Authored-By: Bshar Esfky <98615778+bshar1865@users.noreply.github.com> --- Sora/Utils/MediaPlayer/SubtitleManager.swift | 81 ++++++++++++++++++++ Sora/Utils/MediaPlayer/VideoPlayer.swift | 40 +++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 Sora/Utils/MediaPlayer/SubtitleManager.swift diff --git a/Sora/Utils/MediaPlayer/SubtitleManager.swift b/Sora/Utils/MediaPlayer/SubtitleManager.swift new file mode 100644 index 0000000..d0cdc7b --- /dev/null +++ b/Sora/Utils/MediaPlayer/SubtitleManager.swift @@ -0,0 +1,81 @@ +// +// SubtitleManager.swift +// Sora +// +// Created by Francesco on 10/06/25. +// + +import UIKit +import Foundation +import AVFoundation + +class SubtitleManager { + static let shared = SubtitleManager() + private let subtitleLoader = VTTSubtitlesLoader() + + private init() {} + + func loadSubtitles(from url: URL) async throws -> [SubtitleCue] { + return await withCheckedContinuation { continuation in + subtitleLoader.load(from: url.absoluteString) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + continuation.resume(returning: self.subtitleLoader.cues) + } + } + } + + func createSubtitleOverlay(for cues: [SubtitleCue], player: AVPlayer) -> SubtitleOverlayView { + let overlay = SubtitleOverlayView() + let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + + player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in + let currentTime = time.seconds + let currentCue = cues.first { cue in + currentTime >= cue.startTime && currentTime <= cue.endTime + } + overlay.update(with: currentCue?.text ?? "") + } + + return overlay + } +} + +class SubtitleOverlayView: UIView { + private let label: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .center + label.textColor = .white + label.font = .systemFont(ofSize: 16, weight: .medium) + label.layer.shadowColor = UIColor.black.cgColor + label.layer.shadowOffset = CGSize(width: 1, height: 1) + label.layer.shadowOpacity = 0.8 + label.layer.shadowRadius = 2 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + backgroundColor = .clear + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), + label.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + func update(with text: String) { + label.text = text + } +} diff --git a/Sora/Utils/MediaPlayer/VideoPlayer.swift b/Sora/Utils/MediaPlayer/VideoPlayer.swift index 58a9a65..384153c 100644 --- a/Sora/Utils/MediaPlayer/VideoPlayer.swift +++ b/Sora/Utils/MediaPlayer/VideoPlayer.swift @@ -24,6 +24,7 @@ class VideoPlayerViewController: UIViewController { var episodeNumber: Int = 0 var episodeImageUrl: String = "" var mediaTitle: String = "" + var subtitleOverlay: SubtitleOverlayView? init(module: ScrapingModule) { self.module = module @@ -66,6 +67,24 @@ class VideoPlayerViewController: UIViewController { playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(playerViewController.view) playerViewController.didMove(toParent: self) + + if !subtitles.isEmpty && UserDefaults.standard.bool(forKey: "subtitlesEnabled") { + if let subtitleURL = URL(string: subtitles) { + Task { + do { + let subtitleCues = try await SubtitleManager.shared.loadSubtitles(from: subtitleURL) + await MainActor.run { + if let player = self.player { + let overlay = SubtitleManager.shared.createSubtitleOverlay(for: subtitleCues, player: player) + self.addSubtitleOverlay(overlay) + } + } + } catch { + Logger.shared.log("Failed to load subtitles: \(error.localizedDescription)", type: "Error") + } + } + } + } } addPeriodicTimeObserver(fullURL: fullUrl) @@ -113,8 +132,8 @@ class VideoPlayerViewController: UIViewController { guard let self = self, let currentItem = player.currentItem, currentItem.duration.seconds.isFinite else { - return - } + return + } let currentTime = time.seconds let duration = currentItem.duration.seconds @@ -158,6 +177,22 @@ class VideoPlayerViewController: UIViewController { } } + private func addSubtitleOverlay(_ overlay: SubtitleOverlayView) { + subtitleOverlay?.removeFromSuperview() + subtitleOverlay = overlay + + guard let playerView = playerViewController?.view else { return } + playerView.addSubview(overlay) + overlay.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + overlay.leadingAnchor.constraint(equalTo: playerView.leadingAnchor), + overlay.trailingAnchor.constraint(equalTo: playerView.trailingAnchor), + overlay.bottomAnchor.constraint(equalTo: playerView.bottomAnchor), + overlay.heightAnchor.constraint(equalToConstant: 100) + ]) + } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape @@ -179,5 +214,6 @@ class VideoPlayerViewController: UIViewController { if let timeObserverToken = timeObserverToken { player?.removeTimeObserver(timeObserverToken) } + subtitleOverlay?.removeFromSuperview() } }