|
@@ -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!)
|
|
|
|
+ }
|
|
|
|
+}
|