diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 303d00b2c..edd97bed2 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -26,21 +26,21 @@ jobs:
- '.github/workflows/validate.yml'
js:
- '.node-version'
- - '.eslintrc.js'
+ - '.eslint.config.mjs'
- '.prettierrc.js'
- 'package.json'
- 'tsconfig.json'
- 'yarn.lock'
- 'src/**'
- - 'example/src/**'
- - 'example/**.json'
- - 'example/**.js'
+ - 'apps/example-native/src/**'
+ - 'apps/example-native/**.json'
+ - 'apps/example-native/**.js'
ios:
- 'ios/**'
- - 'example/ios/**'
+ - 'apps/example-native/ios/**'
android:
- 'android/**'
- - 'example/android/**'
+ - 'apps/example-native/android/**'
docs:
- 'docs/**'
check-typescript:
@@ -57,23 +57,13 @@ jobs:
- name: Install Dependencies
if: ${{ needs.check-changes.outputs.should-check-types }}
run: |-
- yarn install --frozen-lockfile
- - name: Install Example Dependencies
- if: ${{ needs.check-changes.outputs.should-check-types }}
- run: |-
- yarn example install --frozen-lockfile
+ yarn install --immutable
- name: Check Lint, Format & Types
if: ${{ needs.check-changes.outputs.should-check-types }}
run: |
- yarn lint
- yarn format
- yarn typecheck
- - name: Example - Check Lint, Format & Types
- if: ${{ needs.check-changes.outputs.should-check-types }}
- run: |
- yarn example lint
- yarn example format
- yarn example typecheck
+ yarn lint:check:all
+ yarn format:check:all
+ yarn types
build-ios:
runs-on: blaze/macos-14
needs: check-changes
@@ -87,21 +77,16 @@ jobs:
+: ruby-lang.org@3.1.0
classic.yarnpkg.com
tuist.io/xcbeautify
- - name: Install Library Dependencies
+ - name: Install Dependencies
if: ${{ needs.check-changes.outputs.should-build-ios }}
- run: yarn install --frozen-lockfile
+ run: yarn install --immutable
- name: Build Library
if: ${{ needs.check-changes.outputs.should-build-ios }}
run: yarn build
- - name: Install Example Dependencies
- if: ${{ needs.check-changes.outputs.should-build-ios }}
- run: |-
- cd example
- yarn install --frozen-lockfile
- name: Bundle Install
if: ${{ needs.check-changes.outputs.should-build-ios }}
run: |-
- cd example/ios
+ cd apps/example-native/ios
pkgx +ruby-lang.org@3.1.0 gem install bundler
pkgx +ruby-lang.org@3.1.0 bundle config set --local path 'vendor/bundle'
pkgx +ruby-lang.org@3.1.0 bundle install
@@ -109,19 +94,19 @@ jobs:
if: ${{ needs.check-changes.outputs.should-build-ios }}
uses: buildjet/cache@v4
with:
- path: example/ios/Pods
- key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }}
+ path: apps/example-native/ios/Pods
+ key: ${{ runner.os }}-pods-${{ hashFiles('apps/example-native/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Install Cococapods
if: ${{ needs.check-changes.outputs.should-build-ios }}
run: |-
- cd example/ios
+ cd apps/example-native/ios
pkgx +ruby-lang.org@3.1.0 bundle exec pod install
- name: Build App
if: ${{ needs.check-changes.outputs.should-build-ios }}
run: |-
- cd example/ios
+ cd apps/example-native/ios
set -o pipefail && xcodebuild build -workspace TrackPlayerExample.xcworkspace -scheme TrackPlayerExample -destination 'platform=iOS Simulator,name=iPhone 15 Pro' | xcbeautify --renderer github-actions
build-android:
runs-on: ubuntu-latest
@@ -134,35 +119,30 @@ jobs:
uses: pkgxdev/setup@v2
with:
+: classic.yarnpkg.com
- - name: Install Library Dependencies
+ - name: Install Dependencies
if: ${{ needs.check-changes.outputs.should-build-android }}
- run: yarn install --frozen-lockfile
+ run: yarn install --immutable
- name: Build Library
if: ${{ needs.check-changes.outputs.should-build-android }}
run: yarn build
- - name: Install Mobile Dependencies
- if: ${{ needs.check-changes.outputs.should-build-android }}
- run: |-
- cd example
- yarn install
- name: Cache Gradle Wrapper
if: ${{ needs.check-changes.outputs.should-build-android }}
uses: actions/cache@v4
with:
path: ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
+ key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('apps/example-native/android/gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache Gradle Dependencies
if: ${{ needs.check-changes.outputs.should-build-android }}
uses: actions/cache@v4
with:
path: ~/.gradle/caches
- key: ${{ runner.os }}-gradle-caches-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
+ key: ${{ runner.os }}-gradle-caches-${{ hashFiles('apps/example-native/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-caches-
- name: Build App
if: ${{ needs.check-changes.outputs.should-build-android }}
run: |-
- cd example/android
+ cd apps/example-native/android
./gradlew assembleDebug --no-daemon
build-docs:
runs-on: ubuntu-latest
@@ -181,7 +161,7 @@ jobs:
- name: Install Dependencies
if: ${{ needs.check-changes.outputs.should-build-docs }}
run: |-
- yarn install --frozen-lockfile
+ yarn install --immutable
- name: Build Docs
if: ${{ needs.check-changes.outputs.should-build-docs }}
run: |-
diff --git a/.gitignore b/.gitignore
index 67f32126d..55cbb6f9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,10 +43,10 @@ android.iml
# Cocoapods
#
-example/ios/Pods
+apps/*/ios/Pods
# Ruby
-example/vendor/
+apps/*/vendor/
# node.js
#
diff --git a/apps/common-app/package.json b/apps/common-app/package.json
new file mode 100644
index 000000000..db62cfbda
--- /dev/null
+++ b/apps/common-app/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "common-app",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "format": "prettier --write src",
+ "format:check": "prettier --check src",
+ "lint": "eslint src --fix",
+ "lint:check": "eslint src --max-warnings=0",
+ "start": "react-native start",
+ "types": "tsc --noEmit true"
+ },
+ "peerDependencies": {
+ "react-native": "*"
+ },
+ "dependencies": {
+ "@react-native-community/slider": "^4.5.7",
+ "@react-native-vector-icons/fontawesome6": "^12.0.1",
+ "react": "19.1.0",
+ "react-native-safe-area-context": "^5.5.0",
+ "react-native-track-player": "workspace:*"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/preset-env": "^7.25.3",
+ "@babel/runtime": "^7.25.0",
+ "@react-native-community/cli": "19.1.1",
+ "@react-native-community/cli-platform-android": "19.1.1",
+ "@react-native-community/cli-platform-ios": "19.1.1",
+ "@react-native/babel-preset": "0.80.2",
+ "@react-native/eslint-config": "0.80.2",
+ "@react-native/metro-config": "0.80.2",
+ "@react-native/typescript-config": "0.80.2",
+ "@types/react": "^19.1.0"
+ },
+ "engines": {
+ "node": ">=24"
+ }
+}
diff --git a/example/src/App.tsx b/apps/common-app/src/App.tsx
similarity index 98%
rename from example/src/App.tsx
rename to apps/common-app/src/App.tsx
index 3c4caaa6d..5b7181293 100644
--- a/example/src/App.tsx
+++ b/apps/common-app/src/App.tsx
@@ -13,9 +13,7 @@ import {
View,
} from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
-import TrackPlayer, {
- useActiveTrack
-} from 'react-native-track-player';
+import TrackPlayer, { useActiveTrack } from 'react-native-track-player';
import {
ActionSheet,
Button,
diff --git a/example/src/assets/data/playlist.json b/apps/common-app/src/assets/data/playlist.json
similarity index 100%
rename from example/src/assets/data/playlist.json
rename to apps/common-app/src/assets/data/playlist.json
diff --git a/example/src/assets/resources/artwork.jpg b/apps/common-app/src/assets/resources/artwork.jpg
similarity index 100%
rename from example/src/assets/resources/artwork.jpg
rename to apps/common-app/src/assets/resources/artwork.jpg
diff --git a/example/src/assets/resources/pure.m4a b/apps/common-app/src/assets/resources/pure.m4a
similarity index 100%
rename from example/src/assets/resources/pure.m4a
rename to apps/common-app/src/assets/resources/pure.m4a
diff --git a/example/src/components/ActionSheet.tsx b/apps/common-app/src/components/ActionSheet.tsx
similarity index 100%
rename from example/src/components/ActionSheet.tsx
rename to apps/common-app/src/components/ActionSheet.tsx
diff --git a/example/src/components/Button.tsx b/apps/common-app/src/components/Button.tsx
similarity index 100%
rename from example/src/components/Button.tsx
rename to apps/common-app/src/components/Button.tsx
diff --git a/example/src/components/OptionSheet.tsx b/apps/common-app/src/components/OptionSheet.tsx
similarity index 97%
rename from example/src/components/OptionSheet.tsx
rename to apps/common-app/src/components/OptionSheet.tsx
index 622a2edf0..e2aa6617a 100644
--- a/example/src/components/OptionSheet.tsx
+++ b/apps/common-app/src/components/OptionSheet.tsx
@@ -41,9 +41,7 @@ export function OptionSheet() {
value: 'stop-playback-and-remove-notification',
},
]}
- value={
- currentOptions.android.appKilledPlaybackBehavior
- }
+ value={currentOptions.android.appKilledPlaybackBehavior}
onSelect={(appKilledPlaybackBehavior: AppKilledPlaybackBehavior) => {
TrackPlayer.updateOptions({
android: {
diff --git a/example/src/components/PlayPauseButton.tsx b/apps/common-app/src/components/PlayPauseButton.tsx
similarity index 89%
rename from example/src/components/PlayPauseButton.tsx
rename to apps/common-app/src/components/PlayPauseButton.tsx
index 7dba8595e..185026c0d 100644
--- a/example/src/components/PlayPauseButton.tsx
+++ b/apps/common-app/src/components/PlayPauseButton.tsx
@@ -14,9 +14,7 @@ export function PlayPauseButton() {
{buffering ? (
) : (
-
+
App);
diff --git a/example/ios/.xcode.env b/apps/example-native/ios/.xcode.env
similarity index 100%
rename from example/ios/.xcode.env
rename to apps/example-native/ios/.xcode.env
diff --git a/example/ios/Podfile b/apps/example-native/ios/Podfile
similarity index 100%
rename from example/ios/Podfile
rename to apps/example-native/ios/Podfile
diff --git a/example/ios/Podfile.lock b/apps/example-native/ios/Podfile.lock
similarity index 94%
rename from example/ios/Podfile.lock
rename to apps/example-native/ios/Podfile.lock
index 8139875dc..f562bee5f 100644
--- a/example/ios/Podfile.lock
+++ b/apps/example-native/ios/Podfile.lock
@@ -1648,7 +1648,7 @@ PODS:
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- SocketRocket
- - react-native-safe-area-context (5.5.0):
+ - react-native-safe-area-context (5.6.2):
- boost
- DoubleConversion
- fast_float
@@ -1667,8 +1667,8 @@ PODS:
- React-hermes
- React-ImageManager
- React-jsi
- - react-native-safe-area-context/common (= 5.5.0)
- - react-native-safe-area-context/fabric (= 5.5.0)
+ - react-native-safe-area-context/common (= 5.6.2)
+ - react-native-safe-area-context/fabric (= 5.6.2)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
@@ -1679,7 +1679,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - react-native-safe-area-context/common (5.5.0):
+ - react-native-safe-area-context/common (5.6.2):
- boost
- DoubleConversion
- fast_float
@@ -1708,7 +1708,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - react-native-safe-area-context/fabric (5.5.0):
+ - react-native-safe-area-context/fabric (5.6.2):
- boost
- DoubleConversion
- fast_float
@@ -1826,7 +1826,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - react-native-vector-icons-fontawesome6 (12.0.1)
+ - react-native-vector-icons-fontawesome6 (12.2.0)
- React-NativeModulesApple (0.80.2):
- boost
- DoubleConversion
@@ -2348,7 +2348,7 @@ DEPENDENCIES:
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- - react-native-track-player (from `../..`)
+ - react-native-track-player (from `../../..`)
- "react-native-vector-icons-fontawesome6 (from `../node_modules/@react-native-vector-icons/fontawesome6`)"
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
@@ -2475,7 +2475,7 @@ EXTERNAL SOURCES:
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-track-player:
- :path: "../.."
+ :path: "../../.."
react-native-vector-icons-fontawesome6:
:path: "../node_modules/@react-native-vector-icons/fontawesome6"
React-NativeModulesApple:
@@ -2551,76 +2551,76 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: bbc1152da7d2d40f9e59c28acc6576fcf5d28e2a
- RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
+ RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7
RCTRequired: e0446b01093475b7082fbeee5d1ef4ad1fe20ac4
RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f
React: e7a4655b09d0e17e54be188cc34c2f3e2087318a
React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b
- React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542
- React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553
- React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691
+ React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7
+ React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2
+ React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac
React-debug: 1834225a63b420b16e9b8b01ba5870aee96d0610
- React-defaultsnativemodule: dd88d445d542d58ab61a8a29a7c1d2272dfed577
- React-domnativemodule: fc3c24f4d3bb92770727ea48b4133dab77ded7f7
- React-Fabric: 00fe76339e568da0d0497cc72daeeb01e463871a
- React-FabricComponents: 7bb179ee55db68f88c007800b0ac62c930115a85
- React-FabricImage: 21e01118011dd1e4ff3cdab20dbf57839cff52ee
- React-featureflags: 6e67f2e252bc8ebb1d538c2ae8c14df432fe5fc0
- React-featureflagsnativemodule: eff5216a5cde5df5d09243d15db1bc401474deef
- React-graphics: 8539372da8754118a565251ed08a88fc70f69340
- React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761
- React-idlecallbacksnativemodule: 7349675d1ccbec876c29b0e206ac08c762baaa36
- React-ImageManager: 4089d8ad52c86a8ae1d7591282fff1665ff5518b
- React-jserrorhandler: 89a7a5fa8d04791e729119d1db03bf0ee85a9e29
- React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c
- React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826
- React-jsinspector: 69e974b6313dbbb635ba503f2f4f2c389b30edbf
- React-jsinspectorcdp: 231ddd5b7164c37589dcde3b8b6960136c891d6d
- React-jsinspectornetwork: ff74911f79cf0a407a7f0ad0eeb0be64687ed815
- React-jsinspectortracing: df2aa2d944bb3fa280d9c920b9a06664bca8a7e8
- React-jsitooling: 77849c27e374a028ed8106e434a35267f6c6600b
- React-jsitracing: 0dc6978e5b38c6e5e01e6aed484e4aec3f5f581b
- React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce
- React-Mapbuffer: 7018c5b7da5b13ed22fe55dae51d50187a00b2d7
- React-microtasksnativemodule: 8ff9cb220a8efa625b5885996bd69e69db9edf02
- react-native-safe-area-context: 3bae4f8474c13ab141c40ed6c5c33f6177778d71
- react-native-slider: c434f7094c9500dfa1176931f48e5957872818f8
- react-native-track-player: 5a85aff2010595a73137ca9df980cf0cec78f997
- react-native-vector-icons-fontawesome6: 624a5578d701e52b18fb7f35b56556395a4249c0
- React-NativeModulesApple: 37c08c3c54db55854de816b0df0f3683832be35a
+ React-defaultsnativemodule: 260aa990a9617c58df46c00321f396ad6ea7cc7f
+ React-domnativemodule: 9b3456a614c325da986867f27ca0eb34cb86828c
+ React-Fabric: fc7bcbac28989e6025ca6ae0988bff61bb78e5d3
+ React-FabricComponents: ae4a9c82bedf7c95bace1b215caf8685bcb32e23
+ React-FabricImage: c9cd4786180c150bb2a3841d65d360fd52be9ef8
+ React-featureflags: 534cd678e05848fbfc8c7288d4b14bcd8894b696
+ React-featureflagsnativemodule: bf7419f4d81226a3c4dd792445a03a6d703ce9a4
+ React-graphics: 18296c3559d54a42baaf7f2ae9c137a2e0fe9d51
+ React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b
+ React-idlecallbacksnativemodule: da8696a714ab16adb56bbfc9e0dfb4de7a713340
+ React-ImageManager: 052ccce122e4fd4e09c5d4f30e56381704dac439
+ React-jserrorhandler: 4c037384a32f57332abfa64181aeea915f9e0f0d
+ React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9
+ React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb
+ React-jsinspector: 4ad0cdfa25a45d1362e2ddd06c78727d7964b34f
+ React-jsinspectorcdp: a649cc98a448e0fd8d54ac2a9e3e53177a1d8bd3
+ React-jsinspectornetwork: 2d701b6b152be202342f8269223046ec664c7d47
+ React-jsinspectortracing: cd898b3d7ea89f3e0ae10020fe3504bb4b327dd8
+ React-jsitooling: feca163583c69ba642cebb6b8ccd2f5e6732fed8
+ React-jsitracing: 1965307a468987b20d2a020f8fe782efa591ded7
+ React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb
+ React-Mapbuffer: a5d550d1add940ed2bc65b20dc1413407bf1a63f
+ React-microtasksnativemodule: 5d00fefc19f0bc9a6432e5533683d6fc9c3da4e1
+ react-native-safe-area-context: d446989793f96dc2f44c33c42dbfb316d983f24e
+ react-native-slider: 83d77040942794b3994a8c3e116258463326cee5
+ react-native-track-player: 4d01ef050bc01cce94ad66b34d1aae4ec797988a
+ react-native-vector-icons-fontawesome6: 1b667969bef2f430d1d7515163b980b856f58670
+ React-NativeModulesApple: b22e6abb44d78270dfdfc7d85efe29e35e0333a7
React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1
- React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9
- React-performancetimeline: 9321ba7605abcfb3a2b497fd7cbaf5cfd8c7cf67
+ React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f
+ React-performancetimeline: a04dae9154c32eda1891fcfa51cb2680a0421b3e
React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e
- React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d
- React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251
- React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b
- React-RCTFabric: 0a9ff5c9d1e1d7fc026bda6671180cbf56861c15
- React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae
- React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab
- React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670
- React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc
- React-RCTRuntime: 96808e8fdce300a26c82d8c24174e33ba5210a7c
- React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307
- React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9
- React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72
+ React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89
+ React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb
+ React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0
+ React-RCTFabric: e7acf005f8ed58d09f755b980ff83703b3af9fcf
+ React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec
+ React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9
+ React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce
+ React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482
+ React-RCTRuntime: a7bca9be4f571586b2a9d4b57cf605421ffb6335
+ React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884
+ React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74
+ React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440
React-rendererconsistency: d20fcb77173861cc7d8356239823e3b36966fc31
- React-renderercss: 56461d1e18db6a325048fdd04a51d68bd7ddb5a8
- React-rendererdebug: fcd44d3eb8a02d74beee778bb142e724016c7375
+ React-renderercss: 63c720c32aaabd4788ac4136a071d49a052d8002
+ React-rendererdebug: a25ddddc73cabf50d814d8dfbc60d257b3d854c4
React-rncore: bafb76fc01b78757a9592e92dbc227f9260bf0ac
- React-RuntimeApple: 01e3ad08793efaa54cf85276457fa4a1f103d5b4
- React-RuntimeCore: 5c4bec5bf402a99b134e55972f2f4e676c70b9ab
+ React-RuntimeApple: 45f8ef1b220a91b4fa4a79820b81990bffd95aa5
+ React-RuntimeCore: a0e095493b22ee3f6c639df4258cc5185674f0b8
React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5
- React-RuntimeHermes: ba549a5834a6592d243b9a605530ecd7b6f5e79c
- React-runtimescheduler: 9a9914d58caec7976aaae381cd2d997408f2260f
+ React-RuntimeHermes: 5b8126fffd1531475861dc0294a10b5f9793271a
+ React-runtimescheduler: 44fa97351d105afd0ffaecc4ed11cadad562deb6
React-timing: 4f97958cc918f0af9444f93e4a7083415e6f5daf
- React-utils: f491e2726eb8ced8af13893e1f77317f0fa9a954
- ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0
- ReactCodegen: 439c427ccc115d71d16cc84256e5fbdc7fcef57a
- ReactCommon: 592ef441605638b95e533653259254b4bd35ff4f
+ React-utils: 3c4b0b7788e4dc132d1bf918bc0615e2b21f36b3
+ ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989
+ ReactCodegen: 9ea66ee246511816b72e9d6e380f884b7b3b99d7
+ ReactCommon: 7aca047f2f453a7d7f0adeccb63810d61829235a
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- Yoga: a742cc68e8366fcfc681808162492bc0aa7a9498
+ Yoga: 1c52fbd270869e556504def7e94fffbf67f53f7b
PODFILE CHECKSUM: 3b4ec20947e91756d94116d805d6728ccbf46a37
diff --git a/example/ios/TrackPlayerExample.xcodeproj/project.pbxproj b/apps/example-native/ios/TrackPlayerExample.xcodeproj/project.pbxproj
similarity index 100%
rename from example/ios/TrackPlayerExample.xcodeproj/project.pbxproj
rename to apps/example-native/ios/TrackPlayerExample.xcodeproj/project.pbxproj
diff --git a/example/ios/TrackPlayerExample.xcodeproj/xcshareddata/xcschemes/TrackPlayerExample.xcscheme b/apps/example-native/ios/TrackPlayerExample.xcodeproj/xcshareddata/xcschemes/TrackPlayerExample.xcscheme
similarity index 100%
rename from example/ios/TrackPlayerExample.xcodeproj/xcshareddata/xcschemes/TrackPlayerExample.xcscheme
rename to apps/example-native/ios/TrackPlayerExample.xcodeproj/xcshareddata/xcschemes/TrackPlayerExample.xcscheme
diff --git a/example/ios/TrackPlayerExample.xcworkspace/contents.xcworkspacedata b/apps/example-native/ios/TrackPlayerExample.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from example/ios/TrackPlayerExample.xcworkspace/contents.xcworkspacedata
rename to apps/example-native/ios/TrackPlayerExample.xcworkspace/contents.xcworkspacedata
diff --git a/example/ios/TrackPlayerExample/AppDelegate.swift b/apps/example-native/ios/TrackPlayerExample/AppDelegate.swift
similarity index 100%
rename from example/ios/TrackPlayerExample/AppDelegate.swift
rename to apps/example-native/ios/TrackPlayerExample/AppDelegate.swift
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Contents.json
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Contents.json
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Icon.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Icon.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Icon.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/Icon.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x 1.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x 1.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x 1.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x 1.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@2x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_20pt@3x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x 1.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x 1.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x 1.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x 1.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@2x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_29pt@3x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x 1.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x 1.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x 1.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x 1.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@2x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_40pt@3x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@2x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_60pt@3x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt@2x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt@2x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_76pt@2x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_83.5@2x.png
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_83.5@2x.png
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/AppIcon.appiconset/icon_83.5@2x.png
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/Contents.json b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/Contents.json
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/Contents.json
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/Contents.json
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Contents.json b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Contents.json
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Contents.json
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Contents.json
diff --git a/example/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Logo.pdf b/apps/example-native/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Logo.pdf
similarity index 100%
rename from example/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Logo.pdf
rename to apps/example-native/ios/TrackPlayerExample/Images.xcassets/Logo.imageset/Logo.pdf
diff --git a/example/ios/TrackPlayerExample/Info.plist b/apps/example-native/ios/TrackPlayerExample/Info.plist
similarity index 100%
rename from example/ios/TrackPlayerExample/Info.plist
rename to apps/example-native/ios/TrackPlayerExample/Info.plist
diff --git a/example/ios/TrackPlayerExample/LaunchScreen.storyboard b/apps/example-native/ios/TrackPlayerExample/LaunchScreen.storyboard
similarity index 100%
rename from example/ios/TrackPlayerExample/LaunchScreen.storyboard
rename to apps/example-native/ios/TrackPlayerExample/LaunchScreen.storyboard
diff --git a/example/ios/TrackPlayerExample/PrivacyInfo.xcprivacy b/apps/example-native/ios/TrackPlayerExample/PrivacyInfo.xcprivacy
similarity index 100%
rename from example/ios/TrackPlayerExample/PrivacyInfo.xcprivacy
rename to apps/example-native/ios/TrackPlayerExample/PrivacyInfo.xcprivacy
diff --git a/example/jest.config.js b/apps/example-native/jest.config.js
similarity index 100%
rename from example/jest.config.js
rename to apps/example-native/jest.config.js
diff --git a/apps/example-native/metro.config.js b/apps/example-native/metro.config.js
new file mode 100644
index 000000000..b380248a5
--- /dev/null
+++ b/apps/example-native/metro.config.js
@@ -0,0 +1,31 @@
+const path = require('path');
+const { getDefaultConfig } = require('@react-native/metro-config');
+const { withMetroConfig } = require('react-native-monorepo-config');
+
+const root = path.resolve(__dirname, '..', '..');
+const commonAppPath = path.resolve(__dirname, '..', 'common-app');
+
+/**
+ * Metro configuration
+ * https://facebook.github.io/metro/docs/configuration
+ *
+ * @type {import('metro-config').MetroConfig}
+ */
+const config = withMetroConfig(getDefaultConfig(__dirname), {
+ root,
+ dirname: __dirname,
+});
+
+// Add watchFolders to watch common-app
+config.watchFolders = [root, commonAppPath];
+
+// Ensure node_modules are resolved correctly
+config.resolver = {
+ ...config.resolver,
+ nodeModulesPaths: [
+ path.resolve(__dirname, 'node_modules'),
+ path.resolve(root, 'node_modules'),
+ ],
+};
+
+module.exports = config;
diff --git a/example/package.json b/apps/example-native/package.json
similarity index 80%
rename from example/package.json
rename to apps/example-native/package.json
index 49ac61e4d..a4c092f33 100644
--- a/example/package.json
+++ b/apps/example-native/package.json
@@ -1,11 +1,12 @@
{
- "name": "react-native-track-player-example",
+ "name": "example-native",
"version": "0.0.1",
"private": true,
"scripts": {
+ "clean": "del-cli android/build android/app/build ios/build",
"android:adb-reverse": "adb reverse tcp:8081 tcp:8081 && adb forward tcp:5277 tcp:5277",
"android:adb-stop": "adb shell am force-stop trackplayer.example",
- "android:dhu": "pkill -f 'desktop-head-unit' || true && $HOME/Library/Android/sdk/extras/google/auto/desktop-head-unit",
+ "android:dhu": "pkill -f 'desktop-head-unit' || true && $ANDROID_SDK_ROOT/extras/google/auto/desktop-head-unit --usb",
"android:build": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"",
"android:clean": "rm -rf android/.gradle && rm -rf android/build && rm -rf android/app/build && rm -rf android/app/.cxx",
"android:ide": "open -a /Applications/Android\\ Studio.app ./android",
@@ -14,20 +15,22 @@
"android:release": "cd android && ./gradlew clean && ./gradlew bundleRelease && cd app/build/outputs/bundle/release && pwd && ls",
"android:uninstall": "adb uninstall trackplayer.example",
"android": "react-native run-android",
- "format:fix": "prettier --write src",
- "format": "prettier --check src",
"ios:build": "react-native build-ios --mode Debug",
"ios:ide": "open ios/*.xcworkspace",
"ios:sim": "react-native run-ios --simulator=\"iPhone\"",
+ "ios:rebuild": "cd ios && pod install && cd ../.. && yarn ios",
"ios": "react-native run-ios",
- "lint:fix": "eslint src --fix",
- "lint": "eslint src --max-warnings=0",
+ "format": "prettier --write index.js",
+ "format:check": "prettier --check index.js",
+ "lint": "eslint . --fix",
+ "lint:check": "eslint . --max-warnings=0",
"start": "react-native start",
"types": "tsc --noEmit true"
},
"dependencies": {
"@react-native-community/slider": "^4.5.7",
"@react-native-vector-icons/fontawesome6": "^12.0.1",
+ "common-app": "workspace:*",
"react": "19.1.0",
"react-native": "0.80.2",
"react-native-safe-area-context": "^5.5.0"
@@ -44,6 +47,7 @@
"@react-native/metro-config": "0.80.2",
"@react-native/typescript-config": "0.80.2",
"@types/react": "^19.1.0",
+ "babel-plugin-module-resolver": "^5.0.2",
"react-native-builder-bob": "^0.40.12",
"react-native-monorepo-config": "^0.1.9",
"typescript": "^5.9.3"
diff --git a/example/react-native.config.js b/apps/example-native/react-native.config.js
similarity index 80%
rename from example/react-native.config.js
rename to apps/example-native/react-native.config.js
index 59d969820..2701c08cf 100644
--- a/example/react-native.config.js
+++ b/apps/example-native/react-native.config.js
@@ -1,5 +1,5 @@
const path = require('path');
-const pkg = require('../package.json');
+const pkg = require('../../package.json');
module.exports = {
project: {
@@ -9,7 +9,7 @@ module.exports = {
},
dependencies: {
[pkg.name]: {
- root: path.join(__dirname, '..'),
+ root: path.join(__dirname, '..', '..'),
platforms: {
// Codegen script incorrectly fails without this
// So we explicitly specify the platforms with empty object
diff --git a/apps/example-native/tsconfig.json b/apps/example-native/tsconfig.json
new file mode 100644
index 000000000..3f5061a50
--- /dev/null
+++ b/apps/example-native/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["../common-app/src/*"]
+ }
+ },
+ "exclude": [
+ "**/node_modules",
+ "**/Pods",
+ "metro.config.js",
+ "android",
+ "ios",
+ ".bundle"
+ ]
+}
diff --git a/apps/example-nextjs/.gitignore b/apps/example-nextjs/.gitignore
new file mode 100644
index 000000000..a680367ef
--- /dev/null
+++ b/apps/example-nextjs/.gitignore
@@ -0,0 +1 @@
+.next
diff --git a/apps/example-nextjs/README.md b/apps/example-nextjs/README.md
new file mode 100644
index 000000000..e839ba5c6
--- /dev/null
+++ b/apps/example-nextjs/README.md
@@ -0,0 +1,174 @@
+# TrackPlayer Next.js Example
+
+This example demonstrates how to use `react-native-track-player` in a Next.js web application using React Native Web.
+
+## Features
+
+- ✅ Full audio playback control (play, pause, skip)
+- ✅ Progress tracking with visual progress bar
+- ✅ Track metadata display with artwork
+- ✅ Responsive UI using React Native components
+- ✅ Server-side rendering compatible (uses dynamic imports)
+- ✅ Web-specific player implementation using HTML5 Audio/Shaka Player
+
+## Quick Start
+
+### From the Monorepo Root
+
+```bash
+# Install dependencies
+yarn install
+
+# Build the library
+yarn prepare
+
+# Start the Next.js dev server
+cd apps/example-nextjs
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) to see the audio player.
+
+## Running the App
+
+You can:
+
+- **Start in development mode:**
+
+```bash
+yarn dev
+```
+
+- **Create and run a production build:**
+
+```bash
+yarn build
+yarn start
+```
+
+- **Analyze the bundle** (opens report in browser):
+
+```bash
+yarn build:analyze
+```
+
+- **Build without minification** (useful for debugging):
+
+```bash
+yarn build:disable-minification
+```
+
+## Project Structure
+
+```
+apps/example-nextjs/
+├── components/
+│ ├── AudioPlayer.js # Main player UI component
+│ └── TrackPlayerProvider.js # Player setup and initialization
+├── pages/
+│ ├── _app.js # Next.js app wrapper
+│ ├── _document.js # HTML document structure
+│ └── index.js # Home page with player
+├── next.config.js # Next.js configuration
+└── package.json # Dependencies
+```
+
+## How It Works
+
+### Server-Side Rendering
+
+The app uses Next.js dynamic imports to avoid SSR issues:
+
+```javascript
+const AudioPlayer = dynamic(() => import('../components/AudioPlayer'), {
+ ssr: false,
+});
+```
+
+This ensures TrackPlayer only loads client-side where browser APIs are available.
+
+### Web Implementation
+
+- **Native (iOS/Android)**: Platform-specific audio APIs
+- **Web**: HTML5 Audio API or Shaka Player for HLS/DASH
+
+The web implementation is automatically selected in browser environments.
+
+## Adding Custom Tracks
+
+Edit `components/TrackPlayerProvider.js`:
+
+```javascript
+const tracks = [
+ {
+ url: 'https://example.com/audio.mp3',
+ title: 'My Track',
+ artist: 'Artist Name',
+ artwork: 'https://example.com/artwork.jpg',
+ duration: 120,
+ },
+];
+```
+
+## Components
+
+### TrackPlayerProvider
+
+Initializes TrackPlayer and loads the queue:
+
+```javascript
+
+
+
+```
+
+### AudioPlayer
+
+Main UI with:
+
+- Track info and artwork
+- Progress bar
+- Playback controls (play/pause/skip)
+
+Uses TrackPlayer hooks:
+
+- `useActiveTrack()` - Current track info
+- `useProgress()` - Playback progress
+- `usePlaybackState()` - Player state
+
+## Troubleshooting
+
+### Audio Not Playing
+
+1. Check browser console for errors
+2. Verify audio URLs are accessible (check CORS)
+3. Some browsers require user interaction before playing
+
+### Module Resolution Issues
+
+```bash
+# From root
+yarn install
+yarn prepare
+
+# Clear Next.js cache
+cd apps/example-nextjs
+rm -rf .next
+yarn dev
+```
+
+## Web vs Native Differences
+
+| Feature | Web | Native |
+| -------------------- | ----------------- | ------------------ |
+| Background playback | Limited | Full |
+| Lock screen controls | No | Yes |
+| Notifications | No | Yes |
+| Car integration | No | Yes |
+| Audio formats | Browser-dependent | Platform-dependent |
+
+## Resources
+
+- [TrackPlayer Docs](https://react-native-track-player.js.org/)
+- [Next.js Documentation](https://nextjs.org/docs)
+- [React Native Web](https://necolas.github.io/react-native-web/)
diff --git a/apps/example-nextjs/babel.config.js b/apps/example-nextjs/babel.config.js
new file mode 100644
index 000000000..da4606b61
--- /dev/null
+++ b/apps/example-nextjs/babel.config.js
@@ -0,0 +1,4 @@
+/** @type {import('@babel/core').TransformOptions} */
+module.exports = {
+ presets: ['next/babel'],
+};
diff --git a/apps/example-nextjs/components/TrackPlayerApp.js b/apps/example-nextjs/components/TrackPlayerApp.js
new file mode 100644
index 000000000..0fccb290b
--- /dev/null
+++ b/apps/example-nextjs/components/TrackPlayerApp.js
@@ -0,0 +1,33 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { ActivityIndicator, StyleSheet, View } from 'react-native';
+import App from '../../common-app/src/App';
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexGrow: 1,
+ justifyContent: 'center',
+ backgroundColor: '#1a1a1a',
+ padding: 20,
+ },
+});
+
+export default function TrackPlayerProvider() {
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
diff --git a/apps/example-nextjs/cypress.config.js b/apps/example-nextjs/cypress.config.js
new file mode 100644
index 000000000..8ae01ad7a
--- /dev/null
+++ b/apps/example-nextjs/cypress.config.js
@@ -0,0 +1,9 @@
+const defineConfig = require('cypress').defineConfig;
+
+module.exports = {
+ default: defineConfig({
+ e2e: {
+ supportFile: false,
+ },
+ }),
+};
diff --git a/apps/example-nextjs/next-env.d.ts b/apps/example-nextjs/next-env.d.ts
new file mode 100644
index 000000000..254b73c16
--- /dev/null
+++ b/apps/example-nextjs/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
diff --git a/apps/example-nextjs/next.config.js b/apps/example-nextjs/next.config.js
new file mode 100644
index 000000000..90f529fe5
--- /dev/null
+++ b/apps/example-nextjs/next.config.js
@@ -0,0 +1,90 @@
+const path = require('path');
+
+const withBundleAnalyzer = require('@next/bundle-analyzer')({
+ enabled: process.env.ANALYZE_BUNDLE === '1',
+});
+
+// this can be used to obtain a more readable bundle for debugging
+const disableMinification = process.env.DISABLE_MINIFICATION === '1';
+
+const config = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ transpilePackages: [
+ 'common-app',
+ 'react-native-track-player',
+ 'react-native-web',
+ 'react-native',
+ ],
+ webpack(webpackConfig, { isServer }) {
+ if (disableMinification) {
+ webpackConfig.optimization.minimizer = [];
+ }
+
+ // Configure aliases - must be done for both client and server
+ webpackConfig.resolve.alias = {
+ ...(webpackConfig.resolve.alias || {}),
+ // Ensure single React instance
+ 'react': path.resolve(__dirname, 'node_modules/react'),
+ 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
+ // Alias react-native to react-native-web
+ 'react-native$': path.resolve(__dirname, 'react-native-shim.js'),
+ 'react-native': path.resolve(__dirname, 'react-native-shim.js'),
+ // Use web-specific implementation for TrackPlayer (compiled)
+ 'react-native-track-player$': path.resolve(
+ __dirname,
+ '../../lib/module/index.js',
+ ),
+ };
+
+ // Ensure proper extensions
+ webpackConfig.resolve.extensions = [
+ '.web.tsx',
+ '.web.ts',
+ '.web.jsx',
+ '.web.js',
+ '.tsx',
+ '.ts',
+ '.jsx',
+ '.js',
+ '.json',
+ '.wasm',
+ ...(webpackConfig.resolve.extensions || []).filter(
+ (ext) =>
+ ![
+ '.web.tsx',
+ '.web.ts',
+ '.web.jsx',
+ '.web.js',
+ '.tsx',
+ '.ts',
+ '.jsx',
+ '.js',
+ ].includes(ext),
+ ),
+ ];
+
+ // Add font file loader for react-native-vector-icons
+ webpackConfig.module.rules.push({
+ test: /\.(ttf|otf|eot|woff|woff2)$/,
+ type: 'asset/resource',
+ generator: {
+ filename: 'static/fonts/[name][ext]',
+ },
+ });
+
+ // Add audio/video file loader
+ webpackConfig.module.rules.push({
+ test: /\.(mp3|mp4|m4a|wav|ogg|webm)$/,
+ type: 'asset/resource',
+ generator: {
+ filename: 'static/media/[name][ext]',
+ },
+ });
+
+ return webpackConfig;
+ },
+};
+
+module.exports = withBundleAnalyzer(config);
diff --git a/apps/example-nextjs/package.json b/apps/example-nextjs/package.json
new file mode 100644
index 000000000..274021823
--- /dev/null
+++ b/apps/example-nextjs/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "example-nextjs",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "next",
+ "build": "",
+ "build:next": "next build",
+ "build:analyze": "ANALYZE_BUNDLE=1 next build",
+ "build:disable-minification": "DISABLE_MINIFICATION=1 next build",
+ "start": "next start",
+ "e2e": "start-server-and-test http://localhost:3000 \"cypress open --e2e\"",
+ "e2e:headless": "start-server-and-test http://localhost:3000 \"cypress run --e2e\"",
+ "lint": "eslint --fix components pages",
+ "lint:check": "eslint --max-warnings=0 components pages",
+ "format": "prettier --write components pages",
+ "format:check": "prettier --check components pages"
+ },
+ "dependencies": {
+ "common-app": "workspace:*",
+ "expo": "54.0.13",
+ "next": "15.5.4",
+ "react": "19.1.1",
+ "react-dom": "19.1.1",
+ "react-native-track-player": "workspace:*",
+ "react-native-web": "0.21.1",
+ "webpack": "5.102.1"
+ },
+ "devDependencies": {
+ "@babel/core": "7.28.4",
+ "@expo/next-adapter": "6.0.0",
+ "@next/bundle-analyzer": "15.5.4",
+ "cypress": "15.4.0",
+ "eslint": "9.37.0",
+ "next-compose-plugins": "2.2.1",
+ "prettier": "3.6.2",
+ "start-server-and-test": "2.1.2",
+ "typescript": "5.8.3"
+ },
+ "installConfig": {
+ "selfReferences": false
+ }
+}
diff --git a/apps/example-nextjs/pages/_app.js b/apps/example-nextjs/pages/_app.js
new file mode 100644
index 000000000..950f5a968
--- /dev/null
+++ b/apps/example-nextjs/pages/_app.js
@@ -0,0 +1,13 @@
+import Head from 'next/head';
+
+export default function App({ Component, pageProps }) {
+ return (
+ <>
+
+ RNTP - Next.js Example
+
+
+
+ >
+ );
+}
diff --git a/apps/example-nextjs/pages/_document.js b/apps/example-nextjs/pages/_document.js
new file mode 100644
index 000000000..6b9f6517e
--- /dev/null
+++ b/apps/example-nextjs/pages/_document.js
@@ -0,0 +1,62 @@
+/* eslint-disable react-native/no-inline-styles */
+import { Children } from 'react';
+import Document, { Html, Head, Main, NextScript } from 'next/document';
+import { AppRegistry } from 'react-native';
+
+// Follows the setup for react-native-web:
+// https://necolas.github.io/react-native-web/docs/setup/#root-element
+// Plus additional React Native scroll and text parity styles for various
+// browsers.
+// Force Next-generated DOM elements to fill their parent's height
+const style = `
+html, body, #__next {
+ -webkit-overflow-scrolling: touch;
+}
+#__next {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+html {
+ scroll-behavior: smooth;
+ -webkit-text-size-adjust: 100%;
+}
+body {
+ /* Allows you to scroll below the viewport; default value is visible */
+ overflow-y: auto;
+ overscroll-behavior-y: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -ms-overflow-style: scrollbar;
+}
+`;
+
+export default class MyDocument extends Document {
+ static async getInitialProps({ renderPage }) {
+ AppRegistry.registerComponent('main', () => Main);
+ // @ts-expect-error
+ const { getStyleElement } = AppRegistry.getApplication('main');
+ const page = await renderPage();
+ const styles = [
+ ,
+ getStyleElement(),
+ ];
+ return { ...page, styles: Children.toArray(styles) };
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/apps/example-nextjs/pages/index.js b/apps/example-nextjs/pages/index.js
new file mode 100644
index 000000000..07b6d6eb5
--- /dev/null
+++ b/apps/example-nextjs/pages/index.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import TrackPlayerApp from '../components/TrackPlayerApp';
+
+export default function App() {
+ return ;
+}
diff --git a/apps/example-nextjs/public/favicon.ico b/apps/example-nextjs/public/favicon.ico
new file mode 100644
index 000000000..5b2799b9b
Binary files /dev/null and b/apps/example-nextjs/public/favicon.ico differ
diff --git a/apps/example-nextjs/react-native-shim.js b/apps/example-nextjs/react-native-shim.js
new file mode 100644
index 000000000..5ff58bf24
--- /dev/null
+++ b/apps/example-nextjs/react-native-shim.js
@@ -0,0 +1,2 @@
+// Shim to re-export react-native-web as react-native
+module.exports = require('react-native-web');
diff --git a/apps/example-nextjs/tsconfig.json b/apps/example-nextjs/tsconfig.json
new file mode 100644
index 000000000..925b94fd1
--- /dev/null
+++ b/apps/example-nextjs/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": ["../../tsconfig.json", "expo/tsconfig.base"],
+ "compilerOptions": {
+ "module": "esnext",
+ "incremental": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "noImplicitAny": false
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"]
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index f6949333c..8586bbacc 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,7 +1,6 @@
import { fixupConfigRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
-import tseslint from '@typescript-eslint/eslint-plugin';
import prettier from 'eslint-plugin-prettier';
import { defineConfig } from 'eslint/config';
import path from 'node:path';
@@ -20,10 +19,8 @@ export default defineConfig([
{
plugins: {
prettier,
- '@typescript-eslint': tseslint,
},
rules: {
- '@typescript-eslint/no-explicit-any': 'error',
'react/react-in-jsx-scope': 'off',
'prettier/prettier': [
'error',
@@ -31,16 +28,25 @@ export default defineConfig([
quoteProps: 'consistent',
singleQuote: true,
tabWidth: 2,
- trailingComma: 'es5',
+ trailingComma: 'all',
useTabs: false,
},
],
},
},
+ {
+ files: ['src/**/*.ts', 'src/**/*.tsx', 'web/**/*.ts', 'web/**/*.tsx'],
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'error',
+ },
+ },
{
ignores: [
- 'node_modules/',
- 'lib/'
+ '**/node_modules/',
+ 'lib/',
+ '**/ios/',
+ '**/android/',
+ '**/.bundle/',
],
},
]);
diff --git a/example/babel.config.js b/example/babel.config.js
deleted file mode 100644
index 486a09304..000000000
--- a/example/babel.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const path = require('path');
-const { getConfig } = require('react-native-builder-bob/babel-config');
-const pkg = require('../package.json');
-
-const root = path.resolve(__dirname, '..');
-
-module.exports = getConfig(
- {
- presets: ['module:@react-native/babel-preset'],
- },
- { root, pkg }
-);
diff --git a/example/metro.config.js b/example/metro.config.js
deleted file mode 100644
index 2da198e82..000000000
--- a/example/metro.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-const path = require('path');
-const { getDefaultConfig } = require('@react-native/metro-config');
-const { withMetroConfig } = require('react-native-monorepo-config');
-
-const root = path.resolve(__dirname, '..');
-
-/**
- * Metro configuration
- * https://facebook.github.io/metro/docs/configuration
- *
- * @type {import('metro-config').MetroConfig}
- */
-module.exports = withMetroConfig(getDefaultConfig(__dirname), {
- root,
- dirname: __dirname,
-});
diff --git a/package.json b/package.json
index 56b2d814f..30f7b8120 100644
--- a/package.json
+++ b/package.json
@@ -50,17 +50,23 @@
}
],
"scripts": {
- "android:format": "cd example/android && ./gradlew :react-native-track-player:format",
- "android:ide": "cd example/android && open -a 'Android Studio' .",
+ "android:ide": "yarn workspace example-native android:ide",
"android:logs": "react-native log-android",
"build": "yarn prepare",
- "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
- "example": "yarn workspace react-native-track-player-example",
- "format": "prettier --write src web",
- "ios:rebuild": "cd example/ios && pod install && cd .. && yarn ios",
+ "build:watch": "tsc --watch --project tsconfig.build.json --outDir lib --declaration false --noEmit false",
+ "clean": "del-cli android/build lib && yarn workspace example-native clean",
+ "example": "yarn workspace example-native",
+ "ios:rebuild": "yarn workspace example-native ios:rebuild",
"ios:format": "swiftformat ios --swiftversion 5.9",
- "ios:ide": "cd example/ios && open TrackPlayerExample.xcworkspace",
- "lint": "eslint src web --max-warnings=0",
+ "ios:ide": "yarn workspace example-native ios:ide",
+ "format": "prettier --write src web",
+ "format:all": "yarn workspaces foreach --all run format",
+ "format:check": "prettier --check src web",
+ "format:check:all": "yarn workspaces foreach --all run format:check",
+ "lint": "eslint --fix src web",
+ "lint:all": "yarn workspaces foreach --all run lint",
+ "lint:check": "eslint src web --max-warnings=0",
+ "lint:check:all": "yarn workspaces foreach --all run lint:check",
"prepare": "bob build",
"release": "release-it --only-version",
"test": "jest",
@@ -131,7 +137,7 @@
},
"funding": "https://github.com/doublesymmetry/react-native-track-player?sponsor=1",
"workspaces": [
- "example"
+ "apps/*"
],
"packageManager": "yarn@4.10.3",
"engines": {
@@ -140,7 +146,7 @@
"jest": {
"preset": "react-native",
"modulePathIgnorePatterns": [
- "/example/node_modules",
+ "/apps/*/node_modules",
"/lib/"
]
},
diff --git a/src/NativeTrackPlayer.web.ts b/src/NativeTrackPlayer.web.ts
index fe6df51cc..1a2f50f47 100644
--- a/src/NativeTrackPlayer.web.ts
+++ b/src/NativeTrackPlayer.web.ts
@@ -1,3 +1,2 @@
-const module = require('../web').default;
-export const Constants = module?.getConstants();
-export default module;
+import webModule from '../web';
+export default webModule;
diff --git a/src/features/activeTrack.ts b/src/features/activeTrack.ts
index 620403595..4c0041085 100644
--- a/src/features/activeTrack.ts
+++ b/src/features/activeTrack.ts
@@ -1,4 +1,3 @@
-
import TrackPlayer from '../NativeTrackPlayer';
import { useUpdatedNativeValue } from '../utils/useUpdatedNativeValue';
import type { Track } from './queue';
diff --git a/src/features/mediaProvider.ts b/src/features/mediaProvider.ts
index 928935dac..d588d8064 100644
--- a/src/features/mediaProvider.ts
+++ b/src/features/mediaProvider.ts
@@ -27,7 +27,7 @@ export type AndroidAutoGetSearchResultsEvent = {
/** The search query */
query: string;
/** Optional search parameters */
- extras?: Record;
+ extras?: Record;
/** Page number for pagination */
page: number;
/** Maximum items per page */
@@ -82,7 +82,7 @@ function onGetChildren(
function onGetSearchResults(
callback: (event: {
query: string;
- extras?: Record;
+ extras?: Record;
page: number;
pageSize: number;
}) => Promise<{
@@ -90,16 +90,15 @@ function onGetSearchResults(
total?: number;
}>,
): () => void {
- return TrackPlayer.onGetSearchResultRequest(
- async ({ requestId, ...data }: AndroidAutoGetSearchResultsEvent) => {
- const { results, total } = await callback(data);
- TrackPlayer.resolveSearchResultRequest(
- requestId,
- results,
- total ?? results.length,
- );
- },
- ).remove;
+ return TrackPlayer.onGetSearchResultRequest(async (event: unknown) => {
+ const { requestId, ...data } = event as AndroidAutoGetSearchResultsEvent;
+ const { results, total } = await callback(data);
+ TrackPlayer.resolveSearchResultRequest(
+ requestId,
+ results,
+ total ?? results.length,
+ );
+ }).remove;
}
export interface MediaProvider {
@@ -111,7 +110,7 @@ export interface MediaProvider {
}) => Promise<{ children: Track[]; total: number }>;
search?: (event: {
query: string;
- extras?: Record;
+ extras?: Record;
page: number;
pageSize: number;
}) => Promise<{ results: Track[]; total: number }>;
diff --git a/src/features/options.ts b/src/features/options.ts
index b4fb7ac04..caf081249 100644
--- a/src/features/options.ts
+++ b/src/features/options.ts
@@ -192,6 +192,7 @@ export interface IOSUpdateOptions {
* });
* ```
*/
+// FIXME: shouldn't this just be a `DeepPartial`??? Also, it isn't being used anywhere
export interface UpdateOptions {
/** Android-specific configuration options */
android?: Partial;
diff --git a/src/features/remoteControls.ts b/src/features/remoteControls.ts
index 1f5f34355..25a36c2d1 100644
--- a/src/features/remoteControls.ts
+++ b/src/features/remoteControls.ts
@@ -4,7 +4,7 @@ import { skipToNext, skipToPrevious } from './queue';
// MARK: - Handlers State
-const customHandlers = new Map();
+const customHandlers = new Map();
// MARK: - Event Interfaces
@@ -66,7 +66,6 @@ export interface RemoteSkipEvent {
index: number;
}
-
// MARK: - Default Handlers
// Install remote control handlers with default behavior immediately when module loads
@@ -119,7 +118,8 @@ TrackPlayer.onRemoteStop(() => {
});
// Seek controls
-TrackPlayer.onRemoteSeek((event: any) => {
+TrackPlayer.onRemoteSeek((e: unknown) => {
+ const event = e as RemoteSeekEvent;
const customHandler = customHandlers.get('seek');
if (customHandler) {
customHandler(event);
@@ -128,7 +128,8 @@ TrackPlayer.onRemoteSeek((event: any) => {
}
});
-TrackPlayer.onRemoteJumpForward((event: any) => {
+TrackPlayer.onRemoteJumpForward((e: unknown) => {
+ const event = e as RemoteJumpForwardEvent;
const customHandler = customHandlers.get('jumpForward');
if (customHandler) {
customHandler(event);
@@ -137,7 +138,8 @@ TrackPlayer.onRemoteJumpForward((event: any) => {
}
});
-TrackPlayer.onRemoteJumpBackward((event: any) => {
+TrackPlayer.onRemoteJumpBackward((e: unknown) => {
+ const event = e as RemoteJumpBackwardEvent;
const customHandler = customHandlers.get('jumpBackward');
if (customHandler) {
customHandler(event);
@@ -409,4 +411,3 @@ export function onRemoteSkip(
export function onRemoteStop(callback: () => void): () => void {
return TrackPlayer.onRemoteStop(callback).remove;
}
-
diff --git a/tsconfig.build.json b/tsconfig.build.json
index b04e5c9c0..91a0cba44 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -1,4 +1,4 @@
{
"extends": "./tsconfig",
- "exclude": ["example", "lib", "docs"]
+ "exclude": ["apps", "lib", "docs"]
}
diff --git a/tsconfig.json b/tsconfig.json
index e310aa8e7..24b3e6b6d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,5 +26,5 @@
"target": "ESNext",
"verbatimModuleSyntax": true
},
- "exclude": ["lib", "docs"]
+ "exclude": ["lib", "docs", "apps"]
}
diff --git a/web/TrackPlayer/Event.ts b/web/TrackPlayer/Event.ts
new file mode 100644
index 000000000..35ce578f2
--- /dev/null
+++ b/web/TrackPlayer/Event.ts
@@ -0,0 +1,11 @@
+// Web-specific event constants
+export enum Event {
+ PlaybackState = 'playback-state',
+ PlaybackProgressUpdated = 'playback-progress-updated',
+ PlaybackQueueEnded = 'playback-queue-ended',
+ PlaybackPlayWhenReadyChanged = 'playback-play-when-ready-changed',
+ PlaybackActiveTrackChanged = 'playback-active-track-changed',
+ PlaybackError = 'playback-error',
+ PlaybackRepeatModeChanged = 'playback-repeat-mode-changed',
+ PlaybackOptionsChanged = 'playback-options-changed',
+}
diff --git a/web/TrackPlayer/Player.ts b/web/TrackPlayer/Player.ts
index 86c26c6bf..a8cb47a80 100644
--- a/web/TrackPlayer/Player.ts
+++ b/web/TrackPlayer/Player.ts
@@ -1,6 +1,12 @@
-import { State } from '../../src/constants/State';
-import type { PlaybackState, Progress, Track } from '../../src/types';
+import { State } from './State';
+import type {
+ PlaybackState,
+ Progress,
+ Track,
+ State as StateType,
+} from '../../src/features';
import { SetupNotCalledError } from './SetupNotCalledError';
+import type shaka from 'shaka-player/dist/shaka-player.ui';
export class Player {
protected hasInitialized = false;
@@ -19,10 +25,10 @@ export class Player {
}
// state getter/setter
- public get state(): PlaybackState {
+ protected get state(): PlaybackState {
return this._state;
}
- public set state(newState: PlaybackState) {
+ protected set state(newState: PlaybackState) {
this._state = newState;
}
@@ -55,8 +61,10 @@ export class Player {
this.state = {
state: State.Error,
error: {
- code: 'not_supported',
- message: 'Browser not supported.',
+ error: {
+ code: 'not_supported',
+ message: 'Browser not supported...',
+ },
},
};
throw new Error('Browser not supported.');
@@ -65,6 +73,7 @@ export class Player {
// build dom element and attach shaka-player
this.element = document.createElement('audio');
this.element.setAttribute('id', 'react-native-track-player');
+ document.body.appendChild(this.element);
this.player = new shaka.Player();
this.player?.attach(this.element);
@@ -98,6 +107,8 @@ export class Player {
this.player!.addEventListener('buffering', ({ buffering }: any) => {
if (buffering === true) {
this.onStateUpdate(State.Buffering);
+ } else {
+ this.onStateUpdate(State.Ready);
}
});
@@ -110,7 +121,7 @@ export class Player {
/**
* event handlers
*/
- protected onStateUpdate(state: Exclude) {
+ protected onStateUpdate(state: Exclude) {
this.state = { state };
}
@@ -120,8 +131,10 @@ export class Player {
this.state = {
state: State.Error,
error: {
- code: error.code.toString(),
- message: error.message,
+ error: {
+ code: error.code.toString(),
+ message: error.message,
+ },
},
};
@@ -130,36 +143,49 @@ export class Player {
}
/**
- * player control
+ * NOTE: this method is sync despite the actual load being async. This
+ * behavior is intentional as it mirrors what happens in Android. State
+ * changes should be captured by event listeners.
*/
- public async load(track: Track) {
- if (!this.player) throw new SetupNotCalledError();
- await this.player.load(track.url as string);
- this.current = track;
- }
-
- public async retry() {
+ public load(track: Track, onComplete?: (track: Track) => void) {
if (!this.player) throw new SetupNotCalledError();
- this.player.retryStreaming();
+ this.player.load(track.url as string).then(() => {
+ this.current = track;
+ onComplete?.(track);
+ });
}
- public async stop() {
+ /**
+ * NOTE: this method is sync despite the actual load being async. This
+ * behavior is intentional as it mirrors what happens in Android. State
+ * changes should be captured by event listeners.
+ */
+ public stop(onComplete?: () => void) {
if (!this.player) throw new SetupNotCalledError();
this.current = undefined;
- await this.player.unload();
+ this.player.unload().then(() => onComplete?.());
}
+ /**
+ * NOTE: this method is sync despite the actual load being async. This
+ * behavior is intentional as it mirrors what happens in Android. State
+ * changes should be captured by event listeners.
+ */
public play() {
if (!this.element) throw new SetupNotCalledError();
this.playWhenReady = true;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return this.element.play().catch((err: any) => console.error(err));
+ this.element.play().catch((err: unknown) => console.error(err));
+ }
+
+ public retry() {
+ if (!this.player) throw new SetupNotCalledError();
+ this.player.retryStreaming();
}
public pause() {
if (!this.element) throw new SetupNotCalledError();
this.playWhenReady = false;
- return this.element.pause();
+ this.element.pause();
}
public togglePlayback() {
diff --git a/web/TrackPlayer/PlaylistPlayer.ts b/web/TrackPlayer/PlaylistPlayer.ts
index 1990227fd..be0866023 100644
--- a/web/TrackPlayer/PlaylistPlayer.ts
+++ b/web/TrackPlayer/PlaylistPlayer.ts
@@ -1,7 +1,11 @@
import { Player } from './Player';
+import { State } from './State';
-import { State } from '../../src/constants/State';
-import type { Track } from '../../src/types';
+import type {
+ Track,
+ State as StateType,
+ RepeatMode as RepeatModeType,
+} from '../../src/features';
import { RepeatMode } from './RepeatMode';
export class PlaylistPlayer extends Player {
@@ -9,33 +13,33 @@ export class PlaylistPlayer extends Player {
protected playlist: Track[] = [];
protected lastIndex?: number;
protected _currentIndex?: number;
- protected repeatMode: RepeatMode = RepeatMode.Off;
+ protected repeatMode: RepeatModeType = RepeatMode.Off;
- protected async onStateUpdate(state: Exclude) {
+ protected onStateUpdate(state: Exclude) {
super.onStateUpdate(state);
if (state === State.Ended) {
- await this.onTrackEnded();
+ this.onTrackEnded();
}
}
- protected async onTrackEnded() {
+ protected onTrackEnded() {
switch (this.repeatMode) {
case RepeatMode.Track:
if (this.currentIndex !== undefined) {
- await this.goToIndex(this.currentIndex);
+ this.goToIndex(this.currentIndex);
}
break;
case RepeatMode.Playlist:
if (this.currentIndex === this.playlist.length - 1) {
- await this.goToIndex(0);
+ this.goToIndex(0);
} else {
- await this.skipToNext();
+ this.skipToNext();
}
break;
default:
try {
- await this.skipToNext();
+ this.skipToNext();
} catch (err) {
if ((err as Error).message !== 'playlist_exhausted') {
throw err;
@@ -59,28 +63,32 @@ export class PlaylistPlayer extends Player {
this._currentIndex = current;
}
- protected async goToIndex(index: number, initialPosition?: number) {
+ protected goToIndex(index: number, initialPosition?: number) {
const track = this.playlist[index];
if (!track) {
throw new Error('playlist_exhausted');
}
- if (this.currentIndex !== index) {
- this.currentIndex = index;
- await this.load(track);
- }
+ const onCompletedLoading = () => {
+ if (initialPosition) {
+ this.seekTo(initialPosition);
+ }
- if (initialPosition) {
- this.seekTo(initialPosition);
- }
+ if (this.playWhenReady) {
+ this.play();
+ }
+ };
- if (this.playWhenReady) {
- await this.play();
+ if (this.currentIndex !== index) {
+ this.currentIndex = index;
+ this.load(track, onCompletedLoading);
+ } else {
+ onCompletedLoading();
}
}
- public async add(tracks: Track[], insertBeforeIndex?: number) {
+ public add(tracks: Track[], insertBeforeIndex?: number) {
if (insertBeforeIndex !== -1 && insertBeforeIndex !== undefined) {
this.playlist.splice(insertBeforeIndex, 0, ...tracks);
} else {
@@ -88,40 +96,40 @@ export class PlaylistPlayer extends Player {
}
if (this.currentIndex === undefined) {
- await this.goToIndex(0);
+ this.goToIndex(0);
}
}
- public async skip(index: number, initialPosition?: number) {
+ public skip(index: number, initialPosition?: number) {
const track = this.playlist[index];
if (track === undefined) {
throw new Error('index out of bounds');
}
- await this.goToIndex(index, initialPosition);
+ this.goToIndex(index, initialPosition);
}
- public async skipToNext(initialPosition?: number) {
+ public skipToNext(initialPosition?: number) {
if (this.currentIndex === undefined) return;
const index = this.currentIndex + 1;
- await this.goToIndex(index, initialPosition);
+ this.goToIndex(index, initialPosition);
}
- public async skipToPrevious(initialPosition?: number) {
+ public skipToPrevious(initialPosition?: number) {
if (this.currentIndex === undefined) return;
const index = this.currentIndex - 1;
- await this.goToIndex(index, initialPosition);
+ this.goToIndex(index, initialPosition);
}
- public getTrack(index: number): Track | null {
+ public getTrack(index: number): Track | undefined {
const track = this.playlist[index];
- return track || null;
+ return track;
}
- public setRepeatMode(mode: RepeatMode) {
+ public setRepeatMode(mode: RepeatModeType) {
this.repeatMode = mode;
}
@@ -129,7 +137,7 @@ export class PlaylistPlayer extends Player {
return this.repeatMode;
}
- public async remove(indexes: number[]) {
+ public remove(indexes: number[]) {
const idxMap = indexes.reduce>((acc, elem) => {
acc[elem] = true;
return acc;
@@ -151,28 +159,31 @@ export class PlaylistPlayer extends Player {
const hasItems = this.playlist.length > 0;
if (isCurrentRemoved && hasItems) {
- await this.goToIndex(this.currentIndex % this.playlist.length);
+ this.goToIndex(this.currentIndex % this.playlist.length);
} else if (isCurrentRemoved) {
- await this.stop();
+ this.stop();
}
}
- public async stop() {
- await super.stop();
- this.currentIndex = undefined;
+ public stop(onComplete?: () => void) {
+ super.stop(() => {
+ this.currentIndex = undefined;
+ onComplete?.();
+ });
}
- public async reset() {
- await this.stop();
- this.playlist = [];
+ public reset() {
+ this.stop(() => {
+ this.playlist = [];
+ });
}
- public async removeUpcomingTracks() {
+ public removeUpcomingTracks() {
if (this.currentIndex === undefined) return;
this.playlist = this.playlist.slice(0, this.currentIndex + 1);
}
- public async move(fromIndex: number, toIndex: number): Promise {
+ public move(fromIndex: number, toIndex: number): void {
if (!this.playlist[fromIndex]) {
throw new Error('index out of bounds');
}
diff --git a/web/TrackPlayer/RepeatMode.ts b/web/TrackPlayer/RepeatMode.ts
index 6d917ad99..684a45e6a 100644
--- a/web/TrackPlayer/RepeatMode.ts
+++ b/web/TrackPlayer/RepeatMode.ts
@@ -1,5 +1,7 @@
-export enum RepeatMode {
- Off = 'off',
- Track = 'track',
- Playlist = 'queue',
-}
+import type { RepeatMode as RepeatModeType } from '../../src/features';
+
+export const RepeatMode = {
+ Off: 'off' as const,
+ Track: 'track' as const,
+ Playlist: 'queue' as const,
+} satisfies Record;
diff --git a/web/TrackPlayer/State.ts b/web/TrackPlayer/State.ts
new file mode 100644
index 000000000..11cb31155
--- /dev/null
+++ b/web/TrackPlayer/State.ts
@@ -0,0 +1,13 @@
+import type { State as StateType } from '../../src/features';
+
+export const State = {
+ None: 'none' as const,
+ Ready: 'ready' as const,
+ Playing: 'playing' as const,
+ Paused: 'paused' as const,
+ Stopped: 'stopped' as const,
+ Loading: 'loading' as const,
+ Buffering: 'buffering' as const,
+ Error: 'error' as const,
+ Ended: 'ended' as const,
+} satisfies Record;
diff --git a/web/TrackPlayerModule.ts b/web/TrackPlayerModule.ts
index 5dacf8be3..730c72d1b 100644
--- a/web/TrackPlayerModule.ts
+++ b/web/TrackPlayerModule.ts
@@ -1,80 +1,66 @@
import { DeviceEventEmitter } from 'react-native';
-import { State } from '../src/constants/State';
-
-// Web-specific event constants
-const Event = {
- PlaybackState: 'playback-state',
- PlaybackProgressUpdated: 'playback-progress-updated',
- PlaybackQueueEnded: 'playback-queue-ended',
- PlaybackPlayWhenReadyChanged: 'playback-play-when-ready-changed',
- PlaybackActiveTrackChanged: 'playback-active-track-changed',
-};
+import type {
+ PlaybackErrorEvent,
+ RepeatMode as RepeatModeType,
+} from '../src/features';
+import { State } from './TrackPlayer/State';
+import { Event } from './TrackPlayer/Event';
+
import type { Spec } from '../src/NativeTrackPlayer';
-import type { PlaybackState, Track, UpdateOptions } from '../src/types';
+import type {
+ Options,
+ PlaybackProgressUpdatedEvent,
+ PlaybackQueueEndedEvent,
+ PlaybackState,
+ PlayingState,
+ RepeatModeChangedEvent,
+ Track,
+ UpdateOptions,
+} from '../src/features';
import { PlaylistPlayer, RepeatMode } from './TrackPlayer';
import { SetupNotCalledError } from './TrackPlayer/SetupNotCalledError';
export class TrackPlayerModule extends PlaylistPlayer implements Spec {
protected emitter = DeviceEventEmitter;
protected progressUpdateEventInterval: NodeJS.Timeout | undefined;
+ protected options: Options = {
+ forwardJumpInterval: 15,
+ backwardJumpInterval: 15,
+ progressUpdateEventInterval: null,
+ repeatMode: RepeatMode.Off,
+ capabilities: [], // irrelevant in web-world
+ };
- public getConstants() {
- return {
- // Capabilities
- CAPABILITY_PLAY: 'play',
- CAPABILITY_PLAY_FROM_ID: 'play-from-id',
- CAPABILITY_PLAY_FROM_SEARCH: 'play-from-search',
- CAPABILITY_PAUSE: 'pause',
- CAPABILITY_STOP: 'stop',
- CAPABILITY_SEEK_TO: 'seek-to',
- CAPABILITY_SKIP: 'skip',
- CAPABILITY_SKIP_TO_NEXT: 'skip-to-next',
- CAPABILITY_SKIP_TO_PREVIOUS: 'skip-to-previous',
- CAPABILITY_SET_RATING: 'set-rating',
- CAPABILITY_JUMP_FORWARD: 'jump-forward',
- CAPABILITY_JUMP_BACKWARD: 'jump-backward',
-
- // Rating Types
- RATING_HEART: 'heart',
- RATING_THUMBS_UP_DOWN: 'thumbs-up-down',
- RATING_3_STARS: '3-stars',
- RATING_4_STARS: '4-stars',
- RATING_5_STARS: '5-stars',
- RATING_PERCENTAGE: 'percentage',
-
- // Pitch Algorithms
- PITCH_ALGORITHM_LINEAR: 'linear',
- PITCH_ALGORITHM_MUSIC: 'music',
- PITCH_ALGORITHM_VOICE: 'voice',
-
- // States
- STATE_BUFFERING: 'STATE_BUFFERING',
- STATE_LOADING: 'STATE_LOADING',
- STATE_NONE: 'STATE_NONE',
- STATE_PAUSED: 'STATE_PAUSED',
- STATE_PLAYING: 'STATE_PLAYING',
- STATE_READY: 'STATE_READY',
- STATE_STOPPED: 'STATE_STOPPED',
-
- // Repeat Modes
- REPEAT_OFF: RepeatMode.Off,
- REPEAT_TRACK: RepeatMode.Track,
- REPEAT_QUEUE: RepeatMode.Playlist,
- };
+ private addStubListener() {
+ return this.emitter.addListener('_', () => {});
}
// observe and emit state changes
- public get state(): PlaybackState {
+ protected get state(): PlaybackState {
return super.state;
}
- public set state(newState: PlaybackState) {
+ protected set state(newState: PlaybackState) {
+ const didStateChange = newState.state !== super.state.state;
+ const didErrorChange =
+ newState.state === State.Error && super.state.state === State.Error
+ ? newState.error === super.state.error
+ : false;
+
super.state = newState;
- this.emitter.emit(Event.PlaybackState, newState);
- }
- public async updateOptions(options: UpdateOptions) {
- this.setupProgressUpdates(options.progressUpdateEventInterval);
+ if (!didStateChange && !didErrorChange) {
+ return;
+ }
+
+ // emit stage change events
+ this.emitter.emit(Event.PlaybackState, newState);
+ if (newState.state === State.Error) {
+ const event: PlaybackErrorEvent = {
+ error: newState.error.error,
+ };
+ this.emitter.emit(Event.PlaybackError, event);
+ }
}
protected setupProgressUpdates(interval?: number) {
@@ -82,13 +68,14 @@ export class TrackPlayerModule extends PlaylistPlayer implements Spec {
this.clearUpdateEventInterval();
if (interval) {
this.clearUpdateEventInterval();
- this.progressUpdateEventInterval = setInterval(async () => {
+ this.progressUpdateEventInterval = setInterval(() => {
if (this.state.state === State.Playing) {
- const progress = await this.getProgress();
- this.emitter.emit(Event.PlaybackProgressUpdated, {
+ const progress = this.getProgress();
+ const event: PlaybackProgressUpdatedEvent = {
...progress,
- track: this.currentIndex,
- });
+ track: this.currentIndex || 0,
+ };
+ this.emitter.emit(Event.PlaybackProgressUpdated, event);
}
}, interval * 1000);
}
@@ -100,19 +87,185 @@ export class TrackPlayerModule extends PlaylistPlayer implements Spec {
}
}
- protected async onPlaylistEnded() {
- await super.onPlaylistEnded();
+ protected onPlaylistEnded() {
+ super.onPlaylistEnded();
this.emitter.emit(Event.PlaybackQueueEnded, {
- track: this.currentIndex,
+ track: this.currentIndex ?? 0,
position: this.element!.currentTime,
});
}
- public get playWhenReady(): boolean {
- return super.playWhenReady;
+ /****************************************
+ * MARK: init and config
+ ****************************************/
+ // setupPlayer is inherited from Player
+
+ public updateOptions(options: UpdateOptions) {
+ this.options = {
+ ...this.options,
+ ...(options as Omit),
+ };
+ this.setupProgressUpdates(options.progressUpdateEventInterval);
+ this.emitter.emit(Event.PlaybackOptionsChanged, options);
+ }
+
+ public getOptions() {
+ return this.options;
+ }
+
+ /****************************************
+ * MARK: events
+ ****************************************/
+ public onAndroidControllerConnected() {
+ return this.addStubListener();
+ }
+ public onAndroidControllerDisconnected() {
+ return this.addStubListener();
+ }
+ public onMetadataChapterReceived() {
+ return this.addStubListener();
+ }
+ public onMetadataCommonReceived() {
+ return this.addStubListener();
+ }
+ public onMetadataTimedReceived() {
+ return this.addStubListener();
+ }
+
+ public onPlaybackActiveTrackChanged(callback: (event: object) => void) {
+ return this.emitter.addListener(Event.PlaybackActiveTrackChanged, callback);
+ }
+
+ public onPlaybackError(callback: (event: { error?: unknown }) => void) {
+ return this.emitter.addListener(Event.PlaybackError, callback);
+ }
+
+ public onPlaybackMetadata() {
+ return this.addStubListener();
+ }
+
+ public onPlaybackPlayWhenReadyChanged(
+ callback: (event: { playWhenReady: boolean }) => void,
+ ) {
+ return this.emitter.addListener(
+ Event.PlaybackPlayWhenReadyChanged,
+ callback,
+ );
}
- public set playWhenReady(pwr: boolean) {
+ public onPlaybackPlayingState(callback: (state: PlayingState) => void) {
+ return this.emitter.addListener(
+ Event.PlaybackState,
+ (state: PlaybackState) => {
+ return callback(this.getPlayingState(state));
+ },
+ );
+ }
+
+ public onPlaybackProgressUpdated(
+ callback: (event: PlaybackProgressUpdatedEvent) => void,
+ ) {
+ return this.emitter.addListener(Event.PlaybackProgressUpdated, callback);
+ }
+
+ public onPlaybackQueueEnded(
+ callback: (event: PlaybackQueueEndedEvent) => void,
+ ) {
+ return this.emitter.addListener(Event.PlaybackQueueEnded, callback);
+ }
+
+ public onPlaybackRepeatModeChanged(
+ callback: (event: RepeatModeChangedEvent) => void,
+ ) {
+ return this.emitter.addListener(Event.PlaybackRepeatModeChanged, callback);
+ }
+
+ public onPlaybackState(callback: (state: PlaybackState) => void) {
+ return this.emitter.addListener(Event.PlaybackState, callback);
+ }
+
+ public onRemoteBookmark() {
+ return this.addStubListener();
+ }
+ public onRemoteDislike() {
+ return this.addStubListener();
+ }
+ public onRemoteJumpBackward() {
+ return this.addStubListener();
+ }
+ public onRemoteJumpForward() {
+ return this.addStubListener();
+ }
+ public onRemoteLike() {
+ return this.addStubListener();
+ }
+ public onRemoteNext() {
+ return this.addStubListener();
+ }
+ public onRemotePause() {
+ return this.addStubListener();
+ }
+ public onRemotePlay() {
+ return this.addStubListener();
+ }
+ public onRemotePlayId() {
+ return this.addStubListener();
+ }
+ public onRemotePlaySearch() {
+ return this.addStubListener();
+ }
+ public onRemotePrevious() {
+ return this.addStubListener();
+ }
+ public onRemoteSeek() {
+ return this.addStubListener();
+ }
+ public onRemoteSetRating() {
+ return this.addStubListener();
+ }
+ public onRemoteSkip() {
+ return this.addStubListener();
+ }
+ public onRemoteStop() {
+ return this.addStubListener();
+ }
+
+ public onOptionsChanged(callback: (event: Options) => void) {
+ return this.emitter.addListener(Event.PlaybackOptionsChanged, callback);
+ }
+
+ /****************************************
+ * MARK: player api
+ ****************************************/
+ public load(track: Track, onComplete?: (track: Track) => void) {
+ if (!this.element) throw new SetupNotCalledError();
+ const lastTrack = this.current;
+ const lastPosition = this.element.currentTime;
+ super.load(track, () => {
+ onComplete?.(track);
+ this.emitter.emit(Event.PlaybackActiveTrackChanged, {
+ lastTrack,
+ lastPosition,
+ lastIndex: this.lastIndex,
+ index: this.currentIndex,
+ track,
+ });
+ });
+ }
+
+ // reset is inherited from PlaylistPlayer
+
+ // play is inherited from Player
+
+ // pause is inherited from Player
+
+ public togglePlayback() {
+ return super.togglePlayback();
+ }
+
+ // stop is inherited from PlaylistPlayer
+
+ public setPlayWhenReady(pwr: boolean) {
const didChange = pwr !== this._playWhenReady;
super.playWhenReady = pwr;
@@ -121,84 +274,129 @@ export class TrackPlayerModule extends PlaylistPlayer implements Spec {
playWhenReady: this._playWhenReady,
});
}
+
+ return super.playWhenReady;
}
- public async getPlayWhenReady(): Promise {
- return this.playWhenReady;
+ public getPlayWhenReady(): boolean {
+ return super.playWhenReady;
}
- public async setPlayWhenReady(pwr: boolean): Promise {
- this.playWhenReady = pwr;
- return this.playWhenReady;
+ // seekTo is inherited from Player
+
+ // seekBy is inherited from Player
+
+ // setVolume is inherited from Player
+
+ // getVolume is inherited from Player
+
+ // setRate is inherited from Player
+
+ // getRate is inherited from Player
+
+ // getProgress is inherited from Player
+
+ public getPlaybackState(): PlaybackState {
+ return this.state;
}
- public async load(track: Track) {
- if (!this.element) throw new SetupNotCalledError();
- const lastTrack = this.current;
- const lastPosition = this.element.currentTime;
- await super.load(track);
-
- this.emitter.emit(Event.PlaybackActiveTrackChanged, {
- lastTrack,
- lastPosition,
- lastIndex: this.lastIndex,
- index: this.currentIndex,
- track,
- });
+ public getPlayingState(state?: PlaybackState): PlayingState {
+ const curState = state ? state.state : this.state.state;
+ return {
+ playing: curState === State.Playing,
+ buffering: curState === State.Buffering,
+ };
}
- public async getQueue(): Promise