mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
Scale tvOS scrub step by swipe rate
tvOS rasterizes Siri Remote touchpad swipes into discrete onMoveCommand events at ~300-400ms, so a fast swipe and a single tap delivered the same fixed step. Track gap between events: rapid same-direction events (under 500ms) build a streak that multiplies the step via a power curve, while deliberate taps still land on the base step.
This commit is contained in:
@@ -49,6 +49,13 @@ struct TVPlayerProgressBar: View {
|
|||||||
/// Accumulated pan translation for scrubbing.
|
/// Accumulated pan translation for scrubbing.
|
||||||
@State private var panAccumulator: CGFloat = 0
|
@State private var panAccumulator: CGFloat = 0
|
||||||
|
|
||||||
|
/// Consecutive-event streak for rapid D-pad/touchpad scrubbing.
|
||||||
|
/// tvOS routes touchpad swipes as rapid `onMoveCommand` events, so we
|
||||||
|
/// amplify step size when events arrive in quick succession.
|
||||||
|
@State private var dpadStreakCount: Int = 0
|
||||||
|
@State private var lastDPadTime: Date?
|
||||||
|
@State private var lastDPadDirection: MoveCommandDirection?
|
||||||
|
|
||||||
/// The time to display. SELECT-based scrub takes priority, then the
|
/// The time to display. SELECT-based scrub takes priority, then the
|
||||||
/// parent's pending remote-seek target, then the actual playback time.
|
/// parent's pending remote-seek target, then the actual playback time.
|
||||||
private var displayTime: TimeInterval {
|
private var displayTime: TimeInterval {
|
||||||
@@ -256,16 +263,16 @@ struct TVPlayerProgressBar: View {
|
|||||||
// Lower values = slower/finer scrubbing
|
// Lower values = slower/finer scrubbing
|
||||||
let baseSensitivity: CGFloat
|
let baseSensitivity: CGFloat
|
||||||
if duration > 3600 {
|
if duration > 3600 {
|
||||||
baseSensitivity = duration / 2000 // ~1.8 sec per unit for 1hr video
|
baseSensitivity = duration / 1500
|
||||||
} else if duration > 600 {
|
} else if duration > 600 {
|
||||||
baseSensitivity = duration / 3000 // ~0.2 sec per unit for 10min video
|
baseSensitivity = duration / 2000
|
||||||
} else {
|
} else {
|
||||||
baseSensitivity = duration / 4000 // very fine control for short videos
|
baseSensitivity = duration / 3000
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply velocity multiplier (faster swipe = faster scrub)
|
// Non-linear velocity response: slow swipes stay precise, fast flicks accelerate.
|
||||||
// Reduced range: 0.3x to 2.0x
|
let normalizedVelocity = abs(velocity) / 500
|
||||||
let velocityMultiplier = min(max(abs(velocity) / 800, 0.3), 2.0)
|
let velocityMultiplier = min(max(pow(normalizedVelocity, 1.4), 0.3), 6.0)
|
||||||
let adjustedSensitivity = baseSensitivity * velocityMultiplier
|
let adjustedSensitivity = baseSensitivity * velocityMultiplier
|
||||||
|
|
||||||
// Update scrub time based on translation delta
|
// Update scrub time based on translation delta
|
||||||
@@ -306,15 +313,35 @@ struct TVPlayerProgressBar: View {
|
|||||||
|
|
||||||
switch direction {
|
switch direction {
|
||||||
case .left, .right:
|
case .left, .right:
|
||||||
// Determine scrub increment based on video length
|
// Track event rate. When events arrive quickly in the same
|
||||||
let scrubAmount: TimeInterval
|
// direction (i.e. a touchpad swipe being rasterized into move
|
||||||
if duration > 3600 {
|
// commands), grow the streak and scale the step size up.
|
||||||
scrubAmount = 30
|
let now = Date()
|
||||||
} else if duration > 600 {
|
let gap = lastDPadTime.map { now.timeIntervalSince($0) } ?? .infinity
|
||||||
scrubAmount = 15
|
|
||||||
|
// tvOS throttles onMoveCommand at ~300-400ms even during a fast
|
||||||
|
// swipe, so we need a generous window to still recognize a burst.
|
||||||
|
if gap < 0.5, lastDPadDirection == direction {
|
||||||
|
dpadStreakCount = min(dpadStreakCount + 1, 30)
|
||||||
} else {
|
} else {
|
||||||
scrubAmount = 10
|
dpadStreakCount = 1
|
||||||
}
|
}
|
||||||
|
lastDPadTime = now
|
||||||
|
lastDPadDirection = direction
|
||||||
|
|
||||||
|
// Base step based on video length.
|
||||||
|
let baseStep: TimeInterval
|
||||||
|
if duration > 3600 {
|
||||||
|
baseStep = 15
|
||||||
|
} else if duration > 600 {
|
||||||
|
baseStep = 8
|
||||||
|
} else {
|
||||||
|
baseStep = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steeper curve so a swipe (few events) actually covers ground.
|
||||||
|
let streakMultiplier = pow(Double(dpadStreakCount), 1.6)
|
||||||
|
let scrubAmount = baseStep * streakMultiplier
|
||||||
|
|
||||||
let currentScrubTime = scrubTime ?? currentTime
|
let currentScrubTime = scrubTime ?? currentTime
|
||||||
if direction == .left {
|
if direction == .left {
|
||||||
@@ -322,10 +349,14 @@ struct TVPlayerProgressBar: View {
|
|||||||
} else {
|
} else {
|
||||||
scrubTime = min(duration, currentScrubTime + scrubAmount)
|
scrubTime = min(duration, currentScrubTime + scrubAmount)
|
||||||
}
|
}
|
||||||
|
scheduleSeek()
|
||||||
|
|
||||||
case .up, .down:
|
case .up, .down:
|
||||||
// Exit scrub mode and let navigation happen
|
// Exit scrub mode and let navigation happen
|
||||||
commitScrub()
|
commitScrub()
|
||||||
|
dpadStreakCount = 0
|
||||||
|
lastDPadTime = nil
|
||||||
|
lastDPadDirection = nil
|
||||||
|
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
break
|
||||||
@@ -349,6 +380,9 @@ struct TVPlayerProgressBar: View {
|
|||||||
isScrubbing = false
|
isScrubbing = false
|
||||||
}
|
}
|
||||||
panAccumulator = 0
|
panAccumulator = 0
|
||||||
|
dpadStreakCount = 0
|
||||||
|
lastDPadTime = nil
|
||||||
|
lastDPadDirection = nil
|
||||||
|
|
||||||
if wasScrubbing {
|
if wasScrubbing {
|
||||||
onScrubbingChanged?(false)
|
onScrubbingChanged?(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user