Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions VERIFY.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ changes, complete these live checks before saying the update is done:
| Nested page Back (web + Android) | Visit Movie A -> Related B -> Cast -> Related C. Visible Back, browser Back, Escape/Backspace, and Android/TV hardware Back unwind C -> Cast -> B -> A -> the originating browse page, one page per press. A deep-focused person filmography must leave the person page immediately; a direct deep link with no prior in-app page uses the safe origin fallback. |
| Web Live TV | Channel starts in Triboon's web player, retunes cleanly, and shows live-specific errors instead of a generic external-player panel. |
| Android ExoPlayer VOD | Movie or episode opens the native branded loader and ExoPlayer surface, not the web video shell; seek does not show the full startup loader. |
| Continue Watching source recovery (web + Android) | Resume an episode on a source made blocked/unresponsive after the watch point was saved. A blocked health verdict advances promptly; an unknown stall retries the same source once, then selects a different ranked release. Playback remains on the same episode and absolute timestamp, never visits show details, and never invokes next episode unless EOF is reached. |
| Android ExoPlayer Live TV | Native Live TV uses ExoPlayer, survives at least 20 Up/Down zaps, and logcat has no fatal/provider-loop markers. |
| Windows native VOD | On Windows 10/11 x64, an H.264 1080p title and HEVC Main10 4K title open the dedicated libmpv surface. Start, pause/resume, seeks/skips, fullscreen, close, direct/remux/transcode fallback, and a simulated sustained stall remain responsive. Diagnostics name the actual decoder; claim GPU only when `hwdec-current` is hardware-backed while frames advance. |
| Windows episode/resume | Saved resume opens at the correct absolute position. Manual next, autoplay at EOF, and episode-strip selection reuse the native surface without revealing details. Close/error/final checkpoint reaches Continue Watching promptly and stale token callbacks cannot change the replacement episode. |
Expand Down Expand Up @@ -689,8 +690,9 @@ Manual checks:
- A season-pack RAR/ZIP mounts and reuses only the requested episode.
- A stalled top candidate gets one 800ms hedge; a ready understudy waits at
most 250ms for higher ranks and prevents additional source launches.
- A sustained web stall retries the same source/kind/timestamp before release
failover.
- A sustained web/native stall retries the same source/kind/timestamp once
before release failover; a confirmed blocked health verdict advances without
waiting, and neither path changes episode or loses the resume timestamp.

### Subtitles / CC / P11

Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ android {
applicationId = "app.triboon.tv"
minSdk = 23 // Media3 1.10.x requires API 23+; Shield/onn/Chromecast TV targets clear this
targetSdk = 36
versionCode = 305
versionName = "2.8.0"
versionCode = 306
versionName = "2.8.1"
}

signingConfigs {
Expand Down
51 changes: 37 additions & 14 deletions android/app/src/main/java/app/triboon/tv/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ public class MainActivity extends Activity {
private static final long NATIVE_VIDEO_STARTUP_STALL_MS = 7000L;
private static final long NATIVE_VIDEO_HEAVY_STARTUP_STALL_MS = 12000L;
private static final long NATIVE_VIDEO_REBUFFER_TRIM_MS = 15000L;
private static final long NATIVE_VIDEO_REBUFFER_RECOVERY_MS = 45000L;
private static final long NATIVE_VIDEO_REBUFFER_RECOVERY_MS = 30000L;
private static final long NATIVE_VIDEO_HEAVY_REBUFFER_RECOVERY_MS = 45000L;
private static final long NATIVE_LIVE_STALL_RECOVERY_MS = 45000L;
private static final long NATIVE_LIVE_STARTUP_STALL_RECOVERY_MS = 12000L;
private static final long NATIVE_LIVE_RECOVERY_COOLDOWN_MS = 15000L;
Expand Down Expand Up @@ -4087,8 +4088,9 @@ && isNativeRecoverableIoError(error) && nativeAllowReconnectResume()) {
updateNativeChrome();
if (state == Player.STATE_READY) {
if ("video".equals(nativeMode)) {
nativeVideoStarted = true;
widenNativeReadTimeoutAfterFirstFrame();
// READY only proves that ExoPlayer can begin; it can still be waiting on
// the resumed byte window with no rendered/advancing frame. PLAYING below
// owns the established boundary used by stall recovery.
applyNativeStartSeekIfReady();
if (!nativePercentResumePending) {
rememberNativeVideoPosition();
Expand All @@ -4102,7 +4104,8 @@ && isNativeRecoverableIoError(error) && nativeAllowReconnectResume()) {
}
}
if (state == Player.STATE_READY && nativeLoading != null
&& nativeLoading.getVisibility() == View.VISIBLE && !nativePercentResumePending) {
&& nativeLoading.getVisibility() == View.VISIBLE && !nativePercentResumePending
&& (!"video".equals(nativeMode) || nativeVideoStarted || !nativePlayer.getPlayWhenReady())) {
nativeVideoUnhealthySinceMs = 0L;
hideNativeLoading();
if (!nativeGuideMode) showNativeChrome(true);
Expand Down Expand Up @@ -4149,6 +4152,12 @@ && isNativeRecoverableIoError(error) && nativeAllowReconnectResume()) {
nativeVideoStarted = true;
widenNativeReadTimeoutAfterFirstFrame();
nativeVideoUnhealthySinceMs = 0L;
nativeVideoMemoryTrimmedDuringBuffer = false;
if (nativeLoading != null && nativeLoading.getVisibility() == View.VISIBLE
&& !nativePercentResumePending) {
hideNativeLoading();
if (!nativeGuideMode) showNativeChrome(true);
}
if (!nativePercentResumePending) {
rememberNativeVideoPosition();
web.evaluateJavascript("window.__tvNativeVideoPlaying && __tvNativeVideoPlaying("
Expand Down Expand Up @@ -4507,11 +4516,22 @@ private void resolveNativePercentStartIfKnown() {
private void completeNativePercentResume() {
if (!nativePercentResumePending) return;
nativePercentResumePending = false;
if (nativePlayer != null && !nativePlayer.getPlayWhenReady()) nativePlayer.play();
if (nativeLoading != null && nativeLoading.getVisibility() == View.VISIBLE) {
if (nativePlayer != null && nativePlayer.isPlaying()) {
// Direct percent-resume may already be playing at the resolved target when the 1s
// progress tick clears the pending flag. onIsPlayingChanged(true) will not fire twice,
// so commit that real-playing boundary here instead of leaving the loader/JS state stuck.
nativeVideoStarted = true;
widenNativeReadTimeoutAfterFirstFrame();
nativeVideoUnhealthySinceMs = 0L;
nativeVideoMemoryTrimmedDuringBuffer = false;
hideNativeLoading();
if (!nativeGuideMode) showNativeChrome(true);
rememberNativeVideoPosition();
web.evaluateJavascript("window.__tvNativeVideoPlaying && __tvNativeVideoPlaying("
+ nativePosSeconds() + "," + nativeDurSeconds() + "," + nativePlaybackToken + ")", null);
} else if (nativePlayer != null && !nativePlayer.getPlayWhenReady()) {
// The eventual onIsPlayingChanged(true) callback owns the boundary in this path.
nativePlayer.play();
}
}

Expand Down Expand Up @@ -6907,17 +6927,18 @@ private void updateNativeLiveWatchdog() {
private void updateNativeVideoWatchdog() {
if (!"video".equals(nativeMode) || nativePlayer == null) return;
int state = nativePlayer.getPlaybackState();
if (state == Player.STATE_READY) {
nativeVideoStarted = true;
widenNativeReadTimeoutAfterFirstFrame();
rememberNativeVideoPosition();
boolean wantsPlayback = nativePlayer.getPlayWhenReady()
&& nativePlayer.getPlaybackSuppressionReason() == Player.PLAYBACK_SUPPRESSION_REASON_NONE;
boolean readyButNotPlaying = state == Player.STATE_READY && wantsPlayback && !nativePlayer.isPlaying();
if (state == Player.STATE_READY && !readyButNotPlaying) {
if (nativeVideoStarted) rememberNativeVideoPosition();
nativeVideoUnhealthySinceMs = 0L;
nativeVideoMemoryTrimmedDuringBuffer = false;
return;
}
if (nativeVideoStarted) {
boolean waitingForData = state == Player.STATE_BUFFERING
|| (nativePlayer.getPlayWhenReady() && !nativePlayer.isPlaying() && nativePlayer.isLoading());
|| readyButNotPlaying;
boolean unhealthy = state == Player.STATE_IDLE || waitingForData;
if (!unhealthy) {
nativeVideoUnhealthySinceMs = 0L;
Expand All @@ -6935,15 +6956,17 @@ private void updateNativeVideoWatchdog() {
Log.w(TAG, "Native VOD rebuffer still waiting after " + elapsed + "ms; trimming UI caches");
trimAndroidMemoryCaches(false);
}
if (elapsed >= NATIVE_VIDEO_REBUFFER_RECOVERY_MS) {
long recoveryMs = nativeLikelyHeavyVod()
? NATIVE_VIDEO_HEAVY_REBUFFER_RECOVERY_MS
: NATIVE_VIDEO_REBUFFER_RECOVERY_MS;
if (elapsed >= recoveryMs) {
Log.w(TAG, "Native VOD rebuffer stalled after " + elapsed + "ms; retrying same source");
notifyNativeVideoError(state == Player.STATE_IDLE ? "native player idle" : "native rebuffer stalled",
nativePosSeconds(), nativeDurSeconds());
}
return;
}
boolean unhealthy = state == Player.STATE_IDLE || state == Player.STATE_BUFFERING
|| (nativePlayer.getPlayWhenReady() && !nativePlayer.isPlaying());
boolean unhealthy = state == Player.STATE_IDLE || state == Player.STATE_BUFFERING || readyButNotPlaying;
if (!unhealthy) {
nativeVideoUnhealthySinceMs = 0L;
return;
Expand Down
4 changes: 2 additions & 2 deletions clients/windows-px8/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ pre-bundle hash.
powershell -ExecutionPolicy Bypass -File .\clients\windows-px8\scripts\build-package.ps1

# A release build additionally creates the versioned alias and rejects a tag
# that does not match package/Cargo/Tauri version 2.8.0.
powershell -ExecutionPolicy Bypass -File .\clients\windows-px8\scripts\build-package.ps1 -Tag v2.8.0
# that does not match package/Cargo/Tauri version 2.8.1.
powershell -ExecutionPolicy Bypass -File .\clients\windows-px8\scripts\build-package.ps1 -Tag v2.8.1
```

The default output is `dist\windows-client\Triboon-Windows-Client.exe`; a tag
Expand Down
4 changes: 2 additions & 2 deletions clients/windows-px8/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion clients/windows-px8/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "triboon-windows-client",
"private": true,
"version": "2.8.0",
"version": "2.8.1",
"description": "Triboon native Windows GPU client (Tauri + libmpv).",
"scripts": {
"tauri": "tauri",
Expand Down
2 changes: 1 addition & 1 deletion clients/windows-px8/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion clients/windows-px8/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "triboon-px8"
version = "2.8.0"
version = "2.8.1"
description = "Triboon native Windows GPU client"
edition = "2021"
rust-version = "1.85"
Expand Down
2 changes: 1 addition & 1 deletion clients/windows-px8/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Triboon",
"version": "2.8.0",
"version": "2.8.1",
"identifier": "app.triboon.windows",
"build": {
"frontendDist": "../ui"
Expand Down
21 changes: 13 additions & 8 deletions docs-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,9 @@ Rules that must not drift:
WEB-sized/remux-to-AAC path. Low-power Android TV and older Chromecast-class
devices also prefer AVC/H.264 for 1080p auto-picks when an AVC source is
available, while HEVC/AV1 remain available as fallback/manual sources.
- After ExoPlayer reaches READY, normal buffering must not remount or restart
a movie from the beginning.
- ExoPlayer `STATE_READY` alone is not a rendered-playback boundary. A READY
player that wants playback but produces no frames remains under the bounded
startup watchdog; recovery always preserves the requested timestamp.
- Continue Watching follows `docs-continue-watching.md`: one canonical Home card
per movie/show, active progress beats next-up, and the saved 4K/1080p source
class carries into remaining TV episodes.
Expand Down Expand Up @@ -268,9 +269,12 @@ Required behavior:
seeks stay fast for other users.
- Health checks keep the 500ms upfront gate. Background triage is lower priority
and must never starve the segment the player is actively waiting on.
- After web VOD has genuinely started, a waiting state with no meaningful
position progress for 45 seconds retries the same source, playback kind, and
timestamp first. Recovery must not silently advance to a different release.
- Web and native VOD distinguish readiness from real playback. Startup without
a first frame is bounded at 10/18 seconds on web and 7/12 seconds on Android
(normal/heavy). Established stalls retry the same source, playback kind, and
timestamp once after 30/45 seconds. A blocked live health verdict or a second
sustained stall advances to the next ranked release at the same timestamp;
it never changes episodes. A missing server session re-mounts the same title.
- Historical local-only Easynews benchmark evidence supports the original
fast-start assumptions, but it is not part of a clean clone and is not a fixed
runtime rule. Do not reintroduce hardcoded "16 warm connections" or "8-12
Expand Down Expand Up @@ -499,9 +503,10 @@ When changing persistence, update:
path uses a `SurfaceView` player surface, decoder fallback, closest-sync
seeks, seeded bandwidth, byte-bounded VOD buffers, short back buffers, live
target-offset tuning, conservative-device HLS caps, and audio offload where
Android supports it. Sustained post-start VOD stalls trim UI caches and retry
the same source at the last trustworthy timestamp instead of silently
switching release or quality.
Android supports it. `STATE_READY` does not count as started until ExoPlayer
actually plays. Sustained post-start VOD stalls trim UI caches and retry the
same source at the last trustworthy timestamp once; a confirmed dead source
or repeated stall advances releases without changing episode or position.
- Sends native capability claims to the web UI/server before source selection,
including HDMI/ARC/eARC audio-output passthrough flags for AC3, E-AC3, E-AC3
JOC, DTS, DTS-HD, and TrueHD. Conservative/budget device detection is allowed
Expand Down
17 changes: 15 additions & 2 deletions docs-continue-watching.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,18 @@ flowchart LR
seeks once ExoPlayer knows duration; remux/transcode performs one
token-guarded server-seek remount at the computed absolute timestamp.
- Native progress, READY state, and the loading surface must not report a false
zero position before that resume handoff completes.
zero position before that resume handoff completes. READY alone is not proof
that resumed frames are advancing; real playback owns that boundary.

## Resume Source Recovery

- Web and native playback re-check the live mount after opening. A confirmed
blocked source advances immediately to the next ranked release.
- Startup without a real first frame uses the bounded player fallback ladder.
An established playback stall retries the same source/kind/timestamp once;
a second sustained stall changes release, never episode.
- Source replacement retains the requested Continue Watching point even when
it occurs before the native player reports its first position callback.

## Focus Rules

Expand Down Expand Up @@ -122,4 +133,6 @@ When changing Continue Watching, verify:
9. Pause or stop web, native direct/remux/transcode, Cast, and Multiview VOD;
the latest position is immediately visible in Continue Watching, including
after backgrounding or closing the app.
10. `npm.cmd test` passes after behavior changes.
10. Resume an episode whose saved source is now blocked: playback advances to a
healthy release at the same episode and timestamp without visiting details.
11. `npm.cmd test` passes after behavior changes.
Loading
Loading