expo-gliph-player 1.0.0
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/LICENSE +21 -0
- package/README.md +348 -0
- package/android/build.gradle +82 -0
- package/android/src/main/AndroidManifest.xml +31 -0
- package/android/src/main/java/com/gliphplayer/DeviceInfoModule.kt +48 -0
- package/android/src/main/java/com/gliphplayer/DeviceInfoPackage.kt +16 -0
- package/android/src/main/java/com/gliphplayer/GliphPlayerModule.kt +360 -0
- package/android/src/main/java/com/gliphplayer/GliphPlayerPackage.kt +48 -0
- package/android/src/main/java/com/gliphplayer/GliphPlayerService.kt +797 -0
- package/android/src/main/res/xml/automotive_app_desc.xml +4 -0
- package/android/src/oldarch/com/gliphplayer/NativeGliphPlayerSpec.kt +56 -0
- package/app.plugin.js +1 -0
- package/expo-gliph-player.podspec +91 -0
- package/ios/GliphAudioPlayer.h +77 -0
- package/ios/GliphAudioPlayer.swift +697 -0
- package/ios/GliphPlayer-Bridging-Header.h +3 -0
- package/ios/GliphPlayerModule.h +12 -0
- package/ios/GliphPlayerModule.mm +243 -0
- package/ios/expo_gliph_player.h +22 -0
- package/lib/commonjs/GliphPlayer.js +310 -0
- package/lib/commonjs/GliphPlayer.js.map +1 -0
- package/lib/commonjs/hooks.js +233 -0
- package/lib/commonjs/hooks.js.map +1 -0
- package/lib/commonjs/index.js +166 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativeGliphPlayer.js +19 -0
- package/lib/commonjs/specs/NativeGliphPlayer.js.map +1 -0
- package/lib/commonjs/types.js +175 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/GliphPlayer.js +305 -0
- package/lib/module/GliphPlayer.js.map +1 -0
- package/lib/module/hooks.js +221 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/NativeGliphPlayer.js +20 -0
- package/lib/module/specs/NativeGliphPlayer.js.map +1 -0
- package/lib/module/types.js +181 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/GliphPlayer.d.ts +113 -0
- package/lib/typescript/src/GliphPlayer.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts +51 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeGliphPlayer.d.ts +80 -0
- package/lib/typescript/src/specs/NativeGliphPlayer.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +348 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +62 -0
- package/src/GliphPlayer.ts +356 -0
- package/src/hooks.ts +256 -0
- package/src/index.ts +61 -0
- package/src/specs/NativeGliphPlayer.ts +105 -0
- package/src/types.ts +395 -0
- package/src/withGliphPlayer.js +166 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeGliphPlayer.ts
|
|
3
|
+
*
|
|
4
|
+
* TurboModule spec for the New React Native Architecture (Codegen).
|
|
5
|
+
* This file is parsed by Codegen to generate C++/Java/ObjC bridge code.
|
|
6
|
+
* All types must be Codegen-compatible (no unions of complex types, etc.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TurboModule } from 'react-native';
|
|
10
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
11
|
+
|
|
12
|
+
// ─── Codegen-compatible track object ────────────────────────────────────────
|
|
13
|
+
export interface TrackObject {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
title: string;
|
|
17
|
+
artist: string;
|
|
18
|
+
album?: string;
|
|
19
|
+
artwork?: string;
|
|
20
|
+
duration?: number;
|
|
21
|
+
genre?: string;
|
|
22
|
+
date?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
rating?: number;
|
|
25
|
+
isLiveStream?: boolean;
|
|
26
|
+
headers?: Object;
|
|
27
|
+
pitchAlgorithm?: number;
|
|
28
|
+
userAgent?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Player options ──────────────────────────────────────────────────────────
|
|
32
|
+
export interface PlayerOptions {
|
|
33
|
+
minBuffer?: number;
|
|
34
|
+
maxBuffer?: number;
|
|
35
|
+
playBuffer?: number;
|
|
36
|
+
backBuffer?: number;
|
|
37
|
+
maxCacheSize?: number;
|
|
38
|
+
iosCategory?: string;
|
|
39
|
+
iosCategoryMode?: string;
|
|
40
|
+
iosCategoryOptions?: ReadonlyArray<string>;
|
|
41
|
+
waitForBuffer?: boolean;
|
|
42
|
+
autoHandleInterruptions?: boolean;
|
|
43
|
+
autoUpdateMetadata?: boolean;
|
|
44
|
+
android?: Object;
|
|
45
|
+
progressUpdateEventInterval?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── TurboModule spec ────────────────────────────────────────────────────────
|
|
49
|
+
export interface Spec extends TurboModule {
|
|
50
|
+
// Setup / teardown
|
|
51
|
+
setupPlayer(options: Object): Promise<void>;
|
|
52
|
+
destroy(): void;
|
|
53
|
+
isServiceRunning(): Promise<boolean>;
|
|
54
|
+
|
|
55
|
+
// Queue management
|
|
56
|
+
// NOTE: Codegen does not support optional number params — use -1 as sentinel for "not provided"
|
|
57
|
+
add(tracks: ReadonlyArray<Object>, insertBeforeIndex: number): Promise<number>;
|
|
58
|
+
remove(tracks: ReadonlyArray<string>): Promise<void>;
|
|
59
|
+
removeUpcomingTracks(): Promise<void>;
|
|
60
|
+
skip(index: number, initialPosition: number): Promise<void>;
|
|
61
|
+
skipToNext(initialPosition: number): Promise<void>;
|
|
62
|
+
skipToPrevious(initialPosition: number): Promise<void>;
|
|
63
|
+
move(fromIndex: number, toIndex: number): Promise<void>;
|
|
64
|
+
|
|
65
|
+
// Playback control
|
|
66
|
+
play(): Promise<void>;
|
|
67
|
+
pause(): Promise<void>;
|
|
68
|
+
stop(): Promise<void>;
|
|
69
|
+
reset(): Promise<void>;
|
|
70
|
+
seekTo(position: number): Promise<void>;
|
|
71
|
+
seekBy(offset: number): Promise<void>;
|
|
72
|
+
setVolume(volume: number): Promise<void>;
|
|
73
|
+
getVolume(): Promise<number>;
|
|
74
|
+
setRate(rate: number): Promise<void>;
|
|
75
|
+
getRate(): Promise<number>;
|
|
76
|
+
setRepeatMode(mode: number): Promise<void>;
|
|
77
|
+
getRepeatMode(): Promise<number>;
|
|
78
|
+
|
|
79
|
+
// Queue getters
|
|
80
|
+
// NOTE: Codegen does not support nullable primitives in return position.
|
|
81
|
+
// getActiveTrackIndex returns -1 when no track is active (sentinel value).
|
|
82
|
+
getQueue(): Promise<ReadonlyArray<Object>>;
|
|
83
|
+
getActiveTrackIndex(): Promise<number>;
|
|
84
|
+
getActiveTrack(): Promise<Object | null>;
|
|
85
|
+
getTrack(index: number): Promise<Object | null>;
|
|
86
|
+
getQueueSize(): Promise<number>;
|
|
87
|
+
|
|
88
|
+
// Playback state
|
|
89
|
+
getPlaybackState(): Promise<Object>;
|
|
90
|
+
getProgress(): Promise<Object>;
|
|
91
|
+
|
|
92
|
+
// Metadata update
|
|
93
|
+
updateMetadataForTrack(index: number, metadata: Object): Promise<void>;
|
|
94
|
+
clearNowPlayingMetadata(): Promise<void>;
|
|
95
|
+
updateNowPlayingMetadata(metadata: Object): Promise<void>;
|
|
96
|
+
|
|
97
|
+
// Capabilities / notification
|
|
98
|
+
updateOptions(options: Object): Promise<void>;
|
|
99
|
+
|
|
100
|
+
// Events (emitter support — required for TurboModule event emission)
|
|
101
|
+
addListener(eventType: string): void;
|
|
102
|
+
removeListeners(count: number): void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default TurboModuleRegistry.get<Spec>('RNGliphPlayer');
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Public TypeScript types for react-native-gliph-player
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ─── Track ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface Track {
|
|
8
|
+
/** Unique identifier for the track (auto-generated if not provided) */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** URL of the audio file (local or remote) */
|
|
11
|
+
url: string | number; // number = require('./file.mp3')
|
|
12
|
+
/** Track title */
|
|
13
|
+
title: string;
|
|
14
|
+
/** Artist name */
|
|
15
|
+
artist: string;
|
|
16
|
+
/** Album name */
|
|
17
|
+
album?: string;
|
|
18
|
+
/** Artwork URL or local require() */
|
|
19
|
+
artwork?: string | number;
|
|
20
|
+
/** Duration in seconds */
|
|
21
|
+
duration?: number;
|
|
22
|
+
/** Genre */
|
|
23
|
+
genre?: string;
|
|
24
|
+
/** Release date string */
|
|
25
|
+
date?: string;
|
|
26
|
+
/** Description / lyrics */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** Rating (0–1 or 0–5 depending on ratingType) */
|
|
29
|
+
rating?: number;
|
|
30
|
+
/** Whether this is a live stream */
|
|
31
|
+
isLiveStream?: boolean;
|
|
32
|
+
/** Custom HTTP headers for the audio request */
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
/** iOS pitch algorithm */
|
|
35
|
+
pitchAlgorithm?: PitchAlgorithm;
|
|
36
|
+
/** Custom user agent */
|
|
37
|
+
userAgent?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Enums ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export enum State {
|
|
43
|
+
/** No player setup yet */
|
|
44
|
+
None = 'none',
|
|
45
|
+
/** Player is ready but not playing */
|
|
46
|
+
Ready = 'ready',
|
|
47
|
+
/** Audio is playing */
|
|
48
|
+
Playing = 'playing',
|
|
49
|
+
/** Audio is paused */
|
|
50
|
+
Paused = 'paused',
|
|
51
|
+
/** Player is stopped */
|
|
52
|
+
Stopped = 'stopped',
|
|
53
|
+
/** Buffering / loading */
|
|
54
|
+
Buffering = 'buffering',
|
|
55
|
+
/** Loading initial data */
|
|
56
|
+
Loading = 'loading',
|
|
57
|
+
/** An error occurred */
|
|
58
|
+
Error = 'error',
|
|
59
|
+
/** End of queue reached */
|
|
60
|
+
Ended = 'ended',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export enum Event {
|
|
64
|
+
/** Playback state changed */
|
|
65
|
+
PlaybackState = 'playback-state',
|
|
66
|
+
/** Playback error */
|
|
67
|
+
PlaybackError = 'playback-error',
|
|
68
|
+
/** Active track changed */
|
|
69
|
+
PlaybackActiveTrackChanged = 'playback-active-track-changed',
|
|
70
|
+
/** Queue ended */
|
|
71
|
+
PlaybackQueueEnded = 'playback-queue-ended',
|
|
72
|
+
/** Track ended */
|
|
73
|
+
PlaybackTrackEnded = 'playback-track-ended',
|
|
74
|
+
/** Playback progress updated */
|
|
75
|
+
PlaybackProgressUpdated = 'playback-progress-updated',
|
|
76
|
+
/** Metadata received (e.g. from stream) */
|
|
77
|
+
PlaybackMetadataReceived = 'playback-metadata-received',
|
|
78
|
+
/** Repeat mode changed */
|
|
79
|
+
PlaybackRepeatModeChanged = 'playback-repeat-mode-changed',
|
|
80
|
+
|
|
81
|
+
// Remote control events (from notification / lock screen / headphones)
|
|
82
|
+
RemotePlay = 'remote-play',
|
|
83
|
+
RemotePause = 'remote-pause',
|
|
84
|
+
RemoteStop = 'remote-stop',
|
|
85
|
+
RemoteSkip = 'remote-skip',
|
|
86
|
+
RemoteNext = 'remote-next',
|
|
87
|
+
RemotePrevious = 'remote-previous',
|
|
88
|
+
RemoteJumpForward = 'remote-jump-forward',
|
|
89
|
+
RemoteJumpBackward = 'remote-jump-backward',
|
|
90
|
+
RemoteSeek = 'remote-seek',
|
|
91
|
+
RemoteSetRating = 'remote-set-rating',
|
|
92
|
+
RemoteDuck = 'remote-duck',
|
|
93
|
+
RemoteLike = 'remote-like',
|
|
94
|
+
RemoteDislike = 'remote-dislike',
|
|
95
|
+
RemoteBookmark = 'remote-bookmark',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export enum RepeatMode {
|
|
99
|
+
/** No repeat */
|
|
100
|
+
Off = 0,
|
|
101
|
+
/** Repeat current track */
|
|
102
|
+
Track = 1,
|
|
103
|
+
/** Repeat entire queue */
|
|
104
|
+
Queue = 2,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export enum Capability {
|
|
108
|
+
Play = 'play',
|
|
109
|
+
Pause = 'pause',
|
|
110
|
+
Stop = 'stop',
|
|
111
|
+
SeekTo = 'seek-to',
|
|
112
|
+
Skip = 'skip',
|
|
113
|
+
SkipToNext = 'skip-to-next',
|
|
114
|
+
SkipToPrevious = 'skip-to-previous',
|
|
115
|
+
JumpForward = 'jump-forward',
|
|
116
|
+
JumpBackward = 'jump-backward',
|
|
117
|
+
SetRating = 'set-rating',
|
|
118
|
+
Like = 'like',
|
|
119
|
+
Dislike = 'dislike',
|
|
120
|
+
Bookmark = 'bookmark',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export enum RatingType {
|
|
124
|
+
Heart = 'heart',
|
|
125
|
+
ThumbsUpDown = 'thumbs-up-down',
|
|
126
|
+
ThreeStars = '3-stars',
|
|
127
|
+
FourStars = '4-stars',
|
|
128
|
+
FiveStars = '5-stars',
|
|
129
|
+
Percentage = 'percentage',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export enum PitchAlgorithm {
|
|
133
|
+
/** Best quality, higher CPU */
|
|
134
|
+
Quality = 0,
|
|
135
|
+
/** Balanced */
|
|
136
|
+
Consistency = 1,
|
|
137
|
+
/** Lowest CPU */
|
|
138
|
+
Varispeed = 2,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export enum IOSCategory {
|
|
142
|
+
Playback = 'playback',
|
|
143
|
+
PlayAndRecord = 'playAndRecord',
|
|
144
|
+
MultiRoute = 'multiRoute',
|
|
145
|
+
Ambient = 'ambient',
|
|
146
|
+
SoloAmbient = 'soloAmbient',
|
|
147
|
+
Record = 'record',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export enum IOSCategoryMode {
|
|
151
|
+
Default = 'default',
|
|
152
|
+
GameChat = 'gameChat',
|
|
153
|
+
Measurement = 'measurement',
|
|
154
|
+
MoviePlayback = 'moviePlayback',
|
|
155
|
+
SpokenAudio = 'spokenAudio',
|
|
156
|
+
VideoChat = 'videoChat',
|
|
157
|
+
VideoRecording = 'videoRecording',
|
|
158
|
+
VoiceChat = 'voiceChat',
|
|
159
|
+
VoicePrompt = 'voicePrompt',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export enum IOSCategoryOptions {
|
|
163
|
+
MixWithOthers = 'mixWithOthers',
|
|
164
|
+
DuckOthers = 'duckOthers',
|
|
165
|
+
InterruptSpokenAudioAndMixWithOthers = 'interruptSpokenAudioAndMixWithOthers',
|
|
166
|
+
AllowBluetooth = 'allowBluetooth',
|
|
167
|
+
AllowBluetoothA2DP = 'allowBluetoothA2DP',
|
|
168
|
+
AllowAirPlay = 'allowAirPlay',
|
|
169
|
+
DefaultToSpeaker = 'defaultToSpeaker',
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export enum AndroidAudioContentType {
|
|
173
|
+
Unknown = 0,
|
|
174
|
+
Speech = 1,
|
|
175
|
+
Music = 2,
|
|
176
|
+
Movie = 3,
|
|
177
|
+
Sonification = 4,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export enum AndroidAudioUsage {
|
|
181
|
+
Unknown = 0,
|
|
182
|
+
Media = 1,
|
|
183
|
+
VoiceCommunication = 2,
|
|
184
|
+
VoiceCommunicationSignalling = 3,
|
|
185
|
+
Alarm = 4,
|
|
186
|
+
Notification = 5,
|
|
187
|
+
NotificationRingtone = 6,
|
|
188
|
+
NotificationEvent = 10,
|
|
189
|
+
AssistanceAccessibility = 11,
|
|
190
|
+
AssistanceNavigationGuidance = 12,
|
|
191
|
+
AssistanceSonification = 13,
|
|
192
|
+
Game = 14,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Options ─────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export interface PlayerOptions {
|
|
198
|
+
/** Minimum seconds to buffer before playback starts (Android) */
|
|
199
|
+
minBuffer?: number;
|
|
200
|
+
/** Maximum seconds to buffer ahead (Android) */
|
|
201
|
+
maxBuffer?: number;
|
|
202
|
+
/** Seconds buffered before playback resumes after stall (Android) */
|
|
203
|
+
playBuffer?: number;
|
|
204
|
+
/** Seconds of audio to keep behind current position (Android) */
|
|
205
|
+
backBuffer?: number;
|
|
206
|
+
/** Max cache size in bytes (Android) */
|
|
207
|
+
maxCacheSize?: number;
|
|
208
|
+
/** iOS audio session category */
|
|
209
|
+
iosCategory?: IOSCategory;
|
|
210
|
+
/** iOS audio session category mode */
|
|
211
|
+
iosCategoryMode?: IOSCategoryMode;
|
|
212
|
+
/** iOS audio session category options */
|
|
213
|
+
iosCategoryOptions?: IOSCategoryOptions[];
|
|
214
|
+
/** Wait for buffer before auto-playing */
|
|
215
|
+
waitForBuffer?: boolean;
|
|
216
|
+
/** Auto handle audio interruptions (calls, etc.) */
|
|
217
|
+
autoHandleInterruptions?: boolean;
|
|
218
|
+
/** Auto update Now Playing metadata */
|
|
219
|
+
autoUpdateMetadata?: boolean;
|
|
220
|
+
/** Android-specific options */
|
|
221
|
+
android?: {
|
|
222
|
+
appKilledPlaybackBehavior?: AppKilledPlaybackBehavior;
|
|
223
|
+
audioContentType?: AndroidAudioContentType;
|
|
224
|
+
audioUsage?: AndroidAudioUsage;
|
|
225
|
+
autoSkipOnError?: boolean;
|
|
226
|
+
};
|
|
227
|
+
/** Progress update interval in seconds */
|
|
228
|
+
progressUpdateEventInterval?: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export enum AppKilledPlaybackBehavior {
|
|
232
|
+
/** Stop playback when app is killed */
|
|
233
|
+
StopPlaybackAndRemoveNotification = 'StopPlaybackAndRemoveNotification',
|
|
234
|
+
/** Continue playback in background service */
|
|
235
|
+
ContinuePlayback = 'ContinuePlayback',
|
|
236
|
+
/** Pause playback but keep notification */
|
|
237
|
+
PausePlayback = 'PausePlayback',
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface UpdateOptions {
|
|
241
|
+
/** Notification icon (Android) — resource name */
|
|
242
|
+
icon?: number;
|
|
243
|
+
/** Notification icon resource name (Android) */
|
|
244
|
+
notificationIcon?: string;
|
|
245
|
+
/** Playback capabilities to show in notification */
|
|
246
|
+
capabilities?: Capability[];
|
|
247
|
+
/** Capabilities to show when paused */
|
|
248
|
+
notificationCapabilities?: Capability[];
|
|
249
|
+
/** Capabilities to show in compact notification view */
|
|
250
|
+
compactCapabilities?: Capability[];
|
|
251
|
+
/** Jump forward interval in seconds */
|
|
252
|
+
forwardJumpInterval?: number;
|
|
253
|
+
/** Jump backward interval in seconds */
|
|
254
|
+
backwardJumpInterval?: number;
|
|
255
|
+
/** Progress update interval in seconds */
|
|
256
|
+
progressUpdateEventInterval?: number;
|
|
257
|
+
/** Rating type */
|
|
258
|
+
ratingType?: RatingType;
|
|
259
|
+
/** Android notification color (hex string) */
|
|
260
|
+
color?: number;
|
|
261
|
+
/** Android notification channel name */
|
|
262
|
+
notificationChannelName?: string;
|
|
263
|
+
/** Android custom actions */
|
|
264
|
+
customActions?: CustomAction[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface CustomAction {
|
|
268
|
+
icon: number;
|
|
269
|
+
title: string;
|
|
270
|
+
name: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Playback state / progress ───────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
export interface PlaybackState {
|
|
276
|
+
state: State;
|
|
277
|
+
error?: PlaybackError;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface Progress {
|
|
281
|
+
/** Current position in seconds */
|
|
282
|
+
position: number;
|
|
283
|
+
/** Total duration in seconds */
|
|
284
|
+
duration: number;
|
|
285
|
+
/** Buffered position in seconds */
|
|
286
|
+
buffered: number;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface PlaybackError {
|
|
290
|
+
code: string;
|
|
291
|
+
message: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Event payloads ──────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
export interface PlaybackRepeatModeChangedEvent {
|
|
297
|
+
mode: RepeatMode;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface PlaybackStateEvent {
|
|
301
|
+
state: State;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface PlaybackErrorEvent {
|
|
305
|
+
code: string;
|
|
306
|
+
message: string;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface PlaybackActiveTrackChangedEvent {
|
|
310
|
+
index: number | null;
|
|
311
|
+
track: Track | null;
|
|
312
|
+
lastIndex: number | null;
|
|
313
|
+
lastTrack: Track | null;
|
|
314
|
+
lastPosition: number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface PlaybackQueueEndedEvent {
|
|
318
|
+
index: number;
|
|
319
|
+
position: number;
|
|
320
|
+
track: Track | null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface PlaybackProgressUpdatedEvent {
|
|
324
|
+
position: number;
|
|
325
|
+
duration: number;
|
|
326
|
+
buffered: number;
|
|
327
|
+
track: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface PlaybackTrackEndedEvent {
|
|
331
|
+
index: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface PlaybackMetadataReceivedEvent {
|
|
335
|
+
source: 'icy' | 'id3' | 'unknown';
|
|
336
|
+
title?: string;
|
|
337
|
+
url?: string;
|
|
338
|
+
artist?: string;
|
|
339
|
+
album?: string;
|
|
340
|
+
date?: string;
|
|
341
|
+
genre?: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface RemoteSeekEvent {
|
|
345
|
+
position: number;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export interface RemoteJumpForwardEvent {
|
|
349
|
+
interval: number;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface RemoteJumpBackwardEvent {
|
|
353
|
+
interval: number;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface RemoteSetRatingEvent {
|
|
357
|
+
rating: number;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface RemoteDuckEvent {
|
|
361
|
+
paused: boolean;
|
|
362
|
+
permanent: boolean;
|
|
363
|
+
focusLoss: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface RemoteSkipEvent {
|
|
367
|
+
index: number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ─── Event map ───────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
export interface EventPayloadByEvent {
|
|
373
|
+
[Event.PlaybackState]: PlaybackStateEvent;
|
|
374
|
+
[Event.PlaybackError]: PlaybackErrorEvent;
|
|
375
|
+
[Event.PlaybackActiveTrackChanged]: PlaybackActiveTrackChangedEvent;
|
|
376
|
+
[Event.PlaybackQueueEnded]: PlaybackQueueEndedEvent;
|
|
377
|
+
[Event.PlaybackTrackEnded]: PlaybackTrackEndedEvent;
|
|
378
|
+
[Event.PlaybackProgressUpdated]: PlaybackProgressUpdatedEvent;
|
|
379
|
+
[Event.PlaybackMetadataReceived]: PlaybackMetadataReceivedEvent;
|
|
380
|
+
[Event.PlaybackRepeatModeChanged]: PlaybackRepeatModeChangedEvent;
|
|
381
|
+
[Event.RemotePlay]: void;
|
|
382
|
+
[Event.RemotePause]: void;
|
|
383
|
+
[Event.RemoteStop]: void;
|
|
384
|
+
[Event.RemoteNext]: void;
|
|
385
|
+
[Event.RemotePrevious]: void;
|
|
386
|
+
[Event.RemoteSkip]: RemoteSkipEvent;
|
|
387
|
+
[Event.RemoteJumpForward]: RemoteJumpForwardEvent;
|
|
388
|
+
[Event.RemoteJumpBackward]: RemoteJumpBackwardEvent;
|
|
389
|
+
[Event.RemoteSeek]: RemoteSeekEvent;
|
|
390
|
+
[Event.RemoteSetRating]: RemoteSetRatingEvent;
|
|
391
|
+
[Event.RemoteDuck]: RemoteDuckEvent;
|
|
392
|
+
[Event.RemoteLike]: void;
|
|
393
|
+
[Event.RemoteDislike]: void;
|
|
394
|
+
[Event.RemoteBookmark]: void;
|
|
395
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const {
|
|
2
|
+
withDangerousMod,
|
|
3
|
+
withPodfile,
|
|
4
|
+
withInfoPlist,
|
|
5
|
+
withAndroidManifest,
|
|
6
|
+
} = require('@expo/config-plugins');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
module.exports = function withGliphPlayer(config) {
|
|
11
|
+
console.log('🔧 Starting expo-gliph-player plugin...');
|
|
12
|
+
|
|
13
|
+
// ===== iOS: Info.plist =====
|
|
14
|
+
config = withInfoPlist(config, (config) => {
|
|
15
|
+
if (!config.modResults.UIBackgroundModes) {
|
|
16
|
+
config.modResults.UIBackgroundModes = [];
|
|
17
|
+
}
|
|
18
|
+
if (!config.modResults.UIBackgroundModes.includes('audio')) {
|
|
19
|
+
config.modResults.UIBackgroundModes.push('audio');
|
|
20
|
+
}
|
|
21
|
+
return config;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ===== iOS: Podfile =====
|
|
25
|
+
config = withPodfile(config, (config) => {
|
|
26
|
+
let content = config.modResults.contents;
|
|
27
|
+
|
|
28
|
+
if (!content.includes("pod 'expo-gliph-player'")) {
|
|
29
|
+
const podLine =
|
|
30
|
+
` pod 'expo-gliph-player', :path => '../node_modules/expo-gliph-player', :build_type => :static_framework`;
|
|
31
|
+
|
|
32
|
+
// Use the real host app target name instead of a hardcoded one, so
|
|
33
|
+
// this plugin works in any Expo project, not just the app it was
|
|
34
|
+
// originally extracted from.
|
|
35
|
+
const targetName = config.modRequest?.projectName;
|
|
36
|
+
const namedTargetRegex = targetName
|
|
37
|
+
? new RegExp(`target '${targetName}' do`)
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
if (namedTargetRegex && namedTargetRegex.test(content)) {
|
|
41
|
+
content = content.replace(namedTargetRegex, (match) => `${match}\n${podLine}`);
|
|
42
|
+
} else {
|
|
43
|
+
// Fallback: insert into the first `target '...' do` block, which
|
|
44
|
+
// is always the main app target in a standard Expo-generated Podfile.
|
|
45
|
+
content = content.replace(/target '([^']+)' do/, (match) => `${match}\n${podLine}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!content.includes("target.name == 'expo-gliph-player'")) {
|
|
50
|
+
const gliphConfig = `
|
|
51
|
+
# Configure expo-gliph-player
|
|
52
|
+
installer.pods_project.targets.each do |target|
|
|
53
|
+
if target.name == 'expo-gliph-player'
|
|
54
|
+
target.build_configurations.each do |config|
|
|
55
|
+
config.build_settings['DEFINES_MODULE'] = 'YES'
|
|
56
|
+
config.build_settings['SWIFT_INSTALL_OBJC_HEADER'] = 'YES'
|
|
57
|
+
config.build_settings['SWIFT_OBJC_INTERFACE_HEADER_NAME'] = 'expo_gliph_player-Swift.h'
|
|
58
|
+
config.build_settings['SWIFT_VERSION'] = '5.9'
|
|
59
|
+
# RCT_NEW_ARCH_ENABLED is intentionally left alone here — it should
|
|
60
|
+
# come from the host app's actual architecture setting via Expo/RN
|
|
61
|
+
# autolinking, not be forced on unconditionally (see podspec).
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end`;
|
|
65
|
+
|
|
66
|
+
content = content.replace(
|
|
67
|
+
/post_install do \|installer\|/,
|
|
68
|
+
`post_install do |installer|${gliphConfig}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
config.modResults.contents = content;
|
|
73
|
+
return config;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ===== Android: AndroidManifest.xml =====
|
|
77
|
+
config = withAndroidManifest(config, (config) => {
|
|
78
|
+
const manifest = config.modResults;
|
|
79
|
+
const application = manifest.manifest?.application?.[0];
|
|
80
|
+
if (!application) return config;
|
|
81
|
+
|
|
82
|
+
if (!application.service) application.service = [];
|
|
83
|
+
|
|
84
|
+
const hasService = application.service.some(
|
|
85
|
+
(s) => s.$?.['android:name'] === 'com.gliphplayer.GliphPlayerService',
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!hasService) {
|
|
89
|
+
const serviceData = {
|
|
90
|
+
$: {
|
|
91
|
+
'android:name': 'com.gliphplayer.GliphPlayerService',
|
|
92
|
+
'android:exported': 'true',
|
|
93
|
+
'android:foregroundServiceType': 'mediaPlayback',
|
|
94
|
+
'android:stopWithTask': 'false',
|
|
95
|
+
},
|
|
96
|
+
'intent-filter': [{
|
|
97
|
+
action: [
|
|
98
|
+
{ $: { 'android:name': 'androidx.media3.session.MediaLibraryService' } },
|
|
99
|
+
{ $: { 'android:name': 'android.media.browse.MediaBrowserService' } },
|
|
100
|
+
],
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
application.service.push(serviceData);
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ===== Android: MainApplication.kt =====
|
|
109
|
+
config = withDangerousMod(config, [
|
|
110
|
+
'android',
|
|
111
|
+
async (config) => {
|
|
112
|
+
// Derive the path from the project's real Android package name instead
|
|
113
|
+
// of guessing against a hardcoded list of paths from one specific app.
|
|
114
|
+
const pkg = config.android?.package;
|
|
115
|
+
if (!pkg) {
|
|
116
|
+
console.warn(
|
|
117
|
+
"[expo-gliph-player] Couldn't determine the Android package name " +
|
|
118
|
+
'(config.android.package is missing) — skipping MainApplication.kt patch. ' +
|
|
119
|
+
'Set "android.package" in app.json/app.config.js.'
|
|
120
|
+
);
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pkgPath = pkg.replace(/\./g, '/');
|
|
125
|
+
const mainAppPath = path.join(
|
|
126
|
+
config.modRequest.projectRoot,
|
|
127
|
+
`android/app/src/main/java/${pkgPath}/MainApplication.kt`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (fs.existsSync(mainAppPath)) {
|
|
131
|
+
let content = fs.readFileSync(mainAppPath, 'utf8');
|
|
132
|
+
|
|
133
|
+
if (!content.includes('import com.gliphplayer.GliphPlayerPackage')) {
|
|
134
|
+
const lines = content.split('\n');
|
|
135
|
+
let lastImportIndex = -1;
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
if (lines[i].trim().startsWith('import ')) {
|
|
138
|
+
lastImportIndex = i;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (lastImportIndex !== -1) {
|
|
142
|
+
lines.splice(lastImportIndex + 1, 0, 'import com.gliphplayer.GliphPlayerPackage');
|
|
143
|
+
content = lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!content.includes('add(GliphPlayerPackage())')) {
|
|
148
|
+
const applyRegex = /PackageList\(this\)\.packages\.apply\s*\{/;
|
|
149
|
+
if (applyRegex.test(content)) {
|
|
150
|
+
content = content.replace(
|
|
151
|
+
applyRegex,
|
|
152
|
+
(match) => `${match}\n add(GliphPlayerPackage())`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fs.writeFileSync(mainAppPath, content, 'utf8');
|
|
158
|
+
} else {
|
|
159
|
+
console.warn(`[expo-gliph-player] MainApplication.kt not found at ${mainAppPath} — skipping patch.`);
|
|
160
|
+
}
|
|
161
|
+
return config;
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
return config;
|
|
166
|
+
};
|