Replaying AVPlayerItem / AVPlayer without re-downloading
Asked Answered
A

2

14

I have an AVPlayer class all set up that streams an audio file. It's a bit long, so I can't post the whole thing here. What I am stuck on is how to allow the user to replay the audio file after they have finished listening to it once. When it finishes the first time, I correctly receive a notification AVPlayerItemDidPlayToEndTimeNotification. When I go to replay it, I immediately receive the same notification, which blocks me from replaying it.

How can I reset this such that the AVPlayerItem doesn't think that it has already played the audio file? I could deallocate everything and set it up again, but I believe that would force the user to download the audio file again, which is pointless and slow.

Here are some parts of the class that I think are relevant. The output that I get when attempting to replay the file looks like this. The first two lines are exactly what I would expect, but the third is a surprise.

is playing
no timer
audio player has finished playing audio

- (id) initWithURL : (NSString *) urlString
{
    self = [super init];
    if (self) {
        self.isPlaying = NO;
        self.verbose = YES;

        if (self.verbose) NSLog(@"url: %@", urlString);
        NSURL *url = [NSURL URLWithString:urlString];

        self.playerItem = [AVPlayerItem playerItemWithURL:url];
        self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];

        [self determineAudioPlayTime : self.playerItem];

        self.lengthOfAudioInSeconds = @0.0f;

        [self.player addObserver:self forKeyPath:@"status" options:0 context:nil];

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

    return self;
}

// this is what gets called when the user clicks the play button after they have 
// listened to the file and the AVPlayerItemDidPlayToEndTimeNotification has been received
- (void) playAgain {
    [self.playerItem seekToTime:kCMTimeZero];
    [self toggleState];
}

- (void) toggleState {
    self.isPlaying = !self.isPlaying;

    if (self.isPlaying) {
        if (self.verbose) NSLog(@"is playing");
        [self.player play];

        if (!timer) {
            NSLog(@"no timer");
            CMTime audioTimer = CMTimeMake(0, 1);
            [self.player seekToTime:audioTimer];

            timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                     target:self
                                                   selector:@selector(updateProgress)
                                                   userInfo:nil
                                                    repeats:YES];
        }

    } else {
        if (self.verbose) NSLog(@"paused");
        [self.player pause];
    }
}

-(void)itemDidFinishPlaying:(NSNotification *) notification {
    if (self.verbose) NSLog(@"audio player has finished playing audio");
    [[NSNotificationCenter defaultCenter] postNotificationName:@"audioFinished" object:self];
    [timer invalidate];
    timer = nil;
    self.totalSecondsPlayed = [NSNumber numberWithInt:0];
    self.isPlaying = NO;
}
Apotheosize answered 7/1, 2015 at 5:14 Comment(3)
Any progress on this? I am facing a similar problem (with a video though) After the AVPlayerItemDidPlayToEndTimeNotification goes through you cannot replay the item. My current workaround is to create a new Item+Player but this just redownloads the whole thingIrrespective
@eyeballz - No. For the time being, I'm just deallocating it and then reinstantiating. Hardly a good solution, but it's the best that I can do for now. If you figure something out, please post.Apotheosize
Ok i got it working for me, but its Video not audio. The problem with video seemed to be that you have to keep the PlayerLayer aswell, but you dont have a layer of that kind anywhere for audio.Irrespective
D
28

You can call the seekToTime method when your player received AVPlayerItemDidPlayToEndTimeNotification

func itemDidFinishPlaying() {
    self.player.seek(to: CMTime.zero)
    self.player.play()
}
Dizzy answered 16/9, 2015 at 6:18 Comment(1)
kCMTimeZero becomes .zero in swift 4+Asante
F
5

Apple recommends using AVQueueplayer with an AVPlayerLooper.

Here's Apple's (slightly revised) sample code:

AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] init];    

AVAsset *asset = // AVAsset with its 'duration' property value loaded
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];

 // Create a new player looper with the queue player and template item
self.playerLooper = [AVPlayerLooper playerLooperWithPlayer:queuePlayer
                                              templateItem:playerItem];

 // Begin looping playback
[queuePlayer play];

The AVPlayerLooper does all the event listening and playing for you, and the queue player is used to create what they call a "treadmill pattern". This pattern is essentially chaining multiple instances of the same AVAssetItem in a queue player and moving each finished asset back to the beginning of the queue.

The advantage of this approach is that it enables the framework to preroll the next asset (which is the same asset in this case, but its start still needs prerolling) before it arrives, reducing latency between the asset's end and looped start.

This is described in greater detail at ~15:00 in the video here: https://developer.apple.com/videos/play/wwdc2016/503/

Friedafriedberg answered 8/5, 2017 at 14:37 Comment(1)
"Apple says XYZ is best for native" is a pretty minimal answer. Please summarize the linked video's arguments here as best you can.Tibbetts

© 2022 - 2024 — McMap. All rights reserved.