Player: die Qualitätsleiter des Servers, Bitrate im Blick, Doppeltipp
Die vier erfundenen Stufen weichen der Leiter, die der Server selbst meldet: 64, 96, 208, 320, 720, 1500, 2000, 3000, 4000, 8000, 10000, 12000, 20000 kbps mit den Auflösungen, die Plex dafür vorsieht. Eine eigene Leiter hieße, Stufen anzubieten, die der Transcoder nicht kennt — er rundet dann still auf die nächste, und die Anzeige log. Angeboten wird nur, was zur Quelle passt. Eine 720p-Datei auf 1080p hochzurechnen bringt kein Bild dazu, und die Stufen sähen alle gleich aus. Oben im Blatt steht, was tatsächlich über die Leitung kommt — gemessene Bitrate, Auflösung, verworfene Bilder. Der Unterschied zur gewählten Stufe ist keine Haarspalterei: die eine sagt, was erlaubt ist, die andere, was ankommt. Ruckelt es trotz hoher Stufe, steht die Antwort dort. „Nur Ton" gibt es, aber ehrlich beschriftet: Plex kennt für Videos keinen reinen Tonstrom, also wird die unterste Sprosse geholt und das Bild ausgeblendet. Dazu Doppeltipp links und rechts für zehn Sekunden zurück und vor. Die Reihenfolge der Gesten ist entscheidend — das einfache Tippen verschluckt sonst das doppelte. Die Kachel hängt nicht mehr in vier Ecken fest, sondern klebt links oder rechts und ist in der Höhe frei. Vier Ecken sind schnell gebaut, aber im Weg: steht sie über einem Knopf, bleiben drei Orte, die ebenso stören. Größer wird sie durch Zusammen- und Auseinanderziehen; der Griff ist weg, er saß zwangsläufig auf dem Bild und stritt sich mit dem Verschieben. Und die Begriffe: „Motor" für engine sagt kein deutscher Entwickler. Fachwörter, die im Deutschen englisch bleiben, bleiben jetzt englisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c2ddc10871
commit
12bb49615b
@@ -13,42 +13,55 @@ struct MiniPlayerPlacementTests {
|
||||
inset: 12
|
||||
)
|
||||
|
||||
@Test("Jede Bildschirmhälfte führt in ihre Ecke")
|
||||
func picksTheNearestCorner() {
|
||||
#expect(placement.nearestCorner(toCenter: CGPoint(x: 50, y: 50)) == .topLeading)
|
||||
#expect(placement.nearestCorner(toCenter: CGPoint(x: 340, y: 50)) == .topTrailing)
|
||||
#expect(placement.nearestCorner(toCenter: CGPoint(x: 50, y: 800)) == .bottomLeading)
|
||||
#expect(placement.nearestCorner(toCenter: CGPoint(x: 340, y: 800)) == .bottomTrailing)
|
||||
@Test("Die nähere Seite gewinnt")
|
||||
func picksTheNearestSide() {
|
||||
#expect(placement.nearestSide(toCenter: CGPoint(x: 50, y: 400)) == .leading)
|
||||
#expect(placement.nearestSide(toCenter: CGPoint(x: 340, y: 400)) == .trailing)
|
||||
}
|
||||
|
||||
@Test("Genau auf der Mitte gewinnt unten rechts")
|
||||
func decidesTheTie() {
|
||||
// Willkürlich, aber festgelegt: ohne eine Regel für den Gleichstand
|
||||
// hinge das Ergebnis an Fließkomma-Zufall, und dieselbe Geste führte
|
||||
// mal hierhin, mal dorthin.
|
||||
let center = CGPoint(x: 195, y: 422)
|
||||
@Test("Die Höhe bleibt frei, nicht auf Ecken festgelegt")
|
||||
func keepsTheVerticalPosition() {
|
||||
// Der Kern des Umbaus: vier Ecken sind schnell gebaut, aber im Weg.
|
||||
let middle = placement.center(side: .trailing, verticalFraction: 0.5)
|
||||
let top = placement.center(side: .trailing, verticalFraction: 0)
|
||||
let bottom = placement.center(side: .trailing, verticalFraction: 1)
|
||||
|
||||
#expect(placement.nearestCorner(toCenter: center) == .bottomTrailing)
|
||||
#expect(top.y < middle.y)
|
||||
#expect(middle.y < bottom.y)
|
||||
#expect(top.x == middle.x && middle.x == bottom.x)
|
||||
}
|
||||
|
||||
@Test("In jeder Ecke bleibt der Sicherheitsabstand")
|
||||
func keepsTheInset() {
|
||||
for corner in MiniPlayerPlacement.Corner.allCases {
|
||||
let start = placement.origin(for: corner)
|
||||
@Test("Anteil und Mittelpunkt rechnen ineinander um")
|
||||
func roundTripsTheFraction() {
|
||||
for fraction in [0.0, 0.25, 0.5, 0.9, 1.0] {
|
||||
let center = placement.center(side: .leading, verticalFraction: fraction)
|
||||
let back = placement.verticalFraction(forCenter: center)
|
||||
|
||||
#expect(start.x >= placement.inset)
|
||||
#expect(start.y >= placement.inset)
|
||||
#expect(start.x + placement.tile.width <= 390 - placement.inset)
|
||||
#expect(start.y + placement.tile.height <= 844 - placement.inset)
|
||||
#expect(abs(back - fraction) < 0.0001)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unten rechts sitzt die Kachel auch wirklich unten rechts")
|
||||
func computesTheFarCorner() {
|
||||
let start = placement.origin(for: .bottomTrailing)
|
||||
@Test("Der Anteil bleibt zwischen null und eins")
|
||||
func clampsTheFraction() {
|
||||
// Sonst wandert die Kachel beim Drehen aus dem Bild: der Anteil wird
|
||||
// gespeichert, und ein Wert über eins landete unterhalb des Randes.
|
||||
#expect(placement.verticalFraction(forCenter: CGPoint(x: 0, y: -9_000)) == 0)
|
||||
#expect(placement.verticalFraction(forCenter: CGPoint(x: 0, y: 9_000)) == 1)
|
||||
#expect(placement.center(side: .leading, verticalFraction: 5).y <= 844 - 12)
|
||||
}
|
||||
|
||||
#expect(start.x == 198.0)
|
||||
#expect(start.y == 731.0)
|
||||
@Test("An jeder Seite bleibt der Sicherheitsabstand")
|
||||
func keepsTheInset() {
|
||||
for side in MiniPlayerPlacement.Side.allCases {
|
||||
for fraction in [0.0, 0.5, 1.0] {
|
||||
let center = placement.center(side: side, verticalFraction: fraction)
|
||||
|
||||
#expect(center.x - 90 >= 12)
|
||||
#expect(center.x + 90 <= 390 - 12)
|
||||
#expect(center.y - 50.5 >= 12)
|
||||
#expect(center.y + 50.5 <= 844 - 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Beim Ziehen rutscht nichts über den Rand")
|
||||
@@ -82,7 +95,7 @@ struct MiniPlayerPlacementTests {
|
||||
inset: 12
|
||||
)
|
||||
|
||||
#expect(narrow.origin(for: .bottomTrailing) == CGPoint(x: 12, y: 12))
|
||||
#expect(narrow.clamped(center: CGPoint(x: 0, y: 0)) == CGPoint(x: 102, y: 62.5))
|
||||
#expect(narrow.center(side: .trailing, verticalFraction: 1) == CGPoint(x: 102, y: 62.5))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ struct PlaybackDeciderTests {
|
||||
#expect(decision.method == .directPlay(engine: .avPlayer))
|
||||
}
|
||||
|
||||
@Test("Unanalysierte Dateien gehen an den toleranteren Motor")
|
||||
@Test("Unanalysierte Dateien gehen an den toleranteren Engine")
|
||||
func unknownStreamsFallBackToVLC() {
|
||||
let part = PlexPart(id: 10, key: "/p", container: "mp4", streams: [])
|
||||
let media = PlexMedia(id: 100, container: "mp4", parts: [part])
|
||||
|
||||
@@ -331,7 +331,7 @@ struct LiveTVGuideTests {
|
||||
#expect(path == "/livetv/sessions/xyz/stream")
|
||||
}
|
||||
|
||||
@Test("Der Tuner wird per POST belegt, und die Antwort trägt den Strom")
|
||||
@Test("Der Tuner wird per POST belegt, und die Antwort trägt den Stream")
|
||||
func tunesThroughDVRWithPost() async throws {
|
||||
// Nachgelesen im Quelltext von Plex' eigenem Client:
|
||||
// POST /livetv/dvrs/{dvr}/channels/{kennung}/tune
|
||||
|
||||
Reference in New Issue
Block a user