Эх сурвалжийг харах

Merge pull request #1910 from nextcloud/improvedVideo

Improved video
Marino Faggiana 3 жил өмнө
parent
commit
835cb5b49b
40 өөрчлөгдсөн 1707 нэмэгдсэн , 500 устгасан
  1. 2 1
      .swiftlint.yml
  2. 1 1
      Cartfile
  3. 1 1
      Cartfile.resolved
  4. 112 14
      Nextcloud.xcodeproj/project.pbxproj
  5. 2 1
      iOSClient/AppDelegate.swift
  6. 31 0
      iOSClient/Brand/iOSClient.plist
  7. 0 1
      iOSClient/Data/NCDatabase.swift
  8. 15 0
      iOSClient/Data/NCManageDatabase+Account.swift
  9. 15 3
      iOSClient/Data/NCManageDatabase+Activity.swift
  10. 35 1
      iOSClient/Data/NCManageDatabase+Metadata.swift
  11. 152 0
      iOSClient/Data/NCManageDatabase+Video.swift
  12. 0 127
      iOSClient/Data/NCManageDatabase.swift
  13. 21 0
      iOSClient/Extensions/String+Extensions.swift
  14. 37 0
      iOSClient/Extensions/UIDevice+Extensions.swift
  15. 31 0
      iOSClient/Extensions/UIFont+Extension.swift
  16. BIN
      iOSClient/Font/Inconsolata/Inconsolata-Black.ttf
  17. BIN
      iOSClient/Font/Inconsolata/Inconsolata-Bold.ttf
  18. BIN
      iOSClient/Font/Inconsolata/Inconsolata-ExtraBold.ttf
  19. BIN
      iOSClient/Font/Inconsolata/Inconsolata-ExtraLight.ttf
  20. BIN
      iOSClient/Font/Inconsolata/Inconsolata-Light.ttf
  21. BIN
      iOSClient/Font/Inconsolata/Inconsolata-Medium.ttf
  22. BIN
      iOSClient/Font/Inconsolata/Inconsolata-Regular.ttf
  23. BIN
      iOSClient/Font/Inconsolata/Inconsolata-SemiBold.ttf
  24. 12 0
      iOSClient/Images.xcassets/captions.bubble.imageset/Contents.json
  25. 1 0
      iOSClient/Images.xcassets/captions.bubble.imageset/subtitles-outline.svg
  26. 1 1
      iOSClient/NCGlobal.swift
  27. 3 1
      iOSClient/Settings/CCAdvanced.m
  28. 5 0
      iOSClient/Supporting Files/en.lproj/Localizable.strings
  29. 70 0
      iOSClient/Utility/NCUtility.swift
  30. 16 4
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCKTVHTTPCache.swift
  31. 222 90
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift
  32. 56 55
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift
  33. 178 0
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib
  34. 56 0
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift
  35. 94 0
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCSubtitlePlayer+PlayerSubtitleDelegate.swift
  36. 343 0
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCSubtitlePlayer.swift
  37. 150 0
      iOSClient/Viewer/NCViewerMedia/NCPlayer/NCSubtitle/NCSubtitles.swift
  38. 21 18
      iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift
  39. 0 165
      iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.storyboard
  40. 24 16
      iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift

+ 2 - 1
.swiftlint.yml

@@ -48,7 +48,8 @@ excluded:
   - iOSClient/Data/NCManageDatabase+Account.swift
   - iOSClient/Data/NCManageDatabase+Activity.swift
   - iOSClient/Data/NCManageDatabase.swift
-  - iOSClient/Data/NCManageDatabse+Metadata.swift
+  - iOSClient/Data/NCManageDatabase+Metadata.swift
+  - iOSClient/Data/NCManageDatabase+Video.swift
   - iOSClient/Diagnostics/NCCapabilitiesViewController.swift
   - iOSClient/EmptyView/NCEmptyDataSet.swift
   - iOSClient/Extensions/UIColor+Extensions.swift

+ 1 - 1
Cartfile

@@ -1,3 +1,3 @@
-github "https://github.com/marinofaggiana/KTVHTTPCache" "2.0.2"
+github "https://github.com/marinofaggiana/KTVHTTPCache" "2.0.5"
 github "https://github.com/marinofaggiana/TOPasscodeViewController" "master"
 github "krzyzanowskim/OpenSSL"

+ 1 - 1
Cartfile.resolved

@@ -1,3 +1,3 @@
 github "krzyzanowskim/OpenSSL" "1.1.1300"
-github "marinofaggiana/KTVHTTPCache" "2.0.2"
+github "marinofaggiana/KTVHTTPCache" "2.0.5"
 github "marinofaggiana/TOPasscodeViewController" "a1b9d1058b2648e636525fc368e220a0cfddb42a"

+ 112 - 14
Nextcloud.xcodeproj/project.pbxproj

@@ -37,10 +37,10 @@
 		AF4BF615275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; };
 		AF4BF616275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; };
 		AF4BF617275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; };
-		AF4BF61927562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabse+Metadata.swift */; };
-		AF4BF61A27562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabse+Metadata.swift */; };
-		AF4BF61B27562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabse+Metadata.swift */; };
-		AF4BF61C27562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabse+Metadata.swift */; };
+		AF4BF61927562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */; };
+		AF4BF61A27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */; };
+		AF4BF61B27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */; };
+		AF4BF61C27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */; };
 		AF4BF61E27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */; };
 		AF4BF61F27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */; };
 		AF4BF62027562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */; };
@@ -144,6 +144,7 @@
 		F72D404923D2082500A97FD0 /* NCViewerNextcloudText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */; };
 		F72D7EB7263B1207000B3DFC /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = F72D7EB6263B1207000B3DFC /* MarkdownKit */; };
 		F72DA9B425F53E4E00B87DB1 /* SwiftRichString in Frameworks */ = {isa = PBXBuildFile; productRef = F72DA9B325F53E4E00B87DB1 /* SwiftRichString */; };
+		F732D23327CF8AED000B0F1B /* NCPlayerToolBar.xib in Resources */ = {isa = PBXBuildFile; fileRef = F732D23227CF8AED000B0F1B /* NCPlayerToolBar.xib */; };
 		F733598125C1C188002ABA72 /* NCAskAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F733598025C1C188002ABA72 /* NCAskAuthorization.swift */; };
 		F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */; };
 		F7381EE1218218C9000B1560 /* NCOffline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7381EDA218218C9000B1560 /* NCOffline.swift */; };
@@ -366,6 +367,10 @@
 		F7E572FD278F146C00F8C99E /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86802642CF5400ED5349 /* OpenSSL.xcframework */; };
 		F7E572FE278F146C00F8C99E /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86802642CF5400ED5349 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		F7E57302278F14FF00F8C99E /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86802642CF5400ED5349 /* OpenSSL.xcframework */; };
+		F7E98C1627E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; };
+		F7E98C1727E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; };
+		F7E98C1827E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; };
+		F7E98C1927E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; };
 		F7EBCDCF277B81FF00A4EF67 /* UICKeyChainStore in Frameworks */ = {isa = PBXBuildFile; productRef = F7EBCDCE277B81FF00A4EF67 /* UICKeyChainStore */; };
 		F7EBCDD1277B820D00A4EF67 /* UICKeyChainStore in Frameworks */ = {isa = PBXBuildFile; productRef = F7EBCDD0277B820D00A4EF67 /* UICKeyChainStore */; };
 		F7EBCDD3277B821700A4EF67 /* UICKeyChainStore in Frameworks */ = {isa = PBXBuildFile; productRef = F7EBCDD2277B821700A4EF67 /* UICKeyChainStore */; };
@@ -386,6 +391,21 @@
 		F7EFC0C6256BC77700461AAD /* NCMoreUserCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7EFC0C5256BC77700461AAD /* NCMoreUserCell.xib */; };
 		F7EFC0CD256BF8DD00461AAD /* NCUserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */; };
 		F7F1E54C2492369A00E42386 /* NCMediaCommandView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7F1E54B2492369A00E42386 /* NCMediaCommandView.xib */; };
+		F7F4F0F727ECDBA4008676F9 /* NCSubtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F0F327ECDBA4008676F9 /* NCSubtitles.swift */; };
+		F7F4F0F827ECDBA4008676F9 /* NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F0F427ECDBA4008676F9 /* NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift */; };
+		F7F4F0F927ECDBA4008676F9 /* NCSubtitlePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F0F527ECDBA4008676F9 /* NCSubtitlePlayer.swift */; };
+		F7F4F0FA27ECDBA4008676F9 /* NCSubtitlePlayer+PlayerSubtitleDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F0F627ECDBA4008676F9 /* NCSubtitlePlayer+PlayerSubtitleDelegate.swift */; };
+		F7F4F10527ECDBDB008676F9 /* Inconsolata-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F0FD27ECDBDB008676F9 /* Inconsolata-SemiBold.ttf */; };
+		F7F4F10627ECDBDB008676F9 /* Inconsolata-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F0FE27ECDBDB008676F9 /* Inconsolata-Medium.ttf */; };
+		F7F4F10727ECDBDB008676F9 /* Inconsolata-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F0FF27ECDBDB008676F9 /* Inconsolata-Black.ttf */; };
+		F7F4F10827ECDBDB008676F9 /* Inconsolata-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F10027ECDBDB008676F9 /* Inconsolata-ExtraLight.ttf */; };
+		F7F4F10927ECDBDB008676F9 /* Inconsolata-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F10127ECDBDB008676F9 /* Inconsolata-Bold.ttf */; };
+		F7F4F10A27ECDBDB008676F9 /* Inconsolata-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F10227ECDBDB008676F9 /* Inconsolata-ExtraBold.ttf */; };
+		F7F4F10B27ECDBDB008676F9 /* Inconsolata-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F10327ECDBDB008676F9 /* Inconsolata-Light.ttf */; };
+		F7F4F10C27ECDBDB008676F9 /* Inconsolata-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F10427ECDBDB008676F9 /* Inconsolata-Regular.ttf */; };
+		F7F4F10E27ECDC3C008676F9 /* UIApplication+Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F10D27ECDC3C008676F9 /* UIApplication+Orientation.swift */; };
+		F7F4F11027ECDC4A008676F9 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F10F27ECDC4A008676F9 /* UIDevice+Extensions.swift */; };
+		F7F4F11227ECDC52008676F9 /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F4F11127ECDC52008676F9 /* UIFont+Extension.swift */; };
 		F7F878AE1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F878AD1FB9E3B900599E4F /* NCEndToEndMetadata.swift */; };
 		F7F878AF1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F878AD1FB9E3B900599E4F /* NCEndToEndMetadata.swift */; };
 		F7F9D1BB25397CE000D9BFF5 /* NCViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F9D1BA25397CE000D9BFF5 /* NCViewer.swift */; };
@@ -477,7 +497,7 @@
 		AF36077527BFB019001A243D /* ParallelWorkerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallelWorkerTest.swift; sourceTree = "<group>"; };
 		AF3FDCC12796ECC300710F60 /* NCTrash+CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCTrash+CollectionView.swift"; sourceTree = "<group>"; };
 		AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Account.swift"; sourceTree = "<group>"; };
-		AF4BF61827562A4B0081CEEF /* NCManageDatabse+Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabse+Metadata.swift"; sourceTree = "<group>"; };
+		AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Metadata.swift"; sourceTree = "<group>"; };
 		AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Activity.swift"; sourceTree = "<group>"; };
 		AF68326927BE65A90010BF0B /* NCMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMenuAction.swift; sourceTree = "<group>"; };
 		AF730AF927843E4C00B7520E /* NCShareExtension+NCDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+NCDelegate.swift"; sourceTree = "<group>"; };
@@ -613,6 +633,7 @@
 		F72E0B9C21AD60BC00898D7B /* WeScan.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WeScan.framework; path = Carthage/Build/iOS/WeScan.framework; sourceTree = "<group>"; };
 		F7320934201B812F008A0888 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
 		F732093B201B81E4008A0888 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		F732D23227CF8AED000B0F1B /* NCPlayerToolBar.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCPlayerToolBar.xib; sourceTree = "<group>"; };
 		F733598025C1C188002ABA72 /* NCAskAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAskAuthorization.swift; sourceTree = "<group>"; };
 		F733B65121997CC1001C1FFA /* TLPhotoPicker.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TLPhotoPicker.framework; path = Carthage/Build/iOS/TLPhotoPicker.framework; sourceTree = "<group>"; };
 		F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -885,6 +906,7 @@
 		F7E45E6D21E75BF200579249 /* ja-JP */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ja-JP"; path = "ja-JP.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		F7E4D9C322ED929B003675FD /* NCShareCommentsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCommentsCell.swift; sourceTree = "<group>"; };
 		F7E856182351D7BE009A3330 /* SwiftyXMLParser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyXMLParser.framework; path = Carthage/Build/iOS/SwiftyXMLParser.framework; sourceTree = "<group>"; };
+		F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Video.swift"; sourceTree = "<group>"; };
 		F7EDE508262DA9D600414FE6 /* NCSelectCommandViewSelect.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCSelectCommandViewSelect.xib; sourceTree = "<group>"; };
 		F7EDE513262DC2CD00414FE6 /* NCSelectCommandViewSelect+CreateFolder.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = "NCSelectCommandViewSelect+CreateFolder.xib"; sourceTree = "<group>"; };
 		F7EDE51A262DD0C400414FE6 /* NCSelectCommandViewCopyMove.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSelectCommandViewCopyMove.xib; sourceTree = "<group>"; };
@@ -893,6 +915,21 @@
 		F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserStatus.swift; sourceTree = "<group>"; };
 		F7F1E54B2492369A00E42386 /* NCMediaCommandView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCMediaCommandView.xib; sourceTree = "<group>"; };
 		F7F35B592578FB63003F5589 /* CollaboraOnlineWebViewKeyboardManager.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CollaboraOnlineWebViewKeyboardManager.framework; path = Carthage/Build/iOS/CollaboraOnlineWebViewKeyboardManager.framework; sourceTree = "<group>"; };
+		F7F4F0F327ECDBA4008676F9 /* NCSubtitles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSubtitles.swift; sourceTree = "<group>"; };
+		F7F4F0F427ECDBA4008676F9 /* NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift"; sourceTree = "<group>"; };
+		F7F4F0F527ECDBA4008676F9 /* NCSubtitlePlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSubtitlePlayer.swift; sourceTree = "<group>"; };
+		F7F4F0F627ECDBA4008676F9 /* NCSubtitlePlayer+PlayerSubtitleDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCSubtitlePlayer+PlayerSubtitleDelegate.swift"; sourceTree = "<group>"; };
+		F7F4F0FD27ECDBDB008676F9 /* Inconsolata-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-SemiBold.ttf"; sourceTree = "<group>"; };
+		F7F4F0FE27ECDBDB008676F9 /* Inconsolata-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Medium.ttf"; sourceTree = "<group>"; };
+		F7F4F0FF27ECDBDB008676F9 /* Inconsolata-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Black.ttf"; sourceTree = "<group>"; };
+		F7F4F10027ECDBDB008676F9 /* Inconsolata-ExtraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-ExtraLight.ttf"; sourceTree = "<group>"; };
+		F7F4F10127ECDBDB008676F9 /* Inconsolata-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Bold.ttf"; sourceTree = "<group>"; };
+		F7F4F10227ECDBDB008676F9 /* Inconsolata-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-ExtraBold.ttf"; sourceTree = "<group>"; };
+		F7F4F10327ECDBDB008676F9 /* Inconsolata-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Light.ttf"; sourceTree = "<group>"; };
+		F7F4F10427ECDBDB008676F9 /* Inconsolata-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Regular.ttf"; sourceTree = "<group>"; };
+		F7F4F10D27ECDC3C008676F9 /* UIApplication+Orientation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Orientation.swift"; sourceTree = "<group>"; };
+		F7F4F10F27ECDC4A008676F9 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
+		F7F4F11127ECDC52008676F9 /* UIFont+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; };
 		F7F67BB81A24D27800EE80DA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
 		F7F878AD1FB9E3B900599E4F /* NCEndToEndMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndMetadata.swift; sourceTree = "<group>"; };
 		F7F9D1BA25397CE000D9BFF5 /* NCViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewer.swift; sourceTree = "<group>"; };
@@ -1380,7 +1417,9 @@
 		F79EDA9E26B004980007D134 /* NCPlayer */ = {
 			isa = PBXGroup;
 			children = (
+				F7F4F0F227ECDBA4008676F9 /* NCSubtitle */,
 				F79EDAA126B004980007D134 /* NCPlayer.swift */,
+				F732D23227CF8AED000B0F1B /* NCPlayerToolBar.xib */,
 				F79EDA9F26B004980007D134 /* NCPlayerToolBar.swift */,
 				F716B75E26F09DF600D37EFC /* NCKTVHTTPCache.swift */,
 			);
@@ -1395,12 +1434,15 @@
 				F78071071EDAB65800EAFFF6 /* NSNotificationCenter+MainThread.h */,
 				F78071081EDAB65800EAFFF6 /* NSNotificationCenter+MainThread.m */,
 				F7A0D1342591FBC5008F8A13 /* String+Extensions.swift */,
-				F70CEF5523E9C7E50007035B /* UIColor+Extensions.swift */,
-				F79B645F26CA661600838ACA /* UIControl+Extensions.swift */,
 				AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */,
+				AFD3323F276A02C000F5AE02 /* UIApplication+Orientation.swift */,
+				F7F4F10D27ECDC3C008676F9 /* UIApplication+Orientation.swift */,
 				AF7E504D27A2D8FF00B5E4AF /* UIBarButton+Extension.swift */,
+				F70CEF5523E9C7E50007035B /* UIColor+Extensions.swift */,
+				F79B645F26CA661600838ACA /* UIControl+Extensions.swift */,
+				F7F4F10F27ECDC4A008676F9 /* UIDevice+Extensions.swift */,
+				F7F4F11127ECDC52008676F9 /* UIFont+Extension.swift */,
 				F713FEFE2472764000214AF6 /* UIImage+animatedGIF.h */,
-				AFD3323F276A02C000F5AE02 /* UIApplication+Orientation.swift */,
 				F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */,
 				F7B7504A2397D38E004E13EC /* UIImage+Extensions.swift */,
 			);
@@ -1477,7 +1519,8 @@
 				F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */,
 				AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */,
 				AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */,
-				AF4BF61827562A4B0081CEEF /* NCManageDatabse+Metadata.swift */,
+				AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */,
+				F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */,
 				F73D5E46246DE09200DF6467 /* NCElementsJSON.swift */,
 			);
 			path = Data;
@@ -1646,6 +1689,40 @@
 			path = UserStatus;
 			sourceTree = "<group>";
 		};
