Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions example/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @format
*/

import ReactTestRenderer from 'react-test-renderer';
import App from '../App';

test('renders correctly', async () => {
await ReactTestRenderer.act(() => {
ReactTestRenderer.create(<App />);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.doublesymmetry.trackplayerexample

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import java.io.File

class LocalAssetHelperModule(
reactContext: ReactApplicationContext,
) : ReactContextBaseJavaModule(reactContext) {

override fun getName(): String = "LocalAssetHelper"

@ReactMethod
fun copyToDocuments(resourceName: String, ext: String, promise: Promise) {
try {
val ctx = reactApplicationContext
val resId = ctx.resources.getIdentifier(resourceName, "raw", ctx.packageName)
if (resId == 0) {
promise.reject("NOT_FOUND", "Raw resource '$resourceName' not found")
return
}

val destFile = File(ctx.filesDir, "$resourceName.$ext")
if (!destFile.exists()) {
ctx.resources.openRawResource(resId).use { input ->
destFile.outputStream().use { output -> input.copyTo(output) }
}
}

promise.resolve("file://${destFile.absolutePath}")
} catch (e: Exception) {
promise.reject("COPY_FAILED", e.message, e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.doublesymmetry.trackplayerexample

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class LocalAssetHelperPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext,
): List<NativeModule> = listOf(LocalAssetHelperModule(reactContext))

override fun createViewManagers(
reactContext: ReactApplicationContext,
): List<ViewManager<*, *>> = emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ class MainApplication : Application(), ReactApplication {
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(LocalAssetHelperPackage())
},
)
}
Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2767,7 +2767,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNTPPlayer (5.0.0):
- RNTPPlayer (5.1.3):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3263,7 +3263,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 77eecab5fd636666ca73a55bb61e2f1a685b7e84
RNReanimated: 361d5b8e20a77cd1f7907ad301f6d51ef79c1545
RNScreens: 69f68c95d395bc4261d27c3aae7b4a458b947b7e
RNTPPlayer: 54fa174461e1e0cec32d9709151a30eacb94d0e6
RNTPPlayer: d3ff34fb6044dc39201b2ad4928fa22a9927fd7e
RNVectorIcons: 6acc19c833be864e9c70894e101a587fe491150a
RNWorklets: b94f5c1b61a985c309dcc0092aacf41515cc39d6
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Expand Down
16 changes: 16 additions & 0 deletions example/ios/TrackPlayerExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
5C01D7F4F7110B6E5D3DA502 /* libPods-TrackPlayerExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D5AA6040636196903910675F /* libPods-TrackPlayerExample.a */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
A1B2C3D400000001LOCALMP3 /* one_love_two_love.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000002LOCALMP3 /* one_love_two_love.mp3 */; };
A1B2C3D400000003HELPER01 /* LocalAssetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000004HELPER01 /* LocalAssetHelper.swift */; };
A1B2C3D400000005HELPER02 /* LocalAssetHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000006HELPER02 /* LocalAssetHelper.mm */; };
A1B2C3D400000007SEAAWAIT /* the_sea_awaits.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000008SEAAWAIT /* the_sea_awaits.mp3 */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -23,6 +27,10 @@
2DD1E159FA463B0C528C8679 /* Pods-TrackPlayerExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TrackPlayerExample.release.xcconfig"; path = "Target Support Files/Pods-TrackPlayerExample/Pods-TrackPlayerExample.release.xcconfig"; sourceTree = "<group>"; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = TrackPlayerExample/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = TrackPlayerExample/LaunchScreen.storyboard; sourceTree = "<group>"; };
A1B2C3D400000002LOCALMP3 /* one_love_two_love.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = one_love_two_love.mp3; path = TrackPlayerExample/one_love_two_love.mp3; sourceTree = "<group>"; };
A1B2C3D400000004HELPER01 /* LocalAssetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LocalAssetHelper.swift; path = TrackPlayerExample/LocalAssetHelper.swift; sourceTree = "<group>"; };
A1B2C3D400000006HELPER02 /* LocalAssetHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = LocalAssetHelper.mm; path = TrackPlayerExample/LocalAssetHelper.mm; sourceTree = "<group>"; };
A1B2C3D400000008SEAAWAIT /* the_sea_awaits.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = the_sea_awaits.mp3; path = TrackPlayerExample/the_sea_awaits.mp3; sourceTree = "<group>"; };
D5AA6040636196903910675F /* libPods-TrackPlayerExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TrackPlayerExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
Expand All @@ -47,6 +55,10 @@
13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
A1B2C3D400000002LOCALMP3 /* one_love_two_love.mp3 */,
A1B2C3D400000004HELPER01 /* LocalAssetHelper.swift */,
A1B2C3D400000006HELPER02 /* LocalAssetHelper.mm */,
A1B2C3D400000008SEAAWAIT /* the_sea_awaits.mp3 */,
);
name = TrackPlayerExample;
sourceTree = "<group>";
Expand Down Expand Up @@ -161,6 +173,8 @@
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
53A5A39B182EDDD94ED7523E /* PrivacyInfo.xcprivacy in Resources */,
A1B2C3D400000001LOCALMP3 /* one_love_two_love.mp3 in Resources */,
A1B2C3D400000007SEAAWAIT /* the_sea_awaits.mp3 in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -247,6 +261,8 @@
buildActionMask = 2147483647;
files = (
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
A1B2C3D400000003HELPER01 /* LocalAssetHelper.swift in Sources */,
A1B2C3D400000005HELPER02 /* LocalAssetHelper.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
10 changes: 10 additions & 0 deletions example/ios/TrackPlayerExample/LocalAssetHelper.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(LocalAssetHelper, NSObject)

RCT_EXTERN_METHOD(copyToDocuments:(NSString *)resourceName
withExtension:(NSString *)ext
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
33 changes: 33 additions & 0 deletions example/ios/TrackPlayerExample/LocalAssetHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation
import React

@objc(LocalAssetHelper)
class LocalAssetHelper: NSObject {
@objc static func requiresMainQueueSetup() -> Bool { false }

@objc func copyToDocuments(
_ resourceName: String,
withExtension ext: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
guard let sourceURL = Bundle.main.url(forResource: resourceName, withExtension: ext) else {
reject("NOT_FOUND", "Bundle resource \(resourceName).\(ext) not found", nil)
return
}

let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destURL = docs.appendingPathComponent("\(resourceName).\(ext)")

if !FileManager.default.fileExists(atPath: destURL.path) {
do {
try FileManager.default.copyItem(at: sourceURL, to: destURL)
} catch {
reject("COPY_FAILED", error.localizedDescription, error)
return
}
}

resolve(destURL.absoluteString)
}
}
9 changes: 9 additions & 0 deletions example/ios/TrackPlayerExample/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>85F4.1</string>
<string>E174.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
Expand Down
Binary file not shown.
Binary file added example/ios/TrackPlayerExample/the_sea_awaits.mp3
Binary file not shown.
26 changes: 2 additions & 24 deletions example/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { withNativeWind } = require('nativewind/metro');

const root = path.resolve(__dirname, '..');
const escape = str => str.replace(/[/\\]/g, '[\\\\/]');

const config = {
watchFolders: [root],
resolver: {
blockList: [
new RegExp(`${escape(root)}${escape('/node_modules/react/')}.*`),
new RegExp(`${escape(root)}${escape('/node_modules/react-native/')}.*`),
],
extraNodeModules: {
'@rntp/player': root,
react: path.resolve(__dirname, 'node_modules/react'),
'react-native': path.resolve(__dirname, 'node_modules/react-native'),
'react-native-css-interop': path.resolve(
__dirname,
'node_modules/react-native-css-interop',
),
},
},
};

// Public example: @rntp/player comes from npm (no monorepo link to repo root).
module.exports = withNativeWind(
mergeConfig(getDefaultConfig(__dirname), config),
mergeConfig(getDefaultConfig(__dirname), {}),
{ input: './src/global.css', inlineRem: 16 },
);
8 changes: 4 additions & 4 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.0.0",
"@rntp/player": "^5.3.0",
"clsx": "^2.1.1",
"fast-xml-parser": "^4.3.2",
"nativewind": "^4.2.2",
"react": "19.2.0",
"react-native": "0.83.9",
"@rntp/player": "^5.0.0",
"react-native-css-interop": "^0.2.2",
"react-native-gesture-handler": "^2.17.1",
"react-native-reanimated": "^4.2.0",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "^4.19.0",
"react-native-vector-icons": "^10.2.0",
"react-native-worklets": "^0.7.0",
"fast-xml-parser": "^4.3.2",
"tailwind-merge": "^3.5.0",
"tailwindcss": "3",
"zustand": "^4.5.4",
"react-native-vector-icons": "^10.2.0"
"zustand": "^4.5.4"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
18 changes: 16 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import TrackPlayer, {
useActiveMediaItem,
} from '@rntp/player';
import { useEventLogStore } from './stores/eventLog';
import { useDownloadedTrackStore } from './stores/downloadedTrack';
import { ensureDownloadedTrack } from './lib/downloadedTracks';
import { FILE_TRACK_RESOURCE_NAME, FILE_TRACK_RESOURCE_EXT } from './data/music';

import MusicScreen from './screens/MusicScreen';
import PodcastScreen from './screens/PodcastScreen';
Expand Down Expand Up @@ -132,8 +135,19 @@ function AppContent() {
}
}, []);

// On iOS, listen to all events in the foreground since there's no background
// service. On Android this is handled by the background event handler.
// Copy a native resource to Documents so the file:// demo track is available.
useEffect(() => {
if (!isReady) return;
let cancelled = false;
ensureDownloadedTrack(FILE_TRACK_RESOURCE_NAME, FILE_TRACK_RESOURCE_EXT).then(uri => {
if (!cancelled) useDownloadedTrackStore.getState().setFileUri(uri);
});
return () => {
cancelled = true;
};
}, [isReady]);

// iOS: all events here. Android: foreground here; background → TaskService (index.js).
useEffect(() => {
if (Platform.OS !== 'ios' || !isReady) return;
const addLog = useEventLogStore.getState().addLog;
Expand Down
Binary file added example/src/assets/TheSeaAwaits.mp3
Binary file not shown.
9 changes: 8 additions & 1 deletion example/src/contexts/StrongSongsFeedContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
import TrackPlayer, { type BrowseItem } from '@rntp/player';
import type { PodcastShowFromFeed } from '../data/strongSongsFeed';
import { fetchStrongSongsFeed } from '../data/strongSongsFeed';
import { album } from '../data/music';
import { buildAlbum } from '../data/music';
import { useDownloadedTrackStore } from '../stores/downloadedTrack';
import { radioStations } from '../data/radio';

export type StrongSongsFeedValue = {
Expand All @@ -29,6 +30,7 @@ export function StrongSongsFeedProvider({ children }: { children: ReactNode }) {
const load = useCallback(async () => {
setLoading(true);
setError(null);
const album = buildAlbum(useDownloadedTrackStore.getState().fileUri);
try {
const data = await fetchStrongSongsFeed();
setShow(data);
Expand Down Expand Up @@ -108,6 +110,11 @@ export function StrongSongsFeedProvider({ children }: { children: ReactNode }) {
load();
}, [load]);

const downloadedFileUri = useDownloadedTrackStore(s => s.fileUri);
useEffect(() => {
if (downloadedFileUri) load();
}, [downloadedFileUri, load]);

const value: StrongSongsFeedValue = {
show,
loading,
Expand Down
Loading
Loading