소스 검색

Add subtitle

Signed-off-by: marinofaggiana <ios@nextcloud.com>
marinofaggiana 3 년 전
부모
커밋
c2ab8a287c

+ 10 - 0
iOSClient/Brand/iOSClient.plist

@@ -75,6 +75,16 @@
 	<string>Photo library access is required to upload your photos and videos to your cloud.</string>
 	<key>NSPhotoLibraryUsageDescription</key>
 	<string>Photo library access is required to upload your photos and videos to your cloud.</string>
+    <key>UIAppFonts</key>
+    <array>
+        <string>Inconsolata-Light.ttf</string>
+        <string>Inconsolata-Regular.ttf</string>
+        <string>Inconsolata-ExtraLight.ttf</string>
+        <string>Inconsolata-Medium.ttf</string>
+        <string>Inconsolata-Bold.ttf</string>
+        <string>Inconsolata-ExtraBold.ttf</string>
+        <string>Inconsolata-Black.ttf</string>
+    </array>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>audio</string>

+ 13 - 4
iOSClient/Data/NCManageDatabase+Metadata.swift

@@ -808,13 +808,22 @@ extension NCManageDatabase {
         return getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameView == %@", account, serverUrl, fileNameConflict))
     }
 
-    func getSubtitles(account: String, serverUrl: String, fileName: String) -> [tableMetadata] {
+    func getSubtitles(account: String, serverUrl: String, fileName: String, exists: Bool) -> [tableMetadata] {
 
         let realm = try! Realm()
-        let nameOnly = (fileName as NSString).deletingPathExtension
+        let nameOnly = (fileName as NSString).deletingPathExtension + "."
+        var metadatas: [tableMetadata] = []
 
         let results = realm.objects(tableMetadata.self).filter("account == %@ AND serverUrl == %@ AND fileName BEGINSWITH[c] %@ AND fileName ENDSWITH[c] '.srt'", account, serverUrl, nameOnly)
-
-        return Array(results.map { tableMetadata.init(value: $0) })
+        if exists {
+            for result in results {
+                if CCUtility.fileProviderStorageExists(result) {
+                    metadatas.append(result)
+                }
+            }
+            return Array(metadatas.map { tableMetadata.init(value: $0) })
+        } else {
+            return Array(results.map { tableMetadata.init(value: $0) })
+        }
     }
 }

+ 21 - 0
iOSClient/Extensions/String+Extensions.swift

