今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

iOS Audio Background Playback && Lock Screen Display and Control

Playback Lock Screen Notification Bar Display#

Background#

When playing audio, it is desired that the notification interface displays and can control audio playback. Previously, the requirement was to pause playback when entering the background, so each time the notification interface was opened, playback would pause, and the effect similar to a music player could not be seen. Later, it was found that after removing the code that paused playback upon entering the background, the notification interface could display the player, but it could not control it and had no progress.

Implementation#

Support Background Playback#

First, the app needs to support background playback, which means removing the code logic that pauses playback when entering the background; on the other hand, set Target -> Signing & Capabilities, add Background Modes, and enable Audio, AirPlay, and Picture in Picture. The image is as follows:

Enterprise WeChat 20211229-141138.png

Note to set AVAudioSession, configure it according to actual needs before playback, and close it after playback.

AVAudioSessionCategory Types

Category TypeIs it muted when "Silent" or locked?Can it mix with other apps?Supports BackgroundScene Description
AVAudioSessionCategoryAmbientYesYesNoCommonly used for background sound in apps, such as listening to music while playing games
AVAudioSessionCategorySoloAmbientYesNoNoAlso background sound, but used in scenarios where you don't want to hear music while playing games
AVAudioSessionCategoryPlaybackNoDefault not allowed, but can supportYesMusic playback, can still listen to music when locked
AVAudioSessionCategoryRecordNoNo, can only recordYesRecorder, cannot play other music while recording
AVAudioSessionCategoryPlayAndRecordNoDefault allowed, can record and playYesPlaying and recording simultaneously, such as in VOIP scenarios
AVAudioSessionCategoryAudioProcessingNoNo, hardware decodes audio, cannot play or recordYesUsed for audio format processing
AVAudioSessionCategoryMultiRouteNoYesNoSimultaneous playback through headphones and USB devices

AVAudioSessionCategoryOption Types

Category Option TypeDescriptionApplicable Categories
AVAudioSessionCategoryOptionMixWithOthersSupports mixing playback with other appsAVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, AVAudioSessionCategoryMultiRoute
AVAudioSessionCategoryOptionDuckOthersLowers the volume of other app audio, highlighting this app's volumeAVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, AVAudioSessionCategoryMultiRoute
AVAudioSessionCategoryOptionAllowBluetoothSupports Bluetooth audio inputAVAudioSessionCategoryRecord, AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryOptionDefaultToSpeakerSets default audio output to speakerAVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthersApp occasionally uses audio playback and stops other app audio during playbackAVAudioSessionCategoryPlayback, AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryMultiRoute
AVAudioSessionCategoryOptionAllowBluetoothA2DPSupports stereo BluetoothAVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryOptionAllowAirPlaySupports AirPlay devicesAVAudioSessionCategoryPlayAndRecord

    func setupAudioSession() {
        do {
            // Set .notifyOthersOnDeactivation, effective when Active is false, notifying the system that this app's playback has ended, allowing other apps to continue playback
            try AVAudioSession.sharedInstance().setActive(true, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)
            
            // Switch to different Categories as needed
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.duckOthers)
        } catch {
            print("set AudioSession error: %@", error)
        }
    }

Lock Screen Notification Bar Display#

After the app supports background playback, it can be seen that the notification bar already displays, but there is no progress, no title, no image, only the app's name and small icon. The code to modify this information is as follows:

#import <MediaPlayer/MPNowPlayingInfoCenter.h>
#import <MediaPlayer/MPRemoteCommandCenter.h>
#import <MediaPlayer/MPRemoteCommand.h>
#import <MediaPlayer/MPMediaItem.h>

