expo-gliph-player 1.0.0 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # 🎵 expo-gliph-player
2
2
 
3
- [![npm version](https://shields.io)](https://npmjs.com)
4
- [![License: MIT](https://shields.io)](https://opensource.org)
5
-
6
- **Fixed version of react-native-gliph-player with full Expo compatibility.** All iOS bridge fixes, Swift/Objective-C issues, and Android `autoSkipOnError` patches are pre-applied.
3
+ [![npm version](https://img.shields.io/npm/v/expo-gliph-player)](https://www.npmjs.com/package/expo-gliph-player)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
5
 
6
+ **Fixed version of react-native-gliph-player with full Expo compatibility.**
7
+ A high-performance, New Architecture (JSI / TurboModules) compatible audio player for React Native. Built for developers who need absolute control over audio playback, background services, and system integration without fighting complex APIs.
8
8
  ---
9
9
 
10
10
  ## 📦 Installation
@@ -18,7 +18,7 @@ yarn add expo-gliph-player
18
18
  ### From GitHub (Alternative):
19
19
 
20
20
  ```bash
21
- yarn add https://github.com
21
+ yarn add https://github.com/alex303606/expo-gliph-player
22
22
  ```
23
23
 
24
24
  ---
@@ -56,59 +56,271 @@ npx expo run:ios
56
56
  # or
57
57
  npx expo run:android
58
58
  ```
59
-
60
59
  ---
61
60
 
62
- ## 💻 Reactive State
61
+ ## 🚀 Full Implementation Example
63
62
 
64
- Instead of manually fetching state, Gliph Player ships with powerful React Hooks. These hooks automatically re-render your components whenever the audio state changes.
63
+ Want to skip straight to the code? Here is a complete, production-ready implementation containing both the main app setup (`App.tsx`) and the user interface (`MusicPlayer.tsx`).
65
64
 
66
- ### Play/Pause State:
65
+ ### 1. App.tsx (Main Entry & Background Engine Setup)
67
66
 
68
67
  ```tsx
69
- import { useIsPlaying } from 'expo-gliph-player';
68
+ import React, { useEffect } from 'react';
69
+ import { View, Platform, PermissionsAndroid } from 'react-native';
70
+ import GliphPlayer, { Capability, AppKilledPlaybackBehavior } from 'expo-gliph-player';
71
+ import { MusicPlayer } from './components/MusicPlayer';
72
+
73
+ const tracks = [
74
+ {
75
+ id: '1',
76
+ url: 'https://example.com',
77
+ title: 'Gliph Journey',
78
+ artist: 'Gliph Labs',
79
+ artwork: 'https://example.com',
80
+ },
81
+ ];
82
+
83
+ export default function App() {
84
+ useEffect(() => {
85
+ const setup = async () => {
86
+ // Android Notification Permission (API 33+)
87
+ if (Platform.OS === 'android' && Platform.Version >= 33) {
88
+ await PermissionsAndroid.request('android.permission.POST_NOTIFICATIONS' as any);
89
+ }
70
90
 
71
- const { playing } = useIsPlaying(); // Returns true or false
91
+ await GliphPlayer.setupPlayer({
92
+ playBuffer: 0.5, // 0.5s buffer for zero-lag seeking
93
+ android: {
94
+ appKilledPlaybackBehavior: AppKilledPlaybackBehavior.ContinuePlayback,
95
+ },
96
+ });
97
+
98
+ await GliphPlayer.updateOptions({
99
+ capabilities: [
100
+ Capability.Play, Capability.Pause,
101
+ Capability.SkipToNext, Capability.SkipToPrevious,
102
+ Capability.SeekTo,
103
+ ],
104
+ });
105
+
106
+ await GliphPlayer.add(tracks);
107
+ };
108
+
109
+ setup();
110
+ }, []);
111
+
112
+ return (
113
+ <View style={{ flex: 1, backgroundColor: '#121212' }}>
114
+ <MusicPlayer />
115
+ </View>
116
+ );
117
+ }
72
118
  ```
73
119
 
74
- ### Progress & Duration (in seconds):
120
+ ### 2. MusicPlayer.tsx (UI Component)
75
121
 
76
122
  ```tsx
77
- import { useProgress } from 'expo-gliph-player';
123
+ import React, { useState } from 'react';
124
+ import { View, Text, TouchableOpacity, Image, StyleSheet } from 'react-native';
125
+ import Slider from '@react-native-community/slider';
126
+ import GliphPlayer, { usePlaybackState, useProgress, useActiveTrack, State, RepeatMode } from 'expo-gliph-player';
127
+
128
+ export const MusicPlayer = () => {
129
+ const { state } = usePlaybackState();
130
+ const { position, duration } = useProgress(500);
131
+ const track = useActiveTrack();
132
+ const [repeatMode, setRepeatMode] = useState('off');
133
+
134
+ const togglePlayback = async () => {
135
+ if (state === State.Playing) {
136
+ await GliphPlayer.pause();
137
+ } else {
138
+ await GliphPlayer.play();
139
+ }
140
+ };
141
+
142
+ const cycleRepeat = async () => {
143
+ const next = repeatMode === 'off' ? 'one' : repeatMode === 'one' ? 'all' : 'off';
144
+ setRepeatMode(next);
145
+ await GliphPlayer.setRepeatMode(
146
+ next === 'one' ? RepeatMode.Track : next === 'all' ? RepeatMode.Queue : RepeatMode.Off
147
+ );
148
+ };
149
+
150
+ return (
151
+ <View style={styles.container}>
152
+ <Image source={{ uri: track?.artwork }} style={styles.artwork} />
153
+ <Text style={styles.title}>{track?.title || 'No Track'}</Text>
154
+
155
+ <Slider
156
+ style={{ width: '100%', height: 40 }}
157
+ value={position}
158
+ maximumValue={duration || 1}
159
+ onSlidingComplete={(val) => GliphPlayer.seekTo(val)}
160
+ minimumTrackTintColor="#1DB954"
161
+ />
162
+
163
+ <View style={styles.controls}>
164
+ <TouchableOpacity onPress={() => GliphPlayer.skipToPrevious()}>
165
+ <Text style={styles.btn}>Prev</Text>
166
+ </TouchableOpacity>
167
+
168
+ <TouchableOpacity onPress={togglePlayback} style={styles.playBtn}>
169
+ <Text style={{ color: '#fff' }}>{state === State.Playing ? 'PAUSE' : 'PLAY'}</Text>
170
+ </TouchableOpacity>
171
+
172
+ <TouchableOpacity onPress={() => GliphPlayer.skipToNext()}>
173
+ <Text style={styles.btn}>Next</Text>
174
+ </TouchableOpacity>
175
+ </View>
176
+
177
+ <TouchableOpacity onPress={cycleRepeat} style={{ marginTop: 20 }}>
178
+ <Text style={{ color: '#1DB954' }}>Repeat: {repeatMode.toUpperCase()}</Text>
179
+ </TouchableOpacity>
180
+ </View>
181
+ );
182
+ };
78
183
 
79
- const { position, duration } = useProgress(500); // Updates every 500ms
184
+ const styles = StyleSheet.create({
185
+ container: { padding: 20, alignItems: 'center', justifyContent: 'center', flex: 1 },
186
+ artwork: { width: 300, height: 300, borderRadius: 10 },
187
+ title: { color: '#fff', fontSize: 24, marginVertical: 20 },
188
+ controls: { flexDirection: 'row', alignItems: 'center', gap: 40, marginTop: 20 },
189
+ playBtn: { width: 80, height: 80, borderRadius: 40, backgroundColor: '#333', justifyContent: 'center', alignItems: 'center' },
190
+ btn: { color: '#fff', fontSize: 18 }
191
+ });
80
192
  ```
193
+ ---
194
+
195
+ ## 💻 Complete Integration
196
+
197
+ Let's put the pieces together. Using react-native-gliph-player requires two steps: initializing the background service, and then actually rendering your UI.
198
+
199
+ ### Step 1: Setting up the Player
81
200
 
82
- ### "Now Playing" Track Metadata:
201
+ You should initialize the player as soon as your app mounts (usually in App.tsx). You configure the buffer size, define what buttons show on the lock screen, and add your initial tracks.
83
202
 
84
203
  ```tsx
85
- import { useActiveTrack } from 'expo-gliph-player';
204
+ import React, { useEffect } from 'react';
205
+ import { Platform, PermissionsAndroid } from 'react-native';
206
+ import GliphPlayer, { Capability, AppKilledPlaybackBehavior } from 'react-native-gliph-player';
207
+
208
+ const myTracks = [
209
+ {
210
+ id: '1',
211
+ url: 'https://example.com/audio.mp3',
212
+ title: 'Awesome Song',
213
+ artist: 'Gliph Labs',
214
+ artwork: 'https://example.com/cover.jpg', // Shows on lock screen
215
+ }
216
+ ];
86
217
 
87
- const track = useActiveTrack(); // Contains url, title, artist, artwork
218
+ export default function App() {
219
+ useEffect(() => {
220
+ const initializeAudio = async () => {
221
+ // 1. Request Android 13+ Notification Permission
222
+ if (Platform.OS === 'android' && Platform.Version >= 33) {
223
+ await PermissionsAndroid.request('android.permission.POST_NOTIFICATIONS' as any);
224
+ }
225
+
226
+ // 2. Setup the engine
227
+ await GliphPlayer.setupPlayer({
228
+ playBuffer: 0.5, // 0.5s buffer for zero-lag seeking
229
+ android: {
230
+ // Keep playing even if user swipes app away
231
+ appKilledPlaybackBehavior: AppKilledPlaybackBehavior.ContinuePlayback,
232
+ },
233
+ });
234
+
235
+ // 3. Define Lock Screen Controls
236
+ await GliphPlayer.updateOptions({
237
+ capabilities: [
238
+ Capability.Play, Capability.Pause,
239
+ Capability.SkipToNext, Capability.SkipToPrevious,
240
+ Capability.SeekTo,
241
+ ],
242
+ });
243
+
244
+ // 4. Add tracks to queue
245
+ await GliphPlayer.add(myTracks);
246
+ };
247
+
248
+ initializeAudio();
249
+ }, []);
250
+
251
+ return <YourMainUI />;
252
+ }
88
253
  ```
89
-
90
254
  ---
91
255
 
92
- ## 🎛️ Playback & Queue Controls
256
+ ## 🔧 Controlling Playback
257
+
258
+ Now that the player is initialized, you can control it from anywhere in your app using the GliphPlayer API.
93
259
 
94
260
  ### Basic Controls
95
261
 
262
+ Building play, pause, next, and previous buttons is straightforward. All commands are async promises.
263
+
96
264
  ```tsx
97
- await GliphPlayer.play();
98
- await GliphPlayer.pause();
99
- await GliphPlayer.stop();
265
+ import GliphPlayer from 'react-native-gliph-player';
266
+ import { TouchableOpacity, Text, View } from 'react-native';
267
+
268
+ export function PlayerControls() {
269
+ return (
270
+ <View style={{ flexDirection: 'row', gap: 20 }}>
271
+ <TouchableOpacity onPress={() => GliphPlayer.skipToPrevious()}>
272
+ <Text>Prev</Text>
273
+ </TouchableOpacity>
274
+
275
+ <TouchableOpacity onPress={() => GliphPlayer.play()}>
276
+ <Text>Play</Text>
277
+ </TouchableOpacity>
278
+
279
+ <TouchableOpacity onPress={() => GliphPlayer.pause()}>
280
+ <Text>Pause</Text>
281
+ </TouchableOpacity>
282
+
283
+ <TouchableOpacity onPress={() => GliphPlayer.skipToNext()}>
284
+ <Text>Next</Text>
285
+ </TouchableOpacity>
286
+ </View>
287
+ );
288
+ }
100
289
  ```
101
290
 
102
- ### Navigation & Seeking
291
+ ### Seeking and Jumping
292
+
293
+ You can seek to a specific second, or jump forward/backward by an offset.
103
294
 
104
295
  ```tsx
105
- await GliphPlayer.skipToNext();
106
- await GliphPlayer.skipToPrevious();
107
- await GliphPlayer.seekTo(60); // Jump to exactly 1 minute (in seconds)
108
- await GliphPlayer.seekBy(15); // Jump forward 15 seconds (great for podcasts)
296
+ // Jump to exactly 1 minute in
297
+ <TouchableOpacity onPress={() => GliphPlayer.seekTo(60)}>
298
+ <Text>Go to 1:00</Text>
299
+ </TouchableOpacity>
300
+
301
+ // Jump forward 15 seconds (great for podcasts)
302
+ <TouchableOpacity onPress={() => GliphPlayer.seekBy(15)}>
303
+ <Text>+15s</Text>
304
+ </TouchableOpacity>
109
305
  ```
110
306
 
111
- ### Managing the Queue
307
+ ### Seeking and Jumping
308
+
309
+ You can seek to a specific second, or jump forward/backward by an offset.
310
+
311
+ ```tsx
312
+ // Jump to exactly 1 minute in
313
+ <TouchableOpacity onPress={() => GliphPlayer.seekTo(60)}>
314
+ <Text>Go to 1:00</Text>
315
+ </TouchableOpacity>
316
+
317
+ // Jump forward 15 seconds (great for podcasts)
318
+ <TouchableOpacity onPress={() => GliphPlayer.seekBy(15)}>
319
+ <Text>+15s</Text>
320
+ </TouchableOpacity>
321
+ ```
322
+
323
+ ## Managing the Queue
112
324
 
113
325
  You can dynamically add, remove, or reorder tracks while the audio is playing.
114
326
 
@@ -116,12 +328,12 @@ You can dynamically add, remove, or reorder tracks while the audio is playing.
116
328
  // Add a track to the end of the queue
117
329
  await GliphPlayer.add({
118
330
  id: 'new-song',
119
- url: 'https://example.com',
331
+ url: 'https://example.com/new.mp3',
120
332
  title: 'Just Added',
121
333
  artist: 'User'
122
334
  });
123
335
 
124
- // Remove a specific track by ID
336
+ // Remove a specific track
125
337
  await GliphPlayer.remove('track-1');
126
338
 
127
339
  // Stop playback and empty the queue entirely
@@ -130,184 +342,142 @@ await GliphPlayer.reset();
130
342
 
131
343
  ---
132
344
 
133
- ## 🔒 Lock Screen & Background Events
345
+ ## 🎛️ Reactive State
134
346
 
135
- To ensure your lock screen and notification panel controls actually respond to user input, you must connect native remote events to the player engine.
347
+ Instead of manually fetching state, Gliph Player ships with powerful React Hooks. These hooks automatically re-render your components whenever the audio state changes.
136
348
 
137
- ### Inside Components (`useTrackPlayerEvents`)
349
+ ### Building a Play/Pause Button
138
350
 
139
- ```tsx
140
- import { useTrackPlayerEvents, Event } from 'expo-gliph-player';
141
- import GliphPlayer from 'expo-gliph-player';
351
+ The useIsPlaying() hook makes it incredibly easy to toggle a play/pause icon.
142
352
 
143
- export function PlaybackObserver() {
144
- useTrackPlayerEvents([
145
- Event.RemotePlay,
146
- Event.RemotePause,
147
- Event.RemoteNext,
148
- Event.RemotePrevious
149
- ], (event) => {
150
- if (event.type === Event.RemotePlay) GliphPlayer.play();
151
- if (event.type === Event.RemotePause) GliphPlayer.pause();
152
- if (event.type === Event.RemoteNext) GliphPlayer.skipToNext();
153
- if (event.type === Event.RemotePrevious) GliphPlayer.skipToPrevious();
154
- });
353
+ ```tsx
354
+ import GliphPlayer, { useIsPlaying } from 'react-native-gliph-player';
355
+ import { TouchableOpacity, Text } from 'react-native';
356
+
357
+ export function PlayPauseButton() {
358
+ const { playing } = useIsPlaying();
359
+
360
+ const toggle = async () => {
361
+ if (playing) {
362
+ await GliphPlayer.pause();
363
+ } else {
364
+ await GliphPlayer.play();
365
+ }
366
+ };
155
367
 
156
- return null; // Headless component, acts as a layout listener
368
+ return (
369
+ <TouchableOpacity onPress={toggle} style={{ padding: 20, backgroundColor: '#333' }}>
370
+ <Text style={{ color: '#fff' }}>{playing ? 'PAUSE' : 'PLAY'}</Text>
371
+ </TouchableOpacity>
372
+ );
157
373
  }
158
374
  ```
159
375
 
160
- ### Global Listeners (`GliphPlayer.addEventListener`)
376
+ ### Building a Progress Slider
161
377
 
162
- To handle audio events even when your React UI components are unmounted, register global listeners inside your app root (`index.js` or `App.tsx` outside the component lifecycle):
378
+ The useProgress() hook returns the current position and duration. It updates automatically so your slider moves smoothly in real-time.
163
379
 
164
380
  ```tsx
165
- import GliphPlayer, { Event } from 'expo-gliph-player';
166
-
167
- GliphPlayer.addEventListener(Event.RemotePlay, () => {
168
- GliphPlayer.play();
169
- });
381
+ import GliphPlayer, { useProgress } from 'react-native-gliph-player';
382
+ import Slider from '@react-native-community/slider';
170
383
 
171
- GliphPlayer.addEventListener(Event.RemotePause, () => {
172
- GliphPlayer.pause();
173
- });
384
+ export function ProgressBar() {
385
+ // Updates 2 times a second (500ms)
386
+ const { position, duration } = useProgress(500);
387
+
388
+ return (
389
+ <Slider
390
+ style={{ width: '100%', height: 40 }}
391
+ value={position}
392
+ maximumValue={duration || 1}
393
+ onSlidingComplete={(value) => GliphPlayer.seekTo(value)}
394
+ minimumTrackTintColor="#1DB954"
395
+ maximumTrackTintColor="#ffffff"
396
+ />
397
+ );
398
+ }
174
399
  ```
175
400
 
176
- ---
177
-
178
- ## 🚀 Full Implementation Example
179
-
180
- Here is a production-ready, complete example split into the main entry file (`App.tsx`) and the user interface component (`MusicPlayer.tsx`).
401
+ ### Showing "Now Playing"
181
402
 
182
- ### 1. App.tsx (Main Entry & Background Engine Setup)
403
+ The useActiveTrack() hook returns the data for the song currently playing. It changes automatically when a song skips.
183
404
 
184
405
  ```tsx
185
- import React, { useEffect } from 'react';
186
- import { View, Platform, PermissionsAndroid } from 'react-native';
187
- import GliphPlayer, { Capability, AppKilledPlaybackBehavior } from 'expo-gliph-player';
188
- import { MusicPlayer } from './components/MusicPlayer';
406
+ import { useActiveTrack } from 'react-native-gliph-player';
407
+ import { View, Image, Text } from 'react-native';
189
408
 
190
- const tracks = [
191
- {
192
- id: '1',
193
- url: 'https://example.com',
194
- title: 'Gliph Journey',
195
- artist: 'Gliph Labs',
196
- artwork: 'https://example.com',
197
- },
198
- ];
409
+ export function NowPlaying() {
410
+ const track = useActiveTrack();
199
411
 
200
- export default function App() {
201
- useEffect(() => {
202
- const setup = async () => {
203
- // 1. Android Notification Permission (API 33+)
204
- if (Platform.OS === 'android' && Platform.Version >= 33) {
205
- await PermissionsAndroid.request('android.permission.POST_NOTIFICATIONS' as any);
206
- }
412
+ if (!track) return <Text>Nothing playing</Text>;
207
413
 
208
- // 2. Setup the engine
209
- await GliphPlayer.setupPlayer({
210
- playBuffer: 0.5, // 0.5s buffer for zero-lag seeking
211
- android: {
212
- appKilledPlaybackBehavior: AppKilledPlaybackBehavior.ContinuePlayback,
213
- },
214
- });
215
-
216
- // 3. Define Lock Screen / Notification Controls
217
- await GliphPlayer.updateOptions({
218
- capabilities: [
219
- Capability.Play, Capability.Pause,
220
- Capability.SkipToNext, Capability.SkipToPrevious,
221
- Capability.SeekTo,
222
- ],
223
- });
414
+ return (
415
+ <View style={{ alignItems: 'center' }}>
416
+ <Image source={{ uri: track.artwork }} style={{ width: 200, height: 200 }} />
417
+ <Text style={{ fontSize: 24, fontWeight: 'bold' }}>{track.title}</Text>
418
+ <Text style={{ fontSize: 18, color: 'gray' }}>{track.artist}</Text>
419
+ </View>
420
+ );
421
+ }
422
+ ```
224
423
 
225
- // 4. Add tracks to queue
226
- await GliphPlayer.add(tracks);
227
- };
424
+ ---
228
425
 
229
- setup();
230
- }, []);
426
+ ## 🔒 Background & Lock Screen
231
427
 
232
- return (
233
- <View style={{ flex: 1, backgroundColor: '#121212' }}>
234
- <MusicPlayer />
235
- </View>
236
- );
237
- }
238
- ```
428
+ One of the biggest advantages of Gliph Player is that audio keeps playing when the app is minimized. The system lock screen and notification panel will show your track artwork and controls automatically.
239
429
 
240
- ### 2. MusicPlayer.tsx (UI Component)
430
+ ### Lock Screen Buttons
431
+ The native OS provides the lock screen controls, but you need to tell Gliph Player how to respond when a user taps "Next" on their lock screen. If you don't listen for these events, the lock screen buttons won't do anything!
432
+ We provide a special hook `useTrackPlayerEvents` to capture these native remote events cleanly inside your components.
241
433
 
242
434
  ```tsx
243
- import React, { useState } from 'react';
244
- import { View, Text, TouchableOpacity, Image, StyleSheet } from 'react-native';
245
- import Slider from '@react-native-community/slider';
246
- import GliphPlayer, { useIsPlaying, useProgress, useActiveTrack, RepeatMode } from 'expo-gliph-player';
435
+ import { useTrackPlayerEvents, Event } from 'expo-gliph-player';
436
+ import GliphPlayer from 'expo-gliph-player';
247
437
 
248
- export const MusicPlayer = () => {
249
- const { playing } = useIsPlaying();
250
- const { position, duration } = useProgress(500);
251
- const track = useActiveTrack();
252
- const [repeatMode, setRepeatMode] = useState('off');
253
-
254
- const togglePlayback = async () => {
255
- if (playing) {
256
- await GliphPlayer.pause();
257
- } else {
258
- await GliphPlayer.play();
438
+ export function PlaybackObserver() {
439
+ // Listen for native remote control events
440
+ useTrackPlayerEvents([
441
+ Event.RemotePlay,
442
+ Event.RemotePause,
443
+ Event.RemoteNext,
444
+ Event.RemotePrevious
445
+ ], (event) => {
446
+ if (event.type === Event.RemotePlay) {
447
+ GliphPlayer.play();
448
+ }
449
+
450
+ if (event.type === Event.RemotePause) {
451
+ GliphPlayer.pause();
259
452
  }
260
- };
453
+
454
+ if (event.type === Event.RemoteNext) {
455
+ GliphPlayer.skipToNext();
456
+ }
457
+
458
+ if (event.type === Event.RemotePrevious) {
459
+ GliphPlayer.skipToPrevious();
460
+ }
461
+ });
261
462
 
262
- const cycleRepeat = async () => {
263
- const next = repeatMode === 'off' ? 'one' : repeatMode === 'one' ? 'all' : 'off';
264
- setRepeatMode(next);
265
- await GliphPlayer.setRepeatMode(
266
- next === 'one' ? RepeatMode.Track : next === 'all' ? RepeatMode.Queue : RepeatMode.Off
267
- );
268
- };
463
+ // This component doesn't need to render anything visual
464
+ return null;
465
+ }
466
+ ```
269
467
 
270
- return (
271
- <View style={styles.container}>
272
- <Image source={{ uri: track?.artwork }} style={styles.artwork} />
273
- <Text style={styles.title}>{track?.title || 'No Track'}</Text>
274
-
275
- <Slider
276
- style={{ width: '100%', height: 40 }}
277
- value={position}
278
- maximumValue={duration || 1}
279
- onSlidingComplete={(val) => GliphPlayer.seekTo(val)}
280
- minimumTrackTintColor="#1DB954"
281
- />
282
-
283
- <View style={styles.controls}>
284
- <TouchableOpacity onPress={() => GliphPlayer.skipToPrevious()}>
285
- <Text style={styles.btn}>Prev</Text>
286
- </TouchableOpacity>
287
-
288
- <TouchableOpacity onPress={togglePlayback} style={styles.playBtn}>
289
- <Text style={{ color: '#fff' }}>{playing ? 'PAUSE' : 'PLAY'}</Text>
290
- </TouchableOpacity>
468
+ ### Background Services (Advanced)
469
+ If you want to handle events globally even when your React UI is completely unmounted, you can register listeners outside of the component tree (e.g. in your `index.js` or `App.tsx` file outside the component lifecycle) using `GliphPlayer.addEventListener()`.
291
470
 
292
- <TouchableOpacity onPress={() => GliphPlayer.skipToNext()}>
293
- <Text style={styles.btn}>Next</Text>
294
- </TouchableOpacity>
295
- </View>
471
+ ```tsx
472
+ import GliphPlayer, { Event } from 'expo-gliph-player';
296
473
 
297
- <TouchableOpacity onPress={cycleRepeat} style={{ marginTop: 20 }}>
298
- <Text style={{ color: '#1DB954' }}>Repeat: {repeatMode.toUpperCase()}</Text>
299
- </TouchableOpacity>
300
- </View>
301
- );
302
- };
474
+ // In index.js or App.tsx (outside component)
475
+ GliphPlayer.addEventListener(Event.RemotePlay, () => {
476
+ GliphPlayer.play();
477
+ });
303
478
 
304
- const styles = StyleSheet.create({
305
- container: { padding: 20, alignItems: 'center', justifyContent: 'center', flex: 1 },
306
- artwork: { width: 300, height: 300, borderRadius: 10 },
307
- title: { color: '#fff', fontSize: 24, marginVertical: 20 },
308
- controls: { flexDirection: 'row', alignItems: 'center', gap: 40, marginTop: 20 },
309
- playBtn: { width: 80, height: 80, borderRadius: 40, backgroundColor: '#333', justifyContent: 'center', alignItems: 'center' },
310
- btn: { color: '#fff', fontSize: 18 }
479
+ GliphPlayer.addEventListener(Event.RemotePause, () => {
480
+ GliphPlayer.pause();
311
481
  });
312
482
  ```
313
483
 
@@ -43,6 +43,22 @@ public typealias EventEmitter = (String, [String: Any]?) -> Void
43
43
  super.init()
44
44
  }
45
45
 
46
+ // ── Thread helper ───────────────────────────────────────────────────────────
47
+
48
+ /// Executes `block` synchronously on the main thread.
49
+ /// Safe to call from any thread — avoids deadlock if already on main.
50
+ /// Several AVAudioSession / UIApplication / MPRemoteCommandCenter APIs are
51
+ /// documented as main-thread-only; the RN bridge does not guarantee that
52
+ /// native module methods run on main, so every touch of those APIs goes
53
+ /// through this helper.
54
+ private func runOnMain(_ block: () -> Void) {
55
+ if Thread.isMainThread {
56
+ block()
57
+ } else {
58
+ DispatchQueue.main.sync(execute: block)
59
+ }
60
+ }
61
+
46
62
  // ── Setup ───────────────────────────────────────────────────────────────────
47
63
 
48
64
  @objc public func setupPlayer(
@@ -52,29 +68,39 @@ public typealias EventEmitter = (String, [String: Any]?) -> Void
52
68
  ) {
53
69
  guard !isSetup else { resolve(nil); return }
54
70
 
55
- self.options = options
56
- self.progressInterval = (options["progressUpdateEventInterval"] as? Double) ?? 1.0
71
+ var setupError: Error?
57
72
 
58
- do {
59
- let session = AVAudioSession.sharedInstance()
60
- let category = mapIOSCategory(options["iosCategory"] as? String)
61
- let mode = mapIOSMode(options["iosCategoryMode"] as? String)
62
- let opts = mapIOSOptions(options["iosCategoryOptions"] as? [String])
73
+ runOnMain {
74
+ self.options = options
75
+ self.progressInterval = (options["progressUpdateEventInterval"] as? Double) ?? 1.0
76
+
77
+ do {
78
+ let session = AVAudioSession.sharedInstance()
79
+ let category = self.mapIOSCategory(options["iosCategory"] as? String)
80
+ let mode = self.mapIOSMode(options["iosCategoryMode"] as? String)
81
+ let opts = self.mapIOSOptions(options["iosCategoryOptions"] as? [String])
82
+
83
+ try session.setCategory(category, mode: mode, options: opts)
84
+ try session.setActive(true)
85
+ } catch {
86
+ setupError = error
87
+ return
88
+ }
63
89
 
64
- try session.setCategory(category, mode: mode, options: opts)
65
- try session.setActive(true)
66
- } catch {
90
+ self.player = AVQueuePlayer()
91
+ self.player?.allowsExternalPlayback = false
92
+ self.player?.automaticallyWaitsToMinimizeStalling = (options["waitForBuffer"] as? Bool) ?? true
93
+
94
+ self.setupRemoteCommands()
95
+ self.setupNotificationObservers()
96
+ self.isSetup = true
97
+ }
98
+
99
+ if let error = setupError {
67
100
  reject("setup_error", "Failed to configure audio session: \(error.localizedDescription)", error)
68
101
  return
69
102
  }
70
103
 
71
- player = AVQueuePlayer()
72
- player?.allowsExternalPlayback = false
73
- player?.automaticallyWaitsToMinimizeStalling = (options["waitForBuffer"] as? Bool) ?? true
74
-
75
- setupRemoteCommands()
76
- setupNotificationObservers()
77
- isSetup = true
78
104
  resolve(nil)
79
105
  }
80
106
 
@@ -89,7 +115,9 @@ public typealias EventEmitter = (String, [String: Any]?) -> Void
89
115
  currentIndex = -1
90
116
  isSetup = false
91
117
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
92
- UIApplication.shared.endReceivingRemoteControlEvents()
118
+ runOnMain {
119
+ UIApplication.shared.endReceivingRemoteControlEvents()
120
+ }
93
121
  }
94
122
 
95
123
  @objc public func isReady() -> Bool { return isSetup }
@@ -345,7 +373,9 @@ public typealias EventEmitter = (String, [String: Any]?) -> Void
345
373
  reject: @escaping RCTPromiseRejectBlock
346
374
  ) {
347
375
  options = opts
348
- setupRemoteCommands()
376
+ runOnMain {
377
+ self.setupRemoteCommands()
378
+ }
349
379
  resolve(nil)
350
380
  }
351
381
 
@@ -426,6 +456,10 @@ public typealias EventEmitter = (String, [String: Any]?) -> Void
426
456
 
427
457
  // ── Remote commands ─────────────────────────────────────────────────────────
428
458
 
459
+ /// Must always be called on the main thread (see `runOnMain` call sites:
460
+ /// `setupPlayer` and `updateOptions`). `MPRemoteCommandCenter` and
461
+ /// `UIApplication.beginReceivingRemoteControlEvents()` are main-thread-only
462
+ /// APIs.
429
463
  private func setupRemoteCommands() {
430
464
  let center = MPRemoteCommandCenter.shared()
431
465
  UIApplication.shared.beginReceivingRemoteControlEvents()
@@ -694,4 +728,4 @@ public typealias EventEmitter = (String, [String: Any]?) -> Void
694
728
  }
695
729
  return opts
696
730
  }
697
- }
731
+ }
@@ -1,22 +1 @@
1
- //
2
- // expo_gliph_player.h
3
- //
4
- // Master header matching the pod's module name (dashes -> underscores).
5
- // Xcode's Swift-generated interop header (expo_gliph_player-Swift.h) hardcodes
6
- // a self-import of <expo_gliph_player/expo_gliph_player.h> for any module that
7
- // has DEFINES_MODULE = YES and contains Swift — this file only needs to EXIST
8
- // under that exact name for the build to find it.
9
- //
10
- // IMPORTANT: do NOT re-export GliphAudioPlayer.h from here (e.g. via
11
- // `#import "GliphAudioPlayer.h"`). The authoritative Objective-C interface
12
- // for the Swift `GliphAudioPlayer` class is auto-generated by the Swift
13
- // compiler itself, inside expo_gliph_player-Swift.h. GliphPlayerModule.mm
14
- // imports that generated header directly. If this file also pulls in a
15
- // second, hand-written declaration of the same class name, both end up in
16
- // the same translation unit and the build fails with:
17
- // "Duplicate interface definition for class 'GliphAudioPlayer'"
18
- // (which in turn cascades into unrelated-looking parse errors later in
19
- // GliphPlayerModule.mm, since Clang's parser gets confused after the
20
- // redefinition).
21
- //
22
1
  #import <Foundation/Foundation.h>
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "expo-gliph-player",
3
- "version": "1.0.0",
4
- "description": "Expo compatible audio player with iOS and Android support",
3
+ "author": {
4
+ "name": "Aleksey Ostrikov",
5
+ "email": "alex303606@gmail.com"
6
+ },
7
+ "version": "1.3.4",
8
+ "description": "Fixed version of react-native-gliph-player with Expo compatibility",
5
9
  "main": "app.plugin.js",
6
10
  "module": "lib/module/index.js",
7
11
  "types": "lib/typescript/src/index.d.ts",
@@ -17,22 +21,14 @@
17
21
  "expo-plugin"
18
22
  ],
19
23
  "license": "MIT",
20
- "repository": {
21
- "type": "git",
22
- "url": "https://github.com/alex303606/expo-gliph-player.git"
23
- },
24
- "bugs": {
25
- "url": "https://github.com/alex303606/expo-gliph-player/issues"
26
- },
27
- "homepage": "https://github.com/alex303606/expo-gliph-player#readme",
28
- "author": "alex303606 <alex303606@gmail.com>",
29
24
  "peerDependencies": {
30
25
  "expo": ">=50.0.0",
31
26
  "react": "*",
32
27
  "react-native": ">=0.71.0"
33
28
  },
34
29
  "dependencies": {
35
- "@expo/config-plugins": ">=7.0.0"
30
+ "@expo/config-plugins": ">=7.0.0",
31
+ "react-native-gliph-player": "^1.3.4"
36
32
  },
37
33
  "files": [
38
34
  "app.plugin.js",
@@ -41,16 +37,9 @@
41
37
  "android",
42
38
  "lib",
43
39
  "*.podspec",
44
- "!ios/**/*.xcodeproj",
45
- "!android/**/*.iml",
46
- "!android/build",
47
- "!android/.gradle",
48
40
  "LICENSE",
49
41
  "README.md"
50
42
  ],
51
- "scripts": {
52
- "prepublishOnly": "echo 'No build needed'"
53
- },
54
43
  "codegenConfig": {
55
44
  "name": "RNGliphPlayerSpec",
56
45
  "type": "modules",
package/src/types.ts CHANGED
@@ -220,8 +220,8 @@ export interface PlayerOptions {
220
220
  /** Android-specific options */
221
221
  android?: {
222
222
  appKilledPlaybackBehavior?: AppKilledPlaybackBehavior;
223
- audioContentType?: AndroidAudioContentType;
224
- audioUsage?: AndroidAudioUsage;
223
+ audioContentType?: AndroidAudioContentType | string;
224
+ audioUsage?: AndroidAudioUsage | string;
225
225
  autoSkipOnError?: boolean;
226
226
  };
227
227
  /** Progress update interval in seconds */
@@ -8,8 +8,6 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
 
10
10
  module.exports = function withGliphPlayer(config) {
11
- console.log('🔧 Starting expo-gliph-player plugin...');
12
-
13
11
  // ===== iOS: Info.plist =====
14
12
  config = withInfoPlist(config, (config) => {
15
13
  if (!config.modResults.UIBackgroundModes) {