@capgo/capacitor-native-audio 8.4.3
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/CapgoCapacitorNativeAudio.podspec +16 -0
- package/LICENSE +373 -0
- package/Package.swift +31 -0
- package/README.md +1229 -0
- package/android/build.gradle +89 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ee/forgr/audio/AudioAsset.java +611 -0
- package/android/src/main/java/ee/forgr/audio/AudioCompletionListener.java +5 -0
- package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +208 -0
- package/android/src/main/java/ee/forgr/audio/Constant.java +36 -0
- package/android/src/main/java/ee/forgr/audio/HlsAvailabilityChecker.java +84 -0
- package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +2022 -0
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +886 -0
- package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +708 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +3 -0
- package/dist/docs.json +1470 -0
- package/dist/esm/audio-asset.d.ts +4 -0
- package/dist/esm/audio-asset.js +6 -0
- package/dist/esm/audio-asset.js.map +1 -0
- package/dist/esm/definitions.d.ts +597 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +82 -0
- package/dist/esm/web.js +553 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +571 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +574 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +157 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +403 -0
- package/ios/Sources/NativeAudioPlugin/Constant.swift +52 -0
- package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +1786 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +152 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +405 -0
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +648 -0
- package/ios/Tests/README.md +39 -0
- package/package.json +101 -0
- package/scripts/configure-dependencies.js +251 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
package ee.forgr.audio;
|
|
2
|
+
|
|
3
|
+
import android.net.Uri;
|
|
4
|
+
import android.os.Handler;
|
|
5
|
+
import android.os.Looper;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
import androidx.media3.common.MediaItem;
|
|
8
|
+
import androidx.media3.common.PlaybackException;
|
|
9
|
+
import androidx.media3.common.PlaybackParameters;
|
|
10
|
+
import androidx.media3.common.Player;
|
|
11
|
+
import androidx.media3.common.util.UnstableApi;
|
|
12
|
+
import androidx.media3.datasource.DefaultHttpDataSource;
|
|
13
|
+
import androidx.media3.exoplayer.DefaultLivePlaybackSpeedControl;
|
|
14
|
+
import androidx.media3.exoplayer.DefaultLoadControl;
|
|
15
|
+
import androidx.media3.exoplayer.ExoPlayer;
|
|
16
|
+
import androidx.media3.exoplayer.hls.HlsMediaSource;
|
|
17
|
+
|
|
18
|
+
@UnstableApi
|
|
19
|
+
public class StreamAudioAsset extends AudioAsset {
|
|
20
|
+
|
|
21
|
+
private static final String TAG = "StreamAudioAsset";
|
|
22
|
+
private ExoPlayer player;
|
|
23
|
+
private final Uri uri;
|
|
24
|
+
private float volume;
|
|
25
|
+
private boolean isPrepared = false;
|
|
26
|
+
private final float initialVolume;
|
|
27
|
+
private static final long LIVE_OFFSET_MS = 5000; // 5 seconds behind live
|
|
28
|
+
private final java.util.Map<String, String> headers;
|
|
29
|
+
|
|
30
|
+
public StreamAudioAsset(NativeAudio owner, String assetId, Uri uri, float volume, java.util.Map<String, String> headers)
|
|
31
|
+
throws Exception {
|
|
32
|
+
super(owner, assetId, null, 0, volume);
|
|
33
|
+
this.uri = uri;
|
|
34
|
+
this.volume = volume;
|
|
35
|
+
this.initialVolume = volume;
|
|
36
|
+
this.headers = headers;
|
|
37
|
+
|
|
38
|
+
createPlayer();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private void createPlayer() {
|
|
42
|
+
// Adjust buffer settings for smoother playback
|
|
43
|
+
DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
|
|
44
|
+
.setBufferDurationsMs(
|
|
45
|
+
60000, // Increase min buffer to 60s
|
|
46
|
+
180000, // Increase max buffer to 180s
|
|
47
|
+
5000, // Increase buffer for playback
|
|
48
|
+
10000 // Increase buffer to start playback
|
|
49
|
+
)
|
|
50
|
+
.setPrioritizeTimeOverSizeThresholds(true)
|
|
51
|
+
.setBackBuffer(60000, true) // Increase back buffer
|
|
52
|
+
.build();
|
|
53
|
+
|
|
54
|
+
player = new ExoPlayer.Builder(owner.getContext())
|
|
55
|
+
.setLoadControl(loadControl)
|
|
56
|
+
.setLivePlaybackSpeedControl(
|
|
57
|
+
new DefaultLivePlaybackSpeedControl.Builder()
|
|
58
|
+
.setFallbackMaxPlaybackSpeed(1.04f)
|
|
59
|
+
.setMaxLiveOffsetErrorMsForUnitSpeed(LIVE_OFFSET_MS)
|
|
60
|
+
.build()
|
|
61
|
+
)
|
|
62
|
+
.build();
|
|
63
|
+
|
|
64
|
+
player.setVolume(volume);
|
|
65
|
+
initializePlayer();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private void initializePlayer() {
|
|
69
|
+
logger.debug("Initializing stream player with volume: " + volume);
|
|
70
|
+
|
|
71
|
+
// Configure HLS source with better settings for live streaming
|
|
72
|
+
DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
|
|
73
|
+
.setAllowCrossProtocolRedirects(true)
|
|
74
|
+
.setConnectTimeoutMs(15000)
|
|
75
|
+
.setReadTimeoutMs(15000)
|
|
76
|
+
.setUserAgent("ExoPlayer");
|
|
77
|
+
|
|
78
|
+
// Add custom headers if provided
|
|
79
|
+
if (headers != null && !headers.isEmpty()) {
|
|
80
|
+
httpDataSourceFactory.setDefaultRequestProperties(headers);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
HlsMediaSource mediaSource = new HlsMediaSource.Factory(httpDataSourceFactory)
|
|
84
|
+
.setAllowChunklessPreparation(true)
|
|
85
|
+
.setTimestampAdjusterInitializationTimeoutMs(LIVE_OFFSET_MS) // 30 seconds timeout
|
|
86
|
+
.createMediaSource(MediaItem.fromUri(uri));
|
|
87
|
+
|
|
88
|
+
player.setMediaSource(mediaSource);
|
|
89
|
+
player.setVolume(volume);
|
|
90
|
+
player.prepare();
|
|
91
|
+
|
|
92
|
+
player.addListener(
|
|
93
|
+
new Player.Listener() {
|
|
94
|
+
@Override
|
|
95
|
+
public void onPlaybackStateChanged(int state) {
|
|
96
|
+
logger.debug("Stream state changed to: " + getStateString(state));
|
|
97
|
+
if (state == Player.STATE_READY && !isPrepared) {
|
|
98
|
+
isPrepared = true;
|
|
99
|
+
if (player.isCurrentMediaItemLive()) {
|
|
100
|
+
player.seekToDefaultPosition();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Override
|
|
106
|
+
public void onIsLoadingChanged(boolean isLoading) {
|
|
107
|
+
logger.debug("Loading state changed: " + isLoading);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Override
|
|
111
|
+
public void onIsPlayingChanged(boolean isPlaying) {
|
|
112
|
+
logger.debug("Playing state changed: " + isPlaying);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Override
|
|
116
|
+
public void onPlayerError(PlaybackException error) {
|
|
117
|
+
logger.error("Player error: " + error.getMessage());
|
|
118
|
+
isPrepared = false;
|
|
119
|
+
// Try to recover by recreating the player
|
|
120
|
+
owner
|
|
121
|
+
.getActivity()
|
|
122
|
+
.runOnUiThread(() -> {
|
|
123
|
+
player.release();
|
|
124
|
+
createPlayer();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private String getStateString(int state) {
|
|
132
|
+
switch (state) {
|
|
133
|
+
case Player.STATE_IDLE:
|
|
134
|
+
return "IDLE";
|
|
135
|
+
case Player.STATE_BUFFERING:
|
|
136
|
+
return "BUFFERING";
|
|
137
|
+
case Player.STATE_READY:
|
|
138
|
+
return "READY";
|
|
139
|
+
case Player.STATE_ENDED:
|
|
140
|
+
return "ENDED";
|
|
141
|
+
default:
|
|
142
|
+
return "UNKNOWN(" + state + ")";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Override
|
|
147
|
+
public void play(double time, float volume) throws Exception {
|
|
148
|
+
logger.debug("Play called with time: " + time + ", isPrepared: " + isPrepared);
|
|
149
|
+
owner
|
|
150
|
+
.getActivity()
|
|
151
|
+
.runOnUiThread(() -> {
|
|
152
|
+
if (!isPrepared) {
|
|
153
|
+
// If not prepared, wait for preparation
|
|
154
|
+
player.addListener(
|
|
155
|
+
new Player.Listener() {
|
|
156
|
+
@Override
|
|
157
|
+
public void onPlaybackStateChanged(int state) {
|
|
158
|
+
logger.debug("Play-wait state changed to: " + getStateString(state));
|
|
159
|
+
if (state == Player.STATE_READY) {
|
|
160
|
+
startPlayback(time, volume);
|
|
161
|
+
startCurrentTimeUpdates();
|
|
162
|
+
player.removeListener(this);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
startPlayback(time, volume);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private void startPlayback(double time, float volume) {
|
|
174
|
+
logger.debug("Starting playback with time: " + time);
|
|
175
|
+
if (time != 0) {
|
|
176
|
+
player.seekTo(Math.round(time * 1000));
|
|
177
|
+
} else if (player.isCurrentMediaItemLive()) {
|
|
178
|
+
player.seekToDefaultPosition();
|
|
179
|
+
}
|
|
180
|
+
player.setPlaybackParameters(new PlaybackParameters(1.0f));
|
|
181
|
+
player.setVolume(volume);
|
|
182
|
+
player.setPlayWhenReady(true);
|
|
183
|
+
startCurrentTimeUpdates();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Override
|
|
187
|
+
public boolean pause() throws Exception {
|
|
188
|
+
final boolean[] wasPlaying = { false };
|
|
189
|
+
owner
|
|
190
|
+
.getActivity()
|
|
191
|
+
.runOnUiThread(() -> {
|
|
192
|
+
cancelFade();
|
|
193
|
+
if (player != null && player.isPlaying()) {
|
|
194
|
+
player.setPlayWhenReady(false);
|
|
195
|
+
stopCurrentTimeUpdates();
|
|
196
|
+
wasPlaying[0] = true;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return wasPlaying[0];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@Override
|
|
203
|
+
public void resume() throws Exception {
|
|
204
|
+
owner
|
|
205
|
+
.getActivity()
|
|
206
|
+
.runOnUiThread(() -> {
|
|
207
|
+
player.setPlayWhenReady(true);
|
|
208
|
+
startCurrentTimeUpdates();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@Override
|
|
213
|
+
public void stop() throws Exception {
|
|
214
|
+
owner
|
|
215
|
+
.getActivity()
|
|
216
|
+
.runOnUiThread(() -> {
|
|
217
|
+
cancelFade();
|
|
218
|
+
// First stop playback
|
|
219
|
+
player.stop();
|
|
220
|
+
// Reset player state
|
|
221
|
+
player.clearMediaItems();
|
|
222
|
+
isPrepared = false;
|
|
223
|
+
|
|
224
|
+
// Create new media source
|
|
225
|
+
DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
|
|
226
|
+
.setAllowCrossProtocolRedirects(true)
|
|
227
|
+
.setConnectTimeoutMs(15000)
|
|
228
|
+
.setReadTimeoutMs(15000)
|
|
229
|
+
.setUserAgent("ExoPlayer");
|
|
230
|
+
|
|
231
|
+
// Add custom headers if provided
|
|
232
|
+
if (headers != null && !headers.isEmpty()) {
|
|
233
|
+
httpDataSourceFactory.setDefaultRequestProperties(headers);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
HlsMediaSource mediaSource = new HlsMediaSource.Factory(httpDataSourceFactory)
|
|
237
|
+
.setAllowChunklessPreparation(true)
|
|
238
|
+
.setTimestampAdjusterInitializationTimeoutMs(LIVE_OFFSET_MS)
|
|
239
|
+
.createMediaSource(MediaItem.fromUri(uri));
|
|
240
|
+
|
|
241
|
+
// Set new media source and prepare
|
|
242
|
+
player.setMediaSource(mediaSource);
|
|
243
|
+
player.prepare();
|
|
244
|
+
|
|
245
|
+
// Add listener for preparation completion
|
|
246
|
+
player.addListener(
|
|
247
|
+
new Player.Listener() {
|
|
248
|
+
@Override
|
|
249
|
+
public void onPlaybackStateChanged(int state) {
|
|
250
|
+
logger.debug("Stop-reinit state changed to: " + getStateString(state));
|
|
251
|
+
if (state == Player.STATE_READY) {
|
|
252
|
+
isPrepared = true;
|
|
253
|
+
player.removeListener(this);
|
|
254
|
+
} else if (state == Player.STATE_IDLE) {
|
|
255
|
+
// Retry preparation if it fails
|
|
256
|
+
player.prepare();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@Override
|
|
265
|
+
public void loop() throws Exception {
|
|
266
|
+
owner
|
|
267
|
+
.getActivity()
|
|
268
|
+
.runOnUiThread(() -> {
|
|
269
|
+
player.setRepeatMode(Player.REPEAT_MODE_ONE);
|
|
270
|
+
player.setPlayWhenReady(true);
|
|
271
|
+
startCurrentTimeUpdates();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@Override
|
|
276
|
+
public void unload() throws Exception {
|
|
277
|
+
owner
|
|
278
|
+
.getActivity()
|
|
279
|
+
.runOnUiThread(() -> {
|
|
280
|
+
cancelFade();
|
|
281
|
+
player.stop();
|
|
282
|
+
player.clearMediaItems();
|
|
283
|
+
player.release();
|
|
284
|
+
isPrepared = false;
|
|
285
|
+
close(); // Ensure fadeExecutor is shutdown
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Override
|
|
290
|
+
public void close() {
|
|
291
|
+
if (fadeExecutor != null && !fadeExecutor.isShutdown()) {
|
|
292
|
+
fadeExecutor.shutdown();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@Override
|
|
297
|
+
protected void finalize() throws Throwable {
|
|
298
|
+
try {
|
|
299
|
+
close();
|
|
300
|
+
} finally {
|
|
301
|
+
super.finalize();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@Override
|
|
306
|
+
public void setVolume(float volume, double duration) throws Exception {
|
|
307
|
+
this.volume = volume;
|
|
308
|
+
owner
|
|
309
|
+
.getActivity()
|
|
310
|
+
.runOnUiThread(() -> {
|
|
311
|
+
cancelFade();
|
|
312
|
+
try {
|
|
313
|
+
if (this.isPlaying() && duration > 0) {
|
|
314
|
+
fadeTo(duration, volume);
|
|
315
|
+
} else {
|
|
316
|
+
player.setVolume(volume);
|
|
317
|
+
}
|
|
318
|
+
} catch (Exception e) {
|
|
319
|
+
logger.error("Error setting volume", e);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@Override
|
|
325
|
+
public float getVolume() throws Exception {
|
|
326
|
+
if (player != null) {
|
|
327
|
+
return player.getVolume();
|
|
328
|
+
}
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@Override
|
|
333
|
+
public boolean isPlaying() throws Exception {
|
|
334
|
+
return player != null && player.isPlaying();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@Override
|
|
338
|
+
public double getDuration() {
|
|
339
|
+
if (isPrepared) {
|
|
340
|
+
final double[] duration = { 0 };
|
|
341
|
+
owner
|
|
342
|
+
.getActivity()
|
|
343
|
+
.runOnUiThread(() -> {
|
|
344
|
+
if (player.getPlaybackState() == Player.STATE_READY) {
|
|
345
|
+
long rawDuration = player.getDuration();
|
|
346
|
+
if (rawDuration != androidx.media3.common.C.TIME_UNSET) {
|
|
347
|
+
duration[0] = rawDuration / 1000.0;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return duration[0];
|
|
352
|
+
}
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@Override
|
|
357
|
+
public double getCurrentPosition() {
|
|
358
|
+
if (isPrepared) {
|
|
359
|
+
final double[] position = { 0 };
|
|
360
|
+
owner
|
|
361
|
+
.getActivity()
|
|
362
|
+
.runOnUiThread(() -> {
|
|
363
|
+
if (player.getPlaybackState() == Player.STATE_READY) {
|
|
364
|
+
position[0] = player.getCurrentPosition() / 1000.0;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return position[0];
|
|
368
|
+
}
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@Override
|
|
373
|
+
public void setCurrentTime(double time) throws Exception {
|
|
374
|
+
owner
|
|
375
|
+
.getActivity()
|
|
376
|
+
.runOnUiThread(() -> {
|
|
377
|
+
player.seekTo(Math.round(time * 1000));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@Override
|
|
382
|
+
public void playWithFadeIn(double time, float volume, double fadeInDurationMs) throws Exception {
|
|
383
|
+
logger.debug("playWithFadeIn called with time: " + time);
|
|
384
|
+
owner
|
|
385
|
+
.getActivity()
|
|
386
|
+
.runOnUiThread(() -> {
|
|
387
|
+
if (!isPrepared) {
|
|
388
|
+
// If not prepared, wait for preparation
|
|
389
|
+
player.addListener(
|
|
390
|
+
new Player.Listener() {
|
|
391
|
+
@Override
|
|
392
|
+
public void onPlaybackStateChanged(int state) {
|
|
393
|
+
if (state == Player.STATE_READY) {
|
|
394
|
+
startPlaybackWithFade(time, volume, fadeInDurationMs);
|
|
395
|
+
player.removeListener(this);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
startPlaybackWithFade(time, volume, fadeInDurationMs);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private void startPlaybackWithFade(Double time, float targetVolume, double fadeInDurationMs) {
|
|
407
|
+
if (!player.isPlayingAd()) {
|
|
408
|
+
// Make sure we're not in an ad
|
|
409
|
+
if (time != null) {
|
|
410
|
+
player.seekTo(Math.round(time * 1000));
|
|
411
|
+
} else if (player.isCurrentMediaItemLive()) {
|
|
412
|
+
long liveEdge = player.getCurrentLiveOffset();
|
|
413
|
+
if (liveEdge > 0) {
|
|
414
|
+
player.seekTo(liveEdge - LIVE_OFFSET_MS);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Wait for buffering to complete before starting playback
|
|
419
|
+
player.addListener(
|
|
420
|
+
new Player.Listener() {
|
|
421
|
+
@Override
|
|
422
|
+
public void onPlaybackStateChanged(int state) {
|
|
423
|
+
if (state == Player.STATE_READY) {
|
|
424
|
+
player.removeListener(this);
|
|
425
|
+
// Ensure playback rate is normal
|
|
426
|
+
player.setPlaybackParameters(new PlaybackParameters(1.0f));
|
|
427
|
+
// Start with volume 0
|
|
428
|
+
player.setVolume(0);
|
|
429
|
+
player.setPlayWhenReady(true);
|
|
430
|
+
startCurrentTimeUpdates();
|
|
431
|
+
// Start fade after ensuring we're actually playing
|
|
432
|
+
checkAndStartFade(fadeInDurationMs, targetVolume);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private void checkAndStartFade(double fadeInDurationMs, float volume) {
|
|
441
|
+
final Handler handler = new Handler(Looper.getMainLooper());
|
|
442
|
+
handler.postDelayed(
|
|
443
|
+
new Runnable() {
|
|
444
|
+
int attempts = 0;
|
|
445
|
+
|
|
446
|
+
@Override
|
|
447
|
+
public void run() {
|
|
448
|
+
if (player.isPlaying()) {
|
|
449
|
+
fadeIn(fadeInDurationMs, volume);
|
|
450
|
+
} else if (attempts < 10) {
|
|
451
|
+
// Try for 5 seconds (10 * 500ms)
|
|
452
|
+
attempts++;
|
|
453
|
+
handler.postDelayed(this, 500);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
500
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private void fadeIn(double fadeInDurationMs, float targetVolume) {
|
|
462
|
+
cancelFade();
|
|
463
|
+
fadeState = FadeState.FADE_IN;
|
|
464
|
+
|
|
465
|
+
final int steps = Math.max(1, (int) (fadeInDurationMs / FADE_DELAY_MS));
|
|
466
|
+
final float fadeStep = targetVolume / steps;
|
|
467
|
+
|
|
468
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
469
|
+
new Runnable() {
|
|
470
|
+
float currentVolume = 0;
|
|
471
|
+
|
|
472
|
+
@Override
|
|
473
|
+
public void run() {
|
|
474
|
+
if (fadeState != FadeState.FADE_IN || player == null || !player.isPlaying() || currentVolume >= targetVolume) {
|
|
475
|
+
fadeState = FadeState.NONE;
|
|
476
|
+
cancelFade();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
final float nextVolume = Math.min(currentVolume + fadeStep, targetVolume);
|
|
481
|
+
owner
|
|
482
|
+
.getActivity()
|
|
483
|
+
.runOnUiThread(() -> {
|
|
484
|
+
if (player != null && player.isPlaying()) {
|
|
485
|
+
player.setVolume(nextVolume);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
currentVolume = nextVolume;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
0,
|
|
492
|
+
FADE_DELAY_MS,
|
|
493
|
+
java.util.concurrent.TimeUnit.MILLISECONDS
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private void fadeTo(double fadeDurationMs, float targetVolume) {
|
|
498
|
+
cancelFade();
|
|
499
|
+
fadeState = FadeState.FADE_TO;
|
|
500
|
+
|
|
501
|
+
if (player == null) return;
|
|
502
|
+
|
|
503
|
+
final int steps = Math.max(1, (int) (fadeDurationMs / FADE_DELAY_MS));
|
|
504
|
+
final float minVolume = zeroVolume;
|
|
505
|
+
final float initialVolume = Math.max(player.getVolume(), minVolume);
|
|
506
|
+
final float finalTargetVolume = Math.max(targetVolume, minVolume);
|
|
507
|
+
final double ratio = Math.pow(finalTargetVolume / initialVolume, 1.0 / steps);
|
|
508
|
+
if (Double.isNaN(ratio) || Double.isInfinite(ratio)) {
|
|
509
|
+
player.setVolume(finalTargetVolume);
|
|
510
|
+
fadeState = FadeState.NONE;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
515
|
+
new Runnable() {
|
|
516
|
+
int currentStep = 0;
|
|
517
|
+
float currentVolume = initialVolume;
|
|
518
|
+
|
|
519
|
+
@Override
|
|
520
|
+
public void run() {
|
|
521
|
+
if (fadeState != FadeState.FADE_TO || player == null || !player.isPlaying() || currentStep >= steps) {
|
|
522
|
+
fadeState = FadeState.NONE;
|
|
523
|
+
cancelFade();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
currentVolume *= (float) ratio;
|
|
528
|
+
final float nextVolume = Math.min(Math.max(currentVolume, minVolume), maxVolume);
|
|
529
|
+
owner
|
|
530
|
+
.getActivity()
|
|
531
|
+
.runOnUiThread(() -> {
|
|
532
|
+
if (player != null && player.isPlaying()) {
|
|
533
|
+
player.setVolume(nextVolume);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
currentStep++;
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
0,
|
|
540
|
+
FADE_DELAY_MS,
|
|
541
|
+
java.util.concurrent.TimeUnit.MILLISECONDS
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@Override
|
|
546
|
+
public void stopWithFade(double fadeOutDurationMs, boolean toPause) throws Exception {
|
|
547
|
+
owner
|
|
548
|
+
.getActivity()
|
|
549
|
+
.runOnUiThread(() -> {
|
|
550
|
+
if (player != null && player.isPlaying()) {
|
|
551
|
+
fadeOut(fadeOutDurationMs, toPause);
|
|
552
|
+
} else if (!toPause) {
|
|
553
|
+
try {
|
|
554
|
+
stop();
|
|
555
|
+
} catch (Exception e) {
|
|
556
|
+
logger.error("Error stopping stream asset", e);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@Override
|
|
563
|
+
public void stopWithFade() throws Exception {
|
|
564
|
+
stopWithFade(DEFAULT_FADE_DURATION_MS, false);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private void fadeOut(double fadeOutDurationMs, boolean toPause) {
|
|
568
|
+
cancelFade();
|
|
569
|
+
fadeState = FadeState.FADE_OUT;
|
|
570
|
+
|
|
571
|
+
if (player == null) return;
|
|
572
|
+
|
|
573
|
+
final int steps = Math.max(1, (int) (fadeOutDurationMs / FADE_DELAY_MS));
|
|
574
|
+
final float initialVolume = player.getVolume();
|
|
575
|
+
final float fadeStep = initialVolume / steps;
|
|
576
|
+
|
|
577
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
578
|
+
new Runnable() {
|
|
579
|
+
float currentVolume = initialVolume;
|
|
580
|
+
|
|
581
|
+
@Override
|
|
582
|
+
public void run() {
|
|
583
|
+
if (fadeState != FadeState.FADE_OUT || player == null || currentVolume <= 0) {
|
|
584
|
+
fadeState = FadeState.NONE;
|
|
585
|
+
cancelFade();
|
|
586
|
+
owner
|
|
587
|
+
.getActivity()
|
|
588
|
+
.runOnUiThread(() -> {
|
|
589
|
+
if (player == null) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (toPause) {
|
|
593
|
+
player.setPlayWhenReady(false);
|
|
594
|
+
stopCurrentTimeUpdates();
|
|
595
|
+
} else {
|
|
596
|
+
try {
|
|
597
|
+
stop();
|
|
598
|
+
} catch (Exception e) {
|
|
599
|
+
logger.error("Error stopping stream asset after fade out", e);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
final float nextVolume = Math.max(currentVolume - fadeStep, 0f);
|
|
607
|
+
owner
|
|
608
|
+
.getActivity()
|
|
609
|
+
.runOnUiThread(() -> {
|
|
610
|
+
if (player != null) {
|
|
611
|
+
player.setVolume(nextVolume);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
currentVolume = nextVolume;
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
0,
|
|
618
|
+
FADE_DELAY_MS,
|
|
619
|
+
java.util.concurrent.TimeUnit.MILLISECONDS
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
@Override
|
|
624
|
+
public void setRate(float rate) throws Exception {
|
|
625
|
+
owner
|
|
626
|
+
.getActivity()
|
|
627
|
+
.runOnUiThread(() -> {
|
|
628
|
+
logger.debug("Setting playback rate to: " + rate);
|
|
629
|
+
player.setPlaybackParameters(new PlaybackParameters(rate));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@Override
|
|
634
|
+
protected void startCurrentTimeUpdates() {
|
|
635
|
+
logger.debug("Starting timer updates");
|
|
636
|
+
if (currentTimeHandler == null) {
|
|
637
|
+
currentTimeHandler = new Handler(Looper.getMainLooper());
|
|
638
|
+
}
|
|
639
|
+
// Reset completion status for this assetId
|
|
640
|
+
dispatchedCompleteMap.put(assetId, false);
|
|
641
|
+
|
|
642
|
+
// Wait for player to be truly ready
|
|
643
|
+
currentTimeHandler.postDelayed(
|
|
644
|
+
new Runnable() {
|
|
645
|
+
@Override
|
|
646
|
+
public void run() {
|
|
647
|
+
if (player != null && player.getPlaybackState() == Player.STATE_READY) {
|
|
648
|
+
startTimeUpdateLoop();
|
|
649
|
+
} else {
|
|
650
|
+
// Check again in 100ms
|
|
651
|
+
currentTimeHandler.postDelayed(this, 100);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
100
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private void startTimeUpdateLoop() {
|
|
660
|
+
currentTimeRunnable = new Runnable() {
|
|
661
|
+
@Override
|
|
662
|
+
public void run() {
|
|
663
|
+
try {
|
|
664
|
+
boolean isPaused = false;
|
|
665
|
+
if (player != null && player.getPlaybackState() == Player.STATE_READY) {
|
|
666
|
+
if (player.isPlaying()) {
|
|
667
|
+
double currentTime = player.getCurrentPosition() / 1000.0; // Get time directly
|
|
668
|
+
logger.debug("Play timer update: currentTime = " + currentTime);
|
|
669
|
+
if (owner != null) owner.notifyCurrentTime(assetId, currentTime);
|
|
670
|
+
currentTimeHandler.postDelayed(this, 100);
|
|
671
|
+
return;
|
|
672
|
+
} else if (!player.getPlayWhenReady()) {
|
|
673
|
+
isPaused = true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
logger.debug("Stopping play timer - not playing or not ready");
|
|
677
|
+
stopCurrentTimeUpdates();
|
|
678
|
+
if (isPaused) {
|
|
679
|
+
logger.verbose("Playback is paused, not dispatching complete");
|
|
680
|
+
} else {
|
|
681
|
+
logger.verbose("Playback is stopped, dispatching complete");
|
|
682
|
+
dispatchComplete();
|
|
683
|
+
}
|
|
684
|
+
} catch (Exception e) {
|
|
685
|
+
logger.error("Error getting current time", e);
|
|
686
|
+
stopCurrentTimeUpdates();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
try {
|
|
691
|
+
if (currentTimeHandler == null) {
|
|
692
|
+
currentTimeHandler = new Handler(Looper.getMainLooper());
|
|
693
|
+
}
|
|
694
|
+
currentTimeHandler.post(currentTimeRunnable);
|
|
695
|
+
} catch (Exception e) {
|
|
696
|
+
logger.error("Error starting current time updates", e);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
@Override
|
|
701
|
+
void stopCurrentTimeUpdates() {
|
|
702
|
+
logger.debug("Stopping play timer updates");
|
|
703
|
+
if (currentTimeHandler != null) {
|
|
704
|
+
currentTimeHandler.removeCallbacks(currentTimeRunnable);
|
|
705
|
+
currentTimeHandler = null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|