Skip to content

Commit 4cbf608

Browse files
[MOO-2383] replace react-native-track-player with react-native-sound (#538)
2 parents 02107f9 + 882dc3d commit 4cbf608

9 files changed

Lines changed: 100 additions & 83 deletions

File tree

.github/workflows/Build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ on:
44
push:
55
branches:
66
- main
7-
- 'mx/release/**'
7+
- 'mx/**'
88
pull_request:
99
branches:
1010
- main
11-
- 'mx/release/**'
11+
- 'mx/**'
1212
jobs:
1313
test:
1414
name: "Build (${{ matrix.os }})"

.github/workflows/NativePipeline.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ on:
8585
pull_request:
8686
branches:
8787
- main
88+
- 'mx/**'
8889

8990
# Surface capture mode in the run title (workflow_dispatch inputs aren't shown in the UI),
9091
# otherwise fall back to the provided run_name / default. inputs.update_baselines is the

configs/e2e/native_dependencies.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
"react-native-image-picker": "7.2.3",
1818
"react-native-permissions": "5.5.1",
1919
"react-native-webview": "13.16.1",
20-
"@sbaiahmed1/react-native-biometrics": "0.15.0"
20+
"@sbaiahmed1/react-native-biometrics": "0.15.0",
21+
"react-native-sound": "0.13.0"
2122
}

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@
105105
"@mendix/pluggable-widgets-tools@11.8.0": "patches/@mendix+pluggable-widgets-tools+11.8.0.patch",
106106
"react-native-gesture-handler@2.31.2": "patches/react-native-gesture-handler+2.31.2.patch",
107107
"react-native-slider@0.11.0": "patches/react-native-slider+0.11.0.patch",
108-
"react-native-snap-carousel@3.9.1": "patches/react-native-snap-carousel+3.9.1.patch",
109-
"react-native-track-player@4.1.2": "patches/react-native-track-player@4.1.2.patch"
108+
"react-native-snap-carousel@3.9.1": "patches/react-native-snap-carousel+3.9.1.patch"
110109
}
111110
},
112111
"packageManager": "pnpm@10.32.0+sha512.9b2634bb3fed5601c33633f2d92593f506270a3963b8c51d2b2d6a828da615ce4e9deebef9614ccebbc13ac8d3c0f9c9ccceb583c69c8578436fa477dbb20d70"

packages/jsActions/mobile-resources-native/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
- We switched to a new sound library for the Play sound action to support react-native 0.84+.
10+
- The Play sound action now plays audio files from online (network) documents on Android by downloading them to a version-based cache before playback.
911
- We have fixed the biometric authentication issue where it was not working on Android and crashing on iOS.
1012

1113
## [12.1.0] Native Mobile Resources - 2026-6-10

