QTMovie hyper lent

Bonjour,


 


J'utilise encore QTKit pour créer des films. Peut-être me faut-il passe à  AVFondation pour résoudre mon problème? (J'ai vu que TOUTES les méthodes de QTMovie sont obsolètes!)


 


Le problème se pose pour deux logiciels différents, et est identique :


 


Je crée des images dans des NSView. Par exemple, une fractale Mandelbrot. Je programme des transitions entre deux images, par exemple un zoom plus précis de la Mandelbrot.


 


Lors de la création du film, je fais une boucle :



NSString *nomFichier = [[NSString alloc]initWithString:[[savePanel URL]relativePath]];
QTMovie *movie = nil;

NSDictionary *attrs = [NSDictionary dictionaryWithObject:@jpeg forKey:QTAddImageCodecType];

int nbIm = [leFilm nombrePlans]; //le nombre d'images total du film
int perSec = [leFilm nombreImagesSec]; //le nombre d'images par secondes, 10 dans mes essais

movie = [[QTMovie alloc] initToWritableFile:nomFichier error:NULL];

for (int i = 0; i < nbIm; i++){

[nbPlan setStringValue:[NSString stringWithFormat:@%d,i]];
[nbPlan displayIfNeeded]; //contrôle de l'image en cours d'édition

[leDessin setPlanEnCours:i];
[leDessin setNeedsDisplay:YES];
[leDessin displayIfNeeded];
[leDessin performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0.0]; //pour voir le dessin s'afficher lors de la création du film
NSImage *uneVue = [leDessin uneVue]; //méthode qui donne une NSImage à  partir de la NSView

[movie addImage:uneVue
forDuration:QTMakeTime(1, perSec)
withAttributes:attrs];
}
[movie updateMovieFile];


Le problème est que plus le nombre d'images créées augmente, plus le logiciel ralentit, jusqu'au bug le plus souvent. L'indicateur "Memory" du Debugg Session dans Xcode m'indique une consommation de mémoire délirante : 8Go passé 50 images, 15Go passé 100, etc.  C'est d'autant plus incompréhensible que le film fait au final 2 ou 5 Mo.


 


Qu'indique réellement l'indice "Memory" des Debugg Session dans Xcode? Un des softs utilise le ramasse miette l'autre non. Les fonctions de dessin marchent très bien et sont très rapides. Les demandes de réactualisation de l'affichage dans la méthode ne sont pas en cause. (test avec et sans)


 


J'ai pensé sinon, puisque QTMovieView dérive de NSView, utiliser cette classe, mais il n'y a pas de méthode apparemment pour récupérer la vue et la mettre dans le film.


 


Auriez-vous une idée?


