@@ -321,6 +321,9 @@ struct AudioPlayerView: View {
321
321
. foregroundColor ( . gray)
322
322
. padding ( . bottom)
323
323
324
+ // Speaker Toggle
325
+
326
+
324
327
// Time and Progress
325
328
HStack {
326
329
Text ( playerViewModel. currentTimeString)
@@ -341,6 +344,9 @@ struct AudioPlayerView: View {
341
344
342
345
// Playback Controls
343
346
HStack ( spacing: 30 ) {
347
+ Button ( action: { } ) {
348
+
349
+ }
344
350
Button ( action: { playerViewModel. skipBackward ( ) } ) {
345
351
Image ( systemName: " gobackward.15 " )
346
352
. font ( . title2)
@@ -355,6 +361,12 @@ struct AudioPlayerView: View {
355
361
Image ( systemName: " goforward.15 " )
356
362
. font ( . title2)
357
363
}
364
+ Button ( action: { playerViewModel. toggleSpeaker ( ) } ) {
365
+ Image ( systemName: playerViewModel. isSpeakerOn ? " speaker.wave.2.fill " : " speaker.wave.2 " )
366
+ . font ( . title2)
367
+ . foregroundColor ( playerViewModel. isSpeakerOn ? . blue : . gray)
368
+ }
369
+ . padding ( . bottom, 5 )
358
370
}
359
371
}
360
372
. onAppear {
@@ -370,13 +382,43 @@ struct AudioPlayerView: View {
370
382
class AudioPlayerViewModel : ObservableObject {
371
383
private var player : AVPlayer ?
372
384
private var timeObserver : Any ?
385
+ private var playerItemObserver : NSKeyValueObservation ?
373
386
374
387
@Published var isPlaying = false
375
388
@Published var progress : Double = 0
376
389
@Published var currentTimeString = " 00:00 "
377
390
@Published var durationString = " 00:00 "
391
+ @Published var isSpeakerOn = true
392
+
393
+ private func configureAudioSession( ) {
394
+ do {
395
+ let audioSession = AVAudioSession . sharedInstance ( )
396
+ try audioSession. setCategory ( . playback, mode: . default)
397
+ try audioSession. setActive ( true )
398
+ updateSpeakerState ( )
399
+ } catch {
400
+ print ( " Failed to configure audio session: \( error) " )
401
+ }
402
+ }
403
+
404
+ private func updateSpeakerState( ) {
405
+ do {
406
+ let audioSession = AVAudioSession . sharedInstance ( )
407
+ try audioSession. overrideOutputAudioPort (
408
+ isSpeakerOn ? . speaker : . none
409
+ )
410
+ } catch {
411
+ print ( " Failed to switch audio output: \( error) " )
412
+ }
413
+ }
414
+
415
+ func toggleSpeaker( ) {
416
+ isSpeakerOn. toggle ( )
417
+ updateSpeakerState ( )
418
+ }
378
419
379
420
func setupPlayer( with url: URL ) {
421
+ configureAudioSession ( )
380
422
player = AVPlayer ( url: url)
381
423
382
424
// Add periodic time observer
@@ -391,24 +433,44 @@ class AudioPlayerViewModel: ObservableObject {
391
433
self . durationString = self . formatTime ( duration)
392
434
}
393
435
436
+ // Observe player item status for completion
437
+ NotificationCenter . default. addObserver ( forName: . AVPlayerItemDidPlayToEndTime,
438
+ object: player? . currentItem,
439
+ queue: . main) { [ weak self] _ in
440
+ self ? . handlePlaybackCompletion ( )
441
+ }
442
+
394
443
// Update duration when item is ready
395
- player ? . currentItem ? . asset . loadValuesAsynchronously ( forKeys : [ " duration " ] ) {
396
- DispatchQueue . main . async {
397
- if let duration = self . player ? . currentItem ? . duration . seconds,
398
- !duration . isNaN {
399
- self . durationString = self . formatTime ( duration)
444
+ Task {
445
+ if let duration = try ? await player ? . currentItem ? . asset . load ( . duration ) as? CMTime ,
446
+ ! duration. seconds. isNaN {
447
+ await MainActor . run {
448
+ self . durationString = self . formatTime ( duration. seconds )
400
449
}
401
450
}
402
451
}
403
452
}
404
453
454
+ private func handlePlaybackCompletion( ) {
455
+ isPlaying = false
456
+ // Reset to beginning
457
+ seek ( to: 0 )
458
+ }
459
+
405
460
func togglePlayback( ) {
406
461
if isPlaying {
407
462
player? . pause ( )
463
+ isPlaying = false
408
464
} else {
465
+ // If we're at the end, seek to beginning before playing
466
+ if let currentTime = player? . currentTime ( ) . seconds,
467
+ let duration = player? . currentItem? . duration. seconds,
468
+ currentTime >= duration {
469
+ seek ( to: 0 )
470
+ }
409
471
player? . play ( )
472
+ isPlaying = true
410
473
}
411
- isPlaying. toggle ( )
412
474
}
413
475
414
476
func seek( to progress: Double ) {
@@ -431,6 +493,7 @@ class AudioPlayerViewModel: ObservableObject {
431
493
if let timeObserver = timeObserver {
432
494
player? . removeTimeObserver ( timeObserver)
433
495
}
496
+ NotificationCenter . default. removeObserver ( self )
434
497
player? . pause ( )
435
498
player = nil
436
499
}
0 commit comments