93 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
c7ad4fad2e Make CloudKit apply path scope-aware and re-upload local-newer merges
CloudKit record names carry a source scope suffix so the same channel
or video ID can exist under different sources, but the apply path
matched local entities by bare ID: incoming records could merge into an
entity from a different scope, and a remote deletion of one scope
deleted every local entity with that ID and cascaded scoped deletions
for all other scopes back to CloudKit.

- Match local entities on the full mapper-derived record name when
  merging incoming records, materializing records for upload, and
  applying remote deletions.
- Remote deletions now remove only the matching entity and no longer
  queue CloudKit deletions (no echo, no cross-scope cascade).
- Insert dedupe for CloudKit-applied records is scope-aware, so a
  same-ID record from a different source inserts instead of being
  silently dropped. Remotely added bookmarks also update the
  fast-lookup cache immediately.
- When conflict resolution during a fetch keeps newer local data, the
  merge result is queued for upload; previously the server (and other
  devices) kept the stale version unless the entity was edited again.
  Strict timestamp comparisons prevent re-upload ping-pong between
  devices.
2026-07-15 20:15:03 +02:00
Arkadiusz Fal
10ca000387 Fix conflict resolver field names for recent channels and playlists
The recent-channel resolver copied avatarURLString/providerName and the
recent-playlist resolver copied authorID/providerName - fields that do
not exist in the records the mapper writes - while missing the real
ones (thumbnailURLString, subscriberCount, isVerified, videoCount). As
a result, when the local side won a conflict, those fields silently
kept the older server values.

Also drop the notificationsEnabled preservation in the subscription
resolver: the subscription mapper never writes that field since
notification preferences moved to their own record type.
2026-07-14 23:50:04 +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
9467ac4dba Migrate CloudKit sync to state-driven CKSyncEngine pattern
Register pending changes with the engine state instead of keeping
app-managed record arrays, and materialize records from local data at
send time in nextRecordZoneChangeBatch. This fixes several data-loss
paths:

- Pending changes now persist inside the engine state serialization,
  so saves queued during the debounce window (or while offline) survive
  app termination. Legacy record-name keys are migrated once.
- Transient CloudKit errors (zoneBusy, serviceUnavailable, rate limits,
  network failures, limitExceeded, batchRequestFailed) re-register the
  change for engine-scheduled retry instead of silently dropping it.
- Conflicts are resolved against the exact record that was sent; the
  resolved record (carrying the server change tag) is cached and takes
  precedence at the next send, and any newer local edit evicts it.
- Queue methods are now synchronous, removing a task-reordering race
  where a fast delete/re-add could end up deleting the re-added entity.
- Records are built fresh at send time, so queued changes no longer
  upload stale field snapshots, and batch size limits are handled by
  the framework.
2026-07-13 23:25:00 +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
8d4f472348 Unify macOS content view backgrounds with opaque window background
Views without an explicit background sat on the translucent window
material, which picks up a wallpaper tint (bluish in dark mode) and
clashed with views drawing windowBackgroundColor, making the video
info header gradient fade visibly mismatch. Add opaqueWindowBackground()
and apply it centrally in NavigationDestinationHandlerModifier to both
stack roots and pushed destinations, plus VideoInfoView for
presentations outside navigation stacks.

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
2026-07-06 09:36:49 +02:00
Arkadiusz Fal
ecb6a9dd7a Replace macOS inline player sheet with full-window overlay
When "Play in a separate window" is disabled, the expanded player was a
native sheet attached to the main window. AppKit refuses fullscreen with
an attached sheet, so pressing F swapped the sheet for an in-window
overlay before fullscreening — a visible two-step transition.

Present the inline player as the full-window overlay from the start, so
fullscreen is just the window's native toggle. This removes the whole
sheet-swap machinery (toggleSheetFullScreen with its detach timing,
SheetWindowResizer, sheet sizing helpers).

- beginInlineOverlay/endInlineOverlay hide/restore the main window
  toolbar and mirror the window's fullscreen state into the renamed
  isMacInlinePlayerFullScreen flag via NSWindow notifications, so exits
  through Esc/green button/Mission Control stay in sync
- Collapsing exits fullscreen only if it was entered for the video
- Esc in the windowed overlay collapses to the mini bar via a new
  onCollapse callback (replacing the sheet's default Esc-dismiss)
- ContentView registers its hosting window (MainContentWindowReader) so
  the overlay targets the right window even when the setting is toggled
  while the Settings window has focus
2026-07-03 21:57:55 +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
9bcb8e35a6 Add native fullscreen for macOS sheet-mode player via in-window overlay
The fullscreen button did nothing in sheet mode: AppKit ignores
toggleFullScreen on a window attached as a sheet, and the old fallback
targeted the key window — the sheet itself. Two other strategies were
ruled out first: fullscreening the sheet's parent is silently refused
while any sheet is attached, and resizing the sheet's backing window to
screen size NaN-asserts inside AppKit's sheet positioning even without
animation.

Working approach: swap the sheet for a plain overlay in the main window
and native-fullscreen the now sheet-free main window.

- isMacPlayerFullScreenOverlay on NavigationCoordinator dismisses the
  sheet (presentation binding excludes it; the setter ignores the
  dismissal write-back) and mounts ExpandedPlayerSheet as a black-backed
  overlay in ContentView
- ExpandedPlayerWindowManager fullscreens the parent after the sheet
  detaches, verifies AppKit accepted it (rolls back if refused), hides
  the window toolbar (NSWindow keeps it as a strip above the overlay
  otherwise) and restores it on exit
- a didExitFullScreen observer restores the sheet on any exit path
  (button, Esc, green button, Window menu); closing the player while
  fullscreen exits the main window's fullscreen too
- scheduleSharedViewAdoptionRetry on entry re-homes the shared render
  view once the dismissing sheet's window loses visibility — its steal
  is declined mid-dismissal and nothing else retried, leaving the
  overlay black until the next video load
2026-06-25 21:55:47 +02:00
Arkadiusz Fal
bef29a79a8 Keep Sparkle resetUpdateCycle on the main actor
SPUUpdater is a main-thread-only API; calling resetUpdateCycle() from a
detached utility task violated Sparkle's threading contract and raised
a Swift 6 concurrency error (non-Sendable SPUUpdater captured in a
detached task). A plain main-actor Task keeps the deferral that avoids
running the feed-cache work synchronously inside the didSet.
2026-06-24 18:52:32 +02:00
Arkadiusz Fal
da6c949f51 Resume CloudKit sync from persisted state instead of full re-fetch
Previously CKSyncEngine was created with nil state serialization on
every launch, replaying the entire zone change history (hundreds of
merge/delete log lines and re-downloads each start). Now the saved
state is loaded so launches only fetch changes since the last session.

To make incremental sync safe:
- Clear persisted state once when the record schema version increases,
  so records previously skipped as unsupported get re-delivered after
  an app update.
- Handle remote zone deletion in fetchedDatabaseChanges by recreating
  the zone and re-uploading local data, instead of leaving sync dead.

Account change and manual Refresh Sync still clear the state and force
a full fetch as before.
2026-06-23 23:54:40 +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
e37ac12565 Fix macOS distribution entitlements and Sparkle beta defaults
- Add macOS-specific entitlements via CODE_SIGN_ENTITLEMENTS[sdk=macosx*]:
  the iOS aps-environment key is silently dropped from macOS exports,
  leaving distribution builds without a push entitlement (no CloudKit
  sync pushes). Release now uses Yattee-macOS.entitlements with
  com.apple.developer.aps-environment = production.
- Release-DeveloperID uses Yattee-macOS-DeveloperID.entitlements, adding
  the Sparkle mach-lookup temporary exceptions (-spks/-spki) required
  for the updater to install in a sandboxed app.
- Default Sparkle updater to the beta channel while only beta releases
  exist, so testers receive updates without toggling Advanced settings.
- Set NSHumanReadableCopyright for the app target.
2026-06-19 21:59:55 +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
88fafc5ada Remove Test Connection from source editing
The Test Connection buttons only probed API/server reachability, which
was misleading: a green result said nothing about whether videos would
actually play. Remove both the remote-server and WebDAV bandwidth test
buttons and all code exclusive to them (testBandwidth, BandwidthTestResult,
and orphaned localization keys). Add-time connectivity validation for
SMB/WebDAV sources is retained.
2026-05-19 20:40:28 +02:00
Arkadiusz Fal
86fbdd23f1 Allow importing account-less legacy instances as sources
Legacy v1 instances without an account had no import path after the
switch to explicit account review. Surface them in a separate "Sources"
section of the legacy import view where they can be added with a single
tap (no sign-in) or removed, mirroring the accounts flow. Instances tied
to an account stay in the Accounts section and are not duplicated.

Detection now gates the entry points and first-launch prompt on any
legacy data (accounts or sources), not accounts alone.
2026-05-18 23:54:42 +02:00
Arkadiusz Fal
c4bcfd1211 Remove dead legacy-import pipeline and unused LegacyAccount.password
The manual account-import flow made the old silent instance-import
pipeline unreachable. Drop the orphaned parsing, reachability checks,
batch import, result/error types, and the old import row view, plus the
httpClient dependency they were the only consumer of. Also remove the
LegacyAccount.password field, which was parsed but never read.
2026-05-17 19:29:21 +02:00
Arkadiusz Fal
bde80bfef1 Add manual legacy account import 2026-05-17 13:08:32 +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
ba33823048 Run CloudKit record mapping on the main actor to fix SwiftData race
CloudKitRecordMapper was a standalone actor, so 'await recordMapper.toCKRecord'
from the @MainActor sync engine hopped off the main actor and read live
SwiftData @Model properties (e.g. LocalPlaylistItem.authorName) from the wrong
executor. SwiftData models are bound to the main ModelContext and are not
thread-safe, so this raced the backing store and crashed with EXC_BAD_ACCESS.

Make the mapper @MainActor (it must touch main-isolated models regardless) and
drop the now-redundant awaits. Fixes the crash for all synced model types.

Fixes an EXC_BAD_ACCESS seen in TestFlight build 261.
2026-05-13 20:56:03 +02:00
Arkadiusz Fal
2ed0dc72be Fix CheckedContinuation double-resume in waitForConnectionReady
The 'resumed' guard flag was set inside a dispatched MainActor Task rather than
synchronously in the connection state handler. Two states arriving in quick
succession (e.g. .ready then .failed) could both pass the guard before either
Task ran, resuming the continuation twice and trapping. Claim the continuation
synchronously on the serial queue before dispatching the side effects.

Fixes a SIGTRAP seen in TestFlight build 261.
2026-05-12 22:32:13 +02:00
Arkadiusz Fal
ec5ac944c4 Fix download completion crash from stale array index across await
completeMultiFileDownload captured an index via firstIndex, then awaited a
detached file-size calculation. Another concurrently-completing download could
mutate activeDownloads during that suspension, leaving the index stale and
crashing in Array.remove(at:) with an out-of-bounds index. Remove the download
by identity instead.

Fixes a SIGTRAP seen in TestFlight build 261 (5 reports).
2026-05-11 22:50:16 +02:00
Arkadiusz Fal
a734bb47f7 Fix downloads never finishing due to duplicate AppEnvironment
SwiftUI evaluates a @State default-value autoclosure more than once,
keeping only the first instance but still running the side effects of
the discarded ones. Each AppEnvironment() built its own DownloadManager,
and each DownloadManager registered a background URLSession under the
same identifier. The download task started on the surviving instance's
session, but iOS delivered the completion delegate callback to the other
(leaked) instance, whose activeDownloads was empty - so the finished
file was dropped and the progress spinner spun forever at 0 KB.

- Make AppEnvironment a process-wide singleton (static let shared) and
  reference it from the App's @State, guaranteeing exactly one instance
  (and one background session, one DataManager/CloudKit stack).
- Make DownloadManager.setDownloadSettings idempotent: only create the
  background session when none exists, so a re-entrant call never
  invalidateAndCancels the live session and kills in-flight downloads.
  Cellular changes already route through refreshCellularAccessSetting().
2026-05-10 23:46:02 +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
d49591eaf4 Skip local-folder watches from iCloud sync 2026-05-09 15:53:54 +02:00
Arkadiusz Fal
c64f13a0e6 Show cached channel header on tvOS while channel loads
Render subscriber count, Subscribe button, and (for subscribed
channels) description in the tvOS loading state instead of just
avatar + name + spinner. Seed the in-memory author cache when
navigating to a channel from a video so the first-time channel
view has a name and avatar to display immediately.
2026-05-09 15:00:53 +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