feat: add silent boot script to enhance boot experience
- Introduced a new script `silent-boot.sh` to manage console output during boot. - Redirect console messages to tty3, keeping tty1 clean. - Modify Plymouth behavior to retain the splash screen until mpv starts playback. - Clear tty1 text buffer to prevent old messages from appearing after playback. - Implemented systemd service configurations to support these changes. - Added instructions for enabling and reverting the silent boot feature.
R
Rüdiger Küpper
<rpr@9it.de>
committete am 21.08.2026 03:47
461e563c8eb742b028a1b2f25f99c24ff4f324c6
| @@ -146,4 +146,11 @@ steht im Protokoll und unten im Webinterface. | ||
| 146 | 146 | | **Playliste** | Reihenfolge per Ziehen am Griff `⠿` oder mit ↑ ↓, Löschen per 🗑 | |
| 147 | +| **Haken** | Ohne Haken bleibt die Datei liegen, wird auf der Wand aber übersprungen | |
|
| 147 | 148 | |
| 148 | -Reihenfolge und Einstellungen bleiben über einen Neustart hinweg erhalten. |
|
| 149 | +Reihenfolge, Haken und Einstellungen bleiben über einen Neustart hinweg |
|
| 150 | +erhalten (`order.json`, `disabled.json`, `settings.json` im Datenverzeichnis). |
|
| 151 | + | |
| 152 | +Ausgeblendete Einträge stehen weiter im Webinterface, sind grau hinterlegt und |
|
| 153 | +mit „ausgeblendet" gekennzeichnet — sie landen nur nicht in der `playlist.m3u`, |
|
| 154 | +die mpv abspielt. Die Änderung greift am nächsten Dateiübergang, ein laufendes |
|
| 155 | +Video wird also nicht unterbrochen. |
|
| 149 | 156 | |
| @@ -328,2 +335,3 @@ scripts/console-mode.sh Desktop abschalten, direkt in die Anzeige booten | ||
| 328 | 335 | scripts/set-splash.sh eigenes Logo als Startbildschirm setzen |
| 336 | +scripts/silent-boot.sh Textkonsole beim Starten unterdruecken |
|
| 329 | 337 | scripts/bisect-mpv.sh misst, welche mpv-Option einen Haenger ausloest |
| @@ -357,2 +365,3 @@ Ein paar Entscheidungen, die vielleicht nicht offensichtlich sind: | ||
| 357 | 365 | | `/api/delete` | POST | `{"name":"a.jpg"}` | |
| 366 | +| `/api/enabled` | POST | `{"name":"a.jpg","enabled":false}` — aus der Wiedergabe nehmen | |
|
| 358 | 367 | | `/api/settings` | POST | `{"image_duration":5,"mute":false}` | |
| @@ -102,2 +102,15 @@ MPV_EXTRA_ARGS="" | ||
| 102 | 102 | |
| 103 | +# --- Startlogo -------------------------------------------------------------- |
|
| 104 | + | |
| 105 | +# Bild, das beim Start zuerst gezeigt wird, bevor die Playliste beginnt. |
|
| 106 | +# Leer = aus. Wird von scripts/set-splash.sh automatisch eingetragen. |
|
| 107 | +# |
|
| 108 | +# Warum ueber den Player und nicht ueber den System-Startbildschirm: Viele |
|
| 109 | +# Fernseher wachen erst auf, wenn mpv den Bildmodus setzt - bis dahin ist der |
|
| 110 | +# Startbildschirm des Systems gar nicht zu sehen. |
|
| 111 | +BOOT_LOGO="" |
|
| 112 | + | |
| 113 | +# Wie lange das Startlogo stehen bleibt (Sekunden). |
|
| 114 | +BOOT_LOGO_SECONDS="8" |
|
| 115 | + | |
| 103 | 116 | # --- Pfade ------------------------------------------------------------------ |
Binärdatei – kein Textvergleich möglich.
| @@ -35,9 +35,12 @@ type Config struct { | ||
| 35 | 35 | |
| 36 | -DisplayMode string // "auto", "drm" oder "desktop" |
|
| 37 | -RunUser string |
|
| 38 | -MpvExtraArgs string |
|
| 39 | -MpvHwdec string |
|
| 40 | -MpvGpuAPI string |
|
| 41 | -FreshForVideo bool // mpv vor jedem Video neu starten |
|
| 42 | -StallTimeout int // Sekunden ohne Fortschritt, bevor ein Video uebersprungen wird |
|
| 36 | +DisplayMode string // "auto", "drm" oder "desktop" |
|
| 37 | +RunUser string |
|
| 38 | +MpvExtraArgs string |
|
| 39 | +MpvHwdec string |
|
| 40 | +MpvGpuAPI string |
|
| 41 | + | |
| 42 | +BootLogo string // Bild, das beim Start zuerst gezeigt wird |
|
| 43 | +BootLogoSeconds float64 // wie lange |
|
| 44 | +FreshForVideo bool // mpv vor jedem Video neu starten |
|
| 45 | +StallTimeout int // Sekunden ohne Fortschritt, bevor ein Video uebersprungen wird |
|
| 43 | 46 | |
| @@ -50,20 +53,21 @@ func Default() *Config { | ||
| 50 | 53 | return &Config{ |
| 51 | -APSSID: "TVWall-01", |
|
| 52 | -APPassword: "tvwall1234", |
|
| 53 | -APCountry: "DE", |
|
| 54 | -APIP: "192.168.4.1", |
|
| 55 | -APChannel: "7", |
|
| 56 | -Hostname: "tvwall-01", |
|
| 57 | -WebPort: 80, |
|
| 58 | -MaxUploadMB: 2048, |
|
| 59 | -ImageDuration: 5, |
|
| 60 | -Audio: true, |
|
| 61 | -ScanInterval: 10, |
|
| 62 | -DisplayMode: "auto", |
|
| 63 | -MpvHwdec: "no", |
|
| 64 | -MpvGpuAPI: "opengl", |
|
| 65 | -StallTimeout: 30, |
|
| 66 | -RunUser: "tvwall", |
|
| 67 | -StateDir: "/var/lib/tvwall", |
|
| 68 | -SocketPath: "/run/tvwall/mpv.sock", |
|
| 54 | +APSSID: "TVWall-01", |
|
| 55 | +APPassword: "tvwall1234", |
|
| 56 | +APCountry: "DE", |
|
| 57 | +APIP: "192.168.4.1", |
|
| 58 | +APChannel: "7", |
|
| 59 | +Hostname: "tvwall-01", |
|
| 60 | +WebPort: 80, |
|
| 61 | +MaxUploadMB: 2048, |
|
| 62 | +ImageDuration: 5, |
|
| 63 | +Audio: true, |
|
| 64 | +ScanInterval: 10, |
|
| 65 | +DisplayMode: "auto", |
|
| 66 | +MpvHwdec: "no", |
|
| 67 | +MpvGpuAPI: "opengl", |
|
| 68 | +BootLogoSeconds: 8, |
|
| 69 | +StallTimeout: 30, |
|
| 70 | +RunUser: "tvwall", |
|
| 71 | +StateDir: "/var/lib/tvwall", |
|
| 72 | +SocketPath: "/run/tvwall/mpv.sock", |
|
| 69 | 73 | } |
| @@ -75,2 +79,3 @@ func (c *Config) ThumbDir() string { return filepath.Join(c.StateDir, "thumb | ||
| 75 | 79 | func (c *Config) OrderFile() string { return filepath.Join(c.StateDir, "order.json") } |
| 80 | +func (c *Config) DisabledFile() string { return filepath.Join(c.StateDir, "disabled.json") } |
|
| 76 | 81 | func (c *Config) PlaylistFile() string { return filepath.Join(c.StateDir, "playlist.m3u") } |
| @@ -151,2 +156,8 @@ func Load(path string) (*Config, error) { | ||
| 151 | 156 | c.MpvExtraArgs = val |
| 157 | +case "BOOT_LOGO": |
|
| 158 | +c.BootLogo = val |
|
| 159 | +case "BOOT_LOGO_SECONDS": |
|
| 160 | + if f, err := strconv.ParseFloat(val, 64); err == nil && f >= 0 { |
|
| 161 | +c.BootLogoSeconds = f |
|
| 162 | +} |
|
| 152 | 163 | case "MPV_GPU_API": |
| @@ -64,2 +64,4 @@ type Player struct { | ||
| 64 | 64 | |
| 65 | +bootLogoDone bool // Startlogo wurde in dieser Dienstlaufzeit schon gezeigt |
|
| 66 | + | |
| 65 | 67 | reloadNow chan struct{} |
| @@ -129,3 +131,24 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 129 | 131 | |
| 130 | -args := p.mpvArgs(disp) |
|
| 132 | +// Die Playliste wird mpv direkt auf der Kommandozeile mitgegeben. Sonst |
|
| 133 | +// muesste erst die IPC-Verbindung stehen, bevor ueberhaupt etwas zu sehen |
|
| 134 | +// ist - das kostet beim Start rund eine Sekunde, in der die Textkonsole |
|
| 135 | +// sichtbar wird. |
|
| 136 | +entries, version := p.pl.EntriesWithVersion() |
|
| 137 | + | |
| 138 | +// Startlogo: beim ersten mpv-Start dieser Dienstlaufzeit bekommt mpv das |
|
| 139 | +// Logo als erste Datei. So ist es das allererste Bild auf dem Schirm. |
|
| 140 | +startLogo := "" |
|
| 141 | +p.mu.RLock() |
|
| 142 | +logoGezeigt := p.bootLogoDone |
|
| 143 | +p.mu.RUnlock() |
|
| 144 | + if !logoGezeigt && p.cfg.BootLogo != "" && p.cfg.BootLogoSeconds > 0 { |
|
| 145 | + if _, err := os.Stat(p.cfg.BootLogo); err == nil { |
|
| 146 | +startLogo = p.cfg.BootLogo |
|
| 147 | + } else { |
|
| 148 | + log.Printf("player: startlogo %q nicht lesbar: %v", p.cfg.BootLogo, err) |
|
| 149 | +} |
|
| 150 | +} |
|
| 151 | + | |
| 152 | +aktive := p.pl.ActiveCount() |
|
| 153 | +args := p.mpvArgs(disp, aktive, startLogo) |
|
| 131 | 154 | log.Printf("player: starte mpv %s", strings.Join(args, " ")) |
| @@ -184,4 +207,29 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 184 | 207 | |
| 185 | -p.applySettings(client) |
|
| 186 | -p.loadPlaylist(client) |
|
| 208 | +var logoTimer *time.Timer |
|
| 209 | + switch { |
|
| 210 | +case startLogo != "": |
|
| 211 | +// mpv zeigt das Logo bereits - nur den Zeitpunkt merken, an dem auf |
|
| 212 | +// die Playliste umgeschaltet wird. applySettings kaeme der |
|
| 213 | +// Anzeigedauer des Logos in die Quere und folgt deshalb spaeter. |
|
| 214 | +p.mu.Lock() |
|
| 215 | +p.bootLogoDone = true |
|
| 216 | +p.status.Current = filepath.Base(startLogo) |
|
| 217 | +p.mu.Unlock() |
|
| 218 | +logoTimer = time.NewTimer(time.Duration(p.cfg.BootLogoSeconds * float64(time.Second))) |
|
| 219 | + log.Printf("player: startlogo %s fuer %.0f s", filepath.Base(startLogo), p.cfg.BootLogoSeconds) |
|
| 220 | + | |
| 221 | +case aktive > 0: |
|
| 222 | +// Playliste lief schon beim Start von mpv mit - nur den Stand merken. |
|
| 223 | +p.applySettings(client) |
|
| 224 | +p.mu.Lock() |
|
| 225 | +p.loadedID = version |
|
| 226 | +p.pending = false |
|
| 227 | +p.mu.Unlock() |
|
| 228 | + log.Printf("player: playliste beim start uebergeben (%d von %d eintraegen aktiv)", |
|
| 229 | +aktive, len(entries)) |
|
| 230 | + | |
| 231 | +default: |
|
| 232 | +p.applySettings(client) |
|
| 233 | +p.loadPlaylist(client) |
|
| 234 | +} |
|
| 187 | 235 | p.setError("") |
| @@ -223,2 +271,8 @@ func (p *Player) runOnce(ctx context.Context, settingsChanged <-chan struct{}) e | ||
| 223 | 271 | |
| 272 | +case <-timerC(logoTimer): |
|
| 273 | +logoTimer.Stop() |
|
| 274 | +logoTimer = nil |
|
| 275 | +p.applySettings(client) // Anzeigedauer wieder auf den Normalwert |
|
| 276 | +p.loadPlaylist(client) |
|
| 277 | + | |
| 224 | 278 | case <-settingsChanged: |
| @@ -292,5 +346,15 @@ func (p *Player) handleEvent(client *mpv.Client, ev mpv.Event) { | ||
| 292 | 346 | |
| 347 | +// timerC liefert den Kanal eines Timers - oder nil, wenn keiner laeuft. |
|
| 348 | +// Ein nil-Kanal blockiert im select-Zweig fuer immer, genau wie gewuenscht. |
|
| 349 | +func timerC(t *time.Timer) <-chan time.Time { |
|
| 350 | + if t == nil { |
|
| 351 | +return nil |
|
| 352 | +} |
|
| 353 | +return t.C |
|
| 354 | +} |
|
| 355 | + | |
| 293 | 356 | // loadPlaylist uebergibt mpv die aktuelle m3u-Datei. |
| 294 | 357 | func (p *Player) loadPlaylist(client *mpv.Client) { |
| 295 | -entries, version := p.pl.EntriesWithVersion() |
|
| 358 | +_, version := p.pl.EntriesWithVersion() |
|
| 359 | +aktive := p.pl.ActiveCount() |
|
| 296 | 360 | |
| @@ -301,3 +365,3 @@ func (p *Player) loadPlaylist(client *mpv.Client) { | ||
| 301 | 365 | |
| 302 | - if len(entries) == 0 { |
|
| 366 | + if aktive == 0 { |
|
| 303 | 367 | if _, err := client.Command("playlist-clear"); err != nil { |
| @@ -322,3 +386,3 @@ func (p *Player) loadPlaylist(client *mpv.Client) { | ||
| 322 | 386 | } |
| 323 | - log.Printf("player: playliste geladen (%d eintraege)", len(entries)) |
|
| 387 | + log.Printf("player: playliste geladen (%d eintraege)", aktive) |
|
| 324 | 388 | } |
| @@ -366,4 +430,9 @@ func (p *Player) applySettings(client *mpv.Client) { | ||
| 366 | 430 | |
| 367 | -func (p *Player) mpvArgs(disp display) []string { |
|
| 431 | +func (p *Player) mpvArgs(disp display, aktive int, startLogo string) []string { |
|
| 368 | 432 | s := p.settings.Get() |
| 433 | +anzeigedauer := s.ImageDuration |
|
| 434 | + if startLogo != "" { |
|
| 435 | +// Etwas laenger als bis zum Umschalten, damit keine Luecke entsteht. |
|
| 436 | +anzeigedauer = p.cfg.BootLogoSeconds + 2 |
|
| 437 | +} |
|
| 369 | 438 | args := []string{ |
| @@ -375,3 +444,3 @@ func (p *Player) mpvArgs(disp display) []string { | ||
| 375 | 444 | "--loop-playlist=inf", |
| 376 | -"--image-display-duration=" + strconv.FormatFloat(s.ImageDuration, 'f', -1, 64), |
|
| 445 | +"--image-display-duration=" + strconv.FormatFloat(anzeigedauer, 'f', -1, 64), |
|
| 377 | 446 | "--hwdec=" + p.cfg.MpvHwdec, |
| @@ -405,2 +474,10 @@ func (p *Player) mpvArgs(disp display) []string { | ||
| 405 | 474 | args = append(args, splitArgs(p.cfg.MpvExtraArgs)...) |
| 475 | +// Die erste Datei bekommt mpv direkt mit: so erscheint das Bild, sobald |
|
| 476 | +// mpv den Bildschirm hat, ohne Umweg ueber die IPC-Verbindung. |
|
| 477 | + switch { |
|
| 478 | +case startLogo != "": |
|
| 479 | +args = append(args, startLogo) |
|
| 480 | +case aktive > 0: |
|
| 481 | +args = append(args, "--playlist="+p.pl.PlaylistFile()) |
|
| 482 | +} |
|
| 406 | 483 | return args |
| @@ -63,2 +63,5 @@ type Entry struct { | ||
| 63 | 63 | MTime int64 `json:"mtime"` |
| 64 | +// Enabled=false: Die Datei bleibt liegen und steht im Webinterface, wird |
|
| 65 | +// auf der Wand aber nicht gezeigt. |
|
| 66 | +Enabled bool `json:"enabled"` |
|
| 64 | 67 | } |
| @@ -69,2 +72,3 @@ type Manager struct { | ||
| 69 | 72 | orderFile string |
| 73 | +disabledFile string |
|
| 70 | 74 | playlistFile string |
| @@ -83,2 +87,3 @@ func New(cfg *config.Config) *Manager { | ||
| 83 | 87 | orderFile: cfg.OrderFile(), |
| 88 | +disabledFile: cfg.DisabledFile(), |
|
| 84 | 89 | playlistFile: cfg.PlaylistFile(), |
| @@ -131,2 +136,3 @@ func (m *Manager) Scan() (bool, error) { | ||
| 131 | 136 | |
| 137 | +ausgeblendet := m.readDisabled() |
|
| 132 | 138 | found := make(map[string]Entry, len(dirEntries)) |
| @@ -142,3 +148,7 @@ func (m *Manager) Scan() (bool, error) { | ||
| 142 | 148 | } |
| 143 | - e := Entry{Name: name, Kind: Kind(name), Size: info.Size(), MTime: info.ModTime().Unix()} |
|
| 149 | + e := Entry{ |
|
| 150 | +Name: name, Kind: Kind(name), |
|
| 151 | +Size: info.Size(), MTime: info.ModTime().Unix(), |
|
| 152 | +Enabled: !ausgeblendet[name], |
|
| 153 | +} |
|
| 144 | 154 | found[name] = e |
| @@ -204,2 +214,60 @@ func (m *Manager) SetOrder(names []string) error { | ||
| 204 | 214 | |
| 215 | +// SetEnabled blendet einen Eintrag auf der Wand ein oder aus. Die Datei bleibt |
|
| 216 | +// dabei liegen und im Webinterface sichtbar. |
|
| 217 | +func (m *Manager) SetEnabled(name string, enabled bool) error { |
|
| 218 | +safe, err := SafeName(name) |
|
| 219 | + if err != nil { |
|
| 220 | +return err |
|
| 221 | +} |
|
| 222 | +ausgeblendet := m.readDisabled() |
|
| 223 | + if enabled { |
|
| 224 | +delete(ausgeblendet, safe) |
|
| 225 | + } else { |
|
| 226 | +ausgeblendet[safe] = true |
|
| 227 | +} |
|
| 228 | +namen := make([]string, 0, len(ausgeblendet)) |
|
| 229 | + for n := range ausgeblendet { |
|
| 230 | +namen = append(namen, n) |
|
| 231 | +} |
|
| 232 | +sort.Strings(namen) |
|
| 233 | +data, err := json.MarshalIndent(namen, "", " ") |
|
| 234 | + if err != nil { |
|
| 235 | +return err |
|
| 236 | +} |
|
| 237 | + if err := config.WriteFileAtomic(m.disabledFile, append(data, '\n'), 0o664); err != nil { |
|
| 238 | + return fmt.Errorf("ausgeblendete eintraege speichern: %w", err) |
|
| 239 | +} |
|
| 240 | +_, err = m.Scan() |
|
| 241 | +return err |
|
| 242 | +} |
|
| 243 | + | |
| 244 | +func (m *Manager) readDisabled() map[string]bool { |
|
| 245 | + out := map[string]bool{} |
|
| 246 | +data, err := os.ReadFile(m.disabledFile) |
|
| 247 | + if err != nil { |
|
| 248 | +return out |
|
| 249 | +} |
|
| 250 | +var namen []string |
|
| 251 | + if json.Unmarshal(data, &namen) != nil { |
|
| 252 | +return out |
|
| 253 | +} |
|
| 254 | + for _, n := range namen { |
|
| 255 | +out[n] = true |
|
| 256 | +} |
|
| 257 | +return out |
|
| 258 | +} |
|
| 259 | + | |
| 260 | +// ActiveCount liefert die Anzahl der Eintraege, die tatsaechlich gezeigt werden. |
|
| 261 | +func (m *Manager) ActiveCount() int { |
|
| 262 | +m.mu.RLock() |
|
| 263 | +defer m.mu.RUnlock() |
|
| 264 | +n := 0 |
|
| 265 | + for _, e := range m.entries { |
|
| 266 | + if e.Enabled { |
|
| 267 | +n++ |
|
| 268 | +} |
|
| 269 | +} |
|
| 270 | +return n |
|
| 271 | +} |
|
| 272 | + | |
| 205 | 273 | // Delete entfernt eine Datei aus dem Medienordner und der Playliste. |
| @@ -234,2 +302,5 @@ func (m *Manager) commit(ordered []Entry) (bool, error) { | ||
| 234 | 302 | names[i] = e.Name |
| 303 | + if !e.Enabled { |
|
| 304 | +continue // bleibt in der Liste, wird aber nicht abgespielt |
|
| 305 | +} |
|
| 235 | 306 | m3u.WriteString(filepath.Join(m.mediaDir, e.Name)) |
| @@ -273,3 +344,4 @@ func sameEntries(a, b []Entry) bool { | ||
| 273 | 344 | for i := range a { |
| 274 | - if a[i].Name != b[i].Name || a[i].MTime != b[i].MTime || a[i].Size != b[i].Size { |
|
| 345 | +if a[i].Name != b[i].Name || a[i].MTime != b[i].MTime || |
|
| 346 | + a[i].Size != b[i].Size || a[i].Enabled != b[i].Enabled { |
|
| 275 | 347 | return false |
| @@ -98,5 +98,9 @@ function renderStatus() { | ||
| 98 | 98 | |
| 99 | -el.counter.textContent = state.entries.length |
|
| 100 | - ? `${state.entries.length} Einträge · ${d.images} Bilder · ${d.videos} Videos` |
|
| 101 | -: ''; |
|
| 99 | + if (!state.entries.length) { |
|
| 100 | +el.counter.textContent = ''; |
|
| 101 | + } else if (d.active === state.entries.length) { |
|
| 102 | + el.counter.textContent = `${state.entries.length} Einträge · ${d.images} Bilder · ${d.videos} Videos`; |
|
| 103 | + } else { |
|
| 104 | + el.counter.textContent = `${d.active} von ${state.entries.length} aktiv · ${d.images} Bilder · ${d.videos} Videos`; |
|
| 105 | +} |
|
| 102 | 106 | |
| @@ -129,2 +133,12 @@ function renderList() { | ||
| 129 | 133 | |
| 134 | + const toggle = document.createElement('label'); |
|
| 135 | +toggle.className = 'toggle'; |
|
| 136 | +toggle.title = entry.enabled ? 'Wird auf der Wand gezeigt' : 'Wird uebersprungen'; |
|
| 137 | + const box = document.createElement('input'); |
|
| 138 | +box.type = 'checkbox'; |
|
| 139 | +box.checked = entry.enabled; |
|
| 140 | + box.setAttribute('aria-label', 'Auf der Wand zeigen'); |
|
| 141 | + box.addEventListener('change', () => setEnabled(entry.name, box.checked, li)); |
|
| 142 | +toggle.appendChild(box); |
|
| 143 | + | |
| 130 | 144 | const img = document.createElement('img'); |
| @@ -149,2 +163,8 @@ function renderList() { | ||
| 149 | 163 | tags.append(badge, size); |
| 164 | + if (!entry.enabled) { |
|
| 165 | + const off = document.createElement('span'); |
|
| 166 | +off.className = 'badge off'; |
|
| 167 | +off.textContent = 'ausgeblendet'; |
|
| 168 | +tags.append(off); |
|
| 169 | +} |
|
| 150 | 170 | meta.append(name, tags); |
| @@ -159,3 +179,4 @@ function renderList() { | ||
| 159 | 179 | |
| 160 | -li.append(grip, img, meta, actions); |
|
| 180 | + li.classList.toggle('off', !entry.enabled); |
|
| 181 | +li.append(grip, toggle, img, meta, actions); |
|
| 161 | 182 | el.list.appendChild(li); |
| @@ -211,3 +232,8 @@ function move(name, delta) { | ||
| 211 | 232 | // Sortieren per Zeigergeraet - funktioniert mit Maus und Finger gleichermassen. |
| 212 | -let dragItem = null; |
|
| 233 | +// |
|
| 234 | +// Bewusst OHNE setPointerCapture und ohne Ereignisse am Griff selbst: Beim |
|
| 235 | +// Umsortieren wird die Zeile im DOM verschoben, dabei verliert der Griff die |
|
| 236 | +// Zeigererfassung und es kommen keine weiteren Ereignisse mehr an. Die |
|
| 237 | +// Ereignisse haengen deshalb am Dokument, das sich nie bewegt. |
|
| 238 | +let dragState = null; |
|
| 213 | 239 | |
| @@ -217,19 +243,28 @@ function startDrag(ev, li) { | ||
| 217 | 243 | dragging = true; |
| 218 | -dragItem = li; |
|
| 244 | + dragState = { li: li, id: ev.pointerId }; |
|
| 219 | 245 | li.classList.add('dragging'); |
| 220 | -ev.target.setPointerCapture(ev.pointerId); |
|
| 221 | - ev.target.addEventListener('pointermove', onDragMove); |
|
| 222 | - ev.target.addEventListener('pointerup', endDrag); |
|
| 223 | - ev.target.addEventListener('pointercancel', endDrag); |
|
| 246 | + document.addEventListener('pointermove', onDragMove, { passive: false }); |
|
| 247 | + document.addEventListener('pointerup', endDrag); |
|
| 248 | + document.addEventListener('pointercancel', endDrag); |
|
| 224 | 249 | } |
| 225 | 250 | |
| 251 | +// Zielposition aus der Zeigerhoehe bestimmen, statt das Element unter dem |
|
| 252 | +// Zeiger zu suchen: unabhaengig davon, was gerade darueber liegt. |
|
| 226 | 253 | function onDragMove(ev) { |
| 227 | -if (!dragItem) return; |
|
| 254 | +if (!dragState) return; |
|
| 255 | +if (ev.pointerId !== dragState.id) return; |
|
| 228 | 256 | ev.preventDefault(); |
| 229 | -const over = document.elementFromPoint(ev.clientX, ev.clientY); |
|
| 230 | - const target = over && over.closest ? over.closest('.item') : null; |
|
| 231 | -if (!target || target === dragItem || target.parentElement !== el.list) return; |
|
| 232 | -const rect = target.getBoundingClientRect(); |
|
| 233 | -const after = ev.clientY > rect.top + rect.height / 2; |
|
| 234 | -el.list.insertBefore(dragItem, after ? target.nextSibling : target); |
|
| 257 | + | |
| 258 | +const y = ev.clientY; |
|
| 259 | +let eingefuegt = false; |
|
| 260 | + for (const item of Array.from(el.list.children)) { |
|
| 261 | +if (item === dragState.li) continue; |
|
| 262 | +const r = item.getBoundingClientRect(); |
|
| 263 | + if (y < r.top + r.height / 2) { |
|
| 264 | +el.list.insertBefore(dragState.li, item); |
|
| 265 | +eingefuegt = true; |
|
| 266 | +break; |
|
| 267 | +} |
|
| 268 | +} |
|
| 269 | +if (!eingefuegt) el.list.appendChild(dragState.li); |
|
| 235 | 270 | } |
| @@ -237,8 +272,9 @@ function onDragMove(ev) { | ||
| 237 | 272 | function endDrag(ev) { |
| 238 | -if (!dragItem) return; |
|
| 239 | - ev.target.removeEventListener('pointermove', onDragMove); |
|
| 240 | - ev.target.removeEventListener('pointerup', endDrag); |
|
| 241 | - ev.target.removeEventListener('pointercancel', endDrag); |
|
| 242 | - dragItem.classList.remove('dragging'); |
|
| 243 | -dragItem = null; |
|
| 273 | +if (!dragState) return; |
|
| 274 | +if (ev && ev.pointerId !== undefined && ev.pointerId !== dragState.id) return; |
|
| 275 | + document.removeEventListener('pointermove', onDragMove); |
|
| 276 | + document.removeEventListener('pointerup', endDrag); |
|
| 277 | + document.removeEventListener('pointercancel', endDrag); |
|
| 278 | + dragState.li.classList.remove('dragging'); |
|
| 279 | +dragState = null; |
|
| 244 | 280 | dragging = false; |
| @@ -247,2 +283,14 @@ function endDrag(ev) { | ||
| 247 | 283 | |
| 284 | +async function setEnabled(name, enabled, li) { |
|
| 285 | + li.classList.toggle('off', !enabled); |
|
| 286 | + try { |
|
| 287 | + await api('/api/enabled', { name: name, enabled: enabled }); |
|
| 288 | +renderedVersion = -1; |
|
| 289 | +toast(enabled ? name + ' wird wieder gezeigt' : name + ' wird übersprungen', 'ok'); |
|
| 290 | + } catch (e) { |
|
| 291 | + li.classList.toggle('off', enabled); |
|
| 292 | + toast('Änderung fehlgeschlagen: ' + e.message, 'err'); |
|
| 293 | +} |
|
| 294 | +} |
|
| 295 | + | |
| 248 | 296 | async function remove(name) { |
| @@ -131,3 +131,3 @@ input[type="number"] { | ||
| 131 | 131 | display: grid; |
| 132 | -grid-template-columns: auto 84px 1fr auto; |
|
| 132 | +grid-template-columns: auto auto 84px 1fr auto; |
|
| 133 | 133 | align-items: center; gap: .7rem; |
| @@ -141,2 +141,9 @@ input[type="number"] { | ||
| 141 | 141 | .grip:active { cursor: grabbing; } |
| 142 | + | |
| 143 | +/* Haken: bestimmt, ob der Eintrag auf der Wand erscheint */ |
|
| 144 | +.toggle { display: grid; place-items: center; padding: .2rem; cursor: pointer; } |
|
| 145 | +.toggle input { width: 22px; height: 22px; accent-color: var(--accent); cursor: pointer; } |
|
| 146 | +.item.off { opacity: .55; } |
|
| 147 | +.item.off .thumb { filter: grayscale(1); } |
|
| 148 | +.badge.off { background: #3a2a1e; color: #e0b088; } |
|
| 142 | 149 | .thumb { |
| @@ -181,3 +188,3 @@ input[type="number"] { | ||
| 181 | 188 | @media (max-width: 420px) { |
| 182 | - .item { grid-template-columns: auto 64px 1fr auto; } |
|
| 189 | + .item { grid-template-columns: auto auto 64px 1fr auto; } |
|
| 183 | 190 | .thumb { width: 64px; height: 44px; } |
| @@ -57,2 +57,3 @@ func (s *Server) Handler() http.Handler { | ||
| 57 | 57 | mux.HandleFunc("/api/delete", s.handleDelete) |
| 58 | + mux.HandleFunc("/api/enabled", s.handleEnabled) |
|
| 58 | 59 | mux.HandleFunc("/api/settings", s.handleSettings) |
| @@ -118,2 +119,3 @@ type deviceInfo struct { | ||
| 118 | 119 | Videos int `json:"videos"` |
| 120 | +Active int `json:"active"` |
|
| 119 | 121 | Thumbs bool `json:"thumbs"` |
| @@ -124,3 +126,3 @@ func (s *Server) handleState(w http.ResponseWriter, r *http.Request) { | ||
| 124 | 126 | entries := s.pl.Entries() |
| 125 | -var images, videos int |
|
| 127 | +var images, videos, active int |
|
| 126 | 128 | for _, e := range entries { |
| @@ -131,2 +133,5 @@ func (s *Server) handleState(w http.ResponseWriter, r *http.Request) { | ||
| 131 | 133 | } |
| 134 | + if e.Enabled { |
|
| 135 | +active++ |
|
| 136 | +} |
|
| 132 | 137 | } |
| @@ -147,2 +152,3 @@ func (s *Server) handleState(w http.ResponseWriter, r *http.Request) { | ||
| 147 | 152 | Videos: videos, |
| 153 | +Active: active, |
|
| 148 | 154 | Thumbs: s.hasFF, |
| @@ -298,2 +304,22 @@ func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) { | ||
| 298 | 304 | |
| 305 | +func (s *Server) handleEnabled(w http.ResponseWriter, r *http.Request) { |
|
| 306 | + if r.Method != http.MethodPost { |
|
| 307 | +httpError(w, http.StatusMethodNotAllowed, "nur POST") |
|
| 308 | +return |
|
| 309 | +} |
|
| 310 | + var req struct { |
|
| 311 | +Name string `json:"name"` |
|
| 312 | +Enabled bool `json:"enabled"` |
|
| 313 | +} |
|
| 314 | + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil { |
|
| 315 | +httpError(w, http.StatusBadRequest, "ungueltige Anfrage") |
|
| 316 | +return |
|
| 317 | +} |
|
| 318 | + if err := s.pl.SetEnabled(req.Name, req.Enabled); err != nil { |
|
| 319 | +httpError(w, http.StatusBadRequest, err.Error()) |
|
| 320 | +return |
|
| 321 | +} |
|
| 322 | + writeJSON(w, http.StatusOK, map[string]any{"ok": true}) |
|
| 323 | +} |
|
| 324 | + | |
| 299 | 325 | func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) { |
| @@ -11,6 +11,9 @@ | ||
| 11 | 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. |
|
| 12 | +# Dasselbe Bild wird ausserdem als Startlogo des Players hinterlegt |
|
| 13 | +# (BOOT_LOGO). Das ist der zuverlaessige Teil: Viele Fernseher wachen erst |
|
| 14 | +# auf, wenn mpv den Bildmodus setzt - der Startbildschirm des Systems ist bis |
|
| 15 | +# dahin gar nicht zu sehen, beim Herunterfahren dagegen schon. |
|
| 16 | +# |
|
| 17 | +# --dauer N haelt zusaetzlich den System-Startbildschirm bis N Sekunden nach |
|
| 18 | +# dem Einschalten (0 = aus, Standard 0). |
|
| 16 | 19 | # =========================================================================== |
| @@ -25,3 +28,5 @@ HOLD_BIN="/usr/local/bin/tvwall-splash-hold" | ||
| 25 | 28 | HOLD_UNIT="/etc/systemd/system/tvwall-splash.service" |
| 26 | -DAUER=12 |
|
| 29 | +TVWALL_CONF="/etc/tvwall/tvwall.conf" |
|
| 30 | +BOOT_LOGO="/var/lib/tvwall/boot-logo.png" |
|
| 31 | +DAUER=0 |
|
| 27 | 32 | |
| @@ -55,2 +60,8 @@ if [[ "${1:-}" == "--zurueck" ]]; then | ||
| 55 | 60 | ok "Halten des Startbildes entfernt" |
| 61 | +if [[ -f "$TVWALL_CONF" ]]; then |
|
| 62 | +sed -i 's|^BOOT_LOGO=.*|BOOT_LOGO=""|' "$TVWALL_CONF" |
|
| 63 | +systemctl restart tvwall 2>/dev/null || true |
|
| 64 | +ok "Startlogo des Players abgeschaltet" |
|
| 65 | +fi |
|
| 66 | +rm -f "$BOOT_LOGO" |
|
| 56 | 67 | update-initramfs -u >/dev/null 2>&1 || true |
| @@ -96,2 +107,25 @@ fi | ||
| 96 | 107 | |
| 108 | +# --- dasselbe Bild als Startlogo des Players ------------------------------- |
|
| 109 | +# Der zuverlaessige Teil: mpv schaltet den Bildschirm ein, also sieht man |
|
| 110 | +# dieses Bild garantiert - unabhaengig davon, wann der Fernseher aufwacht. |
|
| 111 | +if [[ -d "$(dirname "$BOOT_LOGO")" ]]; then |
|
| 112 | +install -m 0644 "$TMP/splash.png" "$BOOT_LOGO" |
|
| 113 | +RUN_USER="$(sed -n 's/^RUN_USER="\(.*\)"/\1/p' "$TVWALL_CONF" 2>/dev/null | head -1)" |
|
| 114 | + [[ -n "${RUN_USER:-}" ]] && chown "$RUN_USER" "$BOOT_LOGO" 2>/dev/null || true |
|
| 115 | +ok "Startlogo des Players: $BOOT_LOGO" |
|
| 116 | + | |
| 117 | +if [[ -f "$TVWALL_CONF" ]]; then |
|
| 118 | +if grep -q "^BOOT_LOGO=" "$TVWALL_CONF"; then |
|
| 119 | +sed -i "s|^BOOT_LOGO=.*|BOOT_LOGO=\"$BOOT_LOGO\"|" "$TVWALL_CONF" |
|
| 120 | +else |
|
| 121 | +printf '\n# Startlogo, von set-splash.sh eingetragen\nBOOT_LOGO="%s"\nBOOT_LOGO_SECONDS="8"\n' \ |
|
| 122 | +"$BOOT_LOGO" >> "$TVWALL_CONF" |
|
| 123 | +fi |
|
| 124 | +ok "BOOT_LOGO in $TVWALL_CONF eingetragen" |
|
| 125 | +systemctl restart tvwall 2>/dev/null && ok "Dienst neu gestartet - das Logo ist sofort zu sehen" || true |
|
| 126 | +fi |
|
| 127 | +else |
|
| 128 | +warn "$(dirname "$BOOT_LOGO") fehlt - Startlogo des Players nicht gesetzt" |
|
| 129 | +fi |
|
| 130 | + | |
| 97 | 131 | # --- Startbild stehenlassen, bis die Wiedergabe uebernimmt ---------------- |
| @@ -99,4 +133,11 @@ fi | ||
| 99 | 133 | # 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" |
|
| 134 | +if [[ "$DAUER" == "0" ]]; then |
|
| 135 | +if [[ -f "$HOLD_UNIT" ]]; then |
|
| 136 | +systemctl disable --now tvwall-splash.service >/dev/null 2>&1 || true |
|
| 137 | +rm -f "$HOLD_UNIT" "$HOLD_BIN" |
|
| 138 | +systemctl daemon-reload |
|
| 139 | +ok "Halten des System-Startbildes entfernt (--dauer 0)" |
|
| 140 | +fi |
|
| 141 | +elif [[ -x /usr/bin/plymouth ]]; then |
|
| 142 | +info "System-Startbild bis $DAUER s nach dem Einschalten halten" |
|
| 102 | 143 | cat > "$HOLD_BIN" <<'HOLD' |
| @@ -149,5 +190,8 @@ cat <<EOF | ||
| 149 | 190 | =========================================================================== |
| 150 | -Fertig. Beim naechsten Neustart erscheint dein Logo und bleibt bis |
|
| 151 | -$DAUER Sekunden nach dem Einschalten stehen - danach uebernimmt die |
|
| 152 | -Wiedergabe. |
|
| 191 | +Fertig. |
|
| 192 | + | |
| 193 | +- Der Player zeigt das Logo beim Start 8 Sekunden lang, bevor die |
|
| 194 | +Playliste beginnt. Das sieht man sicher, weil mpv den Bildschirm |
|
| 195 | +einschaltet. |
|
| 196 | +- Beim Herunterfahren zeigt das System denselben Bildschirm. |
|
| 153 | 197 | |
| @@ -155,4 +199,4 @@ cat <<EOF | ||
| 155 | 199 | |
| 156 | -Anders lange stehen lassen: sudo $0 <logo> --dauer 20 |
|
| 157 | -Zurueck zum Original: sudo $0 --zurueck |
|
| 200 | +Anzeigedauer aendern: BOOT_LOGO_SECONDS in $TVWALL_CONF |
|
| 201 | +Zurueck zum Original: sudo $0 --zurueck |
|
| 158 | 202 | =========================================================================== |
| @@ -0,0 +1,137 @@ | ||
| 1 | +#!/usr/bin/env bash |
|
| 2 | +# =========================================================================== |
|
| 3 | +# Entfernt den Textbildschirm zwischen Startlogo und Wiedergabe. |
|
| 4 | +# |
|
| 5 | +# Drei Aenderungen: |
|
| 6 | +# 1. Konsolenmeldungen auf tty3 umleiten - tty1 bleibt sauber |
|
| 7 | +# 2. Plymouth wird erst beendet, wenn tvwall startet - und zwar von tvwall |
|
| 8 | +# selbst, direkt bevor mpv losgeht. Mit --retain-splash bleibt das Logo |
|
| 9 | +# dabei stehen, bis mpv das erste Mal malt. |
|
| 10 | +# 3. tvwall raeumt tty1 nicht mehr auf (das hat das Bild geloescht), |
|
| 11 | +# leert aber einmal dessen Textpuffer - sonst taucht Plymouths |
|
| 12 | +# Statustext wieder auf, sobald die Wiedergabe endet |
|
| 13 | +# |
|
| 14 | +# Aufruf: sudo ~/silent-boot.sh einschalten |
|
| 15 | +# sudo ~/silent-boot.sh --zurueck rueckgaengig |
|
| 16 | +# =========================================================================== |
|
| 17 | +set -euo pipefail |
|
| 18 | +CMDLINE="/boot/firmware/cmdline.txt"; [[ -f "$CMDLINE" ]] || CMDLINE="/boot/cmdline.txt" |
|
| 19 | +QUIT_DIR="/etc/systemd/system/plymouth-quit.service.d" |
|
| 20 | +TVWALL_DIR="/etc/systemd/system/tvwall.service.d" |
|
| 21 | +EXTRA=(loglevel=3 logo.nologo vt.global_cursor_default=0 consoleblank=0) |
|
| 22 | + | |
| 23 | +ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; } |
|
| 24 | +info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } |
|
| 25 | +die() { printf '\033[1;31mFehler:\033[0m %s\n' "$*" >&2; exit 1; } |
|
| 26 | +[[ $EUID -eq 0 ]] || die "bitte mit sudo starten" |
|
| 27 | + | |
| 28 | +schreibe_cmdline() { |
|
| 29 | +local zeile="$1" |
|
| 30 | +cp -f "$CMDLINE" "$CMDLINE.tvwall-backup" |
|
| 31 | +printf '%s\n' "$zeile" > "$CMDLINE" |
|
| 32 | +if [[ "$(grep -c . "$CMDLINE")" != "1" ]] || ! grep -q "root=" "$CMDLINE"; then |
|
| 33 | +cp -f "$CMDLINE.tvwall-backup" "$CMDLINE" |
|
| 34 | +die "cmdline.txt sah falsch aus - Sicherung zurueckgespielt" |
|
| 35 | +fi |
|
| 36 | +} |
|
| 37 | + | |
| 38 | +if [[ "${1:-}" == "--zurueck" ]]; then |
|
| 39 | +zeile="$(tr -d '\n' < "$CMDLINE")" |
|
| 40 | + zeile="${zeile//console=tty3/console=tty1}" |
|
| 41 | + for e in "${EXTRA[@]}"; do zeile="${zeile// $e/}"; done |
|
| 42 | +schreibe_cmdline "$zeile" |
|
| 43 | +rm -rf "$QUIT_DIR" "$TVWALL_DIR/silent.conf" /usr/local/bin/tvwall-clear-tty |
|
| 44 | +systemctl daemon-reload |
|
| 45 | +ok "rueckgaengig gemacht - bitte neu starten" |
|
| 46 | +exit 0 |
|
| 47 | +fi |
|
| 48 | + | |
| 49 | +# --- 1) Konsole von tty1 wegnehmen ---------------------------------------- |
|
| 50 | +info "Konsolenmeldungen auf tty3 umleiten" |
|
| 51 | +zeile="$(tr -d '\n' < "$CMDLINE")" |
|
| 52 | +zeile="${zeile//console=tty1/console=tty3}" |
|
| 53 | +for e in "${EXTRA[@]}"; do |
|
| 54 | + zeile="${zeile// $e/}" # doppelte Eintraege vermeiden |
|
| 55 | +zeile="$zeile $e" |
|
| 56 | +done |
|
| 57 | +schreibe_cmdline "$zeile" |
|
| 58 | +ok "console=tty3 ${EXTRA[*]}" |
|
| 59 | +echo " ($(grep -c . "$CMDLINE") Zeile, root= vorhanden)" |
|
| 60 | + | |
| 61 | +# --- 2) Logo stehen lassen, bis mpv malt ---------------------------------- |
|
| 62 | +info "Plymouth soll das Bild beim Beenden stehen lassen" |
|
| 63 | +install -d -m 0755 "$QUIT_DIR" |
|
| 64 | +cat > "$QUIT_DIR/retain-splash.conf" <<'UNIT' |
|
| 65 | +# Von tvwall silent-boot.sh: |
|
| 66 | +# - --retain-splash laesst das Startbild auf dem Schirm stehen, bis mpv das |
|
| 67 | +# erste Mal malt. Ohne das gibt Plymouth den Bildschirm frei und die |
|
| 68 | +# Textkonsole blitzt auf. |
|
| 69 | +# - After=tvwall.service: nicht schon beenden, waehrend die Wiedergabe noch |
|
| 70 | +# startet. tvwall beendet Plymouth selbst, direkt bevor mpv losgeht. |
|
| 71 | +[Unit] |
|
| 72 | +After=tvwall.service |
|
| 73 | + | |
| 74 | +[Service] |
|
| 75 | +ExecStart= |
|
| 76 | +ExecStart=-/usr/bin/plymouth quit --retain-splash |
|
| 77 | +UNIT |
|
| 78 | +ok "$QUIT_DIR/retain-splash.conf" |
|
| 79 | + | |
| 80 | +# --- 3) tvwall soll tty1 nicht mehr loeschen ------------------------------ |
|
| 81 | +info "Textpuffer von tty1 leeren" |
|
| 82 | +cat > /usr/local/bin/tvwall-clear-tty <<'CLR' |
|
| 83 | +#!/bin/sh |
|
| 84 | +# Leert den Textpuffer der virtuellen Konsole tty1. |
|
| 85 | +# Plymouth legt dort neben der Grafikausgabe eine Textanzeige an und schreibt |
|
| 86 | +# seine Statuszeilen hinein. Sichtbar wird das erst, wenn die Wiedergabe endet |
|
| 87 | +# und die Konsole ihren Inhalt neu zeichnet - dann steht dort alter Text. |
|
| 88 | +# Wird ausgefuehrt, solange Plymouth den Bildschirm noch im Grafikmodus haelt: |
|
| 89 | +# der Puffer wird geleert, ohne dass davon etwas zu sehen ist. |
|
| 90 | +printf '[3J[2J[H' > /dev/tty1 2>/dev/null |
|
| 91 | +exit 0 |
|
| 92 | +CLR |
|
| 93 | +chmod 0755 /usr/local/bin/tvwall-clear-tty |
|
| 94 | +ok "/usr/local/bin/tvwall-clear-tty" |
|
| 95 | + | |
| 96 | +info "tvwall raeumt tty1 nicht mehr auf" |
|
| 97 | +install -d -m 0755 "$TVWALL_DIR" |
|
| 98 | +cat > "$TVWALL_DIR/silent.conf" <<'UNIT' |
|
| 99 | +# Von tvwall silent-boot.sh: |
|
| 100 | +# - TTYVTDisallocate/setterm haben das stehengelassene Startbild geloescht. |
|
| 101 | +# Der Bildschirmschoner der Konsole wird jetzt ueber consoleblank=0 |
|
| 102 | +# im Bootparameter abgeschaltet. |
|
| 103 | +# - ExecStartPre uebergibt den Bildschirm: Plymouth gibt ihn frei (das Bild |
|
| 104 | +# bleibt dank --retain-splash stehen), direkt danach uebernimmt mpv. |
|
| 105 | +# Das "+" laesst diesen einen Schritt mit vollen Rechten laufen. |
|
| 106 | +[Service] |
|
| 107 | +TTYVTDisallocate=no |
|
| 108 | +NoNewPrivileges=no |
|
| 109 | +ExecStartPre= |
|
| 110 | +ExecStartPre=+/usr/local/bin/tvwall-clear-tty |
|
| 111 | +ExecStartPre=+/bin/sh -c '/usr/bin/plymouth quit --retain-splash 2>/dev/null || true' |
|
| 112 | +ExecStopPost=+/usr/local/bin/tvwall-clear-tty |
|
| 113 | +UNIT |
|
| 114 | +ok "$TVWALL_DIR/silent.conf" |
|
| 115 | + | |
| 116 | +systemctl daemon-reload |
|
| 117 | +ok "systemd neu eingelesen" |
|
| 118 | + | |
| 119 | +cat <<EOF |
|
| 120 | + | |
| 121 | +=========================================================================== |
|
| 122 | +Jetzt neu starten: sudo reboot |
|
| 123 | + | |
| 124 | +Erwartet: Logo erscheint - bleibt stehen - Wiedergabe uebernimmt. |
|
| 125 | +Kein Text mehr dazwischen. |
|
| 126 | + | |
| 127 | +Zur Kontrolle danach: |
|
| 128 | +systemd-analyze critical-chain tvwall.service | head -5 |
|
| 129 | +journalctl -b -o short-monotonic | grep -E "plymouth-quit|starte mpv" |
|
| 130 | +-> die beiden Zeitpunkte sollten dicht beieinander liegen |
|
| 131 | + | |
| 132 | +Rueckgaengig: sudo ~/silent-boot.sh --zurueck && sudo reboot |
|
| 133 | + | |
| 134 | +Hinweis: Startmeldungen liegen danach auf tty3 (Strg+Alt+F3) und |
|
| 135 | +weiterhin vollstaendig im Journal: journalctl -b |
|
| 136 | +=========================================================================== |
|
| 137 | +EOF |