NCSubtitlePlayer.swift 15 KB

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