it-swarm.com.de

Aufrechterhaltung einer guten Bildlaufleistung bei Verwendung von AVPlayer

Ich arbeite an einer Anwendung, in der es eine Sammlungsansicht gibt, und Zellen der Sammlungsansicht können Video enthalten. Im Moment zeige ich das Video mit AVPlayer und AVPlayerLayer an. Leider ist die Bildlaufleistung schrecklich. Es scheint, als würden AVPlayer, AVPlayerItem und AVPlayerLayer einen großen Teil ihrer Arbeit im Haupt-Thread ausführen. Sie entfernen ständig Sperren, warten auf Semaphore usw., wodurch der Haupt-Thread blockiert wird und schwere Frame-Drops verursacht werden.

Gibt es eine Möglichkeit, AVPlayer anzuweisen, so viele Dinge im Haupt-Thread zu beenden? Bisher hat nichts, was ich versucht habe, das Problem gelöst.

Ich habe auch versucht, einen einfachen Video-Player mit AVSampleBufferDisplayLayer zu erstellen. Damit kann ich sicherstellen, dass alles außerhalb des Haupt-Threads geschieht, und ich kann beim Scrollen und Abspielen von Videos ~ 60 Bilder pro Sekunde erreichen. Leider ist diese Methode viel niedriger, und sie bietet keine Funktionen wie Audiowiedergabe und Zeitverschwendung aus der Box. Gibt es eine Möglichkeit, mit AVPlayer eine ähnliche Leistung zu erzielen? Ich würde das lieber benutzen.

Edit: Nach eingehender Betrachtung sieht es nicht so aus, als könnte man mit AVPlayer eine gute Bildlaufleistung erzielen. Durch das Erstellen einer AVPlayer und Assoziierung mit einer AVPlayerItem-Instanz wird eine Reihe von Arbeiten gestartet, die auf den Haupt-Thread trampeln, wo sie dann auf Semaphoren warten und versuchen, eine Reihe von Sperren zu erhalten. Die Zeit, die der Haupt-Thread blockiert, nimmt mit der Anzahl der Videos in der Bildlaufansicht dramatisch zu.

AVPlayer dealloc scheint auch ein großes Problem zu sein. Beim Dealloc'ing einer AVPlayer wird auch versucht, einiges zu synchronisieren. Wieder wird dies extrem schlecht, wenn Sie mehr Spieler erstellen.

Das ist ziemlich deprimierend und macht AVPlayer fast unbrauchbar für das, was ich versuche. Das Blockieren des Haupt-Threads wie dieser ist eine Amateur-Sache, daher ist es schwer zu glauben, dass Apple-Ingenieure diesen Fehler gemacht hätten. Wie auch immer, hoffentlich können sie das bald beheben.

50
Antonio

Erstellen Sie die Variable AVPlayerItem so oft wie möglich in einer Hintergrundwarteschlange (einige Vorgänge, die Sie im Hauptthread ausführen müssen, Sie können jedoch Setup-Vorgänge ausführen und warten, bis die Videoeigenschaften in Hintergrundwarteschlangen geladen werden. Lesen Sie die Dokumente sorgfältig durch). Dies beinhaltet Voodoo-Tänze mit KVO und macht wirklich keinen Spaß.

Die Probleme treten auf, während die AVPlayer darauf wartet, dass der AVPlayerItems-Status AVPlayerItemStatusReadyToPlay wird. Um die Länge der Hiccups zu reduzieren, möchten Sie die AVPlayerItem in einem Hintergrund-Thread näher an AVPlayerItemStatusReadyToPlay bringen, bevor Sie sie der AVPlayer zuweisen.

Es ist schon eine Weile her, seit ich das tatsächlich implementiert habe, aber IIRC, die Haupt-Thread-Blöcke, werden verursacht, weil die zugrunde liegenden AVURLAsset-Eigenschaften faul geladen sind. Wenn Sie sie nicht selbst laden, werden sie im Haupt-Thread beschäftigt, wenn die AVPlayer möchte spielen.

Schauen Sie sich die AVAsset-Dokumentation an, insbesondere die Informationen rund um AVAsynchronousKeyValueLoading. Ich denke, wir mussten die Werte für duration und tracks laden, bevor das Asset für eine AVPlayer verwendet wurde, um die Haupt-Thread-Blöcke zu minimieren. Es ist möglich, dass wir auch alle Spuren durchlaufen und in jedem Segment AVAsynchronousKeyValueLoading ausführen mussten, aber ich kann mich nicht an 100% erinnern.

19
damian

Ich weiß nicht, ob das hilft - aber hier ist etwas Code, den ich zum Laden von Videos in die Hintergrundwarteschlange verwende, der definitiv bei der Blockierung des Hauptthreads hilft (wenn dies nicht 1: 1 kompiliert wird, habe ich von einer größeren Codebasis I abstrahiert arbeite an):

func loadSource() {
    self.status = .Unknown

    let operation = NSBlockOperation()
    operation.addExecutionBlock { () -> Void in
    // create the asset
    let asset = AVURLAsset(URL: self.mediaUrl, options: nil)
    // load values for track keys
    let keys = ["tracks", "duration"]
    asset.loadValuesAsynchronouslyForKeys(keys, completionHandler: { () -> Void in
        // Loop through and check to make sure keys loaded
        var keyStatusError: NSError?
        for key in keys {
            var error: NSError?
            let keyStatus: AVKeyValueStatus = asset.statusOfValueForKey(key, error: &error)
            if keyStatus == .Failed {
                let userInfo = [NSUnderlyingErrorKey : key]
                keyStatusError = NSError(domain: MovieSourceErrorDomain, code: MovieSourceAssetFailedToLoadKeyValueErrorCode, userInfo: userInfo)
                println("Failed to load key: \(key), error: \(error)")
            }
            else if keyStatus != .Loaded {
                println("Warning: Ignoring key status: \(keyStatus), for key: \(key), error: \(error)")
            }
        }
        if keyStatusError == nil {
            if operation.cancelled == false {
                let composition = self.createCompositionFromAsset(asset)
                // register notifications
                let playerItem = AVPlayerItem(asset: composition)
                self.registerNotificationsForItem(playerItem)
                self.playerItem = playerItem
                // create the player
                let player = AVPlayer(playerItem: playerItem)
                self.player = player
            }
        }
        else {
            println("Failed to load asset: \(keyStatusError)")
        }
    })

    // add operation to the queue
    SomeBackgroundQueue.addOperation(operation)
}

func createCompositionFromAsset(asset: AVAsset, repeatCount: UInt8 = 16) -> AVMutableComposition {
     let composition = AVMutableComposition()
     let timescale = asset.duration.timescale
     let duration = asset.duration.value
     let editRange = CMTimeRangeMake(CMTimeMake(0, timescale), CMTimeMake(duration, timescale))
     var error: NSError?
     let success = composition.insertTimeRange(editRange, ofAsset: asset, atTime: composition.duration, error: &error)
     if success {
         for _ in 0 ..< repeatCount - 1 {
          composition.insertTimeRange(editRange, ofAsset: asset, atTime: composition.duration, error: &error)
         }
     }
     return composition
}
12
Andy Poes

Wenn Sie einen Blick auf Facebook AsyncDisplayKit (die Engine hinter den Feeds von Facebook und Instagram) werfen, können Sie das Video größtenteils auf Hintergrund-Threads mithilfe von AVideoNode rendern. Wenn Sie das in einen ASDisplayNode unterknoten und die Ansicht displayNode.view zu jeder Ansicht (Tabelle/Sammlung/Bildlauf) hinzufügen, können Sie einen reibungslosen Bildlauf erzielen ein Hintergrundthread). Das einzige Problem ist, wenn das Videoelement geändert wird, da sich dies auf den Haupt-Thread auswirkt. Wenn Sie nur wenige Videos in dieser Ansicht haben, können Sie diese Methode verwenden!

        dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
            self.mainNode = ASDisplayNode()
            self.videoNode = ASVideoNode()
            self.videoNode!.asset = AVAsset(URL: self.videoUrl!)
            self.videoNode!.frame = CGRectMake(0.0, 0.0, self.bounds.width, self.bounds.height)
            self.videoNode!.gravity = AVLayerVideoGravityResizeAspectFill
            self.videoNode!.shouldAutoplay = true
            self.videoNode!.shouldAutorepeat = true
            self.videoNode!.muted = true
            self.videoNode!.playButton.hidden = true

            dispatch_async(dispatch_get_main_queue(), {
                self.mainNode!.addSubnode(self.videoNode!)
                self.addSubview(self.mainNode!.view)
            })
        })
7
Gregg

Ich habe mit den obigen Antworten herumgespielt und festgestellt, dass sie nur bis zu einem bestimmten Grenzwert zutreffen.

Die einfachste und einfachste Methode, die bisher für mich funktioniert hat, ist, dass der Code, den Sie Ihrer AVPlayerItem-Instanz in einem Hintergrundthread Ihre AVPlayer zuweisen. Ich habe festgestellt, dass das Zuweisen der Variable AVPlayerItem an den Player im Haupt-Thread (selbst wenn das Objekt AVPlayerItem bereit ist) immer eine Abgabe auf die Leistung und die Bildrate hat. 

Swift 4

ex.

let mediaUrl = //your media string
let player = AVPlayer()
let playerItem = AVPlayerItem(url: mediaUrl)

DispatchQueue.global(qos: .default).async {
    player.replaceCurrentItem(with: playerItem)
}
0

Hier ist eine funktionierende Lösung zum Anzeigen einer "Videowand" in einer UICollectionView:

