From 669809574754d649e7991cca731ef1883b36619c Mon Sep 17 00:00:00 2001 From: Swagat Nayak Date: Sun, 14 Mar 2021 06:14:24 +0530 Subject: [PATCH] Final Relese 0.1.1 --- SNAVPlayerSubtitles/Classes/ReplaceMe.swift | 0 .../Classes/SNAVPlayerSubtitles.swift | 353 ++++++++++++++++++ 2 files changed, 353 insertions(+) delete mode 100644 SNAVPlayerSubtitles/Classes/ReplaceMe.swift create mode 100644 SNAVPlayerSubtitles/Classes/SNAVPlayerSubtitles.swift diff --git a/SNAVPlayerSubtitles/Classes/ReplaceMe.swift b/SNAVPlayerSubtitles/Classes/ReplaceMe.swift deleted file mode 100644 index e69de29..0000000 diff --git a/SNAVPlayerSubtitles/Classes/SNAVPlayerSubtitles.swift b/SNAVPlayerSubtitles/Classes/SNAVPlayerSubtitles.swift new file mode 100644 index 0000000..be31aec --- /dev/null +++ b/SNAVPlayerSubtitles/Classes/SNAVPlayerSubtitles.swift @@ -0,0 +1,353 @@ +// +// SNAVPlayerSubtitles.swift +// Pods-SNAVPlayerSubtitles_Example +// +// Created by SWAGAT-CDI on 14/03/21. +// +import ObjectiveC +import MediaPlayer +import AVKit +import CoreMedia + +private struct AssociatedKeys { + static var FontKey = "FontKey" + static var ColorKey = "FontKey" + static var SubtitleKey = "SubtitleKey" + static var SubtitleHeightKey = "SubtitleHeightKey" + static var PayloadKey = "PayloadKey" +} + +@objc public class Subtitles : NSObject { + + // MARK: - Properties + fileprivate var parsedPayload: NSDictionary? + + // MARK: - Public methods + public init(file filePath: URL, encoding: String.Encoding = String.Encoding.utf8) { + + // Get string + let string = try! String(contentsOf: filePath, encoding: encoding) + + // Parse string + parsedPayload = Subtitles.parseSubRip(string) + + } + + @objc public init(subtitles string: String) { + + // Parse string + parsedPayload = Subtitles.parseSubRip(string) + + } + + /// Search subtitles at time + /// + /// - Parameter time: Time + /// - Returns: String if exists + @objc public func searchSubtitles(at time: TimeInterval) -> String? { + + return Subtitles.searchSubtitles(parsedPayload, time) + + } + + // MARK: - Private methods + + /// Subtitle parser + /// + /// - Parameter payload: Input string + /// - Returns: NSDictionary + fileprivate static func parseSubRip(_ payload: String) -> NSDictionary? { + + do { + + // Prepare payload +// var payload = payload.replacingOccurrences(of: "\n\r\n", with: "\n\n") +// payload = payload.replacingOccurrences(of: "\n\n\n", with: "\n\n") +// payload = payload.replacingOccurrences(of: "\r\n", with: "\n") + + // Parsed dict + let parsed = NSMutableDictionary() + + // Get groups + var index: Int = 0 + let regexStr = "(?m)^(\\d{2}:\\d{2}:\\d{2}\\.\\d+) +--> +(\\d{2}:\\d{2}:\\d{2}\\.\\d+).*[\r\n]+\\s*(?s)((?:(?!\r?\n\r?\n).)*)" + let regex = try NSRegularExpression(pattern: regexStr, options: .caseInsensitive) + let matches = regex.matches(in: payload, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, payload.count)) + for m in matches { + + let group = (payload as NSString).substring(with: m.range) + + // Get index + var regex = try NSRegularExpression(pattern: "^[0-9]+", options: .caseInsensitive) + var match = regex.matches(in: group, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, group.count)) + guard match.first != nil else { + continue + } +// let index = (group as NSString).substring(with: i.range) + + // Get "from" & "to" time + regex = try NSRegularExpression(pattern: "\\d{1,2}:\\d{1,2}:\\d{1,2}[\\..]\\d{1,3}", options: .caseInsensitive) + match = regex.matches(in: group, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, group.count)) + guard match.count == 2 else { + continue + } + guard let from = match.first, let to = match.last else { + continue + } + + var h: TimeInterval = 0.0, m: TimeInterval = 0.0, s: TimeInterval = 0.0, c: TimeInterval = 0.0 + + + + var fromTime: Double + var toTime: Double + + if #available(iOS 13.0, *) { + + let fromStr = (group as NSString).substring(with: from.range) + var scanner = Scanner(string: fromStr) + h = scanner.scanDouble() ?? 0.0 + let _ = scanner.scanString(":") + m = scanner.scanDouble() ?? 0.0 + let _ = scanner.scanString(":") + s = scanner.scanDouble() ?? 0.0 + let _ = scanner.scanString(".") + c = scanner.scanDouble() ?? 0.0 + fromTime = (h * 3600.0) + (m * 60.0) + s + (c / 1000.0) + + let toStr = (group as NSString).substring(with: to.range) + scanner = Scanner(string: toStr) + h = scanner.scanDouble() ?? 0.0 + let _ = scanner.scanString(":") + m = scanner.scanDouble() ?? 0.0 + let _ = scanner.scanString(":") + s = scanner.scanDouble() ?? 0.0 + let _ = scanner.scanString(".") + c = scanner.scanDouble() ?? 0.0 + toTime = (h * 3600.0) + (m * 60.0) + s + (c / 1000.0) + + }else{ + + let fromStr = (group as NSString).substring(with: from.range) + var scanner = Scanner(string: fromStr) + scanner.scanDouble(&h) + scanner.scanString(":", into: nil) + scanner.scanDouble(&m) + scanner.scanString(":",into: nil) + scanner.scanDouble(&s) + scanner.scanString(".", into: nil) + scanner.scanDouble(&c) + fromTime = (h * 3600.0) + (m * 60.0) + s + (c / 1000.0) + + let toStr = (group as NSString).substring(with: to.range) + scanner = Scanner(string: toStr) + scanner.scanDouble(&h) + scanner.scanString(":", into: nil) + scanner.scanDouble(&m) + scanner.scanString(":",into: nil) + scanner.scanDouble(&s) + scanner.scanString(".", into: nil) + scanner.scanDouble(&c) + toTime = (h * 3600.0) + (m * 60.0) + s + (c / 1000.0) + + } + + + + + // Get text & check if empty + let range = NSMakeRange(0, to.range.location + to.range.length + 1) + guard (group as NSString).length - range.length > 0 else { + continue + } + let text = (group as NSString).replacingCharacters(in: range, with: "") + + // Create final object + let final = NSMutableDictionary() + final["from"] = fromTime + final["to"] = toTime + final["text"] = text + parsed[index] = final + + index = index+1 + + } + + return parsed + + } catch { + + return nil + + } + + } + + /// Search subtitle on time + /// + /// - Parameters: + /// - payload: Inout payload + /// - time: Time + /// - Returns: String + fileprivate static func searchSubtitles(_ payload: NSDictionary?, _ time: TimeInterval) -> String? { + + let predicate = NSPredicate(format: "(%f >= %K) AND (%f <= %K)", time, "from", time, "to") + + guard let values = payload?.allValues, let result = (values as NSArray).filtered(using: predicate).first as? NSDictionary else { + return nil + } + + guard let text = result.value(forKey: "text") as? String else { + return nil + } + + return text.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + + } + +} + +public extension AVPlayerViewController { + + // MARK: - Public properties + var subtitleLabel: UILabel? { + get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleKey) as? UILabel } + set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + // MARK: - Private properties + fileprivate var subtitleLabelHeightConstraint: NSLayoutConstraint? { + get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleHeightKey) as? NSLayoutConstraint } + set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleHeightKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + fileprivate var parsedPayload: NSDictionary? { + get { return objc_getAssociatedObject(self, &AssociatedKeys.PayloadKey) as? NSDictionary } + set (value) { objc_setAssociatedObject(self, &AssociatedKeys.PayloadKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + // MARK: - Public methods + func addSubtitles() -> Self { + + // Create label + addSubtitleLabel() + + return self + + } + + func open(fileFromLocal filePath: URL, encoding: String.Encoding = String.Encoding.utf8) { + + let contents = try! String(contentsOf: filePath, encoding: encoding) + show(subtitles: contents) + } + + func open(fileFromRemote filePath: URL, encoding: String.Encoding = String.Encoding.utf8) { + + + subtitleLabel?.text = "..." + URLSession.shared.dataTask(with: filePath, completionHandler: { (data, response, error) -> Void in + + if let httpResponse = response as? HTTPURLResponse { + let statusCode = httpResponse.statusCode + + //Check status code + if statusCode != 200 { + NSLog("Subtitle Error: \(httpResponse.statusCode) - \(error?.localizedDescription ?? "")") + return + } + } + // Update UI elements on main thread + DispatchQueue.main.async(execute: { + self.subtitleLabel?.text = "" + + if let checkData = data as Data? { + if let contents = String(data: checkData, encoding: encoding) { + self.show(subtitles: contents) + } + } + + }) + }).resume() + } + + + + func show(subtitles string: String) { + // Parse + parsedPayload = Subtitles.parseSubRip(string) + addPeriodicNotification(parsedPayload: parsedPayload!) + + } + + func showByDictionary(dictionaryContent: NSMutableDictionary) { + + // Add Dictionary content direct to Payload + parsedPayload = dictionaryContent + addPeriodicNotification(parsedPayload: parsedPayload!) + + } + + func addPeriodicNotification(parsedPayload: NSDictionary) { + // Add periodic notifications + self.player?.addPeriodicTimeObserver( + forInterval: CMTimeMake(value: 1, timescale: 60), + queue: DispatchQueue.main, + using: { [weak self] (time) -> Void in + + guard let strongSelf = self else { return } + guard let label = strongSelf.subtitleLabel else { return } + + // Search && show subtitles + label.text = Subtitles.searchSubtitles(strongSelf.parsedPayload, time.seconds) + + // Adjust size + let baseSize = CGSize(width: label.bounds.width, height: CGFloat.greatestFiniteMagnitude) + let rect = label.sizeThatFits(baseSize) + if label.text != nil { + strongSelf.subtitleLabelHeightConstraint?.constant = rect.height + 5.0 + } else { + strongSelf.subtitleLabelHeightConstraint?.constant = rect.height + } + + }) + + } + + + fileprivate func addSubtitleLabel() { + + guard let _ = subtitleLabel else { + + // Label + subtitleLabel = UILabel() + subtitleLabel?.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel?.backgroundColor = UIColor.clear + subtitleLabel?.textAlignment = .center + subtitleLabel?.numberOfLines = 0 + subtitleLabel?.font = UIFont.boldSystemFont(ofSize: UI_USER_INTERFACE_IDIOM() == .pad ? 40.0 : 22.0) + subtitleLabel?.textColor = UIColor.white + subtitleLabel?.numberOfLines = 0; + subtitleLabel?.layer.shadowColor = UIColor.black.cgColor + subtitleLabel?.layer.shadowOffset = CGSize(width: 1.0, height: 1.0); + subtitleLabel?.layer.shadowOpacity = 0.9; + subtitleLabel?.layer.shadowRadius = 1.0; + subtitleLabel?.layer.shouldRasterize = true; + subtitleLabel?.layer.rasterizationScale = UIScreen.main.scale + subtitleLabel?.lineBreakMode = .byWordWrapping + contentOverlayView?.addSubview(subtitleLabel!) + + // Position + var constraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(20)-[l]-(20)-|", options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: ["l" : subtitleLabel!]) + contentOverlayView?.addConstraints(constraints) + constraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[l]-(30)-|", options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: ["l" : subtitleLabel!]) + contentOverlayView?.addConstraints(constraints) + subtitleLabelHeightConstraint = NSLayoutConstraint(item: subtitleLabel!, attribute: .height, relatedBy: .equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1.0, constant: 30.0) + contentOverlayView?.addConstraint(subtitleLabelHeightConstraint!) + + return + + } + + } + +}