Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
3.7 kB · 94 lines
Swift
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495// ScrubPlayerView.swift — a bare AVPlayerLayer view with the two// interactions ClipWizard actually wants and nothing else:// • single tap-and-drag anywhere scrubs left→right (x position maps// to the timeline; playback pauses while the mouse is down and// resumes on release)// • a thin progress bar pinned along the BOTTOM edge — no floating// play-button circle, no system chrome.import AppKitimport AVFoundation
final class ScrubPlayerView: NSView { private let playerLayer = AVPlayerLayer() private let progressTrack = CALayer() private let progressFill = CALayer() private var timeObserver: Any? private var wasPlayingBeforeScrub = false
var player: AVPlayer? { didSet { if let old = oldValue, let obs = timeObserver { old.removeTimeObserver(obs) } timeObserver = nil playerLayer.player = player guard let p = player else { return } // ~30 fps progress updates keep the bar glued to playback. timeObserver = p.addPeriodicTimeObserver( forInterval: CMTime(value: 1, timescale: 30), queue: .main ) { [weak self] _ in self?.updateProgress() } } }
override init(frame: NSRect) { super.init(frame: frame) wantsLayer = true layer?.backgroundColor = NSColor.black.cgColor layer?.cornerRadius = 8 layer?.masksToBounds = true playerLayer.videoGravity = .resizeAspect layer?.addSublayer(playerLayer) progressTrack.backgroundColor = NSColor.white.withAlphaComponent(0.18).cgColor progressFill.backgroundColor = NSColor.systemYellow.cgColor layer?.addSublayer(progressTrack) layer?.addSublayer(progressFill) }
required init?(coder: NSCoder) { fatalError() }
override func layout() { super.layout() CATransaction.begin() CATransaction.setDisableActions(true) playerLayer.frame = bounds let barH: CGFloat = 5 progressTrack.frame = CGRect(x: 0, y: 0, width: bounds.width, height: barH) CATransaction.commit() updateProgress() }
private func updateProgress() { guard let p = player, let item = p.currentItem, item.duration.isNumeric, item.duration.seconds > 0 else { progressFill.frame = .zero return } let frac = CGFloat(p.currentTime().seconds / item.duration.seconds) CATransaction.begin() CATransaction.setDisableActions(true) progressFill.frame = CGRect(x: 0, y: 0, width: bounds.width * max(0, min(1, frac)), height: 5) CATransaction.commit() }
// ── tap + drag = scrub ──────────────────────────────────────────── private func scrub(to event: NSEvent) { guard let p = player, let item = p.currentItem, item.duration.isNumeric, item.duration.seconds > 0 else { return } let x = convert(event.locationInWindow, from: nil).x let frac = max(0, min(1, x / bounds.width)) let t = CMTime(seconds: Double(frac) * item.duration.seconds, preferredTimescale: 600) p.seek(to: t, toleranceBefore: .zero, toleranceAfter: .zero) updateProgress() }
override func mouseDown(with event: NSEvent) { wasPlayingBeforeScrub = (player?.rate ?? 0) > 0 player?.pause() scrub(to: event) }
override func mouseDragged(with event: NSEvent) { scrub(to: event) }
override func mouseUp(with event: NSEvent) { if wasPlayingBeforeScrub { player?.play() } }}