@@ -26,6 +26,27 @@ import UIKit
 import CommonCrypto
 
 extension String {
+
+    func urlSafeBase64Decoded() -> String? {
+        var st = self
+            .replacingOccurrences(of: "_", with: "/")
+            .replacingOccurrences(of: "-", with: "+")
+        let remainder = self.count % 4
+        if remainder > 0 {
+            st = self.padding(toLength: self.count + 4 - remainder,
+                              withPad: "=",
+                              startingAt: 0)
+        }
+        guard let d = Data(base64Encoded: st, options: .ignoreUnknownCharacters) else {
+            return nil
+        }
+        return String(data: d, encoding: .utf8)
+    }
+
+    var alphanumeric: String {
+        return self.components(separatedBy: CharacterSet.alphanumerics.inverted).joined().lowercased()
+    }
+
     public var uppercaseInitials: String? {
         let initials = self.components(separatedBy: .whitespaces)
             .reduce("", {

+ 37 - 0
iOSClient/Extensions/UIDevice+Extensions.swift

@@ -0,0 +1,37 @@
+//
+//  UIDevice+Extensions.swift
+//  Nextcloud
+//
+//  Created by Federico Malagoni on 23/02/22.
+//  Copyright © 2022 Marino Faggiana. All rights reserved.
+//
+//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
+//
+//  This program is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+import Foundation
+extension UIDevice {
+
+    var hasNotch: Bool {
+        if #available(iOS 11.0, *) {
+            if UIApplication.shared.windows.isEmpty { return false }
+            let top = UIApplication.shared.windows[0].safeAreaInsets.top
+            return top > 20
+        } else {
+            // Fallback on earlier versions
+            return false
+        }
+    }
+}

+ 31 - 0
iOSClient/Extensions/UIFont+Extension.swift

@@ -0,0 +1,31 @@
+//
+//  UIFont+Extensions.swift
+//  Nextcloud
+//
+//  Created by Federico Malagoni on 23/02/22.
+//  Copyright © 2022 Marino Faggiana. All rights reserved.
+//
+//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
+//
+//  This program is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+import UIKit
+
+extension UIFont {
+
+    static func incosolataMedium(size: CGFloat) -> UIFont {
+        return UIFont(name: "Inconsolata-Medium", size: size)!
+    }
+}

BIN
iOSClient/Font/Inconsolata/Inconsolata-Black.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-Bold.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-ExtraBold.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-ExtraLight.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-Light.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-Medium.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-Regular.ttf


BIN
iOSClient/Font/Inconsolata/Inconsolata-SemiBold.ttf


+ 70 - 0
iOSClient/Utility/NCUtility.swift

@@ -799,6 +799,76 @@ class NCUtility: NSObject {
             }
         }
     }
+
+    func getEncondingDataType(data: Data) -> String.Encoding? {
+        if let _ = String(data: data, encoding: .utf8) {
+            return .utf8
+        }
+        if let _ = String(data: data, encoding: .ascii) {
+            return .ascii
+        }
+        if let _ = String(data: data, encoding: .isoLatin1) {
+            return .isoLatin1
+        }
+        if let _ = String(data: data, encoding: .isoLatin2) {
+            return .isoLatin2
+        }
+        if let _ = String(data: data, encoding: .windowsCP1250) {
+            return .windowsCP1250
+        }
+        if let _ = String(data: data, encoding: .windowsCP1251) {
+            return .windowsCP1251
+        }
+        if let _ = String(data: data, encoding: .windowsCP1252) {
+            return .windowsCP1252
+        }
+        if let _ = String(data: data, encoding: .windowsCP1253) {
+            return .windowsCP1253
+        }
+        if let _ = String(data: data, encoding: .windowsCP1254) {
+            return .windowsCP1254
+        }
+        if let _ = String(data: data, encoding: .macOSRoman) {
+            return .macOSRoman
+        }
+        if let _ = String(data: data, encoding: .japaneseEUC) {
+            return .japaneseEUC
+        }
+        if let _ = String(data: data, encoding: .nextstep) {
+            return .nextstep
+        }
+        if let _ = String(data: data, encoding: .nonLossyASCII) {
+            return .nonLossyASCII
+        }
+        if let _ = String(data: data, encoding: .shiftJIS) {
+            return .shiftJIS
+        }
+        if let _ = String(data: data, encoding: .symbol) {
+            return .symbol
+        }
+        if let _ = String(data: data, encoding: .unicode) {
+            return .unicode
+        }
+        if let _ = String(data: data, encoding: .utf16) {
+            return .utf16
+        }
+        if let _ = String(data: data, encoding: .utf16BigEndian) {
+            return .utf16BigEndian
+        }
+        if let _ = String(data: data, encoding: .utf16LittleEndian) {
+            return .utf16LittleEndian
+        }
+        if let _ = String(data: data, encoding: .utf32) {
+            return .utf32
+        }
+        if let _ = String(data: data, encoding: .utf32BigEndian) {
+            return .utf32BigEndian
+        }
+        if let _ = String(data: data, encoding: .utf32LittleEndian) {
+            return .utf32LittleEndian
+        }
+        return nil
+    }
 }
 
 // MARK: -

+ 48 - 29
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift

@@ -30,7 +30,7 @@ import JGProgressHUD
 import Alamofire
 
 class NCPlayer: NSObject {
-   
+
     internal let appDelegate = UIApplication.shared.delegate as! AppDelegate
     internal var url: URL
     internal var playerToolBar: NCPlayerToolBar?
@@ -48,10 +48,17 @@ class NCPlayer: NSObject {
     public var metadata: tableMetadata
     public var videoLayer: AVPlayerLayer?
 
+    public var isSubtitleShowed: Bool = false{
+        didSet {
+            self.playerToolBar?.changeSubtitleIconTo(visible: isSubtitleShowed)
+        }
+    }
+    public var subtitleUrls: [URL] = []
+
     // MARK: - View Life Cycle
 
     init(url: URL, autoPlay: Bool, isProxy: Bool, imageVideoContainer: imageVideoContainerView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, detailView: NCViewerMediaDetailView?, viewController: UIViewController) {
-        
+
         self.url = url
         self.autoPlay = autoPlay
         self.isProxy = isProxy
@@ -62,7 +69,7 @@ class NCPlayer: NSObject {
         self.viewController = viewController
 
         super.init()
-        
+
         do {
             try AVAudioSession.sharedInstance().setCategory(.playback)
             try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
@@ -70,19 +77,26 @@ class NCPlayer: NSObject {
         } catch {
             print(error)
         }
-        
+
         openAVPlayer()
     }
-    
+
     internal func openAVPlayer() {
-        
-        #if MFFFLIB
+
+#if MFFFLIB
+        if CCUtility.fileProviderStorageExists(metadata.ocId, fileNameView: NCGlobal.shared.fileNameVideoEncoded) {
+            self.url = URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: NCGlobal.shared.fileNameVideoEncoded))
+            self.isProxy = false
+        }
         if MFFF.shared.existsMFFFSession(url: URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView))) {
             return
         } else {
             MFFF.shared.dismissMessage()
         }
-        #endif
+#endif
+
+        self.setUpForSubtitle()
+        self.isSubtitleShowed = false
 
         print("Play URL: \(self.url)")
         player = AVPlayer(url: self.url)
@@ -144,17 +158,17 @@ class NCPlayer: NSObject {
                         alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", value: "No", comment: ""), style: .default, handler: { _ in }))
                         self.viewController.present(alertController, animated: true)
                     } else {
-                        #if MFFFLIB
+#if MFFFLIB
                         if error?.code == AVError.Code.fileFormatNotRecognized.rawValue {
-                            self.convertVideo()
+                            self.convertVideo(withAlert: true)
+                            break
                         }
-                        #else
+#endif
                         if let title = error?.localizedDescription, let description = error?.localizedFailureReason {
                             NCContentPresenter.shared.messageNotification(title, description: description, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: NCGlobal.shared.errorGeneric, priority: .max)
                         } else {
                             NCContentPresenter.shared.messageNotification("_error_", description: "_error_something_wrong_", delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: NCGlobal.shared.errorGeneric, priority: .max)
                         }
-                        #endif
                     }
                     break
                 case .cancelled:
@@ -166,8 +180,8 @@ class NCPlayer: NSObject {
         })
     }
 
