Commit graph

36 commits

Author SHA1 Message Date
KhooLy
9f5075e86f Capture Android telemetry context atomically
Some checks failed
Apple / iOS simulator (push) Has been cancelled
Apple / tvOS simulator (push) Has been cancelled
Platform Verification / Android shared modules (push) Has been cancelled
2026-08-01 21:22:41 +03:00
KhooLy
0e338c8310 Align telemetry generation with playback sessions 2026-08-01 21:09:47 +03:00
KhooLy
13a6191ba7 fix: advance Android telemetry attempts 2026-08-01 19:26:19 +03:00
KhooLy
e539cfc024 fix: reset Android torrent telemetry sessions 2026-08-01 19:13:32 +03:00
KhooLy
211dc44b37 chore: consolidate app updates 2026-08-01 18:53:39 +03:00
KhooLy
bc9d97ed8a Deactivate torrents when Android playback stops
Some checks are pending
Apple / iOS simulator (push) Waiting to run
Apple / tvOS simulator (push) Waiting to run
Platform Verification / Android shared modules (push) Waiting to run
2026-08-01 16:24:18 +03:00
KhooLy
fd0d3775a8 Pass playback duration to torrent scheduler 2026-08-01 16:05:59 +03:00
KhooLy
a6f5d2f075 Harden Android torrent lifecycle 2026-08-01 15:08:50 +03:00
KhooLy
0ec2fe01d7 feat: rework player settings and source sidebars
Reworks the player's settings sidebar and input controls, adjusts the
sidebar shell layout, and wires the updated sidebars into the playback
surface and screen content.
2026-07-28 00:26:12 +03:00
KhooLy
e36a43bae5 Move shared app policies into fluxa core 2026-07-22 16:56:20 +03:00
KhooLy
d5f3819ebe Fix shared UI boundary violations from the CMP player overlay migration
checkSharedUiBoundary was failing on master because the July 17 move of
player overlays into shared/commonMain kept imports of data/player domain
types (Meta, Stream, Video, IntroTimestamps, UserProfile, Chapter,
MediaTrack, TorrentStreamStatus, TrailerCue). Relocate the plain value
types into the shared-safe com.fluxa.app.shared.feature.player package,
and replace the remaining domain-object usages with small UI models
(StreamSourceUiModel, SkipSegmentUiModel, NextEpisodePreviewUiModel)
mapped at the Android call sites. Also drops the dead, UserProfile-only
QuickSettingsSidebar/TrackSidebar composables that had no callers.
2026-07-20 15:37:30 +03:00
KhooLy
102d6755f8 Add headers/quality/size/provider/seeders/peers to Stream model
fluxa_core already emitted these for plugin-sourced streams, but Gson
silently dropped them since Stream had no matching properties. Renamed
the existing getHeaders() to resolveHeaders() to avoid a JVM signature
clash with the new headers property, and merged it into the raw
behaviorHints-derived headers.
2026-07-20 02:42:52 +03:00
KhooLy
f4ea52a890 Fetch MKV attachments in a single streamed read instead of two requests
fetchAttachmentsViaSeekHead previously issued a 16-byte header range
request, parsed the EBML element length, then issued a second range
request for the full attachments blob. Now it opens one range request
up to MAX_ATTACHMENTS_BYTES and streams the header plus payload off
the same connection, saving a round trip per playback start.
2026-07-19 17:12:41 +03:00
KhooLy
ad6d158f43 Route player HTTP traffic through Cronet for QUIC/connection migration
Adds Cronet (org.chromium.net:cronet-embedded + Google's cronet-okhttp
transport adapter) as an OkHttp network interceptor on the player's two
HTTP clients (cache pre-warm + main ExoPlayer data source). All existing
interceptor logic — header injection, Dolby Vision manifest rewriting,
localhost proxy timeouts — is untouched since Cronet only replaces the
final transport, not the OkHttp API surface.

This mainly benefits direct-URL/debrid streams (real TLS connections),
which can now survive network handoffs (wifi<->cellular) and lossy
connections without a full reconnect/rebuffer. Torrent-proxied streams
stay on loopback where this has no effect either way.

Falls back to plain OkHttp transparently if Cronet engine creation
fails for any reason (unsupported ABI, native lib issue, etc.) so
there's no regression risk.
2026-07-19 17:09:52 +03:00
KhooLy
0b953b6247 Follow SeekHead to locate MKV chapters past the first Cluster
MkvChapterFetcher's fixed prefix scan missed Chapters on releases where
it sits past the first Cluster (e.g. large embedded attachments pushing
it ~10MB in on one observed torrent release). Wire up the new
parseMkvChaptersAtOffsetNative bridge and the seekOffset the Rust
parser now reports: when Chapters isn't in the initial head fetch,
follow the SeekHead-derived offset with one small targeted range fetch
instead. Shrink the initial fetch from a 4MB prefix guess down to 64KB
now that it only needs to cover the (always compact, near-the-front)
SeekHead rather than hoping Chapters itself falls within it.

Add unitTests.isReturnDefaultValues so android.util.Log calls in
Robolectric-less JVM unit tests don't throw, and a regression test
covering the SeekHead follow-up path end to end.
2026-07-19 03:02:23 +03:00
KhooLy
a858cadfd6 Migrate Android trailer resolution to fluxa-core and fix hero playback UI
- Route YouTube trailer resolution through the shared headless engine
  (headless_engine/trailer.rs) instead of a hand-rolled Innertube client
  in TrailerResolver, mirroring what fluxa-desktop already does. Wire
  the two HTTP effect kinds (fetchYoutubeTrailerWatchConfig/Player) as a
  generic passthrough in FluxaAndroidHeadlessEnvironment.
- Wire HeroTrailerVideoSurface's ExoPlayer through
  TrailerResolver.mediaDataSourceFactory() so trailer playback actually
  gets the browser UA + Range header YouTube's CDN requires — this was
  the root cause of the black-screen hero trailer.
- Fix AndroidCatalogHomeDataSource pinning the active billboard movie to
  index 0 of heroItems on every index change, which desynced the pager's
  page index from item identity and made forward swipes visually snap
  back to the previous item.
- Hide synopsis/metadata/play button and shrink the logo while a hero
  trailer plays, soften the bottom scrim so it doesn't black out most of
  the video, and add trailer subtitles (selection/normalize/parse) via
  the same fluxa-core functions fluxa-desktop uses, rendered as a
  position-synced overlay above the shrunk logo.
2026-07-19 02:30:59 +03:00
KhooLy
efdabb628b Fix MKV chapter fetch header/range handling and add regression test
MkvChapterFetcher sent bare stream headers instead of routing through
StreamRequestPolicy (default User-Agent, referer), which many origins
require to respond to a Range request at all — unlike mpv, which always
gets proper headers. Also bail out instead of buffering an unbounded
body when a server ignores the Range header and returns 200 with the
full file.

Add MkvChapterFetcherTest using MockWebServer, exercising the real path
end-to-end (headers, 206 handling, JNI EBML parsing) on the JVM without
an emulator.
2026-07-19 02:30:16 +03:00
KhooLy
f11e9cee12 Stop passing raw Meta into PlayerSettingsPanel and EpisodeSidebar
Both only ever read id/type/seasonsCount from the 39-field Meta model,
in violation of the leaf-UI-gets-narrow-models rule in CLAUDE.md.
Adds seasonsCount to the existing PlayerContentUiModel (already
computed once via meta.toPlayerContentUiModel() at the top of
PlayerScreenContent) and threads that through instead.

The other Player files that take a Meta parameter (PlayerLoadingEffects,
PlayerTrackMemoryEffects, PlayerPipAndEpisodes, etc.) are LaunchedEffect
wrappers that hand Meta straight to ViewModel/repository calls for
persistence and scrobbling — that's the "effects/coordinators" layer
CLAUDE.md explicitly allows to hold domain objects, not a violation.
2026-07-18 18:58:38 +03:00
KhooLy
ad06eb94f5 Add subtitle format labels and container track IDs; rework inline delay controls
Removes the standalone inline audio/subtitle delay adjust pill and
its state, and adds MediaTrack.subtitleFormatLabel/containerTrackId
so subtitle tracks can be labeled by codec (SRT/ASS/VTT/TTML/PGS/DVB/
VOBSUB).
2026-07-18 15:01:46 +03:00
KhooLy
5b153bfd3b Declutter player settings sheets
The sheets opened with an all-caps title, an instruction sentence, a
panel spanning almost half the screen, and two-line rows where every
speed carried a redundant Slower/Faster label. Titles are now title
case (Audio, Subtitles, Playback speed), the instruction line is gone,
the landscape sheet narrows from 46% to 38% width capped at 400dp,
rows tighten to 46dp with single-line speed entries (only 1x keeps a
Normal label), and mono/stereo channel counts render as words instead
of 1.0/2.0.
2026-07-17 17:07:59 +03:00
KhooLy
e8dced3b20 Move player overlays, sidebars, and gesture input into shared CMP
Relocates skip-segment/transient overlays, the sidebar shell + track
list primitives, mark-segment/settings/track sidebars, and the
playerInputControls gesture modifier from app-module Android files
into shared/commonMain. Promotes coil3/coil3-compose to commonMain
(genuinely multiplatform; only coil3-network-okhttp stays
Android-only) so the overlay cards using AsyncImage can move too, and
promotes TorrentStreamStatus to player/commonMain as a plain data
holder.

Where a composable used to reach into UserProfile's JNI-backed
`safeXxx` properties (holdSpeed, holdToSpeedEnabled, language) or an
Android-only Locale API, it now takes the resolved primitive as a
parameter instead, with the app-side caller supplying it - keeping
the commonMain/androidMain boundary honest rather than papering over
it.

EpisodeSidebar (Hilt ViewModel-bound) and TrackSelectionState (direct
FluxaCoreNative JNI calls) stay Android-only, along with the actual
video surface, playback effects, and setup effects - those are the
next sub-steps in the player-extraction migration.
2026-07-17 00:06:49 +03:00
KhooLy
fc6bc620b3 Move data/player/domain logic into commonMain KMP source sets
Sweeps addon repositories, watchlist/profile storage, Trakt/Nuvio
sync, discovery models, and player policy classes out of Android-only
packages into data/commonMain and player/commonMain, with thin
androidMain wrappers where platform APIs are still required.
2026-07-16 23:00:28 +03:00
KhooLy
bd780396d1 Complete shared KMP and CMP migration 2026-07-16 15:50:35 +03:00
KhooLy
658b3ce3e3 KMP/CMP migration: purge legacy Android screens, route TV through shared FluxaApp
Continue the shared CMP/KMP migration: remove the legacy app/ui/catalog
Screen.kt files now superseded by shared/commonMain equivalents (Detail,
Search, Settings, Login, Profile, Watchlist, AddonStore, and the full
legacy Tv* screen set), and thread a DeviceType through FluxaApp/
FluxaAppHost so Android TV routes Home/Search/Discover/Calendar/Library
through shared destinations instead of falling into the removed legacy
composables (which previously hit an unreachable error() case).

Adds TvCatalogHomeScreen wiring plus new TvSearchScreen/TvDiscoverScreen/
TvCalendarScreen/TvLibraryScreen as thin TV-safe wrappers around the
existing shared screens. Removes the orphaned shared player/PlayerScreen.kt,
which had no call sites since the player stays platform-native by design.
2026-07-16 01:17:27 +03:00
KhooLy
20476761a9 Migrate shared UI and Apple playback to KMP 2026-07-14 18:14:18 +03:00
KhooLy
cda0c17b0a Replace TorrServer namings with Native torrent engine 2026-07-14 16:57:20 +03:00
KhooLy
35c0c405ac Align mobile trailer and account sync flows 2026-07-12 02:47:24 +03:00
KhooLy
bbd92ca1aa Don't start the torrent watcher when the Rust engine fails to start
startTorrentServer's result was logged as success and the watcher was
started unconditionally, even when the Rust call failed to report
readiness (blank result). Gate both on the actual running state.
2026-07-10 13:06:57 +03:00
KhooLy
b5120f6a27 Fix blank video on some devices caused by uncleared libass overlay surface
The always-mounted NativeLibassSubtitleOverlay SurfaceView sits above the
video SurfaceView (setZOrderMediaOverlay). Its canvas was only ever locked
and cleared when an ASS renderer was active, so with no embedded ASS track
selected the overlay buffer kept whatever garbage content it started with.
On some devices (reported on Redmi/MIUI) that composites as opaque black,
hiding the video behind fully-drawn player controls.
2026-07-10 13:05:56 +03:00
KhooLy
b3e893b63f Gate torrent stream start behind Wi-Fi-only setting
Adds a real network-transport check (ConnectivityManager) before starting
a torrent stream when Wi-Fi-only is enabled in Settings, refusing to start
on cellular instead of silently ignoring the toggle.

