/
PlayerData.java
386 lines (304 loc) · 12.7 KB
/
PlayerData.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
package expo.modules.av.player;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.Surface;
import org.unimodules.core.Promise;
import org.unimodules.core.arguments.ReadableArguments;
import java.util.Map;
import expo.modules.av.AVManagerInterface;
import expo.modules.av.AudioEventHandler;
import expo.modules.av.AudioFocusNotAcquiredException;
import expo.modules.av.progress.AndroidLooperTimeMachine;
import expo.modules.av.progress.ProgressLooper;
public abstract class PlayerData implements AudioEventHandler {
static final String STATUS_ANDROID_IMPLEMENTATION_KEY_PATH = "androidImplementation";
static final String STATUS_HEADERS_KEY_PATH = "headers";
static final String STATUS_IS_LOADED_KEY_PATH = "isLoaded";
public static final String STATUS_URI_KEY_PATH = "uri";
static final String STATUS_OVERRIDING_EXTENSION_KEY_PATH = "overridingExtension";
static final String STATUS_PROGRESS_UPDATE_INTERVAL_MILLIS_KEY_PATH = "progressUpdateIntervalMillis";
static final String STATUS_DURATION_MILLIS_KEY_PATH = "durationMillis";
static final String STATUS_POSITION_MILLIS_KEY_PATH = "positionMillis";
static final String STATUS_PLAYABLE_DURATION_MILLIS_KEY_PATH = "playableDurationMillis";
static final String STATUS_SHOULD_PLAY_KEY_PATH = "shouldPlay";
public static final String STATUS_IS_PLAYING_KEY_PATH = "isPlaying";
static final String STATUS_IS_BUFFERING_KEY_PATH = "isBuffering";
static final String STATUS_RATE_KEY_PATH = "rate";
static final String STATUS_SHOULD_CORRECT_PITCH_KEY_PATH = "shouldCorrectPitch";
static final String STATUS_VOLUME_KEY_PATH = "volume";
static final String STATUS_IS_MUTED_KEY_PATH = "isMuted";
static final String STATUS_IS_LOOPING_KEY_PATH = "isLooping";
static final String STATUS_DID_JUST_FINISH_KEY_PATH = "didJustFinish";
public static Bundle getUnloadedStatus() {
final Bundle map = new Bundle();
map.putBoolean(STATUS_IS_LOADED_KEY_PATH, false);
return map;
}
public interface VideoSizeUpdateListener {
void onVideoSizeUpdate(final Pair<Integer, Integer> videoWidthHeight);
}
public interface ErrorListener {
void onError(final String error);
}
public interface LoadCompletionListener {
void onLoadSuccess(final Bundle status);
void onLoadError(final String error);
}
public interface StatusUpdateListener {
void onStatusUpdate(final Bundle status);
}
interface SetStatusCompletionListener {
void onSetStatusComplete();
void onSetStatusError(final String error);
}
public interface FullscreenPresenter {
boolean isBeingPresentedFullscreen();
void setFullscreenMode(boolean isFullscreen);
}
final AVManagerInterface mAVModule;
final Uri mUri;
final Map<String, Object> mRequestHeaders;
private ProgressLooper mProgressUpdater = new ProgressLooper(new AndroidLooperTimeMachine());
private FullscreenPresenter mFullscreenPresenter = null;
private StatusUpdateListener mStatusUpdateListener = null;
ErrorListener mErrorListener = null;
VideoSizeUpdateListener mVideoSizeUpdateListener = null;
private int mProgressUpdateIntervalMillis = 500;
boolean mShouldPlay = false;
float mRate = 1.0f;
boolean mShouldCorrectPitch = false;
float mVolume = 1.0f;
boolean mIsMuted = false;
PlayerData(final AVManagerInterface avModule, final Uri uri, final Map<String, Object> requestHeaders) {
mRequestHeaders = requestHeaders;
mAVModule = avModule;
mUri = uri;
}
public static PlayerData createUnloadedPlayerData(final AVManagerInterface avModule, final Context context, final ReadableArguments source, final Bundle status) {
final String uriString = source.getString(STATUS_URI_KEY_PATH);
Map requestHeaders = null;
if (source.containsKey(STATUS_HEADERS_KEY_PATH)) {
requestHeaders = source.getMap(STATUS_HEADERS_KEY_PATH);
}
final String uriOverridingExtension = source.containsKey(STATUS_OVERRIDING_EXTENSION_KEY_PATH) ? source.getString(STATUS_OVERRIDING_EXTENSION_KEY_PATH) : null;
// uriString is guaranteed not to be null (both VideoView.setSource and Sound.loadAsync handle that case)
final Uri uri = Uri.parse(uriString);
if (status.containsKey(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH)
&& status.getString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH).equals(MediaPlayerData.IMPLEMENTATION_NAME)) {
return new MediaPlayerData(avModule, context, uri, requestHeaders);
} else {
return new SimpleExoPlayerData(avModule, context, uri, uriOverridingExtension, requestHeaders);
}
}
abstract String getImplementationName();
// Lifecycle
public abstract void load(final Bundle status, final LoadCompletionListener loadCompletionListener);
public abstract void release();
// Status update listener
private void callStatusUpdateListenerWithStatus(final Bundle status) {
if (mStatusUpdateListener != null) {
mStatusUpdateListener.onStatusUpdate(status);
}
}
final void callStatusUpdateListenerWithDidJustFinish() {
final Bundle status = getStatus();
status.putBoolean(STATUS_DID_JUST_FINISH_KEY_PATH, true);
callStatusUpdateListenerWithStatus(status);
}
final void callStatusUpdateListener() {
callStatusUpdateListenerWithStatus(getStatus());
}
abstract boolean shouldContinueUpdatingProgress();
final void stopUpdatingProgressIfNecessary() {
mProgressUpdater.stopLooping();
}
private void progressUpdateLoop() {
if (!shouldContinueUpdatingProgress()) {
stopUpdatingProgressIfNecessary();
} else {
mProgressUpdater.loop(mProgressUpdateIntervalMillis, () -> {
this.callStatusUpdateListener();
return null;
});
}
}
final void beginUpdatingProgressIfNecessary() {
mProgressUpdater.loop(mProgressUpdateIntervalMillis, () -> {
this.callStatusUpdateListener();
return null;
});
}
public final void setStatusUpdateListener(final StatusUpdateListener listener) {
mStatusUpdateListener = listener;
if (mStatusUpdateListener != null) {
beginUpdatingProgressIfNecessary();
}
}
// Error listener
public final void setErrorListener(final ErrorListener listener) {
mErrorListener = listener;
}
// Status
final boolean shouldPlayerPlay() {
return mShouldPlay && mRate > 0.0;
}
abstract void playPlayerWithRateAndMuteIfNecessary() throws AudioFocusNotAcquiredException;
abstract void applyNewStatus(final Integer newPositionMillis, final Boolean newIsLooping)
throws AudioFocusNotAcquiredException, IllegalStateException;
final void setStatusWithListener(final Bundle status, final SetStatusCompletionListener setStatusCompletionListener) {
if (status.containsKey(STATUS_PROGRESS_UPDATE_INTERVAL_MILLIS_KEY_PATH)) {
mProgressUpdateIntervalMillis = (int) status.getDouble(STATUS_PROGRESS_UPDATE_INTERVAL_MILLIS_KEY_PATH);
}
final Integer newPositionMillis;
if (status.containsKey(STATUS_POSITION_MILLIS_KEY_PATH)) {
// Even though we set the position with an int, this is a double in the map because iOS can
// take a floating point value for positionMillis.
newPositionMillis = (int) status.getDouble(STATUS_POSITION_MILLIS_KEY_PATH);
} else {
newPositionMillis = null;
}
if (status.containsKey(STATUS_SHOULD_PLAY_KEY_PATH)) {
mShouldPlay = status.getBoolean(STATUS_SHOULD_PLAY_KEY_PATH);
}
if (status.containsKey(STATUS_RATE_KEY_PATH)) {
mRate = (float) status.getDouble(STATUS_RATE_KEY_PATH);
}
if (status.containsKey(STATUS_SHOULD_CORRECT_PITCH_KEY_PATH)) {
mShouldCorrectPitch = status.getBoolean(STATUS_SHOULD_CORRECT_PITCH_KEY_PATH);
}
if (status.containsKey(STATUS_VOLUME_KEY_PATH)) {
mVolume = (float) status.getDouble(STATUS_VOLUME_KEY_PATH);
}
if (status.containsKey(STATUS_IS_MUTED_KEY_PATH)) {
mIsMuted = status.getBoolean(STATUS_IS_MUTED_KEY_PATH);
}
final Boolean newIsLooping;
if (status.containsKey(STATUS_IS_LOOPING_KEY_PATH)) {
newIsLooping = status.getBoolean(STATUS_IS_LOOPING_KEY_PATH);
} else {
newIsLooping = null;
}
try {
applyNewStatus(newPositionMillis, newIsLooping);
} catch (final Throwable throwable) {
mAVModule.abandonAudioFocusIfUnused();
setStatusCompletionListener.onSetStatusError(throwable.toString());
return;
}
mAVModule.abandonAudioFocusIfUnused();
setStatusCompletionListener.onSetStatusComplete();
}
public final void setStatus(final Bundle status, final Promise promise) {
if (status == null) {
if (promise != null) {
promise.reject("E_AV_SETSTATUS", "Cannot set null status.");
}
return;
}
try {
setStatusWithListener(status, new SetStatusCompletionListener() {
@Override
public void onSetStatusComplete() {
if (promise == null) {
callStatusUpdateListener();
} else {
promise.resolve(getStatus());
}
}
@Override
public void onSetStatusError(final String error) {
if (promise == null) {
callStatusUpdateListener();
} else {
promise.reject("E_AV_SETSTATUS", error);
}
}
});
} catch (final Throwable throwable) {
if (promise != null) {
promise.reject("E_AV_SETSTATUS", "Encountered an error while setting status!", throwable);
}
}
}
final int getClippedIntegerForValue(final Integer value, final Integer min, final Integer max) {
return (min != null && value < min) ? min : (max != null && value > max) ? max : value;
}
abstract boolean isLoaded();
abstract void getExtraStatusFields(final Bundle map);
// Sometimes another thread would release the player
// in the middle of `getStatus()` call, which would result
// in a null reference method invocation in `getExtraStatusFields`,
// so we need to ensure nothing will release or nullify the property
// while we get the latest status.
public synchronized final Bundle getStatus() {
if (!isLoaded()) {
final Bundle map = getUnloadedStatus();
map.putString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH, getImplementationName());
return map;
}
final Bundle map = new Bundle();
map.putBoolean(STATUS_IS_LOADED_KEY_PATH, true);
map.putString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH, getImplementationName());
map.putString(STATUS_URI_KEY_PATH, mUri.getPath());
map.putInt(STATUS_PROGRESS_UPDATE_INTERVAL_MILLIS_KEY_PATH, mProgressUpdateIntervalMillis);
// STATUS_DURATION_MILLIS_KEY_PATH, STATUS_POSITION_MILLIS_KEY_PATH,
// and STATUS_PLAYABLE_DURATION_MILLIS_KEY_PATH are set in addExtraStatusFields().
map.putBoolean(STATUS_SHOULD_PLAY_KEY_PATH, mShouldPlay);
// STATUS_IS_PLAYING_KEY_PATH and STATUS_IS_BUFFERING_KEY_PATH are set
// in addExtraStatusFields().
map.putDouble(STATUS_RATE_KEY_PATH, (double) mRate);
map.putBoolean(STATUS_SHOULD_CORRECT_PITCH_KEY_PATH, mShouldCorrectPitch);
map.putDouble(STATUS_VOLUME_KEY_PATH, (double) mVolume);
map.putBoolean(STATUS_IS_MUTED_KEY_PATH, mIsMuted);
// STATUS_IS_LOOPING_KEY_PATH is set in addExtraStatusFields().
map.putBoolean(STATUS_DID_JUST_FINISH_KEY_PATH, false);
getExtraStatusFields(map);
return map;
}
// Video specific stuff
public final void setVideoSizeUpdateListener(final VideoSizeUpdateListener videoSizeUpdateListener) {
mVideoSizeUpdateListener = videoSizeUpdateListener;
}
public final void setFullscreenPresenter(final FullscreenPresenter fullscreenPresenter) {
mFullscreenPresenter = fullscreenPresenter;
}
public abstract Pair<Integer, Integer> getVideoWidthHeight();
public abstract void tryUpdateVideoSurface(final Surface surface);
abstract int getAudioSessionId();
public boolean isPresentedFullscreen() {
return mFullscreenPresenter.isBeingPresentedFullscreen();
}
public void toggleFullscreen() {
mFullscreenPresenter.setFullscreenMode(!isPresentedFullscreen());
}
// AudioEventHandler
@Override
public final void handleAudioFocusInterruptionBegan() {
if (!mIsMuted) {
pauseImmediately();
}
}
@Override
public final void handleAudioFocusGained() {
try {
playPlayerWithRateAndMuteIfNecessary();
} catch (final AudioFocusNotAcquiredException e) {
// This is ok -- we might be paused or audio might have been disabled.
}
}
@Override
public final void onPause() {
pauseImmediately();
}
@Override
public final void onResume() {
try {
playPlayerWithRateAndMuteIfNecessary();
} catch (final AudioFocusNotAcquiredException e) {
// Do nothing -- another app has audio focus for now, and handleAudioFocusGained() will be
// called when it abandons it.
}
}
}