vc=84 — rename launcher to 'Straw' + move stream picker into Rust
Two changes: 1. Launcher name is now just 'Straw', not 'Straw debug' — past the debug-branding phase. Kept the .debug applicationId suffix (package stays com.sulkta.straw.debug) on purpose so fdroid updates install in place and the in-app auto-updater keeps working; dropping the suffix would change the package id and force a reinstall that wipes everyone's subs/history. That's a separate, deliberate release-track cutover. 2. Stream-selection logic moved out of Kotlin (resolveStreamPlayback) into the Rust strawcore wrapper as resolve_playback(StreamInfo, max_height) -> ResolvedStreams. The app keeps a thin shim that supplies the resolution cap (Settings.maxResolution) and attaches SponsorBlock segments. Byte-for-byte behavior parity with the old Kotlin picker: highest-bitrate stream at/under the cap, lowest-height fallback when nothing fits, first-element-wins on ties (matching Kotlin maxByOrNull/minByOrNull, not Rust's last-on-ties max_by_key), and isNotBlank() handling for the DASH/HLS URLs. First step of moving all backend logic to Rust; the picking lives at the FFI boundary because it depends on an app setting, keeping strawcore-core a pure extractor. Wrapper cargo check + clippy clean (no new warnings); FFI surface adds ResolvedStreams + resolvePlayback, bindings regen at build.
This commit is contained in:
parent
ea82ba765a
commit
addd074f61
5 changed files with 127 additions and 20 deletions
|
|
@ -21,7 +21,7 @@ mod stream;
|
|||
pub use channel::ChannelInfo;
|
||||
pub use error::StrawcoreError;
|
||||
pub use search::{Page, SearchItem};
|
||||
pub use stream::{AudioStreamItem, StreamInfo, VideoStreamItem};
|
||||
pub use stream::{AudioStreamItem, ResolvedStreams, StreamInfo, VideoStreamItem};
|
||||
|
||||
/// Initialize Android logging + the strawcore-core HTTP downloader.
|
||||
/// Kotlin calls this from StrawApp.onCreate(). Idempotent.
|
||||
|
|
|
|||
|
|
@ -56,6 +56,22 @@ pub struct AudioStreamItem {
|
|||
pub mime_type: String,
|
||||
}
|
||||
|
||||
/// The player-facing URLs picked from a [`StreamInfo`]'s stream lists under
|
||||
/// a resolution ceiling. This is the stream-selection policy that used to
|
||||
/// live in the Kotlin `resolveStreamPlayback`; the app now keeps only a thin
|
||||
/// shim that supplies the ceiling (its `Settings.maxResolution`) and attaches
|
||||
/// SponsorBlock segments on top. SponsorBlock is deliberately NOT here — it's
|
||||
/// a separate concern the app layers over this.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct ResolvedStreams {
|
||||
pub title: String,
|
||||
pub video_url: Option<String>,
|
||||
pub audio_url: Option<String>,
|
||||
pub combined_url: Option<String>,
|
||||
pub dash_mpd_url: Option<String>,
|
||||
pub hls_url: Option<String>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn stream_info(input: String) -> Result<StreamInfo, StrawcoreError> {
|
||||
log::info!("strawcore::stream_info input_len={}", input.len());
|
||||
|
|
@ -170,3 +186,74 @@ fn audio_to_dto(a: strawcore_core::stream::AudioStream) -> AudioStreamItem {
|
|||
mime_type: a.format.mime().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the player URLs from an already-fetched [`StreamInfo`]. Pure +
|
||||
/// synchronous (no network). `max_height` is the app's
|
||||
/// `Settings.maxResolution` ceiling — `Int.MAX_VALUE` means "no cap". This
|
||||
/// is a byte-for-byte port of the prior Kotlin `resolveStreamPlayback`:
|
||||
/// the app keeps a thin shim that passes the ceiling and tacks on
|
||||
/// SponsorBlock segments.
|
||||
#[uniffi::export]
|
||||
pub fn resolve_playback(info: StreamInfo, max_height: i32) -> ResolvedStreams {
|
||||
// Borrow the stream lists for selection before moving the owned String
|
||||
// fields into the result struct (disjoint fields, so this is fine).
|
||||
let video_url = pick_video(&info.video_only, max_height);
|
||||
let audio_url = pick_audio(&info.audio_only);
|
||||
let combined_url = pick_video(&info.combined, max_height);
|
||||
ResolvedStreams {
|
||||
title: info.title,
|
||||
video_url,
|
||||
audio_url,
|
||||
combined_url,
|
||||
// Kotlin used `takeIf { it.isNotBlank() }` — empty OR all-whitespace
|
||||
// collapses to None.
|
||||
dash_mpd_url: info.dash_mpd_url.filter(|s| !s.trim().is_empty()),
|
||||
hls_url: info.hls_url.filter(|s| !s.trim().is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Highest-bitrate video stream at or below `max_height`; if none fit, the
|
||||
/// lowest-height stream (so a too-high-only set still plays without blowing
|
||||
/// a data plan). The FIRST element wins ties — matching Kotlin's
|
||||
/// `maxByOrNull`/`minByOrNull`, which keep the first max/min, unlike Rust's
|
||||
/// `Iterator::max_by_key` (last on ties). Preserving that keeps the picked
|
||||
/// URL identical to the previous Kotlin picker on same-bitrate/-height ties.
|
||||
fn pick_video(streams: &[VideoStreamItem], max_height: i32) -> Option<String> {
|
||||
if streams.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut best_capped: Option<&VideoStreamItem> = None;
|
||||
for s in streams.iter().filter(|s| s.height <= max_height) {
|
||||
match best_capped {
|
||||
// Keep the existing pick unless this one is strictly higher
|
||||
// bitrate — first-on-ties, matching `maxByOrNull`.
|
||||
Some(b) if b.bitrate >= s.bitrate => {}
|
||||
_ => best_capped = Some(s),
|
||||
}
|
||||
}
|
||||
if let Some(s) = best_capped {
|
||||
return Some(s.url.clone());
|
||||
}
|
||||
let mut lowest: Option<&VideoStreamItem> = None;
|
||||
for s in streams {
|
||||
match lowest {
|
||||
// First-on-ties, matching `minByOrNull`.
|
||||
Some(b) if b.height <= s.height => {}
|
||||
_ => lowest = Some(s),
|
||||
}
|
||||
}
|
||||
lowest.map(|s| s.url.clone())
|
||||
}
|
||||
|
||||
/// Highest-bitrate audio stream, first-on-ties (matching Kotlin
|
||||
/// `maxByOrNull { it.bitrate }`).
|
||||
fn pick_audio(streams: &[AudioStreamItem]) -> Option<String> {
|
||||
let mut best: Option<&AudioStreamItem> = None;
|
||||
for s in streams {
|
||||
match best {
|
||||
Some(b) if b.bitrate >= s.bitrate => {}
|
||||
_ => best = Some(s),
|
||||
}
|
||||
}
|
||||
best.map(|s| s.url.clone())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue