tunzo-player 1.0.28 → 1.0.30

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,11 +76,16 @@ yarn add tunzo-player
76
76
  | 3 | High (160kbps) |
77
77
  | 4 | Ultra (320kbps) |
78
78
 
79
- | 4 | Ultra (320kbps) |
80
-
81
79
  ## 📱 Native Configuration (Ionic/Capacitor)
82
80
 
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.
81
+ To ensure background audio works correctly on Android and iOS, you need to install the KeepAwake plugin in your main app.
82
+
83
+ ### **Installation**
84
+
85
+ ```bash
86
+ npm install @capacitor-community/keep-awake
87
+ npx cap sync
88
+ ```
84
89
 
85
90
  ### **Android (`android/app/src/main/AndroidManifest.xml`)**
86
91
 
@@ -88,11 +93,8 @@ Add the following permissions inside the `<manifest>` tag:
88
93
 
89
94
  ```xml
90
95
  <uses-permission android:name="android.permission.WAKE_LOCK" />
91
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
92
96
  ```
93
97
 
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
98
  ### **iOS (`ios/App/App/Info.plist`)**
97
99
 
98
100
  Add `audio` to the `UIBackgroundModes` key to allow background playback:
@@ -104,8 +106,11 @@ Add `audio` to the `UIBackgroundModes` key to allow background playback:
104
106
  </array>
