iOS Avplayer Trigger Streaming Is Out of Buffer

ios avplayer trigger streaming is out of buffer

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
}
}
}

Handle streaming events with AVPlayer

For the first question

You can refer to my answer on this topic ios avplayer trigger streaming is out of buffer

For the second

Here is how I solved this same problem:

Inside where you handle the event for buffer empty add this code:

    if (object == playerItem && [keyPath isEqualToString:@"playbackBufferEmpty"])
{
if (playerItem.playbackBufferEmpty) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"message" object:@"Buffering..."];

if([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground)
{
task = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
}];
}
}
}

Now you will have to stop this background task after your buffer is ready to go again:

if (object == playerItem && [keyPath isEqualToString:@"playbackLikelyToKeepUp"])
{
if (playerItem.playbackLikelyToKeepUp)
{
[player play];

if([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground)
{
[[UIApplication sharedApplication] endBackgroundTask:task];
task = 0;
}
}
}

ps: task is declared on my .h file as UIBackgroundTaskIdentifier task;

AVPlayer Audio Buffering from Live Stream

I was having this same issue. The answer was in creating an error delegate that launched a selector every time the player stopped (The error changes when the network connection is interrupted or the stream didn't load properly):

Here are my delegates, just outside and above my RadioPlayer class:

protocol errorMessageDelegate {
func errorMessageChanged(newVal: String)
}

protocol sharedInstanceDelegate {
func sharedInstanceChanged(newVal: Bool)
}

Now my class:

import Foundation
import AVFoundation
import UIKit

class RadioPlayer : NSObject {

static let sharedInstance = RadioPlayer()
var instanceDelegate:sharedInstanceDelegate? = nil
var sharedInstanceBool = false {
didSet {
if let delegate = self.instanceDelegate {
delegate.sharedInstanceChanged(self.sharedInstanceBool)
}
}
}
private var player = AVPlayer(URL: NSURL(string: Globals.radioURL)!)
private var playerItem = AVPlayerItem?()
private var isPlaying = false

var errorDelegate:errorMessageDelegate? = nil
var errorMessage = "" {
didSet {
if let delegate = self.errorDelegate {
delegate.errorMessageChanged(self.errorMessage)
}
}
}

override init() {
super.init()

errorMessage = ""

let asset: AVURLAsset = AVURLAsset(URL: NSURL(string: Globals.radioURL)!, options: nil)

let statusKey = "tracks"

asset.loadValuesAsynchronouslyForKeys([statusKey], completionHandler: {
var error: NSError? = nil

dispatch_async(dispatch_get_main_queue(), {
let status: AVKeyValueStatus = asset.statusOfValueForKey(statusKey, error: &error)

if status == AVKeyValueStatus.Loaded{

let playerItem = AVPlayerItem(asset: asset)

self.player = AVPlayer(playerItem: playerItem)
self.sharedInstanceBool = true

} else {
self.errorMessage = error!.localizedDescription
print(error!)
}

})

})

NSNotificationCenter.defaultCenter().addObserverForName(
AVPlayerItemFailedToPlayToEndTimeNotification,
object: nil,
queue: nil,
usingBlock: { notification in
print("Status: Failed to continue")
self.errorMessage = "Stream was interrupted"
})

print("Initializing new player")

}

func resetPlayer() {
errorMessage = ""

let asset: AVURLAsset = AVURLAsset(URL: NSURL(string: Globals.radioURL)!, options: nil)

let statusKey = "tracks"

asset.loadValuesAsynchronouslyForKeys([statusKey], completionHandler: {
var error: NSError? = nil

dispatch_async(dispatch_get_main_queue(), {
let status: AVKeyValueStatus = asset.statusOfValueForKey(statusKey, error: &error)

if status == AVKeyValueStatus.Loaded{

let playerItem = AVPlayerItem(asset: asset)
//playerItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: &ItemStatusContext)

self.player = AVPlayer(playerItem: playerItem)
self.sharedInstanceBool = true

} else {
self.errorMessage = error!.localizedDescription
print(error!)
}

})
})
}

func bufferFull() -> Bool {
return bufferAvailableSeconds() > 45.0
}

func bufferAvailableSeconds() -> NSTimeInterval {
// Check if there is a player instance
if ((player.currentItem) != nil) {

// Get current AVPlayerItem
let item: AVPlayerItem = player.currentItem!
if (item.status == AVPlayerItemStatus.ReadyToPlay) {

let timeRangeArray: NSArray = item.loadedTimeRanges
if timeRangeArray.count < 1 { return(CMTimeGetSeconds(kCMTimeInvalid)) }
let aTimeRange: CMTimeRange = timeRangeArray.objectAtIndex(0).CMTimeRangeValue
//let startTime = CMTimeGetSeconds(aTimeRange.end)
let loadedDuration = CMTimeGetSeconds(aTimeRange.duration)

return (NSTimeInterval)(loadedDuration);
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}
else {
return(CMTimeGetSeconds(kCMTimeInvalid))
}
}

func play() {
player.play()
isPlaying = true
print("Radio is \(isPlaying ? "" : "not ")playing")
}

func pause() {
player.pause()
isPlaying = false
print("Radio is \(isPlaying ? "" : "not ")playing")
}

func currentlyPlaying() -> Bool {
return isPlaying
}

}

Now, in the RadioViewController:

import UIKit
import AVFoundation

class RadioViewController: UIViewController, errorMessageDelegate, sharedInstanceDelegate {

// MARK: Properties

var firstErrorSkip = true
var firstInstanceSkip = true

@IBOutlet weak var listenLabel: UILabel!
@IBOutlet weak var radioSwitch: UIImageView!

@IBAction func back(sender: AnyObject) {
print("Dismissing radio view")
if let navigationController = self.navigationController
{
navigationController.popViewControllerAnimated(true)
}
}

@IBAction func switched(sender: AnyObject) {
toggle()
}

override func viewDidLoad() {
super.viewDidLoad()

do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")

} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}

RadioPlayer.sharedInstance.errorDelegate = self
RadioPlayer.sharedInstance.instanceDelegate = self

if RadioPlayer.sharedInstance.currentlyPlaying() {
radioSwitch.image = UIImage(named: "Radio_Switch_Active")
listenLabel.text = "Click to Pause Radio Stream:"
}

}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

func toggle() {
if RadioPlayer.sharedInstance.currentlyPlaying() {
pauseRadio()
} else {
playRadio()
}
}

func playRadio() {
firstErrorSkip = false
firstInstanceSkip = false

if RadioPlayer.sharedInstance.errorMessage != "" || RadioPlayer.sharedInstance.bufferFull() {
resetStream()
} else {
radioSwitch.image = UIImage(named: "Radio_Switch_Active")
listenLabel.text = "Click to Pause Radio Stream:"
RadioPlayer.sharedInstance.play()
}
}

func pauseRadio() {
RadioPlayer.sharedInstance.pause()
radioSwitch.image = UIImage(named: "Radio_Switch_Inactive")
listenLabel.text = "Click to Play Radio Stream:"
}

func resetStream() {
print("Reloading interrupted stream");
RadioPlayer.sharedInstance.resetPlayer()
//RadioPlayer.sharedInstance = RadioPlayer();
RadioPlayer.sharedInstance.errorDelegate = self
RadioPlayer.sharedInstance.instanceDelegate = self
if RadioPlayer.sharedInstance.bufferFull() {
radioSwitch.image = UIImage(named: "Radio_Switch_Active")
listenLabel.text = "Click to Pause Radio Stream:"
RadioPlayer.sharedInstance.play()
} else {
playRadio()
}
}

func errorMessageChanged(newVal: String) {
if !firstErrorSkip {
print("Error changed to '\(newVal)'")
if RadioPlayer.sharedInstance.errorMessage != "" {
print("Showing Error Message")
let alertController = UIAlertController(title: "Stream Failure", message: RadioPlayer.sharedInstance.errorMessage, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default, handler: nil))

self.presentViewController(alertController, animated: true, completion: nil)

pauseRadio()

}
} else {
print("Skipping first init")
firstErrorSkip = false
}
}

func sharedInstanceChanged(newVal: Bool) {
if !firstInstanceSkip {
print("Detected New Instance")
if newVal {
RadioPlayer.sharedInstance.play()
}
} else {
firstInstanceSkip = false
}
}

}

