Analysis of ZFPlayer for iOS video player

1. Introduction

This article mainly analyzes the function realization of ZFPlayer, and summarizes the problems and solutions you encounter. First, the functions that ZFPlayer currently has:

  • Supports horizontal and vertical screen switching, and can also lock the screen orientation in full-screen playback mode

  • Support local video, network video playback

  • Support playing video in TableviewCell

  • Swipe up and down on the left 1/2 position to adjust the screen brightness (the simulator cannot adjust the brightness, please debug on the real machine)

  • Slide up and down on the 1/2 position on the right to adjust the volume (the simulator cannot adjust the volume, please debug on the real machine)

  • Swipe left and right to adjust playback progress

  • In full screen state, drag the slider to control the progress and display the preview image of the video

  • Breakpoint download function

  • Switch video resolution

ZFPlayer is an encapsulation of AVPlayer. Some people will ask what video formats it supports. If you ask this question, you can search for the formats supported by AVPlayer.

Terms closely related to AVPlayer:

  • Asset: AVAsset is an abstract class and cannot be used directly. Its subclass AVURLAsset can generate an Asset object containing media information according to the URL.

  • AVPlayerItem: There is a corresponding relationship with media resources, and it manages the information and status of media resources.

  • AVPlayerLayer: subclass of CALayer, it is mainly used to play video content in iOS

2. Realization of specific functions

2.1 Play video through a network link

AVURLAsset *urlAsset = [AVURLAsset assetWithURL:videoURL];
// Initialize playerItem
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:urlAsset];
// can also be used to initialize playerItem
// AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:videoURL];
?
// Initialize Player
AVPlayer *player = [AVPlayer playerWithPlayerItem:self. playerItem];
// Initialize playerLayer
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
// Add playerLayer to self.layer
[self.layer insertSublayer:self.playerLayer atIndex:0];

2.2 Common operations of the player

  • play:

[player play];

It should be noted that after the player is initialized, it does not necessarily start playing immediately. It needs to wait for the player’s status to become ReadyToPlay before playing.

  • pause:

[player pause];

2.3 Play multiple items

Here we have two ways to achieve it. One is to control the item of the next song by yourself and replace it with the currently playing item.

[player replaceCurrentItemWithPlayerItem:playerItem];

After iOS9, the replaceCurrentItemWithPlayerItem method of AVPlayer will call the semaphore to wait at the bottom layer when switching videos and cause the current thread to freeze. If this method is used to switch video playback in UITableViewCell, it will cause the current thread to freeze for a few seconds. It’s really hard to deal with this pit at the system level. The solution I found later is to recreate AVPlayer and AVPlayerItem every time I need to switch videos.

The other can use AVQueuePlayer to play multiple items. AVQueuePlayer is a subclass of AVPlayer. An array can be used to initialize an AVQueuePlayer object. code show as below:

NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];

Like AVPlayer, directly call the play method to play. The queue player plays the items in the queue sequentially. If you want to skip an item and play the next item, you can call the method advanceToNextItem.

You can insert and delete operations on the queue, calling the methods insertItem:afterItem:, removeItem:, and removeAllItems. Under normal circumstances, before inserting an item, you should check whether it can be inserted. By using the canInsertItem:afterItem: method, the second parameter is nil. The code is as follows:

AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
   [queuePlayer insertItem:anItem afterItem:nil];
}

2.4 seekToTime specifies to start playing from a certain second

You can use seekToTime: to position the playhead to the specified time, the following code:

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn];

seekTime: cannot be precisely positioned. If precise positioning is required, seekToTie:toleranceBefore:toleranceAfter: can be used. The code is as follows:

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];

When tolerance=0, the framework needs to do a lot of decoding work, which consumes more performance. Therefore, use this method only when you must use it, such as developing a complex multimedia editing application, which requires precise control.

I don’t need to say more about replay, click replay seekToTime:kCMTimeZero. There is also about the next time you play from the time you left last time, everyone has an idea. When you leave the current video, record which second it was played, and next time you click seekToTime to start playing at that second Just fine.

[Learning address]:FFmpeg/WebRTC/RTMP/NDK/Android audio and video streaming media advanced development

[Article Benefits]: Receive more audio and video learning packages, Dachang interview questions, technical videos and learning roadmaps for free, including (C/C++, Linux, FFmpeg webRTC rtmp hls rtsp ffplay srs, etc. ) If you need it, you can click1079654574Add to the group to receive~

2.5 Monitor playback progress

Use addPeriodicTimeObserverForInterval:queue:usingBlock: to monitor the progress of the player (1) The method passes in a CMTime structure, which will be called back every certain time, including the start and end of playback (2) If the block is inside The operation takes too long, and you may not receive a callback next time, so try to reduce the time-consuming operation of the block (3) The method will return an observer object. When the playback is complete, you need to remove this observer and add an observer:

id timeObserve = [player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    float current = CMTimeGetSeconds(time);
    float total = CMTimeGetSeconds(songItem.duration);
    if (current) {
        weakSelf. progress = current / total;
        weakSelf.playTime = [NSString stringWithFormat:@"%.f",current];
        weakSelf.playDuration = [NSString stringWithFormat:@"%.2f",total];
    }
}];

Remove observer:

if (timeObserve) {
   [player removeTimeObserver:_timeObserve];
   timeObserve = nil;
 }

2.6 Monitor and change player status

[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

The three states of the player, when the state of the playerItem changes to AVPlayerItemStatusReadyToPlay, it will be played.

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
   AVPlayerItemStatusUnknown,
   AVPlayerItemStatusReadyToPlay,
   AVPlayerItemStatusFailed
};

The observer needs to be removed after playing

[playerItem removeObserver:self forKeyPath:@"status"];

2.7 Monitor buffer progress

[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

The observer needs to be removed after playing

[playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];

2.8 Monitor network buffer status

// The buffer is empty, need to wait for data
[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
// The buffer has enough data to play
[playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];

The observer needs to be removed after playing

[playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
[playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];

2.9 Listen to AVPlayer playback completion notification

Listen to the notification AVPlayerItemDidPlayToEndTimeNotification to handle some things after playing

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

2.10 System volume related

/**
 * Get system volume
 */
- (void) configureVolume
{
    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
    _volumeViewSlider = nil;
    for (UIView *view in [volumeView subviews]){
        if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
            _volumeViewSlider = (UISlider *)view;
            break;
        }
    }
    
    // Apps using this category will not be muted when the phone's mute button is turned on, and the sound can be played when the phone is muted
    NSError *setCategoryError = nil;
    BOOL success = [[AVAudioSession sharedInstance]
                    setCategory: AVAudioSessionCategoryPlayback
                    error: &setCategoryError];
    
    if (!success) { /* handle the error in setCategoryError */ }
    
    // Listen for headphone plug-in and unplug notifications
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
}
?
/**
 * Headphone plugging and unplugging events
 */
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification
{
    NSDictionary *interuptionDict = notification. userInfo;
    
    NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
    
    switch (routeChangeReason) {
            
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            // headphone plugged in
            break;
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
        {
            // Headphones unplugged
            // Unplug the headset and continue playing
            [self play];
        }
?
            break;
?
        case AVAudioSessionRouteChangeReasonCategoryChange:
            // called at start - also when other audio wants to play
            NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange");
            break;
    }
}

set system volume

// 0 ... 1.0 value, 1.0 is the loudest sound.
self.volumeViewSlider.value = ...

2.11 Screen brightness related

// 0 ... 1.0 value, 1.0 is the maximum brightness.
[UIScreen mainScreen].brightness = ...

2.12 Screen rotation related

Except iPhone 4s (320*480) screen aspect ratio is not 16:9, Apple mobile phones are all 16:9, so horizontal and vertical screens can be realized in this way, here must use autolayout, here are two ways to achieve:

  • If you use Xib or Storyboard, you must set the aspect ratio of the player view to 16:9. If it is 4s, you can adapt it separately and add constraints (use sizeClasses)

  • Using masonry, the specific code is as follows:

[self.playerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(20);
        make.left.right.equalTo(self.view);
        // Note here that the aspect ratio of 16:9 has a lower priority than 1000, because the iPhone 4S aspect ratio is not 16:9
        make.height.equalTo(self.playerView.mas_width).multipliedBy(9.0f/16.0f).with.priority(750);
    }];

Regarding screen rotation, you can force the screen to rotate in this way. Some people will ask, why can the screen be rotated in my demo, but not integrated into my own project? I can tell you clearly that it is the horizontal screen of your project. Prohibited, you can see if it is ticked here:

device orientation

Some people will ask again, we want to achieve such a requirement, only the player page supports horizontal screen, other pages do not support horizontal screen. Ok, let me tell you how to implement it below. First, the horizontal screen in the above picture must be checked, and secondly, implement three methods in the ViewController that you need to turn the screen:

// Whether to support automatic screen transfer
- (BOOL)shouldAutorotate
{
    // Call the ZFPlayerSingleton singleton to record whether the playback state locks the screen orientation
    return !ZFPlayerShared.isLockScreen;
}
?
// Which screen rotation directions are supported
- (UIInterfaceOrientationMask) supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskAllButUpsideDown;
}
?
// The default screen orientation when the page is displayed (the current ViewController must be displayed through a modal ViewController (modal with navigation invalid) to call this method)
- (UIInterfaceOrientation) preferredInterfaceOrientationForPresentation
{
    return UIInterfaceOrientationPortrait;
}

The classification of screen rotation has been realized inside ZFPlayer (UITabBarController + ZFPlayerRotation.h UINavigationController + ZFPlayerRotation UIViewController + ZFPlayerRotation), regardless of the rootViewController of your project UINavigationController or UITabBarController, you only need to implement the above three methods on controllers that support the vertical screen.

Let’s talk about forcing the screen to rotate. Even if the user’s mobile phone locks the screen method, calling this method can still rotate:

/**
 * Force the screen to switch
 *
 * @param orientation screen orientation
 */
- (void)interfaceOrientation:(UIInterfaceOrientation)orientation
{
    // under arc
    if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
        SEL selector = NSSelectorFromString(@"setOrientation:");
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
        [invocation setSelector:selector];
        [invocation setTarget:[UIDevice currentDevice]];
        int val = orientation;
        // Start from 2 because the two parameters of 0 and 1 are already occupied by selector and target
        [invocation setArgument: &val atIndex:2];
        [invocation invoke];
    }
    /*
     // non-arc
     if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
     [[UIDevice currentDevice] performSelector:@selector(setOrientation:)
     withObject:@(orientation)];
     }
     
     // Calling this method directly fails to pass the apple review
     [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIInterfaceOrientationLandscapeRight] forKey:@"orientation"];
     */
}
Listen to device rotation notifications to handle some UI display issues

 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
 [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(onDeviceOrientationChange)
                                                 name:UIDeviceOrientationDidChangeNotification
                                               object: nil
];

To be continued….

Original Link: Analysis of ZFPlayer for iOS Video Player – Programmer Sought