diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index 0196b74..76641e3 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 1C7362AF931E0F228E5D2AED /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360B53C4C1496320953C2 /* VideoPlayerView.swift */; }; 1C73631EACF56BABD3B2BCFB /* LayoutTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736BC4450890C45F8FBC63 /* LayoutTools.swift */; }; 1C73633AAF0D77F8AC3557B9 /* SVideoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7362603E8588B4D1A8C617 /* SVideoModel.swift */; }; + 1C73633C00C18FDA2E9F0A2F /* KNetworkProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736DCD945ABAE984FF43EF /* KNetworkProtocol.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 */; }; @@ -147,6 +148,7 @@ 1C736D9BB5498E7E8F11C754 /* HeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderCell.swift; sourceTree = ""; }; 1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HtmlParser.swift; sourceTree = ""; }; 1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; + 1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KNetworkProtocol.swift; sourceTree = ""; }; 1C736DFBD072763248412F74 /* BMPlayerClearityChooseButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerClearityChooseButton.swift; sourceTree = ""; }; 1C736E2CD0C1780F4F5AE0C4 /* KVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KVideoPlayer.swift; sourceTree = ""; }; 1C736E51F1A03E3A1200BDB6 /* BMPlayerControlView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerControlView.swift; sourceTree = ""; }; @@ -247,6 +249,7 @@ 1C7364709899FF62774B0199 /* VideoHelper.swift */, 1C736677D4EF2437358B2387 /* Utility.swift */, 1C7360B6D0757D4FB6433E7B /* AsyncImage.swift */, + 1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */, ); path = util; sourceTree = ""; @@ -608,6 +611,7 @@ 1C7362AF931E0F228E5D2AED /* VideoPlayerView.swift in Sources */, 1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */, 1C736998044A9A7D89411892 /* AsyncImage.swift in Sources */, + 1C73633C00C18FDA2E9F0A2F /* KNetworkProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -696,7 +700,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.3; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -745,7 +749,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.3; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/kplayer/AppDelegate.swift b/kplayer/AppDelegate.swift index 82b8ebf..5152697 100644 --- a/kplayer/AppDelegate.swift +++ b/kplayer/AppDelegate.swift @@ -16,6 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // URLProtocol.registerClass(KNetworkProtocol.self) // Override point for customization after application launch. let splitViewController = self.window!.rootViewController as! UISplitViewController let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 84a0095..626bb42 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -190,6 +190,10 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { return NetworkManager.sharedInstance.baseurl + "/service/download" + enc } + var nameWithoutExtension: String { + name.replacingOccurrences(of: ".mp4", with: "") + } + /** Absolute URL, unter der das Image des Items abgerufen werden kann. */ diff --git a/kplayer/detail/DetailViewController.swift b/kplayer/detail/DetailViewController.swift index 3fa5341..9dcfc1c 100644 --- a/kplayer/detail/DetailViewController.swift +++ b/kplayer/detail/DetailViewController.swift @@ -165,6 +165,10 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout var i = [MediaItem]() if let d = detailItem { + if delegate!.settings().newPlayer { + showAll(d) + return + } if (d.local) { showComposition(d) return @@ -190,6 +194,29 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout } } + private func showAll(_ item: MediaItem) { + let composition = MediaItem(name: item.name, path: item.path, root: item.root, type: ItemType.VIDEO) + + for d in item.children { + if d.children.isEmpty { + let clone = d.clone() + clone.parent = composition + clone.type = ItemType.SNAPSHOT + composition.children.append(clone) + } + else { + for c in d.children { + let clone = c.clone() + clone.parent = composition + composition.children.append(clone) + } + } + } + + showNewVideo(selectedItem: composition.children[0]) + } + + private func showComposition(_ item: MediaItem) { var assets = [URL]() @@ -486,6 +513,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout let model = SVideoModel(allItems: children, currentSnapshot: se, baseItem: baseItem) + model.edit = delegate!.settings().edit + model.loop = delegate!.settings().autoloop + let player = SVideoPlayer(completionHandler: { saved in if saved { baseItem.children = model.allItems diff --git a/kplayer/detail/VideoController.swift b/kplayer/detail/VideoController.swift index 9405eb0..0f89093 100644 --- a/kplayer/detail/VideoController.swift +++ b/kplayer/detail/VideoController.swift @@ -322,7 +322,7 @@ class VideoController: UIViewController, ItemController, BMPlayerDelegate, EditI return } - VideoHelper.export(item: player.avPlayer!.currentItem!, clipStart: loopStart, clipDuration: dur, file: file) { url in + VideoHelper.export(item: player.avPlayer!.currentItem!, clipStart: loopStart, clipDuration: dur, file: file, progress: { p in print(p) }) { url in self.showAlert(title: c.name, message: "saved") } var s : MediaModel; diff --git a/kplayer/svideo/SVideoModel.swift b/kplayer/svideo/SVideoModel.swift index d78f059..6c1de7f 100644 --- a/kplayer/svideo/SVideoModel.swift +++ b/kplayer/svideo/SVideoModel.swift @@ -20,11 +20,13 @@ class SVideoModel : ObservableObject { @Published var paused = false - @Published var edit = true + @Published var edit = false @Published var loop = false @Published var speed: Float = 1.0 + @Published var currentURL: URL? + @Published var scale: CGFloat = 1.0 @Published var dragOffset: CGSize = CGSize.zero diff --git a/kplayer/svideo/SVideoPlayer.swift b/kplayer/svideo/SVideoPlayer.swift index 2ae4f87..b7b8c16 100644 --- a/kplayer/svideo/SVideoPlayer.swift +++ b/kplayer/svideo/SVideoPlayer.swift @@ -10,6 +10,7 @@ import AVKit struct SVideoPlayer: View, EditItemDelegate { // url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")! var player = AVQueuePlayer(items: [AVPlayerItem]()) + var playerLooper : AVPlayerLooper var completionHandler: ((Bool) -> Void)? @ObservedObject @@ -18,16 +19,19 @@ struct SVideoPlayer: View, EditItemDelegate { @State private var lastScaleValue: CGFloat = 1.0 @State private var lastDragOffset: CGSize = CGSize.zero + @State var savetext = "save" + @State var confirmationShown = false @State var seekSmoothly = false @State var smoothTime = -1.0 @State var smoothSeekTime = -1.0 + @State var timeCounter = 0 let steps : [Float] = [0.25, 0.5, 1.0, 2.0 ] init(completionHandler: ((Bool) -> ())?, model: SVideoModel) { self.completionHandler = completionHandler self.model = model - + self.playerLooper = AVPlayerLooper(player: player , templateItem: model.currentPlayerItem()) } func cleanup() { @@ -62,6 +66,25 @@ struct SVideoPlayer: View, EditItemDelegate { }, label: { Text("\(model.speed, specifier: "%.2f")") }).buttonStyle(BorderlessButtonStyle()) + + Button(action: { confirmationShown = true }, label: { + Text(savetext) + }).buttonStyle(BorderlessButtonStyle()) + .confirmationDialog("Save to folder", isPresented: $confirmationShown) { + Button("1") { + save(currentSnapshot: model.currentSnapshot, name: "1") + } + Button("2") { + save(currentSnapshot: model.currentSnapshot, name: "2") + } + Button("3") { + save(currentSnapshot: model.currentSnapshot, name: "3") + } + + Button("cancel", role: .cancel) {} + } + + Text(model.currentSnapshot.name).foregroundColor(Color.blue) ScrollView(.horizontal, showsIndicators: false) { @@ -135,21 +158,34 @@ struct SVideoPlayer: View, EditItemDelegate { } .onAppear() { model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: nil) { time in - + if timeCounter >= 1 { + timeCounter -= 1 + } if model.loop && model.currentSnapshot.length > 0 { if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length { - seekTimeSmoothly(model.currentSnapshot.time) + seekTime(model.currentSnapshot.time) } } + } + player.automaticallyWaitsToMinimizeStalling = false + let item = model.currentPlayerItem() + + item.preferredForwardBufferDuration = 2.0 self.player.replaceCurrentItem(with: item) + model.currentURL = model.currentSnapshot.playerURL + // player.removeAllItems() // player.insert(model.currentPlayerItem(), after: nil) // Start the player going, otherwise controls don't appear - gotoSnapshot(model.currentSnapshot) - player.play() + // player.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + gotoSnapshot(model.currentSnapshot) + } + } .onDisappear { // When this View isn't being shown anymore stop the player @@ -160,23 +196,21 @@ struct SVideoPlayer: View, EditItemDelegate { } func doSnapshot() { - // if edit { let currentItem = player.currentItem! let asset = currentItem.asset - do { - let imgGenerator = AVAssetImageGenerator(asset: asset) - imgGenerator.appliesPreferredTrackTransform = true - let time = currentItem.currentTime() - let cgImage = try imgGenerator.copyCGImage(at: time, actualTime: nil) - let thumbnail = UIImage(cgImage: cgImage) + do { + let imgGenerator = AVAssetImageGenerator(asset: asset) + imgGenerator.appliesPreferredTrackTransform = true + let time = currentItem.currentTime() + let cgImage = try imgGenerator.copyCGImage(at: time, actualTime: nil) + let thumbnail = UIImage(cgImage: cgImage) - showThumbnail(currentItem: model.baseItem, thumbnail: thumbnail, time: time) + showThumbnail(currentItem: model.baseItem, thumbnail: thumbnail, time: time) - } catch let error { - print("*** Error generating thumbnail: \(error.localizedDescription)") - } - // } + } catch let error { + print("*** Error generating thumbnail: \(error.localizedDescription)") + } } func showThumbnail(currentItem: MediaItem, thumbnail: UIImage, time: CMTime) { @@ -187,6 +221,7 @@ struct SVideoPlayer: View, EditItemDelegate { newItem.local = currentItem.local newItem.external = currentItem.external model.allItems.append(newItem) + model.currentSnapshot = newItem } private func move(_ dragged: CGSize, start: CGPoint) -> Bool { @@ -223,22 +258,34 @@ struct SVideoPlayer: View, EditItemDelegate { return true } if dragged.width > dragWidth { - seekTimeSmoothly(time + 5.0) + seekTime(time + 8.0) } else if dragged.width < -dragWidth { - seekTimeSmoothly(time - 5.0) + seekTime(time - 10.0) } } else { if dragged.width > dragWidth { - seekTimeSmoothly(time + 30.0) + seekTime(time + 30.0) } else if dragged.width < -dragWidth { - seekTimeSmoothly(time - 30.0) + seekTime(time - 30.0) } } model.seeking = sk } + if dragged.height > 100 && timeCounter == 0 { + if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) { + if i + 1 < model.allItems.count { + gotoSnapshot(model.allItems[i+1]) + } + else { + gotoSnapshot(model.allItems[0]) + } + timeCounter = 20 + } + } + return false } @@ -247,8 +294,21 @@ struct SVideoPlayer: View, EditItemDelegate { } func seekTime(_ time: Double) { - // print(time) - player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000))) + print("Seek \(time)") + player.pause() + // player.cancelPendingPrerolls() + + if let item = player.currentItem { + // item.cancelPendingSeeks() + } + + player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000)), + toleranceBefore: CMTime.zero, + toleranceAfter: CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(10000))){ _ in + player.play() + player.rate = model.speed + } + } func seekTimeSmoothly(_ time: Double) { @@ -277,11 +337,22 @@ struct SVideoPlayer: View, EditItemDelegate { } func gotoSnapshot(_ currentSnapshot: MediaItem) { - seekTime(currentSnapshot.time) - // player.forceSeekSmoothlyToTime(newChaseTime: currentSnapshot.time) -// loopStart = currentSnapshot.time -// player.loopEnd = loopStart + currentSnapshot.length -// + model.currentSnapshot = currentSnapshot + + if currentSnapshot.playerURL != model.currentURL { + model.currentURL = currentSnapshot.playerURL + player.insert(model.currentPlayerItem(), after: player.currentItem) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + player.advanceToNextItem() + // seekTime(currentSnapshot.time) + } + + // player.replaceCurrentItem(with: model.currentPlayerItem()) + } + else { + seekTime(currentSnapshot.time) + } // if loopMode && currentSnapshot.scale > 0 { // player.zoom = Float(currentSnapshot.scale) // player.xpos = currentSnapshot.offset.x @@ -289,14 +360,11 @@ struct SVideoPlayer: View, EditItemDelegate { // player.transformLayer() // } - model.currentSnapshot = currentSnapshot } func captureZoom() { model.currentSnapshot.scale = model.scale model.currentSnapshot.offset = CGPoint(x: model.dragOffset.width, y: model.dragOffset.height) - - //model.objectWillChange() } func setStart() { @@ -321,6 +389,67 @@ struct SVideoPlayer: View, EditItemDelegate { func seek(_ v: Double) { seekTimeSmoothly(v) } + + func save(currentSnapshot c: MediaItem, name: String) { + do { + try FileHelper.createDir(name: name) + + var file = FileHelper.getDocumentsDirectory().appendingPathComponent(name).appendingPathComponent("\(c.nameWithoutExtension)_\(Int(c.time))") + + if file.pathExtension != "mp4" { + file = file.appendingPathExtension("mp4") + } + print (file) + var dur = c.length + if (dur < 0) { + return + } + player.pause() + VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, + progress: { p in + let percent = Int(p * 100) + savetext = "\(percent)" + }) + { url in + savetext = "saved" + } + var s : MediaModel; + if (c.type == ItemType.SNAPSHOT && c.parent != nil) { + s = c.parent!.toMediaModel() + do { + if c.thumbUrlAbsolute != "" { + let local = try Data(contentsOf: URL(string: c.thumbUrlAbsolute)!) + let tfile = FileHelper.getDocumentsDirectory() + .appendingPathComponent(name) + .appendingPathComponent("\(c.nameWithoutExtension)_\(Int(c.time))") + .appendingPathExtension("mp4") + .appendingPathExtension("jpg") + try local.write(to: tfile) + } + } + catch { + // ignore + } + } + else { + s = c.toMediaModel() + } + + let jfile = FileHelper.getDocumentsDirectory() + .appendingPathComponent(name) + .appendingPathComponent("\(c.nameWithoutExtension)_\(Int(c.time))") + .appendingPathExtension("mp4") + .appendingPathExtension("orig") + let jsonEncoder = JSONEncoder() + let jsonData = try! jsonEncoder.encode(s) + let json = String(data: jsonData, encoding: String.Encoding.utf8) + print(jfile.absoluteString) + print(json) + try json!.write(to: jfile, atomically: true, encoding: .utf8) + } catch { + print(error) + } + } } struct SVideoPlayer_Previews: PreviewProvider { diff --git a/kplayer/util/KNetworkProtocol.swift b/kplayer/util/KNetworkProtocol.swift new file mode 100644 index 0000000..3caa342 --- /dev/null +++ b/kplayer/util/KNetworkProtocol.swift @@ -0,0 +1,16 @@ +// +// Created by Marco Schmickler on 06.12.21. +// Copyright (c) 2021 Marco Schmickler. All rights reserved. +// + +import Foundation + +var requestCount = 0 + +class KNetworkProtocol: URLProtocol { + override class func canInit(with request: URLRequest) -> Bool { + print("Request #\(requestCount): URL = \(request.url!.absoluteString)") + requestCount += 1 + return false + } +} \ No newline at end of file diff --git a/kplayer/util/VideoHelper.swift b/kplayer/util/VideoHelper.swift index 1f4efdb..a288685 100644 --- a/kplayer/util/VideoHelper.swift +++ b/kplayer/util/VideoHelper.swift @@ -7,7 +7,7 @@ import Foundation import AVFoundation class VideoHelper { - public static func export(item: AVPlayerItem, clipStart: Double, clipDuration: Double, file: URL, completion: @escaping (URL?) -> ()) { + public static func export(item: AVPlayerItem, clipStart: Double, clipDuration: Double, file: URL, progress: @escaping (Float) -> (), completion: @escaping (URL?) -> ()) { guard item.asset.isExportable else { completion(nil) return @@ -52,7 +52,14 @@ class VideoHelper { try FileManager.default.removeItem(at: file) } catch { } + + let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in + progress(exportSession.progress) + } + exportSession.exportAsynchronously { + timer.invalidate() + print("ready \(exportSession.error)") // let data = try? Data(contentsOf: tempFileUrl) // _ = try? FileManager.default.removeItem(at: tempFileUrl)