Avplayer item pre buffering while scrolling on UITableView

AVPlayer begins streaming m3u8 when it is instantiated. (I noticed this by monitoring the Network graph in the Debug navigator in Xcode. I instantiated an AVPlayer without calling -[play] and the network was under load.) Instead of loading the AVPlayer once the cell becomes visible, instantiate the AVPlayer before the cell is visible and play the content when it becomes visible.

This can be implemented by first having some data structure hold the AVPlayer items. I would recommend a NSMutableDictionary, as we can set the key to the video's URL we want and the object can be the AVPlayer already loaded with the URL. There are two ways to populate the structure. You could load it all at once in a method like -viewDidLoad or load items in dynamically in -scrollViewDidScroll: to determine if we are close to a cell with a video. I would use the former if there is not a lot of content and the user is almost guaranteed to watch all the videos. I would use the latter if we have a lot of content to load or if the user might not watch all the videos. Here is an example with a UITableViewController.

MyTableViewController.h

#import <UIKit/UIKit.h>
#import <AVKit/AVKit.h>

@interface MyTableViewController : UITableViewController
{
NSMutableDictionary *mediaPlayers; // stores the media players, key = NSString with URL, value = AVPlayer
// you don't need mediaKeyIndexPaths or mediaKeyURLs if you are taking the load-all-at-once approach
NSMutableSet *mediaKeyIndexPaths; // set that stores the key NSIndexPaths that trigger loading a media player
NSDictionary *mediaKeyURLs; // dictionary that maps a key NSIndexPath to a URL (NSString), key = NSIndexPath, value = NSString
}

