Skip to content

macos support + a pile of playback and player fixes - #33

Closed
yuvrajangadsingh wants to merge 68 commits into
NUber-dev:mainfrom
yuvrajangadsingh:mac-integration-v2
Closed

macos support + a pile of playback and player fixes#33
yuvrajangadsingh wants to merge 68 commits into
NUber-dev:mainfrom
yuvrajangadsingh:mac-integration-v2

Conversation

@yuvrajangadsingh

@yuvrajangadsingh yuvrajangadsingh commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

hey, i ported ytubic to macos and ended up fixing a bunch of playback and player things along the way. everything below is runtime verified on a real mac (apple silicon, macos 15) over several days of daily listening. i dont have a windows machine, so windows is NOT tested, happy to adjust anything that looks off for win.

mac port

  • native window chrome: overlay titlebar, traffic lights, fullscreen aware insets (tauri.macos.conf.json)
  • media keys / now playing via a small objc bridge (now_playing.rs). souvlaki stays windows-only behind cfg(windows), both registering would fight over the system now playing entry
  • the macos .app never rebuilt because bundle.targets was nsis-only; the platform config fixes that
  • login window sends a safari ua on mac (matches the real wkwebview fingerprint), windows keeps YT_LOGIN_UA

playback correctness

  • wkwebview (avfoundation) reads yt-dlps un-remuxed dash m4a headers at exactly DOUBLE the real length. no ffmpeg in the bundle means no fixup; chromium reads the sample tables and never sees this, which is why it never showed on windows. it was behind every "song keeps playing after it ended" report on mac. fixes: clamp against the tracks listed duration, fetch the length from the tracks own /next row when a queue entry arrives without one, and a guard that advances when a doubled file runs past its real end
  • long dead outros auto-advance using synced lyrics as the reference (extended uploads with minutes of trailing filler)
  • yt-dlp gets a client chain (android_vr,ios) so videos drm-blocked on one client resolve on the other instead of 403ing and skipping
  • the clean-audio hunt has duration + artist gates so it cant swap a track to a sped-up bootleg

lyrics

  • a synced lrclib record beats a plain one no matter which endpoint returned it, lrclib keeps duplicate rows per song and the exact-match endpoint kept hitting the plain-only duplicate
  • version qualifiers (remix/live) outrank duration closeness, so a remix upload doesnt get the originals timings
  • timestamps rescale for sped-up/slowed re-uploads (a tempo change cant be fixed by a constant offset)
  • artist-less rows dont query lyric providers at all, title-only matching returned confidently wrong lyrics

player

  • apple music style fullscreen: player column on the left (art, meta, seek with remaining time, transport, persistent volume slider), lyrics fill the rest with big bold lines and a visible window that melts into the pane edge
  • artwork crossfades on track change and never swaps mid-track (the itunes hi-res upgrade only applies if it resolves before the art settles)
  • accent extraction clamps near-greyscale covers to white and lifts dark accents in hsl, so the seek fill always reads on top of its own cover
  • the source toggle now actually plays the video: an explicit switch streams the progressive mv file (?video=1, cached side by side with the audio) and fullscreen shows it in place of the artwork. seeded records stay audio-only so nothing starts pulling full videos silently

merge notes

your 0.3.1 is merged in: retry-nonce stream retries and the media-control listener are intact, the cache sweeper unions both stream variants (.video.mp4) with the new .meta.json sidecars, and discord/lastfm ride along untouched.

happy to split any of this into smaller prs if thats easier to review.

