diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index c6b3018..e296ae3 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -4862,13 +4862,13 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wird eine Datei größer als die eingestellte Grenze, rechnet der Server sie unterwegs herunter und passt die Bitrate laufend an die Verbindung an." + "value" : "Die Obergrenze, nicht das Ziel. Im eigenen Netz überträgt der Server das Original; unterwegs beginnt Flixy kleiner und tastet sich hinauf, solange die Verbindung es trägt — höchstens bis zu dieser Stufe. Auflösung und Bitrate gehen dabei zusammen." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "If a file exceeds the limit, the server transcodes it on the fly and adapts the bitrate to your connection." + "value" : "An upper limit, not a target. On your own network the server sends the original; away from home Flixy starts lower and works its way up as long as the connection holds — never above this step. Resolution and bitrate move together." } } } diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index 926c82c..f538d9d 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -227,17 +227,63 @@ final class PlayerModel { /// als ein Sender, der gar nicht mehr angeht. private var downgradeBudget: Int { isLive ? 1 : 2 } + /// **Womit „Auto" anfängt.** + /// + /// Hier lag der Denkfehler: `automatic` hiess wörtlich „nimm, was in den + /// Einstellungen steht", und stand dort „Original", bat die App über eine + /// Zugverbindung um eine 4K-Datei. Das ist keine Automatik, das ist eine + /// Durchreiche. + /// + /// Jetzt ist die Einstellung die **Obergrenze**, nicht das Ziel: „bis + /// hierhin, wenn die Leitung es trägt". Angefangen wird mit einem Wert, + /// der fast überall anläuft; von dort geht es hinauf, solange es gut + /// geht, und hinunter, sobald es hakt. + /// + /// **Im eigenen Netz keine Grenze.** Dort trägt die Leitung das Original, + /// und ein Deckel machte das Bild ohne Grund schlechter. Die + /// Unterscheidung ist nicht geraten — sie steht in `isLocalConnection`. + private var startingCeiling: Int? { + guard !isLocalConnection else { return nil } + // Acht Megabit tragen 1080p ordentlich und laufen auch über eine + // mässige Fernverbindung an. Wer mehr will und mehr hat, kommt über + // das Hinaufschalten dorthin. + return 8_000 + } + + /// Die Auflösung, die zur aktuellen Stufe gehört. + /// + /// **Bitrate allein genügt nicht.** 4K mit zwei Megabit sieht schlechter + /// aus als 720p mit zwei Megabit — dieselbe Datenmenge, auf viermal so + /// viele Bildpunkte verteilt. Wer die Bitrate senkt, muss die Auflösung + /// mitnehmen, sonst spart er Daten und verschenkt das Bild. + private var automaticResolution: String? { + guard quality == .automatic, let ceiling = automaticCeiling else { return nil } + let sprossen = PlayerQuality.available(forSourceHeight: sourceHeight) + .compactMap { option -> PlayerQuality.Rung? in + guard case .limited(let rung) = option else { return nil } + return rung + } + // Die Sprosse, die zu dieser Bitrate gehört — oder die nächstgrössere, + // falls die Zahl von woanders kommt. + let passend = sprossen.first { $0.bitrate >= ceiling } ?? sprossen.last + guard let passend else { return nil } + return "\(passend.width)x\(passend.height)" + } + /// Die Richtlinie, wie sie **jetzt** gilt. /// - /// Die eingestellte Obergrenze, überschrieben von dem, was die Automatik + /// Die eingestellte Obergrenze, gedeckelt von dem, was die Automatik /// unterwegs gelernt hat. Getrennt von `quality`, damit eine schlechte /// Zugfahrt nicht als gemerkte Stufe das Wohnzimmer verschlechtert. private var effectivePolicy: PlaybackPolicy { let base = quality.policy(basedOn: policy) - guard quality == .automatic, let ceiling = automaticCeiling else { return base } + guard quality == .automatic else { return base } + + // Die Einstellung ist die Decke, die Automatik der Boden darunter. + guard let ceiling = automaticCeiling ?? startingCeiling else { return base } return PlaybackPolicy( allowsTranscoding: true, - // Ohne Ersatzmotor: sonst spielte VLC die Datei unverändert ab, + // Ohne Ersatz-Engine: sonst spielte VLC die Datei unverändert ab, // und die abgesenkte Grenze bliebe wirkungslos. allowsFallbackEngine: false, maximumBitrate: min(ceiling, base.maximumBitrate ?? ceiling), @@ -578,7 +624,9 @@ final class PlayerModel { case .transcode(let maximumBitrate): TranscodeSettings( maximumBitrate: maximumBitrate, - resolution: quality.resolution, + // Bei „Auto" die Auflösung zur gewählten Stufe, sonst die der + // ausdrücklich gewählten. + resolution: quality.resolution ?? automaticResolution, // **Nur bei „automatisch".** Eine ausdrücklich gewählte Stufe // ist eine Ansage, keine Obergrenze: wer „720p · 3 Mbps" // antippt, bekommt das und nicht „höchstens 3, meistens @@ -845,7 +893,14 @@ final class PlayerModel { if lastStepUp == nil { self.lastStepUp = now } return } - guard let höher = sprossen.first(where: { $0 > aktuell }) else { return } + // **Bis zur eingestellten Decke, nicht bis zur Startstufe.** Wer + // „Original" eingestellt hat und eine Leitung hat, die es trägt, + // soll dort auch ankommen — das ist die andere Hälfte von + // „richtet sich nach der Verbindung". + let decke = quality.policy(basedOn: policy).maximumBitrate + guard let höher = sprossen.first(where: { $0 > aktuell }), + decke.map({ höher <= $0 }) ?? true + else { return } ziel = höher }