Browse Source

svideo changes

master
marcoschmickler 1 week ago
parent
commit
e4e9b70efd
  1. 108
      kplayer/Images.xcassets/AppIcon.appiconset/Contents.json
  2. 10
      kplayer/core/KSettings.swift
  3. 2
      kplayer/core/KSettingsModel.swift
  4. 6
      kplayer/master/KSettingsView.swift
  5. 97
      kplayer/video/PlayerInteractionMode.swift
  6. 174
      kplayer/video/SVideoPlayer.swift

108
kplayer/Images.xcassets/AppIcon.appiconset/Contents.json

@ -1,95 +1,23 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
{ "filename" : "icon_20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" },
{ "filename" : "icon_20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" },
{ "filename" : "icon_29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" },
{ "filename" : "icon_29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" },
{ "filename" : "icon_40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" },
{ "filename" : "icon_40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" },
{ "filename" : "icon_60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" },
{ "filename" : "icon_60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" },
{ "filename" : "icon_ipad_20@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" },
{ "filename" : "icon_ipad_20@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" },
{ "filename" : "icon_ipad_29@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" },
{ "filename" : "icon_ipad_29@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" },
{ "filename" : "icon_ipad_40@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" },
{ "filename" : "icon_ipad_40@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" },
{ "filename" : "icon_ipad_76@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" },
{ "filename" : "icon_ipad_76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" },
{ "filename" : "icon_ipad_83@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" },
{ "filename" : "icon_1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" }
],
"info" : {
"author" : "xcode",

10
kplayer/core/KSettings.swift

@ -30,6 +30,12 @@ class KSettings: ObservableObject {
@Published
var automaticallyWaitsToMinimizeStalling = true
@Published
var muteAudio = false
@Published
var docked = true
@Published
var embeddedVideoUrl : URL?
@ -41,10 +47,12 @@ class KSettings: ObservableObject {
jump = model.jump
slow = model.slow
confirm = model.confirm
muteAudio = model.muteAudio
docked = model.docked
}
func toModel() -> KSettingsModel {
KSettingsModel(scale: scale, autoloop: autoloop, zoomed: zoomed, jump: jump, slow: slow, confirm: confirm)
KSettingsModel(scale: scale, autoloop: autoloop, zoomed: zoomed, jump: jump, slow: slow, confirm: confirm, muteAudio: muteAudio, docked: docked)
}
func toJSON() -> String {

2
kplayer/core/KSettingsModel.swift

@ -12,4 +12,6 @@ struct KSettingsModel: Codable {
var jump = false
var slow = false
var confirm = false
var muteAudio = false
var docked = true
}

6
kplayer/master/KSettingsView.swift

@ -46,6 +46,12 @@ struct KSettingsView: View {
Toggle(isOn: $kSettings.automaticallyWaitsToMinimizeStalling, label: {
Text("Stalling")
})
Toggle(isOn: $kSettings.muteAudio, label: {
Text("Mute Audio")
})
Toggle(isOn: $kSettings.docked, label: {
Text("Dock Panel")
})
}.padding(30)
}
}

97
kplayer/video/PlayerInteractionMode.swift

@ -0,0 +1,97 @@
//
// Created by Marco Schmickler
// Copyright (c) 2024 Marco Schmickler. All rights reserved.
//
import CoreGraphics
// MARK: - DragAction
/// What a drag gesture should do, resolved independently of AVKit/SwiftUI.
enum DragAction {
case pan // caller updates model.dragOffset
case scrub(to: Double) // seekTimeSmoothly frame-accurate scrub
case step(to: Double) // seekTime discrete jump
case jumpSnapshot(Int) // +1 next / -1 previous
case none
}
// MARK: - DragInput
/// Pure inputs for interpreting a drag. No AVKit or SwiftUI types.
struct DragInput {
let translation: CGSize
let start: CGPoint
let currentTime: Double? // nil when player has no item
let anchorTime: Double // smoothTime baseline for scrubbing
let seeking: Bool
}
// MARK: - InteractionMode
/// One set of drag rules. Each mode is a value type with named constants,
/// so tuning a threshold is a one-line change in one place.
protocol InteractionMode {
func resolve(_ input: DragInput) -> DragAction
}
// MARK: - ScrubMode (paused)
/// When paused, dragging scrubs the timeline. Panning is only unlocked when
/// the video is zoomed or in crop-preview ("small") mode.
struct ScrubMode: InteractionMode {
/// Top zone height in points finer scrub sensitivity above this line.
/// 100 when zoomed/fullscreen, 170 otherwise.
var fineZoneHeight: CGFloat = 170
/// Whether the lower zone should pan instead of coarse-scrub.
var allowsPan: Bool = false
/// Drag-to-seconds ratio for the fine zone (upper).
var fineDivisor: Double = 100
/// Drag-to-seconds ratio for the coarse zone (lower).
var coarseDivisor: Double = 30
func resolve(_ input: DragInput) -> DragAction {
// Match old behaviour: if the player has no time, fall through to pan.
guard input.currentTime != nil else { return .pan }
let d = Double(input.translation.width + input.translation.height)
if input.start.y < fineZoneHeight {
return .scrub(to: input.anchorTime + d / fineDivisor)
}
if allowsPan { return .pan }
return .scrub(to: input.anchorTime + d / coarseDivisor)
}
}
// MARK: - PlayMode (playing)
/// When playing, a vertical drag can jump to the prev/next snapshot,
/// a horizontal drag step-seeks, and the lower portion pans when zoomed.
struct PlayMode: InteractionMode {
/// Whether the lower area should pan instead of seeking.
var allowsPan: Bool = false
/// Y-coordinate above which pan is NOT allowed (upper area seek only).
var panZoneTop: CGFloat = 300
/// Whether vertical drags should jump to adjacent snapshots.
var jumpEnabled: Bool = false
/// Minimum vertical drag distance (pts) to trigger a snapshot jump.
var jumpThreshold: CGFloat = 100
/// Minimum horizontal drag distance (pts) to trigger a step seek.
var stepThreshold: Double = 40
/// Seconds to seek forward on a rightward drag.
var stepForward: Double = 30
/// Seconds to seek backward on a leftward drag.
var stepBackward: Double = 30
func resolve(_ input: DragInput) -> DragAction {
let d = input.translation
if jumpEnabled {
if d.height > jumpThreshold { return .jumpSnapshot(+1) }
if d.height < -jumpThreshold { return .jumpSnapshot(-1) }
}
guard let t = input.currentTime, !input.seeking else { return .none }
if allowsPan && input.start.y > panZoneTop { return .pan }
if d.width > CGFloat(stepThreshold) { return .step(to: t + stepForward) }
if d.width < -CGFloat(stepThreshold) { return .step(to: t - stepBackward) }
return .none
}
}

174
kplayer/video/SVideoPlayer.swift

@ -43,9 +43,11 @@ struct SVideoPlayer: View, EditItemDelegate {
@State var xoffs = 0.0
@State var rotazero = -1000.0
@State var loop5 = false
@State var loop5 = 0 // 0 = off, 1 = 5 s, 2 = 10 s
@State var loop5start = 0.0
@State var fullscreen = false
@State var showHelp = false
@State var cutFlag = false
@State var lores = false
@State var second = false
@ -93,6 +95,7 @@ struct SVideoPlayer: View, EditItemDelegate {
if !model.baseItem.compilation {
doSnapshot()
}
if fullscreen { fullscreen = false }
print("triple tap!")
} else if gestureValue.first != nil {
doubleTapZoom()
@ -102,6 +105,7 @@ struct SVideoPlayer: View, EditItemDelegate {
var body: some View {
VStack {
if !fullscreen {
HStack {
VStack {
HStack {
@ -183,7 +187,10 @@ struct SVideoPlayer: View, EditItemDelegate {
})
.foregroundColor(model.favorite ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle())
KToggleButton(text: "loop5", binding: $loop5)
Button(action: { loop5 = (loop5 + 1) % 3 }) {
Text(loop5 == 0 ? "loop5" : loop5 == 1 ? "loop5" : "loop10")
}.foregroundColor(loop5 > 0 ? Color.yellow : Color.blue)
.buttonStyle(BorderlessButtonStyle())
KToggleButton(text: "cfg", binding: $showSettings).frame(height: 30)
Button(action: { confirmationShown = true }, label: {
Text(savetext)
@ -205,6 +212,9 @@ struct SVideoPlayer: View, EditItemDelegate {
}
KToggleButton(text: "embd", binding: $embedded).frame(height: 30)
Button(action: { showHelp = true }, label: {
Text("?")
}).buttonStyle(BorderlessButtonStyle())
}
}
@ -265,6 +275,7 @@ struct SVideoPlayer: View, EditItemDelegate {
}
}
}
} // end if !fullscreen
// .frame(height: 50)
GeometryReader { geometry in
@ -487,7 +498,7 @@ struct SVideoPlayer: View, EditItemDelegate {
let controlsView = VideoPlayerControlsView(model: model,
player: player)
if !second {
if !second && !fullscreen {
controlsView
} else {
controlsView.hidden()
@ -501,16 +512,21 @@ struct SVideoPlayer: View, EditItemDelegate {
}
}
.statusBarHidden(fullscreen)
.sheet(isPresented: $showSettings, onDismiss: { showSettings = false }) {
KSettingsView(kSettings: LocalManager.sharedInstance.settings, completionHandler: {
showSettings = false
})
}
.sheet(isPresented: $showHelp, onDismiss: { showHelp = false }) {
SVideoHelpView(completionHandler: { showHelp = false })
}
.onAppear() {
model.observed = player
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil, using: timeObserver())
player.automaticallyWaitsToMinimizeStalling = LocalManager.sharedInstance.settings.automaticallyWaitsToMinimizeStalling
player.isMuted = LocalManager.sharedInstance.settings.muteAudio
let item = model.currentPlayerItem()
@ -548,7 +564,7 @@ struct SVideoPlayer: View, EditItemDelegate {
model.bitRate = Int(lastEvent.indicatedBitrate / 1024 / 1024)
}.onChange(of: loop5) { new in
if new {
if new == 1 {
if model.paused {
model.paused = false
player.play()
@ -570,21 +586,7 @@ struct SVideoPlayer: View, EditItemDelegate {
}
private func doubleTapZoom() {
second = !second
if second {
model.scale = lastScale
} else {
lastScale = model.scale
if (lores) {
model.scale = secondScale
model.dragOffset = secondOffset
} else {
model.scale = 1
model.dragOffset = CGSize.zero
}
}
fullscreen.toggle()
}
private func timeObserver() -> (CMTime) -> () {
@ -595,8 +597,9 @@ struct SVideoPlayer: View, EditItemDelegate {
if timeSlomoCounter >= 1 {
timeSlomoCounter -= 1
}
if loop5 {
if time.seconds > loop5start + 5.0 {
if loop5 > 0 {
let loopDuration = loop5 == 2 ? 10.0 : 5.0
if time.seconds > loop5start + loopDuration {
seekTime(loop5start)
}
}
@ -701,90 +704,63 @@ struct SVideoPlayer: View, EditItemDelegate {
scrollTarget = model.allItems.count
}
private func move(_ dragged: CGSize, start: CGPoint) -> Bool {
/// Maps the current combination of mode flags to an InteractionMode value
/// with the appropriate constants. This is the single place to change
/// which flags affect drag behaviour and what the thresholds are.
private func currentMode() -> InteractionMode {
let zoomed = second || fullscreen
let pan = zoomed || small
if model.paused {
if let time = getCurrentTime() {
if smoothTime < 0 {
smoothTime = time
}
if (start.y < 170) {
let delta = (dragged.width + dragged.height) / 100.0
seekTimeSmoothly(smoothTime + delta)
return false
} else {
if second || small {
return true
} else {
let delta = (dragged.width + dragged.height) / 30.0
seekTimeSmoothly(smoothTime + delta)
return false
}
}
} else {
return true
}
return ScrubMode(
fineZoneHeight: zoomed ? 100 : 170,
allowsPan: pan)
}
let fine = model.slow || second
let (threshold, forward, backward): (Double, Double, Double) = {
if fullscreen { return (20, 10, 10) }
else if fine { return (20, 5, 8) }
else { return (40, 30, 30) }
}()
return PlayMode(
allowsPan: pan,
jumpEnabled: model.jump && !fullscreen,
stepThreshold: threshold,
stepForward: forward,
stepBackward: backward)
}
if dragged.height > 100 && model.jump {
if 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 = 50
print("jump")
}
}
return false
}
/// Wrapping prev/next snapshot navigation (replaces two identical blocks).
private func jumpSnapshot(by dir: Int) {
guard let i = model.allItems.index(where: { $0 === model.currentSnapshot })
else { return }
let n = model.allItems.count
gotoSnapshot(model.allItems[(i + dir + n) % n])
print("jump")
}
if dragged.height < -100 && model.jump {
if timeCounter == 0 {
if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) {
if i - 1 >= 0 {
gotoSnapshot(model.allItems[i - 1])
} else {
gotoSnapshot(model.allItems[model.allItems.count - 1])
}
timeCounter = 50
print("jump")
}
}
private func move(_ dragged: CGSize, start: CGPoint) -> Bool {
if model.paused, smoothTime < 0, let t = getCurrentTime() { smoothTime = t }
let input = DragInput(
translation: dragged,
start: start,
currentTime: getCurrentTime(),
anchorTime: smoothTime,
seeking: model.seeking)
switch currentMode().resolve(input) {
case .pan:
return true
case .scrub(let target):
seekTimeSmoothly(target)
return false
case .step(let target):
seekTime(target)
return false
case .jumpSnapshot(let dir):
if timeCounter == 0 { jumpSnapshot(by: dir); timeCounter = 50 }
return false
case .none:
return false
}
if let time = getCurrentTime() {
let dragWidth = 20.0
if !model.seeking {
if (second || small) && start.y > 300 {
return true
}
if (model.slow || second) {
if dragged.width > dragWidth {
seekTime(time + 5.0)
} else if dragged.width < -dragWidth {
seekTime(time - 8.0)
}
} else {
if dragged.width > dragWidth * 2 {
seekTime(time + 30.0)
} else if dragged.width < -dragWidth * 2 {
seekTime(time - 30.0)
}
}
}
// model.seeking = sk
}
return false
}
private func relative() -> String {

Loading…
Cancel
Save