NCSubtitlePlayer.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. //
  2. // NCSubtitlePlayer.swift
  3. // Nextcloud
  4. //
  5. // Created by Federico Malagoni on 18/02/22.
  6. // Copyright © 2022 Federico Malagoni. All rights reserved.
  7. // Copyright © 2022 Marino Faggiana All rights reserved.
  8. //
  9. // Author Federico Malagoni <federico.malagoni@astrairidium.com>
  10. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  11. //
  12. // This program is free software: you can redistribute it and/or modify
  13. // it under the terms of the GNU General Public License as published by
  14. // the Free Software Foundation, either version 3 of the License, or
  15. // (at your option) any later version.
  16. //
  17. // This program is distributed in the hope that it will be useful,
  18. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. // GNU General Public License for more details.
  21. //
  22. // You should have received a copy of the GNU General Public License
  23. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. //
  25. import Foundation
  26. import AVKit
  27. extension NCPlayer {
  28. private struct AssociatedKeys {
  29. static var FontKey = "FontKey"
  30. static var ColorKey = "FontKey"
  31. static var SubtitleKey = "SubtitleKey"
  32. static var SubtitleContainerViewKey = "SubtitleContainerViewKey"
  33. static var SubtitleContainerViewHeightKey = "SubtitleContainerViewHeightKey"
  34. static var SubtitleHeightKey = "SubtitleHeightKey"
  35. static var SubtitleWidthKey = "SubtitleWidthKey"
  36. static var SubtitleContainerViewWidthKey = "SubtitleContainerViewWidthKey"
  37. static var SubtitleBottomKey = "SubtitleBottomKey"
  38. static var PayloadKey = "PayloadKey"
  39. }
  40. private var widthProportion: CGFloat {
  41. return 0.9
  42. }
  43. private var bottomConstantPortrait: CGFloat {
  44. get {
  45. if UIDevice.current.hasNotch {
  46. return -60
  47. } else {
  48. return -40
  49. }
  50. } set {
  51. _ = newValue
  52. }
  53. }
  54. private var bottomConstantLandscape: CGFloat {
  55. get {
  56. if UIDevice.current.hasNotch {
  57. return -120
  58. } else {
  59. return -100
  60. }
  61. } set {
  62. _ = newValue
  63. }
  64. }
  65. var subtitleContainerView: UIView? {
  66. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewKey) as? UIView }
  67. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)}
  68. }
  69. var subtitleLabel: UILabel? {
  70. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleKey) as? UILabel }
  71. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  72. }
  73. fileprivate var subtitleLabelHeightConstraint: NSLayoutConstraint? {
  74. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleHeightKey) as? NSLayoutConstraint }
  75. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleHeightKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  76. }
  77. fileprivate var subtitleContainerViewHeightConstraint: NSLayoutConstraint? {
  78. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewHeightKey) as? NSLayoutConstraint }
  79. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewHeightKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  80. }
  81. fileprivate var subtitleLabelBottomConstraint: NSLayoutConstraint? {
  82. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleBottomKey) as? NSLayoutConstraint }
  83. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleBottomKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  84. }
  85. fileprivate var subtitleLabelWidthConstraint: NSLayoutConstraint? {
  86. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleWidthKey) as? NSLayoutConstraint }
  87. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleWidthKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  88. }
  89. fileprivate var subtitleContainerViewWidthConstraint: NSLayoutConstraint? {
  90. get { return objc_getAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewWidthKey) as? NSLayoutConstraint }
  91. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.SubtitleContainerViewWidthKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  92. }
  93. fileprivate var parsedPayload: NSDictionary? {
  94. get { return objc_getAssociatedObject(self, &AssociatedKeys.PayloadKey) as? NSDictionary }
  95. set (value) { objc_setAssociatedObject(self, &AssociatedKeys.PayloadKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
  96. }
  97. func setUpForSubtitle() {
  98. self.subtitleUrls.removeAll()
  99. if let url = CCUtility.getDirectoryProviderStorageOcId(metadata.ocId) {
  100. let enumerator = FileManager.default.enumerator(atPath: url)
  101. let filePaths = (enumerator?.allObjects as? [String])
  102. if let filePaths = filePaths {
  103. let txtFilePaths = (filePaths.filter { $0.contains(".srt") }).sorted {
  104. guard let str1LastChar = $0.dropLast(4).last, let str2LastChar = $1.dropLast(4).last else {
  105. return false
  106. }
  107. return str1LastChar < str2LastChar
  108. }
  109. for txtFilePath in txtFilePaths {
  110. let subtitleUrl = URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: txtFilePath))
  111. self.subtitleUrls.append(subtitleUrl)
  112. }
  113. }
  114. }
  115. let (all, existing) = NCManageDatabase.shared.getSubtitles(account: metadata.account, serverUrl: metadata.serverUrl, fileName: metadata.fileName)
  116. if !existing.isEmpty {
  117. for subtitle in existing {
  118. let subtitleUrl = URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(subtitle.ocId, fileNameView: subtitle.fileName))
  119. self.subtitleUrls.append(subtitleUrl)
  120. }
  121. }
  122. if all.count != existing.count {
  123. NCContentPresenter.shared.messageNotification("_info_", description: "_subtitle_not_dowloaded_", delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.info, errorCode: NCGlobal.shared.errorNoError)
  124. }
  125. self.setSubtitleToolbarIcon(subtitleUrls: subtitleUrls)
  126. self.hideSubtitle()
  127. }
  128. func setSubtitleToolbarIcon(subtitleUrls: [URL]) {
  129. if subtitleUrls.isEmpty {
  130. playerToolBar?.subtitleButton.isHidden = true
  131. } else {
  132. playerToolBar?.subtitleButton.isHidden = false
  133. }
  134. }
  135. func addSubtitlesTo(_ vc: UIViewController, _ playerToolBar: NCPlayerToolBar?) {
  136. addSubtitleLabel(vc, playerToolBar)
  137. NotificationCenter.default.addObserver(self, selector: #selector(deviceRotated(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
  138. }
  139. func loadText(filePath: URL, _ completion:@escaping (_ contents: String?) -> Void) {
  140. DispatchQueue.global(qos: .background).async {
  141. guard let data = try? Data(contentsOf: filePath),
  142. let encoding = NCUtility.shared.getEncondingDataType(data: data) else {
  143. return
  144. }
  145. if let decodedString = String(data: data, encoding: encoding) {
  146. completion(decodedString)
  147. } else {
  148. completion(nil)
  149. }
  150. }
  151. }
  152. func open(fileFromLocal url: URL) {
  153. subtitleLabel?.text = ""
  154. self.loadText(filePath: url) { contents in
  155. guard let contents = contents else {
  156. return
  157. }
  158. DispatchQueue.main.async {
  159. self.subtitleLabel?.text = ""
  160. self.show(subtitles: contents)
  161. }
  162. }
  163. }
  164. @objc public func hideSubtitle() {
  165. self.subtitleLabel?.isHidden = true
  166. self.subtitleContainerView?.isHidden = true
  167. self.currentSubtitle = nil
  168. }
  169. @objc public func showSubtitle(url: URL) {
  170. self.subtitleLabel?.isHidden = false
  171. self.subtitleContainerView?.isHidden = false
  172. self.currentSubtitle = url
  173. }
  174. private func show(subtitles string: String) {
  175. parsedPayload = try? NCSubtitles.parseSubRip(string)
  176. if let parsedPayload = parsedPayload {
  177. addPeriodicNotification(parsedPayload: parsedPayload)
  178. }
  179. }
  180. private func showByDictionary(dictionaryContent: NSMutableDictionary) {
  181. parsedPayload = dictionaryContent
  182. if let parsedPayload = parsedPayload {
  183. addPeriodicNotification(parsedPayload: parsedPayload)
  184. }
  185. }
  186. func addPeriodicNotification(parsedPayload: NSDictionary) {
  187. // Add periodic notifications
  188. let interval = CMTimeMake(value: 1, timescale: 60)
  189. self.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
  190. guard let strongSelf = self, let label = strongSelf.subtitleLabel, let containerView = strongSelf.subtitleContainerView else {
  191. return
  192. }
  193. DispatchQueue.main.async {
  194. label.text = NCSubtitles.searchSubtitles(strongSelf.parsedPayload, time.seconds)
  195. strongSelf.adjustViewWidth(containerView: containerView)
  196. strongSelf.adjustLabelHeight(label: label)
  197. }
  198. }
  199. }
  200. @objc private func deviceRotated(_ notification: Notification) {
  201. guard let label = self.subtitleLabel,
  202. let containerView = self.subtitleContainerView else { return }
  203. DispatchQueue.main.async {
  204. self.adjustViewWidth(containerView: containerView)
  205. self.adjustLabelHeight(label: label)
  206. self.adjustLabelBottom(label: label)
  207. containerView.layoutIfNeeded()
  208. label.layoutIfNeeded()
  209. }
  210. }
  211. private func adjustLabelHeight(label: UILabel) {
  212. let baseSize = CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude)
  213. let rect = label.sizeThatFits(baseSize)
  214. if label.text != nil {
  215. self.subtitleLabelHeightConstraint?.constant = rect.height + 5.0
  216. } else {
  217. self.subtitleLabelHeightConstraint?.constant = rect.height
  218. }
  219. }
  220. private func adjustLabelBottom(label: UILabel) {
  221. var bottomConstant: CGFloat = bottomConstantPortrait
  222. switch UIApplication.shared.statusBarOrientation {
  223. case .portrait:
  224. bottomConstant = bottomConstantLandscape
  225. case .landscapeLeft, .landscapeRight, .portraitUpsideDown:
  226. bottomConstant = bottomConstantPortrait
  227. default:
  228. ()
  229. }
  230. subtitleLabelBottomConstraint?.constant = bottomConstant
  231. }
  232. private func adjustViewWidth(containerView: UIView) {
  233. let widthConstant: CGFloat = UIScreen.main.bounds.width * widthProportion
  234. subtitleContainerViewWidthConstraint!.constant = widthConstant
  235. subtitleLabel?.preferredMaxLayoutWidth = (widthConstant - 20)
  236. }
  237. fileprivate func addSubtitleLabel(_ vc: UIViewController, _ playerToolBar: NCPlayerToolBar?) {
  238. guard subtitleLabel == nil,
  239. subtitleContainerView == nil else {
  240. return
  241. }
  242. subtitleContainerView = UIView()
  243. subtitleLabel = UILabel()
  244. subtitleContainerView?.translatesAutoresizingMaskIntoConstraints = false
  245. subtitleContainerView?.layer.cornerRadius = 5.0
  246. subtitleContainerView?.layer.masksToBounds = true
  247. subtitleContainerView?.layer.shouldRasterize = true
  248. subtitleContainerView?.layer.rasterizationScale = UIScreen.main.scale
  249. subtitleContainerView?.backgroundColor = UIColor.black.withAlphaComponent(0.35)
  250. subtitleLabel?.translatesAutoresizingMaskIntoConstraints = false
  251. subtitleLabel?.textAlignment = .center
  252. subtitleLabel?.numberOfLines = 0
  253. let fontSize = UIDevice.current.userInterfaceIdiom == .pad ? 38.0 : 20.0
  254. subtitleLabel?.font = UIFont.incosolataMedium(size: fontSize)
  255. subtitleLabel?.lineBreakMode = .byWordWrapping
  256. subtitleLabel?.textColor = .white
  257. subtitleLabel?.backgroundColor = .clear
  258. subtitleContainerView?.addSubview(subtitleLabel!)
  259. var isFound = false
  260. for v in vc.view.subviews where v is UIScrollView {
  261. if let scrollView = v as? UIScrollView {
  262. for subView in scrollView.subviews where subView is imageVideoContainerView {
  263. subView.addSubview(subtitleContainerView!)
  264. isFound = true
  265. break
  266. }
  267. }
  268. }
  269. if !isFound {
  270. vc.view.addSubview(subtitleContainerView!)
  271. }
  272. NSLayoutConstraint.activate([
  273. subtitleLabel!.centerXAnchor.constraint(equalTo: subtitleContainerView!.centerXAnchor),
  274. subtitleLabel!.centerYAnchor.constraint(equalTo: subtitleContainerView!.centerYAnchor)
  275. ])
  276. subtitleContainerViewHeightConstraint = NSLayoutConstraint(item: subtitleContainerView!, attribute: .height, relatedBy: .equal, toItem: subtitleLabel!, attribute: .height, multiplier: 1.0, constant: 0.0)
  277. vc.view?.addConstraint(subtitleContainerViewHeightConstraint!)
  278. var bottomConstant: CGFloat = bottomConstantPortrait
  279. switch UIApplication.shared.statusBarOrientation {
  280. case .portrait, .portraitUpsideDown:
  281. bottomConstant = bottomConstantLandscape
  282. case .landscapeLeft, .landscapeRight:
  283. bottomConstant = bottomConstantPortrait
  284. default:
  285. ()
  286. }
  287. let widthConstant: CGFloat = UIScreen.main.bounds.width * widthProportion
  288. NSLayoutConstraint.activate([
  289. subtitleContainerView!.centerXAnchor.constraint(equalTo: vc.view.centerXAnchor)
  290. ])
  291. subtitleContainerViewWidthConstraint = NSLayoutConstraint(item: subtitleContainerView!, attribute: .width, relatedBy: .lessThanOrEqual, toItem: nil,
  292. attribute: .width, multiplier: 1, constant: widthConstant)
  293. // setting default width == 0 because there is no text inside of the label
  294. subtitleLabelWidthConstraint = NSLayoutConstraint(item: subtitleLabel!, attribute: .width, relatedBy: .equal, toItem: subtitleContainerView,
  295. attribute: .width, multiplier: 1, constant: -20)
  296. subtitleLabelBottomConstraint = NSLayoutConstraint(item: subtitleContainerView!, attribute: .bottom, relatedBy: .equal, toItem: vc.view, attribute:
  297. .bottom, multiplier: 1, constant: bottomConstant)
  298. vc.view?.addConstraint(subtitleContainerViewWidthConstraint!)
  299. vc.view?.addConstraint(subtitleLabelWidthConstraint!)
  300. vc.view?.addConstraint(subtitleLabelBottomConstraint!)
  301. }
  302. internal func showAlertSubtitles() {
  303. let alert = UIAlertController(title: nil, message: NSLocalizedString("_subtitle_", comment: ""), preferredStyle: .actionSheet)
  304. for url in subtitleUrls {
  305. print("Play Subtitle at:\n\(url.path)")
  306. let videoUrlTitle = self.metadata.fileName.alphanumeric.dropLast(3)
  307. let subtitleUrlTitle = url.lastPathComponent.alphanumeric.dropLast(3)
  308. var titleSubtitle = String(subtitleUrlTitle.dropFirst(videoUrlTitle.count))
  309. if titleSubtitle.isEmpty {
  310. titleSubtitle = NSLocalizedString("_subtitle_", comment: "")
  311. }
  312. let action = UIAlertAction(title: titleSubtitle, style: .default, handler: { [self] _ in
  313. if NCUtilityFileSystem.shared.getFileSize(filePath: url.path) > 0 {
  314. self.open(fileFromLocal: url)
  315. if let viewController = viewController {
  316. self.addSubtitlesTo(viewController, self.playerToolBar)
  317. self.showSubtitle(url: url)
  318. }
  319. } else {
  320. let alertError = UIAlertController(title: NSLocalizedString("_error_", comment: ""), message: NSLocalizedString("_subtitle_not_found_", comment: ""), preferredStyle: .alert)
  321. alertError.addAction(UIKit.UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: nil))
  322. viewController?.present(alertError, animated: true, completion: nil)
  323. }
  324. })
  325. alert.addAction(action)
  326. if currentSubtitle == url {
  327. action.setValue(true, forKey: "checked")
  328. }
  329. }
  330. let disable = UIAlertAction(title: NSLocalizedString("_disable_", comment: ""), style: .default, handler: { _ in
  331. self.hideSubtitle()
  332. })
  333. alert.addAction(disable)
  334. if currentSubtitle == nil {
  335. disable.setValue(true, forKey: "checked")
  336. }
  337. alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: { _ in
  338. }))
  339. alert.popoverPresentationController?.sourceView = self.viewController?.view
  340. self.viewController?.present(alert, animated: true, completion: nil)
  341. }
  342. }