@nativescript/canvas-media 2.0.12 → 2.0.16
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/audio/common.d.ts +29 -3
- package/audio/common.js +90 -0
- package/audio/common.js.map +1 -1
- package/audio/index.android.d.ts +32 -1
- package/audio/index.android.js +219 -1
- package/audio/index.android.js.map +1 -1
- package/audio/index.ios.d.ts +27 -4
- package/audio/index.ios.js +183 -142
- package/audio/index.ios.js.map +1 -1
- package/common.d.ts +1 -0
- package/common.js +3 -0
- package/common.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/canvas_media.aar +0 -0
- package/platforms/android/java/org/nativescript/canvas/media/AudioHelper.java +338 -0
- package/platforms/android/java/org/nativescript/canvas/media/AudioView.java +125 -0
- package/platforms/android/java/org/nativescript/canvas/media/SimpleCacheProvider.java +27 -0
- package/platforms/android/java/org/nativescript/canvas/media/VideoHelper.java +60 -5
- package/platforms/ios/src/NSCAudioHelper.h +47 -0
- package/platforms/ios/src/NSCAudioHelper.m +713 -0
- package/platforms/ios/src/NSCAudioView.h +20 -0
- package/platforms/ios/src/NSCAudioView.m +270 -0
- package/platforms/ios/src/NSCCanvasMediaUmbrella.h +3 -1
- package/platforms/ios/src/NSCVideoHelper.h +4 -0
- package/platforms/ios/src/NSCVideoHelper.m +63 -5
- package/video/common.d.ts +3 -0
- package/video/common.js +3 -0
- package/video/common.js.map +1 -1
- package/video/index.android.d.ts +2 -1
- package/video/index.android.js +41 -8
- package/video/index.android.js.map +1 -1
- package/video/index.ios.d.ts +1 -0
- package/video/index.ios.js +26 -1
- package/video/index.ios.js.map +1 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
package org.nativescript.canvas.media;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.net.Uri;
|
|
5
|
+
import android.view.View;
|
|
6
|
+
import android.widget.LinearLayout;
|
|
7
|
+
import android.media.MediaCodecInfo;
|
|
8
|
+
import android.media.MediaCodecList;
|
|
9
|
+
import android.os.Handler;
|
|
10
|
+
import android.os.Looper;
|
|
11
|
+
import java.io.File;
|
|
12
|
+
import java.util.Collections;
|
|
13
|
+
import java.util.Timer;
|
|
14
|
+
import java.util.TimerTask;
|
|
15
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
16
|
+
|
|
17
|
+
import androidx.media3.common.MediaItem;
|
|
18
|
+
import androidx.media3.common.PlaybackException;
|
|
19
|
+
import androidx.media3.common.AudioAttributes;
|
|
20
|
+
import androidx.media3.common.Player;
|
|
21
|
+
import androidx.media3.common.util.UnstableApi;
|
|
22
|
+
import androidx.media3.common.util.Util;
|
|
23
|
+
import androidx.media3.datasource.DefaultDataSource;
|
|
24
|
+
import androidx.media3.datasource.DefaultHttpDataSource;
|
|
25
|
+
import androidx.media3.datasource.cache.CacheDataSource;
|
|
26
|
+
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor;
|
|
27
|
+
import androidx.media3.datasource.cache.SimpleCache;
|
|
28
|
+
import androidx.media3.database.StandaloneDatabaseProvider;
|
|
29
|
+
import androidx.media3.exoplayer.DefaultLoadControl;
|
|
30
|
+
import androidx.media3.exoplayer.ExoPlayer;
|
|
31
|
+
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
|
32
|
+
|
|
33
|
+
@UnstableApi
|
|
34
|
+
public class AudioHelper implements Player.Listener {
|
|
35
|
+
private ExoPlayer _player;
|
|
36
|
+
private DefaultDataSource.Factory _dsf;
|
|
37
|
+
private CacheDataSource.Factory _cdsf;
|
|
38
|
+
private DefaultMediaSourceFactory _msf;
|
|
39
|
+
private boolean _autoplay;
|
|
40
|
+
private boolean _loop;
|
|
41
|
+
private String _src;
|
|
42
|
+
private Timer _timer;
|
|
43
|
+
private AtomicBoolean _loadedDataFired = new AtomicBoolean(false);
|
|
44
|
+
private boolean _canPlayFired = false;
|
|
45
|
+
private boolean _canPlayThroughFired = false;
|
|
46
|
+
private Callback _callback;
|
|
47
|
+
private LeastRecentlyUsedCacheEvictor _leastRecentlyUsedCacheEvictor;
|
|
48
|
+
private StandaloneDatabaseProvider _exoDatabaseProvider;
|
|
49
|
+
private static final long _exoPlayerCacheSize = 100 * 1024 * 1024;
|
|
50
|
+
private boolean _playing = false;
|
|
51
|
+
private AudioView _audioView;
|
|
52
|
+
private LinearLayout _container;
|
|
53
|
+
|
|
54
|
+
public interface Callback {
|
|
55
|
+
void onDurationChange(long duration);
|
|
56
|
+
void onPlaying();
|
|
57
|
+
void onCurrentTimeChanged(long time);
|
|
58
|
+
void onLoadedData();
|
|
59
|
+
void onCanPlay();
|
|
60
|
+
void onCanPlayThrough();
|
|
61
|
+
void onError(String message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public void setCallback(Callback callback) {
|
|
65
|
+
this._callback = callback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Handler handler;
|
|
69
|
+
|
|
70
|
+
public AudioHelper(Context context, String cacheRoot) {
|
|
71
|
+
File cacheDir = new File(cacheRoot, "MEDIA_PLAYER_CACHE");
|
|
72
|
+
if (!cacheDir.exists()) {
|
|
73
|
+
cacheDir.mkdirs();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Looper looper1 = Looper.myLooper();
|
|
77
|
+
if (looper1 == null) {
|
|
78
|
+
Looper.prepare();
|
|
79
|
+
looper1 = Looper.myLooper();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handler = new Handler(looper1);
|
|
83
|
+
|
|
84
|
+
_leastRecentlyUsedCacheEvictor = new LeastRecentlyUsedCacheEvictor(_exoPlayerCacheSize);
|
|
85
|
+
String packageName = context.getPackageName();
|
|
86
|
+
_exoDatabaseProvider = new StandaloneDatabaseProvider(context);
|
|
87
|
+
SimpleCache cache = SimpleCacheProvider.getInstance(cacheDir, _leastRecentlyUsedCacheEvictor, _exoDatabaseProvider);
|
|
88
|
+
DefaultHttpDataSource.Factory factory = new DefaultHttpDataSource.Factory();
|
|
89
|
+
factory.setUserAgent(Util.getUserAgent(context, packageName));
|
|
90
|
+
_dsf = new DefaultDataSource.Factory(context, factory);
|
|
91
|
+
_cdsf = new CacheDataSource.Factory();
|
|
92
|
+
if (cache != null) {
|
|
93
|
+
_cdsf.setCache(cache);
|
|
94
|
+
}
|
|
95
|
+
_cdsf.setUpstreamDataSourceFactory(_dsf);
|
|
96
|
+
_msf = new DefaultMediaSourceFactory(_cdsf);
|
|
97
|
+
|
|
98
|
+
ExoPlayer.Builder builder = new ExoPlayer.Builder(context);
|
|
99
|
+
builder.setMediaSourceFactory(_msf);
|
|
100
|
+
|
|
101
|
+
DefaultLoadControl.Builder loadControl = new DefaultLoadControl.Builder();
|
|
102
|
+
loadControl.setBufferDurationsMs(500, DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, 500, 500);
|
|
103
|
+
builder.setLoadControl(loadControl.build());
|
|
104
|
+
_player = builder.build();
|
|
105
|
+
_player.addListener(this);
|
|
106
|
+
_player.setAudioAttributes(AudioAttributes.DEFAULT, true);
|
|
107
|
+
|
|
108
|
+
_container = new LinearLayout(context);
|
|
109
|
+
_container.setOrientation(LinearLayout.VERTICAL);
|
|
110
|
+
_audioView = new AudioView(context, this);
|
|
111
|
+
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
112
|
+
_container.addView(_audioView, params);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private void _setSrc(String value) {
|
|
116
|
+
try {
|
|
117
|
+
this._player.setMediaItems(Collections.singletonList(MediaItem.fromUri(Uri.parse(value))), true);
|
|
118
|
+
this._player.prepare();
|
|
119
|
+
if (this._autoplay) {
|
|
120
|
+
this._player.setPlayWhenReady(true);
|
|
121
|
+
}
|
|
122
|
+
} catch (Exception e) {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Override
|
|
126
|
+
public void onIsPlayingChanged(boolean isPlaying) {
|
|
127
|
+
long duration = this._player.getDuration();
|
|
128
|
+
if (this._callback != null) {
|
|
129
|
+
this._callback.onDurationChange(duration);
|
|
130
|
+
}
|
|
131
|
+
this._playing = isPlaying;
|
|
132
|
+
if (isPlaying) {
|
|
133
|
+
if (this._callback != null) {
|
|
134
|
+
this._callback.onPlaying();
|
|
135
|
+
this._callback.onLoadedData();
|
|
136
|
+
}
|
|
137
|
+
if (this._audioView != null) {
|
|
138
|
+
this._audioView.setPlaying(true);
|
|
139
|
+
this._audioView.setDuration(duration);
|
|
140
|
+
}
|
|
141
|
+
if (_timer != null) {
|
|
142
|
+
_timer.cancel();
|
|
143
|
+
_timer = null;
|
|
144
|
+
}
|
|
145
|
+
_timer = new Timer();
|
|
146
|
+
AudioHelper that = this;
|
|
147
|
+
_timer.schedule(new TimerTask() {
|
|
148
|
+
@Override
|
|
149
|
+
public void run() {
|
|
150
|
+
handler.post(() -> {
|
|
151
|
+
long current = _player.getCurrentPosition();
|
|
152
|
+
if (that._callback != null) {
|
|
153
|
+
that._callback.onCurrentTimeChanged(current);
|
|
154
|
+
}
|
|
155
|
+
if (that._audioView != null) {
|
|
156
|
+
that._audioView.setCurrentTime(current);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}, 0, 1000);
|
|
161
|
+
} else {
|
|
162
|
+
if (_timer != null) {
|
|
163
|
+
_timer.cancel();
|
|
164
|
+
_timer = null;
|
|
165
|
+
}
|
|
166
|
+
if (this._callback != null) {
|
|
167
|
+
this._callback.onCurrentTimeChanged(this._player.getCurrentPosition());
|
|
168
|
+
}
|
|
169
|
+
if (this._audioView != null) {
|
|
170
|
+
this._audioView.setPlaying(false);
|
|
171
|
+
this._audioView.setCurrentTime(this._player.getCurrentPosition());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Override
|
|
177
|
+
public void onPlayerError(PlaybackException error) {
|
|
178
|
+
if (this._callback != null) {
|
|
179
|
+
this._callback.onError(error == null ? "Playback error" : error.getMessage());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Override
|
|
184
|
+
public void onEvents(Player player, Player.Events events) { }
|
|
185
|
+
|
|
186
|
+
@Override
|
|
187
|
+
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
|
188
|
+
if (playbackState == Player.STATE_READY) {
|
|
189
|
+
if (!this._canPlayFired) {
|
|
190
|
+
this._canPlayFired = true;
|
|
191
|
+
if (this._callback != null) {
|
|
192
|
+
this._callback.onCanPlay();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this._loadedDataFired.compareAndSet(false, true)) {
|
|
197
|
+
if (this._callback != null) {
|
|
198
|
+
this._callback.onLoadedData();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
long duration = this._player.getDuration();
|
|
203
|
+
long buffered = this._player.getBufferedPosition();
|
|
204
|
+
if (this._audioView != null) {
|
|
205
|
+
this._audioView.setDuration(duration);
|
|
206
|
+
this._audioView.setCurrentTime(this._player.getCurrentPosition());
|
|
207
|
+
}
|
|
208
|
+
if (duration > 0 && buffered >= duration - 1000 && !this._canPlayThroughFired) {
|
|
209
|
+
this._canPlayThroughFired = true;
|
|
210
|
+
if (this._callback != null) {
|
|
211
|
+
this._callback.onCanPlayThrough();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public void play() {
|
|
218
|
+
try {
|
|
219
|
+
this._player.setPlayWhenReady(true);
|
|
220
|
+
this._player.play();
|
|
221
|
+
} catch (Exception e) { }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public void pause() {
|
|
225
|
+
try {
|
|
226
|
+
this._player.setPlayWhenReady(false);
|
|
227
|
+
this._player.pause();
|
|
228
|
+
} catch (Exception e) { }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public boolean getMuted() {
|
|
232
|
+
return this._player.getVolume() == 0f;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public void setMuted(boolean value) {
|
|
236
|
+
this._player.setVolume(value ? 0f : 1f);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public long getDuration() {
|
|
240
|
+
return this._player.getDuration();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public double getCurrentTime() {
|
|
244
|
+
return this._player.getCurrentPosition() / 1000.0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public void setCurrentTime(double value) {
|
|
248
|
+
this._player.seekTo((long) (value * 1000.0));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public String getSrc() {
|
|
252
|
+
return this._src;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public LinearLayout getContainer() {
|
|
256
|
+
return this._container;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public boolean getControls() {
|
|
260
|
+
return this._container != null && this._container.getVisibility() == View.VISIBLE;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public void setControls(boolean value) {
|
|
264
|
+
if (this._container != null) {
|
|
265
|
+
this._container.setVisibility(value ? View.VISIBLE : View.GONE);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public boolean isPlaying() {
|
|
270
|
+
return this._playing;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public void setSrc(String value) {
|
|
274
|
+
this._src = value;
|
|
275
|
+
this._loadedDataFired.set(false);
|
|
276
|
+
this._canPlayFired = false;
|
|
277
|
+
this._canPlayThroughFired = false;
|
|
278
|
+
this._setSrc(value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public void load() {
|
|
282
|
+
this.pause();
|
|
283
|
+
if (_timer != null) { _timer.cancel(); _timer = null; }
|
|
284
|
+
this._loadedDataFired.set(false);
|
|
285
|
+
if (this._src != null && !this._src.isEmpty()) {
|
|
286
|
+
this._setSrc(this._src);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public boolean getAutoplay() {
|
|
291
|
+
return this._autoplay;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public void setAutoplay(boolean value) {
|
|
295
|
+
this._autoplay = value;
|
|
296
|
+
this._player.setPlayWhenReady(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public boolean getLoop() {
|
|
300
|
+
return this._loop;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
public void setLoop(boolean value) {
|
|
304
|
+
this._loop = value;
|
|
305
|
+
if (value) {
|
|
306
|
+
this._player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
|
307
|
+
} else {
|
|
308
|
+
this._player.setRepeatMode(Player.REPEAT_MODE_OFF);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public String canPlayType(String type) {
|
|
313
|
+
if (type == null || type.trim().isEmpty()) return "";
|
|
314
|
+
String mime = type;
|
|
315
|
+
int sc = mime.indexOf(';');
|
|
316
|
+
if (sc >= 0) mime = mime.substring(0, sc).trim();
|
|
317
|
+
String t = mime.toLowerCase();
|
|
318
|
+
try {
|
|
319
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
|
320
|
+
MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS);
|
|
321
|
+
MediaCodecInfo[] infos = mcl.getCodecInfos();
|
|
322
|
+
for (MediaCodecInfo info : infos) {
|
|
323
|
+
if (info.isEncoder()) continue;
|
|
324
|
+
String[] types = info.getSupportedTypes();
|
|
325
|
+
for (String s : types) {
|
|
326
|
+
if (s.equalsIgnoreCase(t)) {
|
|
327
|
+
return "probably";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (Throwable e) {}
|
|
333
|
+
|
|
334
|
+
if (t.contains("mp3") || t.contains("mpeg") || t.contains("aac") || t.contains("wav") || t.contains("m4a") || t.contains("flac")) return "probably";
|
|
335
|
+
if (t.contains("ogg") || t.contains("opus") || t.contains("webm")) return "maybe";
|
|
336
|
+
return "";
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package org.nativescript.canvas.media;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.widget.LinearLayout;
|
|
5
|
+
import android.widget.ImageButton;
|
|
6
|
+
import android.widget.SeekBar;
|
|
7
|
+
import android.widget.TextView;
|
|
8
|
+
import android.view.View;
|
|
9
|
+
import android.view.Gravity;
|
|
10
|
+
import android.util.TypedValue;
|
|
11
|
+
import android.view.ViewGroup.LayoutParams;
|
|
12
|
+
import java.lang.ref.WeakReference;
|
|
13
|
+
|
|
14
|
+
public class AudioView extends LinearLayout {
|
|
15
|
+
private ImageButton _playButton;
|
|
16
|
+
private SeekBar _seekBar;
|
|
17
|
+
private TextView _timeLabel;
|
|
18
|
+
private long _durationMs = 0;
|
|
19
|
+
private WeakReference<AudioHelper> _helperRef;
|
|
20
|
+
private boolean _isSeeking = false;
|
|
21
|
+
|
|
22
|
+
public AudioView(Context context, AudioHelper helper) {
|
|
23
|
+
super(context);
|
|
24
|
+
this._helperRef = new WeakReference<>(helper);
|
|
25
|
+
|
|
26
|
+
setOrientation(HORIZONTAL);
|
|
27
|
+
setGravity(Gravity.CENTER_VERTICAL);
|
|
28
|
+
int pad = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
|
|
29
|
+
setPadding(pad, pad, pad, pad);
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
int minWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, getResources().getDisplayMetrics());
|
|
33
|
+
int minHeightPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());
|
|
34
|
+
setMinimumWidth(minWidthPx);
|
|
35
|
+
setMinimumHeight(minHeightPx);
|
|
36
|
+
|
|
37
|
+
_playButton = new ImageButton(context);
|
|
38
|
+
_playButton.setImageResource(android.R.drawable.ic_media_play);
|
|
39
|
+
_playButton.setBackgroundResource(0);
|
|
40
|
+
int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());
|
|
41
|
+
LayoutParams btnParams = new LayoutParams(iconSize, iconSize);
|
|
42
|
+
addView(_playButton, btnParams);
|
|
43
|
+
|
|
44
|
+
_seekBar = new SeekBar(context);
|
|
45
|
+
LayoutParams sbParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f);
|
|
46
|
+
sbParams.leftMargin = pad;
|
|
47
|
+
sbParams.rightMargin = pad;
|
|
48
|
+
addView(_seekBar, sbParams);
|
|
49
|
+
_seekBar.setEnabled(false);
|
|
50
|
+
|
|
51
|
+
_timeLabel = new TextView(context);
|
|
52
|
+
_timeLabel.setText("0:00 / 0:00");
|
|
53
|
+
addView(_timeLabel, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
|
|
54
|
+
|
|
55
|
+
_playButton.setOnClickListener(new OnClickListener() {
|
|
56
|
+
@Override
|
|
57
|
+
public void onClick(View v) {
|
|
58
|
+
AudioHelper h = _helperRef.get();
|
|
59
|
+
if (h == null) return;
|
|
60
|
+
boolean playing = h.isPlaying();
|
|
61
|
+
if (playing) {
|
|
62
|
+
h.pause();
|
|
63
|
+
} else {
|
|
64
|
+
h.play();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
_seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
|
70
|
+
@Override
|
|
71
|
+
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
|
72
|
+
if (fromUser) {
|
|
73
|
+
updateTimeLabel(progress);
|
|
74
|
+
AudioHelper h = _helperRef.get();
|
|
75
|
+
if (h == null) return;
|
|
76
|
+
float seconds = progress / 1000.0f;
|
|
77
|
+
h.setCurrentTime(seconds);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Override
|
|
82
|
+
public void onStartTrackingTouch(SeekBar seekBar) {
|
|
83
|
+
_isSeeking = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Override
|
|
87
|
+
public void onStopTrackingTouch(SeekBar seekBar) {
|
|
88
|
+
_isSeeking = false;
|
|
89
|
+
AudioHelper h = _helperRef.get();
|
|
90
|
+
if (h == null) return;
|
|
91
|
+
float seconds = seekBar.getProgress() / 1000.0f;
|
|
92
|
+
h.setCurrentTime(seconds);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public void setPlaying(boolean playing) {
|
|
98
|
+
_playButton.setImageResource(playing ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public void setDuration(long durationMs) {
|
|
102
|
+
this._durationMs = durationMs;
|
|
103
|
+
if (durationMs > 0) {
|
|
104
|
+
_seekBar.setMax((int)Math.max(durationMs, 1000));
|
|
105
|
+
}
|
|
106
|
+
updateTimeLabel((int)_seekBar.getProgress());
|
|
107
|
+
_seekBar.setEnabled(durationMs > 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public void setCurrentTime(long ms) {
|
|
111
|
+
if (_isSeeking) return;
|
|
112
|
+
_seekBar.setProgress((int)ms);
|
|
113
|
+
updateTimeLabel((int)ms);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private void updateTimeLabel(int ms) {
|
|
117
|
+
int totalSec = ms / 1000;
|
|
118
|
+
int mins = totalSec / 60;
|
|
119
|
+
int secs = totalSec % 60;
|
|
120
|
+
int durationSec = (int)(this._durationMs / 1000);
|
|
121
|
+
int dmins = durationSec / 60;
|
|
122
|
+
int dsecs = durationSec % 60;
|
|
123
|
+
_timeLabel.setText(String.format("%d:%02d / %d:%02d", mins, secs, dmins, dsecs));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package org.nativescript.canvas.media;
|
|
2
|
+
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
import java.io.File;
|
|
5
|
+
|
|
6
|
+
import androidx.media3.datasource.cache.SimpleCache;
|
|
7
|
+
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor;
|
|
8
|
+
import androidx.media3.database.DatabaseProvider;
|
|
9
|
+
|
|
10
|
+
public class SimpleCacheProvider {
|
|
11
|
+
private static SimpleCache instance = null;
|
|
12
|
+
|
|
13
|
+
public synchronized static SimpleCache getInstance(File cacheDir, LeastRecentlyUsedCacheEvictor evictor, DatabaseProvider dbProvider) {
|
|
14
|
+
if (instance == null) {
|
|
15
|
+
try {
|
|
16
|
+
instance = new SimpleCache(cacheDir, evictor, dbProvider);
|
|
17
|
+
} catch (IllegalStateException e) {
|
|
18
|
+
Log.w("SimpleCacheProvider", "SimpleCache already exists for dir: " + cacheDir + " - " + e.getMessage());
|
|
19
|
+
instance = null;
|
|
20
|
+
} catch (Exception e) {
|
|
21
|
+
Log.w("SimpleCacheProvider", "Failed to create SimpleCache: " + e.getMessage());
|
|
22
|
+
instance = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return instance;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -60,6 +60,9 @@ import androidx.media3.exoplayer.ExoPlayer;
|
|
|
60
60
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
|
61
61
|
import androidx.media3.ui.PlayerView;
|
|
62
62
|
|
|
63
|
+
import android.media.MediaCodecInfo;
|
|
64
|
+
import android.media.MediaCodecList;
|
|
65
|
+
|
|
63
66
|
import org.nativescript.canvas_media.R;
|
|
64
67
|
|
|
65
68
|
@UnstableApi
|
|
@@ -80,7 +83,7 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
80
83
|
TextureView _textureView;
|
|
81
84
|
private DefaultDataSource.Factory _dsf;
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
// SimpleCache is provided centrally to avoid duplicate instances locking the same folder.
|
|
84
87
|
private LeastRecentlyUsedCacheEvictor _leastRecentlyUsedCacheEvictor;
|
|
85
88
|
private StandaloneDatabaseProvider _exoDatabaseProvider;
|
|
86
89
|
private static final long _exoPlayerCacheSize = 100 * 1024 * 1024;
|
|
@@ -89,6 +92,8 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
89
92
|
boolean _isCustom = false;
|
|
90
93
|
boolean _playing = false;
|
|
91
94
|
private final AtomicBoolean _loadedDataFired = new AtomicBoolean(false);
|
|
95
|
+
private boolean _canPlayFired = false;
|
|
96
|
+
private boolean _canPlayThroughFired = false;
|
|
92
97
|
Timer _timer;
|
|
93
98
|
SurfaceTexture _st;
|
|
94
99
|
Surface _surface;
|
|
@@ -158,6 +163,10 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
158
163
|
|
|
159
164
|
void onLoadedData();
|
|
160
165
|
|
|
166
|
+
void onCanPlay();
|
|
167
|
+
|
|
168
|
+
void onCanPlayThrough();
|
|
169
|
+
|
|
161
170
|
void onError(String message);
|
|
162
171
|
}
|
|
163
172
|
|
|
@@ -282,14 +291,14 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
282
291
|
_leastRecentlyUsedCacheEvictor = new LeastRecentlyUsedCacheEvictor(VideoHelper._exoPlayerCacheSize);
|
|
283
292
|
String packageName = context.getPackageName();
|
|
284
293
|
_exoDatabaseProvider = new StandaloneDatabaseProvider(context);
|
|
285
|
-
|
|
286
|
-
_cache = new SimpleCache(cacheDir, _leastRecentlyUsedCacheEvictor, _exoDatabaseProvider);
|
|
287
|
-
}
|
|
294
|
+
SimpleCache cache = SimpleCacheProvider.getInstance(cacheDir, _leastRecentlyUsedCacheEvictor, _exoDatabaseProvider);
|
|
288
295
|
DefaultHttpDataSource.Factory factory = new DefaultHttpDataSource.Factory();
|
|
289
296
|
factory.setUserAgent(Util.getUserAgent(context, packageName));
|
|
290
297
|
_dsf = new DefaultDataSource.Factory(context, factory);
|
|
291
298
|
_cdsf = new CacheDataSource.Factory();
|
|
292
|
-
|
|
299
|
+
if (cache != null) {
|
|
300
|
+
_cdsf.setCache(cache);
|
|
301
|
+
}
|
|
293
302
|
_cdsf.setUpstreamDataSourceFactory(_dsf);
|
|
294
303
|
_msf = new DefaultMediaSourceFactory(_cdsf);
|
|
295
304
|
|
|
@@ -1003,6 +1012,8 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
1003
1012
|
public void setSrc(String value) {
|
|
1004
1013
|
this._src = value;
|
|
1005
1014
|
this._loadedDataFired.set(false);
|
|
1015
|
+
this._canPlayFired = false;
|
|
1016
|
+
this._canPlayThroughFired = false;
|
|
1006
1017
|
// Video dimensions will change — release the ImageReader so setupBitmapSurface()
|
|
1007
1018
|
// recreates it at the new size once onVideoSizeChanged fires.
|
|
1008
1019
|
if (_surfaceOwner == SurfaceOwner.BITMAP) {
|
|
@@ -1066,6 +1077,34 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
1066
1077
|
}
|
|
1067
1078
|
}
|
|
1068
1079
|
|
|
1080
|
+
public String canPlayType(String type) {
|
|
1081
|
+
if (type == null || type.trim().isEmpty()) return "";
|
|
1082
|
+
String mime = type;
|
|
1083
|
+
int sc = mime.indexOf(';');
|
|
1084
|
+
if (sc >= 0) mime = mime.substring(0, sc).trim();
|
|
1085
|
+
String t = mime.toLowerCase();
|
|
1086
|
+
try {
|
|
1087
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
|
1088
|
+
MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS);
|
|
1089
|
+
MediaCodecInfo[] infos = mcl.getCodecInfos();
|
|
1090
|
+
for (MediaCodecInfo info : infos) {
|
|
1091
|
+
if (info.isEncoder()) continue;
|
|
1092
|
+
String[] types = info.getSupportedTypes();
|
|
1093
|
+
for (String s : types) {
|
|
1094
|
+
if (s.equalsIgnoreCase(t)) {
|
|
1095
|
+
return "probably";
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} catch (Throwable e) {}
|
|
1101
|
+
|
|
1102
|
+
if (t.contains("mp4") || t.contains("mpeg") || t.contains("h264") || t.contains("video/mp4")) return "probably";
|
|
1103
|
+
if (t.contains("webm") || t.contains("vp8") || t.contains("vp9") || t.contains("video/webm")) return "maybe";
|
|
1104
|
+
if (t.contains("ogg") || t.contains("theora") || t.contains("video/ogg")) return "maybe";
|
|
1105
|
+
return "";
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1069
1108
|
|
|
1070
1109
|
public int getWidth() {
|
|
1071
1110
|
int width = this._container.getWidth();
|
|
@@ -1164,11 +1203,27 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
1164
1203
|
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
|
1165
1204
|
if (playbackState == Player.STATE_READY) {
|
|
1166
1205
|
this._readyState = 2; // HAVE_CURRENT_DATA
|
|
1206
|
+
if (!this._canPlayFired) {
|
|
1207
|
+
this._canPlayFired = true;
|
|
1208
|
+
if (this._callback != null) {
|
|
1209
|
+
this._callback.onCanPlay();
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1167
1213
|
if (_loadedDataFired.compareAndSet(false, true)) {
|
|
1168
1214
|
if (this._callback != null) {
|
|
1169
1215
|
this._callback.onLoadedData();
|
|
1170
1216
|
}
|
|
1171
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
long duration = this._player.getDuration();
|
|
1220
|
+
long buffered = this._player.getBufferedPosition();
|
|
1221
|
+
if (duration > 0 && buffered >= duration - 1000 && !this._canPlayThroughFired) {
|
|
1222
|
+
this._canPlayThroughFired = true;
|
|
1223
|
+
if (this._callback != null) {
|
|
1224
|
+
this._callback.onCanPlayThrough();
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1172
1227
|
}
|
|
1173
1228
|
}
|
|
1174
1229
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NSCAudioHelper.h
|
|
3
|
+
// CanvasMedia
|
|
4
|
+
//
|
|
5
|
+
//
|
|
6
|
+
#import <Foundation/Foundation.h>
|
|
7
|
+
#import <UIKit/UIKit.h>
|
|
8
|
+
#import <AVFoundation/AVFoundation.h>
|
|
9
|
+
#import "NSCPlayerState.h"
|
|
10
|
+
|
|
11
|
+
@protocol NSCAudioHelperListener <NSObject>
|
|
12
|
+
- (void)onTimeUpdate:(double)seconds;
|
|
13
|
+
- (void)onStateChange:(NSCPlayerState)state;
|
|
14
|
+
- (void)onLoadedData;
|
|
15
|
+
- (void)onError:(NSString *)message;
|
|
16
|
+
- (void)onCanPlay;
|
|
17
|
+
- (void)onCanPlayThrough;
|
|
18
|
+
@end
|
|
19
|
+
|
|
20
|
+
@interface NSCAudioHelper : NSObject
|
|
21
|
+
|
|
22
|
+
@property (nonatomic, strong, readonly) AVPlayer *player;
|
|
23
|
+
@property (nonatomic, strong, readonly) AVPlayerItem *currentItem;
|
|
24
|
+
@property (nonatomic, readonly) NSCPlayerState state;
|
|
25
|
+
@property (nonatomic, readonly) NSCPlayerReadyState readyState;
|
|
26
|
+
@property (nonatomic, weak) id<NSCAudioHelperListener> listener;
|
|
27
|
+
@property (nonatomic) BOOL loop;
|
|
28
|
+
@property (nonatomic) BOOL autoplay;
|
|
29
|
+
@property (nonatomic) BOOL muted;
|
|
30
|
+
@property (nonatomic) BOOL controls;
|
|
31
|
+
@property (nonatomic, strong, readonly) UIView *view;
|
|
32
|
+
@property (nonatomic, copy) NSString *src;
|
|
33
|
+
@property (nonatomic, readonly) double duration;
|
|
34
|
+
@property (nonatomic) double currentTime;
|
|
35
|
+
|
|
36
|
+
- (void)play;
|
|
37
|
+
- (void)pause;
|
|
38
|
+
- (void)load;
|
|
39
|
+
|
|
40
|
+
- (NSString *)canPlayType:(NSString *)type;
|
|
41
|
+
|
|
42
|
+
- (void)setPlayIconName:(NSString *)name;
|
|
43
|
+
- (void)setPauseIconName:(NSString *)name;
|
|
44
|
+
- (void)setIconTintHex:(NSString *)hex;
|
|
45
|
+
- (void)setIconTintColor:(UIColor *)color;
|
|
46
|
+
|
|
47
|
+
@end
|