'Expo av playbackObject's onPlaybackStatusUpdate is called only on play/stop instead of interval by progressUpdateIntervalMillis

onPlaybackStatusUpdate should return the output after every 100ms but only returns on play or stop. am I missing something in my code. (I am new to Expo and Expo AV). Please see code below used for loading and playing audio file

if(!sound._loaded)
    await sound.loadAsync(
      require('./assets/Audio.mp3'),
      initialStatus,
    );
    
    sound.setStatusAsync({progressUpdateIntervalMillis: 200});
    sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);

    await sound.playAsync();
    console.log(sound);
  }

  const initialStatus = (status) => {
    console.log("initialStatus");
    console.log(status);
  }

  let onPlaybackStatusUpdate = async (playbackStatus) => {
    //if(!isNaN(playbackStatus.durationMillis))
    //setTotalLength(playbackStatus.durationMillis/1000);
    //setCurrentPos(playbackStatus.positionMillis.toString().split(".")[0]/1000);
    //setIsPlaying(playbackStatus.isPlaying);
    console.log("onPlaybackStatusUpdate");
    console.log(playbackStatus);
  }


Solution 1:[1]

For sound instances create a ref variable and use that to trigger pause/play and also Load.

Working Implementation Here

Like this,

const sound = React.useRef(new Audio.Sound());

For Loading an audio,

const result = await sound.current.loadAsync(require('./test.mp3'), {}, true);
if (result.isLoaded === false) {
  console.log('Error in Loading Audio');
} else {
  // Audio is loaded do whatever needed
}

For pause

const result = await sound.current.getStatusAsync();
if (result.isLoaded) {
  if (result.isPlaying === true) {
    // sound.current.pauseAsync();
    // Pause Here
  }
}

For play,

const result = await sound.current.getStatusAsync();
if (result.isLoaded) {
  if (result.isPlaying === false) {
    // sound.current.playAsync();
    // play Here
  }
}

Solution 2:[2]

Found a fix for web.

The issue lies with the event emitter between react-native and react-native-web.

It's a small change between two files but I made a new .web.js file for Sound.js since ios and android were fine. The main difference is in

this._key.ontimeupdate

You'll have to patch expo-av.

I'm stil on sdk 42 so here's my patch for version 9.2.3 of expo-av that worked as expected.

diff --git a/node_modules/expo-av/build/Audio/Sound.web.js b/node_modules/expo-av/build/Audio/Sound.web.js
new file mode 100644
index 0000000..28f4460
--- /dev/null
+++ b/node_modules/expo-av/build/Audio/Sound.web.js
@@ -0,0 +1,162 @@
+import { EventEmitter } from '@unimodules/core';
+import { PlaybackMixin, assertStatusValuesInBounds, getNativeSourceAndFullInitialStatusForLoadAsync, getUnloadedStatus, } from '../AV';
+import ExponentAV from '../ExponentAV';
+import { throwIfAudioIsDisabled } from './AudioAvailability';
+export class Sound {
+    constructor() {
+        this._loaded = false;
+        this._loading = false;
+        this._key = null;
+        this._lastStatusUpdate = null;
+        this._lastStatusUpdateTime = null;
+        this._subscriptions = [];
+        this._eventEmitter = new EventEmitter(ExponentAV);
+        this._coalesceStatusUpdatesInMillis = 100;
+        this._onPlaybackStatusUpdate = null;
+        this._internalStatusUpdateCallback = ({ key, status, }) => {
+            if (this._key === key) {
+                this._callOnPlaybackStatusUpdateForNewStatus(status);
+            }
+        };
+        this._internalErrorCallback = ({ key, error }) => {
+            if (this._key === key) {
+                this._errorCallback(error);
+            }
+        };
+        this._errorCallback = (error) => {
+            this._clearSubscriptions();
+            this._loaded = false;
+            this._key = null;
+            this._callOnPlaybackStatusUpdateForNewStatus(getUnloadedStatus(error));
+        };
+        // ### Unified playback API ### (consistent with Video.js)
+        // All calls automatically call onPlaybackStatusUpdate as a side effect.
+        // Get status API
+        this.getStatusAsync = async () => {
+            if (this._loaded) {
+                return this._performOperationAndHandleStatusAsync(() => ExponentAV.getStatusForSound(this._key));
+            }
+            const status = getUnloadedStatus();
+            this._callOnPlaybackStatusUpdateForNewStatus(status);
+            return status;
+        };
+    }
+    // Internal methods
+    _callOnPlaybackStatusUpdateForNewStatus(status) {
+        const shouldDismissBasedOnCoalescing = this._lastStatusUpdateTime &&
+            JSON.stringify(status) === this._lastStatusUpdate &&
+            Date.now() - this._lastStatusUpdateTime.getTime() < this._coalesceStatusUpdatesInMillis;
+        if (this._onPlaybackStatusUpdate != null && !shouldDismissBasedOnCoalescing) {
+            this._onPlaybackStatusUpdate(status);
+            this._lastStatusUpdateTime = new Date();
+            this._lastStatusUpdate = JSON.stringify(status);
+        }
+    }
+    async _performOperationAndHandleStatusAsync(operation) {
+        throwIfAudioIsDisabled();
+        if (this._loaded) {
+            const status = await operation();
+            this._callOnPlaybackStatusUpdateForNewStatus(status);
+            return status;
+        }
+        else {
+            throw new Error('Cannot complete operation because sound is not loaded.');
+        }
+    }
+    // TODO: We can optimize by only using time observer on native if (this._onPlaybackStatusUpdate).
+    _subscribeToNativeEvents() {
+        if (this._loaded) {
+            this._subscriptions.push(this._eventEmitter.addListener('didUpdatePlaybackStatus', this._internalStatusUpdateCallback));
+            this._subscriptions.push(this._eventEmitter.addListener('ExponentAV.onError', this._internalErrorCallback));
+        }
+    }
+    _clearSubscriptions() {
+        this._subscriptions.forEach(e => e.remove());
+        this._subscriptions = [];
+    }
+    setOnPlaybackStatusUpdate(onPlaybackStatusUpdate) {
+        this._onPlaybackStatusUpdate = onPlaybackStatusUpdate;
+        this.getStatusAsync();
+    }
+    // Loading / unloading API
+    async loadAsync(source, initialStatus = {}, downloadFirst = true) {
+        throwIfAudioIsDisabled();
+        if (this._loading) {
+            throw new Error('The Sound is already loading.');
+        }
+        if (!this._loaded) {
+            this._loading = true;
+            const { nativeSource, fullInitialStatus, } = await getNativeSourceAndFullInitialStatusForLoadAsync(source, initialStatus, downloadFirst);
+            // This is a workaround, since using load with resolve / reject seems to not work.
+            return new Promise((resolve, reject) => {
+                const loadSuccess = (result) => {
+                    const [key, status] = result;
+                    this._key = key;
+                    this._loaded = true;
+                    this._loading = false;
+                    this._subscribeToNativeEvents();
+                    this._key.ontimeupdate = () => {
+                        this._eventEmitter.emit('didUpdatePlaybackStatus', {
+                            key,
+                            status: ExponentAV.getStatus(this._key),
+                        });
+                    };          
+                    this._callOnPlaybackStatusUpdateForNewStatus(status);
+                    resolve(status);
+                };
+                const loadError = (error) => {
+                    this._loading = false;
+                    reject(error);
+                };
+                ExponentAV.loadForSound(nativeSource, fullInitialStatus)
+                    .then(loadSuccess)
+                    .catch(loadError);
+            });
+        }
+        else {
+            throw new Error('The Sound is already loaded.');
+        }
+    }
+    async unloadAsync() {
+        if (this._loaded) {
+            this._loaded = false;
+            const key = this._key;
+            this._key = null;
+            const status = await ExponentAV.unloadForSound(key);
+            this._callOnPlaybackStatusUpdateForNewStatus(status);
+            this._clearSubscriptions();
+            return status;
+        }
+        else {
+            return this.getStatusAsync(); // Automatically calls onPlaybackStatusUpdate.
+        }
+    }
+    // Set status API (only available while isLoaded = true)
+    async setStatusAsync(status) {
+        assertStatusValuesInBounds(status);
+        return this._performOperationAndHandleStatusAsync(() => ExponentAV.setStatusForSound(this._key, status));
+    }
+    async replayAsync(status = {}) {
+        if (status.positionMillis && status.positionMillis !== 0) {
+            throw new Error('Requested position after replay has to be 0.');
+        }
+        return this._performOperationAndHandleStatusAsync(() => ExponentAV.replaySound(this._key, {
+            ...status,
+            positionMillis: 0,
+            shouldPlay: true,
+        }));
+    }
+}
+/** @deprecated Use `Sound.createAsync()` instead */
+Sound.create = async (source, initialStatus = {}, onPlaybackStatusUpdate = null, downloadFirst = true) => {
+    console.warn(`Sound.create is deprecated in favor of Sound.createAsync with the same API except for the new method name`);
+    return Sound.createAsync(source, initialStatus, onPlaybackStatusUpdate, downloadFirst);
+};
+Sound.createAsync = async (source, initialStatus = {}, onPlaybackStatusUpdate = null, downloadFirst = true) => {
+    const sound = new Sound();
+    sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
+    const status = await sound.loadAsync(source, initialStatus, downloadFirst);
+    return { sound, status };
+};
+Object.assign(Sound.prototype, PlaybackMixin);
+//# sourceMappingURL=Sound.js.map
\ No newline at end of file
diff --git a/node_modules/expo-av/build/ExponentAV.web.js b/node_modules/expo-av/build/ExponentAV.web.js
index 4aeec83..591510e 100644
--- a/node_modules/expo-av/build/ExponentAV.web.js
+++ b/node_modules/expo-av/build/ExponentAV.web.js
@@ -75,6 +75,9 @@ export default {
     get name() {
         return 'ExponentAV';
     },
+    getStatus(element) {
+        return getStatusFromMedia(element);
+    },
     async getStatusForVideo(element) {
         return getStatusFromMedia(element);
     },

Would've submitted a PR to expo but I'm still on version 42 and 45 is in beta. I looked around version 45 and major changes have happened since v43 even with the EvenEmitter logic I dug into to solve this.

Hope this helps someone out!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Kartikey
Solution 2