Commit graph

21 commits

Author SHA1 Message Date
e7d45aa6b4 vc=22: inline→fullscreen position handoff + local playlists 2026-05-25 15:57:56 +00:00
599d299b2a vc=21: seamless background-audio handoff on 🎧 + HOME
vc=20 fixed channel videos but left two player rough edges that Cobb
called out on the phone:

  * Tapping 🎧 (background audio) restarted the stream from the
    beginning instead of picking up where the foreground player was.
  * Pressing HOME on the player auto-entered Picture-in-Picture; what
    Cobb wants is seamless background audio.

Both paths now share one handoff: capture exoPlayer.currentPosition,
stop the activity player, start PlaybackService with EXTRA_POSITION_MS,
and seekTo(position) on setMediaItem. Verified on emulator: 🎧 tap from
~34s in-track resumes service at position=34821ms.

HOME-on-player triggers the same path via StrawActivity.onUserLeaveHint
→ PlayerLeaveHandler.handler (a tiny registry the active PlayerScreen
registers in a DisposableEffect; cleared on dispose so the hook is a
no-op anywhere else in the app). The previous DisposableEffect that
called setAutoEnterEnabled(true) is gone; manual PiP via the ⊟ overlay
button stays — that one is still useful and user-triggered.

Also fixes a latent IllegalStateException that the new HOME path
exposed: PlaybackService and PlayerScreen both built MediaSession with
the default empty ID, which the system rejects when both live in the
same process. Service now sets .setId("straw-bg") so the two
sessions can coexist during the brief activity-vs-service overlap
during handoff.

Smoke (emulator 1440x3040):
  * Subscriptions feed renders (vc=20 fix carries over).
  * 🎧 from ~34s in NCS / Different Heaven → service playing
    position=34821ms, no session-ID crash.
  * HOME from PlayerScreen → focus moves to NexusLauncher,
    PlaybackService running with isForeground=true,
    pictureInPictureParams=null on the activity (no PiP).
2026-05-25 03:55:39 +00:00
709af57f42 v0.1.0-AF (vc=20): channel-videos fix for subscription feed
vc=19 shipped with empty subscription feeds because
strawcore-core's channel_info was parsing the wrong tab + the
wrong renderer type.

strawcore-core e6fbbb7 fixes both — second-browse to the Videos
tab + parse lockupViewModel. This bump pulls that in.
2026-05-24 20:07:04 -07:00
f70b8b71b9 v0.1.0-AE (vc=19): rust pipeline cutover
NCS Spektrem + Rick Astley both play through Rust → ExoPlayer h264
MediaCodec on android-emulator. 4s frame-diff verified, zero
PlaybackException. Phase 7 + Phase 8 of the NPE port arc done.
2026-05-24 18:45:35 -07:00
07e3163e62 v0.1.0-AD (vc=18): rollback to NPE-based playback
Path C (rustypipe iOS-client extractor) shipped on vc=16-17 returns
googlevideo URLs that the YT iOS-bound progressive-download path
deliberately caps at ~917 KiB end byte. Any seek past that returns 403,
making non-HLS videos unplayable. ExoPlayer's IosSafeHttpDataSource
chunking workaround in vc=17 doesn't help because the cap is on the
URL itself, not on the chunk size.

Rolling back to the vc=15 (0.1.0-AA) state — NewPipeExtractor — so
Cobb's phone auto-updates back to a working version while the
strawcore/rustypipe extractor strategy is re-thought (need a non-iOS
client that returns reliably-streamable URLs, OR a Lucy-side stitching
proxy that fetches iOS chunks server-side and re-serves DASH/HLS).

Branched from 5b36de888 (the vc=15 commit) — purely a versionCode +
versionName bump, no other changes.
2026-05-24 14:48:17 -07:00
5b36de8888 v0.1.0-AA (vc=15): inline player on VideoDetail + fullscreen pill
Tap the 16:9 thumbnail box on VideoDetail and the video plays right
there in the card — like YouTube. Uses its own ExoPlayer (released on
nav-back via DisposableEffect) with PlayerView's built-in controls
(play/pause/seek/duration bar).

Top-right ⛶ pill on the inline player jumps to the existing fullscreen
PlayerScreen which still has the full toolset (speed picker, audio-only,
share, PiP, background, SponsorBlock chip). Restarts from 0 on entry —
seek-position handoff between inline + fullscreen is a future refinement.