the login window hardcodes a chrome-on-windows ua. that matches reality
under webview2, but on macos the engine is wkwebview, which fingerprints
as webkit. google's consumer account check flags the mismatch and blocks
sign-in with "this browser or app may not be secure". send a safari ua
on macos so the claimed browser matches the actual engine; other
platforms keep the existing ua.
one stalled provider (hung request that never resolves) kept its query
in isLoading forever, so the panel showed "Loading lyrics..." until you
changed tracks. give each provider an 8s budget via AbortSignal.timeout
(with an AbortController fallback for older webkit) threaded through
every fetch in its chain. musixmatch/genius swallow their own errors
into null, so on an aborted run they now rethrow instead — a timeout
gets retried by react-query rather than cached as "no lyrics" for an
hour. all three queries settle now, so the panel lands on the existing
"No lyrics found." state instead of spinning.
macos trackpad momentum scrolling outruns the virtualizer at 8 rows of
overscan. flicking through a long playlist shows white gaps before rows
mount. 16 keeps rows painted through the fastest fling at the cost of a
few extra mounted rows.
lrclib and musixmatch match by a text search, so a track with no real
synced lyrics could pull back a totally different song's words. wire the
existing hitMatches check into both providers and tighten match.ts so a
single shared artist name (or a loose token overlap) no longer counts as
a match. jaccard + meaningfulContains + a collab-aware heuristic decide
it now.
webkit (wkwebview) on macos only decodes a narrow codec set, so the old
bestaudio pick could hand it an opus/webm track it refused to play. add
a mac audio ladder that prefers aac-in-mp4 and falls back through
opus/webm to a progressive muxed mp4 so audio always plays.

also add a separate video stream variant (progressive h264 mp4, itag 18
floor) cached under `<id>.video.mp4` so the same track can hold both an
audio-only and a music-video download side by side. the stream handler
sniffs the container and serves video/mp4 vs audio/* by magic bytes.
non-macos keeps its original webm-first selection.
…nter

the active line flipped 0.72s ahead of the vocal, which read as the
highlight racing the singer. drop the lookahead to 0.2s so it lands on
the beat. also raise the resting position from 0.36 to 0.45 of the
viewport so the active line sits closer to center with a bit more
upcoming text still visible.
some tracks stream a different edit than the lyric timings were cut to,
so the highlight runs a fixed amount ahead of or behind the vocal. add
a +/- 0.25s nudge that floats over the lyric column, shows the current
offset like "+0.75s", and resets on click. the offset applies to both
the active-line math and the click-to-seek target, and persists per
videoId in localStorage.
the expand button on the player card opens a full-window now-playing
view: the cover blown up and blurred as an ambient backdrop, the sharp
art in front, and synced lyrics beside it. esc or the chevron closes it.

when a track has no lyrics the empty right pane and the "no lyrics found"
line looked broken, so drop them and center the art on its own instead.

the seek fill, play button, and active shuffle/repeat pull a vibrant
accent from the cover art. a client-side canvas read taints on the
cors-less art cdns, so a small rust command fetches the bytes and picks
a saturated dominant color inside a legible lightness band, falling back
to brand red for near-monochrome covers.
the progressive yt-dlp stream reports its duration late, so the store
duration sat at 0 for the first seconds of a track. that collapsed the
seek bar's max to 1, which pinned the played fill as a stray red dot at
the far left and left the total time showing 0:00. fall back to the
browse metadata duration until the element reports its own, so the bar
scales right and the total reads correctly from the start.
when the selected source is the music video and the webview can't decode
that stream (media_err_decode / media_err_src_not_supported), the player
showed a raw error banner and skipped the track. instead drop that
track's source back to audio once and let the resolver retry with the
song stream, which every track has. the selected-source check stops it
looping: once we're on audio a repeat failure falls through to the
normal error handling.
toggling to the music video ran a fuzzy search and returned the first
result that wasn't the current id, which for a video-native track (one
that already is a music video) was a completely different clip.

thread the song/video kind onto queue tracks. a video-native track now
stays on its own id in both modes since its stream carries the audio
too, so it never searches. for genuine song<->video pairs, read the
counterpart id straight from innertube's /next wrapper
(playlistPanelVideoWrapperRenderer.counterpart + musicVideoType) and
seed the source toggle with it, so switching lands on the real other
version (same song, grouped by yt) rather than a search. song-native
tracks with no exposed counterpart keep the search as a fallback.
a tauri wkwebview only bridges navigator.mediaSession to windows smtc,
so macos control center, the touch bar, media keys and airpods showed
"not playing" and couldn't drive playback.

add a native mediaplayer.framework bridge (objc2): push title, artist,
album, duration, elapsed and play state into mpnowplayinginfocenter, and
register mpremotecommandcenter handlers (play, pause, toggle, next, prev,
seek) that emit tauri events the playback store already listens for. the
frontend invokes set_now_playing on track change, play/pause and seek.
all cfg(macos), a no-op elsewhere.

artwork is left out for now: the only artwork api this crate build
exposes is the block-based initWithBoundsSize:requestHandler:, whose
return-pointer ownership can't be validated without running on device,
and a wrong guess risks a crash when control center renders the art.
text, times and the transport commands work without it.
# Conflicts:
#	src/components/layout/lyrics-view.tsx
# Conflicts:
#	src-tauri/Cargo.lock
#	src-tauri/src/lib.rs
- tauri.macos.conf.json: native decorations with an overlay title bar
  (hidden title, traffic lights at 14,12) instead of the Windows-style
  custom frame; per-platform so Windows keeps decorations:false.
- Same file fixes macOS bundling: base config targets ["nsis"] which is
  Windows-only, so tauri build compiled the binary but never regenerated
  YTubic.app — installs silently shipped stale builds. targets ["app"]
  plus createUpdaterArtifacts:false (updater signing needs the private
  key, which local dev builds don't have).
- top-bar: hide the custom min/max/close cells on macOS and start the
  nav cluster clear of the traffic lights.
- capabilities: allow set-fullscreen (immersive player) and
  internal-toggle-maximize (drag-region double-click zoom).
…res cover fix

- yt-dlp: player_client tv,android_vr -> android_vr. YouTube is running
  a DRM experiment on the tv client (yt-dlp #12563), so every uncached
  download 403'd; android_vr downloads clean.
- accent extraction: walk an ordered candidate list (local iTunes cover
  first, then every thumbnail largest->smallest) instead of a single
  fragile URL, and fall back to a neutral grey rather than brand red
  when art has no vibrant color or the fetch fails. Shared hook drives
  both the compact player and the fullscreen view.
- image fetcher: follow same-kind redirects with per-hop validation,
  send browser-like headers, and allow the app's own loopback cover
  server, keeping the SSRF allowlist for everything else.
- iTunes artwork: the 100000x100000-999 URL trick now returns HTTP 400
  (Apple dropped it); request 3000x3000bb and bump the cover cache key
  so stale dead URLs re-resolve.
- fullscreen player: ambient backdrop + accent share the candidate
  list with an onError fallback, native macOS fullscreen while open.
- audio engine: auto-hunt the clean audio ("song") version for
  music-video uploads that YouTube didn't pair with a counterpart.
…t is unknown

Video uploads often carry no artist metadata. hitMatches deliberately
relaxed to title-only in that case, so an exact-title hit for a
different song sailed through: a 6:59 Jokhay track showed the lyrics of
a ~4-minute English song that happens to be called Bittersweet.

- match: durationMatches helper (both durations known, within ±4s).
- lrclib/musixmatch: when the request has no artist, keep only hits
  whose duration vouches for the match (musixmatch now receives the
  track duration and reads track_length off search hits).
- genius: search results carry no durations, so artist-less requests
  are unverifiable there — skip instead of guessing.
- sources: bump lyric query keys to v2 so persisted wrong-song entries
  re-resolve under the new gating.
- audio-engine: skip the auto audio-hunt for artist-less tracks — a
  bare-title search could swap playback itself to the wrong song, which
  is worse than wrong lyrics. The manual Song/Video switch still works.

Verified against live LRCLIB: "Bittersweet" with no artist at 419s —
old path picks a wrong 231s song, new path returns no lyrics.
The backdrop <img> was keyed by URL, so every change — track switches
and the mid-track thumbnail -> iTunes-cover upgrade — unmounted it and
dropped the view to the black scrim for a frame, which read as a blink.
Use the same two-slot 700ms opacity cross-fade the app-wide
BackgroundCover already uses; failed loads still advance the caller's
candidate list.
Stricter follow-up to the duration-vouch gate: with no artist metadata
there is nothing to verify a match against, so don't query the
providers at all, the panel goes straight to "No lyrics found." and
the fullscreen view centers the art instead of reserving a lyrics pane.
Provider-level duration gates stay as a second layer for direct callers.
Keys bumped again so persisted artist-less entries drop out.
Fresh indie releases often exist on no lyric DB at all (correctly
showing "No lyrics found." now that wrong-song matches are gated).
Give that dead end one door: a link that opens the default browser
with a quoted title + artist lyrics search.
@yuvrajangadsingh

Copy link
Copy Markdown
Contributor Author

heads up, the branch is rebuilt on your current main since the old merge went stale after the history rewrite, pr is mergeable again. also pushed a week of fixes from daily driving it: lyric timing for padded and sped up uploads, playlist pages were gluing ytm suggestions onto the real tracks, search history on the empty search page, and cleaner artist lines for tracks played from cards.

NUber-dev added a commit that referenced this pull request Jul 23, 2026
Add native macOS window chrome, Keychain-backed cookie encryption, platform-aware login and media integration, and a universal Intel/Apple Silicon release build. Document feature ideas gathered from the fork network.

Incorporates macOS work and ideas from PR #27 and PR #33.

Co-authored-by: Enoch Fabiyi <ayomikun204@gmail.com>

Co-authored-by: Yuvraj Angad Singh <yuvrajangad.s@gmail.com>
…band

the notch-band gradient starts from solid black over 128px so the
menu-bar strip melts into bright ambient backdrops instead of showing a
hard seam. every app scroller pins its x axis (overscroll-behavior-x +
overflow-x clip): trackpad diagonal scrolls were elastic-bouncing whole
panes sideways in wkwebview even with overflow hidden.
local builds ride a personal branch; accepting an upstream release would
replace them wholesale. the automatic startup check now runs only when
VITE_ENABLE_UPDATE_CHECK=1, which the release workflow sets, so official
builds keep updating while local builds never nag. the manual menu check
always works.
the manual song/video toggle took the first search result of the
opposite kind on faith, which played the 15:54 bollywood dilbar for a
2:49 remix. candidates now need a real title match (qualifier-aware, so
a remix is not 'exact' for the original), artist overlap when both sides
name artists, and a duration inside the plausible counterpart window; no
match shows 'no version found' instead of someone else's video. the
persisted pair cache is dropped once via a store version bump because
pairs cached by the old resolver would have kept winning over the gates.
macos showed two now-playing rows: a blank 'ytubic' entry from the
native mpnowplayinginfocenter path and the real track from webkit's
automatic session for the playing element. webkit publishes its session
regardless, so the webview side (navigator.mediaSession metadata with
artwork, position state, action handlers) is now the single owner and
the native bridge is no longer initialized - which also ends
double-fired transport presses. action handlers register individually
so an unsupported action on some webkit build cannot abort the rest.
…eams at the truth

carry-seek: switching song/video keeps the playhead (token-invalidated,
queue-identity-checked, companion gated on reaching the target frame).
doubled-duration streams: avfoundation reads yt-dlp's un-remuxed dash
m4a headers at 2x the real length; correctedDuration already displayed
the listed truth but playback ran past it into the phantom silent half.
ontimeupdate now advances at the corrected end, re-arming whenever the
playhead returns before it so repeat-one and replays of the same source
end correctly every pass, and the element rewinds first so a stopped
queue never resumes into silence.
when the companion tops out below the cap, rows above its real height
disappear instead of presenting choices that silently fail, and the
check marks what is actually playing rather than the unreachable cap.
the tops-out footer stays.
the fork inherited upstream's windows readme, so the repo read as a
windows project with a download-for-windows button. lead with what this
actually is: the macos port, what it adds on top, mac install with the
gatekeeper step, and upstream credited at the top and in credits.
someone landing here should know what this is and have it installed
before they hit anything technical. wordmark and nav up top, install
second with the gatekeeper step inline instead of buried in an faq,
troubleshooting folded into details blocks, prose instead of bullet
soup. dropped genius from the lyrics list, it has been dead upstream
for weeks.
@yuvrajangadsingh

Copy link
Copy Markdown
Contributor Author

closing this one. you shipped first-class macos support in 0.4.0, which covers the platform side this branch originally existed for, and the branch has drifted well past its original scope since (68 commits now, mostly player and lyrics work that isn't mac specific).

i'll keep the fork going for the extras. if you ever want any of the individual pieces upstream, say the word and i'll open focused prs instead of one big branch — the ones i think stand alone are the video mode with the quality picker, the lyrics record matching (picks the right record instead of the closest, auto-aligns padded intros), and the apple music style fullscreen.

thanks for taking the time to look through it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants