0.83.3: idents and the intro on their own surface, and hidden screens pause their clocks during full-screen play
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m39s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m38s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-03 11:03:57 +00:00
co-authored by Claude Opus 5.5
parent da7be3b6dc
commit 0dfa70bc45
12 changed files with 26 additions and 15 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ android {
applicationId = "tv.orbit.classic"
minSdk = 26
targetSdk = 36
versionCode = 99
versionName = "0.83.2-second-changeover"
versionCode = 100
versionName = "0.83.3-smooth-idents"
}
buildFeatures { compose = true }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
@@ -379,7 +379,7 @@ internal fun DriveInReminderCard(reminder: DriveInReminder, nowMs: Long, onGo: (
internal fun DriveInScreen(session: EmbySampleSession, plan: ChannelPlan?, visit: Int, modifier: Modifier, active: Boolean, tickets: Map<Long, Int>,
onTicket: (DriveInBill) -> Unit, onHandBack: (DriveInBill) -> Unit, onWatch: () -> Unit) {
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) { while (true) { delay(1000); now = System.currentTimeMillis() } }
LaunchedEffect(Unit) { while (true) { delay(1000); if (!FullScreen.active) now = System.currentTimeMillis() } }
val minute = now / 60_000
val lineup = remember(plan, minute) { plan?.let { driveInLineup(driveInBills(it), now) }.orEmpty() }
var chosenKind by remember { mutableStateOf<DriveInKind?>(null) }
@@ -66,7 +66,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI
connection.connectTimeout = 10_000
connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000
connection.setRequestProperty("Accept", "application/json")
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.83.2\"")
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.83.3\"")
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
if (request.method == "POST") {
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
@@ -236,7 +236,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea
catch (_: Exception) { message = "Connected, but this setup could not be saved securely. It will need sign-in after restart." }
}
}
LaunchedEffect(Unit) { while (true) { now = System.currentTimeMillis(); delay(1000) } }
LaunchedEffect(Unit) { while (true) { delay(1000); if (!FullScreen.active) now = System.currentTimeMillis() } }
LaunchedEffect(scheduledChannels) { windingDown = scheduledChannels[TOTS_NUMBER]?.stepIn?.let { it.endMs > System.currentTimeMillis() } == true }
fun smartLoop(programming: ChannelProgramming) = (extraChannels + listOfNotNull(channel, afterDarkChannel, toonamiChannel)).firstOrNull { it.number == programming.number }
// Keeps smart channels topped up in the background. A failure changes nothing on air.
@@ -100,7 +100,7 @@ internal fun guideStep(channel: SampleChannel, cursorMs: Long, nowMs: Long, forw
@Composable
internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modifier: Modifier = Modifier, title: String = "All Channels", picture: @Composable () -> Unit = {}, onWatch: (Int, Long) -> Unit, onBack: () -> Unit) {
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) { while (true) { delay(1000); now = System.currentTimeMillis() } }
LaunchedEffect(Unit) { while (true) { delay(1000); if (!FullScreen.active) now = System.currentTimeMillis() } }
var row by remember { mutableIntStateOf(channels.indexOfFirst { it.number == selectedNumber }.coerceAtLeast(0)) }
var cursor by remember { mutableLongStateOf(now) }
var slotsAhead by remember { mutableIntStateOf(0) }
@@ -73,7 +73,7 @@ internal fun GuidePicture(session: EmbySampleSession, channel: SampleChannel, li
onDispose { lifecycle.removeObserver(observer) }
}
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) { while (true) { delay(1000); now = System.currentTimeMillis() } }
LaunchedEffect(Unit) { while (true) { delay(1000); if (!FullScreen.active) now = System.currentTimeMillis() } }
val current = channel.at(now)
var ready by remember(channel.number, current.startMs, live, foreground) { mutableStateOf(false) }
// A file that can't play here leaves the artwork up until the next programme.
@@ -126,7 +126,7 @@ private fun DriveInCard(session: EmbySampleSession, plan: ChannelPlan?, now: Lon
internal fun HomeScreen(session: EmbySampleSession, channels: List<SampleChannel>, tuned: SampleChannel, modifier: Modifier = Modifier,
picture: @Composable () -> Unit, onCategory: (ChannelCategory) -> Unit, onWatch: (Int) -> Unit, onDriveIn: () -> Unit, driveInPlan: ChannelPlan? = null) {
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) { while (true) { delay(1000); now = System.currentTimeMillis() } }
LaunchedEffect(Unit) { while (true) { delay(1000); if (!FullScreen.active) now = System.currentTimeMillis() } }
val firstFocus = remember { FocusRequester() }
val gear = LocalNavFocus.current
LaunchedEffect(Unit) { firstFocus.requestFocus() }
@@ -1,7 +1,6 @@
package tv.orbit.classic
import android.content.Context
import android.view.TextureView
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
@@ -101,8 +100,8 @@ internal fun IdentOverlay(session: EmbySampleSession, channel: SampleChannel, ne
}
}
DisposableEffect(player) { onDispose { player.release() } }
// Drawn in the window itself, so it can sit over the show's own picture.
AndroidView(modifier = Modifier.fillMaxSize(), factory = { TextureView(it).also(player::setVideoTextureView) })
// Its own surface, faded with the overlay, sitting over the show's own picture.
AndroidView(modifier = Modifier.fillMaxSize(), factory = { overlaySurface(it, player) }, update = { it.alpha = fade.value })
} else {
ProgrammeArtwork(session, channel, next)
ChannelBrands.forNumber(channel.number)?.let { brand ->
@@ -83,7 +83,7 @@ private fun NavItem(label: String, selected: Boolean, focus: FocusRequester?, be
@Composable
private fun OrbitApp() {
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) { while (true) { now = System.currentTimeMillis(); delay(1000) } }
LaunchedEffect(Unit) { while (true) { delay(1000); if (!FullScreen.active) now = System.currentTimeMillis() } }
var realChannelAvailable by remember { mutableStateOf(false) }
var section by remember { mutableStateOf("Home") }
var restoreChecked by remember { mutableStateOf(false) }
@@ -2,7 +2,7 @@ package tv.orbit.classic
import android.content.Context
import android.os.SystemClock
import android.view.TextureView
import android.view.SurfaceView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -86,12 +86,23 @@ internal fun rememberIntro(source: IntroSource?, onEnd: (String) -> Unit): Intro
return intro
}
/** True while a programme plays full screen. Screens hidden behind it stop their clocks, so they don't churn memory under the picture. */
internal object FullScreen {
var active by mutableStateOf(false)
}
/**
* Video drawn straight to the screen above the programme's own picture, rather than through the app's interface,
* so a busy moment in the interface can't make it stutter.
*/
internal fun overlaySurface(context: Context, player: ExoPlayer) = SurfaceView(context).apply { setZOrderMediaOverlay(true) }.also(player::setVideoSurfaceView)
/** Drawn over everything else on the screen while the intro is showing. */
@Composable
internal fun IntroOverlay(intro: IntroPlayback) {
val player = intro.player ?: return
if (!intro.showing) return
Box(Modifier.fillMaxSize().background(Color.Black)) {
AndroidView(modifier = Modifier.fillMaxSize(), factory = { TextureView(it).also(player::setVideoTextureView) })
AndroidView(modifier = Modifier.fillMaxSize(), factory = { overlaySurface(it, player) })
}
}
@@ -61,6 +61,7 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC
val phase = if (identsHere) identPhase(current.startMs, current.endMs, now, identMs) else IdentPhase.None
// Each changeover in the playback notes, so a missing ident can be explained afterwards.
val notes = remember { java.io.File(settingsContext.noBackupFilesDir, "schedule") }
DisposableEffect(Unit) { FullScreen.active = true; onDispose { FullScreen.active = false } }
val introPlayback = rememberIntro(intro) { PlaybackNotes.add(notes, "channel=${channel.number} $it") }
LaunchedEffect(channel, current.startMs) {
PlaybackNotes.add(notes, "channel=${channel.number} programme start=${current.startMs} lengthMs=${current.endMs - current.startMs} nextFollows=${next.startMs == current.endMs} scheduled=${channel is ScheduledChannel} offset=$clockOffsetMs identsOn=$identsOn")
@@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B
next.instanceFollowRedirects = false
next.connectTimeout = 10_000
next.readTimeout = 20_000
next.setRequestProperty("User-Agent", "OrbitTV/0.83.2")
next.setRequestProperty("User-Agent", "OrbitTV/0.83.3")
next.setRequestProperty("Accept-Encoding", "identity")
session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) }
if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) {