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 +345 -175
- package/ios/GliphAudioPlayer.swift +54 -20
- package/ios/expo_gliph_player.h +0 -21
- package/package.json +8 -19
- package/src/types.ts +2 -2
- package/src/withGliphPlayer.js +0 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 🎵 expo-gliph-player
|
|
2
2
|
|
|
3
|
-
[](https://npmjs.com)
|
|
4
|
-
[](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
|
+
[](https://www.npmjs.com/package/expo-gliph-player)
|
|
4
|
+
[](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
|
-
##
|
|
61
|
+
## 🚀 Full Implementation Example
|
|
63
62
|
|
|
64
|
-
|
|
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
|
-
###
|
|
65
|
+
### 1. App.tsx (Main Entry & Background Engine Setup)
|
|
67
66
|
|
|
68
67
|
```tsx
|
|
69
|
-
import {
|
|
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
|
-
|
|
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
|
-
###
|
|
120
|
+
### 2. MusicPlayer.tsx (UI Component)
|
|
75
121
|
|
|
76
122
|
```tsx
|
|
77
|
-
import {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
-
##
|
|
345
|
+
## 🎛️ Reactive State
|
|
134
346
|
|
|
135
|
-
|
|
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
|
-
###
|
|
349
|
+
### Building a Play/Pause Button
|
|
138
350
|
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
376
|
+
### Building a Progress Slider
|
|
161
377
|
|
|
162
|
-
|
|
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, {
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
|
186
|
-
import { View,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
226
|
-
await GliphPlayer.add(tracks);
|
|
227
|
-
};
|
|
424
|
+
---
|
|
228
425
|
|
|
229
|
-
|
|
230
|
-
}, []);
|
|
426
|
+
## 🔒 Background & Lock Screen
|
|
231
427
|
|
|
232
|
-
|
|
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
|
-
###
|
|
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
|
|
244
|
-
import
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
</TouchableOpacity>
|
|
295
|
-
</View>
|
|
471
|
+
```tsx
|
|
472
|
+
import GliphPlayer, { Event } from 'expo-gliph-player';
|
|
296
473
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
);
|
|
302
|
-
};
|
|
474
|
+
// In index.js or App.tsx (outside component)
|
|
475
|
+
GliphPlayer.addEventListener(Event.RemotePlay, () => {
|
|
476
|
+
GliphPlayer.play();
|
|
477
|
+
});
|
|
303
478
|
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
56
|
-
self.progressInterval = (options["progressUpdateEventInterval"] as? Double) ?? 1.0
|
|
71
|
+
var setupError: Error?
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/ios/expo_gliph_player.h
CHANGED
|
@@ -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
|
-
"
|
|
4
|
-
|
|
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 */
|
package/src/withGliphPlayer.js
CHANGED
|
@@ -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) {
|