123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- //
- // NCSubtitlePlayer.swift
- // Nextcloud
- //
- // Created by Federico Malagoni on 18/02/22.
- // Copyright © 2022 Federico Malagoni. All rights reserved.
- // Copyright © 2022 Marino Faggiana All rights reserved.
- //
- // Author Federico Malagoni <federico.malagoni@astrairidium.com>
- // 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 AVKit
- import NextcloudKit
- 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 (all, existing) = NCManageDatabase.shared.getSubtitles(account: metadata.account, serverUrl: metadata.serverUrl, fileName: metadata.fileName)
- if !existing.isEmpty {
- for subtitle in existing {
- let subtitleUrl = URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(subtitle.ocId, fileNameView: subtitle.fileName))
- self.subtitleUrls.append(subtitleUrl)
- }
- }
- if all.count != existing.count {
- let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_subtitle_not_dowloaded_")
- NCContentPresenter.shared.showInfo(error: error)
- }
- self.setSubtitleToolbarIcon(subtitleUrls: subtitleUrls)
- self.hideSubtitle()
- }
- func setSubtitleToolbarIcon(subtitleUrls: [URL]) {
- if subtitleUrls.isEmpty {
- playerToolBar?.subtitleButton.isHidden = true
- } else {
- playerToolBar?.subtitleButton.isHidden = false
- }
- }
- 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 url: URL) {
- subtitleLabel?.text = ""
- self.loadText(filePath: url) { 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
- self.currentSubtitle = nil
- }
- @objc public func showSubtitle(url: URL) {
- self.subtitleLabel?.isHidden = false
- self.subtitleContainerView?.isHidden = false
- self.currentSubtitle = url
- }
- 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!)
- }
- 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: "")
- }
- let action = UIAlertAction(title: titleSubtitle, style: .default, handler: { [self] _ in
- if NCUtilityFileSystem.shared.getFileSize(filePath: url.path) > 0 {
- self.open(fileFromLocal: url)
- if let viewController = viewController {
- self.addSubtitlesTo(viewController, self.playerToolBar)
- self.showSubtitle(url: url)
- }
- } 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)
- }
- })
- alert.addAction(action)
- if currentSubtitle == url {
- action.setValue(true, forKey: "checked")
- }
- }
- let disable = UIAlertAction(title: NSLocalizedString("_disable_", comment: ""), style: .default, handler: { _ in
- self.hideSubtitle()
- })
- alert.addAction(disable)
- if currentSubtitle == nil {
- disable.setValue(true, forKey: "checked")
- }
- alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: { _ in
- }))
- alert.popoverPresentationController?.sourceView = self.viewController?.view
- self.viewController?.present(alert, animated: true, completion: nil)
- }
- }
|