On iPhone Duo the page first lays out at full width, then narrows for the
status column, and the width-change re-snap animated from the first card
to the tapped one. Only animate the re-snap when the current card was
already on screen at the old width.
Pausing rendering in the background left mpv decoding every frame
(hardware decode plus copy back) only to drop it, keeping a 1080p video
at ~15% CPU with the screen off. After 5 s in the background without PiP
the video track is now disabled (vid=no); returning to the foreground
re-enables it and seeks exactly to the current position, which keeps
audio and video in sync (avsync 0.000 vs -0.044 and 22 drops without the
seek). Streams loaded while backgrounded start with video disabled.
Background CPU drops to ~3.7% on an iPhone 12 mini.
The TimelineView re-rendered both mini player lines every frame (~17% of
the main thread on an iPhone 12 mini, even while paused). The text is now
drawn once in a hosted view and a repeating keyframe animation slides its
layer in the render server. macOS keeps the TimelineView.
- Send Now Playing elapsed time once per 5-second bucket; the modulo check
fired for every frame of that second (12 updates/s on average).
- Skip mpv renders while paused unless mpv requests a redraw (seek, new
frame, PiP change, new load) instead of redrawing the same frame at
display rate.
- Read playback time untracked in the progress bar and time labels while
the controls are hidden, so they are not re-rendered on every tick.
- Only clear the current SponsorBlock segment when one is set.
SwiftUI only reports reserved regions that intersect the querying view,
and the subscriptions content stops at the trailing safe-area column, so
on device the channel rail saw no regions and ran up under the time and
Dynamic Island. A background reader that ignores the safe area now
records the regions in global coordinates for the rail placement.
- MPVRenderView keeps its display link paused while the stream has no
video, instead of rendering and presenting 60 empty frames a second
- Chapter segments get clamped played/buffered fractions and are
equatable, so a time update re-renders only the chapter under the
playhead instead of every segment
- The mini player pauses its scrolling titles while the expanded player
covers it (iOS/tvOS)
- Performance logging no longer flags the first sample after a
foreground/background transition as high CPU
With the expanded player open in audio-only mode on an iPhone 12 mini,
app CPU dropped from ~30% to ~12% and wakeups from ~128/s to ~37/s.
Performance Logging (Developer settings) logs a resource sample every 5s
in the foreground and background: CPU by area and thread, wakeups, GPU,
memory, disk, activity counters as rates (mpv property updates, player
time publishes, renders, saves, network, images, CloudKit events, log
entries, main run-loop wakeups), SwiftUI body evaluations of the player
views, and app/player/battery context. Each foreground/background
transition logs a summary of the period. MetricKit metrics and
diagnostics (CPU exceptions, hangs) are logged whenever logging is on.
Background diagnostics (iOS) log lifecycle, audio session and power
events with an app/player/render snapshot and a heartbeat that detects
suspension gaps and main-thread stalls. Logs are persisted per session
in Caches/Logs and the previous session is included in exports, so logs
survive the app being swiped away.
Every mpv time-pos tick updated PlayerState.currentTime and bufferedTime,
re-rendering the invisible player controls, progress bar and chapter
segments several times per second while the app played in the
background, which drained the battery during long audio-only sessions.
- PlayerState keeps currentTime/bufferedTime current for readers but
stops notifying observers while publishesTimeUpdates is off;
PlayerService turns it off on entering the background and back on
(with one catch-up notification) before returning to the foreground
- MarqueeText pauses its animation timeline outside the active scene
- Replace the 100 ms polling loops in RemoteControlCoordinator and
ToastManager (iOS/tvOS) with observation-driven waits
Main-thread samples during 30 s of background audio-only playback on an
iPhone 12 mini went from 1289 to 248.
Development-signed builds now opt into the Production CloudKit
environment via com.apple.developer.icloud-container-environment,
driven by a CLOUDKIT_ENV build setting. A new Debug-CKDev configuration
and "Yattee (CloudKit Dev)" scheme switch to the Development
environment for schema work.
The DEV badge and banner in iCloud settings now reflect the actual
environment (YatteeCloudKitEnvironment Info.plist key) instead of
#if DEBUG, and the sync engine logs the environment at startup.
Brightness controls used UIScreen.main, which on iPhone Duo can be a
different display than the one hosting the player. Route all brightness
reads and writes through PlayerBrightness, which targets the screen of
the window hosting the controls.
Tapping a video after the current one played to the end asked Play Now or Play Next, though replacing a finished video loses nothing. An ended video no longer counts as active playback; a non-empty queue still shows the sheet.
Playback menu items toggle the info panel below the video (⌥⌘I), the
side column (⌥⌘S) and both at once (⌥⌘P) while the corner layout is
active. Showing one region from the hidden state brings back only that
region; hiding the last visible one hides the whole panel.
Every layout pass with a new size recreated the framebuffer and rendered
synchronously on the render queue, blocking the main thread behind the
in-flight render. Animated layout changes (corner layout panel toggles,
rotation) dropped to a few frames per second. Size changes arriving in
quick succession now let the layer stretch the last frame and recreate
the framebuffer once the size settles; one-off resizes still recreate
immediately.
Content-sized pages like the Search empty state only got the opaque
window background behind their own frame, leaving darker window
material on both sides.
The window-level pinch was blocked whenever the landscape panel was
pinned and visible (the corner layout always counts as pinned) or a
stale portrait-panel flag was set. In the corner layout the pinch now
starts when it begins inside the video region, and the panscan
control button is enabled there too.
The iPhone Duo hinge and status-column code uses iOS 27.1 SDK APIs, which
the macos-26 image (Xcode 26.6 max) cannot compile. Build jobs move to the
xcode-27 runner and select the 27.1 RC by full path, since the
Xcode_27.1.app symlink is ambiguous with the beta and App Store Connect
rejects beta-built uploads.
The separate comments group (pill mode, author avatars) is gone: comments
are a row in the pages/sections list whose editor holds both options.
Inline-sections hosts list comments as a locked pill row at the bottom;
a foldable iPhone in the corner layout lists real pages, including
comments and queue in the side column.
Newer Invidious returns a single authorThumbnail URL on comments instead
of the authorThumbnails array, so every comment fell back to the
placeholder avatar. Decode authorThumbnail as a fallback.
SwiftUI's ShareLink menu on macOS offers no Copy Link item, so there was
no way to copy a video URL from a context menu. Add a macOS-only Copy
Link action next to Share.
Play always started from the first video, so shuffle only affected the
rest of the session. When the queue mode is Shuffle, rotate the list to
a random start so the remaining videos stay queued.
Subscribing or unsubscribing, locally or via iCloud, only posted a
notification, so the cached feed stayed valid and Yattee Server never
received the new channel list until the cache expired. Invalidate the
feed cache on every subscription add and removal, and skip the disk
write when it is already stale.
Toggling cellular downloads recreated the background session with the
same identifier before the old one finished invalidating, and nothing
ever replaced a dead session, so every download threw "Task created in
a session that has been invalidated" until relaunch. Wait for the old
session's invalidation before creating the new one, rebuild the session
when the delegate reports the live one invalid, and recreate it once
when task creation hits the invalidated exception.
refreshSync() discarded the persisted engine state and the pending
buffer, and the engine state is where queued saves and deletions live,
so tapping Sync Now within the debounce window dropped history,
progress and bookmark changes. Carry the old engine's pending changes
into the buffer that the new engine adopts, then schedule a send.
tvOS has no other route to Settings, so unchecking it in Sidebar settings
locked users out. Treat Settings as a required sidebar item on tvOS; an
already-stored hidden value is ignored because required items bypass the
visibility map.
NowPlayingService set the media-library key MPMediaItemPropertyMediaType,
which MediaRemote ignores, so external clients classified playback as
audio. Use MPNowPlayingInfoPropertyMediaType instead.
A remote playlist load cancelled on tvOS can still land. If the playlist
it brings is empty, playlistBody showed the stale "Loading was stopped."
instead of the empty state.
With nothing to import, the playlists and subscriptions import screens
had no focusable content; only the container's toolbar Done, which is
not a dependable focus target on tvOS.
A remote playlist this account does not own showed only a spinner while
loading, with or without a cached header, so the tvOS remote had nothing
to focus. Cancel stops at the error view and its Retry, since a sidebar
playlist is a tab root that cannot be popped.
Without a cached header the channel showed only a spinner, and when the
channel request failed while the videos request succeeded (no cache to
build the channel from) it rendered nothing at all; both left the tvOS
remote with nothing to focus. Cancel stops at the error view and its
Retry, since a sidebar channel is a tab root that cannot be popped; the
blank state now shows the load error with Retry on every platform.
The video page showed only a spinner until the video loaded, and
rendered nothing at all in the frame before a videoID load started;
either left the tvOS remote with nothing to focus. Both now show the
spinner with the delayed Cancel, which pops the page.
A screen showing only a spinner has nothing focusable, so a slow or hung
extraction stranded the tvOS remote. Add TVStalledLoadCancelButton, a
Cancel that appears under a loading spinner after three seconds so
normal loads never flash it, and use it for external video extraction.
The Refresh and View Options header was hidden until the first listing
arrived, leaving only a spinner; an SMB or WebDAV connection can hang
for a long time with nothing the remote could focus. Refresh ignores
presses while a load is in flight.
The toolbar Cancel is iOS/macOS only, so while scanning with no shares
found yet (and when discovery is unavailable) the tvOS sheet had nothing
the remote could focus.
A successful load with no contributors, and the first frame before the
load started, rendered an empty form with nothing focusable, stranding
the tvOS remote.