fix: affix playback controls to their default position

This commit is contained in:
2026-10-17 17:10:56 +02:00
parent 0aa0ed781f
commit d2aa949f54
3 changed files with 6 additions and 61 deletions
+2
View File
@@ -49,6 +49,8 @@ Open a file with **⌘O**, drag it onto the window, or use Finder's **Open With*
Compact mode keeps playback in the same floating window; drag to move it and double-click to restore. The titlebar pin keeps the normal window on top. macOS media keys also control playback; previous/next seek five seconds rather than changing tracks. Compact mode keeps playback in the same floating window; drag to move it and double-click to restore. The titlebar pin keeps the normal window on top. macOS media keys also control playback; previous/next seek five seconds rather than changing tracks.
Playback controls stay anchored at the bottom center of the video and cannot be dragged.
### YouTube ### YouTube
Playback defaults to the best available SDR resolution up to **1080p**. Use **gear → Quality** to select another available resolution, including 4K, or enable automatic quality (off by default). **Show Buffer Info** displays the retained network buffer. Playback defaults to the best available SDR resolution up to **1080p**. Use **gear → Quality** to select another available resolution, including 4K, or enable automatic quality (off by default). **Show Buffer Info** displays the retained network buffer.
+2 -16
View File
@@ -1821,9 +1821,6 @@ private struct PlayerContent: View {
@State private var youTubeURL = "" @State private var youTubeURL = ""
@FocusState private var youTubeInputFocused: Bool @FocusState private var youTubeInputFocused: Bool
@State private var controlSize = CGSize(width: 440, height: 68) @State private var controlSize = CGSize(width: 440, height: 68)
// Relative offsets preserve the dragged position across resize/fullscreen.
@State private var controlOffset = CGSize.zero
@State private var dragOrigin = CGSize.zero
private func compactTitleLabel(progress: String?, constrained: Bool) -> some View { private func compactTitleLabel(progress: String?, constrained: Bool) -> some View {
HStack(spacing: 0) { HStack(spacing: 0) {
@@ -2097,8 +2094,8 @@ private struct PlayerContent: View {
} }
private func controllerPosition(in size: CGSize) -> CGPoint { private func controllerPosition(in size: CGSize) -> CGPoint {
let x = size.width / 2 + controlOffset.width * size.width let x = size.width / 2
let y = size.height * 0.96 - controlSize.height / 2 + controlOffset.height * size.height let y = size.height * 0.96 - controlSize.height / 2
return CGPoint( return CGPoint(
x: min(max(x, controlSize.width / 2 + 12), size.width - controlSize.width / 2 - 12), x: min(max(x, controlSize.width / 2 + 12), size.width - controlSize.width / 2 - 12),
y: min(max(y, 32 + controlSize.height / 2 + 12), size.height - controlSize.height / 2 - 12) y: min(max(y, 32 + controlSize.height / 2 + 12), size.height - controlSize.height / 2 - 12)
@@ -2331,17 +2328,6 @@ private struct PlayerContent: View {
.background { .background {
PlayerGlass() PlayerGlass()
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: 12))
.gesture(DragGesture(minimumDistance: 4).onChanged { value in
model.setControlsDragging(true)
controlOffset = CGSize(width: dragOrigin.width + value.translation.width / geometry.size.width,
height: dragOrigin.height + value.translation.height / geometry.size.height)
}.onEnded { _ in
let position = controllerPosition(in: geometry.size)
controlOffset = CGSize(width: (position.x - geometry.size.width / 2) / geometry.size.width,
height: (position.y - (geometry.size.height * 0.96 - controlSize.height / 2)) / geometry.size.height)
dragOrigin = controlOffset
model.setControlsDragging(false)
})
} }
.scaleEffect(min(1, (geometry.size.width - 32) / 440)) .scaleEffect(min(1, (geometry.size.width - 32) / 440))
.frame(width: min(440, geometry.size.width - 32), .frame(width: min(440, geometry.size.width - 32),
+2 -45
View File
@@ -1,48 +1,5 @@
- [x] fix the readme: - [ ] improve chrome cast subtitle rendering
- clear out redundant and irrelevant overly detailed crap from the readme. - maybe this will require stamping the subtitles into the video frames manually before submission to the chrome cast (depending on what it supports)
- we don't need every minute detail (already described perfectly by the code btw) described in painstakingly verbose paragraphs in the readme.
- implementation details are not appropriate in a readme file (without a very good reason to be there).
- the readme should be compressed to actually useful information - nothing more - and verbose, irrelevant, or redundant information deleted.
- [x] evaluate playback optimization opportunities for YouTube and Miruro — measure benefits before changing behavior
- [x] coalesce superseded, not-yet-started seeks in the player and Cast controls; preserve accumulated relative skips and play/pause ordering
- [x] expand Cast H.264/AAC remux coverage with keyframe-aligned segments; preserve seek accuracy, timestamps and receiver compatibility
- [x] reuse decoder/encoder state during sequential Cast playback; reset on random seeks and preserve independent keyframes and AAC continuity
- `.build/cast-media-check --codec-work`: 60s of 720p/30 B-frame video plus 44.1kHz PCM→AAC over loopback HTTP; three paired runs reduced median relay CPU from 3.56s to 2.58s (27%) and generation time from 4.97s to 4.43s (11%); physical-receiver playback still needs manual verification
- [x] evaluate VideoToolbox hardware video decoding for native and Cast playback; retain software fallback for unsupported formats
- M4, three paired 12s/720p/30 MP4 runs: H.264/HEVC/VP9 native decode process CPU fell 77/88/92%; Cast relay process CPU fell 48/58/68%, with generation 2–7% slower. Prefer these measured codecs; AV1 keeps dav1d. Unpaced native decoding is slower; H.264 seek preview rose from 1.8ms to 4.1ms. These are process CPU measurements, not energy measurements.
- `make check-video-decoding` covers native Metal upload, mapped-frame lifetime, exact seek/step/EOF, dependent-packet seek fallback, Cast independent IDRs/AAC, and unsupported codecs; `video-decoding-software-check` and `cast-media-software-check` provide matched baselines. Unsafe seek starts use software decoding (until reopen for native playback).
- Existing Cast millisecond-timestamp limiting drops VP9 MKV frames in the software baseline; a direct relay comparison confirmed VideoToolbox preserves the same output PTS/counts. This evaluation does not change that limiter.
- [x] benchmark cold start, cached/uncached seeks and sustained playback separately to prioritize the actual bottlenecks
- `make benchmark-playback`: M4/16 GiB, macOS 27.0.1; three fresh-process samples per source/latency profile. Shared 120s/720p/30 H.264 B-frame + 48kHz AAC fixture, two-second GOPs; YouTube-shaped split MP4 and Miruro-shaped restricted six-second HLS. Added response delay is per request, not simulated RTT/bandwidth. No playback behavior changed.
| Added delay ms | Path | Cold ms | Cached seek ms | Uncached seek ms | Sustained wall s | Sustained process CPU s |
| ---: | --- | ---: | ---: | ---: | ---: | ---: |
| 0 | Native split | 374.93 | 21.15 | 26.58 | 12.20 | 0.81 |
| 0 | Native HLS | 473.42 | 28.88 | 225.08 | 12.22 | 0.80 |
| 0 | Cast split | 627.88 | 3.36 | 482.14 | 4.27 | 1.60 |
| 0 | Cast HLS | 715.66 | 3.79 | 492.37 | 4.16 | 0.77 |
| 25 | Native split | 406.77 | 20.96 | 127.40 | 12.30 | 1.05 |
| 25 | Native HLS | 920.65 | 40.56 | 245.96 | 12.23 | 0.87 |
| 25 | Cast split | 1251.29 | 3.55 | 1451.26 | 7.61 | 1.65 |
| 25 | Cast HLS | 662.73 | 1.97 | 521.57 | 4.50 | 0.72 |
- Medians above: native sustained is 12 media seconds with real muted audio and offscreen 720p Metal draws; Cast is unpaced generation of 60 media seconds after the first segment. Native cold includes queued autoplay to 53ms of acknowledged audio-clock progress and pause; Cast cold includes preparation, manifest and first six-second segment. Native hits seek to retained 20.5s; misses seek to 100.5s beyond retained coverage. Cast hits repeat segment zero; misses regenerate it after both segment-cache and packet-history eviction.
- Prioritize investigating native HLS random-seek preroll and Cast first-segment/random-seek generation, not more sequential codec tuning: native HLS misses remained 220–415ms without injected delay versus 21–74ms for split MP4; Cast's first segment took 552–915ms while preparation took 5–39ms, and sustained generation remained faster than playback. These are bottleneck candidates, not measured benefits of an unimplemented optimization.
- Memory/request baseline: median pre-validation lifetime peak RSS across profiles was 230–251 MiB native / 238–253 MiB Cast, including renderer/loopback-client overhead. Zero-delay cold windows transferred roughly 51–55 MiB, with 6/12 native and 7/12 Cast upstream requests for split/HLS; counts and bytes include concurrent read-ahead, so even a cache hit can show network activity. Keep existing cache/segment limits and origin/header checks.
- All 24 source sessions passed (96 phase rows): accurate native seek acknowledgements, cache preconditions, real audio-clock advancement, repeated Cast byte equality, and independently decodable Cast endpoint segments with expected frame counts/timestamps/AAC. Large timing spread (25ms-delay Cast split cold 703–1973ms) prevents causal latency/CPU comparisons; no persistence/segment-size benefit is claimed. Fresh process/source is not cold OS caches; process CPU is not total hardware energy. Resolver/CLI launch, live CDN/TLS, bandwidth limits, other codecs/remux, visual appearance and physical receivers are not measured.
- do not enable Miruro HTTP persistence without preserving origin/header checks, or increase caches/shorten Cast segments without measuring the memory/request/codec tradeoffs
- [x] prioritize audio preroll after a paused network preview instead of decoding unnecessary future video
- Temporary stage profiling isolated the cost: the HLS 100.5s miss returned packets from 96.021s in 2–3ms, but video decoding continued after preview while audio caught up. Trying audio first after preview reduced decoded video frames from 200 to 136, preserving all 135 preroll frames and the same target preview. Video remains a fallback when audio cannot progress; local shared demux, playing and frame-stepping scheduling are unchanged. Removed all profiling and experimental wakeup code.
- Six interleaved focused pairs per delay profile supported the change. Final unprofiled validation used two alternating-order pairs per source/delay, including cold start, cache-hit seek, 12 media seconds of paced playback and the cache-miss seek. M4 and the same synthetic H.264/AAC fixture as above; medians below describe this fixture, not live CDN performance.
| Added delay ms | HLS miss before ms | HLS miss after ms | Process CPU before ms | Process CPU after ms |
| ---: | ---: | ---: | ---: | ---: |
| 0 | 215.97 | 161.06 | 53.94 | 42.51 |
| 25 | 241.03 | 183.32 | 53.45 | 44.00 |
- HLS miss latency fell 25%/24%, with process CPU down 21%/18%. Both profiles retained four miss-window upstream requests and 21,205,460 bytes per sample. Split-source seeks showed no consistent improvement; no sustained-playback, RSS or energy benefit is claimed. Cache limits, HTTP persistence, origin/header checks and Cast behavior are unchanged.
- Reproduce full-path comparisons after preserving a parent-revision executable as `.build/playback-benchmark-baseline`: `python3 Checks/playback-benchmark.py .build/playback-benchmark .build/cast-media-check .build/fixtures/playback-benchmark --compare-native .build/playback-benchmark-baseline --runs 2`. `--seek-work` is a faster direct cache-miss mode when both binaries support it.
- Verified the final production build with all 16 comparison sessions (64 phase rows), `make check-network` (including encrypted/fMP4 HLS, exact frame steps, resume/EOF, stalls/cancellation and credential/origin isolation) and `make check-seek-queue`. Remaining HLS preroll and Cast first-segment latency are separate opportunities, not included in this change.
- [ ] add playlist support - [ ] add playlist support
- maybe by allowing multiple file selection? 🤔 - maybe by allowing multiple file selection? 🤔
- [ ] in the "choose what to play" screen, we should change the youtube button to general streaming - [ ] in the "choose what to play" screen, we should change the youtube button to general streaming