Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
6.2 KiB
Swift
166 lines
6.2 KiB
Swift
import Foundation
|
|
|
|
/// Was abgespielt werden soll und wie.
|
|
///
|
|
/// Die Entscheidung *ob* umgerechnet wird, fällt vorher — im
|
|
/// `PlaybackDecider`, aus Profil und Leitung. Hier steht nur noch, was der
|
|
/// Server daraufhin bereitstellen soll. Das ist die Trennung, die es vorher
|
|
/// nicht gab: der Player hat Plex' Transcoder unmittelbar angesprochen und
|
|
/// konnte deshalb mit keinem anderen Server reden.
|
|
public struct PlaybackIntent: Sendable, Equatable {
|
|
public let itemID: String
|
|
/// Welche Fassung — ein Film kann mehrfach vorliegen.
|
|
public let versionIndex: Int
|
|
/// Die Datei innerhalb der Fassung. Plex adressiert Dateien, Jellyfin
|
|
/// Quellen; beide finden sich hierüber wieder.
|
|
public let file: MediaFile
|
|
/// `nil` heißt: unverändert übertragen.
|
|
public let transcode: TranscodeSettings?
|
|
public let startTime: Duration
|
|
/// Ob der Server im selben Netz steht. Plex rechnet dann anders.
|
|
public let isLocalConnection: Bool
|
|
/// Welche Tonspur gespielt werden soll.
|
|
///
|
|
/// **Ohne diese Angabe wählt der Server.** Bei einem umgerechneten Stream
|
|
/// gibt es danach nur noch *eine* Tonspur — was hier nicht gesagt wird,
|
|
/// lässt sich später nicht mehr umstellen, weil im Player schlicht keine
|
|
/// zweite ankommt. Genau daran scheiterte die Sprachwahl bisher: sie lief
|
|
/// auf einer Liste mit einem Eintrag.
|
|
public let audioStreamIndex: Int?
|
|
/// Welcher Untertitel eingebrannt oder mitgeliefert werden soll.
|
|
///
|
|
/// Dieselbe Lage wie beim Ton: rechnet der Server um, liefert er genau
|
|
/// den einen aus, für den er sich entschieden hat. Wer erst im Player
|
|
/// wählen will, hat dann nichts mehr zur Auswahl.
|
|
///
|
|
/// **`-1` heisst ausdrücklich „keine".** Nicht dasselbe wie `nil`: das
|
|
/// hiesse „such dir was aus", und der Server sucht sich dann durchaus
|
|
/// etwas aus.
|
|
public let subtitleStreamIndex: Int?
|
|
|
|
public init(
|
|
itemID: String,
|
|
versionIndex: Int,
|
|
file: MediaFile,
|
|
transcode: TranscodeSettings? = nil,
|
|
startTime: Duration = .zero,
|
|
isLocalConnection: Bool = false,
|
|
audioStreamIndex: Int? = nil,
|
|
subtitleStreamIndex: Int? = nil
|
|
) {
|
|
self.itemID = itemID
|
|
self.versionIndex = versionIndex
|
|
self.file = file
|
|
self.transcode = transcode
|
|
self.startTime = startTime
|
|
self.isLocalConnection = isLocalConnection
|
|
self.audioStreamIndex = audioStreamIndex
|
|
self.subtitleStreamIndex = subtitleStreamIndex
|
|
}
|
|
}
|
|
|
|
/// Wie weit heruntergerechnet werden soll.
|
|
public struct TranscodeSettings: Sendable, Equatable {
|
|
/// In Kilobit je Sekunde. `nil` heißt „so gut wie möglich".
|
|
public let maximumBitrate: Int?
|
|
/// Etwa „1280x720". `nil` lässt die Auflösung, wie sie ist.
|
|
public let resolution: String?
|
|
/// Ob der Server eine **Leiter** liefern soll statt einer festen Stufe.
|
|
///
|
|
/// Dann enthält die Playlist mehrere Varianten, und der Player wechselt
|
|
/// zwischen ihnen, ohne dass der Stream neu aufgebaut werden muss. Die
|
|
/// angegebene Bitrate bleibt dabei die **oberste** Sprosse; was darunter
|
|
/// liegt, erfindet der Server.
|
|
///
|
|
/// Nur bei `automatic` gesetzt. Wer im Sheet ausdrücklich „720p · 3 Mbps"
|
|
/// antippt, soll das bekommen und nicht „höchstens 3, meistens weniger"
|
|
/// unter unverändertem Etikett.
|
|
public let prefersAdaptive: Bool
|
|
/// Nur umpacken, nicht neu rechnen.
|
|
///
|
|
/// Dann werden die Ströme kopiert statt kodiert, und heraus kommt eine
|
|
/// **fortlaufende** MP4 statt einer HLS-Abspielliste. Beides ist wichtig:
|
|
/// kopieren verliert nichts und kostet den Server fast nichts, und eine
|
|
/// fortlaufende Datei lässt sich beliebig weit vorausladen — eine
|
|
/// Abspielliste nicht.
|
|
public let prefersRemux: Bool
|
|
|
|
public init(
|
|
maximumBitrate: Int?,
|
|
resolution: String?,
|
|
prefersAdaptive: Bool = false,
|
|
prefersRemux: Bool = false
|
|
) {
|
|
self.maximumBitrate = maximumBitrate
|
|
self.resolution = resolution
|
|
self.prefersAdaptive = prefersAdaptive
|
|
self.prefersRemux = prefersRemux
|
|
}
|
|
}
|
|
|
|
/// Was der Server für diese Wiedergabe bereitgestellt hat.
|
|
public struct PlaybackTarget: Sendable, Equatable {
|
|
public let url: URL
|
|
/// Die Sitzung, die der Server dafür angelegt hat.
|
|
///
|
|
/// Zum Aufräumen: ohne dieses Kennzeichen rechnet der Transcoder weiter,
|
|
/// wenn längst niemand mehr zusieht.
|
|
public let sessionID: String?
|
|
/// Welche Fassung gespielt wird.
|
|
///
|
|
/// Jellyfin will sie bei jeder Fortschrittsmeldung zurückhaben, sonst
|
|
/// ordnet es die Meldung keiner Wiedergabe zu.
|
|
public let versionID: String?
|
|
|
|
public init(url: URL, sessionID: String? = nil, versionID: String? = nil) {
|
|
self.url = url
|
|
self.sessionID = sessionID
|
|
self.versionID = versionID
|
|
}
|
|
}
|
|
|
|
/// In welchem Zustand sich die Wiedergabe befindet.
|
|
public enum PlaybackReportState: String, Sendable {
|
|
/// Der erste Bericht einer Wiedergabe.
|
|
///
|
|
/// Eigener Fall, weil Jellyfin dafür einen eigenen Endpunkt hat: ohne ihn
|
|
/// erscheint der Titel nirgends als „läuft gerade", und die Sitzung, unter
|
|
/// der die weiteren Meldungen laufen, ist dem Server unbekannt. Plex
|
|
/// unterscheidet das nicht und behandelt ihn wie „spielt".
|
|
case started
|
|
case playing
|
|
case paused
|
|
/// Beim Verlassen des Players. Erst dieser Wert nimmt den Titel auf dem
|
|
/// Server aus „wird gerade abgespielt" heraus.
|
|
case stopped
|
|
case buffering
|
|
}
|
|
|
|
/// Wo man gerade steht.
|
|
public struct PlaybackReport: Sendable, Equatable {
|
|
public let itemID: String
|
|
public let sessionID: String?
|
|
public let versionID: String?
|
|
public let state: PlaybackReportState
|
|
public let time: Duration
|
|
public let duration: Duration
|
|
|
|
public init(
|
|
itemID: String,
|
|
sessionID: String? = nil,
|
|
versionID: String? = nil,
|
|
state: PlaybackReportState,
|
|
time: Duration,
|
|
duration: Duration
|
|
) {
|
|
self.itemID = itemID
|
|
self.sessionID = sessionID
|
|
self.versionID = versionID
|
|
self.state = state
|
|
self.time = time
|
|
self.duration = duration
|
|
}
|
|
}
|
|
|
|
extension PlaybackReportState: Equatable {}
|