# YouTube notes What the YouTube site files do, and what has been learned about YouTube's pages, data and player while building StreamXR (October 2026, English YouTube, desktop Chrome). Read this before touching `src/sites/youtube/`; YouTube changes often, so re-check a fact live when something stops working. How the code is structured is in [ARCHITECTURE.md](../ARCHITECTURE.md). ## The site modules (`src/sites/youtube/page/`) The `youtube-*.js` files are the **only place** that reads, clicks or requests anything from YouTube. When YouTube's markup or data changes, fix it there. They expose everything YouTube offers, whether the VR interface uses it yet or not (the unused parts are the backlogs in [features/](features/)): - `youtube-data.js` (`ns.youtubeData`): helpers, `watchData()` / `playerResponse()`, `innertube()` (POST to YouTube's own `/youtubei/v1/`), `pager()`, `cached()`, and the item readers for the `compactVideoRenderer` / `videoRenderer` and `lockupViewModel` shapes. - `youtube-player.js` (`ns.youtube`): everything on the `#movie_player` API. - `youtube-page.js` (`ns.youtubePage`): Up Next (merged data + DOM scraping, load more, playlists), the controls panel's details, Subscribe / Like / Dislike, in-page navigation. In-page navigation is a `yt-navigate` event with a watch endpoint, sent from `ytd-page-manager`: sent from `ytd-app` itself, YouTube ignored it on a channel page (it worked on the watch page and Home), checked 2026-10-17. - `youtube-content.js` (`ns.youtubeContent`): metadata, description, owner, chapters, heatmap, end screen, cards, playlist, comments / replies, transcript, search. - `youtube-channel.js` (`ns.youtubeChannel`): any channel by id (`currentChannelId()` for the playing video's), via the browse API so the video keeps playing: header, About, Home shelves, Videos / Shorts / Live (latest / popular / oldest), Playlists, Shows, Posts, Store, channel search. - `youtube-home.js` (`ns.youtubeHome`): the Home feed with its topic chips ("Immersive videos" = 360/VR content), Explore feeds (Live, News, Gaming, Sports, Music, Podcasts, Courses, Hype, Memberships), signed-in feeds (unverified), the left menu (`guide()`), your own channel's id (`myChannelId()`: the menu's "Your channel" entry, else the account menu; verified signed in, 0.74.0) and search `suggestions()`. - `youtube-subscriptions.js` (`ns.youtubeSubscriptions`): the Subscriptions feed, the Manage list (`channels()`), Subscribe / Unsubscribe any channel, the notification bell. Verified signed in: `checkSignIn()`, `feed()` (first page = 100 videos), `channels()` (in the Manage list `subscribed` is always true; one channel had only a stub `subscriptionButton` and no bell), subscribe / unsubscribe / `setNotifications` (they report ok only when YouTube's answer shows the new state). Filter chips: none on desktop for the tested account (the documented ones seem app-only). **No community posts** in the desktop feed (signed in, 2026-10-17: no post data in `FEsubscriptions` at all). The Manage page (`FEchannels`, seen in the signed-in page's `ytInitialData`, 2026-10-17): the list is split into **titled shelves** (`shelfRenderer` inside the item section: "Purchased" for channels you're a member of, then "Subscribed"), and its **sort dropdown** (Most relevant, New activity, A-Z) is a set of `listItemViewModel`s with `entityKey` `channel_list_page_sort_chip_entity:`, each tap updating a `chipEntity` (the dropdown's label) and sending a browse `continuationCommand` that reloads the list in that order (`sortsOf`, `channelsOn`). - `youtube-chat.js` (`ns.youtubeChat`): a live stream's chat ("Live chat" below): `info()` (its views), `open(token)` (a view's chat page), `poll(next)`, and the chat items as plain messages and changes (add, remove, banner). The provider (`provider/`) is the only code that calls these modules on the shell's behalf. The maintainer keeps private, live-probed inventories of what each page offers (Watch, Channel, Home, Subscriptions); what matters from them is in this file and the backlogs in `docs/features/`. ## Data and requests - Newer items show short text ("26M", "1y ago", "8mo ago"); the accessibility label holds the long form. `longAgo()` expands short dates. - Signed-in requests need `Authorization: SAPISIDHASH _ https://www.youtube.com")>`; `innertube()` sends it (and retries without it on 401/403). Every response's `responseContext` has a `logged_in` flag; a signed-out Subscribe still answers 200 but changes nothing. - **Which identity:** an account can have several YouTube identities (default + brand channels). Without one, YouTube answers as the account's DEFAULT identity, not the channel the user browses as (once, the Subscriptions feed showed another identity's channels, and test subscriptions changed that identity). `innertube()` sends `X-Goog-PageId` + `context.user.onBehalfOfUser` (ytcfg DELEGATED_SESSION_ID). Check in the page console: `await __streamxr.youtubeSubscriptions.checkAccount()` -> `sameAccount` must be true. - Channel tabs (browse API): a tab's first page keeps its "load more" token at the end of its grid / section; later and re-sorted pages arrive in `onResponseReceivedActions`. Sorting is a chip token (Videos / Shorts / Live) or the sort menu's params (Playlists). A signed-in first page can carry other `onResponseReceived*` actions with no items, so `pageRoot` reads only append / reload continuation lists (Live / Playlists / Shows looked empty before that; the cause is a theory, the fix works). - A newer "load more" shape, `continuationItemViewModel` (token in continuationCommand.innertubeCommand.continuationCommand), is read by `continuationOf` and `nextPageToken` (playlist pages use it). - Ad filtering must match a capital "Ad" (`AD_KEY_RE`): a loose `/[aA]dRenderer$/` also matched "backstagePostThreadRenderer" and silently dropped every channel post. - Text matching in `youtube-page.js` assumes **English** YouTube. ## Subscribe and Like - The watch page's subscribe box holds both buttons ("Subscribe" and "Subscribed"), so its text always matches /subscribed/: `isSubscribed` reads the visible button, else the watch data's subscribeButtonRenderer.subscribed. - The page's own button doesn't update after an API change, so `youtubePage.noteSubscription(channelId, on)` remembers confirmed changes until the next video; the controls panel and the Channel panel tell each other (`noteSubscribed`). - The Channel header's own `subscribed` takes the FIRST subscriptionStateEntity in the reply (signed in, a Home tab may carry others); it only matters when the watch page gives no answer. - The like count falls back to the button's aria-label. ## Video formats The formats are in `src/streamxr/shared/formats.js`; YouTube's detection is the provider's `detectFormat`. - **Quality names** (0.67.1): the player's `getAvailableQualityLevels()` gives only ids (`hd1080` ...), with no frame rate, so a 60 fps video showed "1080p". The names now come from `getAvailableQualityData()` (`qualityLabel`, e.g. "1080p60", or "2160s60" for a 360° / VR180 video: "s" for spherical) where the player has it (`youtube-player.js` `quality().labels`); with Premium it also lists "1080p Premium", kept apart as its own level (0.91.0). Seen signed in with Premium (2026-10-17): the entry has `formatId` "721", `quality` "hd1080", `paygatedQualityDetails.paygatedIndicatorText` "Enhanced bitrate", `isPlayable` true, beside the plain 1080p (`formatId` "399"); it's chosen with `setPlaybackQualityRange(q, q, formatId)` and `getPlaybackQualityLabel()` names the one playing. **Whether you have Premium:** the top bar's logo, `ytInitialData.topbar.desktopTopbarRenderer.logo.topbarLogoRenderer.iconImage.iconType` is `YOUTUBE_PREMIUM_LOGO` (`youtube-player.js` `hasPremium`). **Jump ahead:** the page has no data for it, and the player's `prefetchJumpAhead()` returns undefined and moves nothing. YouTube shows a **Jump ahead pill** in the player's "timely actions" overlay at the moments it applies (`.ytp-timely-actions-overlay ytw-timely-action-view-model button`, its text "Jump ahead"), and a script's `click()` on it jumps (17 s -> 124 s, 2026-10-17), so `youtube-player.js` `jumpAhead()` clicks it and `jumpAheadOffered()` tells whether it's up. A script-sent Ctrl + → keydown on `#movie_player` jumped from the console but did nothing from VR. - Desktop YouTube sends **one eye only** for most 3D videos (checked 2026-10-17 with frames from the page's `