45 Commits

Author SHA1 Message Date
Arkadiusz Fal
9c97036684 Make Enable Logging the master switch over all verbose logging
The verbose toggles (MPV, remote control) kept working with the master
"Enable Logging" toggle off: the settings UI hides them, but their
stored values - verboseMPVLogging is even iCloud-synced - were still
honored at runtime, producing console output and enabling the periodic
MPV stats collection while logging appeared disabled.

Enable Logging now takes precedence everywhere:

- LoggingService gates its DEBUG-build OSLog console output on the
  master toggle (it previously ran before the isEnabled guard, so the
  Xcode console was never silent)
- MPVLogging requires loggingEnabled && verboseMPVLogging, which also
  gates the 10s playback stats task via MPVLogging.verboseEnabled
- rcDebug in LocalNetworkService and RemoteControlCoordinator requires
  the master toggle alongside verboseRemoteControlLogging
2026-07-19 01:00:01 +02:00
Arkadiusz Fal
6aa5fd8ee7 Fix periodic tvOS playback stutter from render stalls on mpvQueue
Build 264 stutters for a split second every ~10 seconds. The periodic
playback-stats task fetches ~30 debug properties in one ~190ms block on
mpvQueue, and MPVClient.render() wrapped mpv_render_context_render in
mpvQueue.sync - so the render thread queued behind the fetch and stalled
~190ms, at exactly the stats interval. Build 261 predates the stats
task, which is why it was unaffected.

libmpv's render API is documented thread-safe relative to the client
API, so render-context calls no longer go through mpvQueue:

- New renderContextLock protects renderContext lifecycle vs use;
  render(), renderWithDepth(), renderSoftware(), reportSwap(),
  shouldRenderUpdateFrame() and hasRenderContext use it
- Render-update callbacks dispatch on a dedicated renderEventQueue so
  frame-ready notifications can't queue behind property/command work
- destroyRenderContext takes mpvQueue then the lock, still excluding
  in-flight renders during teardown
- The stats task skips its property fetch on iOS/tvOS unless verbose
  MPV logging is on (macOS keeps it for the render watchdog)
- performRender slow-frame warnings now break down render vs present
  time and report frame-delivery gaps for future triage
2026-07-19 00:53:06 +02:00
Arkadiusz Fal
4ec9b936a6 Fix background playback setting behavior across platforms
Hide the toggle on macOS, where apps are never suspended and playback
always continues regardless of the setting. Pause playback on iOS when
backgrounding with the setting disabled — the audio background mode
previously kept mpv playing, making the off state a no-op.

Move the continue-playing policy into PlayerService and make the
backend's background render pause unconditional (PiP-gated only):
it is a GPU optimization that should not depend on the setting.
Also ungate the player sheet visibility handlers, which manage
foreground mini player rendering unrelated to background playback.
2026-07-18 16:28:19 +02:00
Arkadiusz Fal
3a8f91f88c Reset player aspect ratio to 16:9 for audio-only playback
Audio-only streams never report a video size, so the aspect ratio of
the last played video stuck to the player area indefinitely once audio
mode was enabled. Reset it in play() when the selected stream is
audio-only, and let the macOS player window fall back to 16:9 instead
of keeping the previous lock.
2026-07-17 18:43:10 +02:00
Arkadiusz Fal
c2554cee04 Apply audio mode to downloaded videos
Downloads bypass stream selection and reach play() as ready-made local
file streams, so audio mode never affected them. Apply the mode at that
choke point instead: swap a local video stream for the separately
downloaded audio track, or play a muxed file with the video track
disabled (vid set per-load in MPVClient, never toggled live). The
transform is bidirectional so stored audio-only queue/history entries
restore full video when the mode is off, and setAudioMode now reloads
downloaded content at the current position in both directions.
2026-07-17 09:27:09 +02:00
Arkadiusz Fal
959641b642 Sync watch progress on natural video completion and backgrounding
Naturally finished videos only saved their 100% progress locally
(play() deliberately skips the sync-on-switch when videoEndedNaturally
is set), so the watched state never reached other devices in the
autoplay flow. Save completion through updateWatchProgress, which
queues the iCloud sync.

Also queue the current playback position when the app enters
background, so backgrounding mid-video hands the position off to other
devices instead of only syncing on explicit stop or video switch.
2026-07-14 23:08:39 +02:00
Arkadiusz Fal
06087220ef Add audio-only music mode
Persistent per-platform mode that plays only the audio track to save
bandwidth and speed up loads. When enabled, stream selection picks the
best audio-only stream (preferred language, codec, bitrate) and never
fetches the video URL; toggling mid-video reloads at the current
position using the quality-switch path (live vid=no toggling is avoided
due to A/V desync).

- Toggle row in the player quality/settings sheet on iOS, macOS and
  tvOS, next to lock controls
- New audioMode control button type (red when active) for player
  controls and mini player layouts
- Picking a video quality explicitly turns the mode off; audio track
  picks keep it on and now work while audio-only is playing
- Stream URL refresh preserves audio-only playback instead of
  resurrecting video
- Queue preloads and history entries resolved before a toggle are
  discarded so selection re-runs with the current mode
- tvOS player shows the video thumbnail instead of a black screen
  during audio-only playback
- PiP button hidden for audio-only streams
2026-07-13 19:06:01 +02:00
Arkadiusz Fal
afa73e6bcb Prefer avfoundation AO on macOS 27 to avoid mpv hotplug listener crash
On macOS 27 beta, mpv's coreaudio AO fails to initialize (channel layout
set rejected with paramErr -50). The existing ao=coreaudio,avfoundation
fallback restored audio, but exposed an mpv bug: init() registers a HAL
hotplug listener (AudioObjectAddPropertyListener on the system object,
with the raw ao pointer as user data) before the step that fails, and
the coreaudio_error path never unregisters it. mpv then frees the ao
and falls back to avfoundation, leaving a dangling listener per
playback start. The next audio device change (AirPods connect,
sleep/wake device republish) invokes the listener with the freed
pointer and crashes with a use-after-free SIGSEGV on the
HALC_ProxyNotification Call Listener Queue (observed in build 262;
faulting address was reused "texture_" shader string memory).

Gate the AO order by OS version: on macOS 27+ use
avfoundation,coreaudio so the failing coreaudio init never runs; older
macOS keeps the native coreaudio first with avfoundation as fallback.
Update the read-only defaults mirror in MPV options settings to match.
2026-07-07 23:51:34 +02:00
Arkadiusz Fal
b642813716 Fall back to avfoundation AO when coreaudio fails on macOS
macOS 27 beta's AUHAL rejects mpv's channel-layout property with paramErr
(-50), so the coreaudio AO fails to initialize and playback is silent.
Change the macOS ao option from "coreaudio" to "coreaudio,avfoundation"
so mpv self-heals: older macOS keeps using coreaudio unchanged, while the
beta drops to avfoundation (AVSampleBufferAudioRenderer) instead of playing
no sound.
2026-07-01 06:40:16 +02:00
Arkadiusz Fal
bf00c48e6c Fix macOS player window black video when mini capsule steals render view
Closing the player window via its close button left isPlayerCollapsing
stuck true (windowShouldClose has no animation completion to clear it),
so the mini capsule preview kept mounting its render container and
grabbed the shared render view on the next expand - the video rendered
into the 44x26 thumbnail while the player window stayed black.

- Clear isPlayerCollapsing on the next runloop in windowShouldClose
- Give containers in the tracked player window priority to claim the
  shared view (even from a visible owner), and refuse steals from the
  visible player window by outside containers
- Prefer the largest container in the player window during recovery so
  thumbnail-sized surfaces are never picked
- Add a watchdog misplaced-view check in MPVBackend that re-attaches
  the render view when it lives outside the visible player window
- Include container bounds and tracked-window flag in attach/decline
  logs
2026-06-22 19:55:32 +02:00
Arkadiusz Fal
b82241564d Fix macOS permanent black video after player collapse/expand cycles
In separate-window mode, hide() keeps the ordered-out window's SwiftUI
hierarchy alive (nilling contentViewController crashes AVKit), and its
updateNSView unconditionally re-attached the shared MPVOGLView -
stealing it into a window CoreAnimation never composites. The layer's
silent skip-render fallback then consumed every frame flag, so video
stayed black (climbing vo drops) until app restart.

- Decline stealing the shared view from a visible container into a
  non-visible one; owner-initiated transfers bypass the guard
- Only transfer to containers in visible windows or the window still
  tracked by ExpandedPlayerWindowManager; park the view otherwise and
  reclaim it when a container gains a window
- Log skip-render frame consumption (rate-limited) and track render
  health counters (draws/skips/dropped draws)
- Add a watchdog to the playback stats task that detects a stalled
  video output and re-attaches the view to a visible container
- Don't mount player layouts during zero-size layout passes
2026-06-21 21:55:37 +02:00
Arkadiusz Fal
5bc774dd83 Fix missing storyboards when advancing to next queued video
Queue items carry a pre-resolved stream, so play() takes the
provided-stream shortcut and skips the full API fetch that populates
storyboards. The follow-up loadOnlineStreams() call fetched the response
containing them but dropped storyboards on the floor.

Keep the storyboards in loadOnlineStreams() and apply them when none are
set (preserving local storyboards for downloaded playback), and bail out
if the current video changed during the fetch.
2026-06-18 23:49:15 +02:00
Arkadiusz Fal
ea1aa6fe7e Always restore from PiP to expanded player window on macOS
The PiP restore handler only expanded the player when mini player
video was disabled, which is the iOS behavior where playback can
continue in the mini player. On macOS the mini player is not a
restore target, so always reopen the player window; iOS keeps the
existing logic.
2026-06-17 19:05:03 +02:00
Arkadiusz Fal
5556be0504 Fix macOS player window staying visible when starting PiP after a stop
MPVBackend.stop() clears the PiP bridge callbacks on macOS to prevent
crashes during window close, but the backend is reused across videos
and setupPiPIfNeeded is guarded by isPiPSetUp, so the callbacks were
never wired again. The next PiP session then started without notifying
PlayerService (no onPiPStatusChanged), leaving the player window
visible alongside the PiP window, both playing.

Extract the callback wiring into wirePiPBridgeCallbacks() and re-run it
in startPiP() and in setupPiPIfNeeded when already set up.
2026-06-17 09:25:50 +02:00
Arkadiusz Fal
32b1dac249 Fix macOS PiP window not resizing when switching videos
Switching videos during active PiP left the window at the old video's
size with white space around the new content, for two reasons:

- MPV reports the new video's width and height as separate property
  events while the old values are kept across switches, so a transient
  mixed size (new width with old height) could reach the PiP capture
  path and AVKit sized the window from that frame. Coalesce the PiP
  capture-size/aspect update with a short debounce so only settled
  dimensions are used.

- On macOS nothing told AVKit about the new aspect: unlike iOS, the
  bridge never flushed the sample buffer renderer on an aspect change
  during active PiP, and AVKit doesn't resize the macOS PiP window on
  its own. Flush the renderer, reset the format description, and
  resize the PiP window to the new aspect ratio explicitly.
2026-06-16 23:59:57 +02:00
Arkadiusz Fal
b21a4920be Fix macOS opening player window instead of switching video in PiP
The PiP-active guard that skips expanding the player when opening a new
video was compiled iOS-only, so on macOS a browser deep link always
re-showed the hidden player window instead of playing the new video in
the existing PiP window. Extend the guard in both openVideo overloads
and the Handoff path to macOS.
2026-06-16 20:42:39 +02:00
Arkadiusz Fal
4effe5d7a5 Fix low-FPS iOS PiP by reporting swaps while PiP is active
Since 1d9c5e3b performRender feeds swap timing to mpv via
mpv_render_context_report_swap, but only after presentRenderbuffer -
which is skipped while PiP is active. Once swap reports stop mid-stream,
display-vdrop's vsync estimation goes stale and mpv drops nearly every
frame (vo drop counter climbing ~19/s on a 24fps stream), turning PiP
into a slideshow.

The frame is genuinely displayed during PiP via the
AVSampleBufferDisplayLayer, so report the swap on that path too.
2026-06-15 09:26:43 +02:00
Arkadiusz Fal
0d05daae9d Fix black screen after restoring PiP when player window was closed on macOS
The expanded player window hidden for PiP is reused on restore, so its
MPVContainerNSView kept a stale weak reference to the shared render view
that the mini player preview had taken during PiP. On restore the mini
preview released the view as an orphan and the reused container skipped
re-attaching it because its guard only compared view identity.

- Skip re-attach in setPlayerView only when the view is still actually
  our subview, matching the iOS container's stolen-view handling
- Transfer the shared render view to another living container on unmount
  instead of orphaning it, since SwiftUI is not guaranteed to re-run
  updateNSView on a hidden window's content after restore
- Force a repaint when restoring the window hidden for PiP; forced draws
  requested while ordered out are dropped and the layer is pull-based
2026-06-08 22:55:31 +02:00
Arkadiusz Fal
d667b56967 Fix silent audio on tvOS with Atmos Continuous Audio Output (#928)
With Atmos "Continuous Audio Output" enabled, some HDMI routes report
32 output channels, which mpv's audiounit AO cannot open - playback
stays silent while other (AVPlayer-based) apps work fine.

Update MPVKit to 0.41.0-n8.1.2, which builds mpv's avfoundation AO
(AVSampleBufferAudioRenderer, previously macOS-only) for iOS/tvOS
(mpvkit/MPVKit#73), and prefer it on tvOS via ao=avfoundation,audiounit
so mpv still falls back to audiounit if it fails to initialize. iOS and
macOS audio outputs are unchanged.

The MPVKit pin must be exactVersion: SwiftPM sorts the pre-release
identifier below 0.41.0, so a range starting at 0.41.0 never resolves
to it. Note this tag also bumps FFmpeg from 8.0 to n8.1.1.
2026-06-05 07:04:04 +02:00
Arkadiusz Fal
76e5220c95 Fix no video on iOS/tvOS devices after GL two-thread hardening
1d9c5e3b removed the main-thread EAGLContext bind from setupAsync so
the context is never left current on two threads. But libmpv's
mpv_render_context_create requires a current GL context on the calling
thread (it probes glGetString(GL_VERSION)), so render context creation
failed with MPV_ERROR_UNSUPPORTED (-18) and every video played with no
--vo ("No render context set") on real iOS/tvOS devices.

Bind the context around the createRenderContext call and unbind right
after. The bind is scoped to setup, before the display link starts and
before any frame is rendered, so the never-left-current invariant that
protects the A10X fence fix (#947/#949) still holds.

Verified on Apple TV 4K (3rd gen): createRenderContext succeeds and
video frames render.

Claude-Session: https://claude.ai/code/session_01UEXP5f6F4S1zLdY8fdVuLJ
2026-06-04 21:24:43 +02:00
Arkadiusz Fal
5c8c55f41c Fix macOS black video after player close/reopen
The macOS CAOpenGLLayer is pull-based: after the shared render view is
re-parented into a reopened player window, a single forced repaint is
issued. draw() consumed the forceDraw flag before validating the
viewport, so when that repaint landed while the layer was still 0x0
(the expanded window builds its content a runloop late), the request
was swallowed. With mpv paused during load, nothing repainted again,
leaving the video permanently black with working audio.

- Validate viewport/framebuffer in draw() before consuming flags;
  on failure keep the forced draw armed and retry (bounded, 5x50ms)
- Repaint from setFrameSize on the first non-zero layout while a
  forced draw is still pending
- Drop the stale fbo=1 fallback; render only into the validated
  live framebuffer binding
- clearToBlack: take the CGL lock and skip glClear when no
  framebuffer is bound - clearing FBO 0 on this core-profile context
  latched GL_INVALID_FRAMEBUFFER_OPERATION, which libmpv reported on
  every load
- Add diagnostics for dropped draws and the first-frame render
2026-06-03 22:00:28 +02:00
Arkadiusz Fal
fa6ad2235c Fix tvOS GL render pipeline defects behind A10X IOFence crash
Apple TV 4K 1st gen on tvOS 26 crashes to the home screen with a
"blocked by IOFence" gpuEvent at playback start (#949) and stutters
with a starved demuxer cache (#947). Defensive fixes in the shared
iOS/tvOS OpenGL path:

- Drain in-flight GPU work (glFinish) before destroying the
  renderbuffer, so a pending presentRenderbuffer fence can't hold the
  CAEAGLLayer IOSurface being torn down or reallocated
- Never leave the EAGLContext current on the main thread: framebuffer
  create/destroy and the background glFinish now unbind on exit, and
  setup no longer binds the context on main at all
- Disable retained backing on tvOS; it exists only for iOS PiP frame
  capture and adds per-frame IOSurface fence pressure
- Report swaps to mpv after each present so display-vdrop (the tvOS
  default video-sync) gets swap-timing feedback, matching macOS

Add verbose-gated diagnostics for remote triage: playback stats every
10s (cache fill/rate, dropped frames, hwdec, avsync) and a rate-limited
slow-frame warning in performRender.

Addresses #949 and #947
2026-05-31 11:27:09 +02:00
Arkadiusz Fal
5f49f2d022 Fix macOS black video after re-parenting the shared player view
The player video surface is a single shared MPVOGLView that is re-parented
between the mini-bar container (main window) and the expanded-player sheet
(a separate sheet window). When presenting the sheet, the new container
attaches the shared view before the old mini-bar container tears down, so
the old container's viewWillMove(toSuperview: nil) was ripping the shared
view out of its new home via removeFromSuperview() — leaving it with no
window and a black surface. Symmetric, so it also blanked the mini-bar
preview after collapsing.

Only detach the shared view if it still belongs to the container
(superview === self). Also force a repaint on window reattach so a paused
frame redraws, and extend the onAppear resumeRendering to macOS.
2026-05-25 19:46:35 +02:00
Arkadiusz Fal
6dfa63c263 Remove Enable DASH setting; make DASH a last-resort format
Drop the dashEnabled toggle and its plumbing. Stream auto-selection now
keeps DASH as a candidate but ranks it strictly below progressive and
HLS formats, so DASH is only chosen when it is the only format available
(including live-stream fallback after HLS). The manual quality selector
hides DASH entirely on iOS/tvOS/macOS. Reconciles BackendSwitcher, which
previously treated HLS/DASH equally, to use the same DASH-last ranking.
2026-05-20 07:14:31 +02:00
Arkadiusz Fal
bc81b03821 Fall back to software OpenGL renderer instead of crashing on GPU-less Macs
MPVOpenGLLayer called fatalError() when CGLChoosePixelFormat failed for
both the 10-bit and 8-bit (kCGLPFAAccelerated) formats, killing the app at
launch on machines without a usable GPU such as virtual machines.

Add a non-accelerated software pixel format as a last resort and make the
whole OpenGL init path failable: createPixelFormat/createContext return nil
(with an error log) instead of aborting, MPVOpenGLLayer.init becomes init?,
and MPVOGLView holds an optional layer, throwing openGLSetupFailed from
setup(with:) when OpenGL is unavailable. That error is already handled by
MPVBackend.setupMPVAsync, so the app now launches cleanly and only logs that
video playback is unavailable.
2026-05-15 07:03:24 +02:00
Arkadiusz Fal
c168fbae02 Add tvOS A/V sync diagnostics on its own settings page
Surface audio-delay (±10/±100 ms) and video-sync-mode controls behind an
"A/V Sync" navigation row in Advanced settings rather than inline, keeping the
Advanced page uncluttered. Audio delay applies live to the running MPV
instance; sync mode takes effect on next playback.
2026-05-14 09:43:25 +02:00
Arkadiusz Fal
6e5714dd86 Fix tvOS MPV startup playback stability 2026-05-10 15:28:12 +02:00
Arkadiusz Fal
82d2830208 Refresh track list when advancing to next queued video
Player settings on tvOS showed the previous video's tracks after queue
advance because availableStreams was never cleared on a video change and
playQueuedVideo only seeded a single pre-resolved stream. Reset the list
on a new video, then fetch the full streams in the background for global
videos, and repoint state.currentStream/currentAudioStream to the
refreshed entries so the picker checkmarks land on the playing tracks.
2026-05-10 15:28:11 +02:00
Arkadiusz Fal
6a343311ea Add tvOS display frame rate and dynamic range matching
Lets the Apple TV switch its HDMI output to match the playing video's
frame rate and dynamic range via AVDisplayManager.preferredDisplayCriteria,
driven from MPV's container-fps and video-params/gamma. Two opt-in toggles
(default off) live under Playback → Display on tvOS; both are no-ops on
other platforms. Anchor an AVKit class symbol so the linker keeps AVKit
linked — Swift only autolinks AVFoundation here, and without AVKit the
UIWindow.avDisplayManager category isn't loaded at runtime.
2026-05-10 15:28:11 +02:00
Arkadiusz Fal
100e762d4b Suppress stale player error after switching videos mid-retry
If the MPV backend was retrying a failed load and the user switched to
another video before retries exhausted, the eventual error was published
to the player UI even though that video was no longer active. Guard the
catch block with a loadingVideoID check so stale errors are dropped.
2026-05-10 15:28:11 +02:00
Arkadiusz Fal
1f0f3a8cf0 Resume and seek when reopening currently-loaded video
When the same video was already loaded (typically paused), opening it
again via the URL scheme, a deep link, or a remote-control loadVideo
command did nothing — the player just stayed paused. Now the same-video
early-return path resumes playback if paused and seeks to the supplied
startTime, so timestamps from URLs and remotes are honoured even when
the video is already loaded.

URLRouter gains a parseTimestamp helper that reads t/time/start query
params in plain-seconds and YouTube-style (1h2m3s) forms, and the deep
link handler now forwards that timestamp through to openVideo.
2026-05-09 15:00:08 +02:00
Arkadiusz Fal
aabf5313fa Expose Background Playback toggle on tvOS, default off
Surfaces the existing iOS/macOS Background Playback setting in the tvOS
Playback settings, defaulting to off so audio stops when the user leaves
the app via the TV button. Pauses playback on .background/.inactive when
the toggle is off, regardless of audio route — the user's setting wins
over AirPlay/HomePod handoff. Also auto-shows the tvOS player controls
when returning to foreground so the paused state is immediately visible
and actionable.
2026-05-09 14:50:38 +02:00
Arkadiusz Fal
42621b8193 Suppress tvOS Now Playing while AirPlay/HomePod route is active
On tvOS, registering MPRemoteCommandCenter handlers makes the system
classify the app as a long-form video media app. When audio is routed
to AirPlay 2 endpoints (HomePods), the system then enforces a ~2s
look-ahead buffer in the AVAudioSession → AirPlay 2 pipe for
multi-speaker sync. The result is a 2-3 second audio drain on pause
and refill on resume.

The buffer lives downstream of mpv, so no mpv command (ao-reload,
seek-flush, audio-add) can flush it; AVAudioSession setCategory
overrides (mode/policy) and setActive(false)/setActive(true) cycles
are also ignored once the app is media-classified.

Workaround: detect the active audio route via routeChangeNotification
on tvOS. While AirPlay/HomePod is the output, suppress
MPNowPlayingInfoCenter publication and disable every MPRemoteCommand
so tvOS un-classifies us. When the route returns to local outputs,
republish the cached Now Playing info and reconfigure remote commands.
A latch defers all media integration until the audio session has been
activated at least once, so no commands are registered before the
route can be evaluated.

Trade-off: while playing to HomePods, the Control Center widget and
external Siri Remote play/pause are not available — but pause/resume
is responsive.
2026-05-09 14:18:17 +02:00
Arkadiusz Fal
5ab9e3d5bf Surface mpv error details on stream load failure
Subscribe to mpv log messages and capture END_FILE error code/string so
load failures bubble up specific causes (HTTP 404/403, DNS failure,
demuxer errors) instead of a generic 10s timeout.
2026-05-08 20:43:27 +02:00
Arkadiusz Fal
b7b7c5ac62 Fix local folder playback after app container UUID changes
After iOS reinstall/restore the app container UUID rotates, which left both
the persisted source.url and the security-scoped bookmark pointing at a
no-longer-current path. Files derived a stale absolute path that got appended
onto the resolved bookmark, producing doubled URLs that MPV could not load.

- Resolve the base URL by picking whichever of the bookmark or source.url
  actually exists on disk.
- Compute MediaFile relative paths against the resolved root so they survive
  later container changes.
- Hold the security-scoped resource access for the source's lifetime via a
  shared resolver, so MPV can open files long after the directory enumeration
  that resolved the bookmark has returned.
- Normalize legacy absolute paths embedded in old recents/history video IDs
  so they re-resolve under the current container.
2026-05-08 18:23:16 +02:00
Arkadiusz Fal
158d518e3a Trim comments and hoist settings read in stream filtering
Drop comments restating what the code shows; hoist allowSoftwareDecodedFormats
out of the recommendedVideoStreams filter closure so the bridge property is
read once per render instead of once per stream.
2026-05-07 18:03:34 +02:00
Arkadiusz Fal
16477641ab Add Allow Software-Decoded Formats playback setting
Lets the auto stream selector pick formats whose codec isn't hardware
decoded on the current device. Defaults off; when on, 4K VP9/AV1 can be
auto-selected on Apple TV models without those decoders. Software-decoded
streams also move into the Recommended section so the selection stays
visible without enabling advanced stream details.
2026-05-07 18:00:14 +02:00
Arkadiusz Fal
fac297e4d6 Cache and prewarm Invidious proxy auto-detection
The proxy auto-detect path (when proxiesVideos is off) HEADed a
googlevideo URL with a 5 s timeout on every video. The verdict is a
property of the network, not the video, so the cost was paid for no
reason on videos 2..N. On a network where the CDN is blocked the full
5 s timeout was added to playback startup every single time.

Two changes:

1) ProxyDetectionCache (actor, per-instance, 10 min TTL). First miss
   pays the HEAD once and caches the verdict; subsequent videos hit
   the cache synchronously. Concurrent callers share one in-flight
   probe. The last-seen sample CDN URL is retained so future probes
   don't need a fresh URL from the current video.

2) PlayerService kicks off InvidiousAPI.prewarmProxyDetection() in
   parallel with the videoWith... API call. By the time streams come
   back, the verdict is usually already cached and proxyStreamsIfNeeded
   is a sync lookup. Cheap when there's nothing to prewarm.

Cache invalidation:
- on InstancesManager.update (URL change, proxy toggle flip)
- on InstancesManager.remove
- TTL covers the network-change case for now (no NWPathMonitor yet)
2026-05-04 08:04:55 +02:00
Arkadiusz Fal
096df34f64 Redesign tvOS player controls with centered transport cluster
- Move Close to a top-right circular icon; bottom row reorganizes into
  left (Settings/Info/Comments), center transport (Previous/PlayPause/Next),
  and right (Queue) clusters with equal side frames so transport stays
  geometrically centered.
- Introduce a circular icon-only `TVTransportButtonStyle` (primary variant
  for Play/Pause) mirroring the new Close button look.
- Always render Previous/Next so Play/Pause position is fixed; dim and
  disable when unavailable.
- Share `isTransportDisabled` on `PlayerState` and reuse it on iOS and
  tvOS; apply it (plus symbol replace transition) to the tvOS Play/Pause
  button.
2026-04-18 20:38:02 +02:00
Arkadiusz Fal
d6d15df105 Deduplicate time formatting and clean up unused code
Extract shared TimeInterval.formattedAsTimestamp replacing 8 identical
formatTime/formattedTime implementations across player views. Remove
unused currentTime parameter from GestureSeekPreviewView. Consolidate
duplicated geometry math in MacOSControlBar into seekPreviewPosition().
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
a7e5ebb068 Fix 5 TestFlight crash types from builds 250-254
- Fix BGTaskScheduler assertion crash on Mac Catalyst by guarding all
  iOS background task APIs with isMacCatalystApp check
- Fix iPad popover crash in UIPopoverPresentationController by adding
  .presentationCompactAdaptation(.sheet) to all 27 confirmationDialogs
- Fix SwiftData assertion crash when accessing deleted Bookmark model
  properties during SwiftUI hit testing in BookmarkRowView
- Fix UICollectionView invalid item count crash on queue swipe-to-delete
  by using ID-based removal with withAnimation instead of stale index
- Fix Range crash in storyboard download when storyboardCount is zero
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
6298b38cba Add video proxy support with live toggle for Invidious/Piped instances
Adds a "Proxy videos" toggle in instance settings that routes video
streams through the instance instead of connecting directly to YouTube
CDN. Includes auto-detection of 403 blocks and live re-application of
proxy settings without requiring app restart or video reload.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
ef3cddefeb Persist author cache to disk for instant channel info across restarts
Back the in-memory authorCache with a JSON file in ~/Library/Caches/AuthorCache/.
Disk is lazy-loaded on first lookup and saved asynchronously on each cache update.
Capped at 500 entries to prevent unbounded growth.

- Cache author data from video detail API responses (PlayerService, VideoInfoView)
- Replace ChannelView's private CachedChannelHeader with shared CachedChannelData
- Enrich author with cached avatar/subscriber count in VideoChannelRow, TVDetailsPanel, VideoInfoView
2026-04-18 20:37:25 +02:00
Arkadiusz Fal
aaf53ef9d1 Fix lock screen always showing 10s seek regardless of system controls setting 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
100df744d9 Yattee v2 rewrite 2026-04-18 20:37:24 +02:00