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 ( ! / ^ h t t p s ? : \/ \/ / 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 */
2079export 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 ) {
0 commit comments