// Update notification bar display
- (void)updateNowPlayingInfo {
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    // Set song title
    [dict setValue:@"Title" forKey:MPMediaItemPropertyTitle];
    // Set artist name
    [dict setValue:@"Artist" forKey:MPMediaItemPropertyArtist];
    // Set album name
    [dict setValue:@"AlbumTitle" forKey:MPMediaItemPropertyAlbumTitle];
    // Set displayed image
    MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:ArtImage];
    [dict setValue:artwork forKey:MPMediaItemPropertyArtwork];
    // Set song duration
    NSTimeInterval duration = self.player.duration;
    [dict setValue:[NSNumber numberWithDouble:duration] forKey:MPMediaItemPropertyPlaybackDuration];
    // Set already played duration
    NSTimeInterval currentTime = self.player.currentTime;
    [dict setValue:[NSNumber numberWithDouble:currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
    // Set playback rate
    [dict setValue:@(1.0) forKey:MPNowPlayingInfoPropertyPlaybackRate];
    
    // Update
    [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}

If you want to stop displaying in the notification bar after playback is complete, you can set it as follows:


    [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:@{}];

To control playback pause, previous track, next track in the notification bar, you can control whether the corresponding functions are enabled by setting properties in MPRemoteCommandCenter, and there are two methods to handle the response events:

  • Method 1: Respond to corresponding events through the remoteControlReceivedWithEvent: method
  • Method 2: Use MPRemoteCommandCenter's Command to addTarget to handle corresponding events

The code to set whether the corresponding functions in the notification bar are enabled is as follows:


// In AppDelegate or the corresponding playback Controller, start receiving system control events
// Receive system control events
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];

- (void)setupCommandCenter {
    MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
    [commandCenter.playCommand removeTarget:self];
    [commandCenter.pauseCommand removeTarget:self];
    
    // Disable pre, next
    commandCenter.previousTrackCommand.enabled = NO;
    commandCenter.nextTrackCommand.enabled = NO;
    
    // Play
    commandCenter.playCommand.enabled = YES;
    
    // Pause
    commandCenter.pauseCommand.enabled = YES;
    
    // Play and pause (headphone control)
    commandCenter.togglePlayPauseCommand.enabled = NO;

    // Drag progress
    commandCenter.changePlaybackPositionCommand.enabled = YES;
}

The code for handling event response method one is as follows:


// Respond to remote events
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
    if (event.type == UIEventTypeRemoteControl) {
        switch (event.subtype) {
            case UIEventSubtypeRemoteControlPlay:
            {
                NSLog(@"RemoteControlEvents: play");
            }
                break;
            case UIEventSubtypeRemoteControlPause:
            {
                NSLog(@"RemoteControlEvents: pause");
            }
                break;
            case UIEventSubtypeRemoteControlTogglePlayPause:
                NSLog(@"Headphone control: pause || play");
                break;
            case UIEventSubtypeRemoteControlNextTrack:
            {
                NSLog(@"RemoteControlEvents: next");
            }
                break;
            case UIEventSubtypeRemoteControlPreviousTrack:
            {
                NSLog(@"RemoteControlEvents: previous");
            }
                break;
            default:
                break;
        }
    }
}

The code for handling event response method two is as follows:


- (void)setupCommandCenter {
    MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];

    // Play
    [commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
        NSLog(@"play");
        return MPRemoteCommandHandlerStatusSuccess;
    }];

    // Pause
    [commandCenter.pauseCommand addTarget:self action:@selector(handlePauseCommand:)];

    // Drag progress
    [commandCenter.changePlaybackPositionCommand addTarget:self action:@selector(handlePlaybackPositionCommand:)];
}

- (MPRemoteCommandHandlerStatus)handlePauseCommand:(id)sender {
    NSLog(@"pause");
    return MPRemoteCommandHandlerStatusSuccess;
}

- (MPRemoteCommandHandlerStatus)handlePlaybackPositionCommand:(MPChangePlaybackPositionCommandEvent *)event {
    [self.player seekToTime:CMTimeMakeWithSeconds(event.positionTime, 1)];

    NSLog(@"changePlaybackPosition to %f", event.positionTime);

    return MPRemoteCommandHandlerStatusSuccess;
}

Issues#

#

Will the notification bar display if beginReceivingRemoteControlEvents is not added, and will it affect the handling of the two methods?
The response of method two will be triggered twice.
Custom playback progress and notification bar progress are inconsistent.

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.