From 8a4cc09210b16ee8f848569b098e659644f966a3 Mon Sep 17 00:00:00 2001 From: marcoschmickler Date: Sun, 14 Nov 2021 13:01:15 +0100 Subject: [PATCH] Local Files --- download.js | 8 +- kplayer.xcodeproj/project.pbxproj | 48 ++ kplayer/AppDelegate.swift | 5 +- kplayer/core/LocalManager.swift | 263 +++++++ kplayer/core/MediaItem.swift | 27 +- kplayer/core/MediaModel.swift | 1 + kplayer/core/NetworkManager.swift | 238 +----- kplayer/detail/BrowserController.swift | 12 +- kplayer/detail/DetailViewController.swift | 25 +- kplayer/detail/EditItemView.swift | 46 ++ kplayer/detail/ItemCell.swift | 2 +- kplayer/detail/VideoController.swift | 75 +- kplayer/master/KSettingsView.swift | 4 +- kplayer/master/MasterViewController.swift | 62 +- kplayer/master/NetworkDelegate.swift | 46 +- kplayer/master/ServerDownloadDelegate.swift | 30 + kplayer/photo/PhotoController.swift | 3 +- kplayer/server/kplayer.js | 102 ++- kplayer/server/links.html | 15 + kplayer/timeline/CenterLine.swift | 126 ++++ kplayer/timeline/FrameImagesView.swift | 451 ++++++++++++ kplayer/timeline/TimelineMeasure.swift | 206 ++++++ kplayer/timeline/TimelineScroller.swift | 178 +++++ kplayer/timeline/TimelineView.swift | 460 ++++++++++++ kplayer/timeline/TrimView.swift | 773 ++++++++++++++++++++ kplayer/timeline/VideoTimelineView.swift | 301 ++++++++ kplayer/video/BMPlayer.swift | 2 +- 27 files changed, 3235 insertions(+), 274 deletions(-) create mode 100644 kplayer/core/LocalManager.swift create mode 100644 kplayer/detail/EditItemView.swift create mode 100644 kplayer/master/ServerDownloadDelegate.swift create mode 100644 kplayer/server/links.html create mode 100644 kplayer/timeline/CenterLine.swift create mode 100644 kplayer/timeline/FrameImagesView.swift create mode 100644 kplayer/timeline/TimelineMeasure.swift create mode 100644 kplayer/timeline/TimelineScroller.swift create mode 100644 kplayer/timeline/TimelineView.swift create mode 100644 kplayer/timeline/TrimView.swift create mode 100644 kplayer/timeline/VideoTimelineView.swift diff --git a/download.js b/download.js index d700b40..0a09d1e 100644 --- a/download.js +++ b/download.js @@ -11,6 +11,10 @@ links = document.querySelectorAll('a[title="Right click the link below and select save as to download:"]') linkname = "href" } + else if (window.location.hostname.endsWith("chaturbate.com")) { + var src = JSON.parse(window.initialRoomDossier).hls_source + window.kplayerUrls.push(src) + } else if (window.location.hostname.endsWith("hegre.com")) { links = document.querySelectorAll('a[class="members-only"], a[class="image-download"]') linkname = "href" @@ -25,7 +29,7 @@ for (var i=0; i0) { - var base = u.split('/').slice(0,3).join('/') + var base = u.split('/').slice(0,-1).join('/') window.webkit.messageHandlers.jsError.postMessage('base: ' + base) window.webkit.messageHandlers.jsError.postMessage('url: ' + u) var r = await fetch(u) @@ -38,7 +42,7 @@ if (!l.startsWith("#")) { window.webkit.messageHandlers.jsError.postMessage('l: ' + base + l) - window.kplayerUrls.push(base + l) + window.kplayerUrls.push(base + "/" + l) } } } diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index 22c1ef3..8547cc4 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -11,19 +11,25 @@ 1C736048BFA120F5C7D36874 /* readme.md in Sources */ = {isa = PBXBuildFile; fileRef = 1C73685B4BBFDAFBF08C032C /* readme.md */; }; 1C7360C0F2A4F0214FE353BD /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367ECBD369A2A0C94C499 /* FileHelper.swift */; }; 1C7360F1D2CF83ECDD586B84 /* BMPlayerCompositionResourceDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73610B997EBA367C806C1B /* BMPlayerCompositionResourceDefinition.swift */; }; + 1C73613562EB375F53A0BD03 /* ServerDownloadDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */; }; 1C7361B3AF46CEB30D3F4FA0 /* KSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736AE5021E3D985FE3402D /* KSettings.swift */; }; 1C7361D2B6E0AE689FAAF4F4 /* VideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736C7FFBDAC665AE04CB65 /* VideoController.swift */; }; + 1C7361D3BA77C40275F89D4A /* TimelineMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7368C7B946BC9E067D37E7 /* TimelineMeasure.swift */; }; + 1C7361F376DA11F17CD3250B /* TrimView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736ABA0E14A51ACAC84AB5 /* TrimView.swift */; }; 1C73631EACF56BABD3B2BCFB /* LayoutTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736BC4450890C45F8FBC63 /* LayoutTools.swift */; }; 1C73635138BBD2BB480A308F /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C736777456388CA571DA17B /* MediaPlayer.framework */; }; + 1C7363D4C34EBBD5C7AAD0A8 /* scratch.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1C7363E0DDA5854D55F8836E /* scratch.txt */; }; 1C73640D928DE56D35175D39 /* UploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736260E748CF136FF37EA7 /* UploadOperation.swift */; }; 1C73646F87B495A47D7943C7 /* NetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369EC16B19B32B515169E /* NetData.swift */; }; 1C736503B656C999E5E12081 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7365B06FA66294E99AC2D3 /* NetworkManager.swift */; }; 1C73654C9EA6D255CFC039C5 /* NetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73620D01687FB4F1811C5C /* NetworkHelper.swift */; }; 1C7365885FAF292F2221ED44 /* PhotoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73673DC671535E3A049F54 /* PhotoController.swift */; }; + 1C7365BEFFB35E8DE8F04CCF /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73661561AD069C92FE3B15 /* TimelineView.swift */; }; 1C73666A07CF2416B1B8D3F0 /* KSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736C94157754DE1C808173 /* KSettingsModel.swift */; }; 1C7366A0CFD2B55BF8C3BAF0 /* NetworkDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */; }; 1C7366DAC06047DE335EFC37 /* BMPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736927EA28AFBEB25D7487 /* BMPlayer.swift */; }; 1C73671FC2CCCACAA2FFC153 /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */; }; + 1C73672CEAE1B9DA7805D4F2 /* CenterLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7361C26ED27AB54594317D /* CenterLine.swift */; }; 1C73673F39A34C3275D0230A /* BMPlayerClearityChooseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736DFBD072763248412F74 /* BMPlayerClearityChooseButton.swift */; }; 1C73675C34BE0990D44570BE /* ItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736253AB7A95EA41B605B7 /* ItemModel.swift */; }; 1C736771C503FB0D52AEB8F7 /* kplayer.js in Sources */ = {isa = PBXBuildFile; fileRef = 1C73625012D50E457D18A785 /* kplayer.js */; }; @@ -39,6 +45,7 @@ 1C7369ABC44CFB530EA71FB6 /* HeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736D9BB5498E7E8F11C754 /* HeaderCell.swift */; }; 1C736A06A2AD75B8C14EEBBE /* HtmlParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */; }; 1C736A5FA5BA53B2597F2ED7 /* Kirschkeks-256x256.png in Resources */ = {isa = PBXBuildFile; fileRef = 1C736059262A57AADE6AB761 /* Kirschkeks-256x256.png */; }; + 1C736A622876405F3EE2D043 /* EditItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7366C09381DC0052B52B69 /* EditItemView.swift */; }; 1C736A7B6221A1D50FB3904C /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73631C96E6C860833052CA /* ItemType.swift */; }; 1C736CB96577F6A9A7BA03E8 /* BMPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7364F924BD979294C3EE4A /* BMPlayerItem.swift */; }; 1C736CD0E54786D3A2405E51 /* BMPlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7366D766CDE0C9872E86F5 /* BMPlayerLayerView.swift */; }; @@ -48,9 +55,14 @@ 1C736D895B75BDCDB35937C1 /* BMTimeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360AE55EB115762C42EB9 /* BMTimeSlider.swift */; }; 1C736DB41BD06D359E6A0DEE /* BMSubtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7366AAB82A46086690E164 /* BMSubtitles.swift */; }; 1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B41C6AC33F3FA592C63 /* MediaModel.swift */; }; + 1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; }; + 1C736ECAE78F5C722423D7ED /* TimelineScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736362946D7A8585B0D875 /* TimelineScroller.swift */; }; + 1C736F1C31D1EC23F59125F0 /* VideoTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736FC4180B42C3A357E9BF /* VideoTimelineView.swift */; }; 1C736F278DDC77F40C8CB1D4 /* BMPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736E51F1A03E3A1200BDB6 /* BMPlayerControlView.swift */; }; 1C736F3570EADA086682E6BC /* BMPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360D6580FB5D09C2BBCCB /* BMPlayerManager.swift */; }; + 1C736F3D082067948BA4DE84 /* FrameImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736A5140D9B25BEC266B72 /* FrameImagesView.swift */; }; 1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; }; + 1C736FAE5D3E5D3BA3C1FAE5 /* links.html in Resources */ = {isa = PBXBuildFile; fileRef = 1C73645DBD6499A726D34973 /* links.html */; }; 1C736FB92B19FE17E4357C85 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73688DAB88F9360FB62A49 /* MediaItem.swift */; }; AA74B07A01F0E99E6DEC7D1B /* Pods_kplayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B75159FFCD5A882E6F167FE /* Pods_kplayer.framework */; }; C98AF5D51B124D6A00D196CC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98AF5D41B124D6A00D196CC /* AppDelegate.swift */; }; @@ -82,28 +94,39 @@ 1C73610B997EBA367C806C1B /* BMPlayerCompositionResourceDefinition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerCompositionResourceDefinition.swift; sourceTree = ""; }; 1C73611D226B48C24DB37535 /* MasterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; 1C73615FFA2AA98BD1C56CD4 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = ""; }; + 1C7361C26ED27AB54594317D /* CenterLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CenterLine.swift; path = timeline/CenterLine.swift; sourceTree = ""; }; 1C73620D01687FB4F1811C5C /* NetworkHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkHelper.swift; sourceTree = ""; }; 1C73625012D50E457D18A785 /* kplayer.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = kplayer.js; sourceTree = ""; }; 1C736253AB7A95EA41B605B7 /* ItemModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemModel.swift; sourceTree = ""; }; 1C736260E748CF136FF37EA7 /* UploadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadOperation.swift; sourceTree = ""; }; 1C73631C96E6C860833052CA /* ItemType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = ""; }; + 1C736362946D7A8585B0D875 /* TimelineScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineScroller.swift; path = timeline/TimelineScroller.swift; sourceTree = ""; }; + 1C7363E0DDA5854D55F8836E /* scratch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = scratch.txt; sourceTree = ""; }; + 1C73645DBD6499A726D34973 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = ""; }; 1C7364709899FF62774B0199 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = ""; }; 1C73648CEC974A2500172064 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = ""; }; 1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDelegate.swift; sourceTree = ""; }; 1C7364F924BD979294C3EE4A /* BMPlayerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerItem.swift; sourceTree = ""; }; + 1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDownloadDelegate.swift; sourceTree = ""; }; + 1C73659CC9B523B957E58DC6 /* LocalManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalManager.swift; sourceTree = ""; }; 1C7365B06FA66294E99AC2D3 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 1C7365F45D765A218FFC100F /* BMPlayerProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerProtocols.swift; sourceTree = ""; }; + 1C73661561AD069C92FE3B15 /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineView.swift; path = timeline/TimelineView.swift; sourceTree = ""; }; 1C7366AAB82A46086690E164 /* BMSubtitles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMSubtitles.swift; sourceTree = ""; }; + 1C7366C09381DC0052B52B69 /* EditItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditItemView.swift; sourceTree = ""; }; 1C7366D766CDE0C9872E86F5 /* BMPlayerLayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerLayerView.swift; sourceTree = ""; }; 1C73673DC671535E3A049F54 /* PhotoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoController.swift; sourceTree = ""; }; 1C736777456388CA571DA17B /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; 1C7367ECBD369A2A0C94C499 /* FileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = ""; }; 1C73685B4BBFDAFBF08C032C /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = readme.md; sourceTree = ""; }; 1C73688DAB88F9360FB62A49 /* MediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; + 1C7368C7B946BC9E067D37E7 /* TimelineMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineMeasure.swift; path = timeline/TimelineMeasure.swift; sourceTree = ""; }; 1C736927EA28AFBEB25D7487 /* BMPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayer.swift; sourceTree = ""; }; 1C7369EC16B19B32B515169E /* NetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetData.swift; sourceTree = ""; }; 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; + 1C736A5140D9B25BEC266B72 /* FrameImagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FrameImagesView.swift; path = timeline/FrameImagesView.swift; sourceTree = ""; }; 1C736A6E8396EE306B1AD3A8 /* KSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettingsView.swift; sourceTree = ""; }; + 1C736ABA0E14A51ACAC84AB5 /* TrimView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TrimView.swift; path = timeline/TrimView.swift; sourceTree = ""; }; 1C736AE5021E3D985FE3402D /* KSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettings.swift; sourceTree = ""; }; 1C736B41C6AC33F3FA592C63 /* MediaModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaModel.swift; sourceTree = ""; }; 1C736B794396F2E50387B8F2 /* stringutil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = stringutil.swift; sourceTree = ""; }; @@ -117,6 +140,7 @@ 1C736E51F1A03E3A1200BDB6 /* BMPlayerControlView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerControlView.swift; sourceTree = ""; }; 1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = ""; }; 1C736F9338CE36708244D42A /* DataLoadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataLoadOperation.swift; sourceTree = ""; }; + 1C736FC4180B42C3A357E9BF /* VideoTimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VideoTimelineView.swift; path = timeline/VideoTimelineView.swift; sourceTree = ""; }; 6D522F61736592330F481B4F /* Pods-kplayer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-kplayer.debug.xcconfig"; path = "Pods/Target Support Files/Pods-kplayer/Pods-kplayer.debug.xcconfig"; sourceTree = ""; }; 8B75159FFCD5A882E6F167FE /* Pods_kplayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_kplayer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C98AF5CF1B124D6A00D196CC /* kplayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = kplayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -160,6 +184,7 @@ 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */, 1C736C7FFBDAC665AE04CB65 /* VideoController.swift */, 1C73602350ACE2436736F981 /* BrowserController.swift */, + 1C7366C09381DC0052B52B69 /* EditItemView.swift */, ); path = detail; sourceTree = ""; @@ -169,6 +194,7 @@ children = ( 1C73625012D50E457D18A785 /* kplayer.js */, 1C73615FFA2AA98BD1C56CD4 /* links.html */, + 1C73645DBD6499A726D34973 /* links.html */, ); path = server; sourceTree = ""; @@ -187,6 +213,7 @@ 1C73611D226B48C24DB37535 /* MasterViewController.swift */, 1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */, 1C736A6E8396EE306B1AD3A8 /* KSettingsView.swift */, + 1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */, ); path = master; sourceTree = ""; @@ -222,6 +249,7 @@ 1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */, 1C736AE5021E3D985FE3402D /* KSettings.swift */, 1C736C94157754DE1C808173 /* KSettingsModel.swift */, + 1C73659CC9B523B957E58DC6 /* LocalManager.swift */, ); path = core; sourceTree = ""; @@ -271,6 +299,7 @@ 1C736059262A57AADE6AB761 /* Kirschkeks-256x256.png */, 8052F5B3AAC2535E5C08A529 /* Pods */, 1C73685B4BBFDAFBF08C032C /* readme.md */, + 1C7363E0DDA5854D55F8836E /* scratch.txt */, ); sourceTree = ""; }; @@ -299,6 +328,13 @@ 1C7363B608460DED4F522D1C /* photo */, 1C736F3946A38499113D351A /* video */, 1C73633B55AC5378053BDCE2 /* server */, + 1C736FC4180B42C3A357E9BF /* VideoTimelineView.swift */, + 1C7361C26ED27AB54594317D /* CenterLine.swift */, + 1C736A5140D9B25BEC266B72 /* FrameImagesView.swift */, + 1C7368C7B946BC9E067D37E7 /* TimelineMeasure.swift */, + 1C736362946D7A8585B0D875 /* TimelineScroller.swift */, + 1C73661561AD069C92FE3B15 /* TimelineView.swift */, + 1C736ABA0E14A51ACAC84AB5 /* TrimView.swift */, ); path = kplayer; sourceTree = ""; @@ -422,6 +458,8 @@ C98AF5E11B124D6A00D196CC /* Images.xcassets in Resources */, 1C736A5FA5BA53B2597F2ED7 /* Kirschkeks-256x256.png in Resources */, 1C73696E4C0353053BF98031 /* links.html in Resources */, + 1C736FAE5D3E5D3BA3C1FAE5 /* links.html in Resources */, + 1C7363D4C34EBBD5C7AAD0A8 /* scratch.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -536,6 +574,16 @@ 1C7361B3AF46CEB30D3F4FA0 /* KSettings.swift in Sources */, 1C73600CB93F16F4F28C116F /* KSettingsView.swift in Sources */, 1C73666A07CF2416B1B8D3F0 /* KSettingsModel.swift in Sources */, + 1C736F1C31D1EC23F59125F0 /* VideoTimelineView.swift in Sources */, + 1C73672CEAE1B9DA7805D4F2 /* CenterLine.swift in Sources */, + 1C736F3D082067948BA4DE84 /* FrameImagesView.swift in Sources */, + 1C7361D3BA77C40275F89D4A /* TimelineMeasure.swift in Sources */, + 1C736ECAE78F5C722423D7ED /* TimelineScroller.swift in Sources */, + 1C7365BEFFB35E8DE8F04CCF /* TimelineView.swift in Sources */, + 1C7361F376DA11F17CD3250B /* TrimView.swift in Sources */, + 1C736A622876405F3EE2D043 /* EditItemView.swift in Sources */, + 1C73613562EB375F53A0BD03 /* ServerDownloadDelegate.swift in Sources */, + 1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/kplayer/AppDelegate.swift b/kplayer/AppDelegate.swift index dc98bad..82b8ebf 100644 --- a/kplayer/AppDelegate.swift +++ b/kplayer/AppDelegate.swift @@ -26,7 +26,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele let controller = masterNavigationController.topViewController as! MasterViewController controller.delegate = NetworkDelegate() - NetworkManager.sharedInstance.loadSettings() + LocalManager.sharedInstance.loadSettings() let url = URL(string: NetworkManager.sharedInstance.vidurl)?.appendingPathComponent("ren").appendingPathComponent("kplayer.txt") @@ -57,7 +57,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele google.children = parser.items - roots.append(NetworkManager.sharedInstance.favorites) + roots.append(LocalManager.sharedInstance.favorites) + roots.append(MediaItem(name: "extern", path:"", root: "", type: ItemType.FAVROOT)) roots.append(web) controller.model.items = roots diff --git a/kplayer/core/LocalManager.swift b/kplayer/core/LocalManager.swift new file mode 100644 index 0000000..328ce04 --- /dev/null +++ b/kplayer/core/LocalManager.swift @@ -0,0 +1,263 @@ +// +// Created by Marco Schmickler on 13.11.21. +// Copyright (c) 2021 Marco Schmickler. All rights reserved. +// + +import Foundation + +class LocalManager { + static let sharedInstance = LocalManager() + + var settings = KSettings() + + var authenticated = false + + var favorites = MediaItem(name: "fav", path: "", root: "", type: ItemType.FAVROOT) + + var externalURL = "" + + func loadSettings() { + do { + let jsonData = try FileHelper.getData(name: "settings.json") + let model = try JSONDecoder().decode(KSettingsModel.self, from: jsonData) + settings = KSettings(model: model) + } catch { + print(error.localizedDescription) + } + } + + func saveSettings() { + let json = settings.toJSON() + let url = FileHelper.getDocumentsDirectory().appendingPathComponent("settings.json") + do { + print(url) + try json.write(to: url, atomically: true, encoding: .utf8) + let input = try String(contentsOf: url) + print(input) + } catch { + print(json) + } + } + + func deleteLocal(selectedItem: MediaItem) { + if (selectedItem.local) { + print(selectedItem.playerURL?.absoluteString) + do { + try FileManager.default.removeItem(at: selectedItem.playerURL!) + } catch { + print(error) + } + } + } + + func saveFavDir(url: URL, item: MediaItem) -> Void { + for c in item.children { + let t = c.time + let ms = Int(t * 1000) + let p = url.appendingPathExtension("\(ms).jpg") + let pt = url.appendingPathExtension("\(ms)_thumb.jpg") + print(p) + + if FileManager.default.fileExists(atPath: pt.absoluteString) { + print("contained") + } else { + if let id = c.image, let imageData = id.jpegData(compressionQuality: 1.0) { + do { + try imageData.write(to: pt) + } catch { + print("error") + } + + let thumb = id.scaleToSize(15 * 16, height: 15 * 9) + if let imageDataT = thumb.jpegData(compressionQuality: 1.0) { + do { + try imageDataT.write(to: pt) + } catch { + } + c.image = thumb + c.thumbUrl = pt.absoluteString + } + + c.loaded = true + } + } + } + + let json = item.toJSON() + + do { + print(url) + try json.write(to: url.appendingPathExtension("json"), atomically: true, encoding: .utf8) + let input = try String(contentsOf: url) + print(input) + } catch { + print(json) + } + } + + func loadFavDirs(_ url: URL, completionHandler: @escaping Weiter) -> Void { + var res = [MediaItem]() + + var loadedJson = [String]() + + var itemsMap = Dictionary() + + let fi = MediaItem(name: "images", path: "", root: "fav", type: ItemType.DETAILS) + fi.local = true + fi.loaded = true + + res.append(fi) + + if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { + for case let fileURL as URL in enumerator { + do { + let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) + if fileAttributes.isRegularFile! { + if (fileURL.pathExtension == "jpg" && fileURL.absoluteString.contains("/images/")) { + let p = fileURL.absoluteString.substringAfter("/Documents") + + var path = p.substringBefore("/") + + let fp = fileURL.absoluteString.substringAfter("/Documents/images/") + + var fpath = fp.substringBefore("/") + + var folder = itemsMap[fpath] + + if folder == nil { + let f = MediaItem(name: fpath, path: "images", root: "fav", type: ItemType.PICS) + f.local = true + f.loaded = true + folder = f + itemsMap[fpath] = folder! + fi.children.append(f) + } + + let m = MediaItem(name: fileURL.lastPathComponent, path: path, root: "fav", type: ItemType.PICS) + m.local = true + m.loaded = true + m.thumbUrl = fileURL.absoluteString + + let jsonURL = fileURL.absoluteString.replacingOccurrences(of: ".jpg", with: ".json") + + do { + let jsonData = try Data(contentsOf: URL(string: jsonURL)!) + let item = try JSONDecoder().decode(MediaModel.self, from: jsonData) + m.scale = item.scale + m.offset = item.offset + m.size = item.size + } catch { + + } + +// var folder = itemsMap[path] +// +// if folder == nil { +// folder = MediaItem(name: item.path, path: item.path, root: item.root, type: ItemType.FOLDER) +// folder!.loaded = true +// folder!.local = item.local +// itemsMap[path] = folder! +// items.append(folder!) +// } + + folder!.children.append(m) + + // print(fileURL.absoluteString) + } + + let p = fileURL.absoluteString.substringAfter("/Documents") + + if (fileURL.pathExtension == "json") { + + } + + if (fileURL.pathExtension == "mp4") { + print(fileURL.absoluteString) + + let jsonURL = fileURL.appendingPathExtension("json") + + do { + let jsonData = try Data(contentsOf: jsonURL) + let item = try JSONDecoder().decode(MediaModel.self, from: jsonData) + let mediaItem = MediaItem(model: item) + mediaItem.externalURL = fileURL.absoluteString + mediaItem.local = true + for i in mediaItem.children { + i.externalURL = fileURL.absoluteString + i.local = true + } + + res.append(mediaItem) + } catch { + var path = p.substringBefore("/") + + let m = MediaItem(name: fileURL.lastPathComponent, path: path, root: "fav", type: ItemType.VIDEO) + m.local = true + m.loaded = true + m.thumbUrl = fileURL.appendingPathExtension("jpg").absoluteString + m.externalURL = fileURL.absoluteString + + res.append(m) + } + } + } + } catch { + print(error, fileURL) + } + } + } + + let m = MediaItem(name: "new", path: "new", root: "fav", type: ItemType.FOLDER) + m.local = true + res.append(m) + + completionHandler(res) + } + +// old + func loadFav2Dirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { + let files = FileHelper.listFiles(name: "1") + var res = [MediaItem]() + for f in files { + if f.pathExtension == "mp4" { + let m = MediaItem(name: f.lastPathComponent, path: "", root: "1", type: ItemType.VIDEO) + + m.local = true + res.append(m) + } + } + + completionHandler(res) + } + + //old + func loadFav1Dirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { + var res = [MediaItem]() + + do { + let jsonData = try FileHelper.getData(name: "fav.json") + let items = try JSONDecoder().decode(MediaModel.self, from: jsonData) + let loaded = MediaItem(model: items) + res = loaded.children + } catch { + print(error.localizedDescription) + } + + // res.append(currentFav) + completionHandler(res) + } + + func fixDocumentURL(_ path: String?) -> String? { + if path == nil { + return path + } + + if path!.contains("file://") { + let p = path!.substringAfter("/Documents/") + let fixed = FileHelper.getDocumentsDirectory().appendingPathComponent(p) + return fixed.absoluteString + } + return path + } + +} \ No newline at end of file diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 1edcdd3..34e4ff7 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -9,7 +9,7 @@ import UIKit /** Repräsentiert ein Item eines der festgelegten Typen. */ -class MediaItem: CustomDebugStringConvertible { +class MediaItem: CustomDebugStringConvertible, ObservableObject { /** Wird durch name, path und root identifiziert. @@ -37,13 +37,21 @@ class MediaItem: CustomDebugStringConvertible { // Nutzinhalt var image: UIImage? - var time: TimeInterval? - var length: TimeInterval? + + @Published + var time: TimeInterval = 0.0 + + @Published + var length: TimeInterval = 0.0 + + @Published var loop = true + var thumbUrl: String? var externalURL: String? var local = false + var external = false var leaf = false var cancelled = false @@ -57,15 +65,18 @@ class MediaItem: CustomDebugStringConvertible { self.time = model.time self.length = model.length self.loop = model.loop - self.thumbUrl = model.thumbURL + self.thumbUrl = LocalManager.sharedInstance.fixDocumentURL(model.thumbURL) + self.externalURL = LocalManager.sharedInstance.fixDocumentURL(model.externalURL) self.scale = model.scale self.offset = model.offset self.size = model.size + self.loaded = true for m in model.children { let item = MediaItem(model: m) item.index = children.count item.parent = self + item.loaded = true children.append(item) } } @@ -80,10 +91,11 @@ class MediaItem: CustomDebugStringConvertible { var model: MediaModel = MediaModel(name: name, path: path, root: root, children: c, type: type) - model.time = time ?? 0 + model.time = time model.loop = loop - model.length = length ?? 0 + model.length = length model.thumbURL = thumbUrl + model.externalURL = externalURL model.size = size model.offset = offset model.scale = scale @@ -180,6 +192,9 @@ class MediaItem: CustomDebugStringConvertible { if local { var file = FileHelper.getDocumentsDirectory() + if external { + file = URL(string: LocalManager.sharedInstance.externalURL)! + } if (path != "") { file = file.appendingPathComponent(path) diff --git a/kplayer/core/MediaModel.swift b/kplayer/core/MediaModel.swift index 412f69b..e55b415 100644 --- a/kplayer/core/MediaModel.swift +++ b/kplayer/core/MediaModel.swift @@ -15,6 +15,7 @@ public struct MediaModel : Codable { var length = 0.0 var loop = false var thumbURL : String? + var externalURL : String? var scale = 0.0 var offset = CGPoint() diff --git a/kplayer/core/NetworkManager.swift b/kplayer/core/NetworkManager.swift index c0c8f5c..fb9062a 100644 --- a/kplayer/core/NetworkManager.swift +++ b/kplayer/core/NetworkManager.swift @@ -13,13 +13,8 @@ class NetworkManager { let baseurl = "http://linkstation:8080/tomcat/media" let vidurl = "http://linkstation:8089" - var settings = KSettings() - - var authenticated = false var offline = false - var favorites = MediaItem(name: "fav", path:"", root: "", type: ItemType.FAVROOT) - var currentDownloads = [String : MediaItem]() lazy var operationQueue: OperationQueue = { @@ -29,7 +24,6 @@ class NetworkManager { return queue }() - internal typealias Weiter = ([MediaItem]) -> Void func getDownloadJs() -> URL { @@ -37,171 +31,9 @@ class NetworkManager { let url = Bundle.main.url(forResource: "download.js", withExtension: nil)! return url } - return URL(string: "http://l inkstation:8089/ren/web/download.js")! - } - - func saveFavDir(name: String, item: MediaItem) -> Void { - let json = item.toJSON() - let url = FileHelper.getDocumentsDirectory().appendingPathComponent(name) - do { - print (url) - try json.write(to: url, atomically: true, encoding: .utf8) - let input = try String(contentsOf: url) - print(input) - } catch { - print(json) - } + return URL(string: "http://linkstation:8089/ren/web/download.js")! } - func loadSettings() { - do { - let jsonData = try FileHelper.getData(name: "settings.json") - let model = try JSONDecoder().decode(KSettingsModel.self, from: jsonData) - settings = KSettings(model: model) - } catch { - print(error.localizedDescription) - } - } - - func saveSettings() { - let json = settings.toJSON() - let url = FileHelper.getDocumentsDirectory().appendingPathComponent("settings.json") - do { - print (url) - try json.write(to: url, atomically: true, encoding: .utf8) - let input = try String(contentsOf: url) - print(input) - } catch { - print(json) - } - } - - func loadFavDirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { - var res = [MediaItem]() - - var itemsMap = Dictionary() - - let fi = MediaItem(name: "images", path: "", root: "fav", type: ItemType.DETAILS) - fi.local = true - fi.loaded = true - - res.append(fi) - - let url = FileHelper.getDocumentsDirectory() - if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { - for case let fileURL as URL in enumerator { - do { - let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey]) - if fileAttributes.isRegularFile! { - if (fileURL.pathExtension == "jpg" && fileURL.absoluteString.contains("/images/")) { - let p = fileURL.absoluteString.substringAfter("/Documents") - - var path = p.substringBefore("/") - - let fp = fileURL.absoluteString.substringAfter("/Documents/images/") - - var fpath = fp.substringBefore("/") - - var folder = itemsMap[fpath] - - if folder == nil { - let f = MediaItem(name: fpath, path: "images", root: "fav", type: ItemType.PICS) - f.local = true - f.loaded = true - folder = f - itemsMap[fpath] = folder! - fi.children.append(f) - } - - let m = MediaItem(name: fileURL.lastPathComponent, path: path, root: "fav", type: ItemType.PICS) - m.local = true - m.loaded = true - m.thumbUrl = fileURL.absoluteString - - let jsonURL = fileURL.absoluteString.replacingOccurrences(of: ".jpg", with: ".json") - - do { - let jsonData = try Data(contentsOf: URL(string: jsonURL)!) - let item = try JSONDecoder().decode(MediaModel.self, from: jsonData) - m.scale = item.scale - m.offset = item.offset - m.size = item.size - } - catch { - - } - -// var folder = itemsMap[path] -// -// if folder == nil { -// folder = MediaItem(name: item.path, path: item.path, root: item.root, type: ItemType.FOLDER) -// folder!.loaded = true -// folder!.local = item.local -// itemsMap[path] = folder! -// items.append(folder!) -// } - - folder!.children.append(m) - - print(fileURL.absoluteString) - } - - if (fileURL.pathExtension == "mp4") { - print(fileURL.absoluteString) - - let p = fileURL.absoluteString.substringAfter("/Documents") - - var path = p.substringBefore("/") - - let m = MediaItem(name: fileURL.lastPathComponent, path: path, root: "fav", type: ItemType.VIDEO) - m.local = true - m.thumbUrl = fileURL.appendingPathExtension("jpg").absoluteString - - res.append(m) - - } - } - } catch { print(error, fileURL) } - } - } - - let m = MediaItem(name: "new", path: "new", root: "fav", type: ItemType.FOLDER) - m.local = true - res.append(m) - - completionHandler(res) - } - - func loadFav2Dirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { - let files = FileHelper.listFiles(name: "1") - var res = [MediaItem]() - for f in files { - if f.pathExtension == "mp4" { - let m = MediaItem(name: f.lastPathComponent, path: "", root: "1", type: ItemType.VIDEO) - - m.local = true - res.append(m) - } - } - - completionHandler(res) - } - - func loadFav1Dirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { - var res = [MediaItem]() - - do { - let jsonData = try FileHelper.getData(name: "fav.json") - let items = try JSONDecoder().decode(MediaModel.self, from: jsonData) - let loaded = MediaItem(model: items) - res = loaded.children - } catch { - print(error.localizedDescription) - } - - // res.append(currentFav) - completionHandler(res) - } func loadVideoDirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { var root = rootParam @@ -312,17 +144,6 @@ class NetworkManager { } } - func deleteLocal(selectedItem: MediaItem) { - if (selectedItem.local) { - print(selectedItem.playerURL?.absoluteString) - do { - try FileManager.default.removeItem(at: selectedItem.playerURL!) - } catch { - print(error) - } - } - } - func listDirs(_ root: String, completionHandler: @escaping ([MediaItem], Bool) -> Void) -> Void { let len = root.count let url = (root as NSString).replacingOccurrences(of: " ", with: "+") @@ -503,6 +324,13 @@ class NetworkManager { } } + func killFFMPEG() { + let url = nodeurl + "killmpeg" + AF.request(url).responseString { response in + print("kill \(response)") + } + } + func favItem(_ item: MediaItem) { let url = baseurl + "/service/linkfav" + item.fullPath @@ -537,27 +365,26 @@ class NetworkManager { } for c in item.children { - if let t = c.time { - let ms = Int(t * 1000) - let p = c.snapshotDirPathForVideo + "\(ms).jpg" - let pt = c.snapshotDirPathForVideo + "\(ms)_thumb.jpg" - print(p) - if hashes.contains(pt) { - print("contained") - } else { - if let id = c.image, let imageData = id.jpegData(compressionQuality: 1.0) { - let op = UploadOperation(baseUrl: self.nodeurl + "upload", data: imageData, path: p) - self.operationQueue.addOperation(op) - - let thumb = id.scaleToSize(15 * 16, height: 15 * 9) - if let imageDataT = thumb.jpegData(compressionQuality: 1.0) { - c.image = thumb - let opT = UploadOperation(baseUrl: self.nodeurl + "upload", data: imageDataT, path: pt) - self.operationQueue.addOperation(opT) - } - - c.loaded = true + let t = c.time + let ms = Int(t * 1000) + let p = c.snapshotDirPathForVideo + "\(ms).jpg" + let pt = c.snapshotDirPathForVideo + "\(ms)_thumb.jpg" + print(p) + if hashes.contains(pt) { + print("contained") + } else { + if let id = c.image, let imageData = id.jpegData(compressionQuality: 1.0) { + let op = UploadOperation(baseUrl: self.nodeurl + "upload", data: imageData, path: p) + self.operationQueue.addOperation(op) + + let thumb = id.scaleToSize(15 * 16, height: 15 * 9) + if let imageDataT = thumb.jpegData(compressionQuality: 1.0) { + c.image = thumb + let opT = UploadOperation(baseUrl: self.nodeurl + "upload", data: imageDataT, path: pt) + self.operationQueue.addOperation(opT) } + + c.loaded = true } } } @@ -615,17 +442,24 @@ class NetworkManager { } func downloadToServer(path: String, url: URL, result: @escaping (String) -> ()) { - let name = path + "/" + url.lastPathComponent - let queryItems = [URLQueryItem(name: "url", value: url.absoluteString), URLQueryItem(name: "name", value: name)] + let date = Date() + let format = DateFormatter() + format.dateFormat = "yyyyMMdd_HHmmss" + let timestamp = format.string(from: date) let cmd: String + let name: String if url.pathExtension == "m3u8" { cmd = "ffmpeg" + name = path + "/" + timestamp + "_" + url.lastPathComponent } else { cmd = "webdl" + name = path + "/" + url.lastPathComponent } + let queryItems = [URLQueryItem(name: "url", value: url.absoluteString), URLQueryItem(name: "name", value: name)] + var urlComps = URLComponents(string: nodeurl + cmd)! urlComps.queryItems = queryItems let p = urlComps.url! diff --git a/kplayer/detail/BrowserController.swift b/kplayer/detail/BrowserController.swift index f2aeacb..700187b 100644 --- a/kplayer/detail/BrowserController.swift +++ b/kplayer/detail/BrowserController.swift @@ -196,8 +196,12 @@ class BrowserController : UIViewController, ItemController, WebBrowserDelegate, return } - if (url2.pathExtension == "mp4" && dl) { - NetworkManager.sharedInstance.downloadToServer(path: self.currentItem!.root, url: url2, result: { + let name = url2.lastPathComponent + let hostcomp = currentItem!.name.split(separator: ".") + let site = String(hostcomp[hostcomp.count-2]) + + if ((url2.pathExtension == "mp4" || url2.pathExtension == "m3u8") && dl) { + NetworkManager.sharedInstance.downloadToServer(path: site, url: url2, result: { (r) in print(r) self.showAlert(title: "download ready", message: r) @@ -208,12 +212,8 @@ class BrowserController : UIViewController, ItemController, WebBrowserDelegate, return } - let name = url2.lastPathComponent - let vc = VideoController() - let hostcomp = currentItem!.name.split(separator: ".") - let site = String(hostcomp[hostcomp.count-2]) let item = MediaItem(name: name, path: name, root: site, type: ItemType.VIDEO) item.externalURL = url diff --git a/kplayer/detail/DetailViewController.swift b/kplayer/detail/DetailViewController.swift index 545a863..4cffc28 100644 --- a/kplayer/detail/DetailViewController.swift +++ b/kplayer/detail/DetailViewController.swift @@ -18,6 +18,9 @@ protocol DetailDelegate { func deleteThumb(selectedItem: MediaItem) func deleteLocal(selectedItem: MediaItem) func saveItem(selectedItem: MediaItem) + func favItem(_ item: MediaItem) + func settings() -> KSettings + func offline() -> Bool } class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDragDelegate { @@ -36,7 +39,7 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout var detailItem: MediaItem? { didSet { - print(detailItem!.children) + // print(detailItem!.children) if collectionView != nil { collectionView.reloadData() } @@ -154,7 +157,7 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout } @objc func settings() { - let kv = KSettingsView(kSettings: NetworkManager.sharedInstance.settings, completionHandler: { + let kv = KSettingsView(kSettings: LocalManager.sharedInstance.settings, completionHandler: { self.dismiss(animated: true, completion: nil); } ) let pc = UIHostingController(rootView: kv) @@ -205,6 +208,7 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout let item = MediaItem(name: item.name, path: item.path, root: item.root, type: ItemType.VIDEO) + vc.detailDelegate = delegate vc.setItems(items: [item]) vc.setCurrentItem(item: item) vc.urls = assets @@ -483,26 +487,25 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout } } - var pc: ItemController? + var pc: VideoController pc = VideoController() + pc.detailDelegate = delegate - pc!.setCurrentItem(item: se) - pc!.setItems(items: children) - pc!.setCompletionHandler(handler: { + pc.setCurrentItem(item: se) + pc.setItems(items: children) + pc.setCompletionHandler(handler: { self.collectionView.reloadData() self.collectionView.collectionViewLayout.invalidateLayout() + self.delegate!.saveItem(selectedItem: se) - if !se.local { - self.delegate!.saveItem(selectedItem: se) - } self.dismiss(animated: true, completion: nil); }) - let navController = UINavigationController(rootViewController: (pc! as! UIViewController)) + let navController = UINavigationController(rootViewController: pc) navController.modalPresentationStyle = .fullScreen navController.modalPresentationCapturesStatusBarAppearance = true navController.navigationBar.barTintColor = UIColor.black - (pc! as! UIViewController).navigationItem.leftItemsSupplementBackButton = true + pc.navigationItem.leftItemsSupplementBackButton = true self.present(navController, animated: false, completion: nil) diff --git a/kplayer/detail/EditItemView.swift b/kplayer/detail/EditItemView.swift new file mode 100644 index 0000000..a96b347 --- /dev/null +++ b/kplayer/detail/EditItemView.swift @@ -0,0 +1,46 @@ +// +// Created by Marco Schmickler on 12.11.21. +// Copyright (c) 2021 Marco Schmickler. All rights reserved. +// + +import Foundation +import SwiftUI + +struct EditItemView : View { + @ObservedObject + var item : MediaItem + + var body: some View { + Form { + Section(header: Text("K Settings")) { + VStack { + Text("Size") + Slider(value: $item.time, in: 0...1000) + Slider(value: $item.length, in: 0...1000) + Toggle(isOn: $item.loop, label: { + Text("Loop") + }) + } + } + Button(action: { + + }, label: { + Text("ok") + }); + Button(action: { + + }, label: { + Text("cancel") + }) + } + .onAppear { + } + } +} + +struct EditItemView_Previews: PreviewProvider { + static var previews: some View { + EditItemView(item: MediaItem(name: "extern", path:"", root: "", type: ItemType.FAVROOT)) + } +} + diff --git a/kplayer/detail/ItemCell.swift b/kplayer/detail/ItemCell.swift index 4f4cd2c..31a0a5e 100644 --- a/kplayer/detail/ItemCell.swift +++ b/kplayer/detail/ItemCell.swift @@ -42,7 +42,7 @@ class ItemCell: UICollectionViewCell { self.item = item if item.type == ItemType.DOWNLOAD { - progress.progress = Float(item.length!) + progress.progress = Float(item.length) return } diff --git a/kplayer/detail/VideoController.swift b/kplayer/detail/VideoController.swift index eae248b..9202dfa 100644 --- a/kplayer/detail/VideoController.swift +++ b/kplayer/detail/VideoController.swift @@ -7,6 +7,17 @@ import Foundation import UIKit import Haneke import AVFoundation +import SwiftUI + +protocol DownloadDelegate { + func killFFMPEG() + + func dlserverlen(result: @escaping (String) -> ()) + func download(url: URL, path: String, result: @escaping (URL) -> () ) + func downloadToServer(path: String, url: URL, result: @escaping (String) -> ()) + + func inProgress() -> Int +} protocol ItemController { func setCurrentItem(item: MediaItem) @@ -21,6 +32,8 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate { var currentItem: MediaItem? var currentSnapshot: MediaItem? var allItems = [MediaItem]() + var detailDelegate: DetailDelegate? + var downloadDelegate: DownloadDelegate? var loopStart = 0.0 var loopEnd = 0.0 @@ -51,6 +64,8 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate { var allowEdit = true var index = 0 + var ffmpeg = false + var urls : [URL]? override func viewDidLoad() { @@ -94,6 +109,16 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate { updateLoop() } + func editItem() { + let kv = EditItemView(item: currentSnapshot!) + let pc = UIHostingController(rootView: kv) + let navController = UINavigationController(rootViewController: pc) // Creating a navigation controller with pc at the root of the navigation stack. + // navController.modalPresentationStyle = .fullScreen + + present(navController, animated: false, completion: nil) + } + + override var prefersStatusBarHidden: Bool { return true } @@ -138,11 +163,13 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate { } @objc func favorite(_ sender: AnyObject) { + downloadDelegate?.killFFMPEG() + print("favorite") - let inProgress = NetworkManager.sharedInstance.currentDownloads.count + let inProgress = downloadDelegate?.inProgress() let alertController = UIAlertController(title: "In Progress: \(inProgress)", message: "Download", preferredStyle: .alert) - NetworkManager.sharedInstance.dlserverlen { c in + downloadDelegate?.dlserverlen { c in alertController.title = "On Server: \(c)"; } @@ -165,7 +192,7 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate { let downloadAction = UIAlertAction(title: "Download to fav", style: .default) { (action) in let url = self.currentItem!.playerURL - NetworkManager.sharedInstance.download(url: url!, path: "download") { url in + self.downloadDelegate?.download(url: url!, path: "download") { url in self.showAlert(title: url.lastPathComponent, message: "ready") } } @@ -177,14 +204,17 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate { let serverAction = UIAlertAction(title: "Download to server", style: .default) { (action) in let url = self.currentItem!.playerURL - NetworkManager.sharedInstance.downloadToServer(path: self.currentItem!.root, url: url!, result: { - (r) in - print(r) - self.showAlert(title: "download ready", message: r) - if (r == "exists") { + if url!.pathExtension == "m3u8" { + self.ffmpeg = true + } + self.downloadDelegate?.downloadToServer(path: self.currentItem!.root, url: url!, result: { + (r) in + print(r) + self.showAlert(title: "download ready", message: r) + if (r == "exists") { - } - }) + } + }) } alertController.addAction(serverAction) @@ -351,7 +381,7 @@ print("play") let r = BMPlayerResourceDefinition(url: i.playerURL!, definition: i.name); def.append(r) - if (url == i.playerURL) { + if (LocalManager.sharedInstance.fixDocumentURL(url.absoluteString) == LocalManager.sharedInstance.fixDocumentURL(i.playerURL?.absoluteString)) { index = count } count += 1 @@ -410,10 +440,14 @@ print("finish") @objc func thumbnailClicked(_ source: UIButton) { currentSnapshot = buttons[source] - player.seek(buttons[source]!.time!) - loopStart = buttons[source]!.time! + player.seek(buttons[source]!.time) + loopStart = buttons[source]!.time loopOption = 0 loopButton!.title = "loop" + + if (edit) { + // editItem() + } // moviePlayer!.currentPlaybackRate = Float(speedOptions[speedOption]) // print("goto \(buttons[source]!.time!) is \(moviePlayer!.currentPlaybackTime)") @@ -424,16 +458,16 @@ print("finish") reviewButton!.title = currentItem!.name if currentItem!.type == ItemType.SNAPSHOT { - player.seek(currentItem!.time!) - if NetworkManager.sharedInstance.settings.autoloop && currentItem!.loop { - loopStart = currentItem!.time! - loopEnd = currentItem!.time! + currentItem!.length! + player.seek(currentItem!.time) + if detailDelegate!.settings().autoloop && currentItem!.loop { + loopStart = currentItem!.time + loopEnd = currentItem!.time + currentItem!.length loopOption = 1 } currentItem = currentItem!.parent } else { if !currentItem!.children.isEmpty { - player.seek(currentItem!.children[0].time!) + player.seek(currentItem!.children[0].time) } else { let duration = player.playerLayer!.playerItem!.duration @@ -617,7 +651,7 @@ print("finish") // timeOption: MPMovieTimeOption.exact); } else { - NetworkManager.sharedInstance.favItem(currentItem!) + detailDelegate!.favItem(currentItem!) } } @@ -626,9 +660,10 @@ print("finish") newItem.image = thumbnail newItem.time = time.seconds newItem.parent = currentItem! + newItem.local = currentItem!.local currentItem!.children.append(newItem) - print(newItem.time!) + print(newItem.time) addItemButton(newItem) } diff --git a/kplayer/master/KSettingsView.swift b/kplayer/master/KSettingsView.swift index 745e96d..9455bdd 100644 --- a/kplayer/master/KSettingsView.swift +++ b/kplayer/master/KSettingsView.swift @@ -24,13 +24,13 @@ struct KSettingsView: View { } } Button(action: { - NetworkManager.sharedInstance.saveSettings() + LocalManager.sharedInstance.saveSettings() self.completionHandler?() }, label: { Text("ok") }); Button(action: { - NetworkManager.sharedInstance.loadSettings() + LocalManager.sharedInstance.loadSettings() self.completionHandler?() }, label: { Text("cancel") diff --git a/kplayer/master/MasterViewController.swift b/kplayer/master/MasterViewController.swift index d365294..fa7a83d 100644 --- a/kplayer/master/MasterViewController.swift +++ b/kplayer/master/MasterViewController.swift @@ -18,7 +18,7 @@ protocol MasterDelegate : DetailDelegate { func loadItem(selectedItem: MediaItem, completionHandler: @escaping (MediaItem) -> Void) } -class MasterViewController: UITableViewController, UISearchResultsUpdating, UITableViewDropDelegate { +class MasterViewController: UITableViewController, UISearchResultsUpdating, UITableViewDropDelegate, UIDocumentPickerDelegate { let searchController = UISearchController(searchResultsController: nil) let model = ItemModel() var delegate : MasterDelegate? @@ -27,11 +27,13 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa var authenticated = false + var currentSelection : MediaItem? + override func awakeFromNib() { super.awakeFromNib() if UIDevice.current.userInterfaceIdiom == .pad { - self.clearsSelectionOnViewWillAppear = false - self.preferredContentSize = CGSize(width: 320.0, height: 600.0) + clearsSelectionOnViewWillAppear = false + preferredContentSize = CGSize(width: 320.0, height: 600.0) } } @@ -77,7 +79,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Only awesome people are allowed", reply: { [unowned self] (success, error) -> Void in - NetworkManager.sharedInstance.authenticated = success + LocalManager.sharedInstance.authenticated = success }) } @@ -123,12 +125,13 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("\(indexPath.row)") - if (!NetworkManager.sharedInstance.authenticated) { + if (!LocalManager.sharedInstance.authenticated) { doAuthenticate() return } let selectedItem = model.items[indexPath.row] + currentSelection = selectedItem if (selectedItem.local && selectedItem.name == "new") { createFolder(selectedItem) @@ -143,6 +146,19 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa if (selectedItem.isFolder()) { print(selectedItem.path) + if (selectedItem.externalURL == nil && selectedItem.type == .FAVROOT) { + selectedItem.externalURL = FileHelper.getDocumentsDirectory().absoluteString + + if selectedItem.name == "extern" { + let documentPicker = + UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) + documentPicker.delegate = self + + present(documentPicker, animated: true, completion: nil) + return + } + } + delegate!.loadFolder(selectedItem: selectedItem) { (neu) in if neu.isFolder() { @@ -155,6 +171,38 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } } + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + print("hello") + + if urls.count > 0 { + let url = urls[0] + + guard url.startAccessingSecurityScopedResource() else { + // Handle the failure here. + return + } + + currentSelection?.externalURL = url.absoluteString + LocalManager.sharedInstance.externalURL = url.absoluteString + + delegate!.loadFolder(selectedItem: currentSelection!) { + (neu) in + if neu.isFolder() { + self.gotoNextFolder(neu) + } else { + self.gotoDetails(neu) + } + } + } + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { + print("hello") + } + fileprivate func gotoNextFolder(_ selectedItem: MediaItem) { let mainStoryboard = UIStoryboard(name: "Main", bundle: nil) let vc = mainStoryboard.instantiateViewController(withIdentifier: "mastertable") as! MasterViewController @@ -311,7 +359,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa let weiter:Weiter = { (g) in - let fav = NetworkManager.sharedInstance.favorites + let fav = LocalManager.sharedInstance.favorites ItemModel().sortItems(selectedItem: fav, children: g) self.model.items = fav.children @@ -322,7 +370,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } } - NetworkManager.sharedInstance.loadFavDirs("", completionHandler: weiter) + LocalManager.sharedInstance.loadFavDirs(FileHelper.getDocumentsDirectory(), completionHandler: weiter) } catch { print(error) } diff --git a/kplayer/master/NetworkDelegate.swift b/kplayer/master/NetworkDelegate.swift index a694f2d..ec7c377 100644 --- a/kplayer/master/NetworkDelegate.swift +++ b/kplayer/master/NetworkDelegate.swift @@ -45,7 +45,10 @@ class NetworkDelegate: MasterDelegate, DetailDelegate { } if selectedItem.type == ItemType.FAVROOT { - NetworkManager.sharedInstance.loadFavDirs("dir", completionHandler: weiter) + let url = URL(string: selectedItem.externalURL!) + + LocalManager.sharedInstance.loadFavDirs(url!, completionHandler: weiter) + return } @@ -78,21 +81,44 @@ class NetworkDelegate: MasterDelegate, DetailDelegate { } func deleteThumb(selectedItem c: MediaItem) { - if let t = c.time { - let ms = Int(t * 1000) - let p = c.snapshotDirPathForVideo + "\(ms).jpg" - let pt = c.snapshotDirPathForVideo + "\(ms)_thumb.jpg" + let t = c.time + let ms = Int(t * 1000) + let p = c.snapshotDirPathForVideo + "\(ms).jpg" + let pt = c.snapshotDirPathForVideo + "\(ms)_thumb.jpg" - NetworkManager.sharedInstance.deleteThumb(p) - NetworkManager.sharedInstance.deleteThumb(pt) - } + NetworkManager.sharedInstance.deleteThumb(p) + NetworkManager.sharedInstance.deleteThumb(pt) } func saveItem(selectedItem: MediaItem) { - NetworkManager.sharedInstance.saveItem(selectedItem) + var item = selectedItem + + if (selectedItem.local || (selectedItem.parent?.local != nil )) { + if item.type == ItemType.SNAPSHOT { + item = selectedItem.parent! + } + if let name = item.externalURL { + LocalManager.sharedInstance.saveFavDir(url: URL(string: name)!, item: item) + } + } + else { + NetworkManager.sharedInstance.saveItem(selectedItem) + } + } + + func favItem(_ item: MediaItem) { + NetworkManager.sharedInstance.favItem(item) } func deleteLocal(selectedItem: MediaItem) { - NetworkManager.sharedInstance.deleteLocal(selectedItem: selectedItem) + LocalManager.sharedInstance.deleteLocal(selectedItem: selectedItem) + } + + func settings() -> KSettings { + LocalManager.sharedInstance.settings + } + + func offline() -> Bool { + NetworkManager.sharedInstance.offline } } diff --git a/kplayer/master/ServerDownloadDelegate.swift b/kplayer/master/ServerDownloadDelegate.swift new file mode 100644 index 0000000..08ba1b5 --- /dev/null +++ b/kplayer/master/ServerDownloadDelegate.swift @@ -0,0 +1,30 @@ +// +// Created by Marco Schmickler on 13.11.21. +// Copyright (c) 2021 Marco Schmickler. All rights reserved. +// + +import Foundation + +class ServerDownloadDelegate : DownloadDelegate { + func killFFMPEG() { + NetworkManager.sharedInstance.killFFMPEG() + print("killffmpeg") + } + + func dlserverlen(result: @escaping (String) -> ()) { + NetworkManager.sharedInstance.dlserverlen(result: result) + } + + func download(url: URL, path: String, result: @escaping (URL) -> ()) { + NetworkManager.sharedInstance.download(url: url, path: path, result: result) + } + + func downloadToServer(path: String, url: URL, result: @escaping (String) -> ()) { + NetworkManager.sharedInstance.downloadToServer(path: path, url: url, result: result) + } + + func inProgress() -> Int { + NetworkManager.sharedInstance.currentDownloads.count + } + +} diff --git a/kplayer/photo/PhotoController.swift b/kplayer/photo/PhotoController.swift index 6e95a7d..7823bd1 100644 --- a/kplayer/photo/PhotoController.swift +++ b/kplayer/photo/PhotoController.swift @@ -101,7 +101,8 @@ class MediaPhotoController: NIToolbarPhotoViewController, NIPhotoAlbumScrollView currentItem.size = scroll.contentSize let jsonFile = currentItem.path + "/" + currentItem.name.replacingOccurrences(of: ".jpg", with: ".json") - NetworkManager.sharedInstance.saveFavDir(name: jsonFile, item: currentItem) + let url = FileHelper.getDocumentsDirectory().appendingPathComponent(jsonFile) + LocalManager.sharedInstance.saveFavDir(url: url, item: currentItem) // print(currentItem.toJSON()) scroll.isScrollEnabled = false diff --git a/kplayer/server/kplayer.js b/kplayer/server/kplayer.js index 07e6a1e..071f6c0 100644 --- a/kplayer/server/kplayer.js +++ b/kplayer/server/kplayer.js @@ -1,10 +1,32 @@ +// pm2 restart kplayer // /srv/samba/daten/node -// pm2 list - var express = require('express'); var fs = require("fs"); var path = require("path"); +var queue = require('queue'); +var wget = require('wget-improved'); + +var downloadUrl = function(url, dest) { + return function(cb) { + try { + // you can also change the mapping url to file here by changing the function + var download = wget.download(url, dest, {}); + download.on('end', function() { + () => console.log("done downloading") + }); + } catch (e) { + + } + }; +}; + +var q = queue(); +//change this one as needed. +q.concurrency = 2; + +//q.start(); + const { exec } = require('child_process'); @@ -111,7 +133,7 @@ app.get('/alive', function (req, res) { app.get('/ffmpeg', function (req, res) { var p = req.query.url - var n = "/srv/samba/ren/webdl/" + req.query.name + var n = "/srv/samba/ren/webdl" + req.query.name + ".mp4" var cmd = 'ffmpeg -i "'+p+'" -codec copy "'+n+'"' console.log(cmd) @@ -129,10 +151,70 @@ app.get('/ffmpeg', function (req, res) { res.send(p) }) +app.get('/wgetcount', function (req, res) { + var cmd = 'ps -fA | grep wget | wc -l' + console.log(cmd) + + const execSync = require('child_process').execSync; + var code = execSync(cmd); + + res.end(code) +}) + +app.get('/killffmpeg', function (req, res) { + var cmd = 'kill `pidof ffmpeg`' + console.log(cmd) + + const execSync = require('child_process').execSync; + exec(cmd, (err, stdout, stderr) => { + // the *entire* stdout and stderr (buffered) + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + + if (err) { + console.log(err) + // node couldn't execute the command + return; + } + + }); + res.end(cmd) +}) + app.get('/webdl', function (req, res) { var p = req.query.url var n = "/srv/samba/ren/webdl/" + req.query.name + const lastSegment = n.split("/").pop(); + + console.log(lastSegment); + + if (fs.existsSync(n)) { + res.send("exists") + return + } + + if (fs.existsSync("/srv/samba/ren/work/facial2/"+lastSegment)) { + res.send("exists") + return + } + if (fs.existsSync("/srv/samba/ren/knk_archiv3/facial/"+lastSegment)) { + res.send("exists") + return + } + if (fs.existsSync("/srv/samba/ren/knk_archiv4/face/"+lastSegment)) { + res.send("exists") + return + } + if (fs.existsSync("/srv/samba/ren/knk_archiv4/sub/"+lastSegment)) { + res.send("exists") + return + } + if (fs.existsSync("/srv/samba/ren/knk_archiv4/sub2/"+lastSegment)) { + res.send("exists") + return + } + var cmd = 'wget -b -O "'+n+'" "'+p+'"' console.log(cmd) exec(cmd, (err, stdout, stderr) => { @@ -149,6 +231,20 @@ app.get('/webdl', function (req, res) { res.send(p) }) +app.get('/pushdl', function (req, res) { + var p = req.query.url + var n = "/srv/samba/ren/webdl/" + req.query.name + + q.push(downloadUrl(p,n)); + console.log("all urls pushed"); + +// q.start() +}) + +app.get('/queuelen', function (req, res) { + res.end("Len" + q.length) +}) + app.get('/deleteThumb/*', function (req, res) { if (req.path.startsWith("/deleteThumb/srv/samba/ren/thumb/")) { var p = req.path.substr(12) diff --git a/kplayer/server/links.html b/kplayer/server/links.html new file mode 100644 index 0000000..690acaa --- /dev/null +++ b/kplayer/server/links.html @@ -0,0 +1,15 @@ + + +Google +XVID +XVID +gustav29 veberle12a +gustav29 ms39084st +XVID +XVID +XVID +XVID + + + + diff --git a/kplayer/timeline/CenterLine.swift b/kplayer/timeline/CenterLine.swift new file mode 100644 index 0000000..c33fd5a --- /dev/null +++ b/kplayer/timeline/CenterLine.swift @@ -0,0 +1,126 @@ +// +// CenterLine.swift +// Examplay +// +// Created by Tomohiro Yamashita on 2020/03/09. +// Copyright © 2020 Tom. All rights reserved. +// + +import UIKit +import AVFoundation + +class CenterLine: UIView { + + var mainView:VideoTimelineView! + + let timeLabel = UILabel() + var parentView:TimelineView? = nil + var duration:Float64 = 0 + var currentTime:Float64 = 0 + let margin:CGFloat = 6 + + override init (frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + self.isUserInteractionEnabled = false + } + + required init(coder aDecoder: NSCoder) { + fatalError("CenterLine init(coder:) has not been implemented") + } + + func configure(parent:TimelineView) { + parentView = parent + self.setNeedsDisplay() + + self.timeLabel.adjustsFontSizeToFitWidth = true + self.timeLabel.textAlignment = .center + + self.timeLabel.text = String("00:00.00") + + + self.addSubview(timeLabel) + } + + func update() { + self.setNeedsDisplay() + let textMargin = margin + 3 + self.timeLabel.frame.size.width = self.bounds.size.width - textMargin * 2 + self.timeLabel.frame.origin = CGPoint(x:textMargin,y:0) + self.timeLabel.textColor = UIColor(hue: 0.94, saturation:0.68, brightness:0.95, alpha: 0.94) + setTimeText() + timeLabel.font = UIFont(name:"HelveticaNeue-CondensedBold" ,size:timeLabel.frame.size.height * 0.9) + + } + + + override func draw(_ rect: CGRect) { + + + gradient() + + let context = UIGraphicsGetCurrentContext()! + context.saveGState() + context.setShadow(offset: CGSize(width: 0,height: 0), blur: 6, color: UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.30).cgColor) + + UIColor(hue: 0, saturation:0, brightness:1, alpha: 0.92).setFill() + + let labelRect = CGRect(x: margin,y: 0.3,width: self.frame.size.width - margin * 2,height: timeLabel.frame.size.height + 1) + let rectPath = UIBezierPath(roundedRect:labelRect, cornerRadius:timeLabel.frame.size.height) + rectPath.fill() + context.restoreGState() + + + + let path = UIBezierPath() + path.move(to: CGPoint(x: self.frame.size.width / 2 , y:timeLabel.frame.size.height)) + path.addLine(to: CGPoint(x:self.frame.size.width / 2 , y:self.frame.size.height)) + path.lineWidth = 1.4 + UIColor(hue: 0, saturation:0, brightness:1.0, alpha: 0.7).setStroke() + path.stroke() + + } + + + func gradient() { + let width = self.frame.size.width + + + let context = UIGraphicsGetCurrentContext()! + + let startColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.06).cgColor + let endColor = UIColor.clear.cgColor + let colors = [startColor, endColor] as CFArray + + let locations = [0, 1] as [CGFloat] + + let space = CGColorSpaceCreateDeviceRGB() + + let gradient = CGGradient(colorsSpace: space, colors: colors, locations: locations)! + context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) - 0.7, y:0), end: CGPoint(x: (width / 2) - 4, y: 0), options: []) + context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) + 0.7, y:0), end: CGPoint(x: (width / 2) + 4, y: 0), options: []) + } + + var ignoreSendScrollToParent = false + func setScrollPoint(_ scrollPoint:CGFloat) { + currentTime = Float64(scrollPoint) * duration + setTimeText() + if !ignoreSendScrollToParent { + if let parent = parentView { + parent.moved(currentTime) + } + } + + mainView!.currentTime = currentTime + ignoreSendScrollToParent = false + } + + func setTimeText() { + let minute = Int(currentTime / 60) + let second = (currentTime - Float64(minute) * 60) + let milliSec = Int((second - Float64(Int(second))) * 100) + let text = String(format: "%02d:%02d.%02d", minute, (Int(second)), milliSec) + timeLabel.text = text + } +} diff --git a/kplayer/timeline/FrameImagesView.swift b/kplayer/timeline/FrameImagesView.swift new file mode 100644 index 0000000..366c348 --- /dev/null +++ b/kplayer/timeline/FrameImagesView.swift @@ -0,0 +1,451 @@ +// +// FrameImagesView.swift +// Examplay +// +// Created by Tomohiro Yamashita on 2020/03/01. +// Copyright © 2020 Tom. All rights reserved. +// + +import Foundation +import UIKit +import AVFoundation + + + +class FrameImage: UIImageView { + var tolerance:Float64? = nil +} + + +// MARK: - FrameImagesView +class FrameImagesView: UIScrollView { + + + var mainView:VideoTimelineView! + + var frameImagesArray:[FrameImage] = [] + + + var thumbnailFrameSize:CGSize = CGSize(width: 640,height: 480) + let preferredTimescale:Int32 = 100 + var timeTolerance = CMTimeMakeWithSeconds(10 , preferredTimescale:100) + + var maxWidth:CGFloat = 0 + var minWidth:CGFloat = 0 + + var parentScroller:TimelineScroller? = nil + + override init (frame: CGRect) { + super.init(frame: frame) + self.isScrollEnabled = false + self.isUserInteractionEnabled = false + self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.02) + } + + required init(coder aDecoder: NSCoder) { + fatalError("FrameImagesView init(coder:) has not been implemented") + } + + + + + func reset() { + discardAllFrameImages() + cancelImageGenerator() + + prepareFrameViews() + layout() + requestVisible(depth:0, wide:2, direction:0) + } + + //MARK: - timer for animation + var animationTimer = Timer() + var animating = false + func startAnimation() { + animating = true + displayFrames() + animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) + RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) + + } + + func stopAnimation() { + animationTimer.invalidate() + animating = false + + + if let parent = parentScroller { + frame.origin = CGPoint(x: parent.frame.size.width / 2, y: parent.measureHeight) + parent.addSubview(self) + } + displayFrames() + } + + @objc func animate(_ timer:Timer) { + if animating == false { + return + } + displayFrames() + } + + //MARK: - layout + + + var uponFrames = Set() + var belowFrames = Set() + var deepFrames = Set() + var hiddenFrames = Set() + + + func layout() { + setThumnailFrameSize() + let coordinated = coordinateFrames() + uponFrames = coordinated.upon + belowFrames = coordinated.below + hiddenFrames = coordinated.hidden + deepFrames = coordinated.deep + displayFrames() + } + + func displayFrames() { + + var baseView:UIView = self + var offset:CGPoint = CGPoint.zero + + if let parent = parentScroller { + let visibleHalf = parent.frame.size.width / 2 + + if animating { + if let layer = parent.layer.presentation() { + offset.x = visibleHalf - layer.bounds.origin.x + offset.y = parent.frame.origin.y + parent.measureHeight + frame.origin = offset + mainView!.timelineView.viewForAnimate.addSubview(self) + } + } + } + + let visibleSide = indexOfVisibleSide() + func locate(_ index:Int, visible:Bool) { + if frameImagesArray.count > index && index >= 0 { + let frameImg = frameImagesArray[index] + if index >= visibleSide.left && index <= visibleSide.right { + let position = positionWithIndex(index) + frameImg.frame = CGRect(x: position, y:0, width: thumbnailFrameSize.width, height: thumbnailFrameSize.height) + if visible { + self.addSubview(frameImg) + } else { + frameImg.removeFromSuperview() + } + } else { + frameImg.removeFromSuperview() + } + + } + } + for element in belowFrames {//do addSubview under the upon + locate(element, visible:true) + } + for element in uponFrames { + locate(element, visible:true) + } + for element in hiddenFrames {// includes deep + locate(element, visible:false) + } + + } + + func positionWithIndex(_ index:Int) -> CGFloat { + let position = frame.size.width * (CGFloat(index) / CGFloat(thumbnailCountFloat())) + return position + } + + func coordinateFrames() -> (upon:Set, below:Set, hidden:Set, deep:Set) { + + let keyDivision = Int(pow(2,Double(Int(log2(maxWidth * 2 / (self.frame.size.width + 1)))))) + if keyDivision <= 0 { + return (Set(), Set(), Set(), Set()) + } + let keyCount = ((frameImagesArray.count - 1) / keyDivision) + 1 + + + var visibleIndexes = Set() + var uponElements = Set() + for index in 0 ... (keyCount - 1) { + let uponIndex = index * keyDivision + uponElements.insert(uponIndex) + visibleIndexes.insert(uponIndex) + } + var belowElements = Set() + let belowDivision = keyDivision / 2 + if belowDivision >= 1 { + for index in 0 ..< (keyCount - 1) { + let belowIndex = (index * keyDivision) + (keyDivision / 2) + belowElements.insert(belowIndex) + visibleIndexes.insert(belowIndex) + } + } + var deepElements = Set() + let deepDivision = belowDivision / 2 + if deepDivision >= 1 { + if let max = visibleIndexes.max() { + for index in visibleIndexes { + if max > index { + deepElements.insert(index + deepDivision) + } + } + } + } + var hiddenElements = Set() + for index in 0 ..< frameImagesArray.count { + if !visibleIndexes.contains(index) { + let hiddenIndex = index + hiddenElements.insert(hiddenIndex) + } + } + return (uponElements,belowElements,hiddenElements, deepElements) + } + + + func setThumnailFrameSize() { + if let asset = mainView!.asset { + var frameSize = CGSize(width: 640,height: 480) + let tracks:Array = asset.tracks(withMediaType:AVMediaType.video) + if tracks.count > 0 { + let track = tracks[0] + frameSize = track.naturalSize.applying(track.preferredTransform) + frameSize.width = abs(frameSize.width) + frameSize.height = abs(frameSize.height) + } + thumbnailFrameSize = mainView!.resizeHeightKeepRatio(frameSize, height:self.frame.size.height) + } + } + + func assetDuration() -> Float64? { + if let asset = mainView!.asset { + return CMTimeGetSeconds(asset.duration) + } + return nil + } + + func prepareFrameViews() { + frameImagesArray = [] + for _ in 0 ... Int(thumbnailCountFloat()) { + let view = FrameImage() + view.alpha = 0 + frameImagesArray += [view] + } + } + + + + func thumbnailCountFloat() -> CGFloat { + return maxWidth / thumbnailFrameSize.width + } + + + func indexWithTime(_ time:Float64) -> Int? { + if let assetDuration = assetDuration() { + let value = time / (assetDuration / Float64(thumbnailCountFloat())) + var intValue = Int(value) + if value - Float64(intValue) >= 0.5 { + intValue += 1 + } + return intValue + } + return nil + } + + func timeWithIndex(_ index:Int) -> Float64 { + if let assetDuration = assetDuration() { + return assetDuration * ((Float64(index)) / Float64(thumbnailCountFloat())) + } + return 0 + } + + + + + //MARK: - frame images + func cancelImageGenerator() { + if let asset = mainView!.asset { + let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) + assetImgGenerate.cancelAllCGImageGeneration() + } + } + + func discardAllFrameImages() { + + for index in 0 ..< frameImagesArray.count { + let view = frameImagesArray[index] + UIView.animate(withDuration: 0.5,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in + + view.alpha = 0 + + },completion: { finished in + + view.image = nil + view.removeFromSuperview() + view.tolerance = nil + }) + } + frameImagesArray = [] + } + + + + func requestAll(){ + + var timesArray = [NSValue]() + for index in 0 ..< frameImagesArray.count { + timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))] + } + requestImageGeneration(timesArray:timesArray) + } + + + var requesting = Set() + func requestVisible(depth:Int, wide:Float, direction:Float) { + var timesArray = [NSValue]() + func request(_ index:Int) { + + if self.requesting.count > 0 && self.requesting.contains(index) { + return + } + + if frameImagesArray.count > index { + let imageView = frameImagesArray[index] + var needsUpdate = false + if let tolerance = imageView.tolerance { + if tolerance > CMTimeGetSeconds(timeTolerance) * 1.2 { + needsUpdate = true + } + } + if imageView.image == nil || needsUpdate { + timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))] + self.requesting.insert(index) + } + } + } + let visibleSide = indexOfVisibleSide() + let width = visibleSide.right - visibleSide.left + var additionLeft = Int(Float(width) * wide) + var additionRight = additionLeft + if direction > 0 { + additionLeft = Int(Float(-width) * direction) + } else if direction < 0 { + additionRight = Int(Float(width) * direction) + } + + + for index in uponFrames { + if index >= visibleSide.left - additionLeft && index <= visibleSide.right + additionRight { + request(index) + } + } + let belowAddLeft = Int(Float(additionLeft) * 0.5) - Int(Float(width) * 0.7) + let belowAddRight = Int(Float(additionRight) * 0.5) - Int(Float(width) * 0.7) + if depth > 0 { + for index in belowFrames { + if index >= visibleSide.left - belowAddLeft && index <= visibleSide.right + belowAddRight { + request(index) + } + } + } + let deepAddLeft = Int(Float(additionLeft) * 0.2) - Int(Float(width) * 0.4) + let deepAddRight = Int(Float(additionRight) * 0.2) - Int(Float(width) * 0.4) + if depth > 1 { + for index in deepFrames { + if index >= visibleSide.left - deepAddLeft && index <= visibleSide.right + deepAddRight { + request(index) + } + } + } + if timesArray.count > 0 { + requestImageGeneration(timesArray:timesArray) + } + } + + func indexOfVisibleSide() -> (left:Int, right:Int) { + func indexWithPosition(_ position:CGFloat) -> Int{ + let index = position * CGFloat(thumbnailCountFloat()) / frame.size.width + var indexInt = Int(index) + if index - CGFloat(indexInt) > 0.5 { + indexInt += 1 + } + return indexInt + } + var visibleLeft = indexWithPosition(-(parentScroller!.frame.width / 2) + parentScroller!.contentOffset.x - thumbnailFrameSize.width) - 1 + var visibleRight = indexWithPosition(parentScroller!.contentOffset.x + (parentScroller!.frame.size.width * 0.5) + thumbnailFrameSize.width) + if visibleLeft < 0 { + visibleLeft = 0 + } + let max = frameImagesArray.count - 1 + if visibleRight > max { + visibleRight = max + } + return (visibleLeft, visibleRight) + } + + func updateTolerance() { + if mainView!.asset == nil { + return + } + let thumbDuration = Float64(thumbnailFrameSize.width / self.frame.size.width) * mainView!.duration * 2 + timeTolerance = CMTimeMakeWithSeconds(thumbDuration , preferredTimescale:100) + + + } + + func requestImageGeneration(timesArray:[NSValue]) { + + if let asset = mainView!.asset { + let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) + assetImgGenerate.appliesPreferredTrackTransform = true + let maxsize = CGSize(width: thumbnailFrameSize.width * 1.5,height: thumbnailFrameSize.height * 1.5) + assetImgGenerate.maximumSize = maxsize + assetImgGenerate.requestedTimeToleranceAfter = timeTolerance + assetImgGenerate.requestedTimeToleranceBefore = timeTolerance + + assetImgGenerate.generateCGImagesAsynchronously(forTimes: timesArray, + completionHandler: + { time,resultImage,actualTime,result,error in + + let timeValue = CMTimeGetSeconds(time) + if let image = resultImage { + DispatchQueue.main.async { + self.setFrameImage(image:UIImage(cgImage:image), time:timeValue) + } + } + if let index = self.indexWithTime(timeValue) { + self.requesting.remove(index) + } + }) + } + } + + + func setFrameImage(image:UIImage, time:Float64) { + + if let index = indexWithTime(time) { + if frameImagesArray.count > index && index >= 0 { + let imageView = frameImagesArray[index] + + imageView.image = image + imageView.tolerance = CMTimeGetSeconds(timeTolerance) + UIView.animate(withDuration: 0.2,delay:Double(0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in + + imageView.alpha = 1 + + },completion: { finished in + + + }) + imageView.alpha = 1 + imageView.backgroundColor = .red + } + } + } +} + + diff --git a/kplayer/timeline/TimelineMeasure.swift b/kplayer/timeline/TimelineMeasure.swift new file mode 100644 index 0000000..f546ce3 --- /dev/null +++ b/kplayer/timeline/TimelineMeasure.swift @@ -0,0 +1,206 @@ +// +// TimelineMeasure.swift +// Examplay +// +// Created by Tomohiro Yamashita on 2020/03/08. +// Copyright © 2020 Tom. All rights reserved. +// + +import UIKit + +class TimelineMeasure: UIView { + + var unitSize:CGFloat = 100 + var frameImagesView:FrameImagesView? = nil + var parentScroller:TimelineScroller? = nil + var parentView:TimelineView? = nil + var stringColor:UIColor = UIColor(hue: 0.0, saturation:0.0, brightness:0.35, alpha: 1) + var animating = false + + override init (frame: CGRect) { + super.init(frame: frame) + + self.isMultipleTouchEnabled = true + self.isUserInteractionEnabled = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("TimelineMeasure init(coder:) has not been implemented") + } + + + + override func draw(_ rect: CGRect) { + + if frameImagesView == nil { + return + } + + let max = frameImagesView!.maxWidth + var width = frameImagesView!.frame.size.width + if animating { + if let layer = frameImagesView!.layer.presentation() { + width = layer.frame.size.width + } + } + if width == 0 { + return + } + var unit = (width / max) * unitSize + + let unitWidth = CGFloat(80) + var division = 0 + while (unit <= unitWidth) { + unit *= 2 + division += 1 + if division > 1000 { + break + } + } + let unitLength = pow(2,Double(division + 1)) / 2 + func index(_ position:CGFloat) -> Int { + return Int(position / unit) + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attributes = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue", size: self.frame.size.height * 0.7)!, NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: stringColor] + + var string = "" + + var visibleRect = rect + if let parent = parentScroller { + + if animating { + if let layer = parent.layer.presentation() { + let offset = layer.bounds.origin + visibleRect.origin = offset + visibleRect.size = parent.frame.size + } else { + visibleRect = parent.visibleRect() + } + } else { + visibleRect = parent.visibleRect() + } + } + let startIndex = index(visibleRect.origin.x) - 10 * (division + 1) + let endIndex = index(visibleRect.origin.x + visibleRect.size.width) + + for index in startIndex ... endIndex { + if index < 0 { + continue + } + let position = CGFloat(index) * unit - visibleRect.origin.x + (visibleRect.size.width / 2) + if division == 0 { + let path = UIBezierPath() + path.move(to: CGPoint(x: position + (unit / 2 ) - (unitWidth / 4) , y:(self.frame.size.height / 2))) + path.addLine(to: CGPoint(x: position + (unit / 2) + (unitWidth / 4), y:(self.frame.size.height / 2))) + path.lineWidth = 1 + stringColor.setStroke() + path.stroke() + } else { + for i in 1 ... 3 { + let point = position + ((unit / 4 ) * CGFloat(i)) + let r:CGFloat = 0.8 + let pointRect = CGRect(x: point - r, y: (self.frame.size.height / 2) - r, width: r * 2,height: r * 2) + let path = UIBezierPath(ovalIn:pointRect) + stringColor.setFill() + path.fill() + } + } + + let length = unitLength * Double(index) + let minute = Int(length / 60) + let second = Int(length - Double(minute * 60)) + + string = String(format: "%02d:%02d", minute, (Int(second))) + + string.draw(with: CGRect(x: position - (unitWidth / 2), y:0, width: unitWidth, height: self.frame.size.height), options: .usesLineFragmentOrigin, attributes: attributes, context: nil) + + + } + } + + //MARK: - timer for animation + var animationTimer = Timer() + func startAnimation() { + animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) + RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) + animating = true + } + + func stopAnimation() { + animating = false + self.setNeedsDisplay() + animationTimer.invalidate() + } + + @objc func animate(_ timer:Timer) { + if animating == false { + return + } + self.setNeedsDisplay() + } + + + + //MARK: - Touch Events + var allTouches = [UITouch]() + + override open func touchesBegan(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if !allTouches.contains(touch) { + allTouches += [touch] + } + if !parentView!.allTouches.contains(touch) { + parentView!.allTouches += [touch] + } + } + } + + + override open func touchesMoved(_ touches: Set, with event: UIEvent?) + { + if parentView!.allTouches.count == 2 { + if parentView!.pinching { + parentView!.updatePinch() + } else { + parentView!.startPinch() + } + } + } + + override open func touchesEnded(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if let index = allTouches.firstIndex(of:touch) { + allTouches.remove(at: index) + } + if let index = parentView!.allTouches.firstIndex(of:touch) { + parentView!.allTouches.remove(at: index) + } + } + if parentView!.pinching && parentView!.allTouches.count < 2 { + parentView!.endPinch() + } + } + + override open func touchesCancelled(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if let index = allTouches.firstIndex(of:touch) { + allTouches.remove(at: index) + } + if let index = parentView!.allTouches.firstIndex(of:touch) { + parentView!.allTouches.remove(at: index) + } + } + + if parentView!.pinching { + parentView!.endPinch() + } + } +} + diff --git a/kplayer/timeline/TimelineScroller.swift b/kplayer/timeline/TimelineScroller.swift new file mode 100644 index 0000000..698cb1b --- /dev/null +++ b/kplayer/timeline/TimelineScroller.swift @@ -0,0 +1,178 @@ +// +// TimelineScroller.swift +// Examplay +// +// Created by Tomohiro Yamashita on 2020/03/01. +// Copyright © 2020 Tom. All rights reserved. +// + +import UIKit + +class TimelineScroller: UIScrollView { + + var parentView:TimelineView? = nil + let frameImagesView = FrameImagesView() + let measure = TimelineMeasure() + let trimView = TrimView() + + + override init (frame: CGRect) { + super.init(frame: frame) + + self.isScrollEnabled = true + self.isDirectionalLockEnabled = true + self.showsHorizontalScrollIndicator = false + self.showsVerticalScrollIndicator = false + self.bounces = false + self.decelerationRate = .fast + self.isMultipleTouchEnabled = true + self.delaysContentTouches = false + self.frameImagesView.parentScroller = self + self.addSubview(frameImagesView) + self.measure.parentScroller = self + self.measure.frameImagesView = self.frameImagesView + self.measure.backgroundColor = .clear + } + + required init(coder aDecoder: NSCoder) { + fatalError("TimelineScroller init(coder:) has not been implemented") + } + + func configure(parent:TimelineView) { + parentView = parent + trimView.configure(parent, scroller:self) + } + + func reset() { + frameImagesView.reset() + } + + var ignoreScrollViewDidScroll:Bool = false + func setContentWidth(_ width:CGFloat) { + setContentWidth(width, setOrigin:true) + } + + func setContentWidth(_ width: CGFloat, setOrigin:Bool) { + ignoreScrollViewDidScroll = true + self.contentSize = CGSize(width:width + self.frame.size.width, height:self.frame.size.height) + frameImagesView.frame.size.width = width + //measure.frame.size.width = width + + if setOrigin { + let halfVisibleWidth = self.frame.size.width / 2 + //frameImagesView.frame.size.height = self.frame.size.height + frameImagesView.frame.origin.x = halfVisibleWidth + } + //measure.frame.origin.x = halfVisibleWidth + measure.setNeedsDisplay() + frameImagesView.displayFrames() + } + + var measureHeight:CGFloat = 5 + func coordinate() { + + let measureMin:CGFloat = 10 + + let wholeHeight = self.frame.size.height + measureHeight = wholeHeight * 0.2 + if measureHeight < measureMin { + measureHeight = measureMin + } + frameImagesView.frame.size.height = wholeHeight - measureHeight + if frameImagesView.animating == false { + frameImagesView.frame.origin = CGPoint(x: self.frame.size.width / 2,y: measureHeight) + } + measure.frame.size.height = measureHeight + //frameImagesView.layout() + + trimView.frame = self.frame + + } + + func visibleRect() -> CGRect { + var visibleRect = frame + visibleRect.origin = contentOffset + if contentSize.width < frame.size.width { + visibleRect.size.width = contentSize.width + } + if contentSize.height < frame.size.height { + visibleRect.size.height = contentSize.height + } + if zoomScale != 1 { + let theScale = 1.0 / zoomScale; + visibleRect.origin.x *= theScale; + visibleRect.origin.y *= theScale; + visibleRect.size.width *= theScale; + visibleRect.size.height *= theScale; + } + return visibleRect + } + + + //MARK: - scroll + func setScrollPoint(_ scrollPoint:CGFloat) { + let offset = (scrollPoint * frameImagesView.frame.size.width) + (self.frame.size.width / 2) + + self.contentOffset.x = offset - (self.frame.size.width / 2) + } + + + //MARK: - Touch Events + var allTouches = [UITouch]() + + override open func touchesBegan(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if !allTouches.contains(touch) { + allTouches += [touch] + } + if !parentView!.allTouches.contains(touch) { + parentView!.allTouches += [touch] + } + } + + } + + override open func touchesMoved(_ touches: Set, with event: UIEvent?) + { + if parentView!.allTouches.count == 2 { + if parentView!.pinching { + parentView!.updatePinch() + } else { + parentView!.startPinch() + } + } + } + + override open func touchesEnded(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if let index = allTouches.firstIndex(of:touch) { + allTouches.remove(at: index) + } + if let index = parentView!.allTouches.firstIndex(of:touch) { + parentView!.allTouches.remove(at: index) + } + } + if parentView!.pinching && parentView!.allTouches.count < 2 { + parentView!.endPinch() + } + } + + override open func touchesCancelled(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if let index = allTouches.firstIndex(of:touch) { + allTouches.remove(at: index) + } + if let index = parentView!.allTouches.firstIndex(of:touch) { + parentView!.allTouches.remove(at: index) + } + } + + if parentView!.pinching { + parentView!.endPinch() + } + } +} + diff --git a/kplayer/timeline/TimelineView.swift b/kplayer/timeline/TimelineView.swift new file mode 100644 index 0000000..da7d225 --- /dev/null +++ b/kplayer/timeline/TimelineView.swift @@ -0,0 +1,460 @@ +// +// TimelineView.swift +// Examplay +// +// Created by Tomohiro Yamashita on 2020/02/18. +// Copyright © 2020 Tom. All rights reserved. +// + +import Foundation +import UIKit +import AVFoundation + +class TimelineView: UIView, UIScrollViewDelegate { + var mainView:VideoTimelineView? = nil + let scroller = TimelineScroller() + let centerLine = CenterLine() + let viewForAnimate = UIScrollView() + + let durationPerHeight:Float64 = 0.35 + var animating = false + + override init (frame: CGRect) { + + super.init(frame: frame) + + self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.05) + + viewForAnimate.frame.origin = CGPoint.zero + viewForAnimate.isScrollEnabled = false + viewForAnimate.isUserInteractionEnabled = false + self.addSubview(viewForAnimate) + + self.addSubview(scroller) + scroller.delegate = self + + self.addSubview(scroller.measure) + scroller.configure(parent: self) + centerLine.configure(parent:self) + coordinate() + + self.addSubview(scroller.trimView) + self.addSubview(centerLine) + + scroller.measure.parentView = self + } + + required init(coder aDecoder: NSCoder) { + fatalError("TimelineView init(coder:) has not been implemented") + } + + + //MARK: - coordinate + func coordinate() { + if mainView == nil { + return + } + frame = mainView!.bounds + viewForAnimate.frame.size = self.frame.size + + scroller.frame = self.bounds + scroller.frameImagesView.frame.size.height = scroller.frame.size.height + scroller.measure.frame = scroller.frame + scroller.measure.frame.size.height = 20 + scroller.coordinate() + + centerLine.timeLabel.frame.size.height = scroller.measure.frame.size.height - 2 + let centerLineWidth:CGFloat = scroller.measure.frame.size.height * 5 + centerLine.frame = CGRect(x: (self.frame.size.width - centerLineWidth) / 2,y: 0,width: centerLineWidth,height: self.frame.size.height) + + centerLine.update() + + guard let view = (mainView) else { return } + guard let _ = (view.asset) else { return } + if scroller.frameImagesView.frame.size.width <= 0 { + return + } + let previousThumbSize = scroller.frameImagesView.thumbnailFrameSize + scroller.frameImagesView.setThumnailFrameSize() + let thumbSize = scroller.frameImagesView.thumbnailFrameSize + let unit = (thumbSize.height / CGFloat(durationPerHeight)) + scroller.measure.unitSize = unit + let contentMaxWidth = (unit * CGFloat(centerLine.duration)) + scroller.frameImagesView.maxWidth = contentMaxWidth + let defineMin = scroller.frame.size.width * 0.8 + var contentMinWidth:CGFloat + + if scroller.frameImagesView.maxWidth <= defineMin { + contentMinWidth = scroller.frameImagesView.maxWidth + } else { + contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth) + } + + scroller.frameImagesView.minWidth = contentMinWidth + + var currentWidth = snapWidth((scroller.frameImagesView.thumbnailFrameSize.width / previousThumbSize.width) * scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth) + if currentWidth < scroller.frameImagesView.minWidth { + currentWidth = scroller.frameImagesView.minWidth + } else if currentWidth > scroller.frameImagesView.maxWidth { + currentWidth = scroller.frameImagesView.maxWidth + } + + scroller.setContentWidth(currentWidth) + + scroller.reset() + + scroller.trimView.layout() + if view.currentTime <= view.duration { + setCurrentTime(view.currentTime, force:false) + } + } + + func snapWidth(_ width:CGFloat, max:CGFloat) -> CGFloat { + let n = log2((2 * max) / width) + var intN = CGFloat(Int(n)) + if n - intN >= 0.5 { + intN += 1 + } + let result = (2 * max) / (pow(2,intN)) + return result + } + + func scrollPoint() -> CGFloat { + return scroller.contentOffset.x / scroller.frameImagesView.frame.size.width + } + + + + //MARK: new movie set + func newMovieSet() { + + coordinate() + if let asset = mainView!.asset{ + scroller.frameImagesView.setThumnailFrameSize() + + let duration = asset.duration + let durationFloat = CMTimeGetSeconds(duration) + centerLine.duration = durationFloat + + let detailThumbSize = scroller.frameImagesView.thumbnailFrameSize + + let unit = (detailThumbSize.height / CGFloat(durationPerHeight)) + scroller.measure.unitSize = unit + + + let contentMaxWidth = (unit * CGFloat(centerLine.duration)) + scroller.frameImagesView.maxWidth = contentMaxWidth + + + let defineMin = scroller.frame.size.width * 0.8 + var contentMinWidth:CGFloat + + if scroller.frameImagesView.maxWidth <= defineMin { + contentMinWidth = scroller.frameImagesView.maxWidth + } else { + contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth) + } + scroller.frameImagesView.minWidth = contentMinWidth + scroller.setContentWidth(scroller.frameImagesView.minWidth) + + scroller.reset() + scroller.trimView.reset(duration:durationFloat) + } + } + + //MARK: - currentTime + func setCurrentTime(_ currentTime:Float64, force:Bool) { + if inAction() && force == false { + return + } + if mainView!.asset == nil { + return + } + var scrollPoint:CGFloat = 0 + scrollPoint = CGFloat(currentTime / mainView!.duration) + + centerLine.ignoreSendScrollToParent = true + centerLine.setScrollPoint(scrollPoint) + scroller.ignoreScrollViewDidScroll = true + scroller.setScrollPoint(scrollPoint) + + scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) + + scroller.frameImagesView.displayFrames() + scroller.measure.setNeedsDisplay() + } + + func moved(_ currentTime:Float64) { + mainView!.timelineIsMoved(currentTime, scrub:true) + } + + + //MARK: - TrimViews + func setTrimmerStatus(enabled:Bool) { + if enabled { + scroller.trimView.alpha = 1 + scroller.trimView.startKnob.isUserInteractionEnabled = true + scroller.trimView.alpha = 1 + scroller.trimView.endKnob.isUserInteractionEnabled = true + + } else { + scroller.trimView.alpha = 0.5 + scroller.trimView.startKnob.isUserInteractionEnabled = false + scroller.trimView.alpha = 0.5 + scroller.trimView.endKnob.isUserInteractionEnabled = false + } + } + + func setTrimmerVisible(_ visible:Bool) { + scroller.trimView.isHidden = !visible + } + + + func setTrim(start:Float64?, end:Float64?) { + var changed = false + if start != nil { + scroller.trimView.startKnob.knobTimePoint = start! + changed = true + } + if end != nil { + scroller.trimView.endKnob.knobTimePoint = end! + changed = true + } + if changed { + scroller.trimView.layout() + } + } + + func setTrimWithAnimation(trim:VideoTimelineTrim, time:Float64) { + scroller.trimView.moveToTimeAndTrimWithAnimation(time, trim:trim) + } + + + var manualScrolledAfterEnd = false + func setTrimViewInteraction(_ active:Bool) { + if mainView!.trimEnabled == false && active { + return + } + + scroller.trimView.startKnob.isUserInteractionEnabled = active + scroller.trimView.endKnob.isUserInteractionEnabled = active + + if active { + setManualScrolledAfterEnd() + } + } + + func setManualScrolledAfterEnd() { + let trim = currentTrim() + if mainView!.asset != nil { + let currentTime = mainView!.currentTime + if currentTime >= trim.end { + manualScrolledAfterEnd = true + } else { + manualScrolledAfterEnd = false + } + } + } + + func currentTrim() -> (start:Float64, end:Float64) { + var start = scroller.trimView.startKnob.knobTimePoint + var end = scroller.trimView.endKnob.knobTimePoint + if mainView!.asset != nil { + if end > mainView!.duration { + end = mainView!.duration + } + if start < 0 { + start = 0 + } + } + return (start, end) + } + + + func swapTrimKnobs() { + let knob = scroller.trimView.endKnob + scroller.trimView.endKnob = scroller.trimView.startKnob + scroller.trimView.startKnob = knob + } + + //MARK: - animation + func startAnimation() { + scroller.frameImagesView.startAnimation() + scroller.measure.startAnimation() + scroller.trimView.startAnimation() + } + + func stopAnimation() { + scroller.frameImagesView.stopAnimation() + scroller.measure.stopAnimation() + scroller.trimView.stopAnimation() + } + + + + + + //MARK: - Gestures + func inAction() -> Bool { + if allTouches.count > 0 || scroller.isTracking || scroller.isDecelerating { + return true + } else { + return false + } + } + + //MARK: - Scrolling + + var allTouches = [UITouch]() + var pinching:Bool = false + + func scrollViewDidScroll(_ scrollView:UIScrollView) { + scroller.trimView.layout() + if scroller.ignoreScrollViewDidScroll { + scroller.ignoreScrollViewDidScroll = false + return + } + scroller.measure.setNeedsDisplay() + scroller.frameImagesView.displayFrames() + setTrimViewInteraction(false) + let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width + self.centerLine.setScrollPoint(scrollPoint) + + guard let mView = (mainView) else { return } + if let receiver = mView.playStatusReceiver { + receiver.videoTimelineMoved() + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) + setTrimViewInteraction(true) + let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width + self.centerLine.setScrollPoint(scrollPoint) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, + willDecelerate decelerate: Bool) { + scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) + if decelerate == false { + setTrimViewInteraction(true) + } + let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width + self.centerLine.setScrollPoint(scrollPoint) + } + + + + //MARK: - Zooming + + var pinchCenterInContent:CGFloat = 0 + var pinchStartDistance:CGFloat = 0 + var pinchStartContent:(x:CGFloat,width:CGFloat) = (0,0) + func pinchCenter(_ pointA:CGPoint, pointB:CGPoint) -> CGPoint { + return CGPoint(x: (pointA.x + pointB.x) / 2, y: (pointA.y + pointB.y) / 2) + } + func pinchDistance(_ pointA:CGPoint, pointB:CGPoint) -> CGFloat { + return sqrt(pow((pointA.x - pointB.x),2) + pow((pointA.y - pointB.y),2)); + } + func startPinch() { + pinching = true + scroller.isScrollEnabled = false + + let touch1 = allTouches[0] + let touch2 = allTouches[1] + let center = pinchCenter(touch1.location(in: self),pointB: touch2.location(in: self)) + + pinchStartDistance = pinchDistance(touch1.location(in: self),pointB: touch2.location(in: self)) + let framewidth = scroller.frame.size.width + pinchStartContent = ((framewidth / 2) - scroller.contentOffset.x,scroller.contentSize.width - framewidth) + pinchCenterInContent = (center.x - pinchStartContent.x) / pinchStartContent.width + } + + func updatePinch() { + let touch1 = allTouches[0] + let touch2 = allTouches[1] + let center = pinchCenter(touch1.location(in: self), pointB:touch2.location(in: self)) + var sizeChange = (1 * pinchDistance(touch1.location(in: self), pointB: touch2.location(in: self))) / pinchStartDistance + + var contentWidth = pinchStartContent.width * sizeChange + + let sizeMin = scroller.frameImagesView.minWidth + let sizeMax = scroller.frameImagesView.maxWidth + + if contentWidth < sizeMin { + let sizeUnit = sizeMin / pinchStartContent.width + sizeChange = ((pow(sizeChange/sizeUnit,2)/4) + 0.75) * sizeUnit + contentWidth = pinchStartContent.width * sizeChange + contentWidth = sizeMin + } else if contentWidth > sizeMax { + sizeChange = sizeMax + contentWidth = sizeMax + } else { + let startRatio = pinchStartContent.width / sizeMax + let currentRatio = startRatio * sizeChange + let effect = ((sin(CGFloat.pi * 2 * log2(2/currentRatio)) * 0.108) - (sin(CGFloat.pi * 6 * log2(2/currentRatio)) * 0.009)) * currentRatio + let resultWidth = sizeMax * (currentRatio + effect) + contentWidth = resultWidth + } + let contentOrigin = center.x - (contentWidth * pinchCenterInContent) + scroller.contentOffset.x = (scroller.frame.size.width / 2) - contentOrigin + scroller.setContentWidth(contentWidth) + scroller.frameImagesView.layout() + + scroller.frameImagesView.requestVisible(depth:2, wide:0, direction:0) + self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width) + scroller.trimView.layout() + } + + func endPinch() { + scroller.frameImagesView.requestVisible(depth:0, wide:1, direction:0) + + let width = snapWidth(scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth) + + let offset = self.resizedPositionWithKeepOrigin(width:scroller.frameImagesView.frame.size.width, origin:scroller.contentOffset.x, destinationWidth:width) + //startAnimation() + + UIView.animate(withDuration: 0.1,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in + + self.scroller.setContentWidth(width, setOrigin:false) + self.scroller.contentOffset.x = offset + self.scroller.frameImagesView.layout() + self.scroller.trimView.layout() + },completion: { finished in + self.pinching = false + self.scroller.isScrollEnabled = true + }) + + self.scroller.frameImagesView.updateTolerance() + + self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width) + + setTrimViewInteraction(true) + + guard let mView = (mainView) else { return } + if let receiver = mView.playStatusReceiver { + receiver.videoTimelineMoved() + } + } + + func resizedPositionWithKeepOrigin(width:CGFloat, origin:CGFloat, destinationWidth:CGFloat) -> CGFloat { + let originPoint = origin / width + let result = originPoint * destinationWidth + return result + } + + +} + + + + + + + + + + + + + diff --git a/kplayer/timeline/TrimView.swift b/kplayer/timeline/TrimView.swift new file mode 100644 index 0000000..d1f7da3 --- /dev/null +++ b/kplayer/timeline/TrimView.swift @@ -0,0 +1,773 @@ +// +// TrimView.swift +// Examplay +// +// Created by Tomohiro Yamashita on 2020/03/12. +// Copyright © 2020 Tom. All rights reserved. +// + +import UIKit + +class TrimView: UIView { + var mainView:VideoTimelineView! + + var timelineView:TimelineView! + var parentScroller:TimelineScroller! + var startKnob = TrimKnob() + var endKnob = TrimKnob() + var movieDuration:Float64 = 0 + + let canPassThroughEachKnobs = true + + override init (frame: CGRect) { + super.init(frame: frame) + self.isUserInteractionEnabled = false + self.backgroundColor = .clear + + } + + required init(coder aDecoder: NSCoder) { + fatalError("TrimView init(coder:) has not been implemented") + } + + func configure(_ timeline:TimelineView, scroller:TimelineScroller) { + timelineView = timeline + parentScroller = scroller + self.frame = timeline.frame + startKnob.configure(timeline, trimmer:self) + endKnob.configure(timeline, trimmer:self) + + timelineView.addSubview(startKnob) + timelineView.addSubview(endKnob) + } + + + func reset(duration:Float64) { + movieDuration = duration + startKnob.knobTimePoint = 0 + endKnob.knobTimePoint = 3 + if duration < endKnob.knobTimePoint { + endKnob.knobTimePoint = duration + } + layout() + } + + + + let knobWidth:CGFloat = 20 + let knobWidthExtend:CGFloat = 5 + func layout() { + if self.isHidden { + return + } + + swapKnobs() + + let knobPositions = knobPositionsAsVisible() + let startPosition = knobPositions.start + let endPosition = knobPositions.end + + startKnob.knobPositionOnScreen = startPosition + endKnob.knobPositionOnScreen = endPosition + startKnob.isOutOfScreen = knobPositions.startFixed + endKnob.isOutOfScreen = knobPositions.endFixed + + startKnob.frame = CGRect(x:startPosition - knobWidth - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height) + endKnob.frame = CGRect(x:endPosition - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height) + + self.setNeedsDisplay() + } + + //MARK: - draw + + override func draw(_ rect: CGRect) { + + + var startRect = startKnob.frame + var endRect = endKnob.frame + if startRect.size.height <= 0 { + return + } + if animating { + if let layer = startKnob.layer.presentation() { + startRect = layer.frame + } + if let layer = endKnob.layer.presentation() { + endRect = layer.frame + } + } + startRect.origin.x += knobWidthExtend + startRect.size.width -= knobWidthExtend * 2 + endRect.origin.x += knobWidthExtend + endRect.size.width -= knobWidthExtend * 2 + if startRect.origin.x > endRect.origin.x + endRect.size.width { + let swapRect = startRect + startRect = endRect + endRect = swapRect + } + + + let beamWidth:CGFloat = 3 + var outerRect = CGRect(x: startRect.origin.x,y: 0,width: endRect.origin.x + endRect.size.width - startRect.origin.x,height:startRect.size.height) + var innerRect = CGRect(x:startRect.origin.x + startRect.size.width,y:beamWidth,width: endRect.origin.x - startRect.origin.x - startRect.size.width,height:startRect.size.height - (beamWidth * 2)) + + let screenLeft = cgToTime(screenToTimelinePosition(0)) + let screenRight = cgToTime(screenToTimelinePosition(timelineView.frame.size.width)) + var color = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 1) + let outColor = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 0.3) + if (endKnob.knobTimePoint < screenLeft || startKnob.knobTimePoint > screenRight) { + color = outColor + } else { + let addition = knobWidth + 10 + let knobWidthTime = cgToTime(knobWidth) + if endKnob.knobTimePoint + knobWidthTime * 0.9 > screenRight { + outerRect.size.width += addition + innerRect.size.width += addition + let outRect = CGRect(x: timelineView.frame.size.width - knobWidth, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2) + let path = UIBezierPath(rect:outRect) + outColor.setFill() + path.fill() + } + if startKnob.knobTimePoint - knobWidthTime * 0.9 < screenLeft { + outerRect.origin.x -= addition + innerRect.origin.x -= addition + outerRect.size.width += addition + innerRect.size.width += addition + let outRect = CGRect(x: 0, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2) + let path = UIBezierPath(rect:outRect) + outColor.setFill() + path.fill() + } + } + + let path = UIBezierPath(roundedRect:outerRect, cornerRadius:5) + path.usesEvenOddFillRule = true + let innerPath = UIBezierPath(roundedRect:innerRect, cornerRadius:2) + path.append(innerPath) + color.setFill() + path.fill() + } + + + + //MARK: - timer for animation + var animationTimer = Timer() + var animating = false + func startAnimation() { + animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) + RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) + animating = true + } + + func stopAnimation() { + animating = false + self.setNeedsDisplay() + animationTimer.invalidate() + } + + @objc func animate(_ timer:Timer) { + if animating == false { + return + } + self.setNeedsDisplay() + } + + + //MARK: - positioning + func swapKnobs() { + if startKnob.knobTimePoint > endKnob.knobTimePoint { + if canPassThroughEachKnobs { + //timelineView.swapTrimKnobs() + } else { + let start = endKnob.knobTimePoint + endKnob.knobTimePoint = startKnob.knobTimePoint + startKnob.knobTimePoint = start + } + } + } + + func anotherKnob(_ knob:TrimKnob) -> TrimKnob { + if knob == startKnob { + return endKnob + } + return startKnob + } + + func knobOnScreen(_ knob:TrimKnob) -> CGFloat { + let maxWidth = scrollMaxWidth() + let offset = scrollOffset() + let position = CGFloat(knob.knobTimePoint / movieDuration) * maxWidth + return position - offset + (timelineView.frame.size.width / 2) + } + + func knobsMinDistanceTime() -> Float64 { + return Float64(0.1) + } + + func knobsMinDistanceFloat() -> CGFloat { + return timeToCG(knobsMinDistanceTime()) + } + + func timeToCG(_ time:Float64) -> CGFloat { + return CGFloat(time / movieDuration) * scrollMaxWidth() + } + + func cgToTime(_ cgFloat:CGFloat) -> Float64 { + return Float64(cgFloat / scrollMaxWidth()) * movieDuration + } + + func screenToTimelinePosition(_ onScreen:CGFloat) -> CGFloat { + return onScreen + scrollOffset() - (timelineView.frame.size.width / 2) + } + + func timelineToScreenPosition(_ position:CGFloat) -> CGFloat { + return position - scrollOffset() + (timelineView.frame.size.width / 2) + } + + func knobPositionsAsVisible() -> (start:CGFloat, end:CGFloat, startFixed:Bool, endFixed:Bool) { + + let positionStart = knobOnScreen(startKnob) + let positionEnd = knobOnScreen(endKnob) + var resultStart = positionStart + var resultEnd = positionEnd + var startFixed:Bool = false + var endFixed:Bool = false + let screenRight = timelineView.frame.size.width// + offset + let minDistance = knobsMinDistanceFloat() + + if positionStart < knobWidth { + if (positionEnd - minDistance - knobWidth) < 0 { + resultStart = positionEnd - minDistance + } else { + resultStart = knobWidth + } + startFixed = true + } else if positionStart > screenRight { + resultStart = screenRight + startFixed = true + } + if positionEnd < 0 { + resultEnd = 0 + endFixed = true + } else if positionEnd + knobWidth > screenRight { + if positionStart + minDistance + knobWidth > screenRight { + resultEnd = positionStart + minDistance + } else { + resultEnd = screenRight - knobWidth + } + } + if true { + let distance = abs(resultStart - resultEnd) + if distance < minDistance { + let value = (minDistance - distance) / 2 + resultStart -= value + resultEnd += value + startFixed = true + endFixed = true + } + } + return (resultStart, resultEnd, startFixed, endFixed) + } + + func scrollOffset() -> CGFloat { + return parentScroller.contentOffset.x + } + + func scrollMaxWidth() -> CGFloat { + return parentScroller.frameImagesView.frame.size.width + } + + func knobTimeOnScreen(_ knob:TrimKnob) -> Float64 { + let offset = scrollOffset() + let position = offset + knob.knobPositionOnScreen - (timelineView.frame.size.width / 2) + let maxWidth = scrollMaxWidth() + let time = Float64(position / maxWidth) * movieDuration + return time + } + + + func knobMoveRange(_ knob:TrimKnob) -> (min:Float64, max:Float64) { + var min:Float64 = 0 + var max:Float64 = movieDuration + let minDistance = knobsMinDistanceTime() + if canPassThroughEachKnobs { + if knob == startKnob { + max -= minDistance + } else if knob == endKnob { + min += minDistance + } + } else { + if knob == startKnob { + max = endKnob.knobTimePoint - minDistance + } else if knob == endKnob { + min = startKnob.knobTimePoint + minDistance + } + } + return (min, max) + } + + func visibleKnobMoveLimit(_ knob:TrimKnob, margin:CGFloat) -> (min:Bool, max:Bool) { + let range = knobMoveRange(knob) + let minOnScreen = timelineToScreenPosition(timeToCG(range.min)) + let maxOnScreen = timelineToScreenPosition(timeToCG(range.max)) + var resultMin = false + var resultMax = false + if minOnScreen >= margin && minOnScreen <= timelineView.frame.width - margin { + resultMin = true + } + if maxOnScreen >= margin && maxOnScreen <= timelineView.frame.width - margin { + resultMax = true + } + return (resultMin , resultMax ) + } + + func directionReachesEnd(_ knob:TrimKnob, direction:CGFloat) -> Bool { + let visibleLimit = visibleKnobMoveLimit(knob, margin:knobWidth) + if direction > 0 { + if visibleLimit.max { + return true + } + } else if direction < 0 { + if visibleLimit.min { + return true + } + } + return false + } + + func fixKnobPoint(_ knob:TrimKnob, move:CGFloat, startKnobPoint:Float64) -> (knobPoint:Float64, fixed:Bool) { + + var timePoint = startKnobPoint + cgToTime(move) + + let moveRange = knobMoveRange(knob) + var fixed = false + if timePoint < moveRange.min { + timePoint = moveRange.min + fixed = true + } + if timePoint > moveRange.max { + timePoint = moveRange.max + fixed = true + } + return (timePoint, fixed) + } + + func updateKnob(_ knob:TrimKnob, timePoint:Float64) { + knob.knobTimePoint = timePoint + layout() + + timelineView.moved(knob.knobTimePoint) + } + + func resetSeek(_ time:Float64) { + if edgeScrolled { + moveToTimeWithAnimation(time) + } else { + mainView!.accurateSeek(time, scrub:true) + } + edgeScrolled = false + } + + func moveToTimeWithAnimation(_ time:Float64) { + moveToTimeAndTrimWithAnimation(time, trim:nil) + } + + func moveToTimeAndTrimWithAnimation(_ time:Float64, trim:VideoTimelineTrim?) { + let player = mainView!.player + if player == nil { + return + } + if player!.timeControlStatus == .playing { + player!.pause() + } + timelineView.animating = true + mainView!.isUserInteractionEnabled = false + timelineView.startAnimation() + + UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in + if let pinnedTrim = trim { + self.timelineView.setTrim(start:pinnedTrim.start,end:pinnedTrim.end) + } + self.timelineView.setCurrentTime(time,force:false) + },completion: { finished in + + self.mainView!.isUserInteractionEnabled = true + if self.mainView!.playing { + self.mainView!.accurateSeek(time, scrub:false) + player!.play() + } else { + self.mainView!.accurateSeek(time, scrub:true) + } + self.timelineView.animating = false + self.timelineView.setManualScrolledAfterEnd() + self.timelineView.stopAnimation() + if let receiver = self.mainView!.playStatusReceiver { + receiver.videoTimelineMoved() + } + }) + } + + //MARK: - edgeScroll + var edgeScrollTimer = Timer() + var edgeScrolled = false + var edgeScrollingKnob:TrimKnob? = nil + var edgeScrollStrength:CGFloat = 0 + var edgeScrollingKnobPosition:CGFloat = 0 + var edgeScrollLastChangedTime:Date = Date() + var edgeScrollLastChangedPosition:CGFloat = 0 + + + func updateEdgeScroll(_ knob:TrimKnob, strength:CGFloat, position:CGFloat) { + var changed = false + if edgeScrollStrength != strength { + edgeScrollStrength = strength + changed = true + } + if edgeScrolling() == false { + startEdgeScroll(knob) + edgeScrolled = true + } else if changed { + edgeScrollLastChangedPosition = scrollOffset() + edgeScrollLastChangedTime = Date() + } + edgeScrollingKnobPosition = position + edgeScrollingKnob = knob + } + + func startEdgeScroll(_ knob:TrimKnob) { + edgeScrollTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.edgeScrollTimer(_:)), userInfo: nil, repeats: true) + RunLoop.main.add(edgeScrollTimer, forMode:RunLoop.Mode.common) + edgeScrollLastChangedTime = Date() + edgeScrollLastChangedPosition = scrollOffset() + } + + @objc func edgeScrollTimer(_ timer:Timer) { + if let knob = edgeScrollingKnob { + let movedPosition = currentEdgeScrollMovedPosition() + let destination = currentEdgeScrollPosition(movedPosition) + let moveRange = knobMoveRange(knob) + var knobPoint = cgToTime(edgeScrollingKnobPosition - (timelineView.frame.size.width / 2) + destination) + + var overLimit:Float64 = 0 + if knobPoint > moveRange.max { + overLimit = knobPoint - moveRange.max + knobPoint = moveRange.max + + } else if knobPoint < moveRange.min { + overLimit = knobPoint - moveRange.min + knobPoint = moveRange.min + + } + updateKnob(knob, timePoint:knobPoint) + if (knob == startKnob && knobPoint > endKnob.knobTimePoint) || (knob == endKnob && knobPoint < startKnob.knobTimePoint) { + timelineView.swapTrimKnobs() + } + timelineView.setCurrentTime(cgToTime(destination) - overLimit,force:true) + } + } + + func stopEdgeScrollTimer() { + edgeScrollTimer.invalidate() + edgeScrollStrength = 0 + edgeScrollingKnob = nil + } + + func edgeScrolling() -> Bool { + return edgeScrollTimer.isValid + } + + func currentEdgeScrollPosition(_ moved:CGFloat) -> CGFloat { + var result:CGFloat = 0 + + let maxWidth = scrollMaxWidth() + result = edgeScrollLastChangedPosition + moved + if result < 0 { + result = 0 + } else if result > maxWidth { + result = maxWidth + } + return result + } + + func currentEdgeScrollMovedPosition() -> CGFloat { + let pastTime = -edgeScrollLastChangedTime.timeIntervalSinceNow + return CGFloat(pastTime) * edgeScrollStrength * 5 + } +} + + +//MARK: - TrimKnob +class TrimKnob:UIView { + var timelineView:TimelineView! + var knobPositionOnScreen:CGFloat = 0 + var trimView:TrimView! + var knobTimePoint:Float64 = 0 + var isOutOfScreen:Bool = false + + + override init (frame: CGRect) { + super.init(frame: frame) + + self.isMultipleTouchEnabled = true + self.isUserInteractionEnabled = true + + } + + required init(coder aDecoder: NSCoder) { + fatalError("TrimKnob init(coder:) has not been implemented") + } + + func configure(_ timeline:TimelineView, trimmer:TrimView) { + timelineView = timeline + trimView = trimmer + } + + //MARK: - Touch Events + var allTouches = [UITouch]() + + override open func touchesBegan(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if !allTouches.contains(touch) { + allTouches += [touch] + } + if !timelineView!.allTouches.contains(touch) { + timelineView!.allTouches += [touch] + } + } + if timelineView!.allTouches.count == 1 && allTouches.count == 1 { + if dragging == false { + startDrag() + } + evaluateTap = true + } else { + evaluateTap = false + } + } + + + override open func touchesMoved(_ touches: Set, with event: UIEvent?) + { + if timelineView!.allTouches.count > 1 { + if dragging { + cancelDrag() + } + } + if timelineView!.allTouches.count == 2 { + if timelineView!.pinching { + timelineView!.updatePinch() + } else { + timelineView!.startPinch() + } + } + if dragging && timelineView!.allTouches.count == 1 && allTouches.count == 1 { + updateDrag() + } + evaluateTap = false + } + + override open func touchesEnded(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if let index = allTouches.firstIndex(of:touch) { + allTouches.remove(at: index) + } + if let index = timelineView!.allTouches.firstIndex(of:touch) { + timelineView!.allTouches.remove(at: index) + } + } + if timelineView!.pinching && timelineView!.allTouches.count < 2 { + timelineView!.endPinch() + } + if dragging { + endDrag() + } + + if evaluateTap && timelineView!.allTouches.count == 0 { + tapped() + } + evaluateTap = false + } + + override open func touchesCancelled(_ touches: Set, with event: UIEvent?) + { + for touch in touches { + if let index = allTouches.firstIndex(of:touch) { + allTouches.remove(at: index) + } + if let index = timelineView!.allTouches.firstIndex(of:touch) { + timelineView!.allTouches.remove(at: index) + } + } + + if timelineView!.pinching { + timelineView!.endPinch() + } + if dragging { + endDrag() + } + evaluateTap = false + } + + + //MARK: - actions + + var dragging:Bool = false + var dragStartPoint = CGPoint.zero + var startKnobTimePoint:Float64 = 0 + var dragStartOffset:CGFloat = 0 + var startTimeOutOfScreen:Float64 = 0 + var scrolling = false + var startCurrentTime:Float64 = 0 + var evaluateTap:Bool = false + var ignoreEdgeScroll = false + + func startDrag() { + + dragging = true + let touch = allTouches[0] + dragStartPoint = touch.location(in: timelineView) + startKnobTimePoint = knobTimePoint + dragStartOffset = trimView.scrollOffset() + startTimeOutOfScreen = trimView.knobTimeOnScreen(self) - startKnobTimePoint + + + startCurrentTime = timelineView.mainView!.currentTime + + if edgeScrollStrength(dragStartPoint.x) != 0 { + ignoreEdgeScroll = true + } else { + ignoreEdgeScroll = false + } + } + + func updateDrag() { + let touch = allTouches[0] + let currentPoint = touch.location(in: timelineView) + let scrolled = trimView.scrollOffset() - dragStartOffset + let move = currentPoint.x - dragStartPoint.x + scrolled + let startKnobPoint = startKnobTimePoint + startTimeOutOfScreen + + let timePoint = startKnobPoint + trimView.cgToTime(move) + let dragPoint = trimView.cgToTime(trimView.screenToTimelinePosition(currentPoint.x)) + let onKnob = trimView.timeToCG(timePoint - dragPoint) + let anotherKnob = trimView.anotherKnob(self) + let anotherTimePoint = anotherKnob.knobTimePoint + let knobWidth = trimView.knobWidth + var swapping:CGFloat = 0 + + + if anotherTimePoint < timePoint && startKnobPoint < anotherTimePoint + { + swapping = trimView.timeToCG(anotherTimePoint - timePoint) + if -swapping > knobWidth { + swapping = -knobWidth + } + } else if anotherTimePoint > timePoint && startKnobPoint > anotherTimePoint + { + swapping = trimView.timeToCG(anotherTimePoint - timePoint) + if swapping > knobWidth { + swapping = knobWidth + } + } + var rangeOut = false + let range = trimView.knobMoveRange(self) + if timePoint > range.max || timePoint < range.min { + rangeOut = true + } + + if rangeOut == false && ((self == trimView.startKnob && dragPoint > anotherTimePoint) || (self == trimView.endKnob && dragPoint < anotherTimePoint)) { + timelineView.swapTrimKnobs() + } + + + let strength = edgeScrollStrength(currentPoint.x) + let reachedEnd = trimView.directionReachesEnd(self, direction:strength) + if strength != 0 && ignoreEdgeScroll == false && reachedEnd == false { + trimView.updateEdgeScroll(self, strength:strength, position:currentPoint.x + onKnob + swapping) + } else { + let fixedKnobPoint = trimView.fixKnobPoint(self, move:move + swapping, startKnobPoint:startKnobPoint) + + trimView.updateKnob(self, timePoint:fixedKnobPoint.knobPoint) + if strength == 0 { + ignoreEdgeScroll = false + + } + if strength == 0 || reachedEnd { + if trimView.edgeScrolling() { + trimView.stopEdgeScrollTimer() + } + } + } + + guard let mainView = (timelineView.mainView) else { return } + if let receiver = mainView.playStatusReceiver { + receiver.videoTimelineTrimChanged() + } + } + + func endDrag() { + let anotherKnob = trimView.anotherKnob(self) + let distance = abs(anotherKnob.knobTimePoint - knobTimePoint) + let minDistance = trimView.knobsMinDistanceTime() + if distance < minDistance { + if self == trimView.startKnob { + knobTimePoint -= (minDistance - distance) + } else if self == trimView.endKnob { + knobTimePoint += (minDistance - distance) + } + + trimView.timelineView.animating = true + trimView.startAnimation() + UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in + + self.trimView.layout() + + },completion: { finished in + self.trimView.stopAnimation() + self.timelineView.animating = false + }) + } + timelineView.setManualScrolledAfterEnd() + trimView.resetSeek(startCurrentTime) + dragging = false + trimView.stopEdgeScrollTimer() + + if evaluateTap == false { + guard let mainView = (timelineView.mainView) else { return } + if let receiver = mainView.playStatusReceiver { + receiver.videoTimelineTrimChanged() + } + } + } + + func cancelDrag() { + knobTimePoint = startKnobTimePoint + + timelineView.setManualScrolledAfterEnd() + dragging = false + trimView.layout() + trimView.resetSeek(startCurrentTime) + trimView.stopEdgeScrollTimer() + } + + func tapped() { + trimView.moveToTimeWithAnimation(knobTimePoint) + timelineView.setManualScrolledAfterEnd() + } + + + func edgeScrollStrength(_ position:CGFloat) -> CGFloat { + var strength:CGFloat = 0 + let edgeWidth:CGFloat = 40 + if position >= timelineView.frame.size.width - edgeWidth { + strength = position + edgeWidth - timelineView.frame.size.width + } else if position <= edgeWidth { + strength = position - edgeWidth + } + return strength + } + + + +} diff --git a/kplayer/timeline/VideoTimelineView.swift b/kplayer/timeline/VideoTimelineView.swift new file mode 100644 index 0000000..a7d6f33 --- /dev/null +++ b/kplayer/timeline/VideoTimelineView.swift @@ -0,0 +1,301 @@ +// +// VideoTimelineView.swift +// VideoTimelineView +// +// Created by Tomohiro Yamashita on 2020/03/28. +// Copyright © 2020 Tom. All rights reserved. +// + +import UIKit +import AVFoundation + +protocol TimelinePlayStatusReceiver: class { + func videoTimelineStopped() + func videoTimelineMoved() + func videoTimelineTrimChanged() +} + + +struct VideoTimelineTrim { + var start:Float64 + var end:Float64 +} + +class VideoTimelineView: UIView { + + public private(set) var asset:AVAsset? = nil + var player:AVPlayer? = nil + + weak var playStatusReceiver:TimelinePlayStatusReceiver? = nil + + var repeatOn:Bool = false + + public private(set) var trimEnabled:Bool = false + + + + var currentTime:Float64 = 0 + public private(set) var duration:Float64 = 0 + + public private(set) var audioPlayer:AVPlayer! + public private(set) var audioPlayer2:AVPlayer! + + let timelineView = TimelineView() + + override init (frame: CGRect) { + super.init(frame: frame) + timelineView.mainView = self + timelineView.centerLine.mainView = self + timelineView.scroller.frameImagesView.mainView = self + timelineView.scroller.trimView.mainView = self + + self.addSubview(timelineView) + } + + required init(coder aDecoder: NSCoder) { + fatalError("MainView init(coder:) has not been implemented") + } + + + func viewDidLayoutSubviews() { + coordinate() + } + + func coordinate() { + timelineView.coordinate() + } + + func new(asset newAsset:AVAsset?) { + if let new = newAsset { + asset = new + duration = CMTimeGetSeconds(new.duration) + player = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) + audioPlayer = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) + audioPlayer.volume = 1.0 + audioPlayer2 = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) + audioPlayer2.volume = 1.0 + timelineView.newMovieSet() + } + } + + + func setTrim(start:Float64, end:Float64, seek:Float64?, animate:Bool) { + + var seekTime = currentTime + if let time = seek { + seekTime = time + } + if animate { + timelineView.setTrimWithAnimation(trim:VideoTimelineTrim(start:start, end:end), time:seekTime) + } else { + timelineView.setTrim(start:start, end:end) + if seek != nil { + moveTo(seek!, animate:animate) + } + } + } + + func setTrimIsEnabled(_ enabled:Bool) { + trimEnabled = enabled + timelineView.setTrimmerStatus(enabled:enabled) + } + + func setTrimmerIsHidden(_ hide:Bool) { + timelineView.setTrimmerVisible(!hide) + } + + func currentTrim() -> (start:Float64, end:Float64) { + let trim = timelineView.currentTrim() + return (trim.start,trim.end) + } + + func moveTo(_ time:Float64, animate:Bool) { + if animate { + + } else { + accurateSeek(time, scrub:false) + timelineView.setCurrentTime(time, force:true) + } + } + + //MARK: - seeking + var previousSeektime:Float64 = 0 + func timelineIsMoved(_ currentTime:Float64, scrub:Bool) { + let move = abs(currentTime - previousSeektime) + let seekTolerance = CMTimeMakeWithSeconds(move, preferredTimescale:100) + + if player != nil { + player!.seek(to:CMTimeMakeWithSeconds(currentTime , preferredTimescale:100), toleranceBefore:seekTolerance,toleranceAfter:seekTolerance) + } + previousSeektime = currentTime + if scrub { + audioScrub() + } + } + + func accurateSeek(_ currentTime:Float64, scrub:Bool) { + previousSeektime = currentTime + timelineIsMoved(currentTime, scrub:scrub) + } + + var scrubed1 = Date() + var scrubed2 = Date() + var canScrub1 = true + var canScrub2 = true + func audioScrub() { + if player == nil { + return + } + if scrubed2.timeIntervalSinceNow < -0.16 && canScrub1 { + canScrub1 = false + self.scrubed1 = Date() + DispatchQueue.main.async { + if self.audioPlayer.timeControlStatus == .playing { + self.audioPlayer.pause() + self.canScrub1 = true + } else { + self.audioPlayer.seek(to: self.player!.currentTime()) + self.audioPlayer.play() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { + self.audioPlayer.pause() + self.audioPlayer.seek(to: self.player!.currentTime()) + self.canScrub1 = true + } + } + + } + } + if scrubed1.timeIntervalSinceNow < -0.16 && canScrub2 { + canScrub2 = false + self.scrubed2 = Date() + DispatchQueue.main.async { + if self.audioPlayer2.timeControlStatus == .playing { + self.audioPlayer2.pause() + self.canScrub2 = true + } else { + self.audioPlayer2.seek(to: self.player!.currentTime()) + self.audioPlayer2.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { + self.audioPlayer2.pause() + self.audioPlayer2.seek(to: self.player!.currentTime()) + self.canScrub2 = true + } + } + } + } + } + + + //MARK: - play + var playerTimer = Timer() + @objc dynamic var playing = false + func play() { + if asset == nil { + return + } + let currentTime = timelineView.centerLine.currentTime + let reached = timeReachesEnd(currentTime) + + if reached.trimEnd { + accurateSeek(timelineView.currentTrim().start, scrub:false) + timelineView.manualScrolledAfterEnd = false + } else if reached.movieEnd { + accurateSeek(0, scrub:false) + timelineView.manualScrolledAfterEnd = false + } + if player != nil { + player!.play() + } + playerTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.playerTimerAction(_:)), userInfo: nil, repeats: true) + RunLoop.main.add(playerTimer, forMode:RunLoop.Mode.common) + playing = true + } + + func stop() { + playing = false + if asset == nil || player == nil { + return + } + player!.pause() + playerTimer.invalidate() + + if let receiver = playStatusReceiver { + receiver.videoTimelineStopped() + } + } + + var reachFlg = false + @objc func playerTimerAction(_ timer:Timer) { + if player == nil { + return + } + var currentPlayerTime = CMTimeGetSeconds(player!.currentTime()) + + let trim = timelineView.currentTrim() + let reached = timeReachesEnd(currentPlayerTime) + if timelineView.inAction() { + if player!.timeControlStatus == .playing { + player!.pause() + } + } else if reached.reached { + if repeatOn && reached.trimEnd { + + if player!.timeControlStatus == .playing { + player!.pause() + } + currentPlayerTime = trim.start + accurateSeek(currentPlayerTime, scrub:false) + reachFlg = true + + } else { + stop() + } + timelineView.setCurrentTime(currentPlayerTime,force:false) + timelineView.manualScrolledAfterEnd = false + } else if timelineView.animating == false { + timelineView.setCurrentTime(currentPlayerTime,force:false) + if player!.timeControlStatus == .paused { + player!.play() + } + if reachFlg { + if let receiver = playStatusReceiver { + receiver.videoTimelineMoved() + } + reachFlg = false + } + } + } + + func timeReachesEnd(_ time:Float64) -> (reached:Bool, trimEnd:Bool, movieEnd:Bool) { + var reached = false + var trimEnd = false + var movieEnd = false + if asset != nil { + let duration = CMTimeGetSeconds(asset!.duration) + let trimTimeEnd = timelineView.currentTrim().end + if (time >= trimTimeEnd && timelineView.manualScrolledAfterEnd == false && trimEnabled) { + trimEnd = true + reached = true + } + if time >= duration { + if trimTimeEnd < duration { + trimEnd = false + } + movieEnd = true + reached = true + } + } + return (reached, trimEnd, movieEnd) + } + + + //MARK: - + func resizeHeightKeepRatio(_ size:CGSize, height:CGFloat) -> CGSize { + var result = size + let ratio = size.width / size.height + result.height = height + result.width = height * ratio + return result + } +} diff --git a/kplayer/video/BMPlayer.swift b/kplayer/video/BMPlayer.swift index f602b11..f1002bd 100644 --- a/kplayer/video/BMPlayer.swift +++ b/kplayer/video/BMPlayer.swift @@ -190,7 +190,7 @@ open class BMPlayer: UIView { let size = asset.avURLAsset.videoSize() if (size.height < 1500) { - zoom = NetworkManager.sharedInstance.settings.scale + zoom = LocalManager.sharedInstance.settings.scale if (zoom < 1.0) { xpos = -100 }