Inline player state (playing/not-playing) is keyed on streamUrl so
navigating to a different video resets it back to the thumbnail-with-
play-overlay default.
2026-05-24 11:17:36 -07:00
75329867e9 v0.1.0-Z (vc=14): VideoDetail to YT-standard order
Under the thumbnail: title → uploader → chips → buttons → description →
Recommended → More from <uploader>. The Z2/W2 inverted layout shipped in
Y put discovery cards before the title — bad UX, fixed back to standard.

Inline player coming next.
2026-05-24 11:12:20 -07:00
94ef84f1ac v0.1.0-Y (vc=13): VideoDetail reorder + home search pill + status-bar padding
VideoDetail card order (top → bottom under the player thumbnail):
  Recommended → More from <uploader> → Video details
NewPipeExtractor fan-out: streamInfo brings 'related', and a parallel
ChannelInfo+VIDEOS-tab fetch brings 'moreFromChannel' (filtered to drop
the current video). Same data shape as feed/search rows; reuses RelatedRow.

Home top bar gets a YouTube-style search pill in the title slot — tap
takes you to the search screen. The drawer 'Search' entry is gone (the
pill replaces it).

Section header below the top bar — 'Latest from your subs' / 'History' —
makes which view you're on obvious. Empty subs state is friendlier.

Status-bar padding (statusBarsPadding) added to VideoDetailScreen,
SearchScreen, ChannelScreen, SettingsScreen — fixes content rendering
under the Pixel camera cutout in edge-to-edge mode.
2026-05-24 11:02:39 -07:00
9ad3302f52 v0.1.0-X (vc=12): revert to NewPipeExtractor for working playback
Phase U (rustypipe Rust extractor) rolled back. Symptom: black screen
on play, root cause: rustypipe 0.11.4's JS deobfuscator can't parse
current YouTube player.js (YT changed the obfuscation pattern, no
upstream rustypipe release since June 2025). Switching clients
(Web → TV → Android/Ios) didn't help — the deobfuscator init fires
universally.

Kept in place for the future:
- rust/strawcore/ Cargo workspace + UniFFI scaffolding
- crafting-table runtime install (rustup + 4 Android targets +
  cargo-ndk + NDK r27c)
- The U-2..U-5 commits in history (re-runnable when rustypipe is
  fixed or we fork it).

Restored from commit 9550b207a (v0.1.0-T):
- NewPipe.init() in StrawApp.onCreate
- libs.newpipe.extractor + libs.squareup.okhttp deps
- NewPipeDownloader.kt + Thumbnails.kt
- ViewModels (Search/VideoDetail/Player/Channel/SubscriptionFeed) on
  NewPipeExtractor calls
- VideoDetailScreen Download dialog using NewPipe's StreamInfo

Future-direction memo: openclaw-workspace/memory/project_rustypipe_fork.md
— fork plan + revival path for the Rust extractor when we're ready
to maintain it.

Verified working in the Android emulator: dQw4w9WgXcQ plays, ExoPlayer
reports state=PLAYING(3), position advancing, video surface rendering.
2026-05-24 09:54:59 -07:00
5be7d4c276 v0.1.0-W2 (vc=11): fix playback — TV+Ios YT clients + visible play errors
Black-screen-on-play bug: rustypipe's default player() uses YT's Web
client, which serves stream URLs that are session/UA-locked. ExoPlayer
fetching with a different UA gets a silent 403 from googlevideo and
renders a black surface.

Fix: pin stream_info() to player_from_clients(id, [Tv, Ios]) — the
TVHTML5 + iOS Innertube clients serve direct-play URLs that work in
any HTTP player. Same trick NewPipe uses. No Apple/iOS code involved
— it's just the API client identifier rustypipe sends to YT.

Also added a Player.Listener in PlayerScreen that Toasts any
PlaybackException (codeName + message) so future stream failures don't
look like silent black screens. Logs to logcat 'StrawPlayer' too.
2026-05-24 09:33:34 -07:00
a13896f5e9 v0.1.0-W (vc=10): U-4 + U-5 — channels via rustypipe + rip NewPipeExtractor
channel_info(url) UniFFI suspend fn. ChannelViewModel +
SubscriptionFeedViewModel both swap. NewPipeExtractor (Java) is OUT —
zero org.schabi.newpipe classes in the APK now.

Cleanup:
- NewPipeDownloader.kt deleted (was the OkHttp adapter)
- Thumbnails.kt deleted (rustypipe returns full URLs)
- NewPipe.init() dropped from StrawApp.onCreate
- libs.newpipe.extractor removed from build.gradle.kts
- STRAW_USER_AGENT + strawHttpClient() now live in net/Http.kt
- RydClient + SponsorBlockClient + PlayerScreen + PlaybackService all
  read from net/Http.kt instead of the extractor package

