fix: [vo/gpu/libplacebo] vk->CreateSwapchainKHR(...): VK_ERROR_OUT_OF_HOST_MEMORY [vo/gpu/libplacebo] Failed (re)creating swapchain!
R
Rüdiger Küpper
<rpr@9it.de>
committete am 21.08.2026 02:33
2f4d870b5ab7c049d585aecd6833463f5366d04d
gelöscht
EHC-Dienstag.jpeg
+0
−0
Binärdatei – kein Textvergleich möglich.
gelöscht
EHC-Montag.jpeg
+0
−0
Binärdatei – kein Textvergleich möglich.
gelöscht
EHC-Woche.jpeg
+0
−0
Binärdatei – kein Textvergleich möglich.
| @@ -85,7 +85,7 @@ sudo systemctl restart tvwall | ||
| 85 | 85 | | `SCAN_INTERVAL` | Takt, in dem der Ordner neu eingelesen wird | |
| 86 | 86 | | `DISPLAY_MODE` | `auto` (empfohlen), `drm` oder `desktop` | |
| 87 | 87 | | `RUN_USER` | Benutzer des Dienstes | |
| 88 | -| `FRESH_MPV_FOR_VIDEO` | `yes` startet mpv vor jedem Video neu (umgeht den `partial file`-Abbruch) | |
|
| 88 | +| `MPV_GPU_API` | `opengl` (Standard) oder `vulkan`/`auto` — siehe Fehlersuche | |
|
| 89 | 89 | | `MPV_HWDEC` | `no` (Standard, Software) oder `auto-safe` (Hardware, nur Pi 4) | |
| 90 | 90 | | `STALL_TIMEOUT` | Sekunden ohne Fortschritt, bevor ein Video übersprungen wird | |
| 91 | 91 | | `MPV_EXTRA_ARGS` | zusätzliche mpv-Optionen, z. B. `--video-rotate=90` | |
| @@ -241,13 +241,32 @@ abschalten. Zweithäufigste Ursache: fehlender Ländercode (`AP_COUNTRY`). | ||
| 241 | 241 | Zuerst `http://192.168.4.1` probieren — Namen wie `tvwall.local` funktionieren |
| 242 | 242 | nicht auf allen Handys. Läuft der Dienst? `systemctl status tvwall`. |
| 243 | 243 | |
| 244 | -**Ein Video bleibt nach ein paar Sekunden stehen.** |
|
| 245 | -Seit Version 1.0.0 wird jede Datei einzeln an mpv übergeben; der früher |
|
| 246 | -beobachtete Abbruch mit `partial file` beim Übergang Bild → Video ist damit |
|
| 247 | -erledigt. Tritt trotzdem etwas auf, misst `scripts/bisect-mpv.sh` jede Variante |
|
| 248 | -einzeln durch. |
|
| 244 | +**Das Bild bleibt nach einigen Wechseln stehen, obwohl das Webinterface |
|
| 245 | +weiterzählt.** |
|
| 246 | +Das ist der Grund für die Voreinstellung `MPV_GPU_API="opengl"`. Über Vulkan |
|
| 247 | +baut libplacebo bei jedem Bildwechsel die Swapchain neu auf und läuft nach rund |
|
| 248 | +neun Wechseln in `VK_ERROR_OUT_OF_HOST_MEMORY`. mpv läuft dann weiter und meldet |
|
| 249 | +auch weiter Dateiwechsel — nur gemalt wird nichts mehr. Im Journal steht: |
|
| 250 | + | |
| 251 | +``` |
|
| 252 | +[vo/gpu/libplacebo] vk->CreateSwapchainKHR(...): VK_ERROR_OUT_OF_HOST_MEMORY |
|
| 253 | +[vo/gpu/libplacebo] Failed (re)creating swapchain! |
|
| 254 | +``` |
|
| 255 | + | |
| 256 | +Nachgemessen auf einem Pi 5 (Wayland/labwc, mpv 0.40), 20 Bildwechsel: |
|
| 249 | 257 | |
| 250 | -Zuerst `MPV_HWDEC="no"` prüfen (seit Version 1.0.0 die Voreinstellung). Der |
|
| 258 | +| Renderer | Swapchain-Fehler | |
|
| 259 | +|---|---| |
|
| 260 | +| `--vo=gpu` (Vulkan) | 24 | |
|
| 261 | +| `--vo=gpu --gpu-api=opengl` | **0** | |
|
| 262 | +| `--vo=gpu-next` (Vulkan) | 84 | |
|
| 263 | + | |
| 264 | +Dasselbe galt für Videos: Mit Vulkan brach die Wiedergabe nach ein bis zwei |
|
| 265 | +Sekunden ab, mit OpenGL lief derselbe Clip vollständig durch. `diagnose.sh` |
|
| 266 | +prüft das mit. |
|
| 267 | + | |
| 268 | +**Ein Video bleibt nach ein paar Sekunden stehen.** |
|
| 269 | +Zuerst `MPV_GPU_API="opengl"` prüfen (siehe oben), dann `MPV_HWDEC="no"`. Der |
|
| 251 | 270 | Raspberry Pi 5 hat *keinen* Hardware-Dekoder für H.264 mehr; mit `auto-safe` |
| 252 | 271 | läuft ein Video dort ein bis zwei Sekunden und friert dann ein — genau so lange, |
| 253 | 272 | wie der bereits gefüllte Puffer reicht. Zum Gegenprüfen hilft der direkte |
| @@ -305,8 +324,9 @@ systemd/ Vorlagen für den Dienst (drm / desktop) | ||
| 305 | 324 | internal/player/display.go Wayland-/X11-/DRM-Erkennung |
| 306 | 325 | scripts/setup-ap.sh Access Point per nmcli |
| 307 | 326 | scripts/diagnose.sh Selbsttest bei Problemen |
| 327 | +scripts/console-mode.sh Desktop abschalten, direkt in die Anzeige booten |
|
| 328 | +scripts/set-splash.sh eigenes Logo als Startbildschirm setzen |
|
| 308 | 329 | scripts/bisect-mpv.sh misst, welche mpv-Option einen Haenger ausloest |
| 309 | -scripts/probe-transition.py prueft den Uebergang Bild -> Video im selben mpv |
|
| 310 | 330 | install.sh Installation auf dem Pi |
| 311 | 331 | ``` |
| 312 | 332 | |
| @@ -320,6 +340,10 @@ Ein paar Entscheidungen, die vielleicht nicht offensichtlich sind: | ||
| 320 | 340 | * **Die Oberfläche kommt ohne externe Bibliotheken aus** und ist ins Programm |
| 321 | 341 | eingebettet. Am Access Point gibt es kein Internet — alles, was von einem CDN |
| 322 | 342 | nachgeladen werden müsste, wäre dort tot. |
| 343 | +* **mpv rendert über OpenGL, nicht über Vulkan.** Auf dem Raspberry Pi 5 baut |
|
| 344 | +die Vulkan-Anbindung von libplacebo bei jedem Bildwechsel die Swapchain neu |
|
| 345 | +auf und läuft dabei nach rund neun Wechseln aus dem Speicher — der Bildschirm |
|
| 346 | +friert ein, während intern alles weiterläuft. Siehe Fehlersuche. |
|
| 323 | 347 | * **Uploads landen erst in einer temporären Datei** und werden nach vollem |
| 324 | 348 | Empfang umbenannt. So gerät keine halbe Datei in die Wiedergabe. |
| 325 | 349 |
| @@ -76,6 +76,15 @@ DISPLAY_MODE="auto" | ||
| 76 | 76 | # Bei DISPLAY_MODE="auto" erkennt install.sh das und traegt es hier ein. |
| 77 | 77 | RUN_USER="tvwall" |
| 78 | 78 | |
| 79 | +# Grafik-Anbindung von mpv. |
|
| 80 | +# opengl = Standard und dringend empfohlen |
|
| 81 | +# vulkan = schneller, aber auf dem Raspberry Pi 5 fehlerhaft: libplacebo baut |
|
| 82 | +# bei jedem Bildwechsel die Swapchain neu auf und laeuft nach rund |
|
| 83 | +# neun Wechseln in VK_ERROR_OUT_OF_HOST_MEMORY - mpv laeuft dann |
|
| 84 | +# weiter, der Bildschirm friert ein. |
|
| 85 | +# auto = mpv entscheidet (waehlt auf dem Pi 5 Vulkan - nicht empfohlen) |
|
| 86 | +MPV_GPU_API="opengl" |
|
| 87 | + | |
| 79 | 88 | # Hardwarebeschleunigte Videodekodierung. |
| 80 | 89 | # no = alles in Software dekodieren (Standard, mpvs eigene Vorgabe) |
| 81 | 90 | # auto-safe = Hardware benutzen, wenn moeglich |
| @@ -84,13 +93,6 @@ RUN_USER="tvwall" | ||
| 84 | 93 | # kann "auto-safe" dagegen Rechenzeit sparen. |
| 85 | 94 | MPV_HWDEC="no" |
| 86 | 95 | |
| 87 | -# mpv vor jedem Video frisch starten. |
|
| 88 | -# Auf manchen Systemen bricht ein Video ab, wenn mpv vorher schon ein Bild |
|
| 89 | -# angezeigt hat ("partial file" im Protokoll). Ein frischer Prozess je Video |
|
| 90 | -# umgeht das; der Preis ist ein kurzer schwarzer Moment davor. |
|
| 91 | -# Mit scripts/probe-transition.py laesst sich pruefen, ob "no" reicht. |
|
| 92 | -FRESH_MPV_FOR_VIDEO="yes" |
|
| 93 | - | |
| 94 | 96 | # Laeuft ein Video so lange nicht weiter (Sekunden), wird es uebersprungen, |
| 95 | 97 | # damit eine einzelne Datei nicht die ganze Wiedergabe anhaelt. 0 = aus. |
| 96 | 98 | STALL_TIMEOUT="30" |
Binärdatei – kein Textvergleich möglich.
| @@ -37,6 +37,7 @@ type Config struct { | ||
| 37 | 37 | RunUser string |
| 38 | 38 | MpvExtraArgs string |
| 39 | 39 | MpvHwdec string |
| 40 | +MpvGpuAPI string |
|
| 40 | 41 | FreshForVideo bool // mpv vor jedem Video neu starten |
| 41 | 42 | StallTimeout int // Sekunden ohne Fortschritt, bevor ein Video uebersprungen wird |
| 42 | 43 | |
| @@ -60,7 +61,7 @@ func Default() *Config { | ||
| 60 | 61 | ScanInterval: 10, |
| 61 | 62 | DisplayMode: "auto", |
| 62 | 63 | MpvHwdec: "no", |
| 63 | -FreshForVideo: true, |
|
| 64 | +MpvGpuAPI: "opengl", |
|
| 64 | 65 | StallTimeout: 30, |
| 65 | 66 | RunUser: "tvwall", |
| 66 | 67 | StateDir: "/var/lib/tvwall", |
| @@ -148,8 +149,10 @@ func Load(path string) (*Config, error) { | ||
| 148 | 149 | c.RunUser = val |
| 149 | 150 | case "MPV_EXTRA_ARGS": |
| 150 | 151 | c.MpvExtraArgs = val |
| 151 | -case "FRESH_MPV_FOR_VIDEO": |
|
| 152 | -c.FreshForVideo = isTrue(val) |
|
| 152 | +case "MPV_GPU_API": |
|
| 153 | + if val != "" { |
|
| 154 | +c.MpvGpuAPI = val |
|
| 155 | +} |
|
| 153 | 156 | case "MPV_HWDEC": |
| 154 | 157 | if val != "" { |
| 155 | 158 | c.MpvHwdec = val |
| @@ -1,18 +1,14 @@ | ||
| 1 | -// Package player startet und ueberwacht mpv und steuert die Wiedergabe. |
|
| 1 | +// Package player startet und ueberwacht mpv und haelt dessen Playliste aktuell. |
|
| 2 | 2 | // |
| 3 | -// mpv bekommt bewusst KEINE Playliste, sondern immer nur die eine Datei, die |
|
| 4 | -// gerade laufen soll (loadfile ... replace). Die Reihenfolge fuehrt dieses |
|
| 5 | -// Programm selbst. Grund: eine gemischte Playliste aus Bildern und Videos |
|
| 6 | -// bringt den Demuxer durcheinander - eine Videodatei, die allein einwandfrei |
|
| 7 | -// laeuft, bricht als zweiter Eintrag nach kurzer Zeit mit "partial file" ab. |
|
| 8 | -// Als jeweils einzige geladene Datei tritt das nicht auf; nebenbei haben wir |
|
| 9 | -// so die volle Kontrolle ueber Reihenfolge, Weiterschalten und Fehlerfaelle. |
|
| 3 | +// mpv bekommt die komplette Playliste (Bilder und Videos gemischt) und schaltet |
|
| 4 | +// selbst weiter; dieses Programm laedt sie nur nach, wenn sich der Medienordner |
|
| 5 | +// geaendert hat - und zwar zwischen zwei Dateien, damit kein laufendes Video |
|
| 6 | +// unterbrochen wird. |
|
| 10 | 7 | package player |
| 11 | 8 | |
| 12 | 9 | import ( |
| 13 | 10 | "context" |
| 14 | 11 | "encoding/json" |
| 15 | -"errors" |
|
| 16 | 12 | "fmt" |
| 17 | 13 | "log" |
| 18 | 14 | "os" |
| @@ -32,20 +28,13 @@ import ( | ||
| 32 | 28 | const ( |
| 33 | 29 | propPath = iota + 1 |
| 34 | 30 | propPause |
| 31 | +propPlaylistPos |
|
| 35 | 32 | propIdle |
| 36 | 33 | propTimePos |
| 37 | 34 | ) |
| 38 | 35 | |
| 39 | -// Takt der Ueberwachung: haengende Dateien und leerer Bildschirm. |
|
| 40 | 36 | const tickInterval = 5 * time.Second |
| 41 | 37 | |
| 42 | -// Wartezeit, bevor nach lauter fehlerhaften Dateien erneut versucht wird. |
|
| 43 | -const retryDelay = 15 * time.Second |
|
| 44 | - | |
| 45 | -// errFreshMpv ist kein Fehler, sondern der Wunsch nach einem frischen |
|
| 46 | -// mpv-Prozess fuer die naechste Datei. Siehe FRESH_MPV_FOR_VIDEO. |
|
| 47 | -var errFreshMpv = errors.New("frischer mpv-prozess fuer das naechste video") |
|
| 48 | - | |
| 49 | 38 | // Status beschreibt den Zustand der Wiedergabe fuer das Webinterface. |
| 50 | 39 | type Status struct { |
| 51 | 40 | Running bool `json:"running"` |
| @@ -58,27 +47,21 @@ type Status struct { | ||
| 58 | 47 | LastError string `json:"last_error"` |
| 59 | 48 | } |
| 60 | 49 | |
| 61 | -// Player haelt genau einen mpv-Prozess am Leben und schaltet die Dateien weiter. |
|
| 50 | +// Player haelt genau einen mpv-Prozess am Leben. |
|
| 62 | 51 | type Player struct { |
| 63 | 52 | cfg *config.Config |
| 64 | 53 | pl *playlist.Manager |
| 65 | 54 | settings *config.Store |
| 66 | 55 | |
| 67 | -mu sync.RWMutex |
|
| 68 | -client *mpv.Client |
|
| 69 | -status Status |
|
| 70 | - | |
| 71 | -playing string // Datei, die wir mpv zuletzt uebergeben haben |
|
| 72 | -index int // deren Platz in der Playliste |
|
| 73 | -errStreak int // Dateien in Folge, die sich nicht abspielen liessen |
|
| 74 | -lastAttempt time.Time // wann zuletzt eine Datei geladen wurde |
|
| 56 | +mu sync.RWMutex |
|
| 57 | +client *mpv.Client |
|
| 58 | +status Status |
|
| 59 | +pending bool |
|
| 60 | +loadedID uint64 // Version der zuletzt an mpv uebergebenen Playliste |
|
| 75 | 61 | |
| 76 | 62 | lastTimePos float64 |
| 77 | 63 | lastProgressAt time.Time |
| 78 | 64 | |
| 79 | -loadedCount int // Dateien, die dieser mpv-Prozess schon geladen hat |
|
| 80 | - | |
| 81 | -advance chan int |
|
| 82 | 65 | reloadNow chan struct{} |
| 83 | 66 | restart chan struct{} |
| 84 | 67 | } |
| @@ -89,7 +72,6 @@ func New(cfg *config.Config, pl *playlist.Manager, settings *config.Store) *Play | ||
| 89 | 72 | cfg: cfg, |
| 90 | 73 | pl: pl, |
| 91 | 74 | settings: settings, |
| 92 | -advance: make(chan int, 4), |
|
| 93 | 75 | reloadNow: make(chan struct{}, 1), |
| 94 | 76 | restart: make(chan struct{}, 1), |
| 95 | 77 | } |
| @@ -99,7 +81,9 @@ func New(cfg *config.Config, pl *playlist.Manager, settings *config.Store) *Play | ||
| 99 | 81 | func (p *Player) Status() Status { |
| 100 | 82 | p.mu.RLock() |
| 101 | 83 | defer p.mu.RUnlock() |
| 102 | -return p.status |
|
| 84 | +s := p.status |
|
| 85 | +s.PendingReload = p.pending |
|
| 86 | +return s |
|
| 103 | 87 | } |
| 104 | 88 | |
| 105 | 89 | // Run laeuft bis ctx abgebrochen wird und startet mpv bei Bedarf neu. |
| @@ -108,19 +92,14 @@ func (p *Player) Run(ctx context.Context) { | ||
| 108 | 92 | backoff := time.Second |
| 109 | 93 | for ctx.Err() == nil { |
| 110 | 94 | start := time.Now() |
| 111 | -err := p.runOnce(ctx, settingsChanged) |
|
| 95 | + if err := p.runOnce(ctx, settingsChanged); err != nil && ctx.Err() == nil { |
|
| 96 | + log.Printf("player: %v", err) |
|
| 97 | +p.setError(err.Error()) |
|
| 98 | +} |
|
| 112 | 99 | p.setRunning(false) |
| 113 | 100 | if ctx.Err() != nil { |
| 114 | 101 | return |
| 115 | 102 | } |
| 116 | - if errors.Is(err, errFreshMpv) { |
|
| 117 | -// Geplanter, kurzer Neustart - kein Grund zu warten. |
|
| 118 | -continue |
|
| 119 | -} |
|
| 120 | - if err != nil { |
|
| 121 | - log.Printf("player: %v", err) |
|
| 122 | -p.setError(err.Error()) |
|
| 123 | -} |
|
| 124 | 103 | if time.Since(start) > time.Minute { |
| 125 | 104 | backoff = time.Second |
| 126 | 105 | } else if backoff < 15*time.Second { |
| @@ -135,8 +114,6 @@ func (p *Player) Run(ctx context.Context) { | ||
| 135 | 114 | } |
| 136 | 115 | } |
| 137 | 116 | |
| 138 | -// runOnce startet mpv, verbindet sich mit dem IPC-Socket und bleibt, bis mpv |
|
| 139 | -// endet oder ctx abgebrochen wird. |
|
| 140 | 117 | func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) error { |
| 141 | 118 | disp, err := resolveDisplay(p.cfg.DisplayMode) |
| 142 | 119 | if err != nil { |
| @@ -154,8 +131,8 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 154 | 131 | log.Printf("player: starte mpv %s", strings.Join(args, " ")) |
| 155 | 132 | |
| 156 | 133 | cmd := exec.Command("mpv", args...) |
| 157 | -// stdin durchreichen: bei --vo=gpu --gpu-context=drm braucht mpv das |
|
| 158 | -// Terminal der Konsole (tty1), um den Bildschirm uebernehmen zu duerfen. |
|
| 134 | +// stdin durchreichen: bei --gpu-context=drm braucht mpv das Terminal der |
|
| 135 | +// Konsole, um den Bildschirm uebernehmen zu duerfen. |
|
| 159 | 136 | cmd.Stdin = os.Stdin |
| 160 | 137 | cmd.Stdout = os.Stdout |
| 161 | 138 | cmd.Stderr = os.Stderr |
| @@ -170,7 +147,6 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 170 | 147 | waitErr = cmd.Wait() |
| 171 | 148 | close(exited) |
| 172 | 149 | }() |
| 173 | - | |
| 174 | 150 | defer func() { |
| 175 | 151 | if cmd.Process != nil { |
| 176 | 152 | _ = cmd.Process.Kill() |
| @@ -188,9 +164,8 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 188 | 164 | p.mu.Lock() |
| 189 | 165 | p.client = client |
| 190 | 166 | p.status = Status{Running: true, Display: disp.mode} |
| 191 | -p.index = 0 |
|
| 192 | -p.errStreak = 0 |
|
| 193 | -p.loadedCount = 0 |
|
| 167 | +p.lastTimePos = -1 |
|
| 168 | +p.lastProgressAt = time.Now() |
|
| 194 | 169 | p.mu.Unlock() |
| 195 | 170 | defer func() { |
| 196 | 171 | p.mu.Lock() |
| @@ -199,7 +174,7 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 199 | 174 | }() |
| 200 | 175 | |
| 201 | 176 | for id, name := range map[int]string{ |
| 202 | -propPath: "path", propPause: "pause", |
|
| 177 | +propPath: "path", propPause: "pause", propPlaylistPos: "playlist-pos", |
|
| 203 | 178 | propIdle: "idle-active", propTimePos: "time-pos", |
| 204 | 179 | } { |
| 205 | 180 | if err := client.ObserveProperty(id, name); err != nil { |
| @@ -208,9 +183,7 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 208 | 183 | } |
| 209 | 184 | |
| 210 | 185 | p.applySettings(client) |
| 211 | - if err := p.step(client, 0); err != nil { // erste Datei laden |
|
| 212 | -return err |
|
| 213 | -} |
|
| 186 | +p.loadPlaylist(client) |
|
| 214 | 187 | p.setError("") |
| 215 | 188 | |
| 216 | 189 | ticker := time.NewTicker(tickInterval) |
| @@ -233,33 +206,18 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 233 | 206 | } |
| 234 | 207 | |
| 235 | 208 | case <-p.pl.Changed(): |
| 236 | -// Aenderungen greifen von selbst beim naechsten Dateiwechsel, weil |
|
| 237 | -// dort die Playliste neu gelesen wird. Nur wenn gerade nichts |
|
| 238 | -// laeuft, muss die Wiedergabe angestossen werden. |
|
| 239 | -p.mu.RLock() |
|
| 240 | -idleNow := p.playing == "" |
|
| 241 | -p.mu.RUnlock() |
|
| 242 | - if idleNow { |
|
| 243 | - if err := p.step(client, 0); err != nil { |
|
| 244 | -return err |
|
| 245 | -} |
|
| 246 | -} |
|
| 247 | - | |
| 248 | -case delta := <-p.advance: |
|
| 249 | - if err := p.step(client, delta); err != nil { |
|
| 250 | -return err |
|
| 251 | -} |
|
| 252 | - | |
| 253 | -case <-p.reloadNow: |
|
| 209 | +// Nicht mitten in einer Datei neu laden - erst am Uebergang. |
|
| 254 | 210 | p.mu.Lock() |
| 255 | -p.playing = "" |
|
| 256 | -p.index = 0 |
|
| 257 | -p.errStreak = 0 |
|
| 211 | +p.pending = p.pl.Version() != p.loadedID |
|
| 212 | +pending, idle := p.pending, p.status.Idle |
|
| 258 | 213 | p.mu.Unlock() |
| 259 | - if err := p.step(client, 0); err != nil { |
|
| 260 | -return err |
|
| 214 | + if pending && idle { |
|
| 215 | +p.loadPlaylist(client) |
|
| 261 | 216 | } |
| 262 | 217 | |
| 218 | +case <-p.reloadNow: |
|
| 219 | +p.loadPlaylist(client) |
|
| 220 | + | |
| 263 | 221 | case <-p.restart: |
| 264 | 222 | return fmt.Errorf("neustart angefordert") |
| 265 | 223 | |
| @@ -267,22 +225,18 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 267 | 225 | p.applySettings(client) |
| 268 | 226 | |
| 269 | 227 | case <-ticker.C: |
| 270 | - if err := p.tick(client); err != nil { |
|
| 271 | -return err |
|
| 272 | -} |
|
| 228 | +p.checkStalled(client) |
|
| 273 | 229 | |
| 274 | 230 | case ev, ok := <-client.Events(): |
| 275 | 231 | if !ok { |
| 276 | 232 | return fmt.Errorf("ipc-verbindung beendet") |
| 277 | 233 | } |
| 278 | - if err := p.handleEvent(client, ev); err != nil { |
|
| 279 | -return err |
|
| 280 | -} |
|
| 234 | +p.handleEvent(client, ev) |
|
| 281 | 235 | } |
| 282 | 236 | } |
| 283 | 237 | } |
| 284 | 238 | |
| 285 | -func (p *Player) handleEvent(client *mpv.Client, ev mpv.Event) error { |
|
| 239 | +func (p *Player) handleEvent(client *mpv.Client, ev mpv.Event) { |
|
| 286 | 240 | switch ev.Name { |
| 287 | 241 | case "property-change": |
| 288 | 242 | p.mu.Lock() |
| @@ -291,8 +245,6 @@ func (p *Player) handleEvent(client *mpv.Client, ev mpv.Event) error { | ||
| 291 | 245 | var s string |
| 292 | 246 | if json.Unmarshal(ev.Data, &s) == nil && s != "" { |
| 293 | 247 | p.status.Current = filepath.Base(s) |
| 294 | -// Eine Datei laeuft wieder - die Fehlerkette ist unterbrochen. |
|
| 295 | -p.errStreak = 0 |
|
| 296 | 248 | } else { |
| 297 | 249 | p.status.Current = "" |
| 298 | 250 | } |
| @@ -303,6 +255,11 @@ func (p *Player) handleEvent(client *mpv.Client, ev mpv.Event) error { | ||
| 303 | 255 | if json.Unmarshal(ev.Data, &b) == nil { |
| 304 | 256 | p.status.Paused = b |
| 305 | 257 | } |
| 258 | +case propPlaylistPos: |
|
| 259 | +var n float64 |
|
| 260 | + if json.Unmarshal(ev.Data, &n) == nil { |
|
| 261 | +p.status.Position = int(n) |
|
| 262 | +} |
|
| 306 | 263 | case propIdle: |
| 307 | 264 | var b bool |
| 308 | 265 | if json.Unmarshal(ev.Data, &b) == nil { |
| @@ -315,150 +272,72 @@ func (p *Player) handleEvent(client *mpv.Client, ev mpv.Event) error { | ||
| 315 | 272 | p.lastProgressAt = time.Now() |
| 316 | 273 | } |
| 317 | 274 | } |
| 275 | +idle, pending := p.status.Idle, p.pending |
|
| 318 | 276 | p.mu.Unlock() |
| 319 | 277 | |
| 320 | -case "end-file": |
|
| 321 | -// "stop" kommt von unserem eigenen loadfile - das darf nicht |
|
| 322 | -// weiterschalten, sonst wird jede zweite Datei uebersprungen. |
|
| 323 | - var e struct { |
|
| 324 | -Reason string `json:"reason"` |
|
| 325 | -FileError string `json:"file_error"` |
|
| 278 | + if ev.ID == propIdle && idle && pending { |
|
| 279 | +p.loadPlaylist(client) |
|
| 326 | 280 | } |
| 327 | -_ = json.Unmarshal(ev.Raw, &e) |
|
| 328 | - switch e.Reason { |
|
| 329 | -case "eof": |
|
| 330 | -return p.step(client, 1) |
|
| 331 | -case "error": |
|
| 332 | -p.mu.Lock() |
|
| 333 | -bad := p.playing |
|
| 334 | -p.errStreak++ |
|
| 335 | -p.mu.Unlock() |
|
| 336 | - log.Printf("player: %q liess sich nicht abspielen (%s) - wird uebersprungen", |
|
| 337 | -bad, strings.TrimSpace(e.FileError)) |
|
| 338 | - p.setError(fmt.Sprintf("%s liess sich nicht abspielen", bad)) |
|
| 339 | -return p.step(client, 1) |
|
| 281 | + | |
| 282 | +case "end-file": |
|
| 283 | +// Der beste Moment fuer eine neue Playliste: zwischen zwei Dateien. |
|
| 284 | +p.mu.RLock() |
|
| 285 | +pending := p.pending |
|
| 286 | +p.mu.RUnlock() |
|
| 287 | + if pending { |
|
| 288 | +p.loadPlaylist(client) |
|
| 340 | 289 | } |
| 341 | 290 | } |
| 342 | -return nil |
|
| 343 | 291 | } |
| 344 | 292 | |
| 345 | -// step schaltet um delta Eintraege weiter und laedt die Datei in mpv. |
|
| 346 | -// delta 0 laedt den aktuellen Eintrag (bzw. steigt neu ein). |
|
| 347 | -func (p *Player) step(client *mpv.Client, delta int) error { |
|
| 348 | -entries, _ := p.pl.EntriesWithVersion() |
|
| 293 | +// loadPlaylist uebergibt mpv die aktuelle m3u-Datei. |
|
| 294 | +func (p *Player) loadPlaylist(client *mpv.Client) { |
|
| 295 | +entries, version := p.pl.EntriesWithVersion() |
|
| 296 | + | |
| 297 | +p.mu.Lock() |
|
| 298 | +p.pending = false |
|
| 299 | +p.loadedID = version |
|
| 300 | +p.mu.Unlock() |
|
| 301 | + | |
| 349 | 302 | if len(entries) == 0 { |
| 350 | -p.mu.Lock() |
|
| 351 | -p.playing, p.index = "", 0 |
|
| 352 | -p.status.Current = "" |
|
| 353 | -p.mu.Unlock() |
|
| 303 | + if _, err := client.Command("playlist-clear"); err != nil { |
|
| 304 | + log.Printf("player: playlist-clear: %v", err) |
|
| 305 | +} |
|
| 354 | 306 | if _, err := client.Command("stop"); err != nil { |
| 355 | 307 | log.Printf("player: stop: %v", err) |
| 356 | 308 | } |
| 357 | -return nil |
|
| 358 | -} |
|
| 359 | - | |
| 360 | -p.mu.RLock() |
|
| 361 | -playing, index, errStreak := p.playing, p.index, p.errStreak |
|
| 362 | -loaded := p.loadedCount |
|
| 363 | -p.mu.RUnlock() |
|
| 364 | - | |
| 365 | -// Lieber gar nichts tun als in einer Schleife lauter kaputte Dateien |
|
| 366 | -// durchzuhecheln - der naechste Takt versucht es in Ruhe erneut. |
|
| 367 | - if errStreak >= len(entries) { |
|
| 368 | - log.Printf("player: keine der %d Dateien liess sich abspielen - warte %s", |
|
| 369 | -len(entries), retryDelay) |
|
| 370 | - p.setError("keine abspielbare Datei in der Playliste") |
|
| 371 | 309 | p.mu.Lock() |
| 372 | -p.playing = "" |
|
| 373 | -p.lastAttempt = time.Now() |
|
| 374 | -p.mu.Unlock() |
|
| 375 | -return nil |
|
| 376 | -} |
|
| 377 | - | |
| 378 | -// Position anhand des Dateinamens bestimmen: so bleibt die Reihenfolge |
|
| 379 | -// auch dann sinnvoll, wenn zwischendurch etwas hinzugekommen oder |
|
| 380 | -// weggefallen ist. |
|
| 381 | -base := index |
|
| 382 | - if playing != "" { |
|
| 383 | - if i := indexOf(entries, playing); i >= 0 { |
|
| 384 | -base = i |
|
| 385 | -} |
|
| 386 | -} |
|
| 387 | -next := ((base+delta)%len(entries) + len(entries)) % len(entries) |
|
| 388 | - | |
| 389 | -entry := entries[next] |
|
| 390 | -path := filepath.Join(p.pl.MediaDir(), entry.Name) |
|
| 391 | - | |
| 392 | -// Ein Video in einen mpv-Prozess zu laden, der vorher schon etwas anderes |
|
| 393 | -// angezeigt hat, endet auf manchen Systemen im Abbruch des Demuxers |
|
| 394 | - // ("partial file"). Deshalb bekommt jedes Video einen frischen Prozess. |
|
| 395 | - if p.cfg.FreshForVideo && entry.Kind == "video" && loaded > 0 { |
|
| 396 | -p.mu.Lock() |
|
| 397 | -p.playing = entry.Name // damit der neue Prozess genau hier einsteigt |
|
| 398 | -p.index = next |
|
| 310 | +p.status.Current = "" |
|
| 399 | 311 | p.mu.Unlock() |
| 400 | - log.Printf("player: starte mpv neu fuer %q", entry.Name) |
|
| 401 | -return errFreshMpv |
|
| 312 | +return |
|
| 402 | 313 | } |
| 403 | 314 | |
| 404 | -p.mu.Lock() |
|
| 405 | -p.playing = entry.Name |
|
| 406 | -p.index = next |
|
| 407 | -p.lastAttempt = time.Now() |
|
| 408 | -p.lastTimePos = -1 |
|
| 409 | -p.lastProgressAt = time.Now() |
|
| 410 | -p.status.Position = next |
|
| 411 | -p.loadedCount++ |
|
| 412 | -p.mu.Unlock() |
|
| 413 | - | |
| 414 | - if _, err := client.Command("loadfile", path, "replace"); err != nil { |
|
| 415 | - log.Printf("player: %q laden: %v", entry.Name, err) |
|
| 416 | - p.setError("Laden von " + entry.Name + ": " + err.Error()) |
|
| 417 | -p.mu.Lock() |
|
| 418 | -p.errStreak++ |
|
| 419 | -p.mu.Unlock() |
|
| 420 | -return nil |
|
| 315 | + if _, err := client.Command("loadlist", p.pl.PlaylistFile(), "replace"); err != nil { |
|
| 316 | + log.Printf("player: playlist laden: %v", err) |
|
| 317 | + p.setError("Playliste laden: " + err.Error()) |
|
| 318 | +return |
|
| 421 | 319 | } |
| 422 | 320 | if err := client.SetProperty("pause", false); err != nil { |
| 423 | 321 | log.Printf("player: fortsetzen: %v", err) |
| 424 | 322 | } |
| 425 | -return nil |
|
| 323 | + log.Printf("player: playliste geladen (%d eintraege)", len(entries)) |
|
| 426 | 324 | } |
| 427 | 325 | |
| 428 | -func indexOf(entries []playlist.Entry, name string) int { |
|
| 429 | - for i, e := range entries { |
|
| 430 | - if e.Name == name { |
|
| 431 | -return i |
|
| 432 | -} |
|
| 326 | +// checkStalled ueberspringt ein Video, das nicht mehr weiterlaeuft, damit eine |
|
| 327 | +// einzelne Datei nicht die ganze Wiedergabe anhaelt. |
|
| 328 | +func (p *Player) checkStalled(client *mpv.Client) { |
|
| 329 | +timeout := time.Duration(p.cfg.StallTimeout) * time.Second |
|
| 330 | + if timeout <= 0 { |
|
| 331 | +return |
|
| 433 | 332 | } |
| 434 | -return -1 |
|
| 435 | -} |
|
| 436 | - | |
| 437 | -// tick prueft regelmaessig, ob die Wiedergabe noch laeuft. |
|
| 438 | -func (p *Player) tick(client *mpv.Client) error { |
|
| 439 | 333 | p.mu.RLock() |
| 440 | -playing, paused := p.playing, p.status.Paused |
|
| 441 | -current := p.status.Current |
|
| 334 | +current, paused, idle := p.status.Current, p.status.Paused, p.status.Idle |
|
| 442 | 335 | since := time.Since(p.lastProgressAt) |
| 443 | -sinceAttempt := time.Since(p.lastAttempt) |
|
| 444 | 336 | p.mu.RUnlock() |
| 445 | 337 | |
| 446 | -// Nichts geladen (leere Playliste oder lauter kaputte Dateien): |
|
| 447 | -// nach einer Weile erneut versuchen. |
|
| 448 | - if playing == "" { |
|
| 449 | - if len(p.pl.Entries()) > 0 && sinceAttempt > retryDelay { |
|
| 450 | -p.mu.Lock() |
|
| 451 | -p.errStreak = 0 |
|
| 452 | -p.mu.Unlock() |
|
| 453 | -return p.step(client, 0) |
|
| 454 | -} |
|
| 455 | -return nil |
|
| 456 | -} |
|
| 457 | - | |
| 458 | -// Haengendes Video: ueberspringen, damit nicht die ganze Wand steht. |
|
| 459 | -timeout := time.Duration(p.cfg.StallTimeout) * time.Second |
|
| 460 | - if timeout <= 0 || paused || current == "" || playlist.Kind(current) != "video" || since < timeout { |
|
| 461 | -return nil |
|
| 338 | +// Bilder haben keinen fortlaufenden Zeitstempel - nur Videos pruefen. |
|
| 339 | + if paused || idle || current == "" || playlist.Kind(current) != "video" || since < timeout { |
|
| 340 | +return |
|
| 462 | 341 | } |
| 463 | 342 | |
| 464 | 343 | log.Printf("player: %q laeuft seit %s nicht weiter - wird uebersprungen", |
| @@ -467,14 +346,9 @@ func (p *Player) tick(client *mpv.Client) error { | ||
| 467 | 346 | p.mu.Lock() |
| 468 | 347 | p.lastProgressAt = time.Now() |
| 469 | 348 | p.mu.Unlock() |
| 470 | - | |
| 471 | -// Reagiert mpv nicht mehr, hilft nur noch ein Neustart des Prozesses. |
|
| 472 | - if _, err := client.Command("playlist-remove", "current"); err != nil { |
|
| 473 | -// playlist-remove ist nur ein billiger Lebenstest; scheitert er, |
|
| 474 | -// steht mpv wirklich. |
|
| 475 | - return fmt.Errorf("mpv reagiert nicht mehr: %w", err) |
|
| 349 | + if _, err := client.Command("playlist-next", "force"); err != nil { |
|
| 350 | + log.Printf("player: weiterschalten: %v", err) |
|
| 476 | 351 | } |
| 477 | -return p.step(client, 1) |
|
| 478 | 352 | } |
| 479 | 353 | |
| 480 | 354 | // applySettings uebernimmt Anzeigedauer und Stummschaltung ohne mpv-Neustart. |
| @@ -498,6 +372,7 @@ func (p *Player) mpvArgs(disp display) []string { | ||
| 498 | 372 | "--idle=yes", |
| 499 | 373 | "--force-window=yes", |
| 500 | 374 | "--keep-open=no", |
| 375 | +"--loop-playlist=inf", |
|
| 501 | 376 | "--image-display-duration=" + strconv.FormatFloat(s.ImageDuration, 'f', -1, 64), |
| 502 | 377 | "--hwdec=" + p.cfg.MpvHwdec, |
| 503 | 378 | "--no-osc", |
| @@ -506,22 +381,26 @@ func (p *Player) mpvArgs(disp display) []string { | ||
| 506 | 381 | "--no-input-default-bindings", |
| 507 | 382 | "--input-vo-keyboard=no", |
| 508 | 383 | "--stop-screensaver=yes", |
| 509 | -// Kein --no-terminal: sonst verschluckt mpv seine eigenen |
|
| 510 | -// Fehlermeldungen, und im Journal steht nur, dass es beendet wurde. |
|
| 384 | +// Kein --no-terminal: sonst verschluckt mpv seine Fehlermeldungen. |
|
| 511 | 385 | "--msg-level=all=warn", |
| 512 | 386 | "--no-input-terminal", |
| 387 | +"--vo=gpu", |
|
| 388 | +} |
|
| 389 | +// Die Vulkan-Anbindung von libplacebo baut bei jedem Bildwechsel die |
|
| 390 | +// Swapchain neu auf und laeuft dabei nach wenigen Wechseln in |
|
| 391 | +// VK_ERROR_OUT_OF_HOST_MEMORY: mpv laeuft weiter, der Bildschirm friert |
|
| 392 | +// ein. Mit OpenGL tritt das nicht auf (auf einem Pi 5 nachgemessen: |
|
| 393 | +// 0 statt 24 Fehlern in 20 Bildwechseln). |
|
| 394 | + if p.cfg.MpvGpuAPI != "" && p.cfg.MpvGpuAPI != "auto" { |
|
| 395 | +args = append(args, "--gpu-api="+p.cfg.MpvGpuAPI) |
|
| 513 | 396 | } |
| 514 | 397 | if p.cfg.Audio { |
| 515 | 398 | args = append(args, "--mute="+yesNo(s.Mute)) |
| 516 | 399 | } else { |
| 517 | -// Ohne Tonausgabe kann auch kein blockierendes Audiogeraet die |
|
| 518 | -// Wiedergabe anhalten - fuer eine reine Anzeigetafel der sichere Weg. |
|
| 519 | 400 | args = append(args, "--no-audio") |
| 520 | 401 | } |
| 521 | 402 | if disp.mode == "drm" { |
| 522 | -args = append(args, "--vo=gpu", "--gpu-context=drm") |
|
| 523 | - } else { |
|
| 524 | -args = append(args, "--vo=gpu") |
|
| 403 | +args = append(args, "--gpu-context=drm") |
|
| 525 | 404 | } |
| 526 | 405 | args = append(args, splitArgs(p.cfg.MpvExtraArgs)...) |
| 527 | 406 | return args |
| @@ -568,47 +447,42 @@ func splitArgs(s string) []string { | ||
| 568 | 447 | // Steuerung aus dem Webinterface |
| 569 | 448 | // --------------------------------------------------------------------------- |
| 570 | 449 | |
| 571 | -func (p *Player) running() bool { |
|
| 450 | +func (p *Player) withClient(fn func(*mpv.Client) error) error { |
|
| 572 | 451 | p.mu.RLock() |
| 573 | -defer p.mu.RUnlock() |
|
| 574 | -return p.client != nil |
|
| 575 | -} |
|
| 576 | - | |
| 577 | -func (p *Player) send(ch chan int, delta int) error { |
|
| 578 | - if !p.running() { |
|
| 452 | +c := p.client |
|
| 453 | +p.mu.RUnlock() |
|
| 454 | + if c == nil { |
|
| 579 | 455 | return fmt.Errorf("player laeuft gerade nicht") |
| 580 | 456 | } |
| 581 | - select { |
|
| 582 | -case ch <- delta: |
|
| 583 | -return nil |
|
| 584 | -default: |
|
| 585 | - return fmt.Errorf("player ist beschaeftigt") |
|
| 586 | -} |
|
| 457 | +return fn(c) |
|
| 587 | 458 | } |
| 588 | 459 | |
| 589 | 460 | // Next springt zum naechsten Eintrag. |
| 590 | -func (p *Player) Next() error { return p.send(p.advance, 1) } |
|
| 461 | +func (p *Player) Next() error { |
|
| 462 | + return p.withClient(func(c *mpv.Client) error { |
|
| 463 | + _, err := c.Command("playlist-next", "force") |
|
| 464 | +return err |
|
| 465 | +}) |
|
| 466 | +} |
|
| 591 | 467 | |
| 592 | 468 | // Prev springt zum vorherigen Eintrag. |
| 593 | -func (p *Player) Prev() error { return p.send(p.advance, -1) } |
|
| 469 | +func (p *Player) Prev() error { |
|
| 470 | + return p.withClient(func(c *mpv.Client) error { |
|
| 471 | + _, err := c.Command("playlist-prev", "force") |
|
| 472 | +return err |
|
| 473 | +}) |
|
| 474 | +} |
|
| 594 | 475 | |
| 595 | 476 | // TogglePause haelt die Wiedergabe an oder setzt sie fort. |
| 596 | 477 | func (p *Player) TogglePause() error { |
| 597 | -p.mu.RLock() |
|
| 598 | -c := p.client |
|
| 599 | -p.mu.RUnlock() |
|
| 600 | - if c == nil { |
|
| 601 | - return fmt.Errorf("player laeuft gerade nicht") |
|
| 602 | -} |
|
| 603 | - _, err := c.Command("cycle", "pause") |
|
| 604 | -return err |
|
| 478 | + return p.withClient(func(c *mpv.Client) error { |
|
| 479 | + _, err := c.Command("cycle", "pause") |
|
| 480 | +return err |
|
| 481 | +}) |
|
| 605 | 482 | } |
| 606 | 483 | |
| 607 | -// ReloadNow beginnt die Playliste sofort von vorn. |
|
| 484 | +// ReloadNow laedt die Playliste sofort neu, auch mitten in einem Video. |
|
| 608 | 485 | func (p *Player) ReloadNow() error { |
| 609 | - if !p.running() { |
|
| 610 | - return fmt.Errorf("player laeuft gerade nicht") |
|
| 611 | -} |
|
| 612 | 486 | select { |
| 613 | 487 | case p.reloadNow <- struct{}{}: |
| 614 | 488 | default: |
| @@ -629,8 +503,6 @@ func (p *Player) setRunning(v bool) { | ||
| 629 | 503 | p.mu.Lock() |
| 630 | 504 | p.status.Running = v |
| 631 | 505 | if !v { |
| 632 | -// p.playing bleibt stehen: Daran erkennt der naechste mpv-Prozess, |
|
| 633 | -// wo er wieder einsteigen soll. |
|
| 634 | 506 | p.status.Current = "" |
| 635 | 507 | } |
| 636 | 508 | p.mu.Unlock() |
gelöscht
internal/player/segment.go
+0
−90
| @@ -1,90 +0,0 @@ | ||
| 1 | -package player |
|
| 2 | - | |
| 3 | -import ( |
|
| 4 | -"path/filepath" |
|
| 5 | -"strings" |
|
| 6 | - | |
| 7 | -"ehc/tvwall/internal/config" |
|
| 8 | -"ehc/tvwall/internal/playlist" |
|
| 9 | -) |
|
| 10 | - | |
| 11 | -// segment ist ein Stueck Playliste, das ein mpv-Prozess am Stueck abspielt: |
|
| 12 | -// entweder eine Folge aufeinanderfolgender Bilder oder genau ein Video. |
|
| 13 | -// |
|
| 14 | -// Diese Aufteilung loest zwei beobachtete Fehler auf einmal: |
|
| 15 | -// |
|
| 16 | -// - Ein Video, das in einen mpv-Prozess geladen wird, der vorher schon etwas |
|
| 17 | -// angezeigt hat, bricht mit "partial file" ab. Jedes Video bekommt deshalb |
|
| 18 | -// einen eigenen, frischen Prozess. |
|
| 19 | -// - Wird dagegen jedes einzelne Bild per loadfile nachgeladen, bleibt die |
|
| 20 | -// Bildausgabe nach etwa neun Wechseln stehen (mpv meldet weiter Wechsel, |
|
| 21 | -// der Bildschirm friert ein). Bilderfolgen laufen deshalb als Playliste |
|
| 22 | -// innerhalb eines Prozesses - mpv schaltet selbst weiter. |
|
| 23 | -type segment struct { |
|
| 24 | -kind string // "images" oder "video" |
|
| 25 | -names []string |
|
| 26 | -} |
|
| 27 | - | |
| 28 | -// buildSegments fasst aufeinanderfolgende Bilder zusammen; jedes Video wird |
|
| 29 | -// ein eigener Abschnitt. |
|
| 30 | -func buildSegments(entries []playlist.Entry) []segment { |
|
| 31 | -var segs []segment |
|
| 32 | - for _, e := range entries { |
|
| 33 | - if e.Kind == "video" { |
|
| 34 | - segs = append(segs, segment{kind: "video", names: []string{e.Name}}) |
|
| 35 | -continue |
|
| 36 | -} |
|
| 37 | - if n := len(segs); n > 0 && segs[n-1].kind == "images" { |
|
| 38 | -segs[n-1].names = append(segs[n-1].names, e.Name) |
|
| 39 | -continue |
|
| 40 | -} |
|
| 41 | - segs = append(segs, segment{kind: "images", names: []string{e.Name}}) |
|
| 42 | -} |
|
| 43 | -return segs |
|
| 44 | -} |
|
| 45 | - | |
| 46 | -func (s segment) equal(o segment) bool { |
|
| 47 | - if s.kind != o.kind || len(s.names) != len(o.names) { |
|
| 48 | -return false |
|
| 49 | -} |
|
| 50 | - for i := range s.names { |
|
| 51 | - if s.names[i] != o.names[i] { |
|
| 52 | -return false |
|
| 53 | -} |
|
| 54 | -} |
|
| 55 | -return true |
|
| 56 | -} |
|
| 57 | - | |
| 58 | -func (s segment) label() string { |
|
| 59 | - if s.kind == "video" && len(s.names) == 1 { |
|
| 60 | -return "Video " + s.names[0] |
|
| 61 | -} |
|
| 62 | -return strings.Join(s.names, ", ") |
|
| 63 | -} |
|
| 64 | - | |
| 65 | -// writePlaylist schreibt die Dateien des Abschnitts als m3u fuer mpv. |
|
| 66 | -func (s segment) writePlaylist(cfg *config.Config, mediaDir string) (string, error) { |
|
| 67 | -var b strings.Builder |
|
| 68 | - b.WriteString("#EXTM3U\n") |
|
| 69 | - for _, n := range s.names { |
|
| 70 | -b.WriteString(filepath.Join(mediaDir, n)) |
|
| 71 | - b.WriteByte('\n') |
|
| 72 | -} |
|
| 73 | -path := filepath.Join(cfg.StateDir, "segment.m3u") |
|
| 74 | - if err := config.WriteFileAtomic(path, []byte(b.String()), 0o664); err != nil { |
|
| 75 | -return "", err |
|
| 76 | -} |
|
| 77 | -return path, nil |
|
| 78 | -} |
|
| 79 | - | |
| 80 | -func sameSegments(a, b []segment) bool { |
|
| 81 | - if len(a) != len(b) { |
|
| 82 | -return false |
|
| 83 | -} |
|
| 84 | - for i := range a { |
|
| 85 | - if !a[i].equal(b[i]) { |
|
| 86 | -return false |
|
| 87 | -} |
|
| 88 | -} |
|
| 89 | -return true |
|
| 90 | -} |
| @@ -0,0 +1,81 @@ | ||
| 1 | +#!/usr/bin/env bash |
|
| 2 | +# =========================================================================== |
|
| 3 | +# Stellt den Pi auf reinen Anzeigebetrieb um: kein Desktop mehr, Start direkt |
|
| 4 | +# in die Konsole, tvwall malt ueber DRM/KMS unmittelbar auf den Bildschirm. |
|
| 5 | +# |
|
| 6 | +# Aufruf: sudo /opt/tvwall/scripts/console-mode.sh [--zurueck] |
|
| 7 | +# |
|
| 8 | +# --zurueck stellt den Desktop-Betrieb wieder her. |
|
| 9 | +# =========================================================================== |
|
| 10 | +set -euo pipefail |
|
| 11 | + | |
| 12 | +CONF="/etc/tvwall/tvwall.conf" |
|
| 13 | +INSTALLER="/opt/tvwall/install.sh" |
|
| 14 | + | |
| 15 | +info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } |
|
| 16 | +ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; } |
|
| 17 | +warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; } |
|
| 18 | +die() { printf '\033[1;31mFehler:\033[0m %s\n' "$*" >&2; exit 1; } |
|
| 19 | + | |
| 20 | +[[ $EUID -eq 0 ]] || die "bitte mit sudo starten: sudo $0" |
|
| 21 | + | |
| 22 | +if [[ "${1:-}" == "--zurueck" ]]; then |
|
| 23 | +info "Stelle den Desktop-Betrieb wieder her" |
|
| 24 | +systemctl set-default graphical.target |
|
| 25 | +systemctl enable lightdm >/dev/null 2>&1 || true |
|
| 26 | +systemctl enable getty@tty1.service >/dev/null 2>&1 || true |
|
| 27 | +sed -i 's|^DISPLAY_MODE=.*|DISPLAY_MODE="auto"|' "$CONF" |
|
| 28 | +[[ -x "$INSTALLER" ]] && "$INSTALLER" --no-ap --yes >/dev/null |
|
| 29 | +ok "Desktop wieder aktiv - bitte neu starten: sudo reboot" |
|
| 30 | +exit 0 |
|
| 31 | +fi |
|
| 32 | + | |
| 33 | +info "Boot-Ziel auf Konsole umstellen" |
|
| 34 | +systemctl set-default multi-user.target >/dev/null |
|
| 35 | +ok "Standard-Ziel: $(systemctl get-default)" |
|
| 36 | + | |
| 37 | +info "Desktop-Anmeldung abschalten" |
|
| 38 | +for dm in lightdm gdm3 sddm; do |
|
| 39 | +if systemctl list-unit-files "$dm.service" >/dev/null 2>&1 && |
|
| 40 | +systemctl is-enabled "$dm" >/dev/null 2>&1; then |
|
| 41 | +systemctl disable "$dm" >/dev/null 2>&1 || true |
|
| 42 | +systemctl stop "$dm" || true |
|
| 43 | +ok "$dm abgeschaltet" |
|
| 44 | +fi |
|
| 45 | +done |
|
| 46 | +sleep 1 |
|
| 47 | +[[ "$(pgrep -c 'labwc|wayfire|Xorg' 2>/dev/null || echo 0)" == "0" ]] \ |
|
| 48 | +&& ok "keine Desktop-Oberflaeche mehr aktiv" \ |
|
| 49 | +|| warn "es laeuft noch eine Oberflaeche - nach dem Neustart ist sie weg" |
|
| 50 | + | |
| 51 | +info "Bildausgabe auf DRM festlegen" |
|
| 52 | +sed -i 's|^DISPLAY_MODE=.*|DISPLAY_MODE="drm"|' "$CONF" |
|
| 53 | +ok 'DISPLAY_MODE="drm"' |
|
| 54 | + | |
| 55 | +# Der Benutzer des Dienstes braucht Zugriff auf Grafik und Konsole. |
|
| 56 | +RUN_USER="$(sed -n 's/^RUN_USER="\(.*\)"/\1/p' "$CONF" | head -1)" |
|
| 57 | +RUN_USER="${RUN_USER:-tvwall}" |
|
| 58 | +for grp in video render input tty; do |
|
| 59 | +getent group "$grp" >/dev/null && usermod -aG "$grp" "$RUN_USER" 2>/dev/null || true |
|
| 60 | +done |
|
| 61 | +ok "Benutzer $RUN_USER hat Zugriff auf Grafik und Konsole" |
|
| 62 | + | |
| 63 | +if [[ -x "$INSTALLER" ]]; then |
|
| 64 | +info "Dienst auf die Konsolen-Variante umstellen" |
|
| 65 | +"$INSTALLER" --no-ap --yes >/dev/null |
|
| 66 | +ok "systemd-Dienst neu eingerichtet" |
|
| 67 | +else |
|
| 68 | +warn "$INSTALLER fehlt - bitte install.sh aus dem Paket erneut ausfuehren" |
|
| 69 | +fi |
|
| 70 | + | |
| 71 | +cat <<EOF |
|
| 72 | + | |
| 73 | +=========================================================================== |
|
| 74 | +Fertig. Nach dem Neustart faehrt der Pi ohne Desktop hoch und zeigt |
|
| 75 | +sofort die Playliste. |
|
| 76 | + | |
| 77 | +sudo reboot |
|
| 78 | + | |
| 79 | +Zurueck zum Desktop: sudo $0 --zurueck |
|
| 80 | +=========================================================================== |
|
| 81 | +EOF |
| @@ -163,6 +163,18 @@ if command -v nmcli >/dev/null; then | ||
| 163 | 163 | && ok "tvwall-ap aktiv" || warn "tvwall-ap nicht aktiv" |
| 164 | 164 | fi |
| 165 | 165 | |
| 166 | +hdr "Bildausgabe" |
|
| 167 | +SWAP="$(journalctl -u tvwall --since "-30 min" --no-pager 2>/dev/null | grep -ci swapchain)" |
|
| 168 | +if [[ "${SWAP:-0}" -gt 0 ]]; then |
|
| 169 | +bad "$SWAP Swapchain-Fehler in den letzten 30 Minuten" |
|
| 170 | +note "Die Vulkan-Anbindung von mpv laeuft auf dem Pi 5 nach wenigen" |
|
| 171 | +note "Bildwechseln aus dem Speicher: der Bildschirm friert ein, waehrend" |
|
| 172 | +note "das Webinterface weiterzaehlt." |
|
| 173 | +note "-> In $CONF setzen: MPV_GPU_API=\"opengl\" und Dienst neu starten" |
|
| 174 | +else |
|
| 175 | + ok "keine Swapchain-Fehler (MPV_GPU_API=${MPV_GPU_API:-opengl})" |
|
| 176 | +fi |
|
| 177 | + | |
| 166 | 178 | hdr "Letzte Protokollzeilen" |
| 167 | 179 | journalctl -u tvwall -n 40 --no-pager 2>/dev/null | sed 's/^/ /' |
| 168 | 180 |
gelöscht
scripts/probe-transition.py
+0
−170
| @@ -1,170 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 |
|
| 2 | -""" |
|
| 3 | -Prueft, ob ein Video in einem mpv-Prozess laeuft, der vorher ein Bild |
|
| 4 | -angezeigt hat - und welche mpv-Option das gegebenenfalls behebt. |
|
| 5 | - | |
| 6 | -Genau dieser Uebergang ist der Grund, warum tvwall vor jedem Video einen |
|
| 7 | -frischen mpv-Prozess startet (FRESH_MPV_FOR_VIDEO). Findet dieses Skript eine |
|
| 8 | -Option, die den Uebergang heil macht, kann der Neustart entfallen. |
|
| 9 | - | |
| 10 | -Aufruf (Dienst vorher anhalten): |
|
| 11 | - | |
| 12 | -sudo systemctl stop tvwall |
|
| 13 | -sudo -u rk XDG_RUNTIME_DIR=/run/user/1000 \\ |
|
| 14 | -python3 probe-transition.py <bild> <video> |
|
| 15 | -sudo systemctl start tvwall |
|
| 16 | -""" |
|
| 17 | -import json |
|
| 18 | -import os |
|
| 19 | -import socket |
|
| 20 | -import subprocess |
|
| 21 | -import sys |
|
| 22 | -import tempfile |
|
| 23 | -import time |
|
| 24 | - | |
| 25 | -BASE = [ |
|
| 26 | -"--fullscreen", "--idle=yes", "--force-window=yes", "--keep-open=no", |
|
| 27 | -"--image-display-duration=2", "--hwdec=no", "--no-osc", "--osd-level=0", |
|
| 28 | -"--cursor-autohide=always", "--no-input-default-bindings", |
|
| 29 | -"--input-vo-keyboard=no", "--no-input-terminal", "--msg-level=all=error", |
|
| 30 | -"--vo=gpu", |
|
| 31 | -] |
|
| 32 | - | |
| 33 | -VARIANTEN = [ |
|
| 34 | - ("Grundzustand (wie der Dienst)", []), |
|
| 35 | - ("--demuxer-thread=no", ["--demuxer-thread=no"]), |
|
| 36 | - ("--cache=no", ["--cache=no"]), |
|
| 37 | - ("--cache=yes", ["--cache=yes"]), |
|
| 38 | - ("ohne Zusatzdateien-Suche", ["--audio-file-auto=no", "--sub-auto=no", |
|
| 39 | -"--cover-art-auto=no"]), |
|
| 40 | - ("--vd-lavc-dr=no", ["--vd-lavc-dr=no"]), |
|
| 41 | - ("--demuxer-lavf-probe-info=nostreams", ["--demuxer-lavf-probe-info=nostreams"]), |
|
| 42 | - ("--gapless-audio=no", ["--gapless-audio=no"]), |
|
| 43 | - ("--force-window=no", ["--force-window=no"]), |
|
| 44 | - ("--no-audio", ["--no-audio"]), |
|
| 45 | -] |
|
| 46 | - | |
| 47 | - | |
| 48 | -class Mpv: |
|
| 49 | -"""Minimaler Client fuer den JSON-IPC-Socket von mpv.""" |
|
| 50 | - | |
| 51 | -def __init__(self, path): |
|
| 52 | -self.sock = socket.socket(socket.AF_UNIX) |
|
| 53 | -for _ in range(80): |
|
| 54 | -try: |
|
| 55 | -self.sock.connect(path) |
|
| 56 | -break |
|
| 57 | -except OSError: |
|
| 58 | -time.sleep(0.25) |
|
| 59 | -else: |
|
| 60 | - raise RuntimeError("mpv-Socket nicht erreichbar") |
|
| 61 | -self.buf = b"" |
|
| 62 | -self.rid = 0 |
|
| 63 | - | |
| 64 | -def command(self, *args): |
|
| 65 | -self.rid += 1 |
|
| 66 | -rid = self.rid |
|
| 67 | -self.sock.sendall( |
|
| 68 | - (json.dumps({"command": list(args), "request_id": rid}) + "\n").encode()) |
|
| 69 | -deadline = time.time() + 5 |
|
| 70 | -while time.time() < deadline: |
|
| 71 | -self.sock.settimeout(max(0.1, deadline - time.time())) |
|
| 72 | -try: |
|
| 73 | -chunk = self.sock.recv(65536) |
|
| 74 | -except (socket.timeout, OSError): |
|
| 75 | -break |
|
| 76 | -if not chunk: |
|
| 77 | -break |
|
| 78 | -self.buf += chunk |
|
| 79 | -while b"\n" in self.buf: |
|
| 80 | -line, self.buf = self.buf.split(b"\n", 1) |
|
| 81 | -try: |
|
| 82 | -msg = json.loads(line) |
|
| 83 | -except ValueError: |
|
| 84 | -continue |
|
| 85 | - if msg.get("request_id") == rid: |
|
| 86 | - return msg.get("data") |
|
| 87 | -return None |
|
| 88 | - | |
| 89 | -def close(self): |
|
| 90 | -try: |
|
| 91 | -self.sock.close() |
|
| 92 | -except OSError: |
|
| 93 | -pass |
|
| 94 | - | |
| 95 | - | |
| 96 | -def probe(extra, image, video): |
|
| 97 | -"""Bild laden, dann Video - und schauen, ob das Video wirklich laeuft.""" |
|
| 98 | -tmp = tempfile.mkdtemp() |
|
| 99 | -sock = os.path.join(tmp, "s") |
|
| 100 | -proc = subprocess.Popen( |
|
| 101 | -["mpv", "--input-ipc-server=" + sock] + BASE + extra, |
|
| 102 | -stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) |
|
| 103 | -try: |
|
| 104 | -m = Mpv(sock) |
|
| 105 | - m.command("loadfile", image, "replace") |
|
| 106 | -time.sleep(2.5) |
|
| 107 | - m.command("loadfile", video, "replace") |
|
| 108 | -time.sleep(2.0) |
|
| 109 | - erst = m.command("get_property", "time-pos") |
|
| 110 | -time.sleep(3.0) |
|
| 111 | - spaeter = m.command("get_property", "time-pos") |
|
| 112 | -m.close() |
|
| 113 | - | |
| 114 | -if not isinstance(erst, (int, float)) or not isinstance(spaeter, (int, float)): |
|
| 115 | -return False, "Video wurde gar nicht erst geladen" |
|
| 116 | -if spaeter - erst < 1.0: |
|
| 117 | -return False, "steht bei %.1f s" % spaeter |
|
| 118 | -return True, "laeuft bis %.1f s" % spaeter |
|
| 119 | -finally: |
|
| 120 | -proc.kill() |
|
| 121 | -try: |
|
| 122 | -err = proc.stderr.read().decode(errors="replace").strip() |
|
| 123 | -except Exception: |
|
| 124 | -err = "" |
|
| 125 | -proc.wait() |
|
| 126 | -if err: |
|
| 127 | -probe.last_error = err.splitlines()[-3:] |
|
| 128 | -else: |
|
| 129 | -probe.last_error = [] |
|
| 130 | - | |
| 131 | - | |
| 132 | -def main(): |
|
| 133 | -if len(sys.argv) != 3: |
|
| 134 | -print(__doc__) |
|
| 135 | -sys.exit(1) |
|
| 136 | -image, video = sys.argv[1], sys.argv[2] |
|
| 137 | -for f in (image, video): |
|
| 138 | -if not os.path.isfile(f): |
|
| 139 | - print("Datei fehlt:", f) |
|
| 140 | -sys.exit(1) |
|
| 141 | - | |
| 142 | - print("\nBild : %s\nVideo: %s\n" % (image, video)) |
|
| 143 | - print("Uebergang Bild -> Video im selben mpv-Prozess:\n") |
|
| 144 | - | |
| 145 | -gut = [] |
|
| 146 | -for name, extra in VARIANTEN: |
|
| 147 | -ok, info = probe(extra, image, video) |
|
| 148 | - print(" %-38s %s (%s)" % (name, "LAEUFT" if ok else "HAENGT", info)) |
|
| 149 | -for zeile in getattr(probe, "last_error", []): |
|
| 150 | - print(" %s" % zeile) |
|
| 151 | -if ok: |
|
| 152 | -gut.append(name) |
|
| 153 | - | |
| 154 | -print() |
|
| 155 | -if not gut: |
|
| 156 | - print("Keine Option hilft. Der Neustart vor jedem Video") |
|
| 157 | - print("(FRESH_MPV_FOR_VIDEO=\"yes\") bleibt die richtige Loesung.") |
|
| 158 | -elif gut == [n for n, _ in VARIANTEN]: |
|
| 159 | - print("Alle Varianten laufen - der Uebergang ist auf diesem System heil.") |
|
| 160 | - print("Dann kann FRESH_MPV_FOR_VIDEO=\"no\" gesetzt werden.") |
|
| 161 | -else: |
|
| 162 | - print("Diese Optionen beheben den Uebergang:") |
|
| 163 | -for n in gut: |
|
| 164 | - print(" ", n) |
|
| 165 | - print("\nTrag die passende in MPV_EXTRA_ARGS ein und setze") |
|
| 166 | - print("FRESH_MPV_FOR_VIDEO=\"no\" - dann entfaellt der Neustart.") |
|
| 167 | - | |
| 168 | - | |
| 169 | -if __name__ == "__main__": |
|
| 170 | -main() |
| @@ -0,0 +1,159 @@ | ||
| 1 | +#!/usr/bin/env bash |
|
| 2 | +# =========================================================================== |
|
| 3 | +# Ersetzt den Startbildschirm (Plymouth-Thema "pix") durch ein eigenes Logo. |
|
| 4 | +# |
|
| 5 | +# Aufruf: sudo /opt/tvwall/scripts/set-splash.sh /pfad/zum/logo.png [--dauer N] |
|
| 6 | +# sudo /opt/tvwall/scripts/set-splash.sh --zurueck |
|
| 7 | +# |
|
| 8 | +# Das Logo wird auf die Bildschirmgroesse zentriert auf schwarzem Grund |
|
| 9 | +# gesetzt - derselbe Hintergrund, den auch der Player verwendet, damit der |
|
| 10 | +# Uebergang vom Start in die Wiedergabe nicht springt. |
|
| 11 | +# |
|
| 12 | +# Zusaetzlich wird das Logo bis --dauer Sekunden nach dem Einschalten |
|
| 13 | +# stehengelassen (Standard 12). Ohne das ist es nach gut vier Sekunden schon |
|
| 14 | +# wieder weg - viele Fernseher brauchen laenger, um sich auf das HDMI-Signal |
|
| 15 | +# zu synchronisieren, und zeigen dann gar nichts davon. |
|
| 16 | +# =========================================================================== |
|
| 17 | +set -euo pipefail |
|
| 18 | + | |
| 19 | +THEME_DIR="/usr/share/plymouth/themes/pix" |
|
| 20 | +SPLASH="$THEME_DIR/splash.png" |
|
| 21 | +BACKUP="$THEME_DIR/splash.original.png" |
|
| 22 | +CONFIG_TXT="/boot/firmware/config.txt" |
|
| 23 | +[[ -f "$CONFIG_TXT" ]] || CONFIG_TXT="/boot/config.txt" |
|
| 24 | +HOLD_BIN="/usr/local/bin/tvwall-splash-hold" |
|
| 25 | +HOLD_UNIT="/etc/systemd/system/tvwall-splash.service" |
|
| 26 | +DAUER=12 |
|
| 27 | + | |
| 28 | +info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } |
|
| 29 | +ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; } |
|
| 30 | +warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; } |
|
| 31 | +die() { printf '\033[1;31mFehler:\033[0m %s\n' "$*" >&2; exit 1; } |
|
| 32 | + | |
| 33 | +[[ $EUID -eq 0 ]] || die "bitte mit sudo starten: sudo $0 $*" |
|
| 34 | +[[ -d "$THEME_DIR" ]] || die "Plymouth-Thema 'pix' nicht gefunden ($THEME_DIR)" |
|
| 35 | + | |
| 36 | +# --- Aufrufparameter ------------------------------------------------------- |
|
| 37 | +ARGS=() |
|
| 38 | +while [[ $# -gt 0 ]]; do |
|
| 39 | +case "$1" in |
|
| 40 | + --dauer) DAUER="${2:-12}"; shift 2 ;; |
|
| 41 | + --zurueck) ARGS+=("--zurueck"); shift ;; |
|
| 42 | + *) ARGS+=("$1"); shift ;; |
|
| 43 | +esac |
|
| 44 | +done |
|
| 45 | +set -- "${ARGS[@]+"${ARGS[@]}"}" |
|
| 46 | + | |
| 47 | +# --- zuruecksetzen --------------------------------------------------------- |
|
| 48 | +if [[ "${1:-}" == "--zurueck" ]]; then |
|
| 49 | +[[ -f "$BACKUP" ]] || die "keine Sicherung gefunden ($BACKUP)" |
|
| 50 | +cp -f "$BACKUP" "$SPLASH" |
|
| 51 | +ok "urspruenglicher Startbildschirm wiederhergestellt" |
|
| 52 | +systemctl disable --now tvwall-splash.service >/dev/null 2>&1 || true |
|
| 53 | +rm -f "$HOLD_UNIT" "$HOLD_BIN" |
|
| 54 | +systemctl daemon-reload |
|
| 55 | +ok "Halten des Startbildes entfernt" |
|
| 56 | +update-initramfs -u >/dev/null 2>&1 || true |
|
| 57 | +ok "fertig - beim naechsten Neustart wieder das Original" |
|
| 58 | +exit 0 |
|
| 59 | +fi |
|
| 60 | + | |
| 61 | +LOGO="${1:-}" |
|
| 62 | +[[ -n "$LOGO" && -f "$LOGO" ]] || die "Bilddatei angeben: sudo $0 /pfad/zum/logo.png" |
|
| 63 | +command -v ffmpeg >/dev/null || die "ffmpeg fehlt: sudo apt install ffmpeg" |
|
| 64 | + | |
| 65 | +# --- Bildschirmaufloesung ermitteln --------------------------------------- |
|
| 66 | +MODE="$(head -1 /sys/class/drm/*/modes 2>/dev/null | grep -m1 -E '^[0-9]+x[0-9]+' || true)" |
|
| 67 | +W="${MODE%x*}"; H="${MODE#*x}" |
|
| 68 | +if [[ ! "$W" =~ ^[0-9]+$ || ! "$H" =~ ^[0-9]+$ ]]; then |
|
| 69 | +W=1920; H=1080 |
|
| 70 | + warn "Aufloesung nicht erkannt - nehme ${W}x${H}" |
|
| 71 | +else |
|
| 72 | + ok "Bildschirm: ${W}x${H}" |
|
| 73 | +fi |
|
| 74 | + | |
| 75 | +# --- Startbild bauen ------------------------------------------------------- |
|
| 76 | +info "Baue Startbild aus $(basename "$LOGO")" |
|
| 77 | +LOGO_H=$(( H * 55 / 100 )) # Logo auf 55 % der Bildhoehe |
|
| 78 | +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT |
|
| 79 | +ffmpeg -v error -y -f lavfi -i "color=c=black:s=${W}x${H}" -i "$LOGO" \ |
|
| 80 | + -filter_complex "[1:v]scale=-1:${LOGO_H}:force_original_aspect_ratio=decrease[logo];\ |
|
| 81 | +[0:v][logo]overlay=(W-w)/2:(H-h)/2:format=auto" \ |
|
| 82 | +-frames:v 1 "$TMP/splash.png" || die "ffmpeg konnte das Bild nicht erzeugen" |
|
| 83 | +ok "Startbild erzeugt ($(stat -c %s "$TMP/splash.png") Bytes)" |
|
| 84 | + | |
| 85 | +# --- einsetzen ------------------------------------------------------------- |
|
| 86 | +[[ -f "$BACKUP" ]] || { cp -f "$SPLASH" "$BACKUP"; ok "Original gesichert: $BACKUP"; } |
|
| 87 | +install -m 0644 "$TMP/splash.png" "$SPLASH" |
|
| 88 | +ok "eingesetzt: $SPLASH" |
|
| 89 | + | |
| 90 | +# --- Regenbogen-Quadrat der Firmware abschalten --------------------------- |
|
| 91 | +if [[ -w "$CONFIG_TXT" ]] && ! grep -q "^disable_splash=1" "$CONFIG_TXT"; then |
|
| 92 | +cp -f "$CONFIG_TXT" "$CONFIG_TXT.tvwall-backup" |
|
| 93 | +printf '\n# von tvwall set-splash.sh: kein Regenbogen-Quadrat beim Einschalten\ndisable_splash=1\n' >> "$CONFIG_TXT" |
|
| 94 | +ok "disable_splash=1 in $(basename "$CONFIG_TXT") eingetragen" |
|
| 95 | +fi |
|
| 96 | + | |
| 97 | +# --- Startbild stehenlassen, bis die Wiedergabe uebernimmt ---------------- |
|
| 98 | +# Ohne das zeigt Plymouth das Logo nur etwa vier Sekunden; viele Fernseher |
|
| 99 | +# sind zu diesem Zeitpunkt noch gar nicht synchronisiert. |
|
| 100 | +if [[ -x /usr/bin/plymouth ]]; then |
|
| 101 | +info "Startbild bis $DAUER s nach dem Einschalten halten" |
|
| 102 | +cat > "$HOLD_BIN" <<'HOLD' |
|
| 103 | +#!/bin/sh |
|
| 104 | +# Haelt den Plymouth-Startbildschirm, bis die angegebene Zeit seit dem |
|
| 105 | +# Einschalten erreicht ist, und uebergibt ihn dann an die Wiedergabe. |
|
| 106 | +# --retain-splash laesst das Bild stehen, bis mpv das erste Mal malt - so |
|
| 107 | +# gibt es dazwischen keine schwarze Luecke. |
|
| 108 | +ZIEL="${1:-12}" |
|
| 109 | +UP="$(cut -d. -f1 /proc/uptime)" |
|
| 110 | +[ "$UP" -lt "$ZIEL" ] && sleep $(( ZIEL - UP )) |
|
| 111 | +[ -x /usr/bin/plymouth ] && /usr/bin/plymouth quit --retain-splash |
|
| 112 | +exit 0 |
|
| 113 | +HOLD |
|
| 114 | +chmod 0755 "$HOLD_BIN" |
|
| 115 | + | |
| 116 | +cat > "$HOLD_UNIT" <<UNIT |
|
| 117 | +# Von tvwall set-splash.sh erzeugt. |
|
| 118 | +[Unit] |
|
| 119 | +Description=Startbild halten, bis die Wiedergabe uebernimmt |
|
| 120 | +After=plymouth-start.service |
|
| 121 | +Before=tvwall.service plymouth-quit.service plymouth-quit-wait.service |
|
| 122 | +ConditionPathExists=/usr/bin/plymouth |
|
| 123 | + | |
| 124 | +[Service] |
|
| 125 | +Type=oneshot |
|
| 126 | +ExecStart=$HOLD_BIN $DAUER |
|
| 127 | +TimeoutStartSec=60 |
|
| 128 | +RemainAfterExit=yes |
|
| 129 | + | |
| 130 | +[Install] |
|
| 131 | +WantedBy=multi-user.target |
|
| 132 | +UNIT |
|
| 133 | +systemctl daemon-reload |
|
| 134 | +systemctl enable tvwall-splash.service >/dev/null 2>&1 |
|
| 135 | +ok "tvwall-splash.service eingerichtet" |
|
| 136 | +else |
|
| 137 | +warn "/usr/bin/plymouth fehlt - Startbild laesst sich nicht halten" |
|
| 138 | +fi |
|
| 139 | + | |
| 140 | +# --- Plymouth liest das Bild aus der initramfs ---------------------------- |
|
| 141 | +if command -v update-initramfs >/dev/null; then |
|
| 142 | +info "Aktualisiere initramfs (dauert einen Moment)" |
|
| 143 | +update-initramfs -u >/dev/null 2>&1 && ok "initramfs aktualisiert" \ |
|
| 144 | +|| warn "initramfs liess sich nicht aktualisieren - meist trotzdem in Ordnung" |
|
| 145 | +fi |
|
| 146 | + | |
| 147 | +cat <<EOF |
|
| 148 | + | |
| 149 | +=========================================================================== |
|
| 150 | +Fertig. Beim naechsten Neustart erscheint dein Logo und bleibt bis |
|
| 151 | +$DAUER Sekunden nach dem Einschalten stehen - danach uebernimmt die |
|
| 152 | +Wiedergabe. |
|
| 153 | + | |
| 154 | +sudo reboot |
|
| 155 | + | |
| 156 | +Anders lange stehen lassen: sudo $0 <logo> --dauer 20 |
|
| 157 | +Zurueck zum Original: sudo $0 --zurueck |
|
| 158 | +=========================================================================== |
|
| 159 | +EOF |