diff --git a/android/src/main/java/expo/modules/audio/AudioRecords.kt b/android/src/main/java/expo/modules/audio/AudioRecords.kt
index 86880d92fd5b6413e8596e1b2b8ae28f2981addd..9ad0d668b440c5ef7b6f3c84e1cd8a81a1c80481 100644
--- a/android/src/main/java/expo/modules/audio/AudioRecords.kt
+++ b/android/src/main/java/expo/modules/audio/AudioRecords.kt
@@ -110,7 +110,9 @@ enum class AndroidAudioEncoder(val value: String) : Enumerable {
 class AudioLockScreenOptions(
   @Field val showSeekForward: Boolean,
   @Field val showSeekBackward: Boolean,
-  @Field val isLiveStream: Boolean? = null
+  @Field val isLiveStream: Boolean? = null,
+  @Field val showNextTrack: Boolean = false,
+  @Field val showPreviousTrack: Boolean = false
 ) : Record
 
 enum class InterruptionMode(val value: String) : Enumerable {
diff --git a/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt b/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt
index 196a5d57f887c2667a88543bd2f6a80044a044f2..9b8338b9cf727361e87c01374183633fb60cac38 100644
--- a/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt
+++ b/android/src/main/java/expo/modules/audio/service/AudioControlsService.kt
@@ -90,6 +90,8 @@ class AudioControlsService : MediaSessionService() {
 
         ACTION_SEEK_FORWARD -> currentPlayerRef.seekTo(currentPlayerRef.currentPosition + SEEK_INTERVAL_MS)
         ACTION_SEEK_BACKWARD -> currentPlayerRef.seekTo(currentPlayerRef.currentPosition - SEEK_INTERVAL_MS)
+        ACTION_NEXT_TRACK -> emitRemoteTrackEvent(ACTION_NEXT_TRACK)
+        ACTION_PREVIOUS_TRACK -> emitRemoteTrackEvent(ACTION_PREVIOUS_TRACK)
       }
     }
 
@@ -192,7 +194,17 @@ class AudioControlsService : MediaSessionService() {
       val compactViewIndices = mutableListOf<Int>()
       var currentIndex = 0
 
-      if (currentOptions?.showSeekBackward == true) {
+      if (currentOptions?.showPreviousTrack == true) {
+        builder.addAction(
+          NotificationCompat.Action(
+            androidx.media3.session.R.drawable.media3_icon_previous,
+            "Previous Track",
+            buildActionPendingIntent(ACTION_PREVIOUS_TRACK)
+          )
+        )
+        compactViewIndices.add(currentIndex)
+        currentIndex++
+      } else if (currentOptions?.showSeekBackward == true) {
         builder.addAction(
           NotificationCompat.Action(
             androidx.media3.session.R.drawable.media3_icon_skip_back,
@@ -218,7 +230,16 @@ class AudioControlsService : MediaSessionService() {
       compactViewIndices.add(currentIndex)
       currentIndex++
 
-      if (currentOptions?.showSeekForward == true) {
+      if (currentOptions?.showNextTrack == true) {
+        builder.addAction(
+          NotificationCompat.Action(
+            androidx.media3.session.R.drawable.media3_icon_next,
+            "Next Track",
+            buildActionPendingIntent(ACTION_NEXT_TRACK)
+          )
+        )
+        compactViewIndices.add(currentIndex)
+      } else if (currentOptions?.showSeekForward == true) {
         builder.addAction(
           NotificationCompat.Action(
             androidx.media3.session.R.drawable.media3_icon_skip_forward,
@@ -240,8 +261,18 @@ class AudioControlsService : MediaSessionService() {
     val session = mediaSession ?: return
     val mediaButtons = mutableListOf<CommandButton>()
 
-    // Add seek backward button if enabled
-    if (currentOptions?.showSeekBackward == true) {
+    // Previous track takes priority over seek-backward for the same slot —
+    // showing both doesn't make sense in one button.
+    if (currentOptions?.showPreviousTrack == true) {
+      mediaButtons.add(
+        CommandButton.Builder(CommandButton.ICON_PREVIOUS)
+          .setDisplayName("Previous Track")
+          .setEnabled(true)
+          .setSessionCommand(SessionCommand(ACTION_PREVIOUS_TRACK, Bundle.EMPTY))
+          .setSlots(CommandButton.SLOT_BACK)
+          .build()
+      )
+    } else if (currentOptions?.showSeekBackward == true) {
       mediaButtons.add(
         CommandButton.Builder(CommandButton.ICON_SKIP_BACK_10)
           .setDisplayName("Seek Backward")
@@ -262,8 +293,17 @@ class AudioControlsService : MediaSessionService() {
         .build()
     )
 
-    // Add seek forward button if enabled
-    if (currentOptions?.showSeekForward == true) {
+    // Next track takes priority over seek-forward for the same slot.
+    if (currentOptions?.showNextTrack == true) {
+      mediaButtons.add(
+        CommandButton.Builder(CommandButton.ICON_NEXT)
+          .setDisplayName("Next Track")
+          .setEnabled(true)
+          .setSessionCommand(SessionCommand(ACTION_NEXT_TRACK, Bundle.EMPTY))
+          .setSlots(CommandButton.SLOT_FORWARD)
+          .build()
+      )
+    } else if (currentOptions?.showSeekForward == true) {
       mediaButtons.add(
         CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_10)
           .setDisplayName("Seek Forward")
@@ -371,7 +411,7 @@ class AudioControlsService : MediaSessionService() {
           updateMetadata(metadata)
         }
         val session = MediaSession.Builder(context, sessionPlayer)
-          .setCallback(AudioMediaSessionCallback())
+          .setCallback(AudioMediaSessionCallback { action -> emitRemoteTrackEvent(action) })
           .build()
 
         // Replace the basic media session with a session connected to our playback service.
@@ -452,7 +492,7 @@ class AudioControlsService : MediaSessionService() {
           updateMetadata(metadata)
         }
         val session = MediaSession.Builder(context, sessionPlayer)
-          .setCallback(AudioMediaSessionCallback())
+          .setCallback(AudioMediaSessionCallback { action -> emitRemoteTrackEvent(action) })
           .build()
 
         player.mediaSession.release()
@@ -504,6 +544,20 @@ class AudioControlsService : MediaSessionService() {
     }
   }
 
+  // expo-audio's AudioPlayer has no native concept of a playlist (queue
+  // management lives entirely in this app's JS), so next/previous-track
+  // lock-screen/notification buttons are wired as a plain event bridge back
+  // to JS rather than an actual native skip — the JS side owns the queue and
+  // decides what "next"/"previous" means.
+  private fun emitRemoteTrackEvent(action: String) {
+    val eventName = when (action) {
+      ACTION_NEXT_TRACK -> "onRemoteNextTrack"
+      ACTION_PREVIOUS_TRACK -> "onRemotePreviousTrack"
+      else -> return
+    }
+    currentPlayer?.emit(eventName, emptyMap<String, Any>())
+  }
+
   private fun hideNotification() {
     val notificationManager: NotificationManager =
       getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -558,6 +612,8 @@ class AudioControlsService : MediaSessionService() {
 
     const val ACTION_SEEK_FORWARD = "expo.modules.audio.action.SEEK_FORWARD"
     const val ACTION_SEEK_BACKWARD = "expo.modules.audio.action.SEEK_BACKWARD"
+    const val ACTION_NEXT_TRACK = "expo.modules.audio.action.NEXT_TRACK"
+    const val ACTION_PREVIOUS_TRACK = "expo.modules.audio.action.PREVIOUS_TRACK"
 
     const val SEEK_INTERVAL_MS = 10000L
   }
diff --git a/android/src/main/java/expo/modules/audio/service/AudioMediaSessionCallback.kt b/android/src/main/java/expo/modules/audio/service/AudioMediaSessionCallback.kt
index 839aaef12d7699ea1a89037b31849775daa079f4..9aac7d3a9a34d99ee455941610b0d292771a179c 100644
--- a/android/src/main/java/expo/modules/audio/service/AudioMediaSessionCallback.kt
+++ b/android/src/main/java/expo/modules/audio/service/AudioMediaSessionCallback.kt
@@ -10,7 +10,9 @@ import androidx.media3.common.util.UnstableApi
 import com.google.common.util.concurrent.ListenableFuture
 
 @OptIn(UnstableApi::class)
-class AudioMediaSessionCallback : MediaSession.Callback {
+class AudioMediaSessionCallback(
+  private val onCustomAction: ((String) -> Unit)? = null
+) : MediaSession.Callback {
   override fun onConnect(
     session: MediaSession,
     controller: MediaSession.ControllerInfo
@@ -35,6 +37,8 @@ class AudioMediaSessionCallback : MediaSession.Callback {
           MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
             .add(SessionCommand(AudioControlsService.ACTION_SEEK_BACKWARD, Bundle.EMPTY))
             .add(SessionCommand(AudioControlsService.ACTION_SEEK_FORWARD, Bundle.EMPTY))
+            .add(SessionCommand(AudioControlsService.ACTION_NEXT_TRACK, Bundle.EMPTY))
+            .add(SessionCommand(AudioControlsService.ACTION_PREVIOUS_TRACK, Bundle.EMPTY))
             .build()
         )
         .build()
@@ -56,6 +60,10 @@ class AudioMediaSessionCallback : MediaSession.Callback {
       AudioControlsService.ACTION_SEEK_BACKWARD -> {
         session.player.seekTo(session.player.currentPosition - AudioControlsService.SEEK_INTERVAL_MS)
       }
+      AudioControlsService.ACTION_NEXT_TRACK,
+      AudioControlsService.ACTION_PREVIOUS_TRACK -> {
+        onCustomAction?.invoke(command.customAction)
+      }
     }
     return super.onCustomCommand(session, controller, command, args)
   }
diff --git a/build/AudioConstants.d.ts b/build/AudioConstants.d.ts
index cfd92ad0ff5765254c4e6db974fe44bce34aad06..30b42be239b073583fc83be82e71a79d8cfd3085 100644
--- a/build/AudioConstants.d.ts
+++ b/build/AudioConstants.d.ts
@@ -15,5 +15,15 @@ export type AudioLockScreenOptions = {
      * and scrub bar, and disable seek controls.
      */
     isLiveStream?: boolean;
+    /**
+     * Whether the next-track button should be displayed on the lock screen. The app is
+     * responsible for handling the `onRemoteNextTrack` event on the active player.
+     */
+    showNextTrack?: boolean;
+    /**
+     * Whether the previous-track button should be displayed on the lock screen. The app is
+     * responsible for handling the `onRemotePreviousTrack` event on the active player.
+     */
+    showPreviousTrack?: boolean;
 };
 //# sourceMappingURL=AudioConstants.d.ts.map
\ No newline at end of file
diff --git a/build/AudioModule.types.d.ts b/build/AudioModule.types.d.ts
index f3ae4ee4044d86032cd5c6dbcfee6f905d1ce99f..c2fe721d154898e006e141bcbe47bec392a9b81f 100644
--- a/build/AudioModule.types.d.ts
+++ b/build/AudioModule.types.d.ts
@@ -222,6 +222,10 @@ export type AudioEvents = {
     playbackStatusUpdate(status: AudioStatus): void;
     /** Fired when audio sampling is enabled and new sample data is available. */
     audioSampleUpdate(data: AudioSample): void;
+    /** Fired when the lock screen/notification next-track button is pressed (requires `showNextTrack: true`). */
+    onRemoteNextTrack(): void;
+    /** Fired when the lock screen/notification previous-track button is pressed (requires `showPreviousTrack: true`). */
+    onRemotePreviousTrack(): void;
 };
 export declare class AudioRecorder extends SharedObject<RecordingEvents> {
     /**
diff --git a/ios/AudioRecords.swift b/ios/AudioRecords.swift
index 33db27c3b8fb40fa9db6659a0787c6b2b503d1ab..c10aeeac010fece05e79553dcab2c673a956cb2a 100644
--- a/ios/AudioRecords.swift
+++ b/ios/AudioRecords.swift
@@ -70,6 +70,8 @@ struct LockScreenOptions: Record {
   @Field var showSeekForward: Bool = false
   @Field var showSeekBackward: Bool = false
   @Field var isLiveStream: Bool? = false
+  @Field var showNextTrack: Bool = false
+  @Field var showPreviousTrack: Bool = false
 }
 
 enum BitRateStrategy: String, Enumerable {
diff --git a/ios/MediaController.swift b/ios/MediaController.swift
index e977f577cd2e16b194b56537ad76b423f40d2496..37c79179de48599c2775ee89e6c1adf210226779 100644
--- a/ios/MediaController.swift
+++ b/ios/MediaController.swift
@@ -303,12 +303,35 @@ class MediaController {
       return .success
     }
 
+    // expo-audio's AudioPlayer has no native concept of a playlist (queue
+    // management lives entirely in this app's JS), so next/previous-track
+    // remote commands are wired as a plain event bridge back to JS rather
+    // than an actual native skip — the JS side owns the queue and decides
+    // what "next"/"previous" means.
+    remoteCommandCenter.nextTrackCommand.addTarget { [weak self] _ in
+      guard let player = self?.activePlayer else {
+        return .commandFailed
+      }
+      player.emit(event: "onRemoteNextTrack", payload: [:])
+      return .success
+    }
+
+    remoteCommandCenter.previousTrackCommand.addTarget { [weak self] _ in
+      guard let player = self?.activePlayer else {
+        return .commandFailed
+      }
+      player.emit(event: "onRemotePreviousTrack", payload: [:])
+      return .success
+    }
+
     remoteCommandCenter.playCommand.isEnabled = true
     remoteCommandCenter.pauseCommand.isEnabled = true
     remoteCommandCenter.togglePlayPauseCommand.isEnabled = true
     remoteCommandCenter.changePlaybackPositionCommand.isEnabled = !isLiveStream
     remoteCommandCenter.skipForwardCommand.isEnabled = options?.showSeekForward ?? false
     remoteCommandCenter.skipBackwardCommand.isEnabled = options?.showSeekBackward ?? false
+    remoteCommandCenter.nextTrackCommand.isEnabled = options?.showNextTrack ?? false
+    remoteCommandCenter.previousTrackCommand.isEnabled = options?.showPreviousTrack ?? false
   }
 
   private func disableRemoteCommands() {
@@ -318,6 +341,8 @@ class MediaController {
     remoteCommandCenter.changePlaybackPositionCommand.isEnabled = false
     remoteCommandCenter.skipForwardCommand.isEnabled = false
     remoteCommandCenter.skipBackwardCommand.isEnabled = false
+    remoteCommandCenter.nextTrackCommand.isEnabled = false
+    remoteCommandCenter.previousTrackCommand.isEnabled = false
 
     // Remove event targets
     remoteCommandCenter.playCommand.removeTarget(self)
@@ -326,5 +351,7 @@ class MediaController {
     remoteCommandCenter.changePlaybackPositionCommand.removeTarget(self)
     remoteCommandCenter.skipForwardCommand.removeTarget(self)
     remoteCommandCenter.skipBackwardCommand.removeTarget(self)
+    remoteCommandCenter.nextTrackCommand.removeTarget(self)
+    remoteCommandCenter.previousTrackCommand.removeTarget(self)
   }
 }
diff --git a/src/AudioConstants.ts b/src/AudioConstants.ts
index dcc0452f7cae66b1bfaf77ed211416b092965bd9..f4a3fdb4d9c75b175d8550296c55c1eaecf99375 100644
--- a/src/AudioConstants.ts
+++ b/src/AudioConstants.ts
@@ -16,4 +16,14 @@ export type AudioLockScreenOptions = {
    * and scrub bar, and disable seek controls.
    */
   isLiveStream?: boolean;
+  /**
+   * Whether the next-track button should be displayed on the lock screen. The app is
+   * responsible for handling the `onRemoteNextTrack` event on the active player.
+   */
+  showNextTrack?: boolean;
+  /**
+   * Whether the previous-track button should be displayed on the lock screen. The app is
+   * responsible for handling the `onRemotePreviousTrack` event on the active player.
+   */
+  showPreviousTrack?: boolean;
 };
diff --git a/src/AudioModule.types.ts b/src/AudioModule.types.ts
index 7e7537173d067fecef767a03dc0f1860c8aca7ac..4eb714009bce82793a3e3f86972a610eebb1fb90 100644
--- a/src/AudioModule.types.ts
+++ b/src/AudioModule.types.ts
@@ -280,6 +280,10 @@ export type AudioEvents = {
   playbackStatusUpdate(status: AudioStatus): void;
   /** Fired when audio sampling is enabled and new sample data is available. */
   audioSampleUpdate(data: AudioSample): void;
+  /** Fired when the lock screen/notification next-track button is pressed (requires `showNextTrack: true`). */
+  onRemoteNextTrack(): void;
+  /** Fired when the lock screen/notification previous-track button is pressed (requires `showPreviousTrack: true`). */
+  onRemotePreviousTrack(): void;
 };
 
 export declare class AudioRecorder extends SharedObject<RecordingEvents> {
