web-dev-qa-db-fra.com

Plusieurs vidéos avec AVPlayer

Je développe une application iOS pour iPad qui doit lire des vidéos sur une partie de l'écran. J'ai plusieurs fichiers vidéo qui doivent être lus l'un après l'autre dans un ordre qui n'est pas donné au moment de la compilation. Il faut que ce ne soit qu'une vidéo en cours de lecture. Il est bien que lorsque vous passez d'une vidéo à la suivante, il y ait un certain retard où la dernière ou la première image des deux vidéos est affichée, mais il ne devrait pas y avoir d'écran vacillant ou blanc sans contenu. Les vidéos ne contiennent pas d'audio. Il est important de prendre en compte l'utilisation de la mémoire. Les vidéos ont une très haute résolution et plusieurs séquences vidéo différentes peuvent être lues côte à côte en même temps.

Pour l'obtenir, j'ai essayé jusqu'ici quelques solutions. Ils sont listés ci-dessous:

1. AVPlayer avec AVComposition avec toutes les vidéos qu'il contient

Dans cette solution, j'ai un AVPlayer qui n'utilisera que sur AVPlayerItem fait en utilisant une AVComposition contenant toutes les vidéos mises côte à côte. Lorsque je vais à des vidéos spécifiques, je recherche l'heure dans la composition où la prochaine vidéo commence.Le problème avec cette solution est que lors de la recherche, le lecteur affichera rapidement certaines des images qu'il recherche, ce qui n'est pas acceptable. Il semble qu'il n'y ait aucun moyen de passer directement à un moment précis de la composition. J'ai essayé de résoudre ce problème en créant une image de la dernière image de la vidéo qui vient de se terminer, puis de montrer cela devant l'AVPLayer lors de la recherche, et enfin de la supprimer après la recherche. Je crée l'image en utilisant AVAssetImageGenerator, mais pour une raison quelconque, la qualité de l'image n'est pas la même que la vidéo, il y a donc des changements notables lors de l'affichage et du masquage de l'image sur la vidéo. Un autre problème est que l'AVPlayer utilise beaucoup de mémoire car un seul AVPlayerItem contient toutes les vidéos.

2. AVPlayer avec plusieurs AVPlayerItems

Cette solution utilise un AVPlayerItem pour chaque vidéo et remplace l'élément de l'AVPlayer lors du passage à une nouvelle vidéo. Le problème est que lorsque vous changez l'élément d'un AVPlayer, il affichera un écran blanc pendant une courte période lors du chargement du nouvel élément. Pour résoudre ce problème, la solution consistant à mettre une image en avant avec la dernière image pendant le chargement pourrait être utilisée, mais toujours avec le problème que la qualité de l'image et de la vidéo est différente et notable.

. Deux AVPlayers l'un sur l'autre à tour de rôle pour jouer à AVPlayerItem

La prochaine solution que j'ai essayée était d'avoir deux AVPlayer les uns sur les autres qui joueraient à tour de rôle AVPlayerItems. Ainsi, lorsque l'un des joueurs a fini de jouer, il reste sur la dernière image de la vidéo. L'autre AVPlayer sera amené à l'avant (avec son élément défini sur zéro, il est donc transparent), et le prochain AVPlayerItem sera inséré dans cet AVPlayer. Dès qu'il est chargé, il commencera à jouer et l'illusion d'une transaction fluide entre les deux vidéos sera intacte. Le problème avec cette solution est l'utilisation de la mémoire. Dans certains cas, j'ai besoin de lire deux vidéos sur l'écran en même temps, ce qui se traduira par 4 AVPlayers avec un AVPlayerItem chargé en même temps. C'est tout simplement trop de mémoire car les vidéos peuvent être en très haute résolution.


Quelqu'un a-t-il des pensées, des suggestions, des commentaires ou quelque chose concernant le problème global et les solutions essayées affichées ci-dessus?.

38
Peter

Le projet est donc désormais en ligne sur l'App Store et il est temps de revenir sur ce fil et de partager mes découvertes et de révéler ce que j'ai fini par faire.

Ce qui n'a pas fonctionné

La première option où j'ai utilisé sur une grande AVComposition avec toutes les vidéos n'était pas assez bonne car il n'y avait aucun moyen de passer à un moment précis dans la composition sans avoir un petit problème de nettoyage. De plus, j'ai eu un problème avec la pause de la vidéo exactement entre deux vidéos dans la composition car l'API ne pouvait pas fournir de garantie de trame pour la pause.

Le troisième avec deux AVPlayers et les laisser se relayer a très bien fonctionné dans la pratique. Surtout sur iPad 4 ou iPhone 5. Les appareils avec une quantité inférieure de RAM était un problème cependant car avoir plusieurs vidéos en mémoire en même temps consommait trop de mémoire. Surtout que je devais gérer des vidéos de très haute résolution.

Ce que j'ai fini par faire

Eh bien, à gauche se trouvait l'option numéro 2. Création d'un AVPlayerItem pour une vidéo en cas de besoin et alimentation de l'AVPlayer. La bonne chose à propos de cette solution était la consommation de mémoire. En créant paresseusement les AVPlayerItems et en les jetant au moment où ils n'étaient plus nécessaires, la consommation de mémoire pouvait être réduite au minimum, ce qui était très important pour prendre en charge les appareils plus anciens avec une mémoire RAM limitée. Le problème avec cette solution était que lors du passage d'une vidéo à la suivante, il y avait un écran vierge pendant un instant pendant que la vidéo suivante était chargée en mémoire. Mon idée de résoudre ce problème était de mettre une image derrière l'AVPlayer qui montrerait quand le joueur tamponnait. Je savais que j'avais besoin d'images que nous perfectionnions exactement pixel par pixel, alors j'ai capturé des images qui étaient des copies exactes de la dernière et de la première image des vidéos. Cette solution fonctionnait très bien dans la pratique.