rustypipe API quirks beat:
- channel_videos(id) is the right method (channel() doesn't exist)
- ChannelInfo struct = basic metadata; Channel<T> wrapper carries
  name/avatar/banner + .content is the paginator of videos
- description is String (not Option), subscriber_count is Option<u64>

End state: strawApp Kotlin is ~UI + thin glue to strawcore. The Rust
core handles search / streamInfo / channel / channel_videos via UniFFI
suspend fns. Tokio + reqwest + rustls + rquickjs all packed in
libstrawcore.so (~6MB per ABI). APK 40MB total.
2026-05-24 09:11:14 -07:00
7327de2843 v0.1.0-V (vc=9): U-3 — streamInfo via rustypipe drives VideoDetail+Player
stream_info(url) UniFFI suspend fn replaces NewPipeExtractor's
StreamInfo.getInfo() for both VideoDetailViewModel and PlayerViewModel.
One Rust round-trip drives the detail screen render AND the player's
resolve(). The VideoDetailUiState.info field cached on detail load is
reused by the Download dialog so we don't refetch.

Deferred to U-3.5:
- like_count (rustypipe's player() doesn't surface engagement data;
  a separate query is needed)
- related (player() doesn't include 'up next'; comes from a separate
  endpoint). Kotlin gets empty list for now — RelatedRow handles it.

Type quirks vs my initial guesses (caught by cargo check):
- details.duration is u32, not Option<u32>
- channel is split into channel_id + channel_name, not a struct
- like_count doesn't exist at this query depth
- VideoFormat::Webm (lowercase mb), VideoCodec::Avc1 (not H264)
- video_only is a separate vec (video_only_streams), not a bool flag
2026-05-24 08:52:43 -07:00
7ff5ac79e5 v0.1.0-U (vc=8): Phase U-1 + U-2 — Rust core + rustypipe search
NewPipeExtractor (Java) → strawcore (Rust) migration begins. Phase U:
- U-1: Rust toolchain + UniFFI smoke test
- U-2: rustypipe search via uniffi suspend fun, SearchViewModel swapped

What landed:
- rust/strawcore — UniFFI-exported Rust crate using proc-macros.
  Builds for arm64-v8a + armeabi-v7a + x86 + x86_64 via cargo-ndk.
  Tokio multi-thread runtime singleton drives rustypipe's async API.
- strawApp/build.gradle.kts — cargoBuildHost + cargoBuild + uniffiBindgen
  Gradle Exec tasks chained into the Android build. Generated Kotlin
  bindings land in src/main/java/uniffi/strawcore/ (gitignored).
- SearchViewModel.kt — calls uniffi.strawcore.search(query) directly.
  NewPipeExtractor still in deps for VideoDetail/Player/Channel paths;
  those move to Rust in U-3 / U-4.
- Build chain quirks beat:
  * cargo absolute path in Exec tasks (PATH wasn't propagating)
  * uniffi-bindgen needs UNSTRIPPED host .so — separate cargoBuildHost
    builds a debug-profile host lib to read metadata from
  * rustypipe rustls-tls-webpki-roots avoids the openssl-sys
    cross-compile tarpit
  * rquickjs-sys 'bindgen' feature opted in (no prebuilt Android
    bindings ship; crafting-table has libclang 14)
- crafting-table runtime install (until Dockerfile catches up):
  rustup + 4 Android targets + cargo-ndk + NDK r27c. Persists in
  /caches/cargo + /caches/android-sdk via the volume mount.

APK size: 22MB (U-1) → 37MB (U-2). libstrawcore.so 3-5MB per ABI carries
rustypipe + reqwest + tokio + rustls + rquickjs. NewPipeExtractor still
in for now (still drives detail + player + channel + feed), so the
Java half is doubled up. U-5 removes it.
2026-05-24 08:36:50 -07:00
9550b207ab v0.1.0-T (vc=7): bug fixes + Opus audit pass #2 + home redesign
User-visible:
- BUG: 'clicking a second item went back to the first video' — VideoDetailViewModel
  guard was short-circuiting on Activity-scoped ViewModel reuse. Now tracks
  loadedUrl and only skips when the requested URL matches.
- BUG: PiP / window mode now auto-enters on Home gesture (Android 12+ via
  setAutoEnterEnabled). Manual PiP button reports failure cause via Toast.
- HOME REDESIGN: replaced 3-tab bottom nav with hamburger ModalNavigationDrawer.
  Default view = sub feed. Drawer items = Subscriptions / History / Search /
  Settings. Top-left hamburger like normal Android apps.

Audit pass #2 (Opus max-effort) — CRIT + HIGH fixes shipped:
- CRIT-1: PlaybackService now calls startForeground() inside onStartCommand
  with a media-playback notification + channel. Pre-fix could throw
  ForegroundServiceDidNotStartInTimeException on Android 12+ and crash-kill.
- CRIT-2: AndroidManifest service exported=false. Previously any installed
  app could craft an Intent and drive playback from attacker URLs.
- HIGH-1: 🎧 background handoff stops the activity player before starting
  the service so we don't dual-host two ExoPlayers + MediaSessions.
- HIGH-2: onStartCommand returns START_NOT_STICKY and tears down on null
  intent; no more crash-restart-crash loop after OS kills.
- HIGH-3: stop service on STATE_ENDED / STATE_IDLE via Player.Listener.
  onTaskRemoved checks playbackState properly so we don't hold WAKE_LOCK
  forever after a video ends in background.
- HIGH-4: Downloader validates scheme=https + googlevideo/youtube host
  before handing the URL to DownloadManager.
- HIGH-5: filename sanitization extended to ASCII control chars, DEL,
  Unicode bidi-override block, leading-dot, trailing whitespace.
- HIGH-6: SubscriptionFeedViewModel cancels prior in-flight refresh,
  caps parallelism at 8 via Semaphore, applies 15s per-channel timeout.
- HIGH-7: sub feed error banner now shows above cached items when refresh
  fails (previously hidden, looked indistinguishable from success).
- HIGH-8: PlayerViewModel falls back to lowest-available stream when no
  stream is under the max-resolution ceiling (was: silent black screen).
- HIGH-9: network_security_config explicit cleartextTrafficPermitted='false'
  on the RYD domain-config block (doesn't inherit from base-config).
- MED-1: PlaybackService.onDestroy nulls field before releasing session to
  close a race with onGetSession during teardown.
- MED-6: Downloader catches enqueue exceptions, returns -1L, caller toasts
  'download refused (bad URL)' instead of crashing.

Deferred (audit said 'can wait'): MED-2..5, MED-7..11, HIGH-10 UX consistency.
2026-05-24 07:49:35 -07:00
Kayos
081f238355 Straw phases P/Q/R/S — bottom nav, sub feed, downloads, background audio
Phase P — bottom navigation:
- StrawHome restructured as a Scaffold with Material3 NavigationBar.
- Three tabs: Home (search + last 10 watches), Library (full watch
  history with count), Subs (channel chips + aggregated feed).

Phase Q — subscription feed:
- New SubscriptionFeedViewModel fans out per-channel ChannelInfo +
  ChannelTabs.VIDEOS fetches in parallel via async/awaitAll.
- Each channel contributes top 5; merged across all subs, capped at
  200, sorted by view count as a soft-recency proxy (extractor doesn't
  reliably surface upload timestamps).
- 10-minute cache TTL avoids hammering YT on tab re-entry.
- Subs tab renders the feed below the avatar row with a Refresh button.

Phase R — download:
- Download button on VideoDetail (next to Play / Share). Pops a tiny
  dialog: Audio (best audioStream) or Video (best videoStream/
  videoOnly fallback).
- Uses Android's DownloadManager — saves into app-private external
  files dir (Android/data/com.sulkta.straw.debug/files/Movies/<kind>/).
  Notification + progress for free. No WRITE_EXTERNAL_STORAGE needed.
- Filenames sanitized (no /:*?\"<>| chars), capped at 120 chars.

Phase S — background audio:
- New "Background" overlay button (🎧) on the player. Tap to pause the
  activity player and start PlaybackService with the audio URL.
- PlaybackService is a Media3 MediaSessionService with its own ExoPlayer
  configured with our custom DataSource.Factory (User-Agent set, cross-
  protocol redirects). Foreground service + media notification.
- Audio survives activity death — swipe the app out of recents, audio
  keeps playing. Stop via notification or open-the-app-and-tap-stop.
- onTaskRemoved keeps the service alive iff something is playing.

Versions shipped: P+Q as vc=4, R as vc=5, S as vc=6. Each landed in the
F-Droid repo for the day-by-day refresh path.

Day-N+ ideas: real MediaController unification (single Player for both
foreground + background paths), MergingMediaSource on the service side
for high-res YT videos, real upload-timestamp sort for feed once the
extractor exposes it consistently, queue/playlist.
2026-05-24 04:30:06 -07:00
Kayos
fa97b698fe Straw phase O: related videos + max-resolution picker (v0.1.0-O / vc=3)
VideoDetail screen:
- New "Related" section at the bottom — pulls
  StreamInfo.relatedItems, filters to StreamInfoItem, renders as
  inline thumbnail rows. Tap → push another VideoDetail. Up to 20
  items shown. Each row uses bestThumbnail() for hi-res.

Settings screen + PlayerViewModel:
- New "Playback" section with a Max-Resolution picker:
  Auto / 1080p / 720p / 480p / 360p / 144p. Persisted to
  SharedPreferences (KEY_MAX_RES) via SettingsStore.maxResolution
  StateFlow.
- PlayerViewModel.resolve filters videoStreams + videoOnlyStreams by
  the ceiling before picking the max-bitrate one. Auto (Int.MAX_VALUE)
  is unchanged behavior. Choosing 720p caps the renderer so 1080p/4K
  streams are skipped — saves bandwidth on mobile + helps low-end
  decoders.

Phase P next ideas: bottom navigation tabs (Home / Subs feed /
Library), Download (audio + video), the MediaSessionService refactor
for true background audio after activity death.
2026-05-24 03:44:54 -07:00
Kayos
253c5e268b Straw phase N: share + playback speed + audio-only toggle (v0.1.0-N / vc=2)
Player overlay (top-right) now hosts four buttons in a row:
- Speed: 1× by default; tap opens a dialog of 0.25× / 0.5× / 0.75× / 1× /
  1.25× / 1.5× / 1.75× / 2×. Applies via exoPlayer.playbackParameters.
- Audio-only toggle (📻/📺): toggles Player.TRACK_TYPE_VIDEO via
  TrackSelectionParameters. Saves bandwidth + battery for screen-off
  listening. Toast confirms state.
- Share (↗): Intent.ACTION_SEND with text/plain containing the YouTube
  URL + the video title as EXTRA_SUBJECT. Hands off to Android share
  sheet.
- PiP (⊟): same as M-1 but now part of the row instead of its own
  floating square.

VideoDetail screen: Play button now lives in a Row with an OutlinedButton
"Share" that fires the same ACTION_SEND chooser. Same UX surface for
users who land on detail without going to player.

Version: STRAW_VERSION_CODE 1 → 2, STRAW_VERSION_NAME "0.1.0-day1" →
"0.1.0-N" so the F-Droid client sees this as an upgrade.

Phase O next (per Cobb's "where are all the features"): quality picker,
related videos on detail, download (audio + video).
2026-05-23 21:20:15 -07:00
Kayos
ff4dc6f121 Sulkta day-1: straw — KMP/Compose YouTube client fork
Initial Sulkta fork of NewPipe with a new :strawApp module that ships a
clean Compose-based Android APK at applicationId com.sulkta.straw.

What's here:
- :strawApp — thin Android application shell, MaterialTheme + StrawHome
  Composable. Lives alongside legacy :app so we don't break upstream.
- buildSrc — STRAW_APPLICATION_ID/VERSION constants alongside the existing
  NEWPIPE_APPLICATION_ID_OLD/NEW.
- docs/sulkta — RECON.md (NewPipe codebase breakdown) + DECISIONS.md
  (stack + scope decisions).

NewPipe's :shared KMP scaffold is at 892 LOC and renders nothing; this
fork picks up there and races ahead. Day-1 ships a hello APK; day-2 wires
NewPipeExtractor + Media3 player + SponsorBlock + Return YouTube Dislike.

GPL-3.0-or-later per upstream.
2026-05-23 17:37:55 -07:00
tobigr
d3d6b0283f Merge branch 'master' into dev 2026-05-23 20:18:19 +02:00
Aayush Gupta
d1bc8c23cf Better share version information between modules
Move important version properties to buildSrc directory to access between modules
as needed.

Also add a simple task to generate a simple BuildConfig class to access version name.
This is better than adding dependency on a third-party library/plugin.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-05-20 18:27:08 +08:00
Aayush Gupta
b06b7c35ca Relocate toml lint task to buildSrc and extend against default task
Fixes build errors after Gradle 9.x upgrade

Ref: https://docs.gradle.org/current/userguide/implementing_custom_tasks.html

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2025-11-21 20:08:26 +08:00