SlopTube

Media pipeline#

Playback is native end to end: InnerTube gives stream URLs, HttpSource downloads them, the NDK AMediaExtractor/AMediaCodec decode them in hardware, video frames land in an AImageReader sampled by GL as an external texture, and AAudio plays the sound. The code is in src/core/innertube/Streams.* (choosing streams) and src/app/media/ (playing them).

Getting streams#

Api::player() asks the clients of kPlayerOrder (ClientProfile.cpp) in turn:

  1. VISIONOS, the only client found whose adaptive streams download completely. It needs the visitorData of an earlier response. The app always loads a feed first, so it has one.
  2. ANDROID, whose progressive itag 18 (360p, muxed) is complete.

Every other client tried has been dropped:

Only formats with a direct url are used; ciphered formats are skipped. A broadcast in progress (videoDetails.isLive) gets the same kind of URLs from VISIONOS, for segments rather than files (see Live streams); ANDROID gives none for it.

Choosing streams (core/innertube/Streams.*)#

Playing (app/media/)#

Piece Role
HttpSource Ranged HTTP reads in 1 MB blocks with about 30 s of read-ahead (2–16 blocks); feeds the extractor through AMediaDataSource; measures bandwidth (shared across videos)
Player Closing a video hands its session to a reaper thread, which joins the decoder (codec, sources, audio: tens of milliseconds) away from the UI; the next video's decoder waits for it, since phones have few hardware decoders. Separate video and audio sources and decoders. Audio is the clock and video frames are released against it. Seeks apply to both. Each opened video is a Session (streams, state, decoder threads, its own AImageReader); a second, standby session can be preloaded
VideoSurface AImageReader → EGLImage → GL_TEXTURE_EXTERNAL_OES, drawn by ImGui like any image (zero copy). Letterboxes the frame, optionally zoomed and cropped to the box (texture coordinates are cropped, nothing is drawn outside the box)
LiveSource The segments of a live stream, fetched in order a few ahead of the one being demuxed (see Live streams)
Log.hpp The single logcat tag

Quality#

Player::qualities() lists what the video offers. Other calls:

A switch works like this:

  1. The new stream is opened on a helper thread at the current position.
  2. The video decoder is swapped while audio keeps playing, so there is no audio gap. The picture holds briefly until the next keyframe.
  3. A manual choice is remembered as a resolution for the following videos.
  4. If the switch fails, playback stays on the current quality.

The watch page's settings button opens the menu (App::drawPlayerMenu). Rows show "Auto (1080p60)" and then each quality.

Speed#

Pressing and holding on the video (watch page, fullscreen or Shorts) plays at 2x until the finger lifts, with an "x 2 ▸▸" pill at the top, as in the official app (App::holdForSpeed):

Measured on the Galaxy A14: an 8 s hold advanced playback 17 s over 9.5 s of wall time (7.6 s at 2x plus 1.9 s at 1x).

Fullscreen#

App::setFullscreen drives Platform::setFullscreen:

Zoom. The video starts fitted, with black bars at the sides as in the official app. Pinching zooms it live (App::drawZoomableVideo, from Touch::pinchScale). On release:

The phone ticks when the pinch reaches the zoom that snaps to fill, and clicks when the video settles fitted or filling, but not at a free zoom.

Leaving fullscreen resets the zoom. Zoom is always about the centre; a zoomed video cannot be panned.

main.cpp re-reads the window insets for a second after a rotation, because Android reports them late. The GL context and the playback survive rotation (configChanges in the manifest).

Shorts#

A short whose video is close to the page's shape (cropping at most 30 %, e.g. 9:16 on a 20:9 screen) fills the page, cropping its sides, as in the official app. Other shorts (16:9 videos in the feed) are letterboxed.

Swiping is made instant by fetching ahead (App::prefetchShorts, screens/ShortsPage.cpp):

A short reached without a standby (swiping back, or two swipes in quick succession) still starts from its prefetched player response, about a second after the swipe.

Seeking#

Three gestures seek, behaving as in the official app:

Seek undo (SeekUndo): once the handle has gone 25 dp from where playback was, bringing it back within 20 dp clicks and the pill reads "Release to cancel"; releasing in the next 2 s leaves playback where it was. The Shorts bar has it too; its seeks are live, so it seeks back.

Chapters come from the next response (see innertube.md). The bar is split at them with 2 dp gaps (3 dp while seeking), the time row ends with the current chapter's title, and the phone ticks each time a seek crosses into another chapter.

Live streams#

A live format has no contentLength but a targetDurationSec (Format::segmentSeconds, 5 s): its URL with &sq=<n> returns segment n, and without it the newest one. Every segment is a complete fragmented MP4 file (ftyp, moov, then emsg, moof, mdat) whose sample times continue the stream's timeline, and whose emsg text carries Sequence-Number: <n>. Measured:

Request Answer
No sq The edge segment, at once
sq behind the edge The segment, at once (the stream keeps maxDvrDurationSec, 12 h)
sq = edge + 1 Held until the segment exists (up to 5 s), then sent
sq further ahead 204 No Content after a few seconds

LiveSource reads the edge's number, starts 3 segments behind it (about 15 s, so the next one is always out before it is needed) and keeps 3 segments downloaded ahead. The player demuxes each one with its own AMediaExtractor over the bytes in memory (SegmentFile), and moves the same decoders on to the next segment when one is used up (Input::advance), so playback runs across segments without a gap. Audio and video start at the same segment. A quality switch opens the new video stream at the segment audio is playing, and the usual switch takes over from there. The broadcast is over when segments stop coming (10 misses, 2 s apart), and playback ends.

Rewinding. The server keeps far more than maxDvrDurationSec (12 h; segments 139 h old still downloaded), and the app offers those 12 h. Segment n starts at about offset + n * targetDurationSec (within half a second: audio segments alternate 4.992 and 5.016 s), where the offset comes from the segment audio is playing and its first sample time. The edge is the head learned by the last source started near it, moved on by one segment every 5 s. Player::liveWindow gives the playable range in the player's clock; a seek inside it starts new sources at the segment holding the target and discards samples before it (restartLive in the decoder loop), and a seek within three segments of the edge starts at the edge again.

The watch page's bar spans the window. "LIVE" with a red dot shows at the edge; behind it, the dot turns grey with how far behind ("LIVE -12:34"), and tapping it goes back to live. The seek pill shows the time behind live. Speed, 2x, precise seeking (there is no storyboard) and downloading are not offered.

Closing a stream does not wait for a held request: http::Request::cancelled makes libcurl give up within about a second.

Haptics#

Platform::haptic makes each vibration with the standard Android APIs:

Haptic How When
LongPress View.performHapticFeedback(LONG_PRESS) 2x starts
ContextClick View.performHapticFeedback(CONTEXT_CLICK) Double-tap like on a short
Click VibrationEffect.EFFECT_CLICK Zoom settles fitted or filling; seek undo arms
Tick VibrationEffect.EFFECT_TICK Pinch reaches the fill zoom; a seek crosses a chapter
Seek 25 ms one-shot at full amplitude Precise seeking opens; easy seek starts

The first two follow the phone's touch feedback setting; the others use the vibrator, which needs the VIBRATE permission (granted at install).

Speed#

The player menu (the gear) lists Quality and Playback speed: 0.25x to 2x, as in the official app, kept for the following videos until the app closes. Holding for 2x returns to it on release. TimeStretch handles every speed the same way.

Background and picture-in-picture#

These go through the Java bridge (see architecture.md):

Downloads and offline viewing (app/downloads/)#

"Download" on the watch page opens a sheet listing the qualities of selectStreams with their sizes. Downloads then saves the video under <data>/downloads/<video id>/:

File Content
manifest.json Item (title, channel, …), chosen video/audio formats (itags, sizes), caption tracks, complete
video, audio The streams; .part while incomplete
thumb, avatar Images shown in lists while offline
captions/<n> json3 cues of each track (up to 12, written and auto-generated)

Known limitations#

Edit this page on GitHub