packages/jsActions/mobile-resources-native/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"react-native-image-picker": "7.2.3",
4242
"react-native-localize": "3.7.0",
4343
"react-native-permissions": "5.5.1",
44-
"react-native-track-player": "4.1.2",
44+
"react-native-sound": "0.13.0",
4545
"url-parse": "^1.4.7"
4646
},
4747
"devDependencies": {

packages/jsActions/mobile-resources-native/src/platform/PlaySound.ts

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,68 @@
55
// - the code between BEGIN USER CODE and END USER CODE
66
// - the code between BEGIN EXTRA CODE and END EXTRA CODE
77
// Other code you write will be lost the next time you deploy the project.
8-
import TrackPlayer, { State, Event } from "react-native-track-player";
8+
import Sound from "react-native-sound";
9+
import RNBlobUtil from "react-native-blob-util";
10+
import { Platform } from "react-native";
911

1012
// BEGIN EXTRA CODE
13+
// Audio downloaded for online (network) documents is cached here. CacheDir is used
14+
// on purpose: the OS is allowed to reclaim it under storage pressure, so we do not
15+
// have to own the cleanup lifecycle ourselves.
16+
const SOUND_CACHE_DIR = `${RNBlobUtil.fs.dirs.CacheDir}/mx-play-sound`;
17+
18+
function getFileExtension(fileName?: string): string {
19+
if (!fileName) {
20+
return "";
21+
}
22+
const dotIndex = fileName.lastIndexOf(".");
23+
return dotIndex >= 0 ? fileName.slice(dotIndex) : "";
24+
}
25+
26+
// Resolves a path the Android media player can actually play.
27+
//
28+
// For online documents the URL points at the runtime and requires the Mendix session
29+
// cookie. The Android media player does not forward that cookie, so playback fails.
30+
// react-native-blob-util shares the platform cookie jar, so we download the file with
31+
// it and hand the local copy to the player instead.
32+
//
33+
// The download is cached by content version (guid + changedDate):
34+
// - the same file is never downloaded twice;
35+
// - when the source changes, changedDate changes, so a fresh copy is fetched even
36+
// though the file name stayed the same;
37+
// - stale versions of the same document are removed before fetching a new one, so the
38+
// cache does not grow unbounded.
39+
async function resolveAndroidSoundPath(
40+
url: string,
41+
guid: string,
42+
changedDate: number,
43+
fileName?: string
44+
): Promise<string> {
45+
// Offline documents already resolve to a local file path; play it directly.
46+
if (!/^https?:\/\//i.test(url)) {
47+
return url;
48+
}
49+
50+
if (!(await RNBlobUtil.fs.exists(SOUND_CACHE_DIR))) {
51+
await RNBlobUtil.fs.mkdir(SOUND_CACHE_DIR);
52+
}
53+
54+
const cachedPath = `${SOUND_CACHE_DIR}/${guid}_${changedDate}${getFileExtension(fileName)}`;
55+
if (await RNBlobUtil.fs.exists(cachedPath)) {
56+
return cachedPath;
57+
}
58+
59+
// Drop previously cached versions of this document (same guid, other changedDate).
60+
const entries = await RNBlobUtil.fs.ls(SOUND_CACHE_DIR);
61+
await Promise.all(
62+
entries
63+
.filter(entry => entry.startsWith(`${guid}_`))
64+
.map(entry => RNBlobUtil.fs.unlink(`${SOUND_CACHE_DIR}/${entry}`).catch(() => undefined))
65+
);
66+
67+
const response = await RNBlobUtil.config({ fileCache: true, path: cachedPath }).fetch("GET", url);
68+
return response.path();
69+
}
1170
// END EXTRA CODE
1271

1372
/**
@@ -19,7 +78,7 @@ import TrackPlayer, { State, Event } from "react-native-track-player";
1978
*/
2079
export async function PlaySound(audioFile?: mendix.lib.MxObject): Promise<void> {
2180
// BEGIN USER CODE
22-
// Documentation https://rntp.dev
81+
// Documentation https://github.com/zmxv/react-native-sound
2382

2483
if (!audioFile) {
2584
return Promise.reject(new Error("Input parameter 'Audio file' is required"));
@@ -32,36 +91,29 @@ export async function PlaySound(audioFile?: mendix.lib.MxObject): Promise<void>
3291

3392
const guid = audioFile.getGuid();
3493
const changedDate = audioFile.get("changedDate") as number;
94+
const fileName = audioFile.get("Name") as string;
3595

3696
try {
3797
const url = await mx.data.getDocumentUrl(guid, changedDate);
38-
// Initialize the player if it hasn't been set up yet
39-
const state = await TrackPlayer.getPlaybackState();
40-
if (state.state === State.None) {
41-
await TrackPlayer.setupPlayer({
42-
maxCacheSize: 1024
43-
});
44-
}
98+
// iOS forwards the session cookie for remote URLs, so it can stream directly.
99+
// Android cannot, so we download the file first (see resolveAndroidSoundPath).
100+
const path = Platform.OS === "ios" ? url : await resolveAndroidSoundPath(url, guid, changedDate, fileName);
45101

46-
await TrackPlayer.reset();
47-
await TrackPlayer.add({
48-
id: guid,
49-
url,
50-
title: `Audio ${guid}`,
51-
artist: "Mendix App"
52-
});
53-
54-
await TrackPlayer.play();
55-
56-
return new Promise<void>((resolve, reject) => {
57-
const subscription = TrackPlayer.addEventListener(Event.PlaybackState, event => {
58-
if (event.state === State.Stopped || event.state === State.Ended) {
59-
subscription.remove();
60-
resolve();
61-
} else if (event.state === State.Error) {
62-
subscription.remove();
63-
reject(new Error(event.error.message || "Playback error"));
102+
return await new Promise<void>((resolve, reject) => {
103+
const sound = new Sound(path, "", error => {
104+
if (error) {
105+
reject(new Error(`Failed to load audio: ${error.message ?? error}`));
106+
return;
64107
}
108+
109+
sound.play(success => {
110+
sound.release();
111+
if (success) {
112+
resolve();
113+
} else {
114+
reject(new Error("Playback failed due to an audio encoding error"));
115+
}
116+
});
65117
});
66118
});
67119
} catch (error) {

patches/react-native-track-player@4.1.2.patch

Lines changed: 0 additions & 28 deletions
This file was deleted.

pnpm-lock.yaml

Lines changed: 12 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)