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 +32 -0
- package/dist/core/player.d.ts +2 -0
- package/dist/core/player.js +79 -3
- package/package.json +3 -1
- package/src/core/player.ts +69 -3
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
|
@@ -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;
|
package/dist/core/player.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/src/core/player.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|