+		F7F4F0F227ECDBA4008676F9 /* NCSubtitle */ = {
+			isa = PBXGroup;
+			children = (
+				F7F4F0F327ECDBA4008676F9 /* NCSubtitles.swift */,
+				F7F4F0F427ECDBA4008676F9 /* NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift */,
+				F7F4F0F527ECDBA4008676F9 /* NCSubtitlePlayer.swift */,
+				F7F4F0F627ECDBA4008676F9 /* NCSubtitlePlayer+PlayerSubtitleDelegate.swift */,
+			);
+			path = NCSubtitle;
+			sourceTree = "<group>";
+		};
+		F7F4F0FB27ECDBDA008676F9 /* Font */ = {
+			isa = PBXGroup;
+			children = (
+				F7F4F0FC27ECDBDB008676F9 /* Inconsolata */,
+			);
+			path = Font;
+			sourceTree = "<group>";
+		};
+		F7F4F0FC27ECDBDB008676F9 /* Inconsolata */ = {
+			isa = PBXGroup;
+			children = (
+				F7F4F0FD27ECDBDB008676F9 /* Inconsolata-SemiBold.ttf */,
+				F7F4F0FE27ECDBDB008676F9 /* Inconsolata-Medium.ttf */,
+				F7F4F0FF27ECDBDB008676F9 /* Inconsolata-Black.ttf */,
+				F7F4F10027ECDBDB008676F9 /* Inconsolata-ExtraLight.ttf */,
+				F7F4F10127ECDBDB008676F9 /* Inconsolata-Bold.ttf */,
+				F7F4F10227ECDBDB008676F9 /* Inconsolata-ExtraBold.ttf */,
+				F7F4F10327ECDBDB008676F9 /* Inconsolata-Light.ttf */,
+				F7F4F10427ECDBDB008676F9 /* Inconsolata-Regular.ttf */,
+			);
+			path = Inconsolata;
+			sourceTree = "<group>";
+		};
 		F7F67B9F1A24D27800EE80DA = {
 			isa = PBXGroup;
 			children = (
@@ -1677,6 +1754,7 @@
 				F7BAAD951ED5A63D00B7EAD4 /* Data */,
 				F73FAEE224D2CA830090692E /* Diagnostics */,
 				F723986F253D867900257F49 /* EmptyView */,
+				F7A0D14E259229FA008F8A13 /* Extensions */,
 				F7A3214D1E9E2A070069AD1B /* Favorites */,
 				F7725A5D251F33BB00D125E0 /* Files */,
 				F7A80BC7252624C100C7CD01 /* FileViewInFolder */,
@@ -1700,7 +1778,6 @@
 				F7E9C41320F4CA870040CF18 /* Transfers */,
 				F78F74322163753B00C2ADAD /* Trash */,
 				F7EFC0CB256BF89300461AAD /* UserStatus */,
-				F7A0D14E259229FA008F8A13 /* Extensions */,
 				F7BFFA991A24D7BB0044ED85 /* Utility */,
 				F79630EC215526B60015EEA5 /* Viewer */,
 			);
@@ -1710,6 +1787,7 @@
 		F7F67BAB1A24D27800EE80DA /* Supporting Files */ = {
 			isa = PBXGroup;
 			children = (
+				F7F4F0FB27ECDBDA008676F9 /* Font */,
 				F72B60941A24F04E004EF66F /* Localizations */,
 				F7D154271E2392A300202FD9 /* Nextcloud-Bridging-Header.h */,
 			);
@@ -2175,9 +2253,12 @@
 				F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */,
 				F77444F622281649000D5EB0 /* NCGridMediaCell.xib in Resources */,
 				F78ACD4421903CF20088454D /* NCListCell.xib in Resources */,
+				F7F4F10727ECDBDB008676F9 /* Inconsolata-Black.ttf in Resources */,
 				F78ACD4621903D010088454D /* NCGridCell.xib in Resources */,
+				F7F4F10827ECDBDB008676F9 /* Inconsolata-ExtraLight.ttf in Resources */,
 				F72685E727C78E490019EF5E /* InfoPlist.strings in Resources */,
 				F769453C22E9CFFF000A798A /* NCShareUserCell.xib in Resources */,
+				F7F4F10927ECDBDB008676F9 /* Inconsolata-Bold.ttf in Resources */,
 				F7A80BCA252624C100C7CD01 /* NCFileViewInFolder.storyboard in Resources */,
 				F76D3CF52428D0C1005DFA87 /* NCViewerPDF.storyboard in Resources */,
 				F700222C1EC479840080073F /* Custom.xcassets in Resources */,
@@ -2193,12 +2274,15 @@
 				F7EFC0C6256BC77700461AAD /* NCMoreUserCell.xib in Resources */,
 				F702F2E725EE5C86008F8E80 /* NCAudioRecorderViewController.storyboard in Resources */,
 				F7632FBF21832F8700721B71 /* NCTrashSectionHeaderMenu.xib in Resources */,
+				F7F4F10B27ECDBDB008676F9 /* Inconsolata-Light.ttf in Resources */,
 				3704EB2A23D5A58400455C5B /* NCMenu.storyboard in Resources */,
 				F7D0F33E264144FC0097D4A3 /* Background.xcassets in Resources */,
 				F7F1E54C2492369A00E42386 /* NCMediaCommandView.xib in Resources */,
 				F710E8111EF95C9C00DC2427 /* ImagesIntro.xcassets in Resources */,
 				F76032A0252F0F8E0015A421 /* NCTransferCell.xib in Resources */,
+				F7F4F10527ECDBDB008676F9 /* Inconsolata-SemiBold.ttf in Resources */,
 				F74C0437253F1CDC009762AB /* NCShares.storyboard in Resources */,
+				F7F4F10C27ECDBDB008676F9 /* Inconsolata-Regular.ttf in Resources */,
 				F7B8B83025681C3400967775 /* GoogleService-Info.plist in Resources */,
 				F7381EE5218218C9000B1560 /* NCOffline.storyboard in Resources */,
 				F7E0CDCF265CE8610044854E /* NCUserStatus.storyboard in Resources */,
@@ -2214,10 +2298,12 @@
 				F787704F22E7019900F287A9 /* NCShareLinkCell.xib in Resources */,
 				F70753F72542A9C000972D44 /* NCViewerMediaPage.storyboard in Resources */,
 				F70A58C024D0545100DED00D /* NCCapabilitiesViewController.storyboard in Resources */,
+				F7F4F10627ECDBDB008676F9 /* Inconsolata-Medium.ttf in Resources */,
 				F749C10D23C4A5340027D966 /* NCIntro.storyboard in Resources */,
 				F7239877253D86D300257F49 /* NCEmptyView.xib in Resources */,
 				F747BA1F22354D2000971601 /* NCCreateFormUploadVoiceNote.storyboard in Resources */,
 				F7651A8A23A2A3F2001403D2 /* NCCreateFormUploadDocuments.storyboard in Resources */,
+				F7F4F10A27ECDBDB008676F9 /* Inconsolata-ExtraBold.ttf in Resources */,
 				F704B5E72430C06700632F5F /* NCCreateFormUploadConflictCell.xib in Resources */,
 				F79728D622F9A0B1003CACA7 /* NCShareUserFolderMenuView.xib in Resources */,
 				F7DFAA8A22E22EF100FC4527 /* NCShareLinkMenuView.xib in Resources */,
@@ -2227,6 +2313,7 @@
 				F704B5E32430AA6F00632F5F /* NCCreateFormUploadConflict.storyboard in Resources */,
 				F77B0F611D118A16002130FE /* Acknowledgements.rtf in Resources */,
 				F7EDE509262DA9D600414FE6 /* NCSelectCommandViewSelect.xib in Resources */,
+				F732D23327CF8AED000B0F1B /* NCPlayerToolBar.xib in Resources */,
 				F73D11FA253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard in Resources */,
 				F7EDE51B262DD0C400414FE6 /* NCSelectCommandViewCopyMove.xib in Resources */,
 				F73B422B2476764F00A30FD3 /* NCNotification.storyboard in Resources */,
@@ -2301,13 +2388,14 @@
 				F702F2D225EE5B5C008F8E80 /* NCGlobal.swift in Sources */,
 				F7707689263A896A00A1BA94 /* UIImage+Extensions.swift in Sources */,
 				2C1D5D7523E2DE3300334ABB /* NCDatabase.swift in Sources */,
+				F7E98C1927E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */,
 				2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */,
 				2C33C48223E2C475005F963B /* NotificationService.swift in Sources */,
 				AF4BF617275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */,
 				D575039F27146F93008DC9DC /* String+Extensions.swift in Sources */,
 				F73D5E4A246DE09200DF6467 /* NCElementsJSON.swift in Sources */,
 				F79B646326CA661600838ACA /* UIControl+Extensions.swift in Sources */,
-				AF4BF61C27562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */,
+				AF4BF61C27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */,
 				AF817EF4274BC781009ED85B /* NCUserBaseUrl.swift in Sources */,
 				F798F0EC2588060A000DAFFD /* UIColor+Extensions.swift in Sources */,
 				2CB7D1CA23E2EDCB00376EF9 /* NCPushNotificationEncryption.m in Sources */,
@@ -2340,9 +2428,10 @@
 				F70460532499095400BB98A7 /* NotificationCenter+MainThread.swift in Sources */,
 				F70BFC7520E0FA7D00C67599 /* NCUtility.swift in Sources */,
 				AF22B20C277C6F4D00DAB0CC /* NCShareCell.swift in Sources */,
+				F7E98C1727E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */,
 				F79B646126CA661600838ACA /* UIControl+Extensions.swift in Sources */,
 				F7EDE4CC262D7B6F00414FE6 /* NCEmptyDataSet.swift in Sources */,
-				AF4BF61A27562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */,
+				AF4BF61A27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */,
 				AF4BF615275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */,
 				F798F0E225880608000DAFFD /* UIColor+Extensions.swift in Sources */,
 				AF3FDCC32796F3FB00710F60 /* NCTrashListCell.swift in Sources */,
@@ -2392,8 +2481,9 @@
 				AF4BF616275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */,
 				F7434B3620E23FE000417916 /* NCManageDatabase.swift in Sources */,
 				F798F0E725880609000DAFFD /* UIColor+Extensions.swift in Sources */,
-				AF4BF61B27562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */,
+				AF4BF61B27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */,
 				F70460542499095400BB98A7 /* NotificationCenter+MainThread.swift in Sources */,
+				F7E98C1827E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */,
 				F785EEA42461A4A600B3F945 /* NCUtility.swift in Sources */,
 				F79B646226CA661600838ACA /* UIControl+Extensions.swift in Sources */,
 				AF817EF3274BC781009ED85B /* NCUserBaseUrl.swift in Sources */,
@@ -2422,6 +2512,7 @@
 				370D26AF248A3D7A00121797 /* NCCellProtocol.swift in Sources */,
 				F77B0DF51D118A16002130FE /* CCUtility.m in Sources */,
 				F70D87D025EE6E58008CBBBD /* NCRenameFile.swift in Sources */,
+				F7F4F0F727ECDBA4008676F9 /* NCSubtitles.swift in Sources */,
 				F790110E21415BF600D7B136 /* NCViewerRichdocument.swift in Sources */,
 				F70B866E2642A21300ED5349 /* NCBackgroundImageColor.swift in Sources */,
 				F78ACD4021903CC20088454D /* NCGridCell.swift in Sources */,
@@ -2446,8 +2537,9 @@
 				F70753F12542A9A200972D44 /* NCViewerMedia.swift in Sources */,
 				F7A80BCB252624C100C7CD01 /* NCFileViewInFolder.swift in Sources */,
 				F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */,
+				F7F4F0FA27ECDBA4008676F9 /* NCSubtitlePlayer+PlayerSubtitleDelegate.swift in Sources */,
 				F77910AB25DD53C700CEDB9E /* NCSettingsBundleHelper.swift in Sources */,
-				AF4BF61927562A4B0081CEEF /* NCManageDatabse+Metadata.swift in Sources */,
+				AF4BF61927562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */,
 				F78A18B623CDD07D00F681F3 /* NCViewerRichWorkspaceWebView.swift in Sources */,
 				F716B75F26F09DF600D37EFC /* NCKTVHTTPCache.swift in Sources */,
 				AF36077127BFA4E8001A243D /* ParallelWorker.swift in Sources */,
@@ -2482,12 +2574,15 @@
 				F78F74362163781100C2ADAD /* NCTrash.swift in Sources */,
 				AF817EF1274BC781009ED85B /* NCUserBaseUrl.swift in Sources */,
 				AF2D7C7C2742556F00ADF566 /* NCShareLinkCell.swift in Sources */,
+				F7F4F0F927ECDBA4008676F9 /* NCSubtitlePlayer.swift in Sources */,
 				F7651A8B23A2A3F2001403D2 /* NCCreateFormUploadDocuments.swift in Sources */,
 				F74AF3A4247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */,
 				F7417DB3216CE925007D05F5 /* NCTrashSectionHeaderFooter.swift in Sources */,
 				F7239871253D86B600257F49 /* NCEmptyDataSet.swift in Sources */,
 				8491B1CD273BBA82001C8C5B /* UIViewController+Menu.swift in Sources */,
 				F702F2F725EE5CED008F8E80 /* NCLogin.swift in Sources */,
+				F7E98C1627E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */,
+				F7F4F11227ECDC52008676F9 /* UIFont+Extension.swift in Sources */,
 				F7F878AE1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */,
 				F7DBC37C23325E02001A85BA /* NCAppConfigView.swift in Sources */,
 				3781B9B023DB2B7E006B4B1D /* AppDelegate+Menu.swift in Sources */,
@@ -2538,9 +2633,11 @@
 				F702F2E625EE5C86008F8E80 /* NCAudioRecorderViewController.swift in Sources */,
 				D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */,
 				F745B253222D88AE00346520 /* NCLoginQRCode.swift in Sources */,
+				F7F4F10E27ECDC3C008676F9 /* UIApplication+Orientation.swift in Sources */,
 				F7CBC31C24F78E79004D3812 /* NCSortMenu.swift in Sources */,
 				F769454822E9F20D000A798A /* NCShareNetworking.swift in Sources */,
 				F7C9555521F0C5470024296E /* NCActivity.swift in Sources */,
+				F7F4F0F827ECDBA4008676F9 /* NCPlayerToolBar+SubtitlePlayerToolBarDelegate.swift in Sources */,
 				F7725A60251F33BB00D125E0 /* NCFiles.swift in Sources */,
 				F704B5E52430AA8000632F5F /* NCCreateFormUploadConflict.swift in Sources */,
 				F765608F23BF813600765969 /* NCContentPresenter.swift in Sources */,
@@ -2549,6 +2646,7 @@
 				F7C7B489245EBA4100D93E60 /* NCViewerQuickLook.swift in Sources */,
 				F758B45E212C569D00515F55 /* NCScanCell.swift in Sources */,
 				F7581D1A25EFDA61004DC699 /* NCLoginWeb+Menu.swift in Sources */,
+				F7F4F11027ECDC4A008676F9 /* UIDevice+Extensions.swift in Sources */,
 				F77B0ED11D118A16002130FE /* Acknowledgements.m in Sources */,
 				F70D8D8124A4A9BF000A5756 /* NCNetworkingProcessUpload.swift in Sources */,
 				F7D96FCC246ED7E200536D73 /* NCNetworkingCheckRemoteUser.swift in Sources */,

+ 2 - 1
iOSClient/AppDelegate.swift

@@ -362,7 +362,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         NCAutoUpload.shared.initAutoUpload(viewController: nil) { _ in
             DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                 NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterUpdateBadgeNumber)
-                NCCommunicationCommon.shared.writeLog("Completition handler refresh task with %lu uploads [Auto upload]")
+                NCCommunicationCommon.shared.writeLog("Completition handler refresh task with [Auto upload]")
                 task.setTaskCompleted(success: true)
             }
         }
@@ -606,6 +606,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         if serverVersionMajor > 0 {
             NCCommunicationCommon.shared.setup(nextcloudVersion: serverVersionMajor)
         }
+        NCKTVHTTPCache.shared.restartProxy(user: user, password: password)
     }
 
     @objc func deleteAccount(_ account: String, wipe: Bool) {

+ 31 - 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>
@@ -132,6 +142,27 @@
 				</array>
 			</dict>
 		</dict>
+		<dict>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>public.text</string>
+			</array>
+			<key>UTTypeDescription</key>
+			<string>SRT Subtitle Format</string>
+			<key>UTTypeIconFiles</key>
+			<array/>
+			<key>UTTypeIdentifier</key>
+			<string>com.company.srt</string>
+			<key>UTTypeReferenceURL</key>
+			<string></string>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>srt</string>
+				</array>
+			</dict>
+		</dict>
 	</array>
 </dict>
 </plist>

+ 0 - 1
iOSClient/Data/NCDatabase.swift

@@ -539,7 +539,6 @@ class tableVideo: Object {
     @objc dynamic var codecNameAudio: String?
     @objc dynamic var codecAudioChannelLayout: String?
     @objc dynamic var codecAudioLanguage: String?
-    @objc dynamic var codecSubtitleLanguage: String?
     @objc dynamic var codecMaxCompatibility: Bool = false
     @objc dynamic var codecQuality: String?
 

+ 15 - 0
iOSClient/Data/NCManageDatabase+Account.swift

@@ -5,6 +5,21 @@
 //  Created by Henrik Storch on 30.11.21.
 //  Copyright © 2021 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 RealmSwift

+ 15 - 3
iOSClient/Data/NCManageDatabase+Activity.swift

@@ -5,6 +5,21 @@
 //  Created by Henrik Storch on 30.11.21.
 //  Copyright © 2021 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 RealmSwift
@@ -13,9 +28,6 @@ import SwiftyJSON
 
 extension NCManageDatabase {
     
-    // MARK: -
-    // MARK: Table Activity
-
     @objc func addActivity(_ activities: [NCCommunicationActivity], account: String) {
 
         let realm = try! Realm()

+ 35 - 1
iOSClient/Data/NCManageDatabse+Metadata.swift → iOSClient/Data/NCManageDatabase+Metadata.swift

@@ -1,10 +1,25 @@
 //
-//  NCManageDatabse+Metadata.swift
+//  NCManageDatabase+Metadata.swift
 //  Nextcloud
 //
 //  Created by Henrik Storch on 30.11.21.
 //  Copyright © 2021 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 RealmSwift
@@ -792,4 +807,23 @@ extension NCManageDatabase {
         }
         return getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameView == %@", account, serverUrl, fileNameConflict))
     }
+
+    func getSubtitles(account: String, serverUrl: String, fileName: String, exists: Bool) -> [tableMetadata] {
+
+        let realm = try! Realm()
+        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)
+        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) })
+        }
+    }
 }

+ 152 - 0
iOSClient/Data/NCManageDatabase+Video.swift

@@ -0,0 +1,152 @@
+//
+//  NCManageDatabase+Video.swift
+//  Nextcloud
+//
+//  Created by Marino Faggiana on 15/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 RealmSwift
+import NCCommunication
+
+extension NCManageDatabase {
+
+    func addVideoTime(metadata: tableMetadata, time: CMTime?, durationTime: CMTime?) {
+
+        if metadata.livePhoto { return }
+        let realm = try! Realm()
+
+        do {
+            try realm.safeWrite {
+                if let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first {
+
+                    if let durationTime = durationTime {
+                        result.duration = durationTime.convertScale(1000, method: .default).value
+                    }
+                    if let time = time {
+                        result.time = time.convertScale(1000, method: .default).value
+                    }
+                    realm.add(result, update: .all)
+
+                } else {
+
+                    let addObject = tableVideo()
+
+                    addObject.account = metadata.account
+                    if let durationTime = durationTime {
+                        addObject.duration = durationTime.convertScale(1000, method: .default).value
+                    }
+                    addObject.ocId = metadata.ocId
+                    if let time = time {
+                        addObject.time = time.convertScale(1000, method: .default).value
+                    }
+                    realm.add(addObject, update: .all)
+                }
+            }
+        } catch let error {
+            NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
+        }
+    }
+
+    func addVideoCodec(metadata: tableMetadata, codecNameVideo: String?, codecNameAudio: String?, codecAudioChannelLayout: String?, codecAudioLanguage: String?, codecMaxCompatibility: Bool, codecQuality: String?) {
+
+        let realm = try! Realm()
+
+        do {
+            try realm.safeWrite {
+                if let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first {
+                    if let codecNameVideo = codecNameVideo { result.codecNameVideo = codecNameVideo }
+                    if let codecNameAudio = codecNameAudio { result.codecNameAudio = codecNameAudio }
+                    if let codecAudioChannelLayout = codecAudioChannelLayout { result.codecAudioChannelLayout = codecAudioChannelLayout }
+                    if let codecAudioLanguage = codecAudioLanguage { result.codecAudioLanguage = codecAudioLanguage }
+                    result.codecMaxCompatibility = codecMaxCompatibility
+                    if let codecQuality = codecQuality { result.codecQuality = codecQuality }
+                    realm.add(result, update: .all)
+                } else {
+                    let addObject = tableVideo()
+                    addObject.account = metadata.account
+                    addObject.ocId = metadata.ocId
+                    addObject.codecNameVideo = codecNameVideo
+                    addObject.codecNameAudio = codecNameAudio
+                    addObject.codecAudioChannelLayout = codecAudioChannelLayout
+                    addObject.codecAudioLanguage = codecAudioLanguage
+                    addObject.codecMaxCompatibility = codecMaxCompatibility
+                    addObject.codecQuality = codecQuality
+                    realm.add(addObject, update: .all)
+                }
+            }
+        } catch let error {
+            NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
+        }
+    }
+
+    func getVideo(metadata: tableMetadata?) -> tableVideo? {
+        guard let metadata = metadata else { return nil }
+
+        let realm = try! Realm()
+        guard let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first else {
+            return nil
+        }
+
+        return tableVideo.init(value: result)
+    }
+
+    func getVideoDurationTime(metadata: tableMetadata?) -> CMTime? {
+        guard let metadata = metadata else { return nil }
+
+        if metadata.livePhoto { return nil }
+        let realm = try! Realm()
+
+        guard let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first else {
+            return nil
+        }
+
+        if result.duration == 0 { return nil }
+        let duration = CMTimeMake(value: result.duration, timescale: 1000)
+        return duration
+    }
+
+    func getVideoTime(metadata: tableMetadata) -> CMTime? {
+
+        if metadata.livePhoto { return nil }
+        let realm = try! Realm()
+
+        guard let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first else {
+            return nil
+        }
+
+        if result.time == 0 { return nil }
+        let time = CMTimeMake(value: result.time, timescale: 1000)
+        return time
+    }
+
+    func deleteVideo(metadata: tableMetadata) {
+
+        let realm = try! Realm()
+
+        do {
+            try realm.safeWrite {
+                let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId)
+                realm.delete(result)
+            }
+        } catch let error {
+            NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
+        }
+    }
+}

