-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
79ec7cc
commit 6698095
Showing
2 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
} | ||
|
||
} | ||
|
||
} |