Switching Videos in AVPlayer Creates Flash When Changing
Asked Answered
U

9

24

I am using AVFoundation's AVPlayer to play 2 video clips made from 1 longer video (so the end of the first matches the beginning of the second)

When the first video ends and the user taps, I create a new AVPlayer and assign it to my PlayerView, and start playing the second clip.

This all works, however, there is a prominent screen "flicker".

My assumption is that this is caused by the player view removing the first clip and then showing the second clip.

What I need is for this flicker to no appear, so that going between the two clips is seamless.

Do anyone know if there is a way to stop this flickr, either via the AVPlayer* classes, or a way to "fake" it by doing something to make it so this isn't visible.

Thanks

Below is the code of my load and play method:

- (void)loadAssetFromFile
{
    NSURL *fileURL = nil;

    switch (playingClip)
    {
        case 1:
            fileURL = [[NSBundle mainBundle] URLForResource:@"wh_3a" withExtension:@"mp4"];
        break;

        case 2:
            fileURL = [[NSBundle mainBundle] URLForResource:@"wh_3b" withExtension:@"mp4"];
        break;

        case 3:
            fileURL = [[NSBundle mainBundle] URLForResource:@"wh_3c" withExtension:@"mp4"];
        break;

        case 4:
            fileURL = [[NSBundle mainBundle] URLForResource:@"wh_3d" withExtension:@"mp4"];
        break;

        default:
            return;
        break;
    }

    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
    NSString *tracksKey = @"tracks";

    [asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:tracksKey] completionHandler:
 ^{
     // The completion block goes here.
     NSError *error = nil;
     AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];

     if (status == AVKeyValueStatusLoaded)
     {
         self.playerItem = [AVPlayerItem playerItemWithAsset:asset];

         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];

         self.player = [AVPlayer playerWithPlayerItem:playerItem];
         [playerView setPlayer:player];

         [self.player seekToTime:kCMTimeZero];

         [self play];
     }
     else {
         // Deal with the error appropriately.
         NSLog(@"The asset's tracks were not loaded:\n%@", [error localizedDescription]);
     }
 }];
}
Unpromising answered 30/8, 2011 at 2:53 Comment(3)
Did you try just setting the background color of the view behind the AVPlayerLayer to, say, black instead of white? You're doing the right thing in setting the playerView from the old player to the new as opposed to removing/readding it. The player view has to be blank at some point, since you're setting it to the new player instance before that instance is ready to play. You could try only setting the playerView over when its status is AVPlayerStatusReadyToPlay?Riplex
were you able to resolve this issue ?Skatole
See the comments for this answer for an actual working impl: https://mcmap.net/q/138951/-looping-avplayer-seamlesslyCrozier
P
6

You do not need to re-create AVPlayer for this task. You can just have multiple AVPlayerItems and then switch which one is current via [AVPlayer replaceCurrentItemWithPlayerItem:item].

Also, you can observe for when current item has changed with the code below.

 static void* CurrentItemObservationContext = &CurrentItemObservationContext;

... After creating a player, register the observer:

 [player1 addObserver:self 
                   forKeyPath:kCurrentItemKey 
                      options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                      context:CurrentItemObservationContext];

...

 - (void)observeValueForKeyPath:(NSString*) path 
                  ofObject:(id)object 
                    change:(NSDictionary*)change 
                   context:(void*)context {
     if (context == CurrentItemObservationContext) {
         AVPlayerItem *item = [change objectForKey:NSKeyValueChangeNewKey];
         if (item != (id)[NSNull null]) {
             [player1 play];
         }
     }
 }
Peptize answered 2/9, 2011 at 23:13 Comment(8)
Another thing you can try is creating multiple AVPlayers in multiple Views. Create the views programmatically via alloc and by adding it as subview to main view. Then use hidden or setAlpha property on each view to control which one is visible. This will allow you to also do transitions between players as needed.Peptize
This doesn't seem to resolve the flicker issue. Thank you for the attempt at a solutionUnpromising
Did you try the solution in the comment I posted? The flicker should go away for sure when you have the two players on top of each other, and set alpha to 1/0 depending on which one should be active.Peptize
Please don't create multiple AVPlayers at once, there's a hardware limitation of max 4 open on the device as it is and you don't want to make it any worse.Riplex
For a transition you only really need 2, but you should share your solution to make the comment more constructive.Peptize
Using AVPlayerItem seems to work on the simulator, but on a device I still see the flash.Taxdeductible
It's not clear how will it actually solve the problem. Why should I put an observer on the player? This observer will work only if AVPlayerItem was replaced. And if it was replaced, this observer wouldn't make any difference, because algorithm of replacements starts and I'll see the flick.Chiasma
It's not that you can't have multiple AVPLayers, what you can't have is too many AVPlayerItems associated with AVPlayers as in player = AVPlayer(playerItem: playerItem). You can read more about it here https://mcmap.net/q/423456/-avplayeritem-fails-with-avstatusfailed-and-error-code-quot-cannot-decode-quot and here https://mcmap.net/q/428598/-too-many-avplayers-causes-terminated-due-to-memory-issueCaller
K
3

There are two workaround that I found. To me both approaches worked and I prefer the second one.

First, as @Alex Kennberg mentioned, create two set of AVPlayerLayer and AVPlayer. and switching them when you switch between videos. Be sure to set background color to clear color.

Second, use UIImageView as the owner view of AVPlayerLayer. Create thumbnail image of a video and set it to the imageview before switching the video. Be sure to set the view mode correctly.

Karisa answered 16/4, 2016 at 16:30 Comment(1)
tried second one, i can see white flicker when swtiching the videos. Can any one help?Pvc
U
1

I ran into the same issue with the video "flashing" and solved it this way in Swift 5.

Set my player variable to this

var player = AVPlayer(playerItem: nil)

Then inside my playVideo function, I changed this

self.player.replaceCurrentItem(with: AVPlayerItem(url: fileURL))

to this

player = AVPlayer(url: fileURL)

"fileURL" is the path to video I want to play.

This removed the flash and played the next video seamlessly for me.

Unmitigated answered 19/8, 2020 at 21:1 Comment(0)
C
1

You can initialise the PlayerItem and seek to zero some time before you assign it to the player. Then the flickering disappears

Cher answered 21/12, 2020 at 10:44 Comment(0)
K
0

I tried this and it worked for me.

 if (layerView1.playerLayer.superlayer) {
        [layerView1.playerLayer removeFromSuperlayer];
 }

But I am also allocating my own AVPlayerLayer instead of using IB to do it.

Kila answered 20/9, 2011 at 23:28 Comment(4)
Thanks @Be.The.Water. A couple of questions... What is your layerView1? Is it your main view, your playerView? After you remove this layer what are you doing? Obviously you must be doing something to re-add the layer at some point. ThanksUnpromising
Yeah sorry it is a class that has the AVPlayerLayer. You can check it out in apples StitchedStreamPlayer sample codes I believe they call it MyPlayerLayerView. I just have happen to name mine layerView1 since I am using two views with two different AVPlayerLayers in my implementation. Essentially I did a [self.view addSubview:layerView1]; at some point to make sure it was added.Kila
not to confuse, I also [layerView1.layer addSublayer:layerView1.playerLayer]; to add it back when I swap videos.Kila
Hi Hursh Prasad, thanks for sharing your tips. I'm trying to understand exactly how your code looks like, but it's not that easy to put together the pieces from the answer, and then from the comments. Would you be so kind to put together a clear piece of code showing what's needed? Thanks a lotFugue
E
0

After too many tries without success, I finally found a solution, not the best one, but works

My entry code bellow, have a look at the loadVideo method

#import "ViewController.h"
#import <AVKit/AVKit.h>

@interface ViewController ()<UIGestureRecognizerDelegate>
@property (nonatomic, strong) NSArray *videos;
@property (nonatomic, assign) NSInteger videoIndex;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];
    self.videos = @[@"1.mp4", @"2.mp4", @"3.mp4", @"4.mp4", @"5.mp4", @"6.mp4"];
    self.videoIndex = 0;
    [self loadVideo];
    [self configureGestures:self.view]; //changes video on swipe

}

-(void)prevZoomPanel{
    if(self.videoIndex <= 0){
        NSLog(@"cant go prev");
        return;
    }
    self.videoIndex -= 1;
    [self loadVideo];
}

-(void)nextZoomPanel{
    if(self.videoIndex >= self.videos.count - 1){
        NSLog(@"cant go next");
        return;
    }
    self.videoIndex += 1;
    [self loadVideo];
}

#pragma mark - Load Video
-(void)loadVideo{
    NSURL * bundle = [[NSBundle mainBundle] bundleURL];
    NSURL * file = [NSURL URLWithString:self.videos[self.videoIndex] relativeToURL:bundle];
    NSURL * absoluteFile = [file absoluteURL];
    AVPlayerItem *item = [AVPlayerItem playerItemWithURL:absoluteFile];

    //*************
    //DO NOT USE '[self.player replaceCurrentItemWithPlayerItem:item]', it flashes, instead, initialize the instace again.
    //Why is replaceCurrentItemWithPlayerItem flashing but playerWithPlayerItem is NOT?
    // if you want to see the diferente, uncomment the code above
    self.player = [AVPlayer playerWithPlayerItem:item];
//    if (self.player == nil) {
//        self.player = [AVPlayer playerWithPlayerItem:item];
//    }else{
//        [self.player replaceCurrentItemWithPlayerItem:item];
//    }



    //*************
    //create an instance of AVPlayerLayer and add it on self.view
    //afraid of this
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    playerLayer.frame = self.view.layer.bounds;
    [self.view.layer addSublayer:playerLayer];

    //*************
    //play the video before remove the old AVPlayerLayer instance, at this time will have 2 sublayers
    [self.player play];


    NSLog(@"sublayers before: %zd", self.view.layer.sublayers.count);


    //*************
    //remove all sublayers after 0.09s, to avoid the flash, 0.08 still flashing.
    //TODO: tested on iPhone X, need to test on slower iPhones to check if the time is enough.
    //Why do I need to wait to remove? Is that safe? What if I swipe a lot too fast, faster than 0.09s ?
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.09 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSArray* sublayers = [NSArray arrayWithArray:self.view.layer.sublayers];
        NSInteger idx = 0;

        for (CALayer *layer in sublayers) {

            if (idx < self.view.layer.sublayers.count && self.view.layer.sublayers.count > 1) {

                //to avoid memory crash, need to remove all sublayer but keep the top one.
                [layer removeFromSuperlayer];
            }
            idx += 1;
        }

        NSLog(@"sublayers after: %zd", self.view.layer.sublayers.count);
    });



    //*************
    //the code bellow is the same of the above, but with no delay
    //uncomment the code bellow AND comment the code above to test
//    NSArray* sublayers = [NSArray arrayWithArray:self.view.layer.sublayers];
//    NSInteger idx = 0;
//
//    for (CALayer *layer in sublayers) {
//
//        if (idx < self.view.layer.sublayers.count && self.view.layer.sublayers.count > 1) {
//
//            //to avoid memory crash, need to remove all sublayer but keep the top one.
//            [layer removeFromSuperlayer];
//        }
//        idx += 1;
//    }



    //*************
    //App's memory usage is about 14MB constantly, didn't increase on videos change.
    //TODO: need to test with more than 100 heavy videos.

}




-(void)configureGestures:(UIView *)view{
    UISwipeGestureRecognizer *right = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(userDidSwipeScreen:)];
    right.direction = UISwipeGestureRecognizerDirectionRight;
    right.delegate = self;
    [view addGestureRecognizer:right];
    UISwipeGestureRecognizer *left = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(userDidSwipeScreen:)];
    left.direction = UISwipeGestureRecognizerDirectionLeft;
    left.delegate = self;
    [view addGestureRecognizer:left];
}

- (void)userDidSwipeScreen:(UISwipeGestureRecognizer *)swipeGestureRecognizer{
    switch (swipeGestureRecognizer.direction) {
        case UISwipeGestureRecognizerDirectionLeft: [self nextZoomPanel];break;
        case UISwipeGestureRecognizerDirectionRight:[self prevZoomPanel];break;
        default: break;
    }
}

@end
Electrodynamometer answered 13/4, 2018 at 5:10 Comment(1)
I was trying to use this in swift, but for some reason the original layer would be of type CALayer which is different from the new layer created, and it crashes after the extra layers are removed. Any insights?Marylouisemaryly
L
0

I found a very simple solution (maybe too simple for some people, but for me it worked): In Interface Builder I set the background color of my view (which gets the video layer attached to) to black. So it's just 'flashing' black now...

Lathrop answered 14/8, 2018 at 13:54 Comment(0)
C
0

As what @blancos says in this answer

Firstly, AVPlayer doesn't show any white screen, its your background which is white

He's 100% correct because when I set my background to white, the flash was white. But when I set the background to green, the flash was green. So to fix it, I set the background to black

view.backgroundColor = .black

When switching videos, I used player.replaceCurrentItem(...):

playerItem: AVPlayerItem?

func switchVideos(url: URL) {

    playerItem = AVPlayerItem(url: url)
    player.replaceCurrentItem(with: playerItem!)
    // if necessary use the KVO to know when the video is ready to play
}
Caller answered 7/11, 2021 at 18:22 Comment(0)
D
0

To remove the flickering when passing from one video to another, you can preload your playerItem array like this:

  private var isLoading = true
  private var player: AVPlayer
  private let playerItems: [AVPlayerItem]

  init(videoURLs: [URL]) {
    self.playerItems = videoURLs.map { AVPlayerItem(url: $0) }
    self.player = AVPlayer(playerItem: playerItems.first)

    bufferVideos()

    player.play()
  }

  // Trying to fix flicker during items transition
  func bufferVideos() {
    guard let firstPlayerItem = playerItems.first else {
      return
    }

    player.isMuted = true

    playerItems.enumerated().forEach { index, playerItem in
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.1 * Double(index)) { [weak self] in
        self?.playItem(playerItem)
      }
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.15 * Double(playerItems.count)) { [weak self] in
      self?.player.isMuted = false
      self?.playItem(firstPlayerItem)
      self?.isLoading = false
    }
  }

  func playItem(_ playerItem: AVPlayerItem) {
    player.replaceCurrentItem(with: playerItem)
    player.seek(to: .zero) { [weak self] _ in
      self?.player.play()
    }
  }
}
Dado answered 1/4 at 13:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.