-    internal func downloadVideo() {
-        
+    internal func downloadVideo(requiredConvert: Bool = false) {
+
         let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
         let fileNameLocalPath = CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)!
         let hud = JGProgressHUD()
@@ -183,7 +197,7 @@ class NCPlayer: NSObject {
         hud.tapOnHUDViewBlock = { hud in
             downloadRequest?.cancel()
         }
-        
+
         NCCommunication.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath) { request in
             downloadRequest = request
         } taskHandler: { task in
@@ -197,7 +211,13 @@ class NCPlayer: NSObject {
                 if let url = urlVideo.url {
                     self.url = url
                     self.isProxy = urlVideo.isProxy
-                    self.openAVPlayer()
+                    if requiredConvert {
+                        #if MFFFLIB
+                        self.convertVideo(withAlert: false)
+                        #endif
+                    } else {
+                        self.openAVPlayer()
+                    }
                 }
             }
             hud.dismiss()
@@ -217,15 +237,15 @@ class NCPlayer: NSObject {
         // At end go back to start & show toolbar
         observerAVPlayerItemDidPlayToEndTime = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) { notification in
             if let item = notification.object as? AVPlayerItem, let currentItem = self.player?.currentItem, item == currentItem {
-                
+
                 NCKTVHTTPCache.shared.saveCache(metadata: self.metadata)
-                
+
                 self.videoSeek(time: .zero)
-               
+
                 if !(self.detailView?.isShow() ?? false) {
                     NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterShowPlayerToolBar, userInfo: ["ocId": self.metadata.ocId, "enableTimerAutoHide": false])
                 }
-                
+
                 self.playerToolBar?.updateToolBar()
             }
         }
@@ -240,7 +260,7 @@ class NCPlayer: NSObject {
         NotificationCenter.default.addObserver(self, selector: #selector(generatorImagePreview), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationWillResignActive), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationDidEnterBackground), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationDidBecomeActive), object: nil)
-        
+
         NotificationCenter.default.addObserver(self, selector: #selector(playerPause), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterPauseMedia), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(playerPlay), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterPlayMedia), object: nil)
     }
@@ -266,7 +286,7 @@ class NCPlayer: NSObject {
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationWillResignActive), object: nil)
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationDidEnterBackground), object: nil)
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationDidBecomeActive), object: nil)
-        
+
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterPauseMedia), object: nil)
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterPlayMedia), object: nil)
     }
