Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to quickly switch between videos

I am creating an application which displays a certain video based on an external event, which may require the playing video to change quickly - once per second or more. However, there must not be a gap or lag between the videos.

What is the best way to do this? There are only four videos, each about two megabytes.

I was considering creating four MPMoviePlayerControllers, and have their views added to the main view but hidden, and switching by pausing and hiding the current video, then unhiding and playing the next video. Is there a more elegant solution?

Edit

Here's some more information for my exact sitaution:

  • The different video frames share mostly common pixels- so it's OK for a frame to stick during switch, but NOT okay for black frames to appear.
  • Each video is only about ten seconds long, and there are only four videos. The general state transitions are 1<->2<->3<->4->1.
  • The video playback should compatible with simultaneous AVAudioRecorder recording. As far as I can tell, MPMoviePlayerController is not.
like image 744
Tim Avatar asked Mar 07 '12 20:03

Tim


2 Answers

You'll need to set up and prebuffer all the video streams to avoid hiccups, so I don't think your multiple MPMoviePlayerController solution is too far off the mark.

However, that specific solution is potentially problematic because each movie player has its own UI. The UIs do not synchronize with each other, so one might be showing the control bar, another not; one might be in full screen mode, etc. Switching among them will cause visual discontinuities.

Since it sounds like your video switching is not necessarily user-initiated, I'm guessing you don't care too much about the standard video player UI.

I would suggest dropping down to the underlying layer, AV Foundation. In theory, you can just create an AVPlayerItem for each video. This is a stream-management object with no UI overhead, so it's perfect for what you're doing. You could then -- again, in theory -- create one AVPlayer and one AVPlayerLayer to handle the display. When you wanted to switch from one AVPlayerItem stream to another, you could call the AVPlayer's replaceCurrentItemWithPlayerItem: message to swap out the data stream.

I made a little test project (GitHub) to verify this, and unfortunately the straightforward solution isn't quite perfect. There is no video flow glitching, but in the transition from AVPlayer to AVPlayer, the presentation layer seems to briefly flash the previous movie's last-seen frame at the size appropriate to the next movie. It seems to help to allocate separate AVPlayer objects for each movie and switch among them to a constant player layer. There still seems to be an instantaneous flash of background, but at least it's a subtle defect. Here's the gist of the code:

@interface ViewController : UIViewController
{
    NSMutableArray *players;
    AVPlayerLayer *playerLayer;
}

@property (nonatomic) IBOutlet UIView *videoView;

- (IBAction) selectVideo:(id)sender;

@end

@implementation ViewController

@synthesize videoView;

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSArray *videoTitles = [NSArray arrayWithObjects:@"Ultimate Dog Tease", 
        @"Backin Up", @"Herman Cain", nil];
    players = [NSMutableArray array];
    for (NSString *title in videoTitles) {
        AVPlayerItem *player = [AVPlayer playerWithURL:[[NSBundle mainBundle] 
            URLForResource:title withExtension:@"mp4"]];
        [player addObserver:self forKeyPath:@"status" options:0 context:nil];
        [players addObject:player];
    }
    playerLayer = [AVPlayerLayer playerLayerWithPlayer:[players objectAtIndex:0]];
    playerLayer.frame = self.videoView.layer.bounds;
    playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    [self.videoView.layer addSublayer:playerLayer];
}

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
    change:(NSDictionary *)change context:(void *)context
{
    [object removeObserver:self forKeyPath:@"status"];
    for (AVPlayer *player in players) {
         if (player.status != AVPlayerStatusReadyToPlay) {
             return;
         }
    }
    // All videos are ready to go
    [self playItemAtIndex:0];
}

- (void) playItemAtIndex:(NSUInteger)idx
{
    AVPlayer *newPlayer = [players objectAtIndex:idx];
    if (newPlayer != playerLayer.player) {
        [playerLayer.player pause];
        playerLayer.player = newPlayer;
    }
    [newPlayer play];
}

- (IBAction) selectVideo:(id)sender 
{
    [self playItemAtIndex:((UILabel *)sender).tag];
}

@end

Half the code is there just to observe the state of the players and make sure that playback doesn't start until all videos have been buffered.

Allocating three separate AVPlayerLayers (in addition to three AVPlayers) prevents any sort of flash. Unfortunately, an AVPlayer connected to an undisplayed AVPlayerLayer seems to assume that it doesn't need to maintain a video buffer. Every switch among layers then produces a transient video stutter. So that's no good.

A couple of things to note when using AV Foundation are:

1) The AVPlayer object doesn't have built-in support for looped playback. You'll have to observe for the end of the current video and manually seek to time zero.

2) There's no UI at all other than the video frame, but again, I'm guessing that this might actually be an advantage for you.

like image 81
GSnyder Avatar answered Sep 30 '22 03:09

GSnyder


The MPMoviePlayerController is a singleton. Four instances will share the same pointer, the same view, etc. With the native player, I think you have only two alternatives: one is to change the contentURL property when you want a transition. If the latency this way unacceptable, the other alternative is to produce a longer video with the shorter clips concatenated. You can create very quick jumps within the single, longer clip by setting currentPlaybackTime.

like image 24
danh Avatar answered Oct 03 '22 03:10

danh