@property (strong, nonatomic) AVPlayerViewController *playerController;

@end

MyTableViewController.m

#import "MyTableViewController.h"

@implementation MyTableViewController

- (void)viewDidLoad
{
[super viewDidLoad];
// if you want to load all items at once, you can forget mediaKeyIndexPaths and mediaKeyURLs
// and instead just load all of your media here
mediaPlayers = [[NSMutableDictionary alloc] init];

// if taking the load-all-at-once approach, load mediaPlayers like this
// NSString *videoURLString = @"https://playeritemvideourl";
// AVPlayer *player = [AVPlayer playerWithURL:videoURLString];
// [mediaPlayers setObject:player forKey:@"https://playeritemvideourl"];

// lets say that the cell with the media in it is defined here
NSIndexPath *dummyMediaIndexPath = [NSIndexPath indexPathForRow:40 inSection:0];
// calculate an index path that, when visible, will trigger loading the media at dummyMediaIndexPath
NSIndexPath *dummyKeyIndexPath = [NSIndexPath indexPathForRow:dummyMediaIndexPath.row-10
inSection:dummyMediaIndexPath.section];

// add the key index path to the set of key index paths
mediaKeyIndexPaths = [[NSMutableSet alloc] initWithObjects:dummyKeyIndexPath, nil];
// define mediaKeyURLs mapping the key dummyKeyIndexPath to the value of the URL string we want
mediaKeyURLs = [[NSDictionary alloc] initWithObjectsAndKeys:
@"https://playeritemvideourl", dummyKeyIndexPath,
nil];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 100;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TextCell" forIndexPath:indexPath];

// this is the row of dummyMediaIndexPath defined in -viewDidLoad
if (indexPath.row == 40)
{
self.playerController = [[AVPlayerViewController alloc] init];
NSString *videoURLString = @"https://playeritemvideourl";
AVPlayer *player = [mediaPlayers objectForKey:videoURLString]; // load player with URL
if (!player)
{
player = [AVPlayer playerWithURL:[NSURL URLWithString:videoURLString]];
NSLog(@"Video with URL: %@ was not preloaded. Loading now.", videoURLString);
}
self.playerController.player = player;
// [player play];
// present your playerController here
[cell setText:@"Player cell"];
}
else
{
[cell setText:[NSString stringWithFormat:@"Cell #%li", (long)indexPath.row]];
}

return cell;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// get visible rows
NSArray *visibleRows = [self.tableView indexPathsForVisibleRows];
// traverse through rows
for (NSIndexPath *i in visibleRows)
{
// we use an NSSet to quickly determine if i is contained in mediaKeyIndexPaths
if ([mediaKeyIndexPaths containsObject:i])
{
[mediaKeyIndexPaths removeObject:i]; // we only need to load a player once
NSString *videoURLString = [mediaKeyURLs objectForKey:i];
NSLog(@"Preloading URL: %@", videoURLString); // for information purposes only
AVPlayer *player = [AVPlayer playerWithURL:[NSURL URLWithString:videoURLString]];
[mediaPlayers setObject:player forKey:videoURLString];
}
}
}

@end

This is about as specific as I can make with the amount of code and information you provided. Hope this helps!

AVPlayer stops playing video after buffering

Since iOS 10.x, you can make some buffer settings, for example you can decide how many seconds you'll need to buffering your video:

    if #available(iOS 10.0, tvOS 10.0, OSX 10.12, *) {
avPlayer?.automaticallyWaitsToMinimizeStalling = .playWhenBufferNotEmpty
//preferredForwardBufferDuration -> default is 0, which means `AVPlayer` handle it independently, try more seconds like 5 or 10.
playerItem.preferredForwardBufferDuration = TimeInterval(5)
}


Related Topics



Leave a reply



Submit