Réponses

  • CéroceCéroce Membre, Modérateur
    août 2015 modifié #2
    Voici ce que je suppose.

    Comme l'autorelease pool n'est vidé qu'à  la fin de la boucle d'événements, et que tu fais une boucle qui crée plein d'instances de NSImage, celles-ci demeurent en mémoire jusqu'à  ce que ta boucle se termine et la boucle d'événements reprend la main.

    Pour le coup, c'est facile à  tester, écris ça:
     
    for (int i = 0; i < nbIm; i++){
    @autoreleasepool {
    [nbPlan setStringValue:[NSString stringWithFormat:@%d,i]];
    [nbPlan displayIfNeeded]; //contrôle de l'image en cours d'édition

    [leDessin setPlanEnCours:i];
    [leDessin setNeedsDisplay:YES];
    [leDessin displayIfNeeded];
    [leDessin performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0.0]; //pour voir le dessin s'afficher lors de la création du film
    NSImage *uneVue = [leDessin uneVue]; //méthode qui donne une NSImage à  partir de la NSView

    [movie addImage:uneVue
    forDuration:QTMakeTime(1, perSec)
    withAttributes:attrs];
    }
    }
    J'aurais pensé que
    [leDessin performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0.0];
    aurait fonctionné, puisque ça donne la main à  la run loop pour dessiner, et j'aurais dit que le pool serait donc vidé.

    Ton approche est de toute façon mauvaise. La génération du film devrait se faire dans un thread secondaire pour ne pas bloquer le thread principal qui contrôle l'IHM. Intéresse-toi à  NSOperation et NSOperationQueue. (Je comprends bien que tu ne sais peut-être pas dessiner en dehors d'une NSView, ça n'empêche que l'approche n'est pas bonne).

    Créer un Autorelease Pool à  chaque itération de la boucle comme je le montre ci-dessus n'est pas optimal, mais tu verras immédiatement si ça change quoi que ce soit.
  • Je rejoins Céroce sur la nécessité de faire la génération dans un autre thread mais perso j'utiliserai un dispatch_async plutôt. 


    Sinon pour éviter ce genre de soucis le plus simple c'est encore d'utiliser un CGBitmapContext et de dessiner dedans pour en dégager une CGImage.


    L'avantage ? CGContextRelease et CGImageRelease qui vont faire ce que leur nom indique et régler le soucis...

  • Merci pour ces réponses. Effectivement, le problème est par là .


    Je vais étudier les deux solutions. Je vous tiens au courant.


  • HerveHerve Membre
    août 2015 modifié #5

    J'ai voulu, plutôt que d'instancier plusieurs NSImage, réécrire la même en en faisant une variable de la classe NSView.


    Gros problème : en fait les différentes méthodes utilisées pour la créer rappellent sans cesse la méthode drawRect, d'où l'inflation exponentielle de la consommation de mémoire (qui est mon vrai problème en fait).


     


    Voici trois méthodes différentes, toutes rappellent drawRect :



    BOOL wasHidden = self.isHidden;
    CGFloat wantedLayer = self.wantsLayer;

    self.hidden = NO;
    self.wantsLayer = YES;

    uneVue = [[NSImage alloc] initWithSize:self.bounds.size];
    [uneVue lockFocus];
    CGContextRef ctx = [NSGraphicsContext currentContext].graphicsPort;
    [self.layer renderInContext:ctx];
    [uneVue unlockFocus];

    self.wantsLayer = wantedLayer;
    self.hidden = wasHidden;


    NSSize imgSize = self.bounds.size;

    NSBitmapImageRep * bir = [self bitmapImageRepForCachingDisplayInRect:[self bounds]];
    [bir setSize:imgSize];

    [self cacheDisplayInRect:[self bounds] toBitmapImageRep:bir]; //c'est ici que drawRect est rappelé

    uneVue = [[NSImage alloc] initWithSize:imgSize];
    [uneVue addRepresentation:bir];

    + la même avec pdf 


     


    La question devient donc : 


    Comment récupérer la vue de ma NSView sans que la NSView ne se redessine indéfiniment? 


     


    J'ai tenté de mettre ces méthodes dans la méthodes drawRect, et de réécrire ma NSImage en fin de méthode, le problème est le même. (La méthode est appelée en fin de drawRect dans l'exemple ci-dessus)


  • PyrohPyroh Membre
    août 2015 modifié #6

    J'ai pas vraiment compris là  désolé...


    Tu sais plutôt mettre les codes en entier ? Parce que ça manque un peu de clarté  ;)


  • HerveHerve Membre
    août 2015 modifié #7

    Ma méthode drawRect est appelée un trop grand nombre de fois, que ce soit par la méthode de capture ou par la boucle créant le film. Le problème de l'inflation de mémoire vient de là .


     


    Même en utilisant toujours la même instance de NSImage (au lieu d'en créer une à  chaque boucle) le problème demeure.


     


    Les codes sont plus haut.


     


    En modifiant le code de ma boucle :



    [leDessin setPlanEnCours:i];
    //[leDessin setNeedsDisplay:YES]; //supprimé parce qu'appelé par la méthode suivante
    [leDessin creeLaVue]; //reécrit mon instance de NSImage dans la NSView
    [leDessin displayIfNeeded]; //inutile semble t-il
    //[leDessin performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0.0]; //supprimé car rappelle drawRect, mais on ne voit plus les images se faire
    //NSImage *uneVue = [leDessin uneVue]; //supprimé pour limiter la conso mémoire //méthode qui donne une NSImage à  partir de la NSView

    [movie addImage:[leDessin uneVue] //directement
    forDuration:QTMakeTime(1, perSec)
    withAttributes:attrs];

    on limite à  l'essentiel l'appel de la méthode drawRect (une fois par boucle) mais la conso mémoire demeure supérieure à  la taille du film final néanmoins


     


    La méthode "creeLaVue" utilise le bitmapRep, mais en mettant 


    monBitmap = NULL;


    pour tenter de vider la mémoire. ???? (dans ce cas, est-que l'emploi du ramasse-miette n'est pas contre-performant?)


     


    Céroce, j'ai regardé la doc concernant les threads, mais je ne vois pas très bien où mettre mon thread...


  • Il te reste que le dessin offscreen dans un CGBitmapContext alors.


  • HerveHerve Membre
    août 2015 modifié #9

    Bizarrement (sans doute parce que ma méthode de dessin utilise de nombreuses transformations affines), il me faut passer par la méthode view>pdf>NSImage pour obtenir une image exacte. 


     


    Je m'intéresse à  NSOperation, sur les conseils de Ceroce.


    J'ai lu en particulier ce tuto qui m'a l'air bien fait :


    http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues


     


    J'ai tenté ceci :


    - Je crée une classe dérivée de NSOpération qui crée un tableau stockant les centaines d'images à  créer. Cette classe reçoit dans son constructeur l'instance de la NSView et des données.


    - Une fois le main de cette classe effectué, je récupère le tableau dans NSDocument où je finalise le film.


     


    L'essai effectué est très décevant :


    La conso mémoire est toujours aussi délirante (15Go pour un film de 600 images!), sans que je ne sache pourquoi d'ailleurs. Pourquoi la création d'une image consomme 400Mo alors que le logiciel hors création du film en consomme 5 fois moins?


    Les images sont bien créée et s'affichent, la NSView est bien reconnue par la NSOperation.


    Je n'ai même pas pu aller au bout du test, l'OS a crashé en cours...


     


    Qu'est-ce que je n'ai pas compris selon vous?


  • HerveHerve Membre
    août 2015 modifié #10

    Je n'y comprends vraiment rien!


     


    J'ai essayé d'écrire les images sur le disque dur pour les récupérer ensuite afin de faire le film en lieu et place d'un NSMutableArray, la conso mémoire grimpe en flèche de la même manière. Et ce malgré l'implémentation d'une NSOperationQueue pour piloter la NSOperation, et la réutilisation du même fichier NSData, NSString pour écrire sur le disque! (l'écriture se fait bien)


     


    J'ai essayé aussi plusieurs boucles "for" imbriquées (une pour les dizaines, une pour les centaines, etc.) dans l'espoir que la mémoire se viderait à  chaque petite boucle, mais non...


     


    C'est vraiment déroutant!


  • Tu saurai poster un sample project qu'on regarde un peu chez nous et qu'on le soumette a Instruments ?


  • CéroceCéroce Membre, Modérateur

    Qu'est-ce que je n'ai pas compris selon vous?

    Que NSView dessine à  l'écran et qu'on ne doit le faire que dans le thread principal.
    C'est pourquoi si on dessine dans un thread secondaire dans une NSView, ça plante.

    Tu dois:
    - créer une NSOperationQueue
    - Pour chaque frame de la vidéo:
    - Créer une NSOperation
    - créer un contexte graphique offscreen
    - Y dessiner
    - Ajouter l'image à  la vidéo
    - supprimer le contexte offscreen

    Le rendu sera plus rapide qu'en passant par des NSViews, parce que tu peux générer les images en parallèles sur tous les threads.

    Pour afficher la progression, de temps en temps, il faudra prendre une des images générées et l'afficher (sur le thread principal) en la passant à  une NSImageView.
  • Merci Ceroce et Pyroh.


    Tant que tu y es Ceroce, comment dessines-tu "dans l'astral", sans NSView ???


    (what do you call a "contexte graphique offscreen" ???


    A tous les coups, toute ma méthode de dessin va être à  réécrire, mais peut-être que non, seulement pour l'export du film??)


     


    ​En tous les cas, Pyroh, si la piste suivie jusque là  n'est pas la bonne, autant chercher à  bien faire avant de vous faire perdre votre temps. Merci pour ta proposition. Nous verrons après.


  • C'est bien ce que je craignais, il y a beaucoup à  réécrire. 


     


    Juste une dernière question : j'ai vu que l'on pouvait faire des boucles dans le main de NSOperation. Peut-on faire des boucles avec des NSOperationQueue?


     


    Au lieu de  :




    self->queue = [[NSOperationQueue alloc] init];
    assert(self->queue != nil);

    if (self.boucleFilm != nil) {
    [self.boucleFilm cancel];
    }

    self.boucleFilm = [[BoucleFilm alloc]initWithNumber:nbIm etDessin:leDessin etNom:nomFichier];
    assert(self.boucleFilm != nil);

    [self.queue addOperation:self.boucleFilm];//la boucle est dans le NSOperation "BoucleFilm"


    faire plutôt :



    for (int i = 0; i <[[leFilm lesImages]count]; i++){
    self->queue = [[NSOperationQueue alloc] init];
    assert(self->queue != nil);

    if (self.boucleFilm != nil) {
    [self.boucleFilm cancel];
    }

    self.boucleFilm = [[BoucleFilm alloc]initWithNumber:nbIm etDessin:leDessin etNom:nomFichier]; //cette fois, "Boucle film" ne produit plus qu'une seule image
    assert(self.boucleFilm != nil);

    [self.queue addOperation:self.boucleFilm];
    }
  • CéroceCéroce Membre, Modérateur
    août 2015 modifié #16
    Une opération va servir à  générer une seule frame. Il n'y a pas de boucle: la frame est générée, point.
    Tu peux utiliser NSBlockOperation, c'est très simple, mais tu peux même utiliser -[NSOperationQueue addOperationWithBlock]. Je te déconseille de sous-classer NSOperation dans ce cas simple.

    Tu peux tout à  fait créer toutes les opérations d'un coup et les placer immédiatement dans la NSOperationQueue. C'est la propriété maxConcurrentOperationCount qui va décider combien d'opération sont exécutées à  la fois. Si tu mets le paramètre à  1, tu obtiens une queue "série". C'est bien pour l'aperçu, parce que tu es sûr que les images seront générées dans l'ordre, mais tu pourrais aller plus vite.

    Quand operationCount atteint 0, tu sais que c'est fini.
  • Merci Céroce,


     


    Je crois que je suis parvenu à  une solution plus simple, qui me permet de conserver Cocoa :



    int i = 0;
    @autoreleasepool {
    while (i < nbIm) {
    @autoreleasepool {
    [nbPlan setStringValue:[NSString stringWithFormat:@%d,i]];
    [nbPlan displayIfNeeded];

    [leDessin setPlanEnCours:i];
    [leDessin creeLaVue];
    [leDessin displayIfNeeded];

    NSData *data = [[leDessin uneVue] TIFFRepresentation];
    NSString *fileImage = [nomFichier stringByAppendingString:[NSString stringWithFormat:@_%d.png, i]];
    [data writeToFile: fileImage atomically: NO];
    }

    i += 1;

    }
    }

    Elle marche, les vues se réactualisent et la conso mémoire demeure normale (moins de 100Mo passé des dizaines d'images).


     


    (Si c'est possible !   :


    https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html


    )


     


    J'ai encore un problème pour récupérer la vue. L'image est faite de nombreuses photos modifiées par un filtre et démultipliées par des affineTransform.


     


    La méthode :



    NSData *pdfData = [self dataWithPDFInsideRect:self.bounds];

    ne prend que la première photo et ses démultiplications, et oublie les autres, alors que celles-ci s'affichent bien à  l'écran. Ah là  là !


     


    Il me faudrait juste une "vraie capture d'écran automatisée" de la NSView. J'ai déjà  essayé avec NSBitmapRep, là  c'étaient les démultiplications qui étaient mal reprises dans l'image...


  • CéroceCéroce Membre, Modérateur
    En fait, pour moi, ce problème était déjà  réglé ;-)
    Je croyais que tu voulais passer à  manière optimale.
  • HerveHerve Membre
    août 2015 modifié #19

    Je serai un éternel bricoleur sans doute!... (la différence entre un pro et un semi-pro...) Cela marche bien, et c'est simple...


    Ceci dit, aucune méthode pour récupérer la NSView dans l'image ne marche : soit les différentes photos modifiées sont oubliées, soit ce sont les translations, etc. Comme Quartz ne semble pas proposer les transformations libres (avec création perso de la matrice. Pas de référence dans la doc.), je préfère rester avec Cocoa.


  • HerveHerve Membre
    août 2015 modifié #20

    Grâce aux @autoreleasepool, la méthode de création du film marche bien en tant que telle maintenant. Merci pour vos suggestions qui m'ont fort bien guidé vers la solution.


     


    Le problème de la mauvaise récupération de l'image vient de l'emploi de cette méthode :



    [self translateOriginToPoint:pointCentral];

    Si je ne modifie pas les origines pour les transformations, tout va bien. Pourtant, j'en ai besoin. Nous avions déjà  discuté ici du problème, je vais relire les posts. Cependant, je suis étonné que le résultat affiché à  l'écran soit celui attendu, mais que même la méthode d'impression par exemple ne sache pas lire l'image. Même ceci :



    - (IBAction)printDocument:(id)sender {
    NSPrintOperation *impression = [NSPrintOperation printOperationWithView:leDessin];
    //...
    }

    ne donne pas la bonne image. Pourquoi permettre la modification des coordonnées du plan si c'est pour que cela ne marche pas ensuite??? Ce qui m'étonne le plus, c'est que l'affichage, lui, marche impec.


     


    Merci par avance pour vos idées éventuelles. J'ai regardé dans NSAffineTransform, mais apparemment aucune méthode ne permet de donner l'origine d'une transformation indépendamment du NSGraphicContext. Il y a une méthode "transformPoint", mais qui est là  pour appliquer une transformation à  un point si j'ai bien compris.


  • CéroceCéroce Membre, Modérateur
    Je te déconseille d'appeler translateOriginToPoint. ça modifie les bounds, et donc ça devient ingérable dès que la vue va être redimensionnée.
    Utilise plutôt une translation sur la CTM avant de faire ton dessin.
  • Merci Céroce, je crois que c'est par là .


    En fait, je me suis rendu compte que NSGraphicContext crée des vues différentes suivant la destination, le WindowContext n'étant pas celui pour l'impression. 


     


    Comment fait-on une translation sur la CTM? (puisque c'est là  le problème??)


  • CéroceCéroce Membre, Modérateur
    +[NSAffineTransform set]
    +[NSAffineTransform concat]
    CGContextConcatCTM()
  • Ah oui, bien sûr!


     


    J'ai fait plusieurs tests, le problème de l'ordre des transformations se pose et aucun n'a été concluant.


    En ce moment, je fais des tests plus convainquant avec la capture d'écran. A défaut, je pense que cela va être plus simple.


    Je pars quelques jours, je reprendrai cela en rentrant.


    Merci à  tous.


  • Bon, la capture écran fonctionne, et, à  condition de créer un NSRect sauvegardant la frame du dessin, on y extrait facilement l'image, quelles que soient les NSAffineTranform. C'est certainement du bricolage (une fois de plus...) mais puisque cela marche, et que je ne compte pas distribuer ce logiciel, on fera comme cela en attendant mieux...


     


    Ce qui est étonnant, c'est que la capture d'écran fonction même si j'utilise une autre application pendant que le film est créé. Est-ce grâce à  



    [leDessin displayIfNeeded];

    ???


  • Pour ceux que l'histoire a pu intéresser, mon idée d'utiliser la capture d'écran s'est avérée mauvaise : passé 150 images, le CPU était trop important et cela plantait.  Du coup je fais tout avec CIImage, y compris les transformations, et cela marche bien mieux. Je peux passer par les méthodes classiques de capture d'image pour faire mon film, et cela va 4 ou 5 fois plus vite...


     


    C'est comme cela qu'on apprend dit-on? J'en ai conclu que, de même que CoreAudio est indispensable dès que l'on veut faire du son de façon un peu poussé, Core Image est très efficace pour faire de l'image à  haut rendement, et il est bon alors de se débarrasser de NSBezierPath et consort  (NSBitmapRep, etc.)


Connectez-vous ou Inscrivez-vous pour répondre.