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 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
@@ -37,4 +37,7 @@ export declare class Player {
37
37
  static setQuality(index: number): void;
38
38
  static getQueue(): any[];
39
39
  static getPlaylist(): any[];
40
+ private static setupMediaSession;
41
+ private static updateMediaSessionMetadata;
42
+ private static updatePositionState;
40
43
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunzo-player",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "A music playback service for Angular and Ionic apps with native audio control support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
  }