0.83.3: idents and the intro on their own surface, and hidden screens pause their clocks during full-screen play
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user