Browse Source

Local Files

master
marcoschmickler 4 years ago
parent
commit
8a4cc09210
  1. 8
      download.js
  2. 48
      kplayer.xcodeproj/project.pbxproj
  3. 5
      kplayer/AppDelegate.swift
  4. 263
      kplayer/core/LocalManager.swift
  5. 27
      kplayer/core/MediaItem.swift
  6. 1
      kplayer/core/MediaModel.swift
  7. 200
      kplayer/core/NetworkManager.swift
  8. 12
      kplayer/detail/BrowserController.swift
  9. 25
      kplayer/detail/DetailViewController.swift
  10. 46
      kplayer/detail/EditItemView.swift
  11. 2
      kplayer/detail/ItemCell.swift
  12. 63
      kplayer/detail/VideoController.swift
  13. 4
      kplayer/master/KSettingsView.swift
  14. 62
      kplayer/master/MasterViewController.swift
  15. 34
      kplayer/master/NetworkDelegate.swift
  16. 30
      kplayer/master/ServerDownloadDelegate.swift
  17. 3
      kplayer/photo/PhotoController.swift
  18. 102
      kplayer/server/kplayer.js
  19. 15
      kplayer/server/links.html
  20. 126
      kplayer/timeline/CenterLine.swift
  21. 451
      kplayer/timeline/FrameImagesView.swift
  22. 206
      kplayer/timeline/TimelineMeasure.swift
  23. 178
      kplayer/timeline/TimelineScroller.swift
  24. 460
      kplayer/timeline/TimelineView.swift
  25. 773
      kplayer/timeline/TrimView.swift
  26. 301
      kplayer/timeline/VideoTimelineView.swift
  27. 2
      kplayer/video/BMPlayer.swift

8
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; i<lnks.length; ++i) {
var u = lnks[i].href
if (u.indexOf(".m3u8")>0) {
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)
}
}
}

48
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 = "<group>"; };
1C73611D226B48C24DB37535 /* MasterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = "<group>"; };
1C73615FFA2AA98BD1C56CD4 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = "<group>"; };
1C7361C26ED27AB54594317D /* CenterLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CenterLine.swift; path = timeline/CenterLine.swift; sourceTree = "<group>"; };
1C73620D01687FB4F1811C5C /* NetworkHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkHelper.swift; sourceTree = "<group>"; };
1C73625012D50E457D18A785 /* kplayer.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = kplayer.js; sourceTree = "<group>"; };
1C736253AB7A95EA41B605B7 /* ItemModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemModel.swift; sourceTree = "<group>"; };
1C736260E748CF136FF37EA7 /* UploadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadOperation.swift; sourceTree = "<group>"; };
1C73631C96E6C860833052CA /* ItemType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; };
1C736362946D7A8585B0D875 /* TimelineScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineScroller.swift; path = timeline/TimelineScroller.swift; sourceTree = "<group>"; };
1C7363E0DDA5854D55F8836E /* scratch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = scratch.txt; sourceTree = "<group>"; };
1C73645DBD6499A726D34973 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = "<group>"; };
1C7364709899FF62774B0199 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = "<group>"; };
1C73648CEC974A2500172064 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = "<group>"; };
1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDelegate.swift; sourceTree = "<group>"; };
1C7364F924BD979294C3EE4A /* BMPlayerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerItem.swift; sourceTree = "<group>"; };
1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDownloadDelegate.swift; sourceTree = "<group>"; };
1C73659CC9B523B957E58DC6 /* LocalManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalManager.swift; sourceTree = "<group>"; };
1C7365B06FA66294E99AC2D3 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
1C7365F45D765A218FFC100F /* BMPlayerProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerProtocols.swift; sourceTree = "<group>"; };
1C73661561AD069C92FE3B15 /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineView.swift; path = timeline/TimelineView.swift; sourceTree = "<group>"; };
1C7366AAB82A46086690E164 /* BMSubtitles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMSubtitles.swift; sourceTree = "<group>"; };
1C7366C09381DC0052B52B69 /* EditItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditItemView.swift; sourceTree = "<group>"; };
1C7366D766CDE0C9872E86F5 /* BMPlayerLayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerLayerView.swift; sourceTree = "<group>"; };
1C73673DC671535E3A049F54 /* PhotoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
1C73685B4BBFDAFBF08C032C /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = readme.md; sourceTree = "<group>"; };
1C73688DAB88F9360FB62A49 /* MediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
1C7368C7B946BC9E067D37E7 /* TimelineMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimelineMeasure.swift; path = timeline/TimelineMeasure.swift; sourceTree = "<group>"; };
1C736927EA28AFBEB25D7487 /* BMPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayer.swift; sourceTree = "<group>"; };
1C7369EC16B19B32B515169E /* NetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetData.swift; sourceTree = "<group>"; };
1C7369F53095B7A4D65679C2 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
1C736A5140D9B25BEC266B72 /* FrameImagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FrameImagesView.swift; path = timeline/FrameImagesView.swift; sourceTree = "<group>"; };
1C736A6E8396EE306B1AD3A8 /* KSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettingsView.swift; sourceTree = "<group>"; };
1C736ABA0E14A51ACAC84AB5 /* TrimView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TrimView.swift; path = timeline/TrimView.swift; sourceTree = "<group>"; };
1C736AE5021E3D985FE3402D /* KSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettings.swift; sourceTree = "<group>"; };
1C736B41C6AC33F3FA592C63 /* MediaModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaModel.swift; sourceTree = "<group>"; };
1C736B794396F2E50387B8F2 /* stringutil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = stringutil.swift; sourceTree = "<group>"; };
@ -117,6 +140,7 @@
1C736E51F1A03E3A1200BDB6 /* BMPlayerControlView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerControlView.swift; sourceTree = "<group>"; };
1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = "<group>"; };
1C736F9338CE36708244D42A /* DataLoadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataLoadOperation.swift; sourceTree = "<group>"; };
1C736FC4180B42C3A357E9BF /* VideoTimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VideoTimelineView.swift; path = timeline/VideoTimelineView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>";
@ -169,6 +194,7 @@
children = (
1C73625012D50E457D18A785 /* kplayer.js */,
1C73615FFA2AA98BD1C56CD4 /* links.html */,
1C73645DBD6499A726D34973 /* links.html */,
);
path = server;
sourceTree = "<group>";
@ -187,6 +213,7 @@
1C73611D226B48C24DB37535 /* MasterViewController.swift */,
1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */,
1C736A6E8396EE306B1AD3A8 /* KSettingsView.swift */,
1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */,
);
path = master;
sourceTree = "<group>";
@ -222,6 +249,7 @@
1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */,
1C736AE5021E3D985FE3402D /* KSettings.swift */,
1C736C94157754DE1C808173 /* KSettingsModel.swift */,
1C73659CC9B523B957E58DC6 /* LocalManager.swift */,
);
path = core;
sourceTree = "<group>";
@ -271,6 +299,7 @@
1C736059262A57AADE6AB761 /* Kirschkeks-256x256.png */,
8052F5B3AAC2535E5C08A529 /* Pods */,
1C73685B4BBFDAFBF08C032C /* readme.md */,
1C7363E0DDA5854D55F8836E /* scratch.txt */,
);
sourceTree = "<group>";
};
@ -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 = "<group>";
@ -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;
};

5
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

263
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<String, MediaItem>()
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
}
}

27
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)

1
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()

200
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 {
@ -40,168 +34,6 @@ class NetworkManager {
return URL(string: "http://linkstation: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)
}
}
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<String, MediaItem>()
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,7 +365,7 @@ class NetworkManager {
}
for c in item.children {
if let t = c.time {
let t = c.time
let ms = Int(t * 1000)
let p = c.snapshotDirPathForVideo + "\(ms).jpg"
let pt = c.snapshotDirPathForVideo + "\(ms)_thumb.jpg"
@ -561,7 +389,6 @@ class NetworkManager {
}
}
}
}
}
@ -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!

12
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

25
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()
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)

46
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))
}
}

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

63
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,7 +204,10 @@ 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: {
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)
@ -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)
}

4
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")

62
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)
}

34
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,7 +81,7 @@ class NetworkDelegate: MasterDelegate, DetailDelegate {
}
func deleteThumb(selectedItem c: MediaItem) {
if let t = c.time {
let t = c.time
let ms = Int(t * 1000)
let p = c.snapshotDirPathForVideo + "\(ms).jpg"
let pt = c.snapshotDirPathForVideo + "\(ms)_thumb.jpg"
@ -86,13 +89,36 @@ class NetworkDelegate: MasterDelegate, DetailDelegate {
NetworkManager.sharedInstance.deleteThumb(p)
NetworkManager.sharedInstance.deleteThumb(pt)
}
}
func saveItem(selectedItem: MediaItem) {
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
}
}

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

3
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

102
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)

15
kplayer/server/links.html

@ -0,0 +1,15 @@
<html>
<body>
<a href="https://www.google.de">Google</a>
<a href="https://www.xvideos.com">XVID</a>
<a href="https://www.xhamster.com">XVID</a>
<a href="https://www.chaturbate.com">gustav29 veberle12a</a>
<a href="https://www.stripchat.com">gustav29 ms39084st</a>
<a href="https://www.kink.com">XVID</a>
<a href="https://www.hegre.com">XVID</a>
<a href="https://www.facialabuse.com">XVID</a>
<a href="https://www.ghettogaggers.com">XVID</a>
</body>
</html>

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

451
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<Int>()
var belowFrames = Set<Int>()
var deepFrames = Set<Int>()
var hiddenFrames = Set<Int>()
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<Int>, below:Set<Int>, hidden:Set<Int>, deep:Set<Int>) {
let keyDivision = Int(pow(2,Double(Int(log2(maxWidth * 2 / (self.frame.size.width + 1))))))
if keyDivision <= 0 {
return (Set<Int>(), Set<Int>(), Set<Int>(), Set<Int>())
}
let keyCount = ((frameImagesArray.count - 1) / keyDivision) + 1
var visibleIndexes = Set<Int>()
var uponElements = Set<Int>()
for index in 0 ... (keyCount - 1) {
let uponIndex = index * keyDivision
uponElements.insert(uponIndex)
visibleIndexes.insert(uponIndex)
}
var belowElements = Set<Int>()
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<Int>()
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<Int>()
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<Int>()
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
}
}
}
}

206
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<UITouch>, 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<UITouch>, with event: UIEvent?)
{
if parentView!.allTouches.count == 2 {
if parentView!.pinching {
parentView!.updatePinch()
} else {
parentView!.startPinch()
}
}
}
override open func touchesEnded(_ touches: Set<UITouch>, 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<UITouch>, 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()
}
}
}

178
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<UITouch>, 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<UITouch>, with event: UIEvent?)
{
if parentView!.allTouches.count == 2 {
if parentView!.pinching {
parentView!.updatePinch()
} else {
parentView!.startPinch()
}
}
}
override open func touchesEnded(_ touches: Set<UITouch>, 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<UITouch>, 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()
}
}
}

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

773
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<UITouch>, 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<UITouch>, 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<UITouch>, 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<UITouch>, 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
}
}

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

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

Loading…
Cancel
Save