@@ -274,7 +294,7 @@ class NCPlayer: NSObject {
     // MARK: - NotificationCenter
 
     @objc func applicationDidEnterBackground(_ notification: NSNotification) {
-                
+
         if metadata.classFile == NCCommunicationCommon.typeClassFile.video.rawValue, let playerToolBar = self.playerToolBar {
             if !playerToolBar.isPictureInPictureActive() {
                 playerPause()
@@ -286,22 +306,22 @@ class NCPlayer: NSObject {
 
         playerToolBar?.updateToolBar()
     }
-    
+
     // MARK: -
 
     func isPlay() -> Bool {
 
         if player?.rate == 1 { return true } else { return false }
     }
-    
+
     @objc func playerPlay() {
-                
+
         player?.play()
         self.playerToolBar?.updateToolBar()
     }
-    
+
     @objc func playerPause() {
-        
+
         player?.pause()
         self.playerToolBar?.updateToolBar()
 
@@ -367,4 +387,3 @@ class NCPlayer: NSObject {
         }
     }
 }
-

+ 31 - 31
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift

@@ -41,14 +41,14 @@ class NCPlayerToolBar: UIView {
     @IBOutlet weak var playbackSlider: UISlider!
     @IBOutlet weak var labelLeftTime: UILabel!
     @IBOutlet weak var labelCurrentTime: UILabel!
-   
+
     enum sliderEventType {
         case began
         case ended
         case moved
     }
 
-    private var ncplayer: NCPlayer?
+    var ncplayer: NCPlayer?
     private var metadata: tableMetadata?
     private var wasInPlay: Bool = false
     private var playbackSliderEvent: sliderEventType = .ended
@@ -83,11 +83,11 @@ class NCPlayerToolBar: UIView {
 
         muteButton.setImage(NCUtility.shared.loadImage(named: "audioOff", color: .lightGray), for: .normal)
         muteButton.isEnabled = false
-        
+
         subtitleButton.setImage(NCUtility.shared.loadImage(named: "captions.bubble", color: .lightGray), for: .normal)
         subtitleButton.isEnabled = false
         subtitleButton.isHidden = true
-        
+
         playbackSlider.value = 0
         playbackSlider.minimumValue = 0
         playbackSlider.maximumValue = 0
@@ -102,10 +102,10 @@ class NCPlayerToolBar: UIView {
 
         backButton.setImage(NCUtility.shared.loadImage(named: "gobackward.10", color: .lightGray), for: .normal)
         backButton.isEnabled = false
-        
+
         playButton.setImage(NCUtility.shared.loadImage(named: "play.fill", color: .lightGray), for: .normal)
         playButton.isEnabled = false
-        
+
         forwardButton.setImage(NCUtility.shared.loadImage(named: "goforward.10", color: .lightGray), for: .normal)
         forwardButton.isEnabled = false
 
@@ -125,12 +125,12 @@ class NCPlayerToolBar: UIView {
     }
 
     // MARK: -
-    
+
     func setMetadata(_ metadata: tableMetadata) {
-        
+
         self.metadata = metadata
     }
-    
+
     func setBarPlayer(ncplayer: NCPlayer) {
 
         self.ncplayer = ncplayer
@@ -147,7 +147,7 @@ class NCPlayerToolBar: UIView {
     }
 
     public func updateToolBar() {
-        
+
         guard let ncplayer = self.ncplayer else { return }
 
         // MUTE
@@ -213,7 +213,7 @@ class NCPlayerToolBar: UIView {
     // MARK: Handle Notifications
 
     @objc func handleRouteChange(notification: Notification) {
-        
+
         guard let userInfo = notification.userInfo, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
 
         switch reason {
@@ -275,13 +275,13 @@ class NCPlayerToolBar: UIView {
             return
         }
         #endif
-        
+
         timerAutoHide?.invalidate()
         if enableTimerAutoHide {
             startTimerAutoHide()
         }
         if !self.isHidden { return }
-        
+
         UIView.animate(withDuration: 0.3, animations: {
             self.alpha = 1
         }, completion: { (_: Bool) in
@@ -369,7 +369,7 @@ class NCPlayerToolBar: UIView {
 
         timerAutoHide?.invalidate()
     }
-    
+
     // MARK: - Event / Gesture
 
     @objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
@@ -404,9 +404,9 @@ class NCPlayerToolBar: UIView {
     // MARK: - Action
 
     @objc func tapTopToolBarWith(gestureRecognizer: UITapGestureRecognizer) { }
-    
+
     @objc func tapToolBarWith(gestureRecognizer: UITapGestureRecognizer) { }
-    
+
     @IBAction func tapPlayerPause(_ sender: Any) {
 
         if ncplayer?.player?.timeControlStatus == .playing {
@@ -469,12 +469,12 @@ class NCPlayerToolBar: UIView {
         skip(seconds: 10)
 
         /*
-        if metadata?.classFile == NCCommunicationCommon.typeClassFile.video.rawValue {
-            skip(seconds: 10)
-        } else if metadata?.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
-            forward()
-        }
-        */
+         if metadata?.classFile == NCCommunicationCommon.typeClassFile.video.rawValue {
+         skip(seconds: 10)
+         } else if metadata?.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
+         forward()
+         }
+         */
     }
 
     @IBAction func tapBack(_ sender: Any) {
@@ -482,18 +482,18 @@ class NCPlayerToolBar: UIView {
         skip(seconds: -10)
 
         /*
-        if metadata?.classFile == NCCommunicationCommon.typeClassFile.video.rawValue {
-            skip(seconds: -10)
-        } else if metadata?.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
-            backward()
-        }
-        */
+         if metadata?.classFile == NCCommunicationCommon.typeClassFile.video.rawValue {
+         skip(seconds: -10)
+         } else if metadata?.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
+         backward()
+         }
+         */
     }
-    
+
     @IBAction func tapSubtitle(_ sender: Any) {
-        
+        self.subtitleIconTapped()
     }
-    
+
     func forward() {
 
         var index: Int = 0

+ 56 - 0
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift

@@ -0,0 +1,56 @@
+//
+//  NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift
+//  Nextcloud
+//
+//  Created by Federico Malagoni on 11/03/22.
+//  Copyright © 2022 Marino Faggiana. All rights reserved.
+//
+//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
+//
+//  This program is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+import Foundation
+
+protocol SubtitlePlayerToolBarDelegate: AnyObject {
+    func subtitleIconTapped()
+    func changeSubtitleIconTo(visible: Bool)
+    func showIconSubtitle()
+}
+
+extension NCPlayerToolBar: SubtitlePlayerToolBarDelegate {
+
+    func subtitleIconTapped() {
+
+        self.ncplayer?.hideOrShowSubtitle()
+    }
+
+    func changeSubtitleIconTo(visible: Bool) {
+
+        let color: UIColor = visible ? .white : .lightGray
+        self.subtitleButton.setImage(NCUtility.shared.loadImage(named: "captions.bubble", color: color), for: .normal)
+    }
+
+    func showIconSubtitle() {
+
+        self.subtitleButton.isEnabled = true
+        self.subtitleButton.isHidden = false
+    }
+
+    func hideIconSubtitle() {
+
+        self.subtitleButton.isEnabled = false
+        self.subtitleButton.isHidden = true
+    }
+}

+ 103 - 0
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCSubtitlePlayer+PlayerSubtitleDelegate.swift

@@ -0,0 +1,103 @@
+//
+//  NCPlayer+PlayerSubtitleDelegate.swift
+//  Nextcloud
+//
+//  Created by Federico Malagoni on 11/03/22.
+//  Copyright © 2022 Marino Faggiana. All rights reserved.
+//
+//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
+//
+//  This program is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+import Foundation
+import NCCommunication
+import UIKit
+import AVFoundation
+import MediaPlayer
+import Alamofire
+import RealmSwift
+
+public protocol PlayerSubtitleDelegate: AnyObject {
+    func hideOrShowSubtitle()
+    func showAlertSubtitles()
+}
+
+extension NCPlayer: PlayerSubtitleDelegate {
+
+    func hideOrShowSubtitle() {
+        if self.isSubtitleShowed {
+            self.hideSubtitle()
+            self.isSubtitleShowed = false
+        } else {
+            self.showAlertSubtitles()
+        }
+    }
+
+    internal func showAlertSubtitles() {
+
+        let alert = UIAlertController(title: nil, message: NSLocalizedString("_subtitle_", comment: ""), preferredStyle: .actionSheet)
+
+        for url in subtitleUrls {
+
+            print("Play Subtitle at:\n\(url.path)")
+
+            let videoUrlTitle = self.metadata.fileName.alphanumeric.dropLast(3)
+            let subtitleUrlTitle = url.lastPathComponent.alphanumeric.dropLast(3)
+
+            var titleSubtitle = String(subtitleUrlTitle.dropFirst(videoUrlTitle.count))
+            if titleSubtitle.isEmpty {
+                titleSubtitle = NSLocalizedString("_subtitle_", comment: "")
+            }
+
+            alert.addAction(UIAlertAction(title: titleSubtitle, style: .default, handler: { [self] _ in
+
+                if NCUtilityFileSystem.shared.getFileSize(filePath: url.path) > 0 {
+
+                    do {
+                        try self.open(fileFromLocal: url)
+                        self.addSubtitlesTo(viewController, self.playerToolBar)
+                        self.isSubtitleShowed = true
+                        self.showSubtitle()
+                    } catch {
+                        print(error)
+                    }
+
+                    /*
+                    self.open(fileFromRemote: url)
+                    self.showSubtitle()
+                    self.isSubtitleShowed = true
+                    */
+
+                } else {
+
+                    let alertError = UIAlertController(title: NSLocalizedString("_error_", comment: ""), message: NSLocalizedString("_subtitle_not_found_", comment: ""), preferredStyle: .alert)
+                    alertError.addAction(UIKit.UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: nil))
+
+                    viewController.present(alertError, animated: true, completion: nil)
+                    self.isSubtitleShowed = false
+                }
+
+            }))
+        }
+
+        alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: { _ in
+            self.isSubtitleShowed = false
+        }))
+
+        alert.popoverPresentationController?.sourceView = self.viewController.view
+
+        self.viewController.present(alert, animated: true, completion: nil)
+    }
+}

+ 343 - 0
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCSubtitlePlayer.swift

@@ -0,0 +1,343 @@
+//
+//  AVPlayer+Extensions.swift
+//  Nextcloud
+//
+//  Created by Federico Malagoni on 18/02/22.
+//  Copyright © 2022 Federico Malagoni. All rights reserved.
+//
+//  Author Federico Malagoni <federico.malagoni@astrairidium.com>
+//
+//  This program is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+import Foundation
+import AVKit
+
+extension NCPlayer {
+
+    private struct AssociatedKeys {
+        static var FontKey = "FontKey"
+        static var ColorKey = "FontKey"
+        static var SubtitleKey = "SubtitleKey"
+        static var SubtitleContainerViewKey = "SubtitleContainerViewKey"
+        static var SubtitleContainerViewHeightKey = "SubtitleContainerViewHeightKey"
+        static var SubtitleHeightKey = "SubtitleHeightKey"
+        static var SubtitleWidthKey = "SubtitleWidthKey"
+        static var SubtitleContainerViewWidthKey = "SubtitleContainerViewWidthKey"
+        static var SubtitleBottomKey = "SubtitleBottomKey"
+        static var PayloadKey = "PayloadKey"
+    }
+
+    private var widthProportion: CGFloat {
+        return 0.9
+    }
+
+    private var bottomConstantPortrait: CGFloat {
+        get {
+            if UIDevice.current.hasNotch {
+                return -60
+            } else {
+                return -40
+            }
+        } set {
+            _ = newValue
+        }
+    }
+
+    private var bottomConstantLandscape: CGFloat {
+        get {
+            if UIDevice.current.hasNotch {
+                return -120
+            } else {
+                return -100
+            }
+        } set {
+            _ = newValue
+        }
+    }
+
+    var subtitleContainerView: UIView? {
+        get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewKey) as? UIView }
+        set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)}
+    }
+
+    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) }
+    }
+
+    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 subtitleContainerViewHeightConstraint: NSLayoutConstraint? {
+        get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewHeightKey) as? NSLayoutConstraint }
+        set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewHeightKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+    }
+
+    fileprivate var subtitleLabelBottomConstraint: NSLayoutConstraint? {
+        get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleBottomKey) as? NSLayoutConstraint }
+        set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleBottomKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+    }
+
+    fileprivate var subtitleLabelWidthConstraint: NSLayoutConstraint? {
+        get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleWidthKey) as? NSLayoutConstraint }
+        set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleWidthKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+    }
+    fileprivate var subtitleContainerViewWidthConstraint: NSLayoutConstraint? {
+        get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewWidthKey) as? NSLayoutConstraint }
+        set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewWidthKey, 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) }
+    }
+
+    func setUpForSubtitle() {
+        self.subtitleUrls.removeAll()
+        if let url = CCUtility.getDirectoryProviderStorageOcId(metadata.ocId) {
+            let enumerator = FileManager.default.enumerator(atPath: url)
+            let filePaths = (enumerator?.allObjects as? [String])
+            if let filePaths = filePaths {
+                let txtFilePaths = (filePaths.filter { $0.contains(".srt") }).sorted {
+                    guard let str1LastChar = $0.dropLast(4).last, let str2LastChar = $1.dropLast(4).last else {
+                        return false
+                    }
+                    return str1LastChar < str2LastChar
+                }
+                for txtFilePath in txtFilePaths {
+                    let subtitleUrl = URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: txtFilePath))
+                    self.subtitleUrls.append(subtitleUrl)
+                }
+            }
+        }
+        let subtitles = NCManageDatabase.shared.getSubtitles(account: metadata.account, serverUrl: metadata.serverUrl, fileName: metadata.fileName, exists: true)
+        if !subtitles.isEmpty {
+            for subtitle in subtitles {
+                let subtitleUrl = URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(subtitle.ocId, fileNameView: subtitle.fileName))
+                self.subtitleUrls.append(subtitleUrl)
+            }
+        }
+        self.setSubtitleToolbarIcon(subtitleUrls: subtitleUrls)
+        self.hideSubtitle()
+        self.isSubtitleShowed = false
+    }
+
+    func setSubtitleToolbarIcon(subtitleUrls: [URL]) {
+        if subtitleUrls.isEmpty {
+            self.playerToolBar?.hideIconSubtitle()
+        } else {
+            self.playerToolBar?.showIconSubtitle()
+        }
+    }
+
+    func addSubtitlesTo(_ vc: UIViewController, _ playerToolBar: NCPlayerToolBar?) {
+        addSubtitleLabel(vc, playerToolBar)
+        NotificationCenter.default.addObserver(self, selector: #selector(deviceRotated(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
+    }
+
+    func loadText(filePath: URL, _ completion:@escaping (_ contents: String?) -> Void) {
+        DispatchQueue.global(qos: .background).async {
+            guard let data = try? Data(contentsOf: filePath),
+                  let encoding = NCUtility.shared.getEncondingDataType(data: data) else {
+                return
+            }
+            if let decodedString = String(data: data, encoding: encoding) {
+                completion(decodedString)
+            } else {
+                completion(nil)
+            }
+         }
+    }
+
+    func open(fileFromLocal filePath: URL) {
+        print("Subtitle filePath \(filePath)")
+        subtitleLabel?.text = ""
+
+        self.loadText(filePath: filePath) { contents in
+            guard let contents = contents else {
+                return
+            }
+            DispatchQueue.main.async {
+                self.subtitleLabel?.text = ""
+                self.show(subtitles: contents)
+            }
+        }
+    }
+
+    @objc public func hideSubtitle() {
+        self.subtitleLabel?.isHidden = true
+        self.subtitleContainerView?.isHidden = true
+    }
+
+    @objc public func showSubtitle() {
+        self.subtitleLabel?.isHidden = false
+        self.subtitleContainerView?.isHidden = false
+    }
+
+    private func show(subtitles string: String) {
+        parsedPayload = try? NCSubtitles.parseSubRip(string)
+        if let parsedPayload = parsedPayload {
+            addPeriodicNotification(parsedPayload: parsedPayload)
+        }
+    }
+
+    private func showByDictionary(dictionaryContent: NSMutableDictionary) {
+        parsedPayload = dictionaryContent
+        if let parsedPayload = parsedPayload {
+            addPeriodicNotification(parsedPayload: parsedPayload)
+        }
+    }
+
+    func addPeriodicNotification(parsedPayload: NSDictionary) {
+        // Add periodic notifications
+        let interval = CMTimeMake(value: 1, timescale: 60)
+        self.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
+            guard let strongSelf = self, let label = strongSelf.subtitleLabel, let containerView = strongSelf.subtitleContainerView else {
+                return
+            }
+            DispatchQueue.main.async {
+                label.text = NCSubtitles.searchSubtitles(strongSelf.parsedPayload, time.seconds)
+                strongSelf.adjustViewWidth(containerView: containerView)
+                strongSelf.adjustLabelHeight(label: label)
+            }
+        }
+    }
+
+    @objc private func deviceRotated(_ notification: Notification) {
+        guard let label = self.subtitleLabel,
+              let containerView = self.subtitleContainerView else { return }
+        DispatchQueue.main.async {
+            self.adjustViewWidth(containerView: containerView)
+            self.adjustLabelHeight(label: label)
+            self.adjustLabelBottom(label: label)
+            containerView.layoutIfNeeded()
+            label.layoutIfNeeded()
+        }
+    }
+
+    private func adjustLabelHeight(label: UILabel) {
+        let baseSize = CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude)
+        let rect = label.sizeThatFits(baseSize)
+        if label.text != nil {
+            self.subtitleLabelHeightConstraint?.constant = rect.height + 5.0
+        } else {
+            self.subtitleLabelHeightConstraint?.constant = rect.height
+        }
+    }
+
+    private func adjustLabelBottom(label: UILabel) {
+        var bottomConstant: CGFloat = bottomConstantPortrait
+
+        switch UIApplication.shared.statusBarOrientation {
+        case .portrait:
+            bottomConstant = bottomConstantLandscape
+        case .landscapeLeft, .landscapeRight, .portraitUpsideDown:
+            bottomConstant = bottomConstantPortrait
+        default:
+            ()
+        }
+        subtitleLabelBottomConstraint?.constant = bottomConstant
+    }
+
+    private func adjustViewWidth(containerView: UIView) {
+        let widthConstant: CGFloat = UIScreen.main.bounds.width * widthProportion
+        subtitleContainerViewWidthConstraint!.constant = widthConstant
+        subtitleLabel?.preferredMaxLayoutWidth = (widthConstant - 20)
+    }
+
+    fileprivate func addSubtitleLabel(_ vc: UIViewController, _ playerToolBar: NCPlayerToolBar?) {
+        guard subtitleLabel == nil,
+              subtitleContainerView == nil else {
+                  return
+              }
+        subtitleContainerView = UIView()
+        subtitleLabel = UILabel()
+
+        subtitleContainerView?.translatesAutoresizingMaskIntoConstraints = false
+        subtitleContainerView?.layer.cornerRadius = 5.0
+        subtitleContainerView?.layer.masksToBounds = true
+        subtitleContainerView?.layer.shouldRasterize = true
+        subtitleContainerView?.layer.rasterizationScale = UIScreen.main.scale
+        subtitleContainerView?.backgroundColor = UIColor.black.withAlphaComponent(0.35)
+
+        subtitleLabel?.translatesAutoresizingMaskIntoConstraints = false
+        subtitleLabel?.textAlignment = .center
+        subtitleLabel?.numberOfLines = 0
+        let fontSize = UIDevice.current.userInterfaceIdiom == .pad ? 38.0 : 20.0
+        subtitleLabel?.font = UIFont.incosolataMedium(size: fontSize)
+        subtitleLabel?.lineBreakMode = .byWordWrapping
+        subtitleLabel?.textColor = .white
+        subtitleLabel?.backgroundColor = .clear
+
+        subtitleContainerView?.addSubview(subtitleLabel!)
+
+        var isFound = false
+
+        for v in vc.view.subviews where v is UIScrollView {
+            if let scrollView = v as? UIScrollView {
+                for subView in scrollView.subviews where subView is imageVideoContainerView {
+                    subView.addSubview(subtitleContainerView!)
+                    isFound = true
+                    break
+                }
+            }
+        }
+
+        if !isFound {
+            vc.view.addSubview(subtitleContainerView!)
+        }
+
+        NSLayoutConstraint.activate([
+            subtitleLabel!.centerXAnchor.constraint(equalTo: subtitleContainerView!.centerXAnchor),
+            subtitleLabel!.centerYAnchor.constraint(equalTo: subtitleContainerView!.centerYAnchor)
+        ])
+
+        subtitleContainerViewHeightConstraint = NSLayoutConstraint(item: subtitleContainerView!, attribute: .height, relatedBy: .equal, toItem: subtitleLabel!, attribute: .height, multiplier: 1.0, constant: 0.0)
+        vc.view?.addConstraint(subtitleContainerViewHeightConstraint!)
+
+        var bottomConstant: CGFloat = bottomConstantPortrait
+
+        switch UIApplication.shared.statusBarOrientation {
+        case .portrait, .portraitUpsideDown:
+            bottomConstant = bottomConstantLandscape
+        case .landscapeLeft, .landscapeRight:
+            bottomConstant = bottomConstantPortrait
+        default:
+            ()
+        }
+
+        let widthConstant: CGFloat = UIScreen.main.bounds.width * widthProportion
+
+        NSLayoutConstraint.activate([
+            subtitleContainerView!.centerXAnchor.constraint(equalTo: vc.view.centerXAnchor)
+        ])
+
+        subtitleContainerViewWidthConstraint = NSLayoutConstraint(item: subtitleContainerView!, attribute: .width, relatedBy: .lessThanOrEqual, toItem: nil,
+                                                                  attribute: .width, multiplier: 1, constant: widthConstant)
+
+        // setting default width == 0 because there is no text inside of the label
+        subtitleLabelWidthConstraint = NSLayoutConstraint(item: subtitleLabel!, attribute: .width, relatedBy: .equal, toItem: subtitleContainerView,
+                                                          attribute: .width, multiplier: 1, constant: -20)
+
+        subtitleLabelBottomConstraint = NSLayoutConstraint(item: subtitleContainerView!, attribute: .bottom, relatedBy: .equal, toItem: vc.view, attribute:
+                                                                .bottom, multiplier: 1, constant: bottomConstant)
+
+        vc.view?.addConstraint(subtitleContainerViewWidthConstraint!)
+        vc.view?.addConstraint(subtitleLabelWidthConstraint!)
+        vc.view?.addConstraint(subtitleLabelBottomConstraint!)
+    }
+}

