Browse Source

power

master
marcoschmickler 3 years ago
parent
commit
31aea52267
  1. 8
      kplayer.xcodeproj/project.pbxproj
  2. 3
      kplayer/AppDelegate.swift
  3. 37
      kplayer/core/DatabaseManager.swift
  4. 2
      kplayer/core/MediaItem.swift
  5. 119
      kplayer/core/NetworkManager.swift
  6. 2
      kplayer/detail/EditItemView.swift
  7. 6
      kplayer/detail/ItemCell.swift
  8. 10
      kplayer/master/KSettingsView.swift
  9. 2
      kplayer/master/NetworkDelegate.swift
  10. 2
      kplayer/master/SearchItemView.swift
  11. 76
      kplayer/server/kplayer.js
  12. 54
      kplayer/server/raspberrypi.js
  13. 135
      kplayer/video/SRangeSlider.swift
  14. 180
      kplayer/video/SVideoPlayer.swift
  15. 22
      kplayer/video/VideoPlayerView.swift

8
kplayer.xcodeproj/project.pbxproj

@ -31,6 +31,7 @@
1C7366BEA68D6E4CEE43417E /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736CC8D90375E86CD01964 /* ImageLoader.swift */; };
1C7366FE5C760C8D5117207F /* SVideoLoopPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */; };
1C7366FF0651A802F09936D6 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736C17C40DAE162AF8DDE3 /* WebViewModel.swift */; };
1C73670151CCBC714795807F /* SRangeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */; };
1C7367084839D2E8B180DB74 /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360295486647982CFEACF /* UIViewController+Alert.swift */; };
1C73671FC2CCCACAA2FFC153 /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */; };
1C73675C34BE0990D44570BE /* ItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736253AB7A95EA41B605B7 /* ItemModel.swift */; };
@ -68,6 +69,7 @@
1C736DFA8544C773E3C22F10 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */; };
1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736677D4EF2437358B2387 /* Utility.swift */; };
1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B41C6AC33F3FA592C63 /* MediaModel.swift */; };
1C736E5C86EFC8E8C1ABA131 /* raspberrypi.js in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367DB924D9AE21DD8D0F2 /* raspberrypi.js */; };
1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */; };
1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; };
1C736EFF1E09988625FF770C /* hints.md in Sources */ = {isa = PBXBuildFile; fileRef = 1C736BF48D5CE855B6E75BE6 /* hints.md */; };
@ -142,6 +144,7 @@
1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
1C736777456388CA571DA17B /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SEmbeddedVideo.swift; sourceTree = "<group>"; };
1C7367DB924D9AE21DD8D0F2 /* raspberrypi.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = raspberrypi.js; sourceTree = "<group>"; };
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>"; };
@ -168,6 +171,7 @@
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>"; };
1C736E32C8574BFE3536F1C2 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRangeSlider.swift; sourceTree = "<group>"; };
1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = "<group>"; };
1C736EEC570C71AAC50F2E95 /* SVideoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVideoModel.swift; sourceTree = "<group>"; };
1C736EF64DE56AD058A4F612 /* KBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KBrowserView.swift; sourceTree = "<group>"; };
@ -238,6 +242,7 @@
1C73615FFA2AA98BD1C56CD4 /* links.html */,
1C73645DBD6499A726D34973 /* links.html */,
1C73619BBFA9295A11C9ACBA /* grabber.js */,
1C7367DB924D9AE21DD8D0F2 /* raspberrypi.js */,
);
path = server;
sourceTree = "<group>";
@ -331,6 +336,7 @@
1C73661C3F9F4E53645551AD /* KToggleButton.swift */,
1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */,
1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */,
1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */,
);
path = video;
sourceTree = "<group>";
@ -672,6 +678,8 @@
1C736F2334FE0F946FD7CABE /* ImageCache.swift in Sources */,
1C7366BEA68D6E4CEE43417E /* ImageLoader.swift in Sources */,
1C736EFF1E09988625FF770C /* hints.md in Sources */,
1C73670151CCBC714795807F /* SRangeSlider.swift in Sources */,
1C736E5C86EFC8E8C1ABA131 /* raspberrypi.js in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

3
kplayer/AppDelegate.swift

@ -29,6 +29,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
LocalManager.sharedInstance.loadSettings()
NetworkManager.sharedInstance.wakeLinkstation()
let url = URL(string: NetworkManager.sharedInstance.vidurl)?.appendingPathComponent("ren").appendingPathComponent("kplayer.txt")
var roots = [MediaItem]()
@ -61,6 +63,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
roots.append(LocalManager.sharedInstance.favorites)
roots.append(MediaItem(name: "extern", path:"", root: "", type: ItemType.FAVROOT))
roots.append(MediaItem(name: "tags", path:"", root: "", type: ItemType.TAGROOT))
roots.append(MediaItem(name: "models", path:"models", root: "", type: ItemType.TAGROOT))
roots.append(web)
controller.model.items = roots

37
kplayer/core/DatabaseManager.swift

@ -13,7 +13,7 @@ class DatabaseManager {
let managedObjectContext : NSManagedObjectContext
let persistentContainer : NSPersistentContainer
var allTags = [String]()
var allTags = [String : [String]]()
init() {
self.persistentContainer = KPersistentContainer(defaultContainerWithName: "kplayer")
@ -49,12 +49,14 @@ class DatabaseManager {
c.rating = Int(s.rating)
c.options = s.options ?? ""
for t in s.tags as! Set<KTag> {
c.tags.append(t.name!)
if let tags = s.tags as? Set<KTag> {
for t in tags {
c.tags.append(t.name!)
}
}
}
}
print(s)
// print(s)
}
return
@ -251,9 +253,10 @@ rollback()
}
func createTag(_ answer: String) {
func createTag(_ answer: String, path: String = "") {
let tag = KTag(context: managedObjectContext)
tag.name = answer
tag.path = path
save()
@ -268,7 +271,17 @@ rollback()
allTags.removeAll()
for t in results {
allTags.append(t.name!)
var path = ""
if t.path != nil {
path = t.path!
}
var tags = allTags[path]
if tags == nil {
tags = [String]()
}
tags!.append(t.name!)
allTags[path] = tags
}
}
@ -310,7 +323,7 @@ rollback()
}
}
func loadTags(completionHandler: @escaping Weiter) -> Void {
func loadTags(path: String, completionHandler: @escaping Weiter) -> Void {
var res = [MediaItem]()
let fetchRequest = KTag.fetchRequest()
@ -318,6 +331,16 @@ rollback()
let results = try! managedObjectContext.fetch(fetchRequest)
for t in results {
if let p = t.path {
if path != p {
continue
}
}
else {
if path != "" {
continue
}
}
let tag = MediaItem(name: t.name!, path: t.name!, root: "tags", type: ItemType.TAG)
tag.loaded = true
let snapshots = t.tagged as! Set<KSnapshot>

2
kplayer/core/MediaItem.swift

@ -220,7 +220,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
}
let enc = thumbUrl!.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
return NetworkManager.sharedInstance.baseurl + "/service/download/srv/samba" + enc.substringStartingFrom(10)
return NetworkManager.sharedInstance.vidurl + enc.substringStartingFrom(10)
}
var nameWithoutExtension: String {

119
kplayer/core/NetworkManager.swift

@ -18,6 +18,9 @@ class NetworkManager {
var currentDownloads = [String : MediaItem]()
var black = [String]()
var blackLoaded = false
lazy var operationQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "Backup queue"
@ -46,7 +49,7 @@ class NetworkManager {
if root.endsWith("/") {
root = String(root[root.startIndex..<root.index(before: root.endIndex)])
}
let url1 = baseurl + "/service/listvideos" + rootParam
let url1 = nodeurl + "listvideos" + rootParam
let len = root.count
var res = [MediaItem]()
let url = (url1 as NSString).replacingOccurrences(of: " ", with: "+")
@ -90,7 +93,7 @@ class NetworkManager {
root = String(root[root.startIndex..<root.index(before: root.endIndex)])
}
let url1 = baseurl + "/service/listpicdirs" + rootParam
let url1 = nodeurl + "listpicdirs" + rootParam
let len = root.count
var res = [MediaItem]()
let url = (url1 as NSString).replacingOccurrences(of: " ", with: "+")
@ -107,15 +110,20 @@ class NetworkManager {
for s in result {
if s.lowercased().hasSuffix(".jpg") {
var rootlen = len
let l = s.count
if s.hasPrefix("/srv/samba/ren/thumbs/") {
rootlen += 7
}
let name = (s as NSString).lastPathComponent
var pathlen = l - len - name.count
var pathlen = l - rootlen - name.count
if (pathlen < 2) {
pathlen = 2
}
let path = (s as NSString).substring(with: NSMakeRange(len + 1, pathlen - 2))
let path = (s as NSString).substring(with: NSMakeRange(rootlen + 1, pathlen - 2))
let folderName = NSURL(fileURLWithPath: path).lastPathComponent!
let fl = path.count
@ -132,10 +140,27 @@ class NetworkManager {
} else {
let iff = MediaItem(name: folderName, path: fpath, root: root, type: ItemType.PICS)
iff.thumbUrl = i.thumbUrl
// var model = folderName.split(separator: "-")[0]
// print(model)
// var found = false
// if let models = DatabaseManager.sharedInstance.allTags["models"] {
// if models.contains(where: { $0 == model }) {
// found = true
// }
// }
//
// if !found {
// DatabaseManager.sharedInstance.createTag(String(model), path: "models")
// }
//
// iff.tags.append(String(model))
items[path] = iff
res.append(iff)
iff.children.append(i)
//
// DatabaseManager.sharedInstance.saveItemMetaData(iff)
}
}
DatabaseManager.sharedInstance.enrichItem(i)
@ -362,12 +387,98 @@ class NetworkManager {
}
}
func wakeLinkstation() {
let url = "http://raspberrypi.local:8081/wakelinkstation"
AF.request(url).responseString { response in
print("wake \(response)")
}
}
func suspendLinkstation() {
let url = "http://raspberrypi.local:8081/suspendlinkstation"
AF.request(url).responseString { response in
print("suspend \(response)")
}
}
func favItem(_ item: MediaItem) {
let url = baseurl + "/service/linkfav" + item.fullPath
AF.request(url)
}
func isBlack(_ item: MediaItem) -> Bool {
let url = URL(string: NetworkManager.sharedInstance.vidurl)?.appendingPathComponent("ren").appendingPathComponent("black.txt")
if !blackLoaded {
do {
let remoteroots = try String(contentsOf: url!)
let components = remoteroots.split(separator: "\n")
for c in components {
if c.starts(with: "rm ") {
let s = String(c.suffix(c.count - 3))
black.append(s)
}
}
} catch {
print("Exception")
}
blackLoaded = true
}
let p = item.fullPath
for c in black {
if p == c {
return true
}
}
return false
}
func blackItem(_ item: MediaItem) {
let p = item.fullPath
let url = nodeurl + "black" + p
black.append(p)
print(url)
AF.request(url).responseString { response in
print("black \(response)")
}
}
func cutItem(_ item: MediaItem) {
var file = item.fullPath
var start = "\(item.time)"
var length = "\(item.length)"
var newfile = file.replacingOccurrences(of: "/srv/samba/ren", with: "/srv/samba/ren/cut")
newfile = newfile.replacingOccurrences(of: ".mp4", with: "_" + start + ".mp4")
let furl = URL(fileURLWithPath: newfile)
let dirUrl = furl.deletingLastPathComponent().path
var line = "mkdir -p " + dirUrl + "; ffmpeg -i '" + file
line += "' -ss " + start + " -t " + length + " -vcodec copy -acodec copy " + newfile
print(line)
let url = nodeurl + "cut?line=" + line.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
AF.request(url).responseString { response in
print("cut \(response)")
}
}
func truncateItem(_ item: MediaItem) {
var file = item.fullPath
var start = "\(item.time)"
print(file)
let url = nodeurl + "truncate?line=" + file.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!+"&start="+start
AF.request(url).responseString { response in
print("truncate \(response)")
}
}
func saveItem(_ item: MediaItem) {
var ch = item.children
// print(ch)

2
kplayer/detail/EditItemView.swift

@ -27,7 +27,7 @@ struct TagEditor: View {
var body: some View {
FlexibleView(
data: DatabaseManager.sharedInstance.allTags,
data: DatabaseManager.sharedInstance.allTags[""]!,
spacing: 15,
alignment: .leading
) { tag in

6
kplayer/detail/ItemCell.swift

@ -8,6 +8,7 @@ import UIKit
import Haneke
var defaultImage = UIImage(named: "Kirschkeks-256x256.png")
var blackImage = UIImage(systemName: "0.circle")
class ItemCell: UICollectionViewCell {
var item: MediaItem?
@ -71,6 +72,11 @@ class ItemCell: UICollectionViewCell {
loop.isHidden = item.length == 0.0
if NetworkManager.sharedInstance.isBlack(item) {
image.image = blackImage
return
}
if let _ = item.thumbUrl, let nsurl = URL(string: item.thumbUrlAbsolute) {
image.hnk_setImageFromURL(nsurl, placeholder: defaultImage)
} else {

10
kplayer/master/KSettingsView.swift

@ -38,6 +38,16 @@ struct KSettingsView: View {
})
}
}
Button(action: {
NetworkManager.sharedInstance.suspendLinkstation()
}, label: {
Text("suspend")
});
Button(action: {
NetworkManager.sharedInstance.wakeLinkstation()
}, label: {
Text("wake")
});
Button(action: {
LocalManager.sharedInstance.saveSettings()
self.completionHandler?()

2
kplayer/master/NetworkDelegate.swift

@ -53,7 +53,7 @@ class NetworkDelegate: MasterDelegate, DetailDelegate {
if selectedItem.type == ItemType.TAGROOT {
DatabaseManager.sharedInstance.loadTags(completionHandler: {
DatabaseManager.sharedInstance.loadTags(path: selectedItem.path, completionHandler: {
c in
selectedItem.children = c
completionHandler(selectedItem)

2
kplayer/master/SearchItemView.swift

@ -15,7 +15,7 @@ struct SearchItemView: View {
var body: some View {
VStack {
FlexibleView(
data: DatabaseManager.sharedInstance.allTags,
data: DatabaseManager.sharedInstance.allTags[""]!,
spacing: 15,
alignment: .leading
) { tag in

76
kplayer/server/kplayer.js

@ -135,28 +135,35 @@ app.get('/listpicdirs/*', function (req, res) {
if ((file.isDirectory() || file.isSymbolicLink()) && !filename.startsWith(".")) {
var folders = getFiles(address + "/" + filename)
var hasFav = true
if (folders.filter(dirent => dirent.name.endsWith(".mp4")).length > 0) {
filetype = "video"
}
else if (folders.filter(dirent => dirent.name.endsWith(".html")).length > 0) {
filetype = "web"
}
// else if (folders.filter(dirent => dirent.name.endsWith(".jpg")).length > 0) {
else {
folders.forEach(function (f) {
if (f.isDirectory()) {
if (hasPics(address + "/" + filename + "/" + f.name)) {
filetype = "pictures"
if (hasFav && f.name.toLowerCase().endsWith(".jpg")) {
var p = address + "/" + filename
p = p.replace("/srv/samba/ren", "/srv/samba/ren/favpic")
var fav = getFiles(p)
fav.forEach(function (g) {
result.push(address + "/" + filename + "/" + g.name)
console.log("f--" + g.name)
hasFav = false
})
if (hasFav) {
console.log("--" + f.name)
var n = address + "/" + filename + "/" + f.name
var r = n.replace("/srv/samba/ren", "/srv/samba/ren/thumbs")
if (fs.existsSync(r)) {
n = r
}
result.push(n)
hasFav = false
}
}
})
}
console.log(filename)
result.push([filename, filetype])
}
})
@ -167,7 +174,7 @@ app.get('/listvideos/*', function (req, res) {
var address = decodeURIComponent(req.path.substr(11))
console.log("listdirs " + address);
if (!req.path.startsWith("/listfiles/srv/samba/ren/")) {
if (!req.path.startsWith("/listvideos/srv/samba/ren/")) {
res.end("Access denied")
return
}
@ -432,6 +439,43 @@ app.get('/deleteThumb/*', function (req, res) {
res.send("ok")
})
app.get('/black/*', function (req, res) {
if (req.path.startsWith("/black/srv/samba/ren/")) {
var p = req.path.substr(6)
fs.appendFileSync('/srv/samba/ren/black.txt', 'rm '+p+'\ntouch '+p+'.black\n');
}
res.send("ok")
})
app.get('/cut', function (req, res) {
var p = req.query.line
fs.appendFileSync('/srv/samba/ren/cut.txt', p+'\n');
res.send("ok")
})
app.get('/truncate', function (req, res) {
var p = req.query.line
var s = req.query.start
var n = p.replace(".mp4", ".orig.mp4")
var cmd = 'mv "'+p+'" "' + n + '"; ffmpeg -i "'+n+'" -ss ' + s + ' -codec copy -movflags faststart "'+p+'"'
console.log(cmd)
exec(cmd, (err, stdout, stderr) => {
if (err) {
console.log(err)
// node couldn't execute the command
return;
}
// the *entire* stdout and stderr (buffered)
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
res.send("ok")
})
//createDirectories(pathname) {}
app.post('/upload', upload.any() , (req, res, next) => {

54
kplayer/server/raspberrypi.js

@ -0,0 +1,54 @@
// pm2 restart kplayer
// /srv/samba/daten/node
var express = require('express');
var fs = require("fs");
var path = require("path");
const { exec, execSync} = require('child_process');
var app = express();
app.get('/', function(req, res) {
res.json({ message: 'WELCOME' });
});
app.get('/wakelinkstation', function (req, res) {
var cmd = '/srv/raspi/bin/wakeup-linkstation.sh'
console.log(cmd)
exec(cmd, (err, stdout, stderr) => {
if (err) {
console.log(err)
// node couldn't execute the command
return;
}
// the *entire* stdout and stderr (buffered)
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
res.send("ok")
})
app.get('/suspendlinkstation', function (req, res) {
var cmd = '/srv/raspi/bin/suspend-linkstation.sh'
console.log(cmd)
exec(cmd, (err, stdout, stderr) => {
if (err) {
console.log(err)
// node couldn't execute the command
return;
}
// the *entire* stdout and stderr (buffered)
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
res.send("ok")
})
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log("Server listening at http://%s:%s", host, port)
})

135
kplayer/video/SRangeSlider.swift

@ -0,0 +1,135 @@
//
// Created by Marco Schmickler on 25.12.22.
// Copyright (c) 2022 Marco Schmickler. All rights reserved.
//
import Foundation
import SwiftUI
public struct SRangeSlider: View {
/// ` Slider` Binding min & max values
@Binding var minValue: Float
@Binding var maxValue: Float
/// Set slider min & max Label values
let minLabel: String
let maxLabel: String
/// Set slider width
let sliderWidth: Float
/// `Slider` background track color
let backgroundTrackColor: Color
/// `Slider` selected track color
let selectedTrackColor: Color
/// Globe background color
let globeColor: Color
/// Globe rounded boarder color
let globeBackgroundColor: Color
/// Slider min & max static and dynamic labels value color
let sliderMinMaxValuesColor: Color
/// `Slider` init
public init(minValue: Binding<Float>,
maxValue: Binding<Float>,
minLabel: String = "0",
maxLabel: String = "100",
sliderWidth: Float = 0,
backgroundTrackColor: Color = Color(UIColor.systemTeal).opacity(0.3),
selectedTrackColor: Color = Color.blue.opacity(25),
globeColor: Color = Color.orange,
globeBackgroundColor: Color = Color.black,
sliderMinMaxValuesColor: Color = Color.black) {
self._minValue = minValue
self._maxValue = maxValue
self.minLabel = minLabel
self.maxLabel = maxLabel
self.sliderWidth = sliderWidth
self.backgroundTrackColor = backgroundTrackColor
self.selectedTrackColor = selectedTrackColor
self.globeColor = globeColor
self.globeBackgroundColor = globeBackgroundColor
self.sliderMinMaxValuesColor = sliderMinMaxValuesColor
}
/// `Slider` view setup
public var body: some View {
VStack {
/// `Slider` start & end static values show in view
HStack {
// start value
Text(minLabel)
.offset(x: 28, y: 20)
.frame(width: 30, height: 30, alignment: .leading)
.foregroundColor(sliderMinMaxValuesColor)
Spacer()
// end value
Text(maxLabel)
.offset(x: -18, y: 20)
.frame(width: 30, height: 30, alignment: .trailing)
.foregroundColor(sliderMinMaxValuesColor)
}
/// `Slider` track view with glob view
ZStack (alignment: Alignment(horizontal: .leading, vertical: .center), content: {
// background track view
Capsule()
.fill(backgroundTrackColor)
.frame(width: CGFloat(self.sliderWidth + 10), height: 30)
// selected track view
Capsule()
.fill(selectedTrackColor)
.offset(x: CGFloat(self.minValue))
.frame(width: CGFloat((self.maxValue) - self.minValue), height: 30)
// minimum value glob view
Circle()
.fill(globeColor)
.frame(width: 30, height: 30)
.background(Circle().stroke(globeBackgroundColor, lineWidth: 2))
.offset(x: CGFloat(self.minValue))
.gesture(DragGesture().onChanged({ (value) in
/// drag validation
if value.location.x > 8 && Float(value.location.x) <= self.sliderWidth &&
value.location.x < CGFloat(self.maxValue - 30) {
// set min value of slider
self.minValue = Float(value.location.x - 8)
}
}))
// minimum value text draw inside minimum glob view
Text(String(format: "%.0f", (self.minValue / self.sliderWidth) * 100))
.offset(x: CGFloat(self.minValue))
.frame(width: 30, height: 30, alignment: .center)
.foregroundColor(sliderMinMaxValuesColor)
// maximum value glob view
Circle()
.fill(globeColor)
.frame(width: 30, height: 30)
.background(Circle().stroke(globeBackgroundColor, lineWidth: 2))
.offset(x: CGFloat(self.maxValue - 18))
.gesture(DragGesture().onChanged({ (value) in
/// drag validation
if Float(value.location.x - 8) <= self.sliderWidth && value.location.x > CGFloat(self.minValue + 50) {
// set max value of slider
self.maxValue = Float(value.location.x - 8)
}
}))
// maximum value text draw inside maximum glob view
Text(String(format: "%.0f", (self.maxValue / self.sliderWidth) * 100))
.offset(x: CGFloat(self.maxValue - 18))
.frame(width: 30, height: 30, alignment: .center)
.foregroundColor(sliderMinMaxValuesColor)
})
.padding()
}
}
}

180
kplayer/video/SVideoPlayer.swift

@ -24,6 +24,8 @@ struct SVideoPlayer: View, EditItemDelegate {
@State var savetext = "save"
@State var confirmationShown = false
@State var dirtyShown = false
@State var blackShown = false
@State var truncateShown = false
@State var seekSmoothly = false
@State var upsidedown = false
@State var more = false
@ -62,8 +64,7 @@ struct SVideoPlayer: View, EditItemDelegate {
do {
player.removeTimeObserver(model.observer)
model.observer = nil
}
catch {
} catch {
print("exception")
}
}
@ -91,7 +92,22 @@ struct SVideoPlayer: View, EditItemDelegate {
closePlayer(withSave: false)
}
}
if !model.baseItem.compilation {
Button(action: { blackShown = true }) {
Text("black")
}
.foregroundColor(NetworkManager.sharedInstance.isBlack(model.baseItem) ? Color.yellow : Color.blue)
.frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Delete?", isPresented: $blackShown) {
Button("delete") {
black();
closePlayer(withSave: false)
blackShown = false
}
Button("cancel", role: .cancel) {
blackShown = false
}
}
}
KToggleButton(text: "\(relative())", binding: $more)
Button(action: {
@ -123,29 +139,29 @@ struct SVideoPlayer: View, EditItemDelegate {
}
KToggleButton(text: "embd", binding: $embedded).frame(height: 30)
Text(model.currentSnapshot.name).foregroundColor(Color.blue)
Text("""
(\(model.codec) \(model.height), \(model.nominalFrameRate), \(model.bitRate)m)\n\(model.scale, specifier: "%.2f")x (\(model.dragOffset.width, specifier: "%.0f"),\(model.dragOffset.height, specifier: "%.0f"))
""").foregroundColor(Color.blue)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(model.allItems) { item in
Button(action: {
gotoSnapshot(item)
}) {
AsyncImage(item: item, placeholder: { Text("Loading ...") },
image: { Image(uiImage: $0).resizable() }).border(.yellow, width: (item===model.currentSnapshot) ? 1 : 0)
.overlay(Image(systemName: "repeat.circle").offset(x: 20, y: -20).opacity((item.length > 0.0) ? 1 : 0))
Group {
Text(model.currentSnapshot.name).foregroundColor(Color.blue)
Text("""
(\(model.codec) \(model.height), \(model.nominalFrameRate), \(model.bitRate)m)\n\(model.scale, specifier: "%.2f")x (\(model.dragOffset.width, specifier: "%.0f"),\(model.dragOffset.height, specifier: "%.0f"))
""").foregroundColor(Color.blue)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(model.allItems) { item in
Button(action: {
gotoSnapshot(item)
}) {
AsyncImage(item: item, placeholder: { Text("Loading ...") },
image: { Image(uiImage: $0).resizable() }).border(.yellow, width: (item === model.currentSnapshot) ? 1 : 0)
.overlay(Image(systemName: "repeat.circle").offset(x: 20, y: -20).opacity((item.length > 0.0) ? 1 : 0))
}
}
}
}
}
Spacer()
Spacer()
Group {
Button(action: { model.edit.toggle() }, label: {
Text("edit")
})
@ -194,7 +210,7 @@ struct SVideoPlayer: View, EditItemDelegate {
if move(dragged, start: gesture.startLocation) {
let f = 1.5
model.dragOffset = CGSize(width: f*dragged.width + lastDragOffset.width, height: f*dragged.height + lastDragOffset.height)
model.dragOffset = CGSize(width: f * dragged.width + lastDragOffset.width, height: f * dragged.height + lastDragOffset.height)
}
}
.onEnded { gesture in
@ -220,12 +236,10 @@ struct SVideoPlayer: View, EditItemDelegate {
} else {
if embedded && !more {
v.overlay(SEmbeddedVideo(embedded: $embedded, down: $embDown), alignment: embDown ? .bottomLeading : .topLeading)
} else
if small && !more {
} else if small && !more {
let normal = 385.0
v.overlay(Rectangle().stroke(Color.black, lineWidth: 2.0).opacity(0.3).frame(width: normal * 16.0 / 9.0, height: normal).offset(model.dragOffset), alignment: .topLeading)
}
else if more {
} else if more {
v.overlay(VStack {
Button(action: {
@ -287,6 +301,41 @@ struct SVideoPlayer: View, EditItemDelegate {
.frame(height: 30)
.foregroundColor(model.paused ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle())
if !model.baseItem.compilation {
Button(action: { blackShown = true }) {
Text("black")
}
.foregroundColor(NetworkManager.sharedInstance.isBlack(model.baseItem) ? Color.yellow : Color.blue)
.frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Delete?", isPresented: $blackShown) {
Button("delete") {
black();
closePlayer(withSave: false)
blackShown = false
}
Button("cancel", role: .cancel) {
blackShown = false
}
}
Button(action: { truncateShown = true }) {
Text("trunc")
}
.foregroundColor(Color.blue)
.frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Truncate?", isPresented: $truncateShown) {
Button("truncate") {
truncate();
closePlayer(withSave: false)
truncateShown = false
}
Button("cancel", role: .cancel) {
truncateShown = false
}
}
Button(action: { cut() }) {
Text("cut")
}
.frame(height: 30).buttonStyle(BorderlessButtonStyle())
}
Button(action: {}) {
Text("start")
}
@ -318,35 +367,35 @@ struct SVideoPlayer: View, EditItemDelegate {
})
}
.frame(width: 50, alignment: .top).offset(x: 0, y: 0), alignment: .topLeading)
}
else if frames && model.zoomed {
} else if frames && model.zoomed {
v.overlay(VStack {
ForEach(model.frames) { f in
Button(action: {
seekTime(model.currentSnapshot.time + f.time)
model.dragOffset.width = CGFloat(f.x)
model.dragOffset.height = CGFloat(f.y)
model.scale = f.scale
}, label: {
Text("\(f.time, specifier: "%.2f")")
}).simultaneousGesture(
LongPressGesture()
.onEnded { _ in
print("Loooong")
for i in 0...model.frames.count - 1 {
if model.frames[i].time == f.time {
model.frames.remove(at: i)
break
}
}
updateOptions()
ForEach(model.frames) { f in
Button(action: {
seekTime(model.currentSnapshot.time + f.time)
model.dragOffset.width = CGFloat(f.x)
model.dragOffset.height = CGFloat(f.y)
model.scale = f.scale
}, label: {
Text("\(f.time, specifier: "%.2f")")
})
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
print("Loooong")
for i in 0...model.frames.count - 1 {
if model.frames[i].time == f.time {
model.frames.remove(at: i)
break
}
)
}
}.frame(width: 70, alignment: .top).offset(x: 0, y: 0), alignment: .topTrailing)
}
else {
}
updateOptions()
}
)
}
}
.frame(width: 70, alignment: .top).offset(x: 0, y: 0), alignment: .topTrailing)
} else {
v
}
}
@ -402,7 +451,8 @@ struct SVideoPlayer: View, EditItemDelegate {
}
.onReceive(bitrateChanged) { notification in
guard let playerItem = notification.object as? AVPlayerItem,
let lastEvent = playerItem.accessLog()?.events.last else {
let lastEvent = playerItem.accessLog()?.events.last
else {
return
}
@ -429,7 +479,7 @@ struct SVideoPlayer: View, EditItemDelegate {
let relativeTime = time.seconds - model.currentSnapshot.time;
for f in model.frames {
// print("\(f.time) \(relativeTime)")
// print("\(f.time) \(relativeTime)")
if f.time > relativeTime && f.time < relativeTime + 1 {
model.scale = CGFloat(f.scale)
model.dragOffset.width = CGFloat(f.x)
@ -529,8 +579,7 @@ struct SVideoPlayer: View, EditItemDelegate {
} else {
if model.scale != 1.0 || small {
return true
}
else {
} else {
let delta = (dragged.width + dragged.height) / 400.0
seekTimeSmoothly(smoothTime + delta)
@ -589,8 +638,7 @@ struct SVideoPlayer: View, EditItemDelegate {
private func relative() -> String {
if let time = getCurrentTime() {
return Utility.formatSecondsToHMS(time - model.currentSnapshot.time)
}
else {
} else {
return "more"
}
}
@ -797,6 +845,18 @@ struct SVideoPlayer: View, EditItemDelegate {
seekTimeSmoothly(v)
}
func black() {
NetworkManager.sharedInstance.blackItem(model.baseItem)
}
func cut() {
NetworkManager.sharedInstance.cutItem(model.currentSnapshot)
}
func truncate() {
NetworkManager.sharedInstance.truncateItem(model.currentSnapshot)
}
func save(currentSnapshot c: MediaItem, name: String) {
do {
try FileHelper.createDir(name: name)
@ -812,7 +872,7 @@ struct SVideoPlayer: View, EditItemDelegate {
return
}
player.pause()
VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, snapshot:c, zoomed: model.zoomed,
VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, snapshot: c, zoomed: model.zoomed,
progress: { p in
let percent = Int(p * 100)
savetext = "\(percent)"

22
kplayer/video/VideoPlayerView.swift

@ -119,6 +119,14 @@ struct VideoPlayerControlsView : View {
Image(systemName: model.paused ? "play" : "pause")
.padding(.trailing, 30)
}
Button(action: moveBack) {
Image(systemName: "backward")
.padding(.trailing, 20)
}
Button(action: moveForward) {
Image(systemName: "forward")
.padding(.trailing, 20)
}
// Current video time
let postime = model.videoPos * model.videoDuration
let postext = Utility.formatSecondsToHMS(postime)
@ -135,6 +143,20 @@ struct VideoPlayerControlsView : View {
.padding(.trailing, 10)
}
private func moveBack() {
let time = player.currentTime().seconds - 5.0;
player.seek(to: CMTime(seconds: time, preferredTimescale: 10000))
pausePlayer(true)
}
private func moveForward() {
let time = player.currentTime().seconds + 5.0;
player.seek(to: CMTime(seconds: time, preferredTimescale: 10000))
pausePlayer(true)
}
private func togglePlayPause() {
pausePlayer(!model.paused)
}

Loading…
Cancel
Save