Halo heisst ab hier Flaro — die Marke stand im Weg
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f98b995050
commit
a7cf6c5def
@@ -0,0 +1,146 @@
|
||||
import Foundation
|
||||
import MediaKit
|
||||
|
||||
/// Entscheidet, wie ein Titel abgespielt wird.
|
||||
///
|
||||
/// Reine Rechnung ohne Netz und ohne Zustand — deshalb vollständig testbar.
|
||||
/// Die zugehörige Testtabelle in `PlaybackDeciderTests` ist die wichtigste im
|
||||
/// Projekt: sie hält fest, welche Datei auf welchem Weg landet, und schlägt
|
||||
/// an, sobald jemand die Fähigkeitenliste anfasst.
|
||||
public struct PlaybackDecider: Sendable {
|
||||
private let capabilities: DeviceCapabilities
|
||||
|
||||
public init(capabilities: DeviceCapabilities = .avFoundation) {
|
||||
self.capabilities = capabilities
|
||||
}
|
||||
|
||||
public func decide(
|
||||
profile: MediaProfile,
|
||||
policy: PlaybackPolicy
|
||||
) -> PlaybackDecision {
|
||||
var rationale: [PlaybackDecision.Rationale] = []
|
||||
|
||||
// 0. Hat jemand ausdrücklich eine Stufe gewählt, wird umgerechnet —
|
||||
// auch wenn die Datei die Grenze gar nicht überschreitet. Sonst
|
||||
// bliebe die Wahl folgenlos: für eine 8-Mbit-Datei „12 Mbit" zu
|
||||
// wählen ergäbe DirectPlay, und am Bild änderte sich nichts.
|
||||
if policy.forcesTranscoding, policy.allowsTranscoding {
|
||||
rationale.append(.qualityChosen)
|
||||
return PlaybackDecision(
|
||||
method: .transcode(maximumBitrate: policy.maximumBitrate),
|
||||
rationale: rationale
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Reicht die Leitung? Das kommt zuerst, weil es unabhängig vom
|
||||
// Format ist: eine 80-Mbit-Datei über Mobilfunk ist auch dann keine
|
||||
// gute Idee, wenn AVPlayer sie problemlos öffnen könnte.
|
||||
if let limit = policy.maximumBitrate, let bitrate = profile.bitrate, bitrate > limit {
|
||||
rationale.append(.bitrateExceedsLimit(mediaBitrate: bitrate, limit: limit))
|
||||
if policy.allowsTranscoding {
|
||||
return PlaybackDecision(method: .transcode(maximumBitrate: limit), rationale: rationale)
|
||||
}
|
||||
// Ohne Transcoder bleibt nur, es trotzdem zu versuchen. Das ist
|
||||
// ehrlicher als eine Fehlermeldung: im WLAN klappt es meistens,
|
||||
// und der Grund steht in der Begründung.
|
||||
rationale.append(.transcodingUnavailable)
|
||||
}
|
||||
|
||||
// 2. Kennt der Server die Datei überhaupt?
|
||||
let container = normalized(profile.container)
|
||||
guard profile.hasKnownStreams, let videoCodec = normalized(profile.videoCodec) else {
|
||||
rationale.append(.streamsUnknown)
|
||||
return fallback(policy: policy, rationale: rationale)
|
||||
}
|
||||
|
||||
// 3. Kann AVPlayer das?
|
||||
var blockers: [PlaybackDecision.Rationale] = []
|
||||
if let container, !capabilities.containers.contains(container) {
|
||||
blockers.append(.containerUnsupported(container))
|
||||
}
|
||||
if !capabilities.videoCodecs.contains(videoCodec) {
|
||||
blockers.append(.videoCodecUnsupported(videoCodec))
|
||||
}
|
||||
// Geprüft wird die Spur, die tatsächlich klingen soll — welche das
|
||||
// ist, hat der Aufrufer beim Bauen des Profils entschieden.
|
||||
if let audioCodec = normalized(profile.audioCodec),
|
||||
!capabilities.audioCodecs.contains(audioCodec) {
|
||||
blockers.append(.audioCodecUnsupported(audioCodec))
|
||||
}
|
||||
|
||||
guard blockers.isEmpty else {
|
||||
rationale.append(contentsOf: blockers)
|
||||
|
||||
// **Solange das Bild durchgereicht werden kann, hilft der Server.**
|
||||
//
|
||||
// Zwei Fälle, und beide enden hier. Steht nur die Schachtel im
|
||||
// Weg — Matroska aussen, H.264 oder HEVC innen, AC3 dazu —, wird
|
||||
// umgepackt: dieselben Ströme, andere Verpackung, nichts geht
|
||||
// verloren. Steht der **Ton** im Weg — DTS, TrueHD, Vorbis —,
|
||||
// rechnet der Server genau diese eine Spur um und kopiert das
|
||||
// Bild. Beides zusammen geht ebenso.
|
||||
//
|
||||
// Das kostet Tonqualität, und zwar nur beim Ton. Dagegen steht,
|
||||
// was es einbringt: der Titel läuft überhaupt — mit Bild-in-Bild,
|
||||
// AirPlay und Sperrbildschirm, und ohne dass das ganze Bild neu
|
||||
// gerechnet werden muss.
|
||||
//
|
||||
// Stimmt dagegen der **Videocodec** nicht, ist hier Schluss: dann
|
||||
// fehlt nicht der Zugang, sondern das Können, und jeder Weg über
|
||||
// den Server hiesse, jedes Bild neu zu rechnen.
|
||||
let bildBleibt = blockers.allSatisfy {
|
||||
switch $0 {
|
||||
case .containerUnsupported, .audioCodecUnsupported: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
// Umpackbar muss die Schachtel nur sein, wenn sie überhaupt im
|
||||
// Weg steht. Eine MP4 mit DTS-Ton bleibt die MP4, die sie ist —
|
||||
// sie an dieser Liste zu messen, hiesse sie an einer Frage
|
||||
// scheitern zu lassen, die sich gar nicht stellt.
|
||||
let schachtelTraegt = container.map {
|
||||
capabilities.containers.contains($0)
|
||||
|| DeviceCapabilities.remuxableContainers.contains($0)
|
||||
} ?? true
|
||||
if bildBleibt, schachtelTraegt, policy.allowsTranscoding {
|
||||
return PlaybackDecision(method: .remux, rationale: rationale)
|
||||
}
|
||||
|
||||
return fallback(policy: policy, rationale: rationale)
|
||||
}
|
||||
|
||||
rationale.append(.nativelySupported)
|
||||
return PlaybackDecision(method: .directPlay, rationale: rationale)
|
||||
}
|
||||
|
||||
/// Was passiert, wenn AVPlayer nicht kann.
|
||||
///
|
||||
/// **Hier stand einmal eine zweite Engine.** VLCKit hatte Vorrang vor dem
|
||||
/// Transcoder: die Datei ging unverändert über die Leitung, der Server
|
||||
/// musste nicht rechnen. Es gibt sie nicht mehr — was der Systemplayer
|
||||
/// nicht öffnet, rechnet der Server um, und was heruntergeladen wird,
|
||||
/// kommt schon in einer Hülle, die er öffnet.
|
||||
private func fallback(
|
||||
policy: PlaybackPolicy,
|
||||
rationale: [PlaybackDecision.Rationale]
|
||||
) -> PlaybackDecision {
|
||||
if policy.allowsTranscoding {
|
||||
return PlaybackDecision(
|
||||
method: .transcode(maximumBitrate: policy.maximumBitrate),
|
||||
rationale: rationale
|
||||
)
|
||||
}
|
||||
var rationale = rationale
|
||||
rationale.append(.transcodingUnavailable)
|
||||
return PlaybackDecision(method: .directPlay, rationale: rationale)
|
||||
}
|
||||
|
||||
/// Plex schreibt Codecs mal groß, mal klein, und hängt bei Containern
|
||||
/// gelegentlich Punkte an. Ein Vergleich ohne diese Angleichung geht
|
||||
/// sporadisch schief — und zwar nur bei manchen Dateien, was die Suche
|
||||
/// nach dem Grund unangenehm macht.
|
||||
private func normalized(_ value: String?) -> String? {
|
||||
guard let value, !value.isEmpty else { return nil }
|
||||
return value.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: ". "))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user