+ 150 - 0
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCSubtitles.swift

@@ -0,0 +1,150 @@
+//
+//  NCSubtitles
+//  Nextcloud
+//
+//  Created by Marc Hervera.
+//  Copyright 2017 Marc Hervera AVPlayerViewController-Subtitles v1.3.1 iOS
+//
+//  Modified by Federico Malagoni on 23/02/22 for Nextcloud.
+//
+//  Licensed under Apache License v2.0.
+//
+
+import AVKit
+
+class NCSubtitles {
+
+    // MARK: - Private properties
+
+    private var parsedPayload: NSDictionary?
+
+    // MARK: - Public methods
+
+    public init(file filePath: URL, encoding: String.Encoding = .utf8) throws {
+        // Get string
+        let string = try String(contentsOf: filePath, encoding: encoding)
+        // Parse string
+        parsedPayload = try NCSubtitles.parseSubRip(string)
+    }
+
+    public init(subtitles string: String) throws {
+        // Parse string
+        parsedPayload = try NCSubtitles.parseSubRip(string)
+    }
+
+    /// Search subtitles at time
+    ///
+    /// - Parameter time: Time
+    /// - Returns: String if exists
+    public func searchSubtitles(at time: TimeInterval) -> String? {
+        return NCSubtitles.searchSubtitles(parsedPayload, time)
+    }
+
+    // MARK: - Static methods
+
+    /// Subtitle parser
+    ///
+    /// - Parameter payload: Input string
+    /// - Returns: NSDictionary
+    static func parseSubRip(_ payload: String) throws -> NSDictionary? {
+        // 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
+        let regexStr = "(\\d+)\\n([\\d:,.]+)\\s+-{2}\\>\\s+([\\d:,.]+)\\n([\\s\\S]*?(?=\\n{2,}|$))"
+        let regex = try NSRegularExpression(pattern: regexStr, options: .caseInsensitive)
+        let matches = regex.matches(in: payload, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSRange(location: 0, length: 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: NSRange(location: 0, length: group.count))
+
+            guard let i = match.first 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: NSRange(location: 0, length: 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
+
+            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)
+            let 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)
+            let toTime = (h * 3600.0) + (m * 60.0) + s + (c / 1000.0)
+
+            // Get text & check if empty
+            let range = NSRange(location: 0, length: 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
+        }
+
+        return parsed
+    }
+
+    /// Search subtitle on time
+    ///
+    /// - Parameters:
+    ///   - payload: Inout payload
+    ///   - time: Time
+    /// - Returns: String
+    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)
+    }
+}