Caught on first emulator smoke: stream_info hardcoded
`player_from_clients(&[Android, Ios])` from U-3 era. Android first
trips YT's "Precondition check failed" because needs_po_token
doesn't flag Android — request fires unsigned and YT rejects it.
The fork's audit-fixed player_client_order is [Ios, Tv] without
botguard (HIGH-3 in the audit). Use rp.query().player(id) directly
so we inherit that order and pick up future tweaks automatically.
C-1: rust/strawcore/Cargo.toml now points at Sulkta-Coop/rustypipe
v0.11.5-sulkta.2 (kayos/m1-sig-port) instead of upstream 0.11.4.
Upstream's sig-regex hard-fails on current YT player c2f7551f;
the fork soft-fails, defaults to iOS-first, makes Deobfuscation
errors switchable through the client-fallback chain, and adds
observability via Reporter Level::WRN.
C-2: strawApp/build.gradle.kts restores the U-1 era Gradle Rust glue:
- cargoBuild → cross-compile strawcore for 4 Android ABIs
- cargoBuildHost → host-arch debug build for uniffi-bindgen
to read metadata from
- uniffiBindgen → generate Kotlin bindings from libstrawcore.so
Wired into the Android build via mergeXxxJniLibFolders +
compileXxxKotlin task dependencies.
Bumps + gitignores:
- .gitignore now excludes rust/target, strawApp/src/main/jniLibs,
and strawApp/src/main/java/uniffi (generated)
- jna 5.14.0 added as the JNI bridge runtime
Next: cargoBuildHost + uniffiBindgen verification in crafting-table,
then C-3 swaps SearchViewModel back to uniffi.strawcore.search().
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.
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.
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.
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