Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions mobile/ios/Runner/Audio/PinepodsAudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class PinepodsAudioPlayer: NSObject {
private var skipSilenceEnabled = false
private var skipSegments: [(start: Double, end: Double)] = []

// AVPlayer.play() always resets rate to 1.0, so we track the desired rate here and reassert it after play().
private var currentPlaybackRate: Float = 1.0

init(eventSink: FlutterEventSink?) {
self.eventSink = eventSink
super.init()
Expand Down Expand Up @@ -282,8 +285,16 @@ class PinepodsAudioPlayer: NSObject {
}
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
NSLog("[PinepodsAudioPlayer] Audio interruption ended - should resume available")
// Don't auto-resume for podcasts - let user decide
// shouldResume only fires for transient interruptions (e.g. Siri), not phone calls.
NSLog("[PinepodsAudioPlayer] Audio interruption ended - resuming playback")
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
NSLog("[PinepodsAudioPlayer] Failed to reactivate audio session after interruption: \(error)")
}
play()
} else {
NSLog("[PinepodsAudioPlayer] Audio interruption ended - resume not recommended by system")
}

@unknown default:
Expand Down Expand Up @@ -495,22 +506,20 @@ class PinepodsAudioPlayer: NSObject {
if finished {
NSLog("[PinepodsAudioPlayer] Seeked to start position: \(startPosition)ms")
}
self?.player?.play()
// Update now playing info again after playback starts
self?.updateNowPlayingPlaybackInfo()
self?.play()
}
} else {
player?.play()
// Update now playing info after playback starts
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.updateNowPlayingPlaybackInfo()
}
play()
}
}

func play() {
NSLog("[PinepodsAudioPlayer] play")
NSLog("[PinepodsAudioPlayer] play, reasserting rate \(currentPlaybackRate)")
player?.play()
// play() resets rate to 1.0, so reassert the desired rate.
if currentPlaybackRate != 1.0 {
player?.rate = currentPlaybackRate
}
// Slight delay to allow rate to update before refreshing Now Playing
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
self?.updateNowPlayingPlaybackInfo()
Expand Down Expand Up @@ -562,7 +571,11 @@ class PinepodsAudioPlayer: NSObject {

func setPlaybackSpeed(_ speed: Float) {
NSLog("[PinepodsAudioPlayer] setPlaybackSpeed: \(speed)")
player?.rate = speed
currentPlaybackRate = speed
// Setting a nonzero rate while paused would start playback, so only apply it if already playing.
if player?.rate != 0 {
player?.rate = speed
}
updateNowPlayingPlaybackInfo()
}

Expand Down
10 changes: 7 additions & 3 deletions mobile/lib/services/pinepods/pinepods_audio_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,10 @@ class PinepodsAudioService {
// Start playing with the existing audio service
await _audioPlayerService.playEpisode(episode: episode, resume: resume);

// Apply server-side speed after episode starts — overrides any locally stored speed
await _audioPlayerService.setPlaybackSpeed(playDetails.playbackSpeed);
// Only apply the server speed if it's a real per-podcast override, not the echoed-back default (#882, #963).
if (playDetails.playbackSpeedCustomized) {
await _audioPlayerService.setPlaybackSpeed(playDetails.playbackSpeed);
}

// Handle skip intro if enabled and episode just started
if (playDetails.startSkip > 0 && !resume) {
Expand Down Expand Up @@ -323,7 +325,9 @@ class PinepodsAudioService {
.getPlayEpisodeDetails(userId, podcastId, pinepodsEpisode.isYoutube);

if (_currentEpisodeId == episodeId) {
await _audioPlayerService.setPlaybackSpeed(playDetails.playbackSpeed);
if (playDetails.playbackSpeedCustomized) {
await _audioPlayerService.setPlaybackSpeed(playDetails.playbackSpeed);
}

if (playDetails.startSkip > 0 && !resume) {
await _audioPlayerService.seek(position: playDetails.startSkip);
Expand Down
4 changes: 4 additions & 0 deletions mobile/lib/services/pinepods/pinepods_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ class PinepodsService {
playbackSpeed: (data['playback_speed'] as num?)?.toDouble() ?? 1.0,
startSkip: data['start_skip'] ?? 0,
endSkip: data['end_skip'] ?? 0,
playbackSpeedCustomized: data['playback_speed_customized'] as bool? ?? false,
);
}
return PlayEpisodeDetails(playbackSpeed: 1.0, startSkip: 0, endSkip: 0);
Expand Down Expand Up @@ -2792,11 +2793,14 @@ class PlayEpisodeDetails {
final double playbackSpeed;
final int startSkip;
final int endSkip;
// True only for a genuine per-podcast override, not the echoed-back account default.
final bool playbackSpeedCustomized;

PlayEpisodeDetails({
required this.playbackSpeed,
required this.startSkip,
required this.endSkip,
this.playbackSpeedCustomized = false,
});
}

Expand Down