ios avplayer trigger streaming is out of buffer

2019-01-16 07:08发布

I want to reconnect to the server when the streaming buffer is empty.

How can I trigger a method when the AVPlayer or AVPlayerItem buffer is empty?

I know there are playbackLikelyToKeepUp, playbackBufferEmpty and playbackBufferFull methods to check the buffer status, but those are not callbacks.

Are there any callback functions, or any observers I should add?

4条回答
Melony?
2楼-- · 2019-01-16 07:17

Try this code it should solve all your nightmares:

#import <AVFoundation/AVFoundation.h>

@interface CustomAVPlayerItem : AVPlayerItem
{
    BOOL bufferEmptyVideoWasStopped;
}

-(void)manuallyRegisterEvents;

@end

=========== in the .m file

#import "CustomAVPlayerItem.h"
#import "AppDelegate.h"

@implementation CustomAVPlayerItem

-(void)manuallyRegisterEvents
{
    //NSLog(@"manuallyRegisterEvents %lu", (unsigned long)self.hash);

    [self addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:NULL];
    [self addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:NULL];

    bufferEmptyVideoWasStopped=NO;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == self && [keyPath isEqualToString:@"playbackBufferEmpty"])
    {
        if (self.playbackBufferEmpty)
        {
            //NSLog(@"AVPLAYER playbackBufferEmpty");
            bufferEmptyVideoWasStopped=YES;
            [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_VIDEO_PLAYBACKBUFFER_EMPTY object:self];
        }
    }
    else if (object == self && [keyPath isEqualToString:@"playbackLikelyToKeepUp"])
    {
        if (self.playbackLikelyToKeepUp)
        {
            //NSLog(@"AVPLAYER playbackLikelyToKeepUp");

            if(bufferEmptyVideoWasStopped)
            {
                bufferEmptyVideoWasStopped=NO;

                [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_VIDEO_PLAYBACKBUFFER_FULL object:self];
            }
            else
                [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_VIDEO_PLAYBACKBUFFER_KEEP_UP object:self];

        }
    }
}

-(void)dealloc
{
    //NSLog(@"dealloc CustomAVPlayerItem %lu", (unsigned long)self.hash);

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

@end

===== and now where you want to use it

-(void)startVideoWithSound
{
    if([CustomLog jsonFieldAvailable:[videoObject objectForKey:@"video_url"]])
    {
        CustomAVPlayerItem *playerItem=[[CustomAVPlayerItem alloc] initWithURL:[[OfflineManager new] getCachedURLForVideoURL:[videoObject objectForKey:@"video_url"]]];

        AVPlayer *player=[AVPlayer playerWithPlayerItem:playerItem];
        [playerItem manuallyRegisterEvents];
        [player play];

        player.muted=NO;

        [viewPlayer setPlayer:player];

        viewPlayer.hidden=NO;

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifBufferEmpty:) name:NOTIFICATION_VIDEO_PLAYBACKBUFFER_EMPTY object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifBufferFull:) name:NOTIFICATION_VIDEO_PLAYBACKBUFFER_FULL object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifBufferKeepUp:) name:NOTIFICATION_VIDEO_PLAYBACKBUFFER_KEEP_UP object:nil];
    }
}

- (void)notifBufferEmpty:(NSNotification *)notification
{
    //NSLog(@"notifBufferEmpty");

    [[viewPlayer player] play]; // resume it
    viewLoading.hidden=NO;
}

- (void)notifBufferFull:(NSNotification *)notification
{
    //NSLog(@"notifBufferFull");
    viewLoading.hidden=YES;
}

- (void)notifBufferKeepUp:(NSNotification *)notification
{
    //NSLog(@"notifBufferKeepUp");
    viewLoading.hidden=YES;
}
查看更多
Melony?
3楼-- · 2019-01-16 07:26

You will need to drop down into Core Audio and CFReadStream to do this. WIth CFReadStream, you can provide a callback that gets called on certain stream events like end encountered, read error, etc. From there you can trigger a reconnect to the server. If you're consuming an HTTP stream, you can add the range header to the HTTP request so you can tell the server to send the stream from a point you specify (which would be the last byte you received before + 1).

查看更多
唯我独甜
4楼-- · 2019-01-16 07:39

you can add observer for those keys:

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

The first one will warn you when your buffer is empty and the second when your buffer is good to go again.

Then to handle the key change you can use this code:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
    if (!player)
    {
        return;
    }

    else if (object == playerItem && [keyPath isEqualToString:@"playbackBufferEmpty"])
    {
        if (playerItem.playbackBufferEmpty) {
            //Your code here
        }
    }

    else if (object == playerItem && [keyPath isEqualToString:@"playbackLikelyToKeepUp"])
    {
        if (playerItem.playbackLikelyToKeepUp)
        {
            //Your code here
        }
    }
}
查看更多
再贱就再见
5楼-- · 2019-01-16 07:41
/* Swift 3.0, Add Observers */    
func setupPlayerObservers(){
    player.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
    player.addObserver(self, forKeyPath: "rate", options: [.new], context: nil)
    player.currentItem?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
    player.currentItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.old, .new], context: nil)
    player.currentItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.old, .new], context: nil)

}    

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    //this is when the player is ready and rendering frames
    if keyPath == "currentItem.loadedTimeRanges" {

        if !hasLoaded {
            activityIndicatorView.stopAnimating()
            Mixpanel.mainInstance().track(event: "Play", properties: ["success": true, "type": "clip"])
        }
        hasLoaded = true
    }

    if keyPath == "rate" {
        if player.rate == 0.0 {
            playPauseButton.setImage(UIImage(named: "PlayButton"), for: UIControlState())
        } else {
            playPauseButton.setImage(UIImage(named: "PauseButton"), for: UIControlState())
        }

    }

    if keyPath == "status" {
        // Do something here if you get a failed || error status
    }

    if keyPath == "playbackBufferEmpty" {
        let time = Int(CMTimeGetSeconds(player.currentTime()))

        bufferingCount += 1
        if bufferingCount % 4 == 0 {
            Mixpanel.mainInstance().track(event: "VideoBuffered", properties: ["numTimes": bufferingCount, "type": "clip", "currentTime": time])
        }
        activityIndicatorView.isHidden = false
        activityIndicatorView.startAnimating()
    }


    if keyPath == "playbackLikelyToKeepUp" {
        activityIndicatorView.isHidden = true
        activityIndicatorView.stopAnimating()
    }
}
/* Remove observers in deinit */
deinit {
    player.removeTimeObserver(timeObserver)
    player.removeObserver(self, forKeyPath: "currentItem.loadedTimeRanges")
    player.removeObserver(self, forKeyPath: "rate")
    player.currentItem?.removeObserver(self, forKeyPath: "playbackBufferEmpty")
    player.currentItem?.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
    player.currentItem?.removeObserver(self, forKeyPath: "status")
}
查看更多
登录 后发表回答