Le problème avec cette solution

J'ai eu le problème cependant que la position de l'image à l'intérieur de l'UIImageView n'était pas la même que la position de la vidéo à l'intérieur de l'AVPlayer si la vidéo/image n'était pas sa taille native ou un module 4 à l'échelle. Autrement dit, j'ai eu un problème avec la façon dont les demi-pixels étaient traités avec un UIImageView et un AVPlayer. Cela ne semblait pas être la même chose.

Comment je l'ai corrigé

J'ai essayé beaucoup de choses depuis que mon application utilisait les vidéos de manière interactive où elles étaient affichées dans différentes tailles. J'ai essayé de changer le grossissementfiltre et le minificationFilter d'AVPlayerLayer et CALayer pour utiliser le même algorithme, mais je n'ai rien changé. À la fin, j'ai fini par créer une application iPad qui pourrait automatiquement prendre des captures d'écran des vidéos dans toutes les tailles dont j'avais besoin, puis utiliser la bonne image lorsque la vidéo a été mise à l'échelle à une certaine taille. Cela a donné des images au pixel près dans toutes les tailles que je montrais une vidéo spécifique. Pas une chaîne d'outils parfaite, mais le résultat était parfait.

Réflexion finale

La raison principale pour laquelle ce problème de position était très visible pour moi (et donc très important à résoudre), était parce que le contenu vidéo que mon application joue est des animations dessinées, où beaucoup le contenu est dans une position fixe et seulement une partie de la l'image bouge. Si tout le contenu n'est déplacé que d'un pixel, cela donne un problème très visible et laid. À la WWDC cette année, j'ai discuté de ce problème avec un ingénieur Apple qui est un expert en AVFoundation. Lorsque je lui ai présenté le problème, sa suggestion était essentiellement de choisir l'option 3, mais je lui ai expliqué que cela n'était pas possible parce que la consommation de mémoire et que j'ai déjà essayé cette solution. Dans cette lumière, il a dit que je choisis la bonne solution et m'a demandé de déposer un rapport de bogue pour le positionnement UIImage/AVPlayer lorsque la vidéo est mise à l'échelle.

48
Peter

Vous l'avez peut-être déjà regardé, mais avez-vous vérifié AVQueuePlayerDocumentation

Il est conçu pour lire AVPlayerItems dans une file d'attente et est une sous-classe directe d'AVPlayer, il suffit donc de l'utiliser de la même manière. Vous l'avez configuré comme suit:

AVPlayerItem *firstItem = [AVPlayerItem playerItemWithURL: firstItemURL];
AVPlayerItem *secondItem = [AVPlayerItem playerItemWithURL: secondItemURL];

AVQueuePlayer *player = [AVQueuePlayer queuePlayerWithItems:[NSArray arrayWithObjects:firstItem, secondItem, nil]];

[player play];

Si vous souhaitez ajouter de nouveaux éléments à la file d'attente au moment de l'exécution, utilisez simplement cette méthode:

[player insertItem:thirdPlayerItem afterItem:firstPlayerItem];

Je n'ai pas testé pour voir si cela réduit le problème de scintillement que vous avez mentionné, mais il semble que ce serait la voie à suivre.

15
Justyn

Mise à jour - https://youtu.be/7QlaO7WxjGg

Voici votre réponse en utilisant une vue de collection comme exemple, qui jouera 8 à la fois (notez qu'aucune gestion de mémoire d'aucune sorte n'est nécessaire; vous pouvez utiliser ARC):

    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = (UICollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath];

    // Enumerate array of visible cells' indexPaths to find a match
    if ([self.collectionView.indexPathsForVisibleItems
         indexOfObjectPassingTest:^BOOL(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
             return (obj.item == indexPath.item);
         }]) dispatch_async(dispatch_get_main_queue(), ^{
             [self drawLayerForPlayerForCell:cell atIndexPath:indexPath];
         });


    return cell;
}

- (void)drawPosterFrameForCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    [self.imageManager requestImageForAsset:AppDelegate.sharedAppDelegate.assetsFetchResults[indexPath.item]
                                                          targetSize:AssetGridThumbnailSize
                                                         contentMode:PHImageContentModeAspectFill
                                                             options:nil
                                                       resultHandler:^(UIImage *result, NSDictionary *info) {
                                                           cell.contentView.layer.contents = (__bridge id)result.CGImage;
                                                       }];
}

- (void)drawLayerForPlayerForCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    cell.contentView.layer.sublayers = nil;
    [self.imageManager requestPlayerItemForVideo:(PHAsset *)self.assetsFetchResults[indexPath.item] options:nil resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            if([[info objectForKey:PHImageResultIsInCloudKey] boolValue]) {
                [self drawPosterFrameForCell:cell atIndexPath:indexPath];
            } else {
                AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:[AVPlayer playerWithPlayerItem:playerItem]];
                [playerLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
                [playerLayer setBorderColor:[UIColor whiteColor].CGColor];
                [playerLayer setBorderWidth:1.0f];
                [playerLayer setFrame:cell.contentView.layer.bounds];
                [cell.contentView.layer addSublayer:playerLayer];
                [playerLayer.player play];
            }
        });
    }];
}

La méthode drawPosterFrameForCell place une image où une vidéo ne peut pas être lue car elle est stockée sur iCloud, et non sur l'appareil.

Quoi qu'il en soit, c'est le point de départ; une fois que vous comprenez comment cela fonctionne, vous pouvez faire tout ce que vous vouliez, sans aucun des problèmes de mémoire que vous avez décrits.

1
James Bush