tunzo-player 1.0.23 → 1.0.25
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 +32 -0
- package/dist/core/player.d.ts +3 -0
- package/dist/core/player.js +91 -0
- package/package.json +1 -1
- package/src/core/player.ts +96 -0
package/README.md
CHANGED
|
@@ -76,4 +76,36 @@ yarn add tunzo-player
|
|
|
76
76
|
| 3 | High (160kbps) |
|
|
77
77
|
| 4 | Ultra (320kbps) |
|
|
78
78
|
|
|
79
|
+
| 4 | Ultra (320kbps) |
|
|
80
|
+
|
|
81
|
+
## 📱 Native Configuration (Ionic/Capacitor)
|
|
82
|
+
|
|
83
|
+
To ensure background audio works correctly on Android and iOS (preventing the app from pausing when the screen locks), you must configure your native projects.
|
|
84
|
+
|
|
85
|
+
### **Android (`android/app/src/main/AndroidManifest.xml`)**
|
|
86
|
+
|
|
87
|
+
Add the following permissions inside the `<manifest>` tag:
|
|
88
|
+
|
|
89
|
+
```xml
|
|
90
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
91
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Note:** Modern Android versions might require a foreground service notification to keep the audio alive indefinitely. The `MediaSession` API implemented in this package helps, but for guaranteed persistence, consider using a native audio plugin if issues persist.
|
|
95
|
+
|
|
96
|
+
### **iOS (`ios/App/App/Info.plist`)**
|
|
97
|
+
|
|
98
|
+
Add `audio` to the `UIBackgroundModes` key to allow background playback:
|
|
99
|
+
|
|
100
|
+
```xml
|
|
101
|
+
<key>UIBackgroundModes</key>
|
|
102
|
+
<array>
|
|
103
|
+
<string>audio</string>
|
|
104
|
+
</array>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 🤝 Contributing
|
|
108
|
+
|
|
109
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
110
|
+
|
|
79
111
|
# tunzo-player
|
package/dist/core/player.d.ts
CHANGED
package/dist/core/player.js
CHANGED
|
@@ -7,6 +7,7 @@ class Player {
|
|
|
7
7
|
static initialize(playlist, quality = 3) {
|
|
8
8
|
this.playlist = playlist;
|
|
9
9
|
this.selectedQuality = quality;
|
|
10
|
+
this.setupMediaSession();
|
|
10
11
|
}
|
|
11
12
|
/** Call this once on user gesture to unlock audio in WebView */
|
|
12
13
|
static unlockAudio() {
|
|
@@ -27,9 +28,16 @@ class Player {
|
|
|
27
28
|
url = url.replace('http://', 'https://');
|
|
28
29
|
}
|
|
29
30
|
this.audio.src = url;
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
this.audio.title = song.name || song.title || 'Unknown Title'; // Help some browsers identify the track
|
|
33
|
+
this.audio.preload = 'auto'; // Improve loading
|
|
30
34
|
this.audio.load(); // Ensure audio is loaded before play
|
|
31
35
|
this.audio.play().then(() => {
|
|
32
36
|
this.isPlaying = true;
|
|
37
|
+
this.updateMediaSessionMetadata(song);
|
|
38
|
+
if ('mediaSession' in navigator) {
|
|
39
|
+
navigator.mediaSession.playbackState = 'playing';
|
|
40
|
+
}
|
|
33
41
|
}).catch((err) => {
|
|
34
42
|
this.isPlaying = false;
|
|
35
43
|
console.warn('Audio play failed:', err);
|
|
@@ -37,10 +45,27 @@ class Player {
|
|
|
37
45
|
// Set duration
|
|
38
46
|
this.audio.onloadedmetadata = () => {
|
|
39
47
|
this.duration = this.audio.duration;
|
|
48
|
+
this.updatePositionState();
|
|
40
49
|
};
|
|
41
50
|
// Set current time
|
|
42
51
|
this.audio.ontimeupdate = () => {
|
|
43
52
|
this.currentTime = this.audio.currentTime;
|
|
53
|
+
// Update position state less frequently to avoid spamming, but enough to keep sync
|
|
54
|
+
if (Math.floor(this.currentTime) % 5 === 0) {
|
|
55
|
+
this.updatePositionState();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// Handle buffering/stalled states
|
|
59
|
+
this.audio.onwaiting = () => {
|
|
60
|
+
if ('mediaSession' in navigator) {
|
|
61
|
+
navigator.mediaSession.playbackState = 'none'; // Or 'paused' to indicate buffering
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
this.audio.onplaying = () => {
|
|
65
|
+
this.isPlaying = true;
|
|
66
|
+
if ('mediaSession' in navigator) {
|
|
67
|
+
navigator.mediaSession.playbackState = 'playing';
|
|
68
|
+
}
|
|
44
69
|
};
|
|
45
70
|
// Auto-play next song
|
|
46
71
|
this.audio.onended = () => {
|
|
@@ -54,10 +79,16 @@ class Player {
|
|
|
54
79
|
static pause() {
|
|
55
80
|
this.audio.pause();
|
|
56
81
|
this.isPlaying = false;
|
|
82
|
+
if ('mediaSession' in navigator) {
|
|
83
|
+
navigator.mediaSession.playbackState = 'paused';
|
|
84
|
+
}
|
|
57
85
|
}
|
|
58
86
|
static resume() {
|
|
59
87
|
this.audio.play();
|
|
60
88
|
this.isPlaying = true;
|
|
89
|
+
if ('mediaSession' in navigator) {
|
|
90
|
+
navigator.mediaSession.playbackState = 'playing';
|
|
91
|
+
}
|
|
61
92
|
}
|
|
62
93
|
static togglePlayPause() {
|
|
63
94
|
if (this.isPlaying) {
|
|
@@ -88,6 +119,7 @@ class Player {
|
|
|
88
119
|
}
|
|
89
120
|
static seek(seconds) {
|
|
90
121
|
this.audio.currentTime = seconds;
|
|
122
|
+
this.updatePositionState();
|
|
91
123
|
}
|
|
92
124
|
static autoNext() {
|
|
93
125
|
this.next();
|
|
@@ -148,6 +180,65 @@ class Player {
|
|
|
148
180
|
static getPlaylist() {
|
|
149
181
|
return this.playlist;
|
|
150
182
|
}
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// Native Media Session (Lock Screen Controls)
|
|
185
|
+
// -------------------------------------------------------------------------
|
|
186
|
+
static setupMediaSession() {
|
|
187
|
+
if ('mediaSession' in navigator) {
|
|
188
|
+
navigator.mediaSession.setActionHandler('play', () => this.resume());
|
|
189
|
+
navigator.mediaSession.setActionHandler('pause', () => this.pause());
|
|
190
|
+
navigator.mediaSession.setActionHandler('previoustrack', () => this.prev());
|
|
191
|
+
navigator.mediaSession.setActionHandler('nexttrack', () => this.next());
|
|
192
|
+
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
|
193
|
+
if (details.seekTime !== undefined) {
|
|
194
|
+
this.seek(details.seekTime);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
static updateMediaSessionMetadata(song) {
|
|
200
|
+
var _a;
|
|
201
|
+
if ('mediaSession' in navigator) {
|
|
202
|
+
const artwork = [];
|
|
203
|
+
if (song.image) {
|
|
204
|
+
if (Array.isArray(song.image)) {
|
|
205
|
+
// Assuming image array contains objects with url/link and quality
|
|
206
|
+
song.image.forEach((img) => {
|
|
207
|
+
let src = img.link || img.url || (typeof img === 'string' ? img : '');
|
|
208
|
+
if (src) {
|
|
209
|
+
// 🚀 Auto-convert http → https for images too
|
|
210
|
+
if (src.startsWith('http://')) {
|
|
211
|
+
src = src.replace('http://', 'https://');
|
|
212
|
+
}
|
|
213
|
+
artwork.push({ src, sizes: '500x500', type: 'image/jpeg' });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else if (typeof song.image === 'string') {
|
|
218
|
+
let src = song.image;
|
|
219
|
+
if (src.startsWith('http://')) {
|
|
220
|
+
src = src.replace('http://', 'https://');
|
|
221
|
+
}
|
|
222
|
+
artwork.push({ src: src, sizes: '500x500', type: 'image/jpeg' });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
navigator.mediaSession.metadata = new MediaMetadata({
|
|
226
|
+
title: song.name || song.title || 'Unknown Title',
|
|
227
|
+
artist: song.primaryArtists || song.artist || 'Unknown Artist',
|
|
228
|
+
album: ((_a = song.album) === null || _a === void 0 ? void 0 : _a.name) || song.album || 'Unknown Album',
|
|
229
|
+
artwork: artwork.length > 0 ? artwork : undefined
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
static updatePositionState() {
|
|
234
|
+
if ('mediaSession' in navigator && this.duration > 0) {
|
|
235
|
+
navigator.mediaSession.setPositionState({
|
|
236
|
+
duration: this.duration,
|
|
237
|
+
playbackRate: this.audio.playbackRate,
|
|
238
|
+
position: this.audio.currentTime
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
151
242
|
}
|
|
152
243
|
exports.Player = Player;
|
|
153
244
|
Player.audio = new Audio();
|
package/package.json
CHANGED
package/src/core/player.ts
CHANGED
|
@@ -17,6 +17,7 @@ export class Player {
|
|
|
17
17
|
static initialize(playlist: any[], quality = 3) {
|
|
18
18
|
this.playlist = playlist;
|
|
19
19
|
this.selectedQuality = quality;
|
|
20
|
+
this.setupMediaSession();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/** Call this once on user gesture to unlock audio in WebView */
|
|
@@ -41,9 +42,16 @@ export class Player {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
this.audio.src = url;
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
this.audio.title = song.name || song.title || 'Unknown Title'; // Help some browsers identify the track
|
|
47
|
+
this.audio.preload = 'auto'; // Improve loading
|
|
44
48
|
this.audio.load(); // Ensure audio is loaded before play
|
|
45
49
|
this.audio.play().then(() => {
|
|
46
50
|
this.isPlaying = true;
|
|
51
|
+
this.updateMediaSessionMetadata(song);
|
|
52
|
+
if ('mediaSession' in navigator) {
|
|
53
|
+
navigator.mediaSession.playbackState = 'playing';
|
|
54
|
+
}
|
|
47
55
|
}).catch((err) => {
|
|
48
56
|
this.isPlaying = false;
|
|
49
57
|
console.warn('Audio play failed:', err);
|
|
@@ -52,11 +60,30 @@ export class Player {
|
|
|
52
60
|
// Set duration
|
|
53
61
|
this.audio.onloadedmetadata = () => {
|
|
54
62
|
this.duration = this.audio.duration;
|
|
63
|
+
this.updatePositionState();
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
// Set current time
|
|
58
67
|
this.audio.ontimeupdate = () => {
|
|
59
68
|
this.currentTime = this.audio.currentTime;
|
|
69
|
+
// Update position state less frequently to avoid spamming, but enough to keep sync
|
|
70
|
+
if (Math.floor(this.currentTime) % 5 === 0) {
|
|
71
|
+
this.updatePositionState();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Handle buffering/stalled states
|
|
76
|
+
this.audio.onwaiting = () => {
|
|
77
|
+
if ('mediaSession' in navigator) {
|
|
78
|
+
navigator.mediaSession.playbackState = 'none'; // Or 'paused' to indicate buffering
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.audio.onplaying = () => {
|
|
83
|
+
this.isPlaying = true;
|
|
84
|
+
if ('mediaSession' in navigator) {
|
|
85
|
+
navigator.mediaSession.playbackState = 'playing';
|
|
86
|
+
}
|
|
60
87
|
};
|
|
61
88
|
|
|
62
89
|
// Auto-play next song
|
|
@@ -74,11 +101,17 @@ export class Player {
|
|
|
74
101
|
static pause() {
|
|
75
102
|
this.audio.pause();
|
|
76
103
|
this.isPlaying = false;
|
|
104
|
+
if ('mediaSession' in navigator) {
|
|
105
|
+
navigator.mediaSession.playbackState = 'paused';
|
|
106
|
+
}
|
|
77
107
|
}
|
|
78
108
|
|
|
79
109
|
static resume() {
|
|
80
110
|
this.audio.play();
|
|
81
111
|
this.isPlaying = true;
|
|
112
|
+
if ('mediaSession' in navigator) {
|
|
113
|
+
navigator.mediaSession.playbackState = 'playing';
|
|
114
|
+
}
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
static togglePlayPause() {
|
|
@@ -110,6 +143,7 @@ export class Player {
|
|
|
110
143
|
|
|
111
144
|
static seek(seconds: number) {
|
|
112
145
|
this.audio.currentTime = seconds;
|
|
146
|
+
this.updatePositionState();
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
static autoNext() {
|
|
@@ -186,4 +220,66 @@ export class Player {
|
|
|
186
220
|
static getPlaylist(): any[] {
|
|
187
221
|
return this.playlist;
|
|
188
222
|
}
|
|
223
|
+
|
|
224
|
+
// -------------------------------------------------------------------------
|
|
225
|
+
// Native Media Session (Lock Screen Controls)
|
|
226
|
+
// -------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
private static setupMediaSession() {
|
|
229
|
+
if ('mediaSession' in navigator) {
|
|
230
|
+
navigator.mediaSession.setActionHandler('play', () => this.resume());
|
|
231
|
+
navigator.mediaSession.setActionHandler('pause', () => this.pause());
|
|
232
|
+
navigator.mediaSession.setActionHandler('previoustrack', () => this.prev());
|
|
233
|
+
navigator.mediaSession.setActionHandler('nexttrack', () => this.next());
|
|
234
|
+
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
|
235
|
+
if (details.seekTime !== undefined) {
|
|
236
|
+
this.seek(details.seekTime);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private static updateMediaSessionMetadata(song: any) {
|
|
243
|
+
if ('mediaSession' in navigator) {
|
|
244
|
+
const artwork = [];
|
|
245
|
+
if (song.image) {
|
|
246
|
+
if (Array.isArray(song.image)) {
|
|
247
|
+
// Assuming image array contains objects with url/link and quality
|
|
248
|
+
song.image.forEach((img: any) => {
|
|
249
|
+
let src = img.link || img.url || (typeof img === 'string' ? img : '');
|
|
250
|
+
if (src) {
|
|
251
|
+
// 🚀 Auto-convert http → https for images too
|
|
252
|
+
if (src.startsWith('http://')) {
|
|
253
|
+
src = src.replace('http://', 'https://');
|
|
254
|
+
}
|
|
255
|
+
artwork.push({ src, sizes: '500x500', type: 'image/jpeg' });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} else if (typeof song.image === 'string') {
|
|
259
|
+
let src = song.image;
|
|
260
|
+
if (src.startsWith('http://')) {
|
|
261
|
+
src = src.replace('http://', 'https://');
|
|
262
|
+
}
|
|
263
|
+
artwork.push({ src: src, sizes: '500x500', type: 'image/jpeg' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
navigator.mediaSession.metadata = new MediaMetadata({
|
|
268
|
+
title: song.name || song.title || 'Unknown Title',
|
|
269
|
+
artist: song.primaryArtists || song.artist || 'Unknown Artist',
|
|
270
|
+
album: song.album?.name || song.album || 'Unknown Album',
|
|
271
|
+
artwork: artwork.length > 0 ? artwork : undefined
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private static updatePositionState() {
|
|
277
|
+
if ('mediaSession' in navigator && this.duration > 0) {
|
|
278
|
+
navigator.mediaSession.setPositionState({
|
|
279
|
+
duration: this.duration,
|
|
280
|
+
playbackRate: this.audio.playbackRate,
|
|
281
|
+
position: this.audio.currentTime
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
189
285
|
}
|