105
107
  ```
106
108
 
109
+ **Note:** The player uses HTML5 Audio with MediaSession API for lock screen controls. This works with MP4/AAC audio streams (like JioSaavn) on both Android and iOS.
110
+
107
111
  ## 🤝 Contributing
108
112
 
109
113
  Contributions are welcome! Please open an issue or submit a pull request.
110
114
 
111
115
  # tunzo-player
116
+
@@ -1,5 +1,6 @@
1
1
  import { BehaviorSubject } from 'rxjs';
2
2
  export declare class Player {
3
+ private static audio;
3
4
  private static currentSong;
4
5
  private static currentIndex;
5
6
  private static isPlaying;
@@ -10,15 +11,13 @@ export declare class Player {
10
11
  static queue$: BehaviorSubject<any[]>;
11
12
  private static playlist;
12
13
  private static selectedQuality;
13
- private static isNative;
14
- private static webAudio;
15
14
  /** Initialize with playlist and quality */
16
- static initialize(playlist: any[], quality?: number): Promise<void>;
17
- /** Call this once on user gesture to unlock audio in WebView (Web only) */
15
+ static initialize(playlist: any[], quality?: number): void;
16
+ /** Setup audio element for better compatibility */
17
+ private static setupAudioElement;
18
+ /** Call this once on user gesture to unlock audio in WebView */
18
19
  static unlockAudio(): void;
19
- static play(song: any, index?: number): Promise<void>;
20
- private static playNative;
21
- private static playWeb;
20
+ static play(song: any, index?: number): void;
22
21
  static pause(): void;
23
22
  static resume(): void;
24
23
  static togglePlayPause(): void;
@@ -40,7 +39,7 @@ export declare class Player {
40
39
  static setQuality(index: number): void;
41
40
  static getQueue(): any[];
42
41
  static getPlaylist(): any[];
43
- private static setupNativeListeners;
44
- private static setupWebListeners;
45
- private static updateWebMediaSession;
42
+ private static setupMediaSession;
43
+ private static updateMediaSessionMetadata;
44
+ private static updatePositionState;
46
45
  }
@@ -1,153 +1,103 @@
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
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.Player = void 0;
13
4
  const rxjs_1 = require("rxjs");
14
- const native_audio_1 = require("@capgo/native-audio");
15
5
  const keep_awake_1 = require("@capacitor-community/keep-awake");
16
- const core_1 = require("@capacitor/core");
17
6
  class Player {
18
7
  /** Initialize with playlist and quality */
19
- static initialize(playlist_1) {
20
- return __awaiter(this, arguments, void 0, function* (playlist, quality = 3) {
21
- this.playlist = playlist;
22
- this.selectedQuality = quality;
23
- // Configure native audio if on native platform
24
- if (this.isNative) {
25
- try {
26
- yield native_audio_1.NativeAudio.configure({
27
- showNotification: true,
28
- background: true,
29
- focus: true
30
- });
31
- }
32
- catch (e) {
33
- console.warn('NativeAudio configure failed:', e);
34
- }
35
- this.setupNativeListeners();
36
- }
37
- else {
38
- this.setupWebListeners();
39
- }
40
- });
8
+ static initialize(playlist, quality = 3) {
9
+ this.playlist = playlist;
10
+ this.selectedQuality = quality;
11
+ this.setupMediaSession();
12
+ this.setupAudioElement();
41
13
  }
42
- /** Call this once on user gesture to unlock audio in WebView (Web only) */
43
- static unlockAudio() {
44
- if (!this.isNative) {
45
- this.webAudio.src = '';
46
- this.webAudio.load();
47
- this.webAudio.play().catch(() => { });
48
- }
49
- }
50
- static play(song_1) {
51
- return __awaiter(this, arguments, void 0, function* (song, index = 0) {
52
- var _a;
53
- if (!song || !song.downloadUrl)
54
- return;
55
- this.currentSong = song;
56
- this.currentIndex = index;
57
- let url = ((_a = song.downloadUrl[this.selectedQuality]) === null || _a === void 0 ? void 0 : _a.url) || '';
58
- // 🚀 Auto-convert http → https
59
- if (url.startsWith('http://')) {
60
- url = url.replace('http://', 'https://');
61
- }
62
- try {
63
- if (this.isNative) {
64
- yield this.playNative(url, song);
65
- }
66
- else {
67
- this.playWeb(url, song);
68
- }
69
- this.isPlaying = true;
70
- keep_awake_1.KeepAwake.keepAwake(); // Keep CPU awake for streaming
14
+ /** Setup audio element for better compatibility */
15
+ static setupAudioElement() {
16
+ // Enable background playback
17
+ this.audio.preload = 'auto';
18
+ // @ts-ignore - Some browsers support this
19
+ this.audio.preservesPitch = false;
20
+ // Setup event listeners
21
+ this.audio.onloadedmetadata = () => {
22
+ this.duration = this.audio.duration;
23
+ this.updatePositionState();
24
+ };
25
+ this.audio.ontimeupdate = () => {
26
+ this.currentTime = this.audio.currentTime;
27
+ if (Math.floor(this.currentTime) % 5 === 0) {
28
+ this.updatePositionState();
71
29
  }
72
- catch (err) {
73
- this.isPlaying = false;
74
- console.warn('Audio play failed:', err);
30
+ };
31
+ this.audio.onended = () => {
32
+ this.autoNext();
33
+ };
34
+ this.audio.onplaying = () => {
35
+ this.isPlaying = true;
36
+ if ('mediaSession' in navigator) {
37
+ navigator.mediaSession.playbackState = 'playing';
75
38
  }
76
- });
77
- }
78
- static playNative(url, song) {
79
- return __awaiter(this, void 0, void 0, function* () {
80
- var _a;
81
- try {
82
- // Unload previous if any (though preload might handle it, safer to be clean)
83
- // NativeAudio.unload({ assetId: 'currentSong' }).catch(() => {});
84
- // Prepare artwork for lock screen
85
- let artworkPath = '';
86
- if (song.image) {
87
- if (Array.isArray(song.image)) {
88
- // Get highest quality image
89
- const img = song.image[song.image.length - 1];
90
- artworkPath = img.link || img.url || (typeof img === 'string' ? img : '');
91
- }
92
- else if (typeof song.image === 'string') {
93
- artworkPath = song.image;
94
- }
95
- }
96
- if (artworkPath.startsWith('http://')) {
97
- artworkPath = artworkPath.replace('http://', 'https://');
98
- }
99
- yield native_audio_1.NativeAudio.preload({
100
- assetId: 'currentSong',
101
- assetPath: url,
102
- isUrl: true,
103
- audioChannelNum: 1,
104
- // Metadata for Lock Screen
105
- notificationMetadata: {
106
- album: ((_a = song.album) === null || _a === void 0 ? void 0 : _a.name) || song.album || 'Unknown Album',
107
- artist: song.primaryArtists || song.artist || 'Unknown Artist',
108
- title: song.name || song.title || 'Unknown Title',
109
- artworkUrl: artworkPath
110
- }
111
- });
112
- yield native_audio_1.NativeAudio.play({ assetId: 'currentSong' });
39
+ };
40
+ this.audio.onpause = () => {
41
+ this.isPlaying = false;
42
+ if ('mediaSession' in navigator) {
43
+ navigator.mediaSession.playbackState = 'paused';
113
44
  }
114
- catch (e) {
115
- console.error("Native play error", e);
116
- throw e;
45
+ };
46
+ this.audio.onwaiting = () => {
47
+ if ('mediaSession' in navigator) {
48
+ navigator.mediaSession.playbackState = 'none';
117
49
  }
118
- });
50
+ };
51
+ this.audio.onerror = (e) => {
52
+ console.error('Audio error:', this.audio.error, e);
53
+ };
119
54
  }
120
- static playWeb(url, song) {
121
- this.webAudio.src = url;
122
- // @ts-ignore
123
- this.webAudio.title = song.name || song.title || 'Unknown Title';
124
- this.webAudio.preload = 'auto';
125
- this.webAudio.load();
126
- this.webAudio.play();
127
- // Basic MediaSession for Web
128
- if ('mediaSession' in navigator) {
129
- this.updateWebMediaSession(song);
55
+ /** Call this once on user gesture to unlock audio in WebView */
56
+ static unlockAudio() {
57
+ this.audio.src = '';
58
+ this.audio.load();
59
+ this.audio.play().catch(() => { });
60
+ }
61
+ static play(song, index = 0) {
62
+ var _a;
63
+ if (!song || !song.downloadUrl)
64
+ return;
65
+ this.currentSong = song;
66
+ this.currentIndex = index;
67
+ let url = ((_a = song.downloadUrl[this.selectedQuality]) === null || _a === void 0 ? void 0 : _a.url) || '';
68
+ // 🚀 Auto-convert http → https
69
+ if (url.startsWith('http://')) {
70
+ url = url.replace('http://', 'https://');
130
71
  }
72
+ this.audio.src = url;
73
+ this.audio.load();
74
+ this.audio.play().then(() => {
75
+ this.isPlaying = true;
76
+ this.updateMediaSessionMetadata(song);
77
+ keep_awake_1.KeepAwake.keepAwake(); // Keep screen/CPU awake
78
+ if ('mediaSession' in navigator) {
79
+ navigator.mediaSession.playbackState = 'playing';
80
+ }
81
+ }).catch((err) => {
82
+ this.isPlaying = false;
83
+ console.warn('Audio play failed:', err);
84
+ });
131
85
  }
132
86
  static pause() {
133
- if (this.isNative) {
134
- native_audio_1.NativeAudio.pause({ assetId: 'currentSong' });
135
- }
136
- else {
137
- this.webAudio.pause();
138
- }
87
+ this.audio.pause();
139
88
  this.isPlaying = false;
140
89
  keep_awake_1.KeepAwake.allowSleep();
90
+ if ('mediaSession' in navigator) {
91
+ navigator.mediaSession.playbackState = 'paused';
92
+ }
141
93
  }
142
94
  static resume() {
143
- if (this.isNative) {
144
- native_audio_1.NativeAudio.resume({ assetId: 'currentSong' });
145
- }
146
- else {
147
- this.webAudio.play();
148
- }
95
+ this.audio.play();
149
96
  this.isPlaying = true;
150
97
  keep_awake_1.KeepAwake.keepAwake();
98
+ if ('mediaSession' in navigator) {
99
+ navigator.mediaSession.playbackState = 'playing';
100
+ }
151
101
  }
152
102
  static togglePlayPause() {
153
103
  if (this.isPlaying) {
@@ -177,13 +127,8 @@ class Player {
177
127
  }
178
128
  }
179
129
  static seek(seconds) {
180
- if (this.isNative) {
181
- native_audio_1.NativeAudio.setCurrentTime({ assetId: 'currentSong', time: seconds });
182
- }
183
- else {
184
- this.webAudio.currentTime = seconds;
185
- }
186
- this.currentTime = seconds;
130
+ this.audio.currentTime = seconds;
131
+ this.updatePositionState();
187
132
  }
188
133
  static autoNext() {
189
134
  this.next();
@@ -245,62 +190,75 @@ class Player {
245
190
  return this.playlist;
246
191
  }
247
192
  // -------------------------------------------------------------------------
248
- // Listeners
193
+ // Native Media Session (Lock Screen Controls)
249
194
  // -------------------------------------------------------------------------
250
- static setupNativeListeners() {
251
- // Song Finished
252
- native_audio_1.NativeAudio.addListener('complete', (result) => {
253
- if (result.assetId === 'currentSong') {
254
- this.autoNext();
255
- }
256
- });
257
- // Time Update (Progress) - Note: Plugin might not emit this frequently
258
- // We might need to poll for current time if the plugin doesn't emit 'progress'
259
- // @capgo/native-audio usually emits 'progress' or we use getCurrentTime
260
- // Checking docs: usually we poll or listen to 'progress'
261
- // Assuming 'progress' event exists or we use setInterval
262
- setInterval(() => __awaiter(this, void 0, void 0, function* () {
263
- if (this.isPlaying && this.isNative) {
264
- try {
265
- const result = yield native_audio_1.NativeAudio.getCurrentTime({ assetId: 'currentSong' });
266
- this.currentTime = result.currentTime;
267
- const durResult = yield native_audio_1.NativeAudio.getDuration({ assetId: 'currentSong' });
268
- this.duration = durResult.duration;
195
+ static setupMediaSession() {
196
+ if ('mediaSession' in navigator) {
197
+ navigator.mediaSession.setActionHandler('play', () => this.resume());
198
+ navigator.mediaSession.setActionHandler('pause', () => this.pause());
199
+ navigator.mediaSession.setActionHandler('previoustrack', () => this.prev());
200
+ navigator.mediaSession.setActionHandler('nexttrack', () => this.next());
201
+ navigator.mediaSession.setActionHandler('seekto', (details) => {
202
+ if (details.seekTime !== undefined) {
203
+ this.seek(details.seekTime);
269
204
  }
270
- catch (e) { }
271
- }
272
- }), 1000);
273
- }
274
- static setupWebListeners() {
275
- this.webAudio.onended = () => this.autoNext();
276
- this.webAudio.ontimeupdate = () => {
277
- this.currentTime = this.webAudio.currentTime;
278
- this.duration = this.webAudio.duration || 0;
279
- };
280
- this.webAudio.onplaying = () => this.isPlaying = true;
281
- this.webAudio.onpause = () => this.isPlaying = false;
205
+ });
206
+ }
282
207
  }
283
- static updateWebMediaSession(song) {
284
- var _a;
285
- // ... (Keep existing Web MediaSession logic if needed, or simplify)
286
- // Since we are focusing on Native, we can keep the basic one or copy the previous logic
287
- // For brevity, I'll omit the full implementation here as Native is the priority
288
- // But to be safe, let's keep a minimal version
208
+ static updateMediaSessionMetadata(song) {
209
+ var _a, _b;
289
210
  if ('mediaSession' in navigator) {
211
+ // Extract artwork from image array
212
+ const artwork = [];
213
+ if (song.image && Array.isArray(song.image)) {
214
+ // Get the highest quality image (last in array, usually 500x500)
215
+ const highQualityImage = song.image[song.image.length - 1];
216
+ if (highQualityImage && highQualityImage.url) {
217
+ let src = highQualityImage.url;
218
+ if (src.startsWith('http://')) {
219
+ src = src.replace('http://', 'https://');
220
+ }
221
+ // Skip placeholder images
222
+ if (!src.includes('_i/share-image')) {
223
+ artwork.push({
224
+ src,
225
+ sizes: highQualityImage.quality || '500x500',
226
+ type: 'image/jpeg'
227
+ });
228
+ }
229
+ }
230
+ }
231
+ // Extract artist name from artists.primary array
232
+ let artistName = 'Unknown Artist';
233
+ if (((_a = song.artists) === null || _a === void 0 ? void 0 : _a.primary) && Array.isArray(song.artists.primary) && song.artists.primary.length > 0) {
234
+ artistName = song.artists.primary.map((artist) => artist.name).join(', ');
235
+ }
236
+ else if (song.primaryArtists) {
237
+ artistName = song.primaryArtists;
238
+ }
239
+ else if (song.artist) {
240
+ artistName = song.artist;
241
+ }
290
242
  navigator.mediaSession.metadata = new MediaMetadata({
291
243
  title: song.name || song.title || 'Unknown Title',
292
- artist: song.primaryArtists || song.artist || 'Unknown Artist',
293
- album: ((_a = song.album) === null || _a === void 0 ? void 0 : _a.name) || song.album || '',
294
- artwork: [{ src: 'https://via.placeholder.com/500', sizes: '500x500', type: 'image/png' }] // Placeholder
244
+ artist: artistName,
245
+ album: ((_b = song.album) === null || _b === void 0 ? void 0 : _b.name) || 'Unknown Album',
246
+ artwork: artwork.length > 0 ? artwork : undefined
247
+ });
248
+ }
249
+ }
250
+ static updatePositionState() {
251
+ if ('mediaSession' in navigator && this.duration > 0) {
252
+ navigator.mediaSession.setPositionState({
253
+ duration: this.duration,
254
+ playbackRate: this.audio.playbackRate,
255
+ position: this.audio.currentTime
295
256
  });
296
- navigator.mediaSession.setActionHandler('play', () => this.resume());
297
- navigator.mediaSession.setActionHandler('pause', () => this.pause());
298
- navigator.mediaSession.setActionHandler('previoustrack', () => this.prev());
299
- navigator.mediaSession.setActionHandler('nexttrack', () => this.next());
300
257
  }
301
258
  }
302
259
  }
303
260
  exports.Player = Player;
261
+ Player.audio = new Audio();
304
262
  Player.currentSong = null;
305
263
  Player.currentIndex = 0;
306
264
  Player.isPlaying = false;
@@ -311,5 +269,3 @@ Player.queue = [];
311
269
  Player.queue$ = new rxjs_1.BehaviorSubject([]);
312
270
  Player.playlist = [];
313
271
  Player.selectedQuality = 3;
314
- Player.isNative = core_1.Capacitor.isNativePlatform();
315
- Player.webAudio = new Audio(); // Fallback for web
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunzo-player",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
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",
@@ -30,7 +30,6 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@capacitor-community/keep-awake": "^7.1.0",
33
- "@capgo/native-audio": "^7.9.9",
34
33
  "rxjs": "^7.8.2"
35
34
  }
36
35
  }
@@ -1,9 +1,8 @@
1
1
  import { BehaviorSubject } from 'rxjs';
2
- import { NativeAudio } from '@capgo/native-audio';
3
2
  import { KeepAwake } from '@capacitor-community/keep-awake';
4
- import { Capacitor } from '@capacitor/core';
5
3
 
6
4
  export class Player {
5
+ private static audio = new Audio();
7
6
  private static currentSong: any = null;
8
7
  private static currentIndex = 0;
9
8
  private static isPlaying = false;
@@ -14,41 +13,72 @@ export class Player {
14
13
  static queue$ = new BehaviorSubject<any[]>([]);
15
14
  private static playlist: any[] = [];
16
15
  private static selectedQuality = 3;
17
- private static isNative = Capacitor.isNativePlatform();
18
- private static webAudio = new Audio(); // Fallback for web
19
16
 
20
17
  /** Initialize with playlist and quality */
21
- static async initialize(playlist: any[], quality = 3) {
18
+ static initialize(playlist: any[], quality = 3) {
22
19
  this.playlist = playlist;
23
20
  this.selectedQuality = quality;
21
+ this.setupMediaSession();
22
+ this.setupAudioElement();
23
+ }
24
+
25
+ /** Setup audio element for better compatibility */
26
+ private static setupAudioElement() {
27
+ // Enable background playback
28
+ this.audio.preload = 'auto';
29
+ // @ts-ignore - Some browsers support this
30
+ this.audio.preservesPitch = false;
31
+
32
+ // Setup event listeners
33
+ this.audio.onloadedmetadata = () => {
34
+ this.duration = this.audio.duration;
35
+ this.updatePositionState();
36
+ };
24
37
 
25
- // Configure native audio if on native platform
26
- if (this.isNative) {
27
- try {
28
- await NativeAudio.configure({
29
- showNotification: true,
30
- background: true,
31
- focus: true
32
- });
33
- } catch (e) {
34
- console.warn('NativeAudio configure failed:', e);
38
+ this.audio.ontimeupdate = () => {
39
+ this.currentTime = this.audio.currentTime;
40
+ if (Math.floor(this.currentTime) % 5 === 0) {
41
+ this.updatePositionState();
35
42
  }
36
- this.setupNativeListeners();
37
- } else {
38
- this.setupWebListeners();
39
- }
43
+ };
44
+
45
+ this.audio.onended = () => {
46
+ this.autoNext();
47
+ };
48
+
49
+ this.audio.onplaying = () => {
50
+ this.isPlaying = true;
51
+ if ('mediaSession' in navigator) {
52
+ navigator.mediaSession.playbackState = 'playing';
53
+ }
54
+ };
55
+
56
+ this.audio.onpause = () => {
57
+ this.isPlaying = false;
58
+ if ('mediaSession' in navigator) {
59
+ navigator.mediaSession.playbackState = 'paused';
60
+ }
61
+ };
62
+
63
+ this.audio.onwaiting = () => {
64
+ if ('mediaSession' in navigator) {
65
+ navigator.mediaSession.playbackState = 'none';
66
+ }
67
+ };
68
+
69
+ this.audio.onerror = (e) => {
70
+ console.error('Audio error:', this.audio.error, e);
71
+ };
40
72
  }
41
73
 
42
- /** Call this once on user gesture to unlock audio in WebView (Web only) */
74
+ /** Call this once on user gesture to unlock audio in WebView */
43
75
  static unlockAudio() {
44
- if (!this.isNative) {
45
- this.webAudio.src = '';
46
- this.webAudio.load();
47
- this.webAudio.play().catch(() => { });
48
- }
76
+ this.audio.src = '';
77
+ this.audio.load();
78
+ this.audio.play().catch(() => { });
49
79
  }
50
80
 
51
- static async play(song: any, index: number = 0) {
81
+ static play(song: any, index: number = 0) {
52
82
  if (!song || !song.downloadUrl) return;
53
83
 
54
84
  this.currentSong = song;
@@ -61,94 +91,38 @@ export class Player {
61
91
  url = url.replace('http://', 'https://');
62
92
  }
63
93
 
64
- try {
65
- if (this.isNative) {
66
- await this.playNative(url, song);
67
- } else {
68
- this.playWeb(url, song);
69
- }
94
+ this.audio.src = url;
95
+ this.audio.load();
96
+
97
+ this.audio.play().then(() => {
70
98
  this.isPlaying = true;
71
- KeepAwake.keepAwake(); // Keep CPU awake for streaming
72
- } catch (err) {
99
+ this.updateMediaSessionMetadata(song);
100
+ KeepAwake.keepAwake(); // Keep screen/CPU awake
101
+ if ('mediaSession' in navigator) {
102
+ navigator.mediaSession.playbackState = 'playing';
103
+ }
104
+ }).catch((err) => {
73
105
  this.isPlaying = false;
74
106
  console.warn('Audio play failed:', err);
75
- }
76
- }
77
-
78
- private static async playNative(url: string, song: any) {
79
- try {
80
- // Unload previous if any (though preload might handle it, safer to be clean)
81
- // NativeAudio.unload({ assetId: 'currentSong' }).catch(() => {});
82
-
83
- // Prepare artwork for lock screen
84
- let artworkPath = '';
85
- if (song.image) {
86
- if (Array.isArray(song.image)) {
87
- // Get highest quality image
88
- const img = song.image[song.image.length - 1];
89
- artworkPath = img.link || img.url || (typeof img === 'string' ? img : '');
90
- } else if (typeof song.image === 'string') {
91
- artworkPath = song.image;
92
- }
93
- }
94
- if (artworkPath.startsWith('http://')) {
95
- artworkPath = artworkPath.replace('http://', 'https://');
96
- }
97
-
98
- await NativeAudio.preload({
99
- assetId: 'currentSong',
100
- assetPath: url,
101
- isUrl: true,
102
- audioChannelNum: 1,
103
- // Metadata for Lock Screen
104
- notificationMetadata: {
105
- album: song.album?.name || song.album || 'Unknown Album',
106
- artist: song.primaryArtists || song.artist || 'Unknown Artist',
107
- title: song.name || song.title || 'Unknown Title',
108
- artworkUrl: artworkPath
109
- }
110
- });
111
-
112
- await NativeAudio.play({ assetId: 'currentSong' });
113
-
114
- } catch (e) {
115
- console.error("Native play error", e);
116
- throw e;
117
- }
118
- }
119
-
120
- private static playWeb(url: string, song: any) {
121
- this.webAudio.src = url;
122
- // @ts-ignore
123
- this.webAudio.title = song.name || song.title || 'Unknown Title';
124
- this.webAudio.preload = 'auto';
125
- this.webAudio.load();
126
- this.webAudio.play();
127
-
128
- // Basic MediaSession for Web
129
- if ('mediaSession' in navigator) {
130
- this.updateWebMediaSession(song);
131
- }
107
+ });
132
108
  }
133
109
 
134
110
  static pause() {
135
- if (this.isNative) {
136
- NativeAudio.pause({ assetId: 'currentSong' });
137
- } else {
138
- this.webAudio.pause();
139
- }
111
+ this.audio.pause();
140
112
  this.isPlaying = false;
141
113
  KeepAwake.allowSleep();
114
+ if ('mediaSession' in navigator) {
115
+ navigator.mediaSession.playbackState = 'paused';
116
+ }
142
117
  }
143
118
 
144
119
  static resume() {
145
- if (this.isNative) {
146
- NativeAudio.resume({ assetId: 'currentSong' });
147
- } else {
148
- this.webAudio.play();
149
- }
120
+ this.audio.play();
150
121
  this.isPlaying = true;
151
122
  KeepAwake.keepAwake();
123
+ if ('mediaSession' in navigator) {
124
+ navigator.mediaSession.playbackState = 'playing';
125
+ }
152
126
  }
153
127
 
154
128
  static togglePlayPause() {
@@ -179,12 +153,8 @@ export class Player {
179
153
  }
180
154
 
181
155
  static seek(seconds: number) {
182
- if (this.isNative) {
183
- NativeAudio.setCurrentTime({ assetId: 'currentSong', time: seconds });
184
- } else {
185
- this.webAudio.currentTime = seconds;
186
- }
187
- this.currentTime = seconds;
156
+ this.audio.currentTime = seconds;
157
+ this.updatePositionState();
188
158
  }
189
159
 
190
160
  static autoNext() {
@@ -261,61 +231,72 @@ export class Player {
261
231
  }
262
232
 
263
233
  // -------------------------------------------------------------------------
264
- // Listeners
234
+ // Native Media Session (Lock Screen Controls)
265
235
  // -------------------------------------------------------------------------
266
236
 
267
- private static setupNativeListeners() {
268
- // Song Finished
269
- NativeAudio.addListener('complete', (result) => {
270
- if (result.assetId === 'currentSong') {
271
- this.autoNext();
272
- }
273
- });
237
+ private static setupMediaSession() {
238
+ if ('mediaSession' in navigator) {
239
+ navigator.mediaSession.setActionHandler('play', () => this.resume());
240
+ navigator.mediaSession.setActionHandler('pause', () => this.pause());
241
+ navigator.mediaSession.setActionHandler('previoustrack', () => this.prev());
242
+ navigator.mediaSession.setActionHandler('nexttrack', () => this.next());
243
+ navigator.mediaSession.setActionHandler('seekto', (details) => {
244
+ if (details.seekTime !== undefined) {
245
+ this.seek(details.seekTime);
246
+ }
247
+ });
248
+ }
249
+ }
274
250
 
275
- // Time Update (Progress) - Note: Plugin might not emit this frequently
276
- // We might need to poll for current time if the plugin doesn't emit 'progress'
277
- // @capgo/native-audio usually emits 'progress' or we use getCurrentTime
278
- // Checking docs: usually we poll or listen to 'progress'
279
- // Assuming 'progress' event exists or we use setInterval
280
- setInterval(async () => {
281
- if (this.isPlaying && this.isNative) {
282
- try {
283
- const result = await NativeAudio.getCurrentTime({ assetId: 'currentSong' });
284
- this.currentTime = result.currentTime;
285
-
286
- const durResult = await NativeAudio.getDuration({ assetId: 'currentSong' });
287
- this.duration = durResult.duration;
288
- } catch (e) { }
251
+ private static updateMediaSessionMetadata(song: any) {
252
+ if ('mediaSession' in navigator) {
253
+ // Extract artwork from image array
254
+ const artwork = [];
255
+ if (song.image && Array.isArray(song.image)) {
256
+ // Get the highest quality image (last in array, usually 500x500)
257
+ const highQualityImage = song.image[song.image.length - 1];
258
+ if (highQualityImage && highQualityImage.url) {
259
+ let src = highQualityImage.url;
260
+ if (src.startsWith('http://')) {
261
+ src = src.replace('http://', 'https://');
262
+ }
263
+ // Skip placeholder images
264
+ if (!src.includes('_i/share-image')) {
265
+ artwork.push({
266
+ src,
267
+ sizes: highQualityImage.quality || '500x500',
268
+ type: 'image/jpeg'
269
+ });
270
+ }
271
+ }
289
272
  }
290
- }, 1000);
291
- }
292
273
 
293
- private static setupWebListeners() {
294
- this.webAudio.onended = () => this.autoNext();
295
- this.webAudio.ontimeupdate = () => {
296
- this.currentTime = this.webAudio.currentTime;
297
- this.duration = this.webAudio.duration || 0;
298
- };
299
- this.webAudio.onplaying = () => this.isPlaying = true;
300
- this.webAudio.onpause = () => this.isPlaying = false;
301
- }
274
+ // Extract artist name from artists.primary array
275
+ let artistName = 'Unknown Artist';
276
+ if (song.artists?.primary && Array.isArray(song.artists.primary) && song.artists.primary.length > 0) {
277
+ artistName = song.artists.primary.map((artist: any) => artist.name).join(', ');
278
+ } else if (song.primaryArtists) {
279
+ artistName = song.primaryArtists;
280
+ } else if (song.artist) {
281
+ artistName = song.artist;
282
+ }
302
283
 
303
- private static updateWebMediaSession(song: any) {
304
- // ... (Keep existing Web MediaSession logic if needed, or simplify)
305
- // Since we are focusing on Native, we can keep the basic one or copy the previous logic
306
- // For brevity, I'll omit the full implementation here as Native is the priority
307
- // But to be safe, let's keep a minimal version
308
- if ('mediaSession' in navigator) {
309
284
  navigator.mediaSession.metadata = new MediaMetadata({
310
285
  title: song.name || song.title || 'Unknown Title',
311
- artist: song.primaryArtists || song.artist || 'Unknown Artist',
312
- album: song.album?.name || song.album || '',
313
- artwork: [{ src: 'https://via.placeholder.com/500', sizes: '500x500', type: 'image/png' }] // Placeholder
286
+ artist: artistName,
287
+ album: song.album?.name || 'Unknown Album',
288
+ artwork: artwork.length > 0 ? artwork : undefined
289
+ });
290
+ }
291
+ }
292
+
293
+ private static updatePositionState() {
294
+ if ('mediaSession' in navigator && this.duration > 0) {
295
+ navigator.mediaSession.setPositionState({
296
+ duration: this.duration,
297
+ playbackRate: this.audio.playbackRate,
298
+ position: this.audio.currentTime
314
299
  });
315
- navigator.mediaSession.setActionHandler('play', () => this.resume());
316
- navigator.mediaSession.setActionHandler('pause', () => this.pause());
317
- navigator.mediaSession.setActionHandler('previoustrack', () => this.prev());
318
- navigator.mediaSession.setActionHandler('nexttrack', () => this.next());
319
300
  }
320
301
  }
321
302
  }