CMTime-Präzision und Rundung auf einem Cloud-Mac testen
Ein Schnittprogramm speichert die Endzeiten von Clips als Fließkommawerte in Sekunden. Nach dem erneuten Öffnen des Projekts taucht an einer Schnittstelle jedoch ein zusätzliches Frame auf. Bevor Sie den Player anpassen, sollten Sie die Zeitberechnung von der Benutzeroberfläche trennen. Mit festen Eingaben, Umrechnungsregeln und Prüfungen auf einem Cloud-Mac lässt sich die Ursache leichter eingrenzen als durch Verschieben des Abspielkopfs. Die folgenden Tests verwenden ausschließlich Swift und CoreMedia. Videomaterial oder eine grafische Sitzung sind dafür nicht erforderlich.
Zuerst die Bedeutung der Zeitwerte festlegen
CMTime stellt rationale Zeitwerte als value / timescale dar und enthält zusätzlich Flags sowie eine epoch. Die Zeitskala ist nicht die Bildrate, und eine höhere Zeitskala liefert nicht automatisch ein korrekteres Ergebnis. Bei einer Bildrate von 30000/1001 Frames pro Sekunde muss die Dauer eines Frames beispielsweise als 1001/30000 Sekunden dargestellt werden. Runden Sie die Bildrate nicht zuerst auf 29.97, um anschließend den Kehrwert zu bilden.
Für diese Tests gilt folgende Vereinbarung: Alle Werte müssen numerische Zeitwerte mit Epoche null sein. Zeitdauern dürfen nicht negativ sein, Versätze dagegen schon. Erfüllt eine Eingabe diese Bedingungen nicht, muss ein Fehler zurückgegeben werden. Sie darf nicht durch null ersetzt werden, sonst wird ein beschädigter Datensatz als Videoanfang interpretiert.
„Als gleiche Sekundenanzahl angezeigt“ bedeutet nicht „zeitlich gleich“. Die Oberfläche darf Werte formatieren. Für interne Vergleiche von Zeitgrenzen sollten jedoch CMTime-Werte erhalten bleiben und mit CMTimeCompare verglichen werden.
Unterscheiden Sie außerdem zwischen Zeitpunkten und Zeitdauern. Beide verwenden denselben Typ, haben in der Anwendungslogik aber unterschiedliche Bedeutungen. Zeitpunkte auf verschiedenen Zeitleisten dürfen nicht ohne vorherige Zuordnung einfach addiert werden.
Ausführbare Präzisionstests anlegen
Speichern Sie den folgenden Inhalt als media-time.swift im Arbeitsverzeichnis des Build-Benutzers. Das Skript prüft zunächst die Eingaben und anschließend die Addition von Frame-Dauern, gleichwertige Brüche und die Rundung bei der Umrechnung. Die Prüffunktion beendet den Prozess bei einem Fehler ausdrücklich. Sie verlässt sich nicht auf Assertions, die durch Optimierungseinstellungen entfernt werden könnten.
import CoreMedia
import Foundation
import Darwin
func check(_ condition: Bool, _ name: String) {
guard condition else {
FileHandle.standardError.write(Data("FAIL: \(name)\n".utf8))
exit(1)
}
}
enum TimeError: Error {
case unsupported
}
func accepted(_ time: CMTime) throws -> CMTime {
guard time.isNumeric, time.epoch == 0 else {
throw TimeError.unsupported
}
return time
}
let frame = try accepted(CMTime(value: 1001, timescale: 30000))
var total = CMTime.zero
for _ in 0..<30 {
total = CMTimeAdd(total, frame)
}
check(
CMTimeCompare(total, CMTime(value: 1001, timescale: 1000)) == 0,
"frame accumulation"
)
let third = CMTime(value: 1, timescale: 3)
let exact = CMTimeConvertScale(
third, timescale: 600, method: .roundHalfAwayFromZero
)
check(exact.value == 200, "exact conversion value")
check(!exact.hasBeenRounded, "exact conversion flag")
for sign: Int64 in [-1, 1] {
let source = CMTime(value: sign, timescale: 1200)
let rounded = CMTimeConvertScale(
source, timescale: 600, method: .roundHalfAwayFromZero
)
check(rounded.value == sign, "signed halfway rounding")
check(rounded.hasBeenRounded, "rounding flag")
}
for bad in [CMTime.invalid, CMTime.indefinite, CMTime.positiveInfinity] {
do {
_ = try accepted(bad)
check(false, "reject nonnumeric time")
} catch TimeError.unsupported {
}
}
print("PASS: media time precision")
Prüfen Sie vor dem Start, ob die Entwicklungswerkzeuge verfügbar sind, und führen Sie dann das Skript aus:
set -eu
xcrun --find swift
xcrun swift media-time.swift
Das Skript sollte ausschließlich die Erfolgsmeldung ausgeben. Bei einem Fehler liefert es einen Exit-Code ungleich null. Geprüft wird hier die Zeitberechnung, nicht die Dekodiergeschwindigkeit oder die Leistung eines bestimmten Knotens.
Erst an Schnittstellengrenzen runden
Eine einheitliche Zeitskala von 600 beseitigt keine Rundungsfehler. Sie kann eine Drittelsekunde exakt darstellen, die oben genannte Frame-Dauer jedoch nicht. Wird jedes Frame vor der Addition umgerechnet, summieren sich die Rundungsfehler mit jeder Addition. Behalten Sie deshalb während der Berechnung die ursprüngliche Zeitskala bei und rechnen Sie erst danach entsprechend den Anforderungen der Ausgabeschnittstelle um.
Die Rundungsregel von .roundHalfAwayFromZero ist für positive und negative Werte symmetrisch: Werte, die genau auf einer halben Zieleinheit liegen, werden von null weg gerundet. Verlangt die Anwendungslogik stattdessen ein Abschneiden in Richtung null oder ein Abrunden, müssen Sie die Methode ersetzen und negative Testfälle ergänzen. Nur die Vorwärtswiedergabe zu testen, reicht nicht aus.
Umrechnungsergebnisse nachvollziehbar prüfen
Ein Fehlerprotokoll sollte mindestens value, timescale und epoch der Eingabe sowie die Zielzeitskala, die Rundungsmethode und die Flags der Ausgabe enthalten. hasBeenRounded weist auf einen Präzisionsverlust hin. Das Flag ist weder automatisch ein Fehlerkriterium noch beschreibt es allein die Größe des Fehlers. Welche Abweichung zulässig ist, muss das Exportformat oder die Produktvorgabe ausdrücklich festlegen.
Schnittgrenzen mit halboffenen Intervallen prüfen
Definieren Sie Schnittintervalle als [start, end): Der Anfang gehört zum aktuellen Clip, das Ende zum nächsten. So wird eine gemeinsame Grenze benachbarter Clips nicht beiden Clips zugleich zugeordnet. Ergänzen Sie am Ende des bisherigen Skripts die folgenden Prüfungen:
func contains(_ point: CMTime, start: CMTime, end: CMTime) throws -> Bool {
let p = try accepted(point)
let a = try accepted(start)
let b = try accepted(end)
guard CMTimeCompare(a, b) <= 0 else {
throw TimeError.unsupported
}
return CMTimeCompare(p, a) >= 0 && CMTimeCompare(p, b) < 0
}
let start = CMTime.zero
let end = CMTime(value: 1, timescale: 1)
check(try contains(start, start: start, end: end), "inclusive start")
check(!(try contains(end, start: start, end: end)), "exclusive end")
check(
!(try contains(start, start: start, end: start)),
"empty interval"
)
Geben Sie dieselbe Grenze außerdem einmal als 1/1 und einmal als 600/600 ein. Damit stellen Sie sicher, dass die Entscheidung nicht von übereinstimmenden Zählern oder Zeitskalen abhängt. Ergänzen Sie Tests für einen Zeittakt vor dem Anfang, einen Zeittakt vor dem Ende und ein umgekehrtes Intervall. Der letzte Fall muss einen Fehler auslösen, statt Anfang und Ende stillschweigend zu vertauschen.
In den Build integrieren und den Prüfumfang abgrenzen
Legen Sie das Skript und die Zeitfunktionen der Anwendung im selben Repository ab und führen Sie die Tests bei Änderungen an den Zeitalgorithmen gemeinsam aus. Das eigenständige Skript überprüft die zugrunde liegende Semantik. Die Projekttests müssen zusätzlich die produktive Implementierung direkt aufrufen. So verhindern Sie, dass die Testfälle korrekt sind, die Anwendung aber weiterhin die alte Logik verwendet.
Protokollieren Sie bei jedem Lauf die Commit-Kennung, xcrun swift --version und den Exit-Code. Schlägt ein Zeittest fehl, muss der nachfolgende Export blockiert werden. Verdecken Sie deterministische Fehler nicht durch Wiederholungsversuche. Behalten Sie bei neuen Regressionstestfällen die ursprünglichen Brüche bei, statt sie bereits in den Testdateien in Dezimalzahlen umzuwandeln.
Prüfen Sie abschließend drei Ebenen getrennt: die rationale Berechnung, die Zuordnung zwischen Zeitleisten und die tatsächliche Wiedergabe des Mediums. Dieses Skript deckt nur die erste Ebene und die ausdrücklich festgelegten Intervallregeln ab. Bei Material mit variabler Bildrate müssen Sie die tatsächlichen Zeitstempel auslesen, statt sie aus einer festen Frame-Dauer abzuleiten. Sorgen Sie zunächst auf jeder Ebene für klare Eingaben und eindeutige Ergebnisse. Erst danach sollten Sie die Bildausgabe untersuchen, um festzustellen, wo das zusätzliche Frame tatsächlich entsteht.
Häufig gestellte Fragen
Darf ich CMTime für Vergleiche in Double-Sekunden umwandeln?
Für die Anzeige ist das sinnvoll, für exakte Grenzen verwenden Sie CMTimeCompare. Die Umwandlung kann rationale Präzision verlieren; außerdem bildet ein Sekundenwert weder epoch noch ungültige Zeitzustände vollständig ab.
Ist eine einheitliche Zeitskala von 600 immer sicher?
Nein. Nicht jede Bilddauer lässt sich darin exakt darstellen. Bewahren Sie die Quellwerte auf, legen Sie die Rundungsregel an Schnittstellen fest und prüfen Sie hasBeenRounded anhand Ihrer Genauigkeitsanforderungen.
Build-Aufgaben an einen exklusiven Cloud-Mac übergeben
Wählen Sie einen Apple-Silicon-Server und nutzen Sie ihn exklusiv – mit nativer macOS-Oberfläche und Kommandozeile. Modell, Laufzeit und Standort richten sich nach Ihrer Bestellbestätigung.