Note: this file already carried unrelated uncommitted engine-resiliency
work (ensureEngineReady/health-check/restart-on-failure) from before this
change — that part isn't mine, it's included here only because it was
already sitting in the working tree and touches the same file.
2026-07-10 02:01:55 +03:00
KhooLy
3bbbcb206c Delegate TrailerResolver to the shared fluxa-streaming-engine resolver
The Kotlin innertube client had drifted onto deprecated client configs
(ANDROID_TESTSUITE, TVHTML5_SIMPLY_EMBEDDED_PLAYER) that YouTube no
longer accepts. Route through FluxaStreamingNative's new
resolveYoutubeTrailerJson instead, so there's one maintained resolver
shared with the desktop app rather than two independent copies to keep
working. Public API (TrailerResolveResult/TrailerResult/SubtitleInfo)
is unchanged, so callers need no changes.
2026-07-07 20:41:08 +03:00
KhooLy
852373a4c1 Fix libass fonts and glyph rendering to match mpv
Embedded MKV font attachments now reach libass reliably. The font scan
was disabled for the local torrent proxy, which is where all torrent
playback lives, so styled fansub tracks fell back to system fonts.

Font sourcing is now layered: a persistent PrefixCaptureBuffer harvests
attachments from the bytes ExoPlayer already downloads (the old capture
died with each ExtractorInput ExoPlayer recreated per load), and an HTTP
scan locates the Attachments element via the SeekHead and fetches it
exactly instead of guessing with an 8 MB prefix. The scan waits for the
player's own connection to open before touching the proxy, backs off
hard between retries, and aborts once fonts exist from any source.
Fonts that arrive after renderer creation recreate it and replay
buffered events. Truncated attachments are dropped instead of being fed
to FreeType half-parsed.

Two compositing bugs caused the sheared "fake italic" glyphs: coverage
rows are packed 4-byte aligned but ALPHA_8 copyPixelsFromBuffer consumes
bitmap rowBytes, so odd-width glyphs skewed one pixel per row; and the
glyph cache was keyed by the native ASS_Image bitmap pointer, which
libass reuses across frames, serving stale glyphs. Rows are repacked
when strides differ and the cache is keyed by content hash.

Startup cost is now near zero: renderer creation moved off ExoPlayer's
loader thread onto the libass HandlerThread, and XDG_CACHE_HOME points
at app storage so fontconfig's /system/fonts scan (~2 s) is cached and
subsequent inits take ~80 ms. libass diagnostics stream to logcat via
ass_set_message_cb.
2026-07-03 23:43:50 +03:00
KhooLy
3c9893d0b8 Rebuild libass renderer: PTS clock, SurfaceView, HandlerThread, ALPHA_8 glyphs
Five architectural changes:

1. PTS-driven clock: VideoFrameMetadataListener on ExoPlayer delivers the
   exact presentation timestamp of each video frame. LibassRenderThread
   drives rendering at that PTS rather than polling currentPosition.

2. Translucent SurfaceView: NativeLibassSubtitleView becomes a SurfaceView
   with PixelFormat.TRANSLUCENT and setZOrderMediaOverlay(true). Its surface
   is composited by the hardware layer between the video surface and the Compose
   window, with zero main-thread involvement during playback.

3. Single HandlerThread ownership: LibassRenderThread owns the renderer handle.
   All operations (render, addEvent, clearEvents, surface lifecycle, teardown)
   are Handler messages to this thread. The ReentrantLock is gone — no
   contention because there is no sharing.

4. ALPHA_8 glyph patches via lockHardwareCanvas: nativeRenderImages returns the
   ASS_Image list as flat int/byte arrays instead of blending into a full ARGB
   frame. Kotlin draws each glyph as a tiny ALPHA_8 bitmap on the hardware
   canvas with a tinted Paint. Glyph bitmaps are cached by libass bitmap pointer,
   so a static or moving subtitle re-draws cached textures at new positions and
   uploads nothing.

5. detect_change gating: nativeRenderImages returns -1 when detect_change == 0.
   Kotlin skips lockHardwareCanvas entirely — zero work when the frame is
   unchanged. Paused playback is fully idle.

LibassEventRelay routes setHeader, addEvent, clearEvents through the thread.
relay.activeRenderer remains a StateFlow<NativeLibassRenderer?> for the existing
PlayerPlaybackSurface observer. Tests updated to drain the HandlerThread between
async relay operations and direct render() calls.
2026-07-03 20:20:24 +03:00
KhooLy
f5afade800 Player: fix zoom/aspect ratio, ExoPlayer crop, and IPT PQ C2 tone-map wiring
- Account for pixelWidthHeightRatio when computing exoVideoAspectRatio so
  anamorphic streams fill correctly
- Replace gestureState.pinchBelowFitAccum with a snappedToFillThisGesture flag:
  one pinch-out snaps to fill, subsequent movement cycles the zoom overlay
- Clamp fillScale to MIN_MANUAL_FILL_SCALE (1.34) so landscape content always
  fully covers the screen before the crop overlay appears
- ExoPlayerEngine.setZoomed now sets VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
  vs SCALE_TO_FIT instead of being a no-op
- Wire iptPqc2UseHdr and iptPqc2PreDecide from resolved stream data into the
  engine request so the IPT PQ C2 tone-map effect receives its configuration
2026-07-03 12:53:17 +03:00
KhooLy
75ef192177 Fix six libass bugs: multi-track mixing, duration inference, font dir, render perf, thread safety, and subtitle matching
Multi-track relay (high): relay now tracks selectedTrackId driven by
updateTracks(). LibassInterceptingTrackOutput drops samples for non-selected
tracks and re-primes the header (via headerGeneration counter) when the
selected track switches. NativeLibassSubtitleOverlay gates relayRenderer on
embeddedSubtitle != null so disabling subs actually stops drawing.

Duration inference (high): pendingBody is flushed in onSeekFlush() with
lastInterEventMs capped to 5s, fixing the last-subtitle-never-shown problem.
Duration cap lowered from 60s to 30s to reduce lingering on sparse dialogue.

Font fallback (medium): LibassInjectingExtractorsFactory now accepts fontsDir
and threads it through to setHeader(), matching what the local/external path
already did. C++ nativeCreate uses fonts_dir for ass_set_fonts_dir and probes
for a NotoSans-Regular.ttf default_font instead of passing nullptr.

Render loop (medium): C++ nativeRender returns jint (1=has image, 0=no image).
When detect_change==0 and bitmap dimensions are unchanged it skips memset+blend
entirely and returns the cached last_had_image. Kotlin onDraw uses the int
result to decide whether to draw and switches to postInvalidateDelayed(100)
during subtitle-free periods instead of postInvalidateOnAnimation at 60 fps.

Thread safety (low): ensure_api() replaced bool+flag with std::call_once so
the first-call race between ExoPlayer's loader thread and main is eliminated.

Subtitle matching (low): selectedNativeAssSubtitle now tries label then URL
then (label+language) before falling back to language-only, preventing two
external subs in the same language from selecting the wrong one.
fetchSubtitleBytes switches to OkHttp and forwards StreamRequestPolicy headers
so addon URLs that require Referer/auth don't 403.
2026-07-03 11:53:57 +03:00
KhooLy
adf7700f7d Fix mpv/ExoPlayer audio passthrough, channel layout, AO, and DV track selection
mpv:
- Set ao=audiotrack explicitly so the AudioTrack output is guaranteed
  (avoids openSLES which lacks encoded passthrough)
- Add audio-channels=7.1,5.1,stereo so mpv negotiates multichannel
  instead of defaulting to auto-safe stereo downmix
- Build audio-spdif at init time by querying AudioManager output device
  encodings; only advertises AC3/E-AC3/DTS/DTS-HD/TrueHD formats the
  device actually reports

ExoPlayer:
- Guard setVideoEffects(emptyList()) behind a videoEffectsActive flag:
  only call it when IptPqc2ToneMapEffect was actually installed, never
  on a clean path — the empty-list call triggers the GL VideoFrameProcessor
  pipeline in media3 1.10.1 which tone-maps HDR even for an empty effect list
- detectPreferredVideoMimeType now accepts Context and checks display +
  decoder DV capability first; on fully DV-capable devices it returns
  VIDEO_DOLBY_VISION so DV variant tracks are preferred rather than
  deprioritized behind H.265/H.264
2026-07-03 03:51:49 +03:00