Tester la précision et les arrondis de CMTime sur un Mac cloud

Tester la précision et les arrondis de CMTime sur un Mac cloud

Tester la précision et les arrondis de CMTime sur un Mac cloud

Un outil de montage enregistre la fin d’un segment sous forme de secondes en virgule flottante. À la réouverture du projet, une image supplémentaire apparaît au raccord. Avant de toucher au lecteur, isolez les calculs temporels de l’interface. Sur un Mac cloud, des entrées, des règles de conversion et des assertions fixes permettent de cerner le problème plus facilement qu’en déplaçant la tête de lecture. Les exemples ci-dessous utilisent uniquement Swift et CoreMedia : aucun fichier vidéo ni aucune session graphique ne sont nécessaires.

Définir le contrat temporel

CMTime représente un temps rationnel sous la forme value / timescale, auquel s’ajoutent des indicateurs et un epoch. L’échelle de temps n’est pas la fréquence d’images, et une résolution plus élevée ne garantit pas un résultat plus juste. À 30000/1001 images par seconde, par exemple, la durée d’une image doit s’écrire 1001/30000 seconde. Ne commencez pas par ramener la fréquence à 29.97 avant d’en prendre l’inverse.

Le contrat retenu ici impose des valeurs temporelles numériques et un epoch nul. Les durées ne doivent pas être négatives ; les décalages peuvent l’être. Toute entrée qui ne respecte pas ces conditions doit provoquer une erreur, et non être remplacée par zéro : un enregistrement corrompu serait alors interprété comme le début de la séquence.

« Afficher le même nombre de secondes » ne signifie pas « représenter le même temps ». L’interface peut formater l’affichage, mais les comparaisons de limites en interne doivent conserver les valeurs CMTime et utiliser CMTimeCompare.

Il faut aussi distinguer un instant d’une durée. Tous deux utilisent le même type, mais leur sens métier diffère : des instants appartenant à des timelines différentes ne peuvent pas être additionnés directement sans mise en correspondance préalable.

Créer des cas de test exécutables pour la précision

Enregistrez le contenu suivant dans un fichier media-time.swift, dans le répertoire de travail du compte utilisé pour la compilation. Le script contrôle d’abord les entrées, puis vérifie le cumul des durées d’image, les fractions équivalentes et les arrondis lors des conversions. La fonction de vérification termine explicitement le processus en cas d’échec ; elle ne repose pas sur des assertions susceptibles d’être supprimées par les options d’optimisation.

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")

Vérifiez la disponibilité des outils de développement, puis lancez le script :

set -eu
xcrun --find swift
xcrun swift media-time.swift

Seul le message de réussite est attendu ; un échec renvoie un code de sortie non nul. Ce test vérifie les calculs temporels, pas la vitesse de décodage ni les performances d’un nœud particulier.

Réserver les arrondis aux frontières des interfaces

Tout ramener à une échelle de 600 ne supprime pas les erreurs. Cette échelle représente exactement un tiers de seconde, mais pas la durée d’image utilisée plus haut. Si chaque durée d’image est convertie avant d’être additionnée, les erreurs d’arrondi s’accumulent au fil des opérations. Conservez l’échelle d’origine pendant les calculs, puis effectuez la conversion selon les exigences de l’interface de sortie.

Avec .roundHalfAwayFromZero, la règle à mi-chemin est symétrique pour les valeurs positives et négatives : une demi-unité cible est arrondie en s’éloignant de zéro. Si les règles métier imposent une troncature vers zéro ou un arrondi vers le bas, changez de méthode et ajoutez des cas négatifs. Tester uniquement la lecture en marche avant ne suffit pas.

Examiner les éléments de diagnostic après conversion

Pour chaque échec, consignez au minimum les champs d’entrée value, timescale et epoch, ainsi que l’échelle cible, la méthode d’arrondi et les indicateurs de sortie. hasBeenRounded signale qu’une perte de précision a eu lieu : ce n’est pas un critère d’échec automatique, et cet indicateur ne suffit pas à quantifier l’erreur. La tolérance doit être explicitement définie par le format d’export ou les règles du produit.

Vérifier les raccords avec des intervalles semi-ouverts

Définissez les segments de montage par des intervalles [start, end) : le début appartient au segment courant, la fin au segment suivant. Ainsi, une limite commune à deux segments adjacents n’appartient pas aux deux à la fois. Ajoutez les vérifications suivantes à la fin du script précédent :

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"
)

Testez également une même limite sous les formes 1/1 et 600/600, afin de vérifier que la décision ne dépend pas de l’égalité des numérateurs ou des échelles. Ajoutez des cas situés un pas avant le début, un pas avant la fin, ainsi qu’un intervalle dont les bornes sont inversées. Ce dernier doit provoquer une erreur, et non une permutation silencieuse du début et de la fin.

Intégrer les tests à la compilation et délimiter la validation

Placez le script dans le même dépôt que les fonctions temporelles métier et exécutez-les ensemble à chaque modification des algorithmes temporels. Le script autonome sert à confirmer la sémantique des opérations de base. Les tests du projet doivent aussi appeler directement l’implémentation de production, pour éviter que les exemples soient corrects alors que l’application utilise encore l’ancienne logique.

À chaque exécution, consignez l’identifiant du commit, xcrun swift --version et le code de sortie. Un échec des tests temporels doit bloquer l’export qui suit. N’ajoutez pas de nouvelles tentatives pour masquer une erreur déterministe. Pour tout nouveau cas de non-régression, conservez la fraction d’origine au lieu de la remplacer d’avance par une valeur décimale dans le fichier de test.

Enfin, validez séparément trois niveaux : l’exactitude des calculs rationnels, celle de la mise en correspondance des timelines, puis la lecture effective du média. Ce script ne couvre que le premier niveau et les règles d’intervalle explicitement définies. Pour les médias à fréquence d’images variable, lisez les horodatages réels au lieu de les déduire d’une durée d’image fixe. En établissant d’abord des entrées et des conclusions claires à chaque niveau, puis en examinant l’image, vous pourrez déterminer d’où vient exactement cette image d’écart.

Questions fréquentes

Peut-on comparer des CMTime après conversion en secondes Double ?

Cette conversion convient à l’affichage, mais utilisez CMTimeCompare pour les limites exactes. Un flottant peut perdre la précision rationnelle et ne représente pas à lui seul l’epoch ni les états temporels invalides.

Une échelle de 600 convient-elle à toutes les durées ?

Non. Certaines durées d’image ne sont pas représentables exactement à cette échelle. Conservez les valeurs sources, précisez la règle de conversion et contrôlez hasBeenRounded aux frontières des interfaces.

Confiez vos builds à un Mac cloud dédié

Choisissez un nœud physique Apple Silicon et disposez de la machine entière, avec macOS natif, interface graphique et ligne de commande. Le modèle, la durée et le nœud sont ceux indiqués dans la confirmation de commande.