后台播放音乐问题总结

后台保活一直是iOS开发中的一个难点问题,前面有一篇文章专门详细的分析了https://heron-newland.github.io/tutorial/2020/08/23/iOS保活方案研究/以及各种方案的优劣点,本文主要根据开发经验,介绍一种比较靠谱的方案.

关键点

  1. 后台播放音乐一定需要申请后台任务, 即使app实现了后台常驻工功能.
/// 申请一段后台运行时间用于响铃激活
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
    if (self.backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
        [self beginBackgroundTask];
    }
}
  1. 申请后台任务是有时间限制的, 需要使用定时器去检测后台任务剩下的时间, 如果小于一定的值,那么需要重新去申请, 并且app在后台去申请后台任务会失败,最好在申请前播放一段无声音乐,然后去激活一下后台,然后再申请后台任务权限
-(void)checkBackgroundStatus{
if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 61.0) {
    NSError *error;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
    if (error) {
        NSLog(@"重复申请后台失败%@",error);
    }
    [self ringTheBell:0 once:true volumeMax:false mute:true];
    [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
}
}
  1. 后台音乐播放使用混合模式,这样能在其他音乐播放时,仍旧能播放后台音乐
1
2
  NSError *error;
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
  1. 检测是否耳机连接
1
2
3
4
5
6
7
8
9
10
11
12
13
//检测耳机是否连接,连接情况下音量最大只能调到30%

-(BOOL)isHeadsetPluggedIn {
AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
for (AVAudioSessionPortDescription* desc in [route outputs]) {
    if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones] || [[desc portType] isEqualToString:AVAudioSessionPortBluetoothA2DP]){
        return YES;
    }else{
        return NO;
    }
}
return NO;
}

完整的流程如下:

  1. 让app常驻后台(申请后台蓝牙连接实现常驻)
  2. 初始化一个定时器, 在app退到后台申请后台任务权限,并开启定时器
  3. 定时器去检查后台任务的剩余时长, 如果小于61秒,那么播放一段无声音乐,重新申请后台任务权限
  4. 执行后台任务(任意事件)
  5. 回到前台结束后台任务, 结束定时器
  6. 循环以上五个步骤

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

- (id)init {
  self = [super init];
  //初始化
  [self audioSession];
  //定时任务申请后台执行权限
  self.timer  = [NSTimer timerWithTimeInterval:3 repeats:true block:^(NSTimer * _Nonnull timer) {
      [self checkBackgroundStatus];
  }];
  [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  [self.timer setFireDate:[NSDate distantFuture]];
  return self;
}
//检测耳机是否连接,连接情况下音量最大只能调到30%
- (BOOL)isHeadsetPluggedIn {
  AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
  for (AVAudioSessionPortDescription* desc in [route outputs]) {
      if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones] || [[desc portType] isEqualToString:AVAudioSessionPortBluetoothA2DP]){
          return YES;
      }else{
          return NO;
      }
  }
  return NO;
  }



- (void)checkBackgroundStatus{
  if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 61.0) {
      NSError *error;
      [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
      if (error) {
          NSLog(@"重复申请后台失败%@",error);
      }
      [self doBackgroundTask];
      [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
  }
  }

- (void)audioSessionInterrupt:(NSNotification *)noti {
  NSLog(@"收到系统级别的打断:%@",[noti userInfo]);
  int type = [noti.userInfo[AVAudioSessionInterruptionOptionKey] intValue];

  switch (type) {
      case AVAudioSessionInterruptionTypeBegan: // 被打断
      {
          NSLog(@"播放");
          

      }
          break;
      case AVAudioSessionInterruptionTypeEnded: // 中断结束
      {
          NSLog(@"暂停");
          [self stopPlayer ];
      }
          break;
      default:
          NSLog(@"其他");
          break;

  }
  }
  //系统音量回调

- (void)volumeChangeNotification:(NSNotification *)noti {

}

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
  [self restoreSystemVolume];
  }

- (void)stopPlayer {
  if (_player != nil) {
      NSError *err;
      [self.audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&err];
      NSLog(@"停止播放失败:%@",err);
      [_player stop];
      _player = nil;
      [self restoreSystemVolume];
  }
  }

- (void)restoreSystemVolume {
  __weak typeof(self) weakSelf = self;
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      if (weakSelf.originVolume > -1) {
          MPMusicPlayerController *mpVC = [MPMusicPlayerController applicationMusicPlayer];
          mpVC.volume = weakSelf.originVolume;
          weakSelf.originVolume = -1;
      }
  });
  }


