0.99.15: hand-run playback test (Beerus), and the revised Afterburn premiere proposal
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m10s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m28s

Plays one episode in Orbit's own player via MainActivity's orbit_play_test extra, in a separate non-exported activity;
books nothing, reports nothing to Emby, ends by itself. Booking fingerprints identical to 0.99.14.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-04 15:01:22 +00:00
co-authored by Claude Opus 5.5
parent 0bd9a89bb4
commit fc6c8d3594
11 changed files with 375 additions and 68 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ android {
applicationId = "tv.orbit.classic"
minSdk = 26
targetSdk = 36
versionCode = 144
versionName = "0.99.14-source-diagnostics"
versionCode = 145
versionName = "0.99.15-playback-test"
}
buildFeatures { compose = true }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
+2
View File
@@ -6,5 +6,7 @@
<activity android:name=".MainActivity" android:exported="true" android:screenOrientation="landscape">
<intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LEANBACK_LAUNCHER" /></intent-filter>
</activity>
<!-- The hand-run playback test, opened only by MainActivity's orbit_play_test extra. -->
<activity android:name=".PlaybackTestActivity" android:exported="false" />
</application>
</manifest>
@@ -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.99.14\"")
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.99.15\"")
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
if (request.method == "POST") {
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
@@ -70,7 +70,11 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
/** Called when the programme's first picture is on screen, so anything held over the changeover can go. */
onFirstFrame: (() -> Unit)? = null,
/** Called when a scheduled programme's file runs out before its slot ends. */
onEndedEarly: (() -> Unit)? = null) {
onEndedEarly: (() -> Unit)? = null,
/** False for the hand-run playback test: nothing is reported to Emby, so it records no progress or resume point. */
reportPlayback: Boolean = true,
/** The playback test's own listener on the player, for its diagnostics. */
onPlayer: ((ExoPlayer) -> Unit)? = null) {
val context = LocalContext.current
val logoSetting = remember { OrbitSettings(context).channelLogo }
var failure by remember { mutableStateOf<String?>(null) }
@@ -105,6 +109,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
prepare(); playWhenReady = true
}
}
LaunchedEffect(player) { onPlayer?.invoke(player) }
fun textTracks() = player.currentTracks.groups.filter { it.type == C.TRACK_TYPE_TEXT && it.isSupported }
fun chooseSubtitle(index: Int?, tracks: List<Tracks.Group>) {
subtitleIndex = index
@@ -159,7 +164,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
val position = player.currentPosition
player.stop(); ready = false
stopReported = true
EmbyCleanup.run { session.report(plan, "Playing/Stopped", position, true) }
if (reportPlayback) EmbyCleanup.run { session.report(plan, "Playing/Stopped", position, true) }
}
}
lifecycle.addObserver(observer)
@@ -253,7 +258,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
player.removeListener(listener)
if (handedBack) return@onDispose
player.release()
if (!stopReported) EmbyCleanup.run { session.report(plan, "Playing/Stopped", position, true) }
if (!stopReported && reportPlayback) EmbyCleanup.run { session.report(plan, "Playing/Stopped", position, true) }
}
}
LaunchedEffect(player, ready) {
@@ -279,7 +284,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
}
}
LaunchedEffect(player, ready) {
if (ready && foreground) {
if (ready && foreground && reportPlayback) {
val startPosition = player.currentPosition; val startPaused = !player.isPlaying
withContext(Dispatchers.IO) { if (foreground) session.report(plan, "Playing", startPosition, startPaused) }
while (foreground) {
@@ -52,6 +52,10 @@ class MainActivity : ComponentActivity() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (intent?.getBooleanExtra("orbit_intro_test", false) == true) IntroTrial.nextTune = true
intent?.getStringExtra("orbit_inspect_series")?.take(120)?.let { SeriesInspection.title = it }
// The hand-run playback test opens over Orbit, which stops in the background as it would for any other app, and comes back when it ends.
intent?.getStringExtra("orbit_play_test")?.takeIf(EmbyClient::validId)?.let { item ->
startActivity(android.content.Intent(this, PlaybackTestActivity::class.java).putExtra(PlaybackTestActivity.ITEM, item))
}
// One typeface everywhere, matching the channel logos.
setContent { MaterialTheme { CompositionLocalProvider(androidx.tv.material3.LocalTextStyle provides androidx.compose.ui.text.TextStyle(fontFamily = Barlow)) { OrbitApp() } } }
}
@@ -0,0 +1,139 @@
package tv.orbit.classic
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.sp
import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.text.CueGroup
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
/**
* The hand-run playback test (approved by Paul on 4 November 2026 for Dragon Ball Super: Beerus): one episode, by its
* Emby id, in Orbit's own player, opened with `--es orbit_play_test <id>` on MainActivity. English subtitles are chosen the
* way Orbit chooses them with its subtitles setting on. It books nothing and never touches the scheduler, plans, cursors or
* pools, reports nothing to Emby (so no progress or resume point is recorded there), and ends by itself after
* [TEST_MS], or on Back, returning to Orbit as it was. It logs formats, languages and counts only (tag OrbitPlayTest),
* never a title, path or address.
*/
class PlaybackTestActivity : ComponentActivity() {
companion object {
const val ITEM = "item"
const val TEST_MS = 3 * 60_000L
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val item = intent?.getStringExtra(ITEM)?.takeIf(EmbyClient::validId) ?: run { finish(); return }
setContent { MaterialTheme { PlaybackTest(item) { finish() } } }
}
}
/** What the test saw, summed up at the end. Counts and formats only. */
internal class PlaybackTestRecord {
var ready = false
var firstFrame = false
var video = "none"
var audio = "none"
var subtitles = emptyList<String>()
var subtitleSelected = "none"
var cueUpdates = 0
var firstCueMs: Long? = null
var errors = 0
fun summary() = "playtest result ready=$ready firstFrame=$firstFrame video=$video audio=$audio subtitles=${subtitles.size}[${subtitles.joinToString(", ")}]" +
" subtitleSelected=$subtitleSelected cueUpdates=$cueUpdates firstCuePositionMs=${firstCueMs ?: "none"} errors=$errors"
}
/** A short, safe label: a language code or a format's MIME type (one slash at most), else "unknown". */
private val SafeLabel = Regex("""[A-Za-z0-9.+_-]{1,20}(/[A-Za-z0-9.+_-]{1,20})?""")
internal fun playTestLabel(value: String?) = value?.trim()?.takeIf(SafeLabel::matches) ?: "unknown"
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@Composable
private fun PlaybackTest(itemId: String, done: () -> Unit) {
val context = LocalContext.current
var session by remember { mutableStateOf<EmbySampleSession?>(null) }
var plan by remember { mutableStateOf<EmbyPlaybackPlan?>(null) }
var problem by remember { mutableStateOf<String?>(null) }
val record = remember { PlaybackTestRecord() }
val subtitles = remember { mutableStateOf<String?>(SUBTITLES_BY_DEFAULT) }
val startedAt = remember { android.os.SystemClock.elapsedRealtime() }
var ended by remember { mutableStateOf(false) }
fun finish(reason: String) {
if (ended) return
ended = true
Log.i("OrbitPlayTest", record.summary() + " ended=$reason")
done()
}
LaunchedEffect(itemId) {
Log.i("OrbitPlayTest", "playtest start item=$itemId")
try {
withContext(Dispatchers.IO) {
val saved = RememberedConnectionStore.get(context).load() ?: throw EmbyCheckException("No saved connection.")
val active = EmbyClient(HttpsEmbyTransport(saved.identity.deviceId)).resume(saved)
session = active
val item = SampleItem("Playback test", "Episode", null, "", itemId)
active.rememberScheduledItems(listOf(item))
plan = active.prepare(item)
}
} catch (failure: Exception) {
Log.w("OrbitPlayTest", "playtest failed=${failure.javaClass.simpleName} status=${(failure as? EmbyCheckException)?.httpStatus}")
problem = "The playback test couldn't start."
delay(5_000)
finish("failed")
}
}
// The test ends by itself, whatever happens.
LaunchedEffect(plan) { if (plan != null) { delay(PlaybackTestActivity.TEST_MS); finish("time") } }
// The test's own session ends without signing out, so Orbit's saved connection carries on.
DisposableEffect(Unit) { onDispose { session?.let { active -> EmbyCleanup.run { active.detach() } } } }
Box(Modifier.fillMaxSize().background(Color.Black), contentAlignment = Alignment.Center) {
val ready = plan
val active = session
if (ready != null && active != null) {
EmbyPlaybackDialog(active, ready, onClose = { finish("back") }, subtitlePreference = subtitles, announce = false, reportPlayback = false,
onPlayer = { player ->
player.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_READY && !record.ready) { record.ready = true; Log.i("OrbitPlayTest", "playtest readyMs=${android.os.SystemClock.elapsedRealtime() - startedAt}") }
if (state == Player.STATE_ENDED) finish("ended")
}
override fun onRenderedFirstFrame() { record.firstFrame = true }
override fun onPlayerError(error: PlaybackException) { record.errors++; Log.w("OrbitPlayTest", "playtest error=${error.errorCodeName}") }
override fun onTracksChanged(tracks: Tracks) {
fun selected(type: Int) = tracks.groups.filter { it.type == type && it.isSelected }.mapNotNull { g -> (0 until g.length).firstOrNull(g::isTrackSelected)?.let(g::getTrackFormat) }.firstOrNull()
selected(C.TRACK_TYPE_VIDEO)?.let { record.video = "${playTestLabel(it.sampleMimeType)} ${it.width}x${it.height}" }
selected(C.TRACK_TYPE_AUDIO)?.let { record.audio = "${playTestLabel(it.sampleMimeType)} ${playTestLabel(it.language)} ${it.channelCount}ch" }
val text = tracks.groups.filter { it.type == C.TRACK_TYPE_TEXT }
record.subtitles = text.map { g -> g.getTrackFormat(0).let { "${playTestLabel(it.sampleMimeType ?: it.codecs)} ${playTestLabel(it.language)}" + (if (g.isSupported) "" else " unsupported") + (if (g.isSelected) " selected" else "") } }
record.subtitleSelected = selected(C.TRACK_TYPE_TEXT)?.let { playTestLabel(it.language) } ?: "none"
Log.i("OrbitPlayTest", "playtest tracks video=${record.video} audio=${record.audio} subtitles=${record.subtitles.joinToString(", ")}")
}
override fun onCues(cueGroup: CueGroup) {
if (cueGroup.cues.isEmpty()) return
record.cueUpdates++
if (record.firstCueMs == null) { record.firstCueMs = player.currentPosition; Log.i("OrbitPlayTest", "playtest firstCuePositionMs=${record.firstCueMs}") }
}
})
})
} else Text(problem ?: "Playback test…", color = Muted, fontSize = 16.sp)
}
}
@@ -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.99.14")
next.setRequestProperty("User-Agent", "OrbitTV/0.99.15")
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()) {
@@ -0,0 +1,30 @@
package tv.orbit.classic
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class PlaybackTestTest {
@Test fun labelsAreShortAndSafe() {
assertEquals("text/x-ssa", playTestLabel("text/x-ssa"))
assertEquals("jpn", playTestLabel("jpn"))
assertEquals("unknown", playTestLabel(null))
for (hostile in listOf("https://host/stream?api_key=SECRET", "/mnt/media/Beerus.mkv", "media/anime/Beerus.mkv", "Japanese (Crunchyroll)", "a".repeat(41))) assertEquals("unknown", playTestLabel(hostile))
}
@Test fun theSummaryHoldsFormatsAndCountsOnly() {
val record = PlaybackTestRecord().apply {
ready = true; firstFrame = true; video = "video/avc 1920x1080"; audio = "audio/mp4a-latm jpn 2ch"
subtitles = listOf("text/x-ssa eng selected", "text/x-ssa por"); subtitleSelected = "eng"; cueUpdates = 41; firstCueMs = 12_345
}
assertEquals("playtest result ready=true firstFrame=true video=video/avc 1920x1080 audio=audio/mp4a-latm jpn 2ch subtitles=2[text/x-ssa eng selected, text/x-ssa por]" +
" subtitleSelected=eng cueUpdates=41 firstCuePositionMs=12345 errors=0", record.summary())
assertFalse(PlaybackTestRecord().summary().contains("null"))
}
@Test fun englishIsChosenTheWayOrbitChoosesIt() {
// The test asks for subtitles as Orbit's setting does: English where there is some, whatever the track order.
assertEquals(2, subtitleFor(SUBTITLES_BY_DEFAULT, listOf("por", "spa", "eng")))
assertEquals(null, subtitleFor(null, listOf("eng")))
}
}
+59
View File
@@ -0,0 +1,59 @@
# Orbit TV 0.99.15: hand-run playback test (Beerus)
Paul approved building this on 4 November 2026 at 14:48. Installing it needs its own approval. It adds a test that plays one chosen episode in Orbit's existing Media3 player, without making any booking. Scheduling, plans, cursors, pools and presentation are untouched.
## What's in it
- **`PlaybackTestActivity`:** a separate, non-exported screen. It opens only when MainActivity is launched by hand with `--es orbit_play_test <Emby episode id>`, an id-shaped value.
- Orbit itself goes to the background, as it would for any other app, so its channel and picture players stop.
- Orbit comes back as it was when the test ends.
- **The test:**
- It restores the saved connection without signing in again.
- It prepares the episode with the same direct-play request Orbit uses for channels.
- It plays it in the same `EmbyPlaybackDialog` and player.
- It asks for subtitles exactly as Orbit does with its subtitles setting on: English where a track exists.
- It ends by itself after 3 minutes, or on Back, or at the end of the file.
- Its session ends with `detach`, never sign-out, so Orbit's saved connection carries on.
- **Isolation:**
- **Scheduler:** the test never calls it or the schedule store.
- **Emby:** a new `reportPlayback = false` option on the player dialog means the test reports nothing to Emby, so no progress, resume point or watched state is recorded. Every other caller keeps the default, true, and is unchanged.
- **Diagnostics:** a new `onPlayer` callback hands the test the player for its own listener. It's null for every other caller.
- **Log** (tag OrbitPlayTest; formats, language codes and counts only, never a title, path or address):
- when the test starts;
- the time to ready;
- the selected video format and size;
- the selected audio format, language and channels;
- every subtitle track, with format, language, whether it's supported and whether it's selected;
- the first subtitle line's position;
- the number of subtitle updates;
- any player errors;
- a final summary with how the test ended.
- **Version:** 0.99.15 (versionCode 145).
## Checks before install
- **443 unit tests pass** (2 skipped: hand-run replays that need input files). Lint has no errors.
- **Three new tests in PlaybackTestTest:**
- Labels allow only a language code or a MIME type. Paths, URLs, `api_key` values, display names and long strings become "unknown".
- The summary holds formats and counts only.
- English is chosen the way Orbit chooses it.
- **Booking fingerprints:** the four-week replay of all 30 channels on 0.99.15 is identical to 0.99.14 on every channel (docs/replays/replay-fingerprints-0.99.15.txt).
## Running it (after approval, with the TV idle and Paul watching)
1. Snapshot as usual, then install.
2. Launch with `adb shell "am start -n tv.orbit.classic/.MainActivity --es orbit_play_test 8016755"`. That's Beerus episode 1 in copy 8016753, and the whole command is quoted.
3. **The log checks:**
- the picture: `video/avc 1920x1080`;
- Japanese audio: `audio/mp4a-latm jpn 2ch`;
- English subtitles selected;
- subtitle updates above zero;
- no errors.
4. **Paul's eyes:** whether the English subtitles show on screen and read well. That's actual rendering, which the log can't show, and screenshots are blocked by the app's secure flag.
5. **Afterwards:** check that plans and cursors are unchanged, and that Orbit is back to normal.
**Known, by design:** with Orbit's own subtitles setting off, the default, a Beerus booking wouldn't show subtitles. The test uses the setting-on choice. The premiere proposal covers how bookings should handle this.
## Rollback
Install 0.99.14. Nothing is saved by this change.
+66 -60
View File
@@ -1,82 +1,88 @@
# Dragon Ball Super: Beerus on Vortex Prime: programming proposal (read-only)
# Dragon Ball Super: Beerus: Vortex Afterburn premiere proposal (revised, read-only)
4 November 2026. This is a proposal only: Beerus isn't added to Vortex, nothing is booked, and no media or catalogue is changed. It would ship as Living Network Stage D's first booking, as its own release, after your approval. Committed bookings are never rewritten.
Revised on 4 November 2026 after Paul's direction at 14:48. It replaces the earlier Saturday 17:00 Vortex Prime proposal. This is a proposal only:
- Beerus isn't added to Vortex, and nothing is booked.
- No media or catalogue is changed.
- Committed bookings are never rewritten.
## What's confirmed
The booking would ship as Living Network Stage D's first release, after your separate approval.
- **Copy:** Emby 8016753, provisionally. It has episodes 1 to 4, none missing, 23 minutes each.
- **Video:** H.264 High 1080p in mkv.
- **Audio:** Japanese AAC stereo.
- **Subtitles:** English ASS, flagged forced and default.
- **Box verdict:** all 4 are playable, as a compatibility result from metadata and the box's decoders. See docs/replays/beerus-inspection-2026-11-04.md.
- **Not yet confirmed:** actual playback in Orbit's player, and whether the English subtitles show automatically. See the playback test below.
## Decided
## The existing schedule
- **Source:** Emby copy **8016753**, approved by Paul. Episodes 1 to 4, 23 minutes each.
- H.264 1080p in mkv.
- Japanese AAC stereo.
- English ASS subtitles, flagged forced and default.
- **Not the 4K copy 8055740.** The booking will be pinned to 8016753 by its Emby id, so the other copy can never be mixed in.
- **No Sunday encore** for now.
- **Vortex's committed plan** runs to Wednesday 11 November at 13:22, and it stays as it is.
- Its Saturday 7 November Vortex Prime hour (16:00 to 18:00) is Cardcaptor Sakura, Batman Beyond, Gundam Wing, Yu-Gi-Oh! and ThunderCats.
- **Dragon Ball Super (2015)** is airing in order: episodes 1 to 6, the start of the Beerus saga, between 6 and 11 November, plus Broly at Friday's Movie Night.
- At its current pace, it should be past the Beerus saga, its first 14 or so episodes, around 18 to 20 November. That's an estimate, to be confirmed from its bookings at the next top-ups.
## What the schedule shows
- **Vortex's committed plan** ends on Wednesday 11 November at 13:22 and stays as it is.
- **Saturday evenings:** Vortex Movie Night takes 20:00 to 22:00 on Fridays and Saturdays, and Afterburn picks up after the film.
- A four-week replay from the box's current plan (projection only; nothing written) has Saturday 21 November's Movie Night as two films, Fusion Reborn at 20:21 and The Path to Power at 21:12, running to 22:32.
- So a 21:00 premiere needs Movie Night to give way.
- **Dragon Ball Super (2015):** the same replay has it reaching the end of its Beerus saga (season 1, episode 14) on about 20 November. It then moves on to the Resurrection 'F' saga (season 2) from 22 November.
- On 21 November, the remake would retell the story the original finished the day before.
- It's your call whether that back-to-back retelling is a feature or too soon. Starting on 28 November or 5 December would leave a gap. I've kept 21 November, as you asked.
- The projection uses today's pool and doesn't model other channels' clash avoidance, so exact dates can move by a day or so.
## Proposal
| | Proposed |
|---|---|
| **Premiere** | **Saturday 21 November, 17:00**, Vortex Prime. This is the first Saturday clear of both the committed week and Dragon Ball Super (2015)'s run through the same saga. |
| **Cadence** | Weekly: one new-to-Orbit episode every Saturday at 17:00. |
| **Progression** | In order from episode 1, with its own cursor. Episodes 1 to 4 air on 21 and 28 November and 5 and 12 December. New episodes join as the library gains them. Japan airs weekly, so Orbit stays about four weeks behind with a buffer in hand. |
| **If the library runs dry** | If no unaired, playable episode is in the library by the Saturday, the slot goes back to normal Vortex Prime rotation. There's no repeat billed as new, and no promotion. |
| **Optional encore** | Sunday 16:00, that week's episode again, for anyone who missed Saturday. Your choice. |
| **Not on Vortex otherwise** | Beerus airs only in this slot, so it stays an event and never enters everyday rotation. |
| **The Dragon Ball Super (2015) overlap** | A soft rule for later: don't book Dragon Ball Super (2015)'s matching episodes, or Battle of Gods or Resurrection 'F', on Vortex within 3 days of the same part of Beerus. The episode mapping needs checking first. Until then, the 21 November start keeps them apart for episodes 1 to 4. |
| **Event** | "Vortex Afterburn Premiere: Dragon Ball Super: Beerus" |
| **When** | **Saturday 21 November 2026, 21:00** (an on-time appointment) |
| **Programme** | Episode 1, The God Who Destroys, at 21:00. Episode 2, The God of Destruction Heads to the Planet of the Kai, at about 21:23. They run straight through, in order, with nothing between them. Afterburn's usual rotation follows from about 21:46. |
| **Movie Night that evening** | **Recommended:** a shorter Movie Night of one film that finishes by 21:00. The appointment's on-time rule picks it, and Vortex has Dragon Ball films of about 50 minutes, such as Fusion Reborn and Bojack Unbound, so the evening runs as a Dragon Ball film from 20:00 and then the premiere. **Fallback:** if the replay for the booking release shows no film fits cleanly, the premiere moves to 22:00, after a normal Movie Night. |
| **Following Saturday** | 28 November, 21:00: episodes 3 (The God of Destruction Heads to Earth) and 4 (The God of Saiyans), as the next double bill. This is subject to both still being playable and the booking fitting. |
| **After that** | **Double bills** while at least 4 unaired, playable episodes are in the library when the week is booked, so there's a week's buffer in hand. **A single episode** with 1 to 3. **No new episode:** the slot returns to normal Afterburn and isn't promoted. Japan airs one episode a week, so the series is likely to settle into single episodes in December. Each step down is automatic, but every booking stays a normal booking that can't be rewritten once made. |
| **Progression** | Beerus has its own in-order cursor from episode 1. It doesn't move Dragon Ball Super (2015) or any other show. |
| **Elsewhere on Vortex** | Beerus only airs in these premiere events, not in everyday rotation. |
### Channel fit
### How it would be built (the Stage D release, later)
- **For:**
- Vortex Prime (16:00 to 18:00) is the energetic, colourful after-school action hour, and Dragon Ball is its anchor franchise.
- Beerus is mainstream and comedic, so it suits Prime better than Afterburn (21:00 to 23:00), which is darker and more intense.
- A fixed weekly slot also echoes how Toonami premiered new episodes.
- **Against:**
- Japanese audio with English subtitles is unusual for Prime, which is otherwise mostly English-dubbed shows.
- If you'd rather keep Prime dubbed, the alternative is Saturday 21:00 in Afterburn, or waiting for a dub in the library.
- **The event:** a Saturday 21:00 event on Vortex, using the same mechanism as Evidence Sunday Mystery and the After Dark appointments.
- Its only title is Beerus.
- It's on time, so what comes before is chosen to finish by 21:00.
- It has one start a night.
- Its run length is 2 episodes or 1, by the buffer rule above.
- Story grouping's "back to back, nothing between" booking, from Stage A, keeps the pair together.
- **Presentation key:** `vortex.afterburn.premiere`, saved on each booked episode.
- Until the dedicated Afterburn ident exists, the key has no ident of its own, so the standard Vortex transition plays. A missing special ident never stops a booking from playing.
- **The first booking** appears at the top-up that brings 21 November into the seven-day horizon, about 14 to 15 November. **The release must be installed before then.**
- It's then checked on the box: two episodes at 21:00, the key, and what Movie Night became.
- **The never-rewrite rule:** nothing already committed changes. If the event can't be booked as proposed (unplayable, or doesn't fit), the evening books as normal, and nothing is promoted.
### How it would be built (Stage D, later, for approval)
### Subtitles: needs a decision
- **The Saturday event:** a 17:00 to 18:00 event on Vortex, using the same mechanism as Evidence Sunday Mystery and the After Dark events:
- its only title is Beerus;
- one start a night;
- it's on time, so the programme before it finishes by 17:00;
- it has its own presentation key, such as `vortex.prime.premiere`.
- **The rest of the hour:** returns to the usual Vortex Prime rotation.
- **Bookings:** the first one appears at the top-up when 21 November comes within the seven-day horizon, around 14 November. Nothing already committed changes.
- **Copy rules:** episode 1 of copy B is never used, and the copy is pinned by its Emby id, 8016753, so the duplicate can't be mixed in.
**Orbit's subtitles setting is off on your box**, which is the default. With it off, Beerus would play in Japanese with no subtitles unless someone turns them on with right on the remote.
## Presentation and What's On (later; only once booked)
**Recommended:** the booking release also turns English subtitles on automatically for any programme whose sound has no English track. That's Beerus today, and the rule would apply anywhere else this happens. Programmes with English sound are unchanged, and your setting still controls those.
- **The booked programme:** carries a "New on Vortex" or "First on Orbit" tag on each episode's first Orbit airing.
- **What's On, once a booking exists:**
- "Dragon Ball Super: Beerus: Saturday 17:00 on Vortex" in the week before;
- "New on Vortex tonight" on the day;
- "Episode 2 next Saturday" after each airing, only if that next booking exists.
- **No announcement** before a playable episode is booked. If a booking is lost, its promotion goes with it.
- **Idents:** the Vortex Prime ident package comes later. Until then it uses Vortex's standard presentation.
The alternative is turning the setting on for everything.
## Playback test: needs your choice
## Promotion and What's On
The compatibility check passed. That isn't the same as seeing it play. Orbit can only play booked programmes, so a test that doesn't touch bookings needs one of these:
- **Only once the event is booked:** "First on Orbit" and "New on Vortex" on the booked episodes. What's On reads that booking:
- "Saturday 21:00: the Vortex Afterburn Premiere of Dragon Ball Super: Beerus, episodes 1 and 2", in the week before;
- "Tonight on Vortex" on the day;
- "Next Saturday: episodes 3 and 4", only if that booking exists.
- **No assumed dates:** if a booking is lost, its promotion goes with it.
1. **A small debug-only release (recommended).** An intent extra plays one chosen episode once, in Orbit's normal player, then returns.
- It runs only when started by hand.
- It books nothing and is fully separate from scheduling.
- It has its own tests, and fingerprints must stay identical.
- We'd run it on episode 1 of copy A while the TV is idle. It would check picture, sound, the subtitles showing by default and how the ASS styling renders.
2. **The Emby app on the box.** Play episode 1 there yourself. This checks the file and subtitles, but not Orbit's player.
## Playback verification (0.99.15, awaiting install approval)
The compatibility check passed, but that isn't watched playback. 0.99.15 adds a hand-run test that plays episode 1 in Orbit's own player for 3 minutes. It checks:
- the picture;
- the Japanese audio;
- that English subtitles get selected;
- that subtitle lines are produced.
Whether they render well on screen needs your eyes during the test. See docs/VERIFICATION-0.99.15.md.
## What needs you
1. Approve copy 8016753 as the Beerus source.
2. Choose the slot: Saturday 17:00 Vortex Prime from 21 November (recommended), or Afterburn, or wait for a dub.
3. Choose whether to have the Sunday encore.
4. Choose the playback test: option 1 or 2.
Nothing is added to Vortex until you've approved all of this as a Stage D release.
1. Keep 21 November, the day after the original's Beerus saga ends, or start on 28 November or 5 December?
2. Movie Night on premiere nights: shorten it to one film ending by 21:00 (recommended), or move the premiere to 22:00?
3. Subtitles: on automatically for programmes without English sound (recommended), or turn the setting on for everything?
4. Install 0.99.15 and run the playback test with you watching.
@@ -0,0 +1,62 @@
--- 0.99.14
101 committed=ceabf650e4810201 kept=true fourWeeks=e18874a80beb3a55 programmes=2177
102 committed=8c81144d3b36e833 kept=true fourWeeks=9709a1ca2f5535ea programmes=1322
103 committed=7ffea57b7e1cb472 kept=true fourWeeks=647399c086fdb2e4 programmes=1822
104 committed=e126e9ffa3955107 kept=true fourWeeks=c76da7a762869ce3 programmes=1735
105 committed=6cade544765d2ea1 kept=true fourWeeks=018a087fb33a0c60 programmes=2557
106 committed=0288c16eee5a2421 kept=true fourWeeks=9bd2fb23de4d7feb programmes=1150
107 committed=760efbb2924e4202 kept=true fourWeeks=f5647f529049aab8 programmes=1011
201 committed=8285c8e353c30297 kept=true fourWeeks=1432d42080236f60 programmes=962
202 committed=e5d035e62d63daab kept=true fourWeeks=f4e034a1c71ac493 programmes=1132
203 committed=eda55bfc0c2a0322 kept=true fourWeeks=92598db1029e57a1 programmes=1138
204 committed=34d70788b01a7f2b kept=true fourWeeks=7075e49beb136fdb programmes=1262
301 committed=c52f04f716ebb7e6 kept=true fourWeeks=be7f588cb8f45b0b programmes=362
302 committed=c8682948ae9da547 kept=true fourWeeks=b52216995141a77c programmes=442
303 committed=69f9f7a1b0a600f7 kept=true fourWeeks=b78067d5fe62f8d5 programmes=419
304 committed=00ffa5e31b47af35 kept=true fourWeeks=80a6083310a0a39f programmes=493
305 committed=20bcd7b52807a154 kept=true fourWeeks=560742628cc8bdf3 programmes=503
306 committed=8f0834ad01578303 kept=true fourWeeks=72af90f39e52b7ec programmes=492
307 committed=ad3946c4d730d332 kept=true fourWeeks=c148014574309a44 programmes=441
308 committed=5cd79b96711429dc kept=true fourWeeks=3a1336770d917f73 programmes=514
309 committed=4a0bd1974ae8edb3 kept=true fourWeeks=10c076d0783c3c78 programmes=498
401 committed=af07a051503088f4 kept=true fourWeeks=0afb306085ac669e programmes=880
601 committed=7b828830b81e9e13 kept=true fourWeeks=ea118968a94a8ba9 programmes=2203
602 committed=ae97602ec57c90a7 kept=true fourWeeks=5a0bcc83098512df programmes=2337
603 committed=8f07d376dc34ec46 kept=true fourWeeks=869d0c295e7e7995 programmes=2316
604 committed=67f6f095d1ec3ba8 kept=true fourWeeks=71fca98d5f2d0f6f programmes=3253
605 committed=63d770ea8815b616 kept=true fourWeeks=b337a4f18b1f2c35 programmes=4508
611 committed=57b262c3da28ad26 kept=true fourWeeks=40412f13bc54bd40 programmes=2754
612 committed=8a5ace31156c9d43 kept=true fourWeeks=1675c47b4acc635e programmes=579
613 committed=d6164e2905c0082f kept=true fourWeeks=db55ae486c4998d1 programmes=521
614 committed=5c0ab156234c7836 kept=true fourWeeks=4c6f6d2a686b66ba programmes=3844
--- 0.99.15
101 committed=ceabf650e4810201 kept=true fourWeeks=e18874a80beb3a55 programmes=2177
102 committed=8c81144d3b36e833 kept=true fourWeeks=9709a1ca2f5535ea programmes=1322
103 committed=7ffea57b7e1cb472 kept=true fourWeeks=647399c086fdb2e4 programmes=1822
104 committed=e126e9ffa3955107 kept=true fourWeeks=c76da7a762869ce3 programmes=1735
105 committed=6cade544765d2ea1 kept=true fourWeeks=018a087fb33a0c60 programmes=2557
106 committed=0288c16eee5a2421 kept=true fourWeeks=9bd2fb23de4d7feb programmes=1150
107 committed=760efbb2924e4202 kept=true fourWeeks=f5647f529049aab8 programmes=1011
201 committed=8285c8e353c30297 kept=true fourWeeks=1432d42080236f60 programmes=962
202 committed=e5d035e62d63daab kept=true fourWeeks=f4e034a1c71ac493 programmes=1132
203 committed=eda55bfc0c2a0322 kept=true fourWeeks=92598db1029e57a1 programmes=1138
204 committed=34d70788b01a7f2b kept=true fourWeeks=7075e49beb136fdb programmes=1262
301 committed=c52f04f716ebb7e6 kept=true fourWeeks=be7f588cb8f45b0b programmes=362
302 committed=c8682948ae9da547 kept=true fourWeeks=b52216995141a77c programmes=442
303 committed=69f9f7a1b0a600f7 kept=true fourWeeks=b78067d5fe62f8d5 programmes=419
304 committed=00ffa5e31b47af35 kept=true fourWeeks=80a6083310a0a39f programmes=493
305 committed=20bcd7b52807a154 kept=true fourWeeks=560742628cc8bdf3 programmes=503
306 committed=8f0834ad01578303 kept=true fourWeeks=72af90f39e52b7ec programmes=492
307 committed=ad3946c4d730d332 kept=true fourWeeks=c148014574309a44 programmes=441
308 committed=5cd79b96711429dc kept=true fourWeeks=3a1336770d917f73 programmes=514
309 committed=4a0bd1974ae8edb3 kept=true fourWeeks=10c076d0783c3c78 programmes=498
401 committed=af07a051503088f4 kept=true fourWeeks=0afb306085ac669e programmes=880
601 committed=7b828830b81e9e13 kept=true fourWeeks=ea118968a94a8ba9 programmes=2203
602 committed=ae97602ec57c90a7 kept=true fourWeeks=5a0bcc83098512df programmes=2337
603 committed=8f07d376dc34ec46 kept=true fourWeeks=869d0c295e7e7995 programmes=2316
604 committed=67f6f095d1ec3ba8 kept=true fourWeeks=71fca98d5f2d0f6f programmes=3253
605 committed=63d770ea8815b616 kept=true fourWeeks=b337a4f18b1f2c35 programmes=4508
611 committed=57b262c3da28ad26 kept=true fourWeeks=40412f13bc54bd40 programmes=2754
612 committed=8a5ace31156c9d43 kept=true fourWeeks=1675c47b4acc635e programmes=579
613 committed=d6164e2905c0082f kept=true fourWeeks=db55ae486c4998d1 programmes=521
614 committed=5c0ab156234c7836 kept=true fourWeeks=4c6f6d2a686b66ba programmes=3844