Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

playbackState.state !== State.Ended not triggering on audio completion, causing repeat playback #2432

Open
MalikAamirDev opened this issue Feb 11, 2025 · 4 comments
Labels

Comments

@MalikAamirDev
Copy link

MalikAamirDev commented Feb 11, 2025

Describe the Bug
I am encountering an issue where playbackState.state === State.Ended is not reliably triggered when an audio file finishes playing. Instead of reaching Ended, the playback state sometimes gets stuck in Loading or Buffering, and the same audio repeats unexpectedly.

This issue occurs inconsistently and seems to affect both real devices and emulators.

Steps To Reproduce

  1. Setup Location-Based Playback

    • Implement react-native-track-player to play audio based on the user’s location.
    • Use a queue of audio tracks that play when the user reaches specific GPS coordinates.
  2. Trigger Audio Playback by Changing Location

    • When the user arrives at a predefined location, start playing the assigned audio track.
    • If a video is playing, queue the next audio track instead of playing it immediately.
  3. Monitor Playback State

    • Use usePlaybackState() to track changes in playback state.
    • Log the state changes (Loading, Playing, Paused, Buffering, Ended, etc.).
  4. Issue Observed:

    • Sometimes, when a track finishes playing, playbackState.state does not transition to State.Ended.
    • Instead, it gets stuck in Buffering or Loading, causing the same track to replay instead of moving to the next track.
    • This behavior occurs inconsistently but affects both real devices and emulators.

Code To Reproduce

import { useEffect, useRef, useState } from 'react';
import TrackPlayer, { Event, State, usePlaybackState, useProgress } from 'react-native-track-player';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getTranslationFromObject } from '../utils/translations';

function useTourPlayer() {
  const [pointQueue, setPointQueue] = useState([]);
  const [currentPoint, setCurrentPoint] = useState(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const pointsRef = useRef(null);
  const playbackState = usePlaybackState();
  const { position, duration } = useProgress();

  async function enqueue(points) {
    const queue = points.filter(
      (point) => point.preloadedAudio && !pointQueue.find((item) => item.id === point.id)
    );
    if (queue.length) {
      setPointQueue((prev) => [...prev, ...queue]);
    }
  }

  async function play(point) {
    try {
      await TrackPlayer.reset();
      if (point && point.preloadedAudio) {
        const track = {
          id: point.id,
          url: getTranslationFromObject(point.audioFile.URI),
          title: getTranslationFromObject(point.title),
          artist: getTranslationFromObject(point.mainTitle || point.title),
          artwork: point.tourImage,
        };

        await point.preloadedAudio.add(track);
        setIsPlaying(true);
        setCurrentPoint(point);
        pointsRef.current = point;
        await point.preloadedAudio.play();
        await AsyncStorage.setItem(point.tourId, point.id);
      }
    } catch (error) {
      console.error('Error playing audio:', error);
    }
  }

  useEffect(() => {
    if (!isPlaying && pointQueue.length) {
      play(pointQueue[0]).then(() => setPointQueue((prev) => prev.slice(1)));
    }
  }, [pointQueue]);

  useEffect(() => {
    async function handlePlayBackState() {
      if (!currentPoint || !currentPoint.preloadedAudio || duration === 0) return;
      const progress = await TrackPlayer.getProgress();
      const isEndOfPlayback =
        progress.duration > 60 ? progress.position >= progress.duration - 1.5 : progress.position >= progress.duration - 0.5;

      if ((playbackState.state === State.Ended || isEndOfPlayback) && currentPoint) {
        setIsPlaying(false);
        currentPoint?.preloadedAudio?.stop();
        setCurrentPoint(null);
      }
    }
    handlePlayBackState();
  }, [playbackState, currentPoint, position]);

  return { play, enqueue, isPlaying, currentPoint };
}

export default useTourPlayer;

Expected Behavior:
• State should transition to Ended when audio finishes.
• Should not get stuck in Buffering or Loading after finishing a track.

Actual Behavior:
• The track sometimes repeats instead of stopping.
• The state gets stuck in Buffering or Loading instead of Ended.

Replicable on Example App?
No

Environment Info:
System:
OS: macOS 15.3
CPU: (8) arm64 Apple M2
Memory: 143.38 MB / 8.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 23.2.0
path: /opt/homebrew/bin/node
Yarn:
version: 1.22.22
path: /opt/homebrew/bin/yarn
npm:
version: 10.9.0
path: /opt/homebrew/bin/npm
Watchman:
version: 2024.11.11.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 24.2
- iOS 18.2
- macOS 15.2
- tvOS 18.2
- visionOS 2.2
- watchOS 11.2
Android SDK: Not Found
IDEs:
Android Studio: 2024.2 AI-242.23726.103.2422.12816248
Xcode:
version: 16.2/16C5032a
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.13
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.72.10
wanted: 0.72.10
react-native-macos: Not Found
npmGlobalPackages:
"react-native": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false

react-native-track-player: 4.1.1
Both Android and iOS
Both Real and Simulators/Emulators
Android: 14
ios: 18.1.1

@alexd-femmi
Copy link

Having the same issue - anyone found any workarounds yet?

@lovegaoshi
Copy link
Contributor

i cant reproduce. if u cant reproduce using (a modified ver of) the example app, most likely people dont know what u did or even what ur talking about. ur best bet is to reproduce with the example app, bc ppl know how to compile and run that. otherwise gl.

@MalikAamirDev
Copy link
Author

i cant reproduce. if u cant reproduce using (a modified ver of) the example app, most likely people dont know what u did or even what ur talking about. ur best bet is to reproduce with the example app, bc ppl know how to compile and run that. otherwise gl.

please check the Code To Reproduce section now I have updated the code with what I'm using

@MalikAamirDev
Copy link
Author

Having the same issue - anyone found any workarounds yet?

You can respond with this message in the GitHub issue section:


Hey @alexd-femmi, I ran into the same issue and found a workaround. I added a 2-second silent buffer at the end of each audio file and then manually checked the playback progress to stop the player when it reaches the expected end. Here's the approach:

const progress = await TrackPlayer.getProgress();
const isEndOfPlayback =
  progress.duration > 60 ? progress.position >= progress.duration - 1.5 : progress.position >= progress.duration - 0.5;

if ((playbackState.state === State.Ended || isEndOfPlayback) && currentPoint) {
  setIsPlaying(false);
  currentPoint?.preloadedAudio?.stop();
  setCurrentPoint(null);
}

It’s not a perfect solution, but it ensures the player stops correctly without cutting off the last part of the audio. Hope this helps! Let me know if you find a better fix. 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants