0.81.1: Tots morning blink and the roll-in ident
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Orbit TV 0.81.1: Orbit Tots
|
||||
|
||||
Built, tested and linted. Not installed yet, so nothing below marked "on the box" has been checked.
|
||||
|
||||
## What's in it
|
||||
|
||||
- Orbit Tots on 614, with hand-approved shows: Morning 06:00 to 09:00, Playtime 09:00 to 17:00, Wind Down 17:00 to 21:00, Night 21:00 to 06:00.
|
||||
- Shuffled episodes, per channel. Only Tots uses it; every other channel stays in season order.
|
||||
- Hello, TOTS!, its roll-in variation and Sleepy TOTS, chosen by the next programme's start time. Jingle at most every 30 minutes, in Morning and Wind Down only.
|
||||
- A TOTS-only bug, a nightcap planet on the evening tuning screen, and still frames.
|
||||
- Channel logo, Idents and Ident sound switches under Watching.
|
||||
- Wind Down Now under Watching, with a confirmation. Its plan is saved apart from Tots' own and ends by the next 06:00.
|
||||
- Drive-In reminders held back while watching Tots from 17:00 to 06:00.
|
||||
|
||||
## Measured
|
||||
|
||||
- Unit tests: 306 passed, 0 failed (12 new in TotsTest). Lint passed.
|
||||
- APK 27,530,032 bytes, up about 1.07 MB. Tots assets 1,056,301 bytes against the 1.5 MB target.
|
||||
- Morning film 331,543 bytes, 5.0 s, -23.3 LUFS, peak -8.8 dBFS, average brightness 180 to 189 of 255, largest frame-to-frame change 0.51. Roll-in film 304,476 bytes, same jingle, brightness 180 to 190, largest change 0.81.
|
||||
- Evening film 339,386 bytes, 6.0 s, -30.0 LUFS, peak -17.7 dBFS, average brightness 44 to 94 of 255, largest frame-to-frame change 4.48, during its slow fades.
|
||||
|
||||
## Still to prove on the box
|
||||
|
||||
- The title list finds each show, and each has English episodes that play.
|
||||
- Morning and evening changeovers, with the jingle balanced against real programme sound.
|
||||
- Logo, ident and sound switches.
|
||||
- Wind Down Now on and off, including across a restart.
|
||||
|
||||
## Known limits
|
||||
|
||||
- No child profiles, PIN or lock yet, so a child can change channel from Tots. Paul accepted this for now; that work is next.
|
||||
- Wind Down Now's stop takes effect at once, after a confirmation. Its start waits for the current programme.
|
||||
- The jingle's 30 minute gap resets if the app restarts.
|
||||
- Fimbles isn't in the library. Rollback: reinstall 0.80.0; turning Tots off removes it from the guide.
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 91
|
||||
versionName = "0.81.0-orbit-tots"
|
||||
versionCode = 92
|
||||
versionName = "0.81.1-orbit-tots"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -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.81.0\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.81.1\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -38,7 +38,7 @@ private val IdentVideos = mapOf(101 to R.raw.ident_101, 102 to R.raw.ident_102,
|
||||
611 to R.raw.ident_611, 612 to R.raw.ident_612, 613 to R.raw.ident_613)
|
||||
|
||||
/** [atMs] is when the next programme starts, for a channel whose ident follows the time of day. */
|
||||
internal fun identVideo(number: Int, atMs: Long = 0) = if (number == TOTS_NUMBER) totsMood(atMs).video else IdentVideos[number]
|
||||
internal fun identVideo(number: Int, atMs: Long = 0) = if (number == TOTS_NUMBER) totsVideo(atMs) else IdentVideos[number]
|
||||
internal fun identLength(number: Int, atMs: Long = 0) = when (number) {
|
||||
TOTS_NUMBER -> totsMood(atMs).lengthMs
|
||||
in IdentVideos -> IDENT_VIDEO_MS
|
||||
|
||||
@@ -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.81.0")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.81.1")
|
||||
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()) {
|
||||
|
||||
@@ -24,6 +24,12 @@ internal enum class TotsMood(val video: Int, val still: Int, val lengthMs: Long,
|
||||
val evening get() = this == WindDown || this == Night
|
||||
}
|
||||
|
||||
/** Mornings take turns between the two films, by the programme's start, so the guide and player always agree. */
|
||||
internal fun totsVideo(atMs: Long, zone: java.time.ZoneId = java.time.ZoneId.systemDefault()): Int {
|
||||
val mood = totsMood(atMs, zone)
|
||||
return if (!mood.evening && (atMs / 60_000) % 2 == 1L) R.raw.ident_614_morning_roll else mood.video
|
||||
}
|
||||
|
||||
internal fun totsMood(atMs: Long, zone: java.time.ZoneId = java.time.ZoneId.systemDefault()) = when (java.time.Instant.ofEpochMilli(atMs).atZone(zone).hour) {
|
||||
in 6 until 9 -> TotsMood.Morning
|
||||
in 9 until 17 -> TotsMood.Playtime
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -37,7 +37,9 @@ class TotsTest {
|
||||
}
|
||||
|
||||
@Test fun identFollowsTheNextProgramme() {
|
||||
assertEquals(TotsMood.Morning.video, identVideo(TOTS_NUMBER, at(7)))
|
||||
assertEquals(R.raw.ident_614_morning, totsVideo(at(7), zone))
|
||||
assertEquals(R.raw.ident_614_morning_roll, totsVideo(at(7) + 60_000L, zone))
|
||||
assertEquals(R.raw.ident_614_evening, totsVideo(at(18) + 60_000L, zone))
|
||||
assertEquals(5_000L, identLength(TOTS_NUMBER, at(7)))
|
||||
assertEquals(6_000L, identLength(TOTS_NUMBER, at(18)))
|
||||
}
|
||||
|
||||
@@ -36,4 +36,5 @@ done <<'LIST'
|
||||
LIST
|
||||
# Tots keeps its jingles, and has a film for morning and evening. Built by orbit-tots/build-idents.py.
|
||||
cp "$D/tots/tots_morning.mp4" "$OUT/ident_614_morning.mp4"
|
||||
cp "$D/tots/tots_morning_roll.mp4" "$OUT/ident_614_morning_roll.mp4"
|
||||
cp "$D/tots/tots_evening.mp4" "$OUT/ident_614_evening.mp4"
|
||||
|
||||
@@ -30,20 +30,23 @@ def nightcap(x, y, r, sway=0.0, cloth='#A99BE8', brim='#EEE9FF', tip='#FFE08A'):
|
||||
+ stroke(f'M{p(-.76, -.62)} Q{p(0, -.92)} {p(.76, -.62)}', OUTLINE, r * .34) + stroke(f'M{p(-.76, -.62)} Q{p(0, -.92)} {p(.76, -.62)}', brim, r * .2)
|
||||
+ star(x + tx * r, y + ty * r, r * .2, tip))
|
||||
|
||||
def planet(x, y, r, colour, ring_colour=RING, eyes=1.0):
|
||||
ring = lambda arc: f'<path d="{arc}" fill="none" stroke="{OUTLINE}" stroke-width="12" stroke-linecap="round"/><path d="{arc}" fill="none" stroke="{ring_colour}" stroke-width="6" stroke-linecap="round"/>'
|
||||
# The ring sits low, under the smile.
|
||||
def ring(x, y, r, front, colour=RING, tilt=-12):
|
||||
# The ring sits low, under the smile. Drawn in two halves so the planet can sit inside it.
|
||||
rx, ry, low = r * 1.42, r * .22, y + r * .38
|
||||
back = f'M{x - rx} {low} A{rx} {ry} 0 0 1 {x + rx} {low}'
|
||||
front = f'M{x - rx} {low} A{rx} {ry} 0 0 0 {x + rx} {low}'
|
||||
arc = f'M{x - rx} {low} A{rx} {ry} 0 0 {0 if front else 1} {x + rx} {low}'
|
||||
return (f'<g transform="rotate({tilt} {x} {low})"><path d="{arc}" fill="none" stroke="{OUTLINE}" stroke-width="12" stroke-linecap="round"/>'
|
||||
f'<path d="{arc}" fill="none" stroke="{colour}" stroke-width="6" stroke-linecap="round"/></g>')
|
||||
|
||||
def ball(x, y, r, colour, eyes=1.0, turn=0.0):
|
||||
# Eyes close to a soft line for the sleepy blink.
|
||||
lid = max(.025, .17 * eyes)
|
||||
face = (f'<ellipse cx="{x - r * .34}" cy="{y - r * .42}" rx="{r * .12}" ry="{r * lid}" fill="{OUTLINE}"/>'
|
||||
return (f'<g transform="rotate({turn:.2f} {x} {y})"><circle cx="{x}" cy="{y}" r="{r}" fill="{colour}" stroke="{OUTLINE}" stroke-width="5"/>'
|
||||
f'<ellipse cx="{x - r * .34}" cy="{y - r * .42}" rx="{r * .12}" ry="{r * lid}" fill="{OUTLINE}"/>'
|
||||
f'<ellipse cx="{x + r * .34}" cy="{y - r * .42}" rx="{r * .12}" ry="{r * lid}" fill="{OUTLINE}"/>'
|
||||
f'<path d="M{x - r * .32} {y - r * .08} Q{x} {y + r * .32} {x + r * .32} {y - r * .08}" fill="none" stroke="{OUTLINE}" stroke-width="{r * .12}" stroke-linecap="round"/>')
|
||||
return (f'<g transform="rotate(-12 {x} {low})">{ring(back)}</g>'
|
||||
f'<circle cx="{x}" cy="{y}" r="{r}" fill="{colour}" stroke="{OUTLINE}" stroke-width="5"/>{face}'
|
||||
f'<g transform="rotate(-12 {x} {low})">{ring(front)}</g>')
|
||||
f'<path d="M{x - r * .32} {y - r * .08} Q{x} {y + r * .32} {x + r * .32} {y - r * .08}" fill="none" stroke="{OUTLINE}" stroke-width="{r * .12}" stroke-linecap="round"/></g>')
|
||||
|
||||
def planet(x, y, r, colour, ring_colour=RING, eyes=1.0):
|
||||
return ring(x, y, r, False, ring_colour) + ball(x, y, r, colour, eyes) + ring(x, y, r, True, ring_colour)
|
||||
|
||||
def ess(x, y, colour):
|
||||
return letter([f'M{x + 24} {y + 5} C{x + 10} {y - 8} {x - 24} {y - 6} {x - 22} {y + 13} C{x - 20} {y + 31} {x + 24} {y + 25} {x + 24} {y + 44} C{x + 24} {y + 63} {x - 14} {y + 64} {x - 26} {y + 48}'], colour)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The two Orbit Tots films: Hello, TOTS! and Sleepy TOTS. Soft flat illustration drawn from the logo's own shapes, one SVG a frame.
|
||||
build-idents.py <morning|evening> <out folder> [frame numbers for stills]
|
||||
"""The Orbit Tots films: Hello, TOTS!, its roll-in variation, and Sleepy TOTS. Soft flat illustration drawn from the logo's own shapes, one SVG a frame.
|
||||
build-idents.py <morning|roll|evening> <out folder> [frame numbers for stills]
|
||||
"""
|
||||
import concurrent.futures, importlib.util, math, os, random, subprocess, sys
|
||||
from pathlib import Path
|
||||
@@ -71,8 +71,11 @@ def page(body, defs):
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}"><defs>{brands["SILVER"]}{defs}</defs>{body}</svg>'
|
||||
|
||||
MORNING_BITS = Scatter(4, 16, ('#FFF4C2', '#FFFFFF', '#FFC2D1', '#CDBBF5', '#FFE08A'))
|
||||
def morning(frame):
|
||||
t = (frame - 1) / FPS
|
||||
def slow_blink(t, start):
|
||||
return (1 - ease(between(t, start, start + .28))) + ease(between(t, start + .4, start + .75))
|
||||
|
||||
def morning_sky(t):
|
||||
# The same morning for both films.
|
||||
defs = '<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5DBDF5"/><stop offset=".7" stop-color="#A9E2FB"/><stop offset="1" stop-color="#D6F3FB"/></linearGradient>'
|
||||
body = f'<rect width="{W}" height="{H}" fill="url(#sky)"/>'
|
||||
# The sunburst, turning slowly behind everything.
|
||||
@@ -85,6 +88,11 @@ def morning(frame):
|
||||
# Soft hills along the foot.
|
||||
body += f'<path d="M0 640 Q320 560 640 636 T1280 610 V720 H0 Z" fill="#8FE07E"/><path d="M0 680 Q300 626 720 682 T1280 660 V720 H0 Z" fill="#6FCF6A"/>'
|
||||
body += MORNING_BITS.draw(t)
|
||||
return body, defs
|
||||
|
||||
def morning(frame):
|
||||
t = (frame - 1) / FPS
|
||||
body, defs = morning_sky(t)
|
||||
# Mark first, so the letters sit over its ring.
|
||||
body += mark_layer(tots.SUN, between(t, 2.6, 3.4), ease(between(t, 2.5, 3.1)))
|
||||
for name, colour_body, start in (('t1', tots.tee(*FINAL['t1'], tots.CORAL, -5), 1.05), ('t2', tots.tee(*FINAL['t2'], tots.SKY, 4), 1.4), ('s', tots.ess(*FINAL['s'], tots.LEAF), 1.75)):
|
||||
@@ -101,7 +109,46 @@ def morning(frame):
|
||||
u = ease(between(t, 1.5, 2.6))
|
||||
x, y = bezier([(330, 320), (470, 90), (820, 110), (fx, fy)], u); size = 1.5 - .5 * u
|
||||
tilt = 10 * math.sin(t * 2.4) * (1 - ease(between(t, 2.2, 3.0)))
|
||||
body += f'<g transform="translate({x:.2f},{y:.2f}) rotate({tilt:.2f}) scale({ZOOM * size:.4f}) translate({-FINAL["o"][0]},{-FINAL["o"][1]})">' + tots.planet(*FINAL['o'], 36, tots.SUN) + '</g>'
|
||||
body += f'<g transform="translate({x:.2f},{y:.2f}) rotate({tilt:.2f}) scale({ZOOM * size:.4f}) translate({-FINAL["o"][0]},{-FINAL["o"][1]})">' + tots.planet(*FINAL['o'], 36, tots.SUN, eyes=slow_blink(t, 3.75)) + '</g>'
|
||||
return page(body, defs)
|
||||
|
||||
def roll_in(frame):
|
||||
# The letters are waiting, with the ring held open between the two Ts. The planet rolls along the ground and jumps in.
|
||||
t = (frame - 1) / FPS
|
||||
body, defs = morning_sky(t)
|
||||
body += mark_layer(tots.SUN, between(t, 2.7, 3.5), ease(between(t, 2.6, 3.2)))
|
||||
for name, colour_body, start in (('t1', tots.tee(*FINAL['t1'], tots.CORAL, -5), .05), ('t2', tots.tee(*FINAL['t2'], tots.SKY, 4), .25), ('s', tots.ess(*FINAL['s'], tots.LEAF), .45)):
|
||||
u = between(t, start, start + .6)
|
||||
if u > 0:
|
||||
body += letter(colour_body, name, 110 * (1 - settle(u)), .7 + .3 * settle(u), ease(u * 3.5))
|
||||
fx, fy = screen(*FINAL['o'])
|
||||
ground, land, leap = 582, 2.5, 1.9
|
||||
# A nudge as it lands, settling back, so the ring feels caught.
|
||||
shake = 11 * math.sin(between(t, land, land + .7) * math.tau * 1.5) * (1 - clamp(between(t, land, land + .7))) if t > land else 0
|
||||
open_ring = ease(between(t, .55, 1.0))
|
||||
ring_at = f'translate({fx:.2f},{fy:.2f}) scale({ZOOM * (.6 + .4 * settle(between(t, .55, 1.1))):.4f}) translate({-FINAL["o"][0]},{-FINAL["o"][1]})'
|
||||
behind = f'<g opacity="{open_ring:.3f}" transform="{ring_at}">' + tots.ring(*FINAL['o'], 36, False, tilt=-12 + shake) + '</g>'
|
||||
front = f'<g opacity="{open_ring:.3f}" transform="{ring_at}">' + tots.ring(*FINAL['o'], 36, True, tilt=-12 + shake) + '</g>'
|
||||
squash, stretch = 0, 0
|
||||
if t < leap - .22:
|
||||
u = between(t, .55, leap - .22)
|
||||
eased = 1 - (1 - clamp(u)) ** 1.6
|
||||
x, y, turn = -110 + (fx - 110 + 110) * eased, ground, 360 * eased
|
||||
elif t < leap:
|
||||
# Crouches ready to jump.
|
||||
x, y, turn, squash = fx - 110 * 0 - 0, ground, 360, math.sin(math.pi * between(t, leap - .22, leap)) * .16
|
||||
elif t < land:
|
||||
u = between(t, leap, land)
|
||||
x, y, turn = fx, ground + (fy - ground) * u - 250 * 4 * u * (1 - u) * .62, 360
|
||||
stretch = .07 * math.sin(math.pi * u)
|
||||
else:
|
||||
x, y, turn = fx, fy, 360
|
||||
squash = .09 * math.sin(math.pi * clamp(between(t, land, land + .3)))
|
||||
sx, sy = 1 + squash - stretch, 1 - squash + stretch
|
||||
drawn = (f'<g transform="translate({x:.2f},{y:.2f}) scale({ZOOM * sx:.4f},{ZOOM * sy:.4f}) translate({-FINAL["o"][0]},{-FINAL["o"][1]})">'
|
||||
+ tots.ball(*FINAL['o'], 36, tots.SUN, eyes=slow_blink(t, 3.75), turn=turn) + '</g>')
|
||||
# The ring is only in front once the planet has come down through it.
|
||||
body += behind + drawn + front if t >= land - .12 else behind + front + drawn
|
||||
return page(body, defs)
|
||||
|
||||
EVENING_STARS = Scatter(9, 34, ('#FFE9A8', '#EDE6FF', '#FFD9E4'), (20, 500), speed=3)
|
||||
@@ -149,7 +196,7 @@ def render(job):
|
||||
|
||||
if __name__ == '__main__':
|
||||
which, folder = sys.argv[1], Path(sys.argv[2]); folder.mkdir(parents=True, exist_ok=True)
|
||||
draw, count = (morning, 150) if which == 'morning' else (evening, 180)
|
||||
draw, count = {'morning': (morning, 150), 'roll': (roll_in, 150), 'evening': (evening, 180)}[which]
|
||||
frames = [int(f) for f in sys.argv[3:]] or list(range(1, count + 1))
|
||||
with concurrent.futures.ThreadPoolExecutor(6) as pool:
|
||||
list(pool.map(render, [(draw, frame, folder) for frame in frames]))
|
||||
|
||||
Reference in New Issue
Block a user