Browse Source

Swift Video 3

master
marcoschmickler 4 years ago
parent
commit
380ac64c91
  1. 8
      kplayer.xcodeproj/project.pbxproj
  2. 1
      kplayer/AppDelegate.swift
  3. 4
      kplayer/core/MediaItem.swift
  4. 30
      kplayer/detail/DetailViewController.swift
  5. 2
      kplayer/detail/VideoController.swift
  6. 4
      kplayer/svideo/SVideoModel.swift
  7. 191
      kplayer/svideo/SVideoPlayer.swift
  8. 16
      kplayer/util/KNetworkProtocol.swift
  9. 9
      kplayer/util/VideoHelper.swift

8
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 = "<group>"; };
1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HtmlParser.swift; sourceTree = "<group>"; };
1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = "<group>"; };
1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KNetworkProtocol.swift; sourceTree = "<group>"; };
1C736DFBD072763248412F74 /* BMPlayerClearityChooseButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerClearityChooseButton.swift; sourceTree = "<group>"; };
1C736E2CD0C1780F4F5AE0C4 /* KVideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KVideoPlayer.swift; sourceTree = "<group>"; };
1C736E51F1A03E3A1200BDB6 /* BMPlayerControlView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BMPlayerControlView.swift; sourceTree = "<group>"; };
@ -247,6 +249,7 @@
1C7364709899FF62774B0199 /* VideoHelper.swift */,
1C736677D4EF2437358B2387 /* Utility.swift */,
1C7360B6D0757D4FB6433E7B /* AsyncImage.swift */,
1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */,
);
path = util;
sourceTree = "<group>";
@ -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";

1
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

4
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.
*/

30
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

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

4
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

191
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 {

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

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

Loading…
Cancel
Save