/// 后台任务
- (void)doBackgroundTask{
  /// 申请一段后台运行时间用于响铃激活
  if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
      if (self.backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
          [self beginBackgroundTask];
      }
  }
  //    dispatch_after(DISPATCH_TIME_NOW + 1, dispatch_get_main_queue(), ^{
  NSError *err;
  MPMusicPlayerController *mpVC = [MPMusicPlayerController applicationMusicPlayer];
  if (volumeMax) {
      // 正在播放,则不用重新保存系统音量值
      if (_player == nil) {
          self.originVolume = mpVC.volume;
      } else {
          [self stopPlayer];
          _player = nil;
      }
#if DEBUG
        mpVC.volume = 0.3f;
#else
        mpVC.volume = 1.0f;
#endif
    } else {
        if (!shouldMute) {//设备断开
            self.originVolume = mpVC.volume;
            mpVC.volume = 0.7f;
        }else{
        self.originVolume = -1;
        }
    }
    if ([self isHeadsetPluggedIn]){
#if DEBUG
        mpVC.volume = 0.3f;
#else
        mpVC.volume = 0.3f;
#endif
    }
    NSURL *url = [[NSBundle mainBundle] URLForResource:[@(bellIndex) description]
                                         withExtension:@"caf"];
    
    // 初始化播放器
    _player = [[AVAudioPlayer alloc] initWithContentsOfURL:url
                                                     error:&err];
NSLog(@"初始化播放器:%@",err);
    // 设置播放器声音
    
    if (shouldMute) {
        _player.volume = 0.0f;
    }else{
        _player.volume = 1.0f;
    }
    // 设置代理
    _player.delegate = self;
    // 设置播放速率
    _player.rate = 1.0;
    // 设置播放次数 负数代表无限循环
    if (once) {
        _player.numberOfLoops = 1;
    } else {
        // 根据铃声时长定义,循环播放约15s
        NSArray *loops = @[@15, @5, @8, @45, @8, @8, @7, @15, @8, @15];
        if (bellIndex < loops.count) {
            _player.numberOfLoops = [[loops objectAtIndex:bellIndex] integerValue];
        } else {
            _player.numberOfLoops = 1;
        }
    }
    
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((once ? 0.1 : 0.6) * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
        [weakSelf.audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
        [weakSelf.audioSession setActive:YES error:nil];
        // 准备播放
        if (![weakSelf.player prepareToPlay]) {            
            NSLog(@"警报预播放失败");
        }
        if (![weakSelf.player play]) {           
            NSLog(@"警报播放失败");
        }
        [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:@{}];
    });
    //    });
}

// 申请后台
// https://www.jianshu.com/p/1f2572c08816
- (void)beginBackgroundTask {
  [self.timer setFireDate:[NSDate distantPast]];
  NSLog(@"== beginBackgroundTask ==");
  __weak typeof(self) weakSelf = self;
  self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
      // 在时间到之前会进入这个block,一般是iOS7及以上是3分钟。按照规范,在这里要手动结束后台,你不写也是会结束的(据说会crash)
      NSLog(@"== backgroundTask expiration handler called ==");
      [weakSelf endBackgroundTask];
  }];
  }

// 注销后台
- (void)endBackgroundTask {
  [self.timer setFireDate:[NSDate distantFuture]];
  NSLog(@"== endBackgroundTask ==");
  if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
      [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
      self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
  } else {
      NSLog(@"== backgroundTaskIdentifier UIBackgroundTaskInvalid ==");
  }
  }

// 继续播放后台背景音乐, 取消激活当前应用的audio session
- (void)resumeBackgroundSoundWithError:(NSError **)error {
  [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:error];
  }

//暂停后台背景音乐的播放,激活当前应用的audio
- (void)pauseBackgroundSoundWithError:(NSError **)error {
  AVAudioSession *session = [AVAudioSession sharedInstance];
  [session setCategory:AVAudioSessionCategoryPlayback withOptions: AVAudioSessionCategoryOptionAllowBluetooth error:error];
  [session setActive:YES error:error];
  }

- (void)pauseBackgroundSoundWithCategoryRecord {
  AVAudioSession *session = [AVAudioSession sharedInstance];
  [session setCategory:AVAudioSessionCategoryRecord error:nil];
  [session setActive:YES error:nil];
  }

- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {
  NSLog(@"%@==%@",player,error);
  }
- (AVAudioSession *)audioSession {
  if (!_audioSession) {
      _audioSession = [AVAudioSession sharedInstance];
      NSError *error;
      [_audioSession setCategory:AVAudioSessionCategoryPlayback error:&error];
      [_audioSession setActive:YES error:&error];
      if (error) {
          NSLog(@"音频初始化失败:%@",error);
      }
  }
  return _audioSession;
  }
@end