tunzo-player 1.0.24 → 1.0.27

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,6 +37,8 @@ export declare class Player {
37
37
  static setQuality(index: number): void;
38
38
  static getQueue(): any[];
39
39
  static getPlaylist(): any[];
40
+ private static enableBackgroundMode;
41
+ private static disableBackgroundMode;
40
42
  private static setupMediaSession;
41
43
  private static updateMediaSessionMetadata;
42
44
  private static updatePositionState;
@@ -1,7 +1,18 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.Player = void 0;
4
13
  const rxjs_1 = require("rxjs");
14
+ const capacitor_background_mode_1 = require("@anuradev/capacitor-background-mode");
15
+ const keep_awake_1 = require("@capacitor-community/keep-awake");
5
16
  class Player {
6
17
  /** Initialize with playlist and quality */
7
18
  static initialize(playlist, quality = 3) {
@@ -28,11 +39,14 @@ class Player {
28
39
  url = url.replace('http://', 'https://');
29
40
  }
30
41
  this.audio.src = url;
42
+ // @ts-ignore
43
+ this.audio.title = song.name || song.title || 'Unknown Title'; // Help some browsers identify the track
31
44
  this.audio.preload = 'auto'; // Improve loading
32
45
  this.audio.load(); // Ensure audio is loaded before play
33
46
  this.audio.play().then(() => {
34
47
  this.isPlaying = true;
35
48
  this.updateMediaSessionMetadata(song);
49
+ this.enableBackgroundMode();
36
50
  if ('mediaSession' in navigator) {
37
51
  navigator.mediaSession.playbackState = 'playing';
38
52
  }
@@ -48,6 +62,22 @@ class Player {
48
62
  // Set current time
49
63
  this.audio.ontimeupdate = () => {
50
64
  this.currentTime = this.audio.currentTime;
65
+ // Update position state less frequently to avoid spamming, but enough to keep sync
66
+ if (Math.floor(this.currentTime) % 5 === 0) {
67
+ this.updatePositionState();
68
+ }
69
+ };
70
+ // Handle buffering/stalled states
71
+ this.audio.onwaiting = () => {
72
+ if ('mediaSession' in navigator) {
73
+ navigator.mediaSession.playbackState = 'none'; // Or 'paused' to indicate buffering
74
+ }
75
+ };
76
+ this.audio.onplaying = () => {
77
+ this.isPlaying = true;
78
+ if ('mediaSession' in navigator) {
79
+ navigator.mediaSession.playbackState = 'playing';
80
+ }
51
81
  };
52
82
  // Auto-play next song
53
83
  this.audio.onended = () => {
@@ -61,6 +91,7 @@ class Player {
61
91
  static pause() {
62
92
  this.audio.pause();
63
93
  this.isPlaying = false;
94
+ this.disableBackgroundMode();
64
95
  if ('mediaSession' in navigator) {
65
96
  navigator.mediaSession.playbackState = 'paused';
66
97
  }
@@ -163,6 +194,43 @@ class Player {
163
194
  return this.playlist;
164
195
  }
165
196
  // -------------------------------------------------------------------------
197
+ // Capacitor Background Mode & Keep Awake
198
+ // -------------------------------------------------------------------------
199
+ static enableBackgroundMode() {
200
+ return __awaiter(this, void 0, void 0, function* () {
201
+ try {
202
+ yield keep_awake_1.KeepAwake.keepAwake();
203
+ yield capacitor_background_mode_1.BackgroundMode.enable({
204
+ title: "Tunzo Player",
205
+ text: "Playing music in background",
206
+ icon: "ic_launcher",
207
+ color: "042730",
208
+ resume: true,
209
+ hidden: false,
210
+ bigText: true,
211
+ disableWebViewOptimization: true
212
+ });
213
+ }
214
+ catch (err) {
215
+ // Plugin might not be installed or on web
216
+ }
217
+ });
218
+ }
219
+ static disableBackgroundMode() {
220
+ return __awaiter(this, void 0, void 0, function* () {
221
+ try {
222
+ yield keep_awake_1.KeepAwake.allowSleep();
223
+ // We might want to keep background mode enabled if we want to resume later,
224
+ // but for battery saving, we can disable it or move to background.
225
+ // await BackgroundMode.disable();
226
+ yield capacitor_background_mode_1.BackgroundMode.moveToBackground();
227
+ }
228
+ catch (err) {
229
+ // Plugin might not be installed or on web
230
+ }
231
+ });
232
+ }
233
+ // -------------------------------------------------------------------------
166
234
  // Native Media Session (Lock Screen Controls)
167
235
  // -------------------------------------------------------------------------
168
236
  static setupMediaSession() {
@@ -186,20 +254,28 @@ class Player {
186
254
  if (Array.isArray(song.image)) {
187
255
  // Assuming image array contains objects with url/link and quality
188
256
  song.image.forEach((img) => {
189
- const src = img.link || img.url || (typeof img === 'string' ? img : '');
257
+ let src = img.link || img.url || (typeof img === 'string' ? img : '');
190
258
  if (src) {
259
+ // 🚀 Auto-convert http → https for images too
260
+ if (src.startsWith('http://')) {
261
+ src = src.replace('http://', 'https://');
262
+ }
191
263
  artwork.push({ src, sizes: '500x500', type: 'image/jpeg' });
192
264
  }
193
265
  });
194
266
  }
195
267
  else if (typeof song.image === 'string') {
196
- artwork.push({ src: song.image, sizes: '500x500', type: 'image/jpeg' });
268
+ let src = song.image;
269
+ if (src.startsWith('http://')) {
270
+ src = src.replace('http://', 'https://');
271
+ }
272
+ artwork.push({ src: src, sizes: '500x500', type: 'image/jpeg' });
197
273
  }
198
274
  }
199
275
  navigator.mediaSession.metadata = new MediaMetadata({
200
276
  title: song.name || song.title || 'Unknown Title',
201
277
  artist: song.primaryArtists || song.artist || 'Unknown Artist',
202
- album: ((_a = song.album) === null || _a === void 0 ? void 0 : _a.name) || song.album || '',
278
+ album: ((_a = song.album) === null || _a === void 0 ? void 0 : _a.name) || song.album || 'Unknown Album',
203
279
  artwork: artwork.length > 0 ? artwork : undefined
204
280
  });
205
281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunzo-player",
3
- "version": "1.0.24",
3
+ "version": "1.0.27",
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",
@@ -29,6 +29,8 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
+ "@anuradev/capacitor-background-mode": "^7.2.1",
33
+ "@capacitor-community/keep-awake": "^7.1.0",
32
34
  "rxjs": "^7.8.2"
33
35
  }
34
36
  }
@@ -1,4 +1,6 @@
1
1
  import { BehaviorSubject } from 'rxjs';
2
+ import { BackgroundMode } from '@anuradev/capacitor-background-mode';
3
+ import { KeepAwake } from '@capacitor-community/keep-awake';
2
4
 
3
5
  export class Player {
4
6
  private static audio = new Audio();
@@ -42,11 +44,14 @@ export class Player {
42
44
  }
43
45
 
44
46
  this.audio.src = url;
47
+ // @ts-ignore
48
+ this.audio.title = song.name || song.title || 'Unknown Title'; // Help some browsers identify the track
45
49
  this.audio.preload = 'auto'; // Improve loading
46
50
  this.audio.load(); // Ensure audio is loaded before play
47
51
  this.audio.play().then(() => {
48
52
  this.isPlaying = true;
49
53
  this.updateMediaSessionMetadata(song);
54
+ this.enableBackgroundMode();
50
55
  if ('mediaSession' in navigator) {
51
56
  navigator.mediaSession.playbackState = 'playing';
52
57
  }
@@ -64,6 +69,24 @@ export class Player {
64
69
  // Set current time
65
70
  this.audio.ontimeupdate = () => {
66
71
  this.currentTime = this.audio.currentTime;
72
+ // Update position state less frequently to avoid spamming, but enough to keep sync
73
+ if (Math.floor(this.currentTime) % 5 === 0) {
74
+ this.updatePositionState();
75
+ }
76
+ };
77
+
78
+ // Handle buffering/stalled states
79
+ this.audio.onwaiting = () => {
80
+ if ('mediaSession' in navigator) {
81
+ navigator.mediaSession.playbackState = 'none'; // Or 'paused' to indicate buffering
82
+ }
83
+ };
84
+
85
+ this.audio.onplaying = () => {
86
+ this.isPlaying = true;
87
+ if ('mediaSession' in navigator) {
88
+ navigator.mediaSession.playbackState = 'playing';
89
+ }
67
90
  };
68
91
 
69
92
  // Auto-play next song
@@ -81,6 +104,7 @@ export class Player {
81
104
  static pause() {
82
105
  this.audio.pause();
83
106
  this.isPlaying = false;
107
+ this.disableBackgroundMode();
84
108
  if ('mediaSession' in navigator) {
85
109
  navigator.mediaSession.playbackState = 'paused';
86
110
  }
@@ -201,6 +225,40 @@ export class Player {
201
225
  return this.playlist;
202
226
  }
203
227
 
228
+ // -------------------------------------------------------------------------
229
+ // Capacitor Background Mode & Keep Awake
230
+ // -------------------------------------------------------------------------
231
+
232
+ private static async enableBackgroundMode() {
233
+ try {
234
+ await KeepAwake.keepAwake();
235
+ await BackgroundMode.enable({
236
+ title: "Tunzo Player",
237
+ text: "Playing music in background",
238
+ icon: "ic_launcher",
239
+ color: "042730",
240
+ resume: true,
241
+ hidden: false,
242
+ bigText: true,
243
+ disableWebViewOptimization: true
244
+ });
245
+ } catch (err) {
246
+ // Plugin might not be installed or on web
247
+ }
248
+ }
249
+
250
+ private static async disableBackgroundMode() {
251
+ try {
252
+ await KeepAwake.allowSleep();
253
+ // We might want to keep background mode enabled if we want to resume later,
254
+ // but for battery saving, we can disable it or move to background.
255
+ // await BackgroundMode.disable();
256
+ await BackgroundMode.moveToBackground();
257
+ } catch (err) {
258
+ // Plugin might not be installed or on web
259
+ }
260
+ }
261
+
204
262
  // -------------------------------------------------------------------------
205
263
  // Native Media Session (Lock Screen Controls)
206
264
  // -------------------------------------------------------------------------
@@ -226,20 +284,28 @@ export class Player {
226
284
  if (Array.isArray(song.image)) {
227
285
  // Assuming image array contains objects with url/link and quality
228
286
  song.image.forEach((img: any) => {
229
- const src = img.link || img.url || (typeof img === 'string' ? img : '');
287
+ let src = img.link || img.url || (typeof img === 'string' ? img : '');
230
288
  if (src) {
289
+ // 🚀 Auto-convert http → https for images too
290
+ if (src.startsWith('http://')) {
291
+ src = src.replace('http://', 'https://');
292
+ }
231
293
  artwork.push({ src, sizes: '500x500', type: 'image/jpeg' });
232
294
  }
233
295
  });
234
296
  } else if (typeof song.image === 'string') {
235
- artwork.push({ src: song.image, sizes: '500x500', type: 'image/jpeg' });
297
+ let src = song.image;
298
+ if (src.startsWith('http://')) {
299
+ src = src.replace('http://', 'https://');
300
+ }
301
+ artwork.push({ src: src, sizes: '500x500', type: 'image/jpeg' });
236
302
  }
237
303
  }
238
304
 
239
305
  navigator.mediaSession.metadata = new MediaMetadata({
240
306
  title: song.name || song.title || 'Unknown Title',
241
307
  artist: song.primaryArtists || song.artist || 'Unknown Artist',
242
- album: song.album?.name || song.album || '',
308
+ album: song.album?.name || song.album || 'Unknown Album',
243
309
  artwork: artwork.length > 0 ? artwork : undefined
244
310
  });
245
311
  }