+ 0 - 127
iOSClient/Data/NCManageDatabase.swift

@@ -1682,133 +1682,6 @@ class NCManageDatabase: NSObject {
             NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
         }
     }
-    // MARK: -
-    // MARK: Table Video
-
-    func addVideoTime(metadata: tableMetadata, time: CMTime?, durationTime: CMTime?) {
-
-        if metadata.livePhoto { return }
-        let realm = try! Realm()
-
-        do {
-            try realm.safeWrite {
-                if let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first {
-
-                    if let durationTime = durationTime {
-                        result.duration = durationTime.convertScale(1000, method: .default).value
-                    }
-                    if let time = time {
-                        result.time = time.convertScale(1000, method: .default).value
-                    }
-                    realm.add(result, update: .all)
-
-                } else {
-
-                    let addObject = tableVideo()
-
-                    addObject.account = metadata.account
-                    if let durationTime = durationTime {
-                        addObject.duration = durationTime.convertScale(1000, method: .default).value
-                    }
-                    addObject.ocId = metadata.ocId
-                    if let time = time {
-                        addObject.time = time.convertScale(1000, method: .default).value
-                    }
-                    realm.add(addObject, update: .all)
-                }
-            }
-        } catch let error {
-            NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
-        }
-    }
-    
-    func addVideoCodec(metadata: tableMetadata, codecNameVideo: String?, codecNameAudio: String?, codecAudioChannelLayout: String?, codecAudioLanguage: String?, codecSubtitleLanguage: String?, codecMaxCompatibility: Bool, codecQuality: String?) {
-
-        let realm = try! Realm()
-
-        do {
-            try realm.safeWrite {
-                if let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first {
-                    if let codecNameVideo = codecNameVideo { result.codecNameVideo = codecNameVideo }
-                    if let codecNameAudio = codecNameAudio { result.codecNameAudio = codecNameAudio }
-                    if let codecAudioChannelLayout = codecAudioChannelLayout { result.codecAudioChannelLayout = codecAudioChannelLayout }
-                    if let codecAudioLanguage = codecAudioLanguage { result.codecAudioLanguage = codecAudioLanguage }
-                    if let codecSubtitleLanguage = codecSubtitleLanguage { result.codecSubtitleLanguage = codecSubtitleLanguage }
-                    result.codecMaxCompatibility = codecMaxCompatibility
-                    if let codecQuality = codecQuality { result.codecQuality = codecQuality }
-                    realm.add(result, update: .all)
-                } else {
-                    let addObject = tableVideo()
-                    addObject.account = metadata.account
-                    addObject.ocId = metadata.ocId
-                    addObject.codecNameVideo = codecNameVideo
-                    addObject.codecNameAudio = codecNameAudio
-                    addObject.codecAudioChannelLayout = codecAudioChannelLayout
-                    addObject.codecAudioLanguage = codecAudioLanguage
-                    addObject.codecSubtitleLanguage = codecSubtitleLanguage
-                    addObject.codecMaxCompatibility = codecMaxCompatibility
-                    addObject.codecQuality = codecQuality
-                    realm.add(addObject, update: .all)
-                }
-            }
-        } catch let error {
-            NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
-        }
-    }
-    
-    func getVideo(metadata: tableMetadata?) -> tableVideo? {
-        guard let metadata = metadata else { return nil }
-        
-        let realm = try! Realm()
-        guard let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first else {
-            return nil
-        }
-        
-        return tableVideo.init(value: result)
-    }
-
-    func getVideoDurationTime(metadata: tableMetadata?) -> CMTime? {
-        guard let metadata = metadata else { return nil }
-
-        if metadata.livePhoto { return nil }
-        let realm = try! Realm()
-
-        guard let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first else {
-            return nil
-        }
-
-        if result.duration == 0 { return nil }
-        let duration = CMTimeMake(value: result.duration, timescale: 1000)
-        return duration
-    }
-
-    func getVideoTime(metadata: tableMetadata) -> CMTime? {
-
-        if metadata.livePhoto { return nil }
-        let realm = try! Realm()
-
-        guard let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId).first else {
-            return nil
-        }
-
-        if result.time == 0 { return nil }
-        let time = CMTimeMake(value: result.time, timescale: 1000)
-        return time
-    }
-
-    func deleteVideo(metadata: tableMetadata) {
-
-        let realm = try! Realm()
-
-        do {
-            try realm.safeWrite {
-                let result = realm.objects(tableVideo.self).filter("account == %@ AND ocId == %@", metadata.account, metadata.ocId)
-                realm.delete(result)
-            }
-        } catch let error {
-            NCCommunicationCommon.shared.writeLog("Could not write to database: \(error)")
-        }
-    }
 }
 
 // MARK: -

+ 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


+ 12 - 0
iOSClient/Images.xcassets/captions.bubble.imageset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "subtitles-outline.svg",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 1 - 0
iOSClient/Images.xcassets/captions.bubble.imageset/subtitles-outline.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M20,4A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4H20M20,18V6H4V18H20M6,10H8V12H6V10M6,14H14V16H6V14M16,14H18V16H16V14M10,10H18V12H10V10Z" /></svg>

+ 1 - 1
iOSClient/NCGlobal.swift

@@ -112,7 +112,7 @@ class NCGlobal: NSObject {
     // Database Realm
     //
     let databaseDefault                             = "nextcloud.realm"
-    let databaseSchemaVersion: UInt64               = 216
+    let databaseSchemaVersion: UInt64               = 217
 
     // Intro selector
     //

+ 3 - 1
iOSClient/Settings/CCAdvanced.m

@@ -415,9 +415,11 @@
     
     [CCUtility removeGroupDirectoryProviderStorage];
     [CCUtility removeGroupLibraryDirectory];
-    
+
     [CCUtility removeDocumentsDirectory];
     [CCUtility removeTemporaryDirectory];
+
+    [[NCKTVHTTPCache shared] deleteAllCache];
     
     [CCUtility createDirectoryStandard];
 

+ 5 - 0
iOSClient/Supporting Files/en.lproj/Localizable.strings

@@ -830,7 +830,9 @@
 "_certificates_"            = "Certificates";
 "_privacy_screen_"          = "Splash screen when app inactive";
 "_saving_"                  = "Saving …";
+"_video_not_streamed_"      = "The server does not allow video streaming, do you want to download it?";
 // Video
+"_select_trace_"            = "Select the trace";
 "_video_processing_"        = "Video processing";
 "_video_being_processed_"   = "Video being processed …";
 "_downloading_"             = "Downloading";
@@ -841,4 +843,7 @@
 "_stay_app_foreground_"     = "Keep the app in the foreground …";
 "_conversion_available_"    = "The conversion is always available on menu";
 "_video_format_not_recognized_"     = "This video needs to be processed to be played, do you want to do it now?";
+"_video_must_download_"             = "This video needs to be downloaded and processed to be played, do you want to do it now?";
 "_conversion_max_compatibility_"    = "Max compatibility, the conversion can take much longer";
+"_video_tap_for_close_"     = "A slight pressure to close the processing";
+"_subtitle_not_found_"      = "Subtitle not found";

+ 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: -

+ 16 - 4
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCKTVHTTPCache.swift

@@ -31,17 +31,17 @@ class NCKTVHTTPCache: NSObject {
         return instance
     }()
 
-    func getVideoURL(metadata: tableMetadata) -> URL? {
+    func getVideoURL(metadata: tableMetadata) -> (url: URL?, isProxy: Bool) {
 
         if CCUtility.fileProviderStorageExists(metadata) {
 
-            return URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView))
+            return (URL(fileURLWithPath: CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)), false)
 
         } else {
 
-            guard let stringURL = (metadata.serverUrl + "/" + metadata.fileName).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil }
+            guard let stringURL = (metadata.serverUrl + "/" + metadata.fileName).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return (nil, false) }
 
-            return NCKTVHTTPCache.shared.getProxyURL(stringURL: stringURL)
+            return (NCKTVHTTPCache.shared.getProxyURL(stringURL: stringURL), true)
         }
     }
 
@@ -92,6 +92,11 @@ class NCKTVHTTPCache: NSObject {
         KTVHTTPCache.cacheDelete(with: videoURL)
     }
 
+    @objc func deleteAllCache() {
+
+        KTVHTTPCache.cacheDeleteAllCaches()
+    }
+
     func saveCache(metadata: tableMetadata) {
 
         if !CCUtility.fileProviderStorageExists(metadata) {
@@ -109,6 +114,13 @@ class NCKTVHTTPCache: NSObject {
         }
     }
 
+    func getDownloadStatusCode(metadata: tableMetadata) -> Int {
+
+        guard let stringURL = (metadata.serverUrl + "/" + metadata.fileName).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return 0 }
+        let url = URL(string: stringURL)
+        return KTVHTTPCache.downloadStatusCode(url)
+    }
+
     private func setupHTTPCache() {
 
         KTVHTTPCache.cacheSetMaxCacheLength(NCGlobal.shared.maxHTTPCache)

+ 222 - 90
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift

@@ -26,29 +26,45 @@ import NCCommunication
 import UIKit
 import AVFoundation
 import MediaPlayer
+import JGProgressHUD
+import Alamofire
 
 class NCPlayer: NSObject {
-   
+
     internal let appDelegate = UIApplication.shared.delegate as! AppDelegate
     internal var url: URL
-    internal var playerToolBar: NCPlayerToolBar?
-    internal var viewController: UIViewController
-
-    private var imageVideoContainer: imageVideoContainerView
-    private var detailView: NCViewerMediaDetailView?
+    internal weak var playerToolBar: NCPlayerToolBar?
+    internal weak var viewController: UIViewController?
+    internal var autoPlay: Bool
+    internal var isProxy: Bool
+    internal var isStartPlayer: Bool
+
+    private weak var imageVideoContainer: imageVideoContainerView?
+    private weak var detailView: NCViewerMediaDetailView?
     private var observerAVPlayerItemDidPlayToEndTime: Any?
     private var observerAVPlayertTime: Any?
+    private var observerAVPlayertStatus: Any?
 
     public var player: AVPlayer?
     public var durationTime: CMTime = .zero
     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, imageVideoContainer: imageVideoContainerView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, detailView: NCViewerMediaDetailView?, viewController: UIViewController) {
-        
+    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
+        self.isStartPlayer = false
         self.imageVideoContainer = imageVideoContainer
         self.playerToolBar = playerToolBar
         self.metadata = metadata
@@ -56,7 +72,7 @@ class NCPlayer: NSObject {
         self.viewController = viewController
 
         super.init()
-        
+
         do {
             try AVAudioSession.sharedInstance().setCategory(.playback)
             try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
@@ -64,51 +80,44 @@ class NCPlayer: NSObject {
         } catch {
             print(error)
         }
-        
-        #if MFFFLIB
+    }
+
+    deinit {
+
+        print("deinit NCPlayer with ocId \(metadata.ocId)")
+        deactivateObserver()
+    }
+
+    func openAVPlayer() {
+
+#if MFFFLIB
+        MFFF.shared.setDelegate = self
+        MFFF.shared.dismissMessage()
+        NotificationCenter.default.addObserver(self, selector: #selector(convertVideoDidFinish(_:)), name: NSNotification.Name(rawValue: self.metadata.ocId), object: nil)
+
         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
-              
-        openAVPlayer() { status, error in
-            
-            switch status {
-            case .loaded:
-                if autoPlay {
-                    self.player?.play()
-                }
-                break
-            case .failed:
-                #if MFFFLIB
-                self.convertVideo(error: error)
-                #else
-                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:
-                break
-            default:
-                break
-            }
+#endif
+
+        // Check already started
+        if isStartPlayer {
+            activateObserver()
+            return
         }
-    }
-    
-    internal func openAVPlayer(completion: @escaping (_ status: AVKeyValueStatus, _ error: NSError?)->()) {
-        
+
+        playerToolBar?.show()
+        setUpForSubtitle()
+        isSubtitleShowed = false
+
         print("Play URL: \(self.url)")
         player = AVPlayer(url: self.url)
         playerToolBar?.setMetadata(self.metadata)
-        
+
         if metadata.livePhoto {
             player?.isMuted = false
         } else if metadata.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
@@ -120,70 +129,131 @@ class NCPlayer: NSObject {
             }
         }
 
+        observerAVPlayertStatus = player?.currentItem?.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil)
+    }
+
+    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        switch keyPath {
+        case "status":
+            if let playerItem = self.player?.currentItem,
+                let object = object as? AVPlayerItem,
+                playerItem === object{
+                if (playerItem.status == .readyToPlay || playerItem.status == .failed) {
+                    print("player ready")
+                    self.startPlayer()
+                } else {
+                    print("player not ready")
+                }
+            }
+            break
+        default:
+            break
+        }
+    }
+
+    func startPlayer() {
+
         player?.currentItem?.asset.loadValuesAsynchronously(forKeys: ["playable"], completionHandler: {
-            
+
             var error: NSError? = nil
             let status = self.player?.currentItem?.asset.statusOfValue(forKey: "playable", error: &error) ?? .unknown
-            
+
             DispatchQueue.main.async {
-                if status == .loaded {
+
+                switch status {
+                case .loaded:
+
                     self.durationTime = self.player?.currentItem?.asset.duration ?? .zero
                     NCManageDatabase.shared.addVideoTime(metadata: self.metadata, time: nil, durationTime: self.durationTime)
 
-                    self.activateObserver(playerToolBar: self.playerToolBar)
-                    
                     self.videoLayer = AVPlayerLayer(player: self.player)
-                    self.videoLayer!.frame = self.imageVideoContainer.bounds
+                    self.videoLayer!.frame = self.imageVideoContainer?.bounds ?? .zero
                     self.videoLayer!.videoGravity = .resizeAspect
-                    
+
                     if self.metadata.classFile == NCCommunicationCommon.typeClassFile.video.rawValue {
-                    
-                        self.imageVideoContainer.layer.addSublayer(self.videoLayer!)
-                        self.imageVideoContainer.playerLayer = self.videoLayer
-                        self.imageVideoContainer.metadata = self.metadata
-                        self.imageVideoContainer.image = self.imageVideoContainer.image?.image(alpha: 0)
+                        self.imageVideoContainer?.layer.addSublayer(self.videoLayer!)
+                        self.imageVideoContainer?.playerLayer = self.videoLayer
+                        self.imageVideoContainer?.metadata = self.metadata
+                        self.imageVideoContainer?.image = self.imageVideoContainer?.image?.image(alpha: 0)
                     }
-                    
+
                     self.playerToolBar?.setBarPlayer(ncplayer: self)
                     self.generatorImagePreview()
                     if !(self.detailView?.isShow() ?? false) {
                         NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterShowPlayerToolBar, userInfo: ["ocId":self.metadata.ocId, "enableTimerAutoHide": false])
                     }
+                    self.activateObserver()
+                    if self.autoPlay {
+                        self.player?.play()
+                    }
+                    self.isStartPlayer = true
+                    break
+
+                case .failed:
+
+                    self.playerToolBar?.hide()
+                    if self.isProxy && NCKTVHTTPCache.shared.getDownloadStatusCode(metadata: self.metadata) == 200 {
+                        let alertController = UIAlertController(title: NSLocalizedString("_error_", value: "Error", comment: ""), message: NSLocalizedString("_video_not_streamed_", comment: ""), preferredStyle: .alert)
+                        alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", value: "Yes", comment: ""), style: .default, handler: { _ in
+                            self.downloadVideo()
+                        }))
+                        alertController.addAction(UIAlertAction(title: NSLocalizedString("_no_", value: "No", comment: ""), style: .default, handler: { _ in }))
+                        self.viewController?.present(alertController, animated: true)
+                    } else {
+#if MFFFLIB
+                        if error?.code == AVError.Code.fileFormatNotRecognized.rawValue {
+                            self.convertVideo(withAlert: true)
+                            break
+                        }
+#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)
+                        }
+                    }
+                    break
+
+                case .cancelled:
+                    break
+
+                default:
+                    break
                 }
-                
-                completion(status, error)
             }
         })
     }
 
-    deinit {
-        print("deinit NCPlayer")
-
-        deactivateObserver()
-    }
+    func activateObserver() {
 
-    func activateObserver(playerToolBar: NCPlayerToolBar?) {
+        // At end go back to start & show toolbar
+        observerAVPlayerItemDidPlayToEndTime = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: .main) {  [weak self] notification in
 
-        self.playerToolBar = playerToolBar
+            guard let self = self else {
+                return
+            }
 
-        // 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()
             }
         }
 
         // Evey 1 second update toolbar
-        observerAVPlayertTime = player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: .main, using: { _ in
+        observerAVPlayertTime = player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: .main, using: { [weak self] _ in
+
+            guard let self = self else {
+                return
+            }
+
             if self.player?.currentItem?.status == .readyToPlay {
                 self.playerToolBar?.updateToolBar()
             }
@@ -192,33 +262,44 @@ 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)
+
+        if let player = self.player {
+            NotificationCenter.default.addObserver(self, selector: #selector(playerStalled), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: player.currentItem)
+        }
     }
 
     func deactivateObserver() {
 
+        print("deactivating Observer")
+
         if isPlay() {
             playerPause()
         }
 
-        self.playerToolBar = nil
-
         if let observerAVPlayerItemDidPlayToEndTime = self.observerAVPlayerItemDidPlayToEndTime {
             NotificationCenter.default.removeObserver(observerAVPlayerItemDidPlayToEndTime)
         }
-        self.observerAVPlayerItemDidPlayToEndTime = nil
+        observerAVPlayerItemDidPlayToEndTime = nil
 
-        if  let observerAVPlayertTime = self.observerAVPlayertTime {
+        if let observerAVPlayertTime = self.observerAVPlayertTime {
             player?.removeTimeObserver(observerAVPlayertTime)
         }
-        self.observerAVPlayertTime = nil
+        observerAVPlayertTime = nil
+
+        if observerAVPlayertStatus != nil {
+            player?.currentItem?.removeObserver(self, forKeyPath: "status")
+        }
+        observerAVPlayertStatus = nil
 
         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.AVPlayerItemPlaybackStalled, 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)
     }
@@ -226,7 +307,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()
@@ -238,24 +319,30 @@ class NCPlayer: NSObject {
 
         playerToolBar?.updateToolBar()
     }
-    
+
     // MARK: -
 
     func isPlay() -> Bool {
 
         if player?.rate == 1 { return true } else { return false }
     }
-    
+
+    @objc func playerStalled() {
+
+        print("current player \(String(describing: player)) stalled.\nCalling playerPlay()")
+        playerPlay()
+    }
+
     @objc func playerPlay() {
-                
+
         player?.play()
-        self.playerToolBar?.updateToolBar()
+        playerToolBar?.updateToolBar()
     }
-    
+
     @objc func playerPause() {
-        
+
         player?.pause()
-        self.playerToolBar?.updateToolBar()
+        playerToolBar?.updateToolBar()
 
         if let playerToolBar = self.playerToolBar, playerToolBar.isPictureInPictureActive() {
             playerToolBar.pictureInPictureController?.stopPictureInPicture()
@@ -265,7 +352,7 @@ class NCPlayer: NSObject {
     func videoSeek(time: CMTime) {
 
         player?.seek(to: time)
-        self.saveTime(time)
+        saveTime(time)
     }
 
     func saveTime(_ time: CMTime) {
@@ -314,9 +401,54 @@ class NCPlayer: NSObject {
                     try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath), options: .atomic)
                 }
             } catch let error as NSError {
+                print("GeneratorImagePreview localized error:")
                 print(error.localizedDescription)
             }
         }
     }
-}
 
+    internal func downloadVideo(requiredConvert: Bool = false) {
+
+        guard let view = appDelegate.window?.rootViewController?.view else { return }
+        let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
+        let fileNameLocalPath = CCUtility.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)!
+        let hud = JGProgressHUD()
+        var downloadRequest: DownloadRequest?
+
+        hud.indicatorView = JGProgressHUDRingIndicatorView()
+        if let indicatorView = hud.indicatorView as? JGProgressHUDRingIndicatorView {
+            indicatorView.ringWidth = 1.5
+        }
+        hud.show(in: view)
+        hud.textLabel.text = NSLocalizedString(metadata.fileNameView, comment: "")
+        hud.detailTextLabel.text = NSLocalizedString("_tap_to_cancel_", comment: "")
+        hud.tapOnHUDViewBlock = { hud in
+            downloadRequest?.cancel()
+        }
+
+        NCCommunication.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath) { request in
+            downloadRequest = request
+        } taskHandler: { task in
+            // task
+        } progressHandler: { progress in
+            hud.progress = Float(progress.fractionCompleted)
+        } completionHandler: { _, _, _, _, _, error, _, _ in
+            if error == nil {
+                NCManageDatabase.shared.addLocalFile(metadata: self.metadata)
+                let urlVideo = NCKTVHTTPCache.shared.getVideoURL(metadata: self.metadata)
+                if let url = urlVideo.url {
+                    self.url = url
+                    self.isProxy = urlVideo.isProxy
+                    if requiredConvert {
+                        #if MFFFLIB
+                        self.convertVideo(withAlert: false)
+                        #endif
+                    } else {
+                        self.openAVPlayer()
+                    }
+                }
+            }
+            hud.dismiss()
+        }
+    }
+}

+ 56 - 55
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift

@@ -30,8 +30,10 @@ import MediaPlayer
 
 class NCPlayerToolBar: UIView {
 
-    @IBOutlet weak var playerTopToolBarView: UIView!
+    @IBOutlet weak var playerTopToolBarView: UIStackView!
+    @IBOutlet weak var playerToolBarView: UIView!
     @IBOutlet weak var pipButton: UIButton!
+    @IBOutlet weak var subtitleButton: UIButton!
     @IBOutlet weak var muteButton: UIButton!
     @IBOutlet weak var playButton: UIButton!
     @IBOutlet weak var forwardButton: UIButton!
@@ -39,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
@@ -60,27 +62,21 @@ class NCPlayerToolBar: UIView {
     override func awakeFromNib() {
         super.awakeFromNib()
 
-        // for disable gesture of UIPageViewController
-        let panRecognizer = UIPanGestureRecognizer(target: self, action: nil)
-        addGestureRecognizer(panRecognizer)
-        let singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:)))
-        addGestureRecognizer(singleTapGestureRecognizer)
-
-        self.layer.cornerRadius = 15
-        self.layer.masksToBounds = true
-
         let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
         blurEffectView.frame = self.bounds
         blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-        self.insertSubview(blurEffectView, at: 0)
-
+        playerToolBarView.insertSubview(blurEffectView, at: 0)
         playerTopToolBarView.layer.cornerRadius = 10
         playerTopToolBarView.layer.masksToBounds = true
+        playerTopToolBarView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapTopToolBarWith(gestureRecognizer:))))
 
         let blurEffectTopToolBarView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
         blurEffectTopToolBarView.frame = playerTopToolBarView.bounds
         blurEffectTopToolBarView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
         playerTopToolBarView.insertSubview(blurEffectTopToolBarView, at: 0)
+        playerToolBarView.layer.cornerRadius = 10
+        playerToolBarView.layer.masksToBounds = true
+        playerToolBarView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapToolBarWith(gestureRecognizer:))))
 
         pipButton.setImage(NCUtility.shared.loadImage(named: "pip.enter", color: .lightGray), for: .normal)
         pipButton.isEnabled = false
@@ -88,6 +84,10 @@ 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
@@ -100,9 +100,17 @@ class NCPlayerToolBar: UIView {
         labelLeftTime.text = NCUtility.shared.stringFromTime(.zero)
         labelLeftTime.textColor = .lightGray
 
+        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)
+
+        if #available(iOS 13.0, *) {
+            playButton.setImage(NCUtility.shared.loadImage(named: "play.fill", color: .lightGray, symbolConfiguration: UIImage.SymbolConfiguration(pointSize: 30)), for: .normal)
+        } else {
+            playButton.setImage(NCUtility.shared.loadImage(named: "play.fill", color: .lightGray, size: 30), for: .normal)
+        }
         playButton.isEnabled = false
+
+        forwardButton.setImage(NCUtility.shared.loadImage(named: "goforward.10", color: .lightGray), for: .normal)
         forwardButton.isEnabled = false
 
         NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil)
@@ -121,12 +129,12 @@ class NCPlayerToolBar: UIView {
     }
 
     // MARK: -
-    
+
     func setMetadata(_ metadata: tableMetadata) {
-        
+
         self.metadata = metadata
     }
-    
+
     func setBarPlayer(ncplayer: NCPlayer) {
 
         self.ncplayer = ncplayer
@@ -143,7 +151,7 @@ class NCPlayerToolBar: UIView {
     }
 
     public func updateToolBar() {
-        
+
         guard let ncplayer = self.ncplayer else { return }
 
         // MUTE
@@ -209,7 +217,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 {
@@ -271,19 +279,17 @@ class NCPlayerToolBar: UIView {
             return
         }
         #endif
-        
+
         timerAutoHide?.invalidate()
         if enableTimerAutoHide {
             startTimerAutoHide()
         }
         if !self.isHidden { return }
-        
+
         UIView.animate(withDuration: 0.3, animations: {
             self.alpha = 1
-            self.playerTopToolBarView.alpha = 1
         }, completion: { (_: Bool) in
             self.isHidden = false
-            self.playerTopToolBarView.isHidden = false
         })
 
         updateToolBar()
@@ -298,10 +304,8 @@ class NCPlayerToolBar: UIView {
 
         UIView.animate(withDuration: 0.3, animations: {
             self.alpha = 0
-            self.playerTopToolBarView.alpha = 0
         }, completion: { (_: Bool) in
             self.isHidden = true
-            self.playerTopToolBarView.isHidden = true
         })
     }
 
@@ -369,7 +373,7 @@ class NCPlayerToolBar: UIView {
 
         timerAutoHide?.invalidate()
     }
-    
+
     // MARK: - Event / Gesture
 
     @objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
@@ -403,19 +407,11 @@ class NCPlayerToolBar: UIView {
 
     // MARK: - Action
 
-    @objc func didSingleTapWith(gestureRecognizer: UITapGestureRecognizer) {
-        // nothing
-    }
+    @objc func tapTopToolBarWith(gestureRecognizer: UITapGestureRecognizer) { }
 
-    @IBAction func buttonPlayerToolBarTouchInside(_ sender: UIButton) {
-        // nothing
-    }
+    @objc func tapToolBarWith(gestureRecognizer: UITapGestureRecognizer) { }
 
-    @IBAction func buttonPlayerTopToolBarTouchInside(_ sender: UIButton) {
-        // nothing
-    }
-
-    @IBAction func playerPause(_ sender: Any) {
+    @IBAction func tapPlayerPause(_ sender: Any) {
 
         if ncplayer?.player?.timeControlStatus == .playing {
             ncplayer?.playerPause()
@@ -429,6 +425,7 @@ class NCPlayerToolBar: UIView {
             if let reason = ncplayer?.player?.reasonForWaitingToPlay {
                 switch reason {
                 case .evaluatingBufferingRate:
+                    self.ncplayer?.player?.playImmediately(atRate: 1)
                     print("reasonForWaitingToPlay.evaluatingBufferingRate")
                 case .toMinimizeStalls:
                     print("reasonForWaitingToPlay.toMinimizeStalls")
@@ -441,7 +438,7 @@ class NCPlayerToolBar: UIView {
         }
     }
 
-    @IBAction func setMute(_ sender: Any) {
+    @IBAction func tapMute(_ sender: Any) {
 
         let mute = CCUtility.getAudioMute()
 
@@ -451,7 +448,7 @@ class NCPlayerToolBar: UIView {
         reStartTimerAutoHide()
     }
 
-    @IBAction func setPip(_ sender: Any) {
+    @IBAction func tapPip(_ sender: Any) {
 
         guard let videoLayer = ncplayer?.videoLayer else { return }
 
@@ -472,32 +469,36 @@ class NCPlayerToolBar: UIView {
         }
     }
 
-    @IBAction func forwardButtonSec(_ sender: Any) {
+    @IBAction func tapForward(_ sender: Any) {
 
         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 backButtonSec(_ sender: Any) {
+    @IBAction func tapBack(_ sender: Any) {
 
         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

+ 178 - 0
iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib

@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
+        <capability name="Image references" minToolsVersion="12.0"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="NCPlayerToolBar" customModule="Nextcloud" customModuleProvider="target">
+            <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <stackView opaque="NO" contentMode="scaleToFill" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="XfW-XC-eMf" userLabel="Player Top Tool Bar">
+                    <rect key="frame" x="299" y="54" width="105" height="35"/>
+                    <subviews>
+                        <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="a3d-ja-utA">
+                            <rect key="frame" x="5" y="5" width="25" height="25"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="25" id="TIW-zu-jSX"/>
+                                <constraint firstAttribute="width" constant="25" id="jR3-bv-VnZ"/>
+                            </constraints>
+                            <state key="normal" image="pip.enter" catalog="system"/>
+                            <connections>
+                                <action selector="tapPip:" destination="iN0-l3-epB" eventType="touchUpInside" id="fB5-g1-OnB"/>
+                            </connections>
+                        </button>
+                        <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Mqe-Hd-USv">
+                            <rect key="frame" x="40" y="5" width="25" height="25"/>
+                            <constraints>
+                                <constraint firstAttribute="width" constant="25" id="HQs-6R-hb9"/>
+                                <constraint firstAttribute="height" constant="25" id="s43-Qc-Shn"/>
+                            </constraints>
+                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                            <state key="normal" image="captions.bubble" catalog="system"/>
+                            <connections>
+                                <action selector="tapSubtitle:" destination="iN0-l3-epB" eventType="touchUpInside" id="Cxi-6E-bKD"/>
+                            </connections>
+                        </button>
+                        <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Fml-c2-FMY">
+                            <rect key="frame" x="75" y="5" width="25" height="25"/>
+                            <constraints>
+                                <constraint firstAttribute="width" constant="25" id="S8g-UR-4zh"/>
+                                <constraint firstAttribute="height" constant="25" id="zjo-O1-SI2"/>
+                            </constraints>
+                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                            <state key="normal" image="audioOn"/>
+                            <connections>
+                                <action selector="tapMute:" destination="iN0-l3-epB" eventType="touchUpInside" id="FWr-cn-Pgw"/>
+                            </connections>
+                        </button>
+                    </subviews>
+                    <directionalEdgeInsets key="directionalLayoutMargins" top="5" leading="5" bottom="5" trailing="5"/>
+                </stackView>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="85m-50-8yp">
+                    <rect key="frame" x="10" y="794" width="394" height="58"/>
+                    <subviews>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ixi-yR-HDH" userLabel="Container play">
+                            <rect key="frame" x="0.0" y="0.0" width="118" height="58"/>
+                            <subviews>
+                                <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uP7-aY-x4n">
+                                    <rect key="frame" x="3.5" y="9" width="40" height="40"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="40" id="0jv-Tl-Nch"/>
+                                        <constraint firstAttribute="height" constant="40" id="jnZ-cX-iJY"/>
+                                    </constraints>
+                                    <state key="normal" image="gobackward.10" catalog="system"/>
+                                    <connections>
+                                        <action selector="tapBack:" destination="iN0-l3-epB" eventType="touchUpInside" id="q3g-BH-iUc"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hx9-d5-yiD">
+                                    <rect key="frame" x="39" y="9" width="40" height="40"/>
+                                    <constraints>
+                                        <constraint firstAttribute="width" constant="40" id="Ime-ag-2Hm"/>
+                                        <constraint firstAttribute="height" constant="40" id="snM-fJ-0LV"/>
+                                    </constraints>
+                                    <state key="normal">
+                                        <imageReference key="image" image="play.fill" catalog="system" symbolScale="default"/>
+                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="default"/>
+                                    </state>
+                                    <connections>
+                                        <action selector="tapPlayerPause:" destination="iN0-l3-epB" eventType="touchUpInside" id="XHf-om-3g9"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bGn-IC-3V1">
+                                    <rect key="frame" x="74.5" y="9" width="40" height="40"/>
+                                    <constraints>
+                                        <constraint firstAttribute="height" constant="40" id="VDT-no-B6f"/>
+                                        <constraint firstAttribute="width" constant="40" id="eA5-wy-Sqb"/>
+                                    </constraints>
+                                    <state key="normal" image="goforward.10" catalog="system"/>
+                                    <connections>
+                                        <action selector="tapForward:" destination="iN0-l3-epB" eventType="touchUpInside" id="45z-IH-Fyr"/>
+                                    </connections>
+                                </button>
+                            </subviews>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <constraints>
+                                <constraint firstItem="hx9-d5-yiD" firstAttribute="centerY" secondItem="ixi-yR-HDH" secondAttribute="centerY" id="Brn-9w-mrJ"/>
+                                <constraint firstItem="bGn-IC-3V1" firstAttribute="centerY" secondItem="ixi-yR-HDH" secondAttribute="centerY" id="JBD-51-iYm"/>
+                                <constraint firstItem="uP7-aY-x4n" firstAttribute="centerY" secondItem="ixi-yR-HDH" secondAttribute="centerY" id="MPU-Hg-zQa"/>
+                                <constraint firstItem="hx9-d5-yiD" firstAttribute="centerX" secondItem="ixi-yR-HDH" secondAttribute="centerX" id="Vyv-jg-Cwu"/>
+                                <constraint firstItem="uP7-aY-x4n" firstAttribute="centerX" secondItem="ixi-yR-HDH" secondAttribute="centerX" multiplier="0.4" id="bwk-Bx-qVb"/>
+                                <constraint firstItem="bGn-IC-3V1" firstAttribute="centerX" secondItem="ixi-yR-HDH" secondAttribute="centerX" multiplier="1.6" id="wJP-ph-5c5"/>
+                            </constraints>
+                        </view>
+                        <slider opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="MY0-FC-j88">
+                            <rect key="frame" x="121" y="14" width="265" height="31"/>
+                        </slider>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="99:99:99" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="svM-TQ-AyQ">
+                            <rect key="frame" x="338.5" y="44" width="45.5" height="12"/>
+                            <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                            <nil key="textColor"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OHB-2J-Gqb">
+                            <rect key="frame" x="123" y="44" width="45" height="12"/>
+                            <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                            <nil key="textColor"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                    </subviews>
+                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <constraints>
+                        <constraint firstItem="ixi-yR-HDH" firstAttribute="centerY" secondItem="85m-50-8yp" secondAttribute="centerY" id="5sz-fh-GbG"/>
+                        <constraint firstItem="OHB-2J-Gqb" firstAttribute="leading" secondItem="MY0-FC-j88" secondAttribute="leading" id="6n7-If-xdS"/>
+                        <constraint firstItem="ixi-yR-HDH" firstAttribute="height" secondItem="85m-50-8yp" secondAttribute="height" id="8xM-7l-FXN"/>
+                        <constraint firstAttribute="height" constant="58" id="9Uv-dV-to4"/>
+                        <constraint firstItem="svM-TQ-AyQ" firstAttribute="top" secondItem="MY0-FC-j88" secondAttribute="bottom" id="Aan-ac-nr9"/>
+                        <constraint firstItem="MY0-FC-j88" firstAttribute="leading" secondItem="ixi-yR-HDH" secondAttribute="trailing" constant="5" id="BAz-4C-w3A"/>
+                        <constraint firstAttribute="trailing" secondItem="svM-TQ-AyQ" secondAttribute="trailing" constant="10" id="JXl-DO-x6b"/>
+                        <constraint firstItem="ixi-yR-HDH" firstAttribute="leading" secondItem="85m-50-8yp" secondAttribute="leading" id="Kh2-nU-bMU"/>
+                        <constraint firstItem="ixi-yR-HDH" firstAttribute="width" secondItem="85m-50-8yp" secondAttribute="width" multiplier="0.3" id="gXG-4n-Ee3"/>
+                        <constraint firstAttribute="trailing" secondItem="MY0-FC-j88" secondAttribute="trailing" constant="10" id="hBO-J2-Kdo"/>
+                        <constraint firstItem="OHB-2J-Gqb" firstAttribute="top" secondItem="MY0-FC-j88" secondAttribute="bottom" id="jdb-Vq-WoF"/>
+                        <constraint firstItem="MY0-FC-j88" firstAttribute="centerY" secondItem="85m-50-8yp" secondAttribute="centerY" id="lG8-DN-rTE"/>
+                    </constraints>
+                </view>
+            </subviews>
+            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
+            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <constraints>
+                <constraint firstItem="85m-50-8yp" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="10" id="5H2-Gg-PEb"/>
+                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="85m-50-8yp" secondAttribute="trailing" constant="10" id="BXT-Qo-qFl"/>
+                <constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="85m-50-8yp" secondAttribute="bottom" constant="10" id="N7Q-PF-7lb"/>
+                <constraint firstItem="XfW-XC-eMf" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="10" id="fq3-6h-kkx"/>
+                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="XfW-XC-eMf" secondAttribute="trailing" constant="10" id="uYG-Ai-CGv"/>
+            </constraints>
+            <connections>
+                <outlet property="backButton" destination="uP7-aY-x4n" id="SyC-qV-IMq"/>
+                <outlet property="forwardButton" destination="bGn-IC-3V1" id="0OZ-f2-eWU"/>
+                <outlet property="labelCurrentTime" destination="OHB-2J-Gqb" id="pFy-CJ-x2A"/>
+                <outlet property="labelLeftTime" destination="svM-TQ-AyQ" id="UDV-Lh-12z"/>
+                <outlet property="muteButton" destination="Fml-c2-FMY" id="Fo1-Ep-ZPz"/>
+                <outlet property="pipButton" destination="a3d-ja-utA" id="9nt-RD-IXd"/>
+                <outlet property="playButton" destination="hx9-d5-yiD" id="Enk-Ge-2Yx"/>
+                <outlet property="playbackSlider" destination="MY0-FC-j88" id="bVe-Kc-80k"/>
+                <outlet property="playerToolBarView" destination="85m-50-8yp" id="eZK-p1-v65"/>
+                <outlet property="playerTopToolBarView" destination="XfW-XC-eMf" id="Qdp-IW-YhT"/>
+                <outlet property="subtitleButton" destination="Mqe-Hd-USv" id="ZHN-bf-WT5"/>
+            </connections>
+            <point key="canvasLocation" x="137.68115942028987" y="152.67857142857142"/>
+        </view>
+    </objects>
+    <resources>
+        <image name="audioOn" width="28" height="28"/>
+        <image name="captions.bubble" catalog="system" width="128" height="110"/>
+        <image name="gobackward.10" catalog="system" width="121" height="128"/>
+        <image name="goforward.10" catalog="system" width="121" height="128"/>
+        <image name="pip.enter" catalog="system" width="128" height="96"/>
+        <image name="play.fill" catalog="system" width="116" height="128"/>
+    </resources>
+</document>

+ 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
+    }
+}

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

@@ -0,0 +1,94 @@
+//
+//  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 {
+
+                    self.open(fileFromLocal: url)
+                    if let viewController = viewController {
+                        self.addSubtitlesTo(viewController, self.playerToolBar)
+                        self.isSubtitleShowed = true
+                        self.showSubtitle()
+                    }
+
+                } 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) {
+
+        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)
+    }
+}

+ 21 - 18
iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift

@@ -36,12 +36,12 @@ class NCViewerMedia: UIViewController {
     @IBOutlet weak var statusViewImage: UIImageView!
     @IBOutlet weak var statusLabel: UILabel!
     @IBOutlet weak var detailView: NCViewerMediaDetailView!
-    @IBOutlet weak var playerToolBar: NCPlayerToolBar!
 
     private var _autoPlay: Bool = false
 
     let appDelegate = UIApplication.shared.delegate as! AppDelegate
     weak var viewerMediaPage: NCViewerMediaPage?
+    var playerToolBar: NCPlayerToolBar?
     var ncplayer: NCPlayer?
     var image: UIImage?
     var metadata: tableMetadata = tableMetadata()
@@ -92,9 +92,26 @@ class NCViewerMedia: UIViewController {
             statusViewImage.image = nil
             statusLabel.text = ""
         }
+        
+        if metadata.classFile == NCCommunicationCommon.typeClassFile.video.rawValue || metadata.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
 
-        playerToolBar.viewerMediaPage = viewerMediaPage
+            playerToolBar = Bundle.main.loadNibNamed("NCPlayerToolBar", owner: self, options: nil)?.first as? NCPlayerToolBar
+            if let playerToolBar = playerToolBar {
+                view.addSubview(playerToolBar)
+                playerToolBar.translatesAutoresizingMaskIntoConstraints = false
+                playerToolBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
+                playerToolBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
+                playerToolBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
+                playerToolBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
+                playerToolBar.viewerMediaPage = viewerMediaPage
+            }
 
+            let urlVideo = NCKTVHTTPCache.shared.getVideoURL(metadata: metadata)
+            if let url = urlVideo.url {
+                self.ncplayer = NCPlayer.init(url: url, autoPlay: self.autoPlay, isProxy: urlVideo.isProxy, imageVideoContainer: self.imageVideoContainer, playerToolBar: self.playerToolBar, metadata: self.metadata, detailView: self.detailView, viewController: self)
+            }
+        }
+        
         detailViewTopConstraint.constant = 0
         detailView.hide()
 
@@ -145,25 +162,11 @@ class NCViewerMedia: UIViewController {
 
         if metadata.classFile == NCCommunicationCommon.typeClassFile.video.rawValue || metadata.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
 
-            NCKTVHTTPCache.shared.restartProxy(user: appDelegate.user, password: appDelegate.password)
-
-            if ncplayer == nil, let url = NCKTVHTTPCache.shared.getVideoURL(metadata: metadata) {
-                self.ncplayer = NCPlayer.init(url: url, autoPlay: self.autoPlay, imageVideoContainer: self.imageVideoContainer, playerToolBar: self.playerToolBar, metadata: self.metadata, detailView: self.detailView, viewController: self)
-            } else {
-                self.ncplayer?.activateObserver(playerToolBar: self.playerToolBar)
-                if detailView.isShow() == false && ncplayer?.isPlay() == false {
-                    NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterShowPlayerToolBar, userInfo: ["ocId": metadata.ocId, "enableTimerAutoHide": false])
-                }
-            }
-
             if let ncplayer = self.ncplayer {
+                ncplayer.openAVPlayer()
                 self.viewerMediaPage?.updateCommandCenter(ncplayer: ncplayer, metadata: self.metadata)
             }
             
-            #if MFFFLIB
-            MFFF.shared.setDelegate = self.ncplayer
-            #endif
-            
         } else if metadata.classFile == NCCommunicationCommon.typeClassFile.image.rawValue {
 
             viewerMediaPage?.clearCommandCenter()
@@ -476,7 +479,7 @@ extension NCViewerMedia {
             }
 
             self.scrollView.pinchGestureRecognizer?.isEnabled = false
-            self.playerToolBar.hide()
+            self.playerToolBar?.hide()
         }
     }
 

+ 0 - 165
iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.storyboard

@@ -4,7 +4,6 @@
     <dependencies>
         <deployment identifier="iOS"/>
         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
-        <capability name="Image references" minToolsVersion="12.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -246,159 +245,6 @@
                                             <outlet property="sizeValue" destination="XLb-0a-du9" id="9jm-Ku-sgt"/>
                                         </connections>
                                     </view>
-                                    <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dgJ-dQ-lSp" userLabel="Player Top Tool Bar">
-                                        <rect key="frame" x="299" y="10" width="100" height="45"/>
-                                        <subviews>
-                                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9aW-Pg-83x" userLabel="Button Player Top Tool Bar">
-                                                <rect key="frame" x="0.0" y="0.0" width="100" height="45"/>
-                                                <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
-                                                <connections>
-                                                    <action selector="buttonPlayerTopToolBarTouchInside:" destination="sBp-t2-eFh" eventType="touchUpInside" id="Afs-Nt-FaQ"/>
-                                                </connections>
-                                            </button>
-                                            <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8AB-hx-yqN" userLabel="Mute Button">
-                                                <rect key="frame" x="60" y="10" width="25" height="25"/>
-                                                <constraints>
-                                                    <constraint firstAttribute="width" constant="25" id="6yU-23-dkA"/>
-                                                    <constraint firstAttribute="height" constant="25" id="hBk-Ku-BAd"/>
-                                                </constraints>
-                                                <state key="normal" image="audioOn"/>
-                                                <connections>
-                                                    <action selector="setMute:" destination="sBp-t2-eFh" eventType="touchUpInside" id="cVl-BA-mPJ"/>
-                                                </connections>
-                                            </button>
-                                            <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NwE-zQ-Y5D" userLabel="Mute Button">
-                                                <rect key="frame" x="15" y="10" width="25" height="25"/>
-                                                <constraints>
-                                                    <constraint firstAttribute="width" constant="25" id="CaQ-c0-MES"/>
-                                                    <constraint firstAttribute="height" constant="25" id="N06-qe-ZVY"/>
-                                                </constraints>
-                                                <state key="normal" image="pip.enter" catalog="system"/>
-                                                <connections>
-                                                    <action selector="setPip:" destination="sBp-t2-eFh" eventType="touchUpInside" id="svR-8R-DQY"/>
-                                                </connections>
-                                            </button>
-                                        </subviews>
-                                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                        <constraints>
-                                            <constraint firstAttribute="trailing" secondItem="9aW-Pg-83x" secondAttribute="trailing" id="8db-gr-hzG"/>
-                                            <constraint firstItem="NwE-zQ-Y5D" firstAttribute="centerY" secondItem="dgJ-dQ-lSp" secondAttribute="centerY" id="Ad6-Aj-hvc"/>
-                                            <constraint firstAttribute="bottom" secondItem="9aW-Pg-83x" secondAttribute="bottom" id="B8A-Zh-HYL"/>
-                                            <constraint firstAttribute="width" constant="100" id="LTs-QZ-nVw"/>
-                                            <constraint firstAttribute="trailing" secondItem="8AB-hx-yqN" secondAttribute="trailing" constant="15" id="bQK-xA-Jyu"/>
-                                            <constraint firstItem="NwE-zQ-Y5D" firstAttribute="leading" secondItem="dgJ-dQ-lSp" secondAttribute="leading" constant="15" id="cZY-tl-BOh"/>
-                                            <constraint firstItem="9aW-Pg-83x" firstAttribute="leading" secondItem="dgJ-dQ-lSp" secondAttribute="leading" id="clL-y7-JuR"/>
-                                            <constraint firstItem="9aW-Pg-83x" firstAttribute="top" secondItem="dgJ-dQ-lSp" secondAttribute="top" id="jQP-bV-BQ9"/>
-                                            <constraint firstItem="8AB-hx-yqN" firstAttribute="centerY" secondItem="dgJ-dQ-lSp" secondAttribute="centerY" id="uNT-D4-mK3"/>
-                                            <constraint firstAttribute="height" constant="45" id="xf9-B5-HMs"/>
-                                        </constraints>
-                                    </view>
-                                    <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sBp-t2-eFh" customClass="NCPlayerToolBar" customModule="Nextcloud" customModuleProvider="target">
-                                        <rect key="frame" x="15" y="661" width="384" height="65"/>
-                                        <subviews>
-                                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="a0D-B0-eGX" userLabel="Button Player Tool Bar">
-                                                <rect key="frame" x="0.0" y="0.0" width="384" height="65"/>
-                                                <connections>
-                                                    <action selector="buttonPlayerToolBarTouchInside:" destination="sBp-t2-eFh" eventType="touchUpInside" id="hyl-lR-OGD"/>
-                                                </connections>
-                                            </button>
-                                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Z2D-bl-6Qu" userLabel="Container play">
-                                                <rect key="frame" x="0.0" y="0.0" width="115.33333333333333" height="65"/>
-                                                <subviews>
-                                                    <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Isy-sX-Gji">
-                                                        <rect key="frame" x="3" y="12.666666666666629" width="40" height="40"/>
-                                                        <constraints>
-                                                            <constraint firstAttribute="width" constant="40" id="COm-r2-ubw"/>
-                                                            <constraint firstAttribute="height" constant="40" id="mbb-ql-zCc"/>
-                                                        </constraints>
-                                                        <state key="normal" image="gobackward.10" catalog="system"/>
-                                                        <connections>
-                                                            <action selector="backButtonSec:" destination="sBp-t2-eFh" eventType="touchUpInside" id="Q46-Tp-inF"/>
-                                                        </connections>
-                                                    </button>
-                                                    <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x3E-b2-obf">
-                                                        <rect key="frame" x="37.666666666666629" y="12.666666666666629" width="40" height="40"/>
-                                                        <constraints>
-                                                            <constraint firstAttribute="width" constant="40" id="Cmv-LX-Phg"/>
-                                                            <constraint firstAttribute="height" constant="40" id="djE-Ml-YD0"/>
-                                                        </constraints>
-                                                        <state key="normal">
-                                                            <imageReference key="image" image="play.fill" catalog="system" symbolScale="default"/>
-                                                            <preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="default"/>
-                                                        </state>
-                                                        <connections>
-                                                            <action selector="playerPause:" destination="sBp-t2-eFh" eventType="touchUpInside" id="pRl-bT-hpi"/>
-                                                        </connections>
-                                                    </button>
-                                                    <button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="F9L-PP-AbM">
-                                                        <rect key="frame" x="72" y="12.666666666666629" width="40" height="40"/>
-                                                        <constraints>
-                                                            <constraint firstAttribute="width" constant="40" id="hlL-XQ-OLa"/>
-                                                            <constraint firstAttribute="height" constant="40" id="vEe-Fb-a9N"/>
-                                                        </constraints>
-                                                        <state key="normal" image="goforward.10" catalog="system"/>
-                                                        <connections>
-                                                            <action selector="forwardButtonSec:" destination="sBp-t2-eFh" eventType="touchUpInside" id="QPi-TQ-rNF"/>
-                                                        </connections>
-                                                    </button>
-                                                </subviews>
-                                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                                <constraints>
-                                                    <constraint firstItem="F9L-PP-AbM" firstAttribute="centerY" secondItem="Z2D-bl-6Qu" secondAttribute="centerY" id="Hb6-zo-b2N"/>
-                                                    <constraint firstItem="x3E-b2-obf" firstAttribute="centerX" secondItem="Z2D-bl-6Qu" secondAttribute="centerX" id="Hj0-C7-K75"/>
-                                                    <constraint firstItem="F9L-PP-AbM" firstAttribute="centerX" secondItem="Z2D-bl-6Qu" secondAttribute="centerX" multiplier="1.6" id="a8O-oF-jJM"/>
-                                                    <constraint firstItem="Isy-sX-Gji" firstAttribute="centerX" secondItem="Z2D-bl-6Qu" secondAttribute="centerX" multiplier="0.4" id="mLO-jA-XH6"/>
-                                                    <constraint firstItem="Isy-sX-Gji" firstAttribute="centerY" secondItem="Z2D-bl-6Qu" secondAttribute="centerY" id="ngz-yI-ozW"/>
-                                                    <constraint firstItem="x3E-b2-obf" firstAttribute="centerY" secondItem="Z2D-bl-6Qu" secondAttribute="centerY" id="qMZ-ZT-IWE"/>
-                                                </constraints>
-                                            </view>
-                                            <slider opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="SR4-e8-1hC">
-                                                <rect key="frame" x="118.33333333333331" y="17.666666666666629" width="257.66666666666663" height="31"/>
-                                            </slider>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="99:99:99" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OUD-HH-cXH">
-                                                <rect key="frame" x="328.66666666666669" y="47.666666666666629" width="45.333333333333314" height="12"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="10"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kVv-X4-6SK">
-                                                <rect key="frame" x="120.33333333333331" y="47.666666666666629" width="45" height="12"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="10"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                        </subviews>
-                                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                        <constraints>
-                                            <constraint firstItem="Z2D-bl-6Qu" firstAttribute="width" secondItem="sBp-t2-eFh" secondAttribute="width" multiplier="0.3" id="13g-iK-lwr"/>
-                                            <constraint firstItem="kVv-X4-6SK" firstAttribute="leading" secondItem="SR4-e8-1hC" secondAttribute="leading" id="5hb-O9-gEt"/>
-                                            <constraint firstItem="SR4-e8-1hC" firstAttribute="leading" secondItem="Z2D-bl-6Qu" secondAttribute="trailing" constant="5" id="6He-ZU-Vwa"/>
-                                            <constraint firstItem="Z2D-bl-6Qu" firstAttribute="height" secondItem="sBp-t2-eFh" secondAttribute="height" id="AbA-kH-vt3"/>
-                                            <constraint firstAttribute="height" constant="65" id="EyI-HW-pHA"/>
-                                            <constraint firstItem="SR4-e8-1hC" firstAttribute="centerY" secondItem="sBp-t2-eFh" secondAttribute="centerY" id="L8n-9C-y6b"/>
-                                            <constraint firstItem="a0D-B0-eGX" firstAttribute="leading" secondItem="sBp-t2-eFh" secondAttribute="leading" id="OIL-1t-uuh"/>
-                                            <constraint firstAttribute="trailing" secondItem="SR4-e8-1hC" secondAttribute="trailing" constant="10" id="Q3r-ex-Cpf"/>
-                                            <constraint firstAttribute="bottom" secondItem="a0D-B0-eGX" secondAttribute="bottom" id="a3T-pV-xrr"/>
-                                            <constraint firstItem="Z2D-bl-6Qu" firstAttribute="centerY" secondItem="sBp-t2-eFh" secondAttribute="centerY" id="gdD-MC-K1m"/>
-                                            <constraint firstItem="a0D-B0-eGX" firstAttribute="top" secondItem="sBp-t2-eFh" secondAttribute="top" id="itu-re-FU4"/>
-                                            <constraint firstAttribute="trailing" secondItem="OUD-HH-cXH" secondAttribute="trailing" constant="10" id="j5v-RG-0JI"/>
-                                            <constraint firstItem="Z2D-bl-6Qu" firstAttribute="leading" secondItem="sBp-t2-eFh" secondAttribute="leading" id="jHq-yM-afK"/>
-                                            <constraint firstItem="kVv-X4-6SK" firstAttribute="top" secondItem="SR4-e8-1hC" secondAttribute="bottom" id="l6a-l5-ZvL"/>
-                                            <constraint firstAttribute="trailing" secondItem="a0D-B0-eGX" secondAttribute="trailing" id="lbh-DN-SZF"/>
-                                            <constraint firstItem="OUD-HH-cXH" firstAttribute="top" secondItem="SR4-e8-1hC" secondAttribute="bottom" id="wwk-tz-5dj"/>
-                                        </constraints>
-                                        <connections>
-                                            <outlet property="backButton" destination="Isy-sX-Gji" id="RBh-0O-yAN"/>
-                                            <outlet property="forwardButton" destination="F9L-PP-AbM" id="cc0-4F-r5v"/>
-                                            <outlet property="labelCurrentTime" destination="kVv-X4-6SK" id="vyf-Bb-TPL"/>
-                                            <outlet property="labelLeftTime" destination="OUD-HH-cXH" id="MjW-XN-Uer"/>
-                                            <outlet property="muteButton" destination="8AB-hx-yqN" id="9zQ-k7-auv"/>
-                                            <outlet property="pipButton" destination="NwE-zQ-Y5D" id="veJ-QD-fOd"/>
-                                            <outlet property="playButton" destination="x3E-b2-obf" id="0Nw-L4-W7M"/>
-                                            <outlet property="playbackSlider" destination="SR4-e8-1hC" id="Khx-Oe-NEQ"/>
-                                            <outlet property="playerTopToolBarView" destination="dgJ-dQ-lSp" id="22g-Yn-k5r"/>
-                                        </connections>
-                                    </view>
                                 </subviews>
                                 <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                 <constraints>
@@ -417,19 +263,14 @@
                         <viewLayoutGuide key="safeArea" id="Yo6-7W-moG"/>
                         <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         <constraints>
-                            <constraint firstItem="Yo6-7W-moG" firstAttribute="trailing" secondItem="dgJ-dQ-lSp" secondAttribute="trailing" constant="15" id="15m-hD-wYt"/>
                             <constraint firstAttribute="bottom" secondItem="CdQ-LC-Trx" secondAttribute="bottom" id="4qB-8y-OcG"/>
                             <constraint firstAttribute="trailing" secondItem="CdQ-LC-Trx" secondAttribute="trailing" id="IwE-oE-d3Y"/>
-                            <constraint firstItem="Yo6-7W-moG" firstAttribute="bottom" secondItem="sBp-t2-eFh" secondAttribute="bottom" constant="10" id="QHF-av-zeT"/>
-                            <constraint firstItem="Yo6-7W-moG" firstAttribute="trailing" secondItem="sBp-t2-eFh" secondAttribute="trailing" constant="15" id="TCr-e0-gnG"/>
                             <constraint firstItem="2AU-85-K8y" firstAttribute="leading" secondItem="Yo6-7W-moG" secondAttribute="leading" constant="10" id="X10-OG-EKg"/>
                             <constraint firstItem="Yo6-7W-moG" firstAttribute="top" secondItem="2AU-85-K8y" secondAttribute="top" constant="-10" id="avO-83-uMQ"/>
                             <constraint firstItem="Yo6-7W-moG" firstAttribute="bottom" secondItem="P8R-4f-zAl" secondAttribute="top" constant="400" id="bor-cg-Alz"/>
                             <constraint firstItem="P8R-4f-zAl" firstAttribute="leading" secondItem="Yo6-7W-moG" secondAttribute="leading" id="dly-i5-fPW"/>
-                            <constraint firstItem="dgJ-dQ-lSp" firstAttribute="top" secondItem="Yo6-7W-moG" secondAttribute="top" constant="10" id="g7l-5Z-sJj"/>
                             <constraint firstItem="CdQ-LC-Trx" firstAttribute="leading" secondItem="fIE-H6-KKc" secondAttribute="leading" id="g8C-2m-KkX"/>
                             <constraint firstItem="CdQ-LC-Trx" firstAttribute="top" secondItem="fIE-H6-KKc" secondAttribute="top" id="hcQ-lB-JwU"/>
-                            <constraint firstItem="sBp-t2-eFh" firstAttribute="leading" secondItem="Yo6-7W-moG" secondAttribute="leading" constant="15" id="hwP-QY-nRI"/>
                             <constraint firstItem="Yo6-7W-moG" firstAttribute="trailing" secondItem="P8R-4f-zAl" secondAttribute="trailing" id="jf2-Nv-gFi"/>
                         </constraints>
                     </view>
@@ -440,7 +281,6 @@
                         <outlet property="imageVideoContainer" destination="kPV-JM-UnM" id="2pA-VW-FuK"/>
                         <outlet property="imageViewBottomConstraint" destination="vEd-X2-yGs" id="wp3-67-aZ2"/>
                         <outlet property="imageViewTopConstraint" destination="tdo-XY-uqv" id="AM2-tz-fSt"/>
-                        <outlet property="playerToolBar" destination="sBp-t2-eFh" id="E24-0a-bN6"/>
                         <outlet property="scrollView" destination="CdQ-LC-Trx" id="3np-FR-s39"/>
                         <outlet property="statusLabel" destination="DAi-gz-qGP" id="zZH-1B-HiI"/>
                         <outlet property="statusViewImage" destination="2AU-85-K8y" id="22h-Ec-bj0"/>
@@ -457,12 +297,7 @@
         </scene>
     </scenes>
     <resources>
-        <image name="audioOn" width="28" height="28"/>
-        <image name="gobackward.10" catalog="system" width="121" height="128"/>
-        <image name="goforward.10" catalog="system" width="121" height="128"/>
         <image name="networkInProgress" width="300" height="300"/>
-        <image name="pip.enter" catalog="system" width="128" height="96"/>
-        <image name="play.fill" catalog="system" width="116" height="128"/>
         <systemColor name="systemBackgroundColor">
             <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </systemColor>

+ 24 - 16
iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift

@@ -109,21 +109,21 @@ class NCViewerMediaPage: UIViewController {
 
     deinit {
         print("#deinit NCViewerMediaPage")
-        // Clear
+    }
+
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+
         if let ncplayer = currentViewController.ncplayer, ncplayer.isPlay() {
             ncplayer.playerPause()
             ncplayer.saveCurrentTime()
         }
-        currentViewController.playerToolBar.stopTimerAutoHide()
+        currentViewController.playerToolBar?.stopTimerAutoHide()
         clearCommandCenter()
 
         metadatas.removeAll()
         ncplayerLivePhoto = nil
 
-        #if MFFFLIB
-        MFFF.shared.dismissMessage()
-        #endif
-
         // Remove Observer
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDeleteFile), object: nil)
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterRenameFile), object: nil)
@@ -151,6 +151,10 @@ class NCViewerMediaPage: UIViewController {
         }
     }
 
+    override var prefersHomeIndicatorAutoHidden: Bool {
+        return currentScreenMode == .full
+    }
+
     // MARK: -
     
     func getViewerMedia(index: Int, metadata: tableMetadata) -> NCViewerMedia {
@@ -184,7 +188,7 @@ class NCViewerMediaPage: UIViewController {
             progressView.isHidden = false
 
             if !currentViewController.detailView.isShow() {
-                currentViewController.playerToolBar.show(enableTimerAutoHide: enableTimerAutoHide)
+                currentViewController.playerToolBar?.show(enableTimerAutoHide: enableTimerAutoHide)
             }
 
             NCUtility.shared.colorNavigationController(navigationController, backgroundColor: NCBrandColor.shared.systemBackground, titleColor: NCBrandColor.shared.label, tintColor: nil, withoutShadow: false)
@@ -196,7 +200,7 @@ class NCViewerMediaPage: UIViewController {
             navigationController?.setNavigationBarHidden(true, animated: true)
             progressView.isHidden = true
 
-            currentViewController.playerToolBar.hide()
+            currentViewController.playerToolBar?.hide()
 
             view.backgroundColor = .black
             textColor = .white
@@ -209,6 +213,7 @@ class NCViewerMediaPage: UIViewController {
         }
 
         setNeedsStatusBarAppearanceUpdate()
+        setNeedsUpdateOfHomeIndicatorAutoHidden()
         currentViewController.reloadDetail()
     }
 
@@ -361,19 +366,19 @@ class NCViewerMediaPage: UIViewController {
         if metadata.classFile == NCCommunicationCommon.typeClassFile.video.rawValue || metadata.classFile == NCCommunicationCommon.typeClassFile.audio.rawValue {
 
             MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = true
-            skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand.addTarget { [weak self] event in
+            skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand.addTarget { event in
 
                 let seconds = Float64((event as! MPSkipIntervalCommandEvent).interval)
-                self?.currentViewController.playerToolBar.skip(seconds: seconds)
-                return .success
+                self.currentViewController.playerToolBar?.skip(seconds: seconds)
+                return.success
             }
 
             MPRemoteCommandCenter.shared().skipBackwardCommand.isEnabled = true
-            skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand.addTarget { [weak self] event in
+            skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand.addTarget { event in
 
                 let seconds = Float64((event as! MPSkipIntervalCommandEvent).interval)
-                self?.currentViewController.playerToolBar.skip(seconds: -seconds)
-                return .success
+                self.currentViewController.playerToolBar?.skip(seconds: -seconds)
+                return.success
             }
         }
 
@@ -607,8 +612,11 @@ extension NCViewerMediaPage: UIGestureRecognizerDelegate {
 
                 AudioServicesPlaySystemSound(1519) // peek feedback
 
-                if let url = NCKTVHTTPCache.shared.getVideoURL(metadata: metadata) {
-                    self.ncplayerLivePhoto = NCPlayer.init(url: url, autoPlay: true, imageVideoContainer: self.currentViewController.imageVideoContainer, playerToolBar: nil, metadata: metadata, detailView: nil, viewController: self)
+                let urlVideo = NCKTVHTTPCache.shared.getVideoURL(metadata: metadata)
+                
+                if let url = urlVideo.url {
+                    self.ncplayerLivePhoto = NCPlayer.init(url: url, autoPlay: true, isProxy: urlVideo.isProxy, imageVideoContainer: self.currentViewController.imageVideoContainer, playerToolBar: nil, metadata: metadata, detailView: nil, viewController: self)
+                    self.ncplayerLivePhoto?.openAVPlayer()
                 }
             }