1) Speichern Sie alle Ihre Zellen in einer NSMapTable (von nun an greifen Sie nur noch auf ein Zellobjekt aus der NSMapTable zu):

self.cellCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsWeakMemory valueOptions:NSPointerFunctionsStrongMemory capacity:AppDelegate.sharedAppDelegate.assetsFetchResults.count];
    for (NSInteger i = 0; i < AppDelegate.sharedAppDelegate.assetsFetchResults.count; i++) {
        [self.cellCache setObject:(AssetPickerCollectionViewCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]] forKey:[NSIndexPath indexPathForItem:i inSection:0]];
    }

2) Fügen Sie diese Methode Ihrer UICollectionViewCell-Unterklasse hinzu:

- (void)setupPlayer:(PHAsset *)phAsset {
typedef void (^player) (void);
player play = ^{
    NSString __autoreleasing *serialDispatchCellQueueDescription = ([NSString stringWithFormat:@"%@ serial cell queue", self]);
    dispatch_queue_t __autoreleasing serialDispatchCellQueue = dispatch_queue_create([serialDispatchCellQueueDescription UTF8String], DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialDispatchCellQueue, ^{
        __weak typeof(self) weakSelf = self;
        __weak typeof(PHAsset) *weakPhAsset = phAsset;
        [[PHImageManager defaultManager] requestPlayerItemForVideo:weakPhAsset options:nil
                                                     resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) {
                                                         if(![[info objectForKey:PHImageResultIsInCloudKey] boolValue]) {
                                                             AVPlayer __autoreleasing *player = [AVPlayer playerWithPlayerItem:playerItem];
                                                             __block typeof(AVPlayerLayer) *weakPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
                                                             [weakPlayerLayer setFrame:weakSelf.contentView.bounds]; //CGRectMake(self.contentView.bounds.Origin.x, self.contentView.bounds.Origin.y, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height * (9.0/16.0))];
                                                             [weakPlayerLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
                                                             [weakPlayerLayer setBorderWidth:0.25f];
                                                             [weakPlayerLayer setBorderColor:[UIColor whiteColor].CGColor];
                                                             [player play];
                                                             dispatch_async(dispatch_get_main_queue(), ^{
                                                                 [weakSelf.contentView.layer addSublayer:weakPlayerLayer];
                                                             });
                                                         }
                                                     }];
    });

    }; play();
}

3) Rufen Sie die obige Methode von Ihrem UICollectionView-Delegierten auf folgende Weise auf:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{

    if ([[self.cellCache objectForKey:indexPath] isKindOfClass:[AssetPickerCollectionViewCell class]])
        [self.cellCache setObject:(AssetPickerCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:indexPath] forKey:indexPath];

    dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH), ^{
        NSInvocationOperation *invOp = [[NSInvocationOperation alloc]
                                        initWithTarget:(AssetPickerCollectionViewCell *)[self.cellCache objectForKey:indexPath]
                                        selector:@selector(setupPlayer:) object:AppDelegate.sharedAppDelegate.assetsFetchResults[indexPath.item]];
        [[NSOperationQueue mainQueue] addOperation:invOp];
    });

    return (AssetPickerCollectionViewCell *)[self.cellCache objectForKey:indexPath];
}

Im Folgenden wird beschrieben, wie Sie eine PHFetchResult-Sammlung mit allen Videos im Ordner Video der Fotos-App füllen:

// Collect all videos in the Videos folder of the Photos app
- (PHFetchResult *)assetsFetchResults {
    __block PHFetchResult *i = self->_assetsFetchResults;
    if (!i) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumVideos options:nil];
            PHAssetCollection *collection = smartAlbums.firstObject;
            if (![collection isKindOfClass:[PHAssetCollection class]]) collection = nil;
            PHFetchOptions *allPhotosOptions = [[PHFetchOptions alloc] init];
            allPhotosOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
            i = [PHAsset fetchAssetsInAssetCollection:collection options:allPhotosOptions];
            self->_assetsFetchResults = i;
        });
    }
    NSLog(@"assetsFetchResults (%ld)", self->_assetsFetchResults.count);

    return i;
}

Wenn Sie Videos filtern möchten, die lokal sind (und nicht in iCloud), so würde ich davon ausgehen, da Sie nach glattem Bildlauf suchen:

// Filter videos that are stored in iCloud
- (NSArray *)phAssets {
    NSMutableArray *assets = [NSMutableArray arrayWithCapacity:self.assetsFetchResults.count];
    [[self assetsFetchResults] enumerateObjectsUsingBlock:^(PHAsset *asset, NSUInteger idx, BOOL *stop) {
        if (asset.sourceType == PHAssetSourceTypeUserLibrary)
            [assets addObject:asset];
    }];

    return [NSArray arrayWithArray:(NSArray *)assets];
}
0
James Bush