@picsa/capacitor-video-player 7.1.0 → 7.2.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.
Files changed (69) hide show
  1. package/CapacitorVideoPlayer.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/android/build.gradle +71 -71
  4. package/android/src/main/AndroidManifest.xml +3 -3
  5. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/CapacitorVideoPlayer.java +71 -71
  6. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/CapacitorVideoPlayerPlugin.java +1256 -1256
  7. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/FullscreenExoPlayerFragment.java +1591 -1595
  8. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/Notifications/MyRunnable.java +21 -21
  9. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/Notifications/NotificationCenter.java +61 -61
  10. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/PickerVideo/AdapterVideoList.java +47 -47
  11. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/PickerVideo/ModelVideo.java +49 -49
  12. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/PickerVideo/PickerVideoFragment.java +119 -119
  13. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/PickerVideo/VideoRecyclerViewHolder.java +65 -65
  14. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/Utilities/FilesUtils.java +101 -101
  15. package/android/src/main/java/com/jeep/plugin/capacitor/capacitorvideoplayer/Utilities/FragmentUtils.java +32 -32
  16. package/android/src/main/res/drawable/bg_round_rect_white_50.xml +9 -9
  17. package/android/src/main/res/drawable/bg_rounded_rectangle_white_corner_rounded.xml +10 -10
  18. package/android/src/main/res/drawable/exo_close_btn.xml +11 -11
  19. package/android/src/main/res/drawable/gradient_transparent_middle.xml +12 -12
  20. package/android/src/main/res/drawable/ic_arrow_left.xml +5 -5
  21. package/android/src/main/res/drawable/ic_baseline_lq.xml +7 -7
  22. package/android/src/main/res/drawable/ic_exo_icon_fastforward.xml +35 -35
  23. package/android/src/main/res/drawable/ic_exo_icon_pause.xml +26 -26
  24. package/android/src/main/res/drawable/ic_exo_icon_play.xml +36 -36
  25. package/android/src/main/res/drawable/ic_exo_icon_rewind.xml +35 -35
  26. package/android/src/main/res/drawable/ic_expand.xml +5 -5
  27. package/android/src/main/res/drawable/ic_fit.xml +5 -5
  28. package/android/src/main/res/drawable/ic_image_background.xml +12 -12
  29. package/android/src/main/res/drawable/ic_img_16_9_background.xml +10 -10
  30. package/android/src/main/res/drawable/ic_img_9_16_background.xml +10 -10
  31. package/android/src/main/res/drawable/ic_outline_lock.xml +5 -5
  32. package/android/src/main/res/drawable/ic_outline_lock_open.xml +5 -5
  33. package/android/src/main/res/drawable/ic_pip_white.xml +5 -5
  34. package/android/src/main/res/drawable/ic_views.xml +18 -18
  35. package/android/src/main/res/drawable/ic_zoom.xml +5 -5
  36. package/android/src/main/res/layout/bridge_layout_main.xml +15 -15
  37. package/android/src/main/res/layout/exo_playback_control_view.xml +289 -289
  38. package/android/src/main/res/layout/exoplayer_layout_youtube.xml +361 -361
  39. package/android/src/main/res/layout/fragment_fs_exoplayer.xml +50 -50
  40. package/android/src/main/res/layout/fragment_picker_video.xml +21 -21
  41. package/android/src/main/res/layout/row_video.xml +76 -76
  42. package/android/src/main/res/values/colors.xml +14 -14
  43. package/android/src/main/res/values/strings.xml +3 -3
  44. package/android/src/main/res/values/styles.xml +3 -3
  45. package/dist/docs.json +14 -14
  46. package/ios/Plugin/CapacitorVideoPlayer.swift +63 -63
  47. package/ios/Plugin/CapacitorVideoPlayerPlugin.h +10 -10
  48. package/ios/Plugin/CapacitorVideoPlayerPlugin.m +22 -22
  49. package/ios/Plugin/CapacitorVideoPlayerPlugin.swift +631 -631
  50. package/ios/Plugin/Extensions/CapacitorVideoPlayerPlugin.AVPlayerViewController.swift +59 -59
  51. package/ios/Plugin/Extensions/CapacitorVideoPlayerPlugin.File.swift +126 -126
  52. package/ios/Plugin/Extensions/CapacitorVideoPlayerPlugin.Fullscreen.swift +94 -94
  53. package/ios/Plugin/Extensions/CapacitorVideoPlayerPlugin.Notifications.swift +140 -140
  54. package/ios/Plugin/Extensions/CapacitorVideoPlayerPlugin.Observers.swift +180 -180
  55. package/ios/Plugin/Info.plist +24 -24
  56. package/ios/Plugin/VideoPicker/VideoHelper.swift +127 -127
  57. package/ios/Plugin/VideoPicker/VideoPickerViewController.swift +144 -144
  58. package/ios/Plugin/VideoPlayer/Extensions/AVURLAsset.swift +18 -18
  59. package/ios/Plugin/VideoPlayer/Extensions/AllOrientationAVPlayerController.swift +12 -12
  60. package/ios/Plugin/VideoPlayer/Extensions/CMTime.swift +23 -23
  61. package/ios/Plugin/VideoPlayer/Extensions/LandscapeAVPlayerController.swift +12 -12
  62. package/ios/Plugin/VideoPlayer/Extensions/Notification.Name.swift +18 -18
  63. package/ios/Plugin/VideoPlayer/Extensions/PortraitAVPlayerController.swift +12 -12
  64. package/ios/Plugin/VideoPlayer/Extensions/View.swift +54 -54
  65. package/ios/Plugin/VideoPlayer/FullScreenVideoPlayerView.swift +601 -601
  66. package/ios/Plugin/VideoPlayer/Models/Video.swift +28 -28
  67. package/ios/Plugin/VideoPlayer/Models/VideoModel.swift +42 -42
  68. package/package.json +92 -92
  69. package/readme.md +227 -190
@@ -1,1595 +1,1591 @@
1
- package com.jeep.plugin.capacitor.capacitorvideoplayer;
2
-
3
- import android.annotation.SuppressLint;
4
- import android.app.Activity;
5
- import android.app.ActivityManager;
6
- import android.app.PictureInPictureParams;
7
- import android.content.ContentUris;
8
- import android.content.Context;
9
- import android.content.pm.ActivityInfo;
10
- import android.content.pm.PackageManager;
11
- import android.content.res.Configuration;
12
- import android.content.res.TypedArray;
13
- import android.graphics.Bitmap;
14
- import android.graphics.BitmapFactory;
15
- import android.graphics.Color;
16
- import android.graphics.drawable.Drawable;
17
- import android.net.Uri;
18
- import android.os.AsyncTask;
19
- import android.os.Build;
20
- import android.os.Bundle;
21
- import android.os.Handler;
22
- import android.os.Looper;
23
- import android.provider.MediaStore;
24
- import android.support.v4.media.session.MediaSessionCompat;
25
- import android.util.Log;
26
- import android.util.Rational;
27
- import android.util.TypedValue;
28
- import android.view.KeyEvent;
29
- import android.view.LayoutInflater;
30
- import android.view.View;
31
- import android.view.ViewGroup;
32
- import android.view.WindowManager;
33
- import android.widget.ImageButton;
34
- import android.widget.ImageView;
35
- import android.widget.LinearLayout;
36
- import android.widget.ProgressBar;
37
- import android.widget.TextView;
38
- import android.widget.Toast;
39
- import androidx.annotation.RequiresApi;
40
- import androidx.appcompat.view.ContextThemeWrapper;
41
- import androidx.constraintlayout.widget.ConstraintLayout;
42
- import androidx.core.graphics.drawable.DrawableCompat;
43
- import androidx.fragment.app.Fragment;
44
- import androidx.mediarouter.app.MediaRouteButton;
45
- import androidx.mediarouter.media.MediaControlIntent;
46
- import androidx.mediarouter.media.MediaRouteSelector;
47
- import androidx.mediarouter.media.MediaRouter;
48
- import com.getcapacitor.JSObject;
49
- import androidx.media3.common.C;
50
- import androidx.media3.exoplayer.DefaultLoadControl;
51
- import androidx.media3.exoplayer.ExoPlayer;
52
- import androidx.media3.exoplayer.LoadControl;
53
- import androidx.media3.common.MediaItem;
54
- import androidx.media3.common.MediaMetadata;
55
- import androidx.media3.common.PlaybackParameters;
56
- import androidx.media3.common.Player;
57
- import androidx.media3.common.AudioAttributes;
58
- import androidx.media3.cast.CastPlayer;
59
- import androidx.media3.cast.SessionAvailabilityListener;
60
- import androidx.media3.session.MediaSession;
61
- import androidx.media3.exoplayer.source.MediaSource;
62
- import androidx.media3.exoplayer.source.MergingMediaSource;
63
- import androidx.media3.exoplayer.source.ProgressiveMediaSource;
64
- import androidx.media3.exoplayer.source.SingleSampleMediaSource;
65
- import androidx.media3.exoplayer.dash.DashMediaSource;
66
- import androidx.media3.exoplayer.hls.HlsMediaSource;
67
- import androidx.media3.exoplayer.smoothstreaming.SsMediaSource;
68
- import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
69
- import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
70
- import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
71
- import androidx.media3.exoplayer.trackselection.TrackSelector;
72
- import androidx.media3.ui.AspectRatioFrameLayout;
73
- import androidx.media3.ui.CaptionStyleCompat;
74
- import androidx.media3.ui.DefaultTimeBar;
75
- import androidx.media3.ui.PlayerControlView;
76
- import androidx.media3.ui.PlayerView;
77
- import androidx.media3.datasource.DataSource;
78
- import androidx.media3.datasource.DefaultDataSource;
79
- import androidx.media3.datasource.DefaultHttpDataSource;
80
- import androidx.media3.common.VideoSize;
81
- import androidx.media3.common.MimeTypes;
82
- import androidx.media3.common.util.Util;
83
- import com.google.android.gms.cast.framework.CastButtonFactory;
84
- import com.google.android.gms.cast.framework.CastContext;
85
- import com.google.android.gms.cast.framework.CastState;
86
- import com.google.android.gms.cast.framework.CastStateListener;
87
- import com.google.android.gms.tasks.OnCompleteListener;
88
- import com.google.android.gms.tasks.Task;
89
- import com.jeep.plugin.capacitor.capacitorvideoplayer.Notifications.NotificationCenter;
90
- import java.io.IOException;
91
- import java.io.InputStream;
92
- import java.net.HttpURLConnection;
93
- import java.net.URL;
94
- import java.util.Arrays;
95
- import java.util.HashMap;
96
- import java.util.List;
97
- import java.util.Locale;
98
- import java.util.Map;
99
- import java.util.concurrent.Executor;
100
- import java.util.concurrent.Executors;
101
- import org.json.JSONException;
102
-
103
- public class FullscreenExoPlayerFragment extends Fragment {
104
-
105
- public String videoPath;
106
- public Float videoRate;
107
- public String playerId;
108
- public String subTitle;
109
- public String language;
110
- public JSObject subTitleOptions;
111
- public JSObject headers;
112
- public Boolean isTV;
113
- public Boolean isInternal;
114
- public Long videoId;
115
- public Boolean exitOnEnd;
116
- public Boolean loopOnEnd;
117
- public Boolean pipEnabled;
118
- public Boolean bkModeEnabled;
119
- public Boolean showControls;
120
- public String displayMode = "all";
121
- public String title;
122
- public String smallTitle;
123
- public String accentColor;
124
- public Boolean chromecast;
125
- public String artwork;
126
-
127
- private static final String TAG = FullscreenExoPlayerFragment.class.getName();
128
- public static final long UNKNOWN_TIME = -1L;
129
- private final List<String> supportedFormat = Arrays.asList(
130
- new String[] { "mp4", "webm", "ogv", "3gp", "flv", "dash", "mpd", "m3u8", "ism", "ytube", "" }
131
- );
132
- private Player.Listener listener;
133
- private PlayerView styledPlayerView;
134
- private String vType = null;
135
- private static ExoPlayer player;
136
- private boolean playWhenReady = true;
137
- private boolean firstReadyToPlay = true;
138
- private boolean isEnded = false;
139
- private int currentWindow = 0;
140
- private long playbackPosition = 0;
141
- private Uri uri = null;
142
- private Uri sturi = null;
143
- private ProgressBar Pbar;
144
- private View view;
145
- private ImageButton closeBtn;
146
- private ImageButton pipBtn;
147
- private ImageButton resizeBtn;
148
- private ConstraintLayout constLayout;
149
- private LinearLayout linearLayout;
150
- private TextView header_tv;
151
- private TextView header_below;
152
- private static ImageView cast_image;
153
- private DefaultTimeBar exo_progress;
154
- private TextView exo_position;
155
- private TextView exo_duration;
156
- private TextView exo_label_separation;
157
- private TextView live_text;
158
- private Context context;
159
- private boolean isMuted = false;
160
- private float curVolume = (float) 0.5;
161
- private String stForeColor = "";
162
- private String stBackColor = "";
163
- private Integer stFontSize = 16;
164
- private boolean isInPictureInPictureMode = false;
165
- private TrackSelector trackSelector;
166
- // Current playback position (in milliseconds).
167
- private int mCurrentPosition;
168
- private int mDuration;
169
- private static final int videoStep = 10000;
170
- private boolean isCastSession = false;
171
-
172
- // Tag for the instance state bundle.
173
- private static final String PLAYBACK_TIME = "play_time";
174
-
175
- private PictureInPictureParams.Builder pictureInPictureParams;
176
- private MediaSession mediaSession;
177
- private PlayerControlView.VisibilityListener visibilityListener;
178
- private PackageManager packageManager;
179
- private Boolean isPIPModeeEnabled = true;
180
- final Handler handler = new Handler();
181
- final Runnable mRunnable = new Runnable() {
182
- @RequiresApi(api = Build.VERSION_CODES.N)
183
- public void run() {
184
- checkPIPPermission();
185
- }
186
- };
187
-
188
- private Integer resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_FIT;
189
- private MediaRouteButton mediaRouteButton;
190
- private CastContext castContext;
191
- private CastPlayer castPlayer;
192
- private MediaItem mediaItem;
193
- private MediaRouter mRouter;
194
- private MediaRouter.Callback mCallback = new EmptyCallback();
195
- private MediaRouteSelector mSelector;
196
- private CastStateListener castStateListener = null;
197
- private Boolean playerReady = false;
198
-
199
- // Thread safety and lifecycle management
200
- private boolean isFragmentDestroyed = false;
201
- private Handler mainHandler;
202
-
203
- /**
204
- * Create Fragment View
205
- * @param inflater
206
- * @param container
207
- * @param savedInstanceState
208
- * @return View
209
- */
210
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
211
-
212
- context = requireContext();
213
-
214
- packageManager = context.getPackageManager();
215
-
216
- // Initialize thread safety components
217
- mainHandler = new Handler(Looper.getMainLooper());
218
- isFragmentDestroyed = false;
219
-
220
- view = inflater.inflate(R.layout.fragment_fs_exoplayer, container, false);
221
- constLayout = view.findViewById(R.id.fsExoPlayer);
222
- linearLayout = view.findViewById(R.id.linearLayout);
223
- styledPlayerView = view.findViewById(R.id.videoViewId);
224
- header_tv = view.findViewById(R.id.header_tv);
225
- header_below = view.findViewById(R.id.header_below);
226
- Pbar = view.findViewById(R.id.indeterminateBar);
227
- exo_progress = view.findViewById(R.id.exo_progress);
228
- exo_progress.setVisibility(View.GONE);
229
- exo_position = view.findViewById(R.id.exo_position);
230
- exo_position.setVisibility(View.GONE);
231
- exo_duration = view.findViewById(R.id.exo_duration);
232
- exo_duration.setVisibility(View.GONE);
233
- exo_label_separation = view.findViewById(R.id.exo_label_separation);
234
- exo_label_separation.setVisibility(View.GONE);
235
- live_text = view.findViewById(R.id.live_text);
236
- resizeBtn = view.findViewById(R.id.exo_resize);
237
- cast_image = view.findViewById(R.id.cast_image);
238
- mediaRouteButton = view.findViewById(R.id.media_route_button);
239
- styledPlayerView.setShowPreviousButton(false);
240
- styledPlayerView.setShowNextButton(false);
241
- styledPlayerView.setShowFastForwardButton(false);
242
- styledPlayerView.setShowRewindButton(false);
243
-
244
- Activity mAct = requireActivity();
245
- if (displayMode.equals("landscape")) {
246
- mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
247
- }
248
- if (displayMode.equals("portrait")) {
249
- mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
250
- }
251
- if (!showControls) {
252
- styledPlayerView.setUseController(false);
253
- } else {
254
- styledPlayerView.setUseController(true);
255
- }
256
-
257
- if (!chromecast) {
258
- mediaRouteButton.setVisibility(View.GONE);
259
- } else {
260
- initializeCastService();
261
- }
262
-
263
- if (title != "") {
264
- header_tv.setText(title);
265
- }
266
- if (smallTitle != "") {
267
- header_below.setText(smallTitle);
268
- }
269
- if (accentColor != "") {
270
- Pbar.getIndeterminateDrawable().setColorFilter(Color.parseColor(accentColor), android.graphics.PorterDuff.Mode.MULTIPLY);
271
- exo_progress.setPlayedColor(Color.parseColor(accentColor));
272
- exo_progress.setScrubberColor(Color.parseColor(accentColor));
273
- }
274
-
275
- closeBtn = view.findViewById(R.id.exo_close);
276
- pipBtn = view.findViewById(R.id.exo_pip);
277
- styledPlayerView.requestFocus();
278
- linearLayout.setVisibility(View.INVISIBLE);
279
- styledPlayerView.setControllerShowTimeoutMs(3000);
280
- styledPlayerView.setControllerVisibilityListener(
281
- new PlayerView.ControllerVisibilityListener() {
282
- @Override
283
- public void onVisibilityChanged(int visibility) {
284
- linearLayout.setVisibility(visibility);
285
- }
286
- }
287
- );
288
-
289
- listener =
290
- new Player.Listener() {
291
- @Override
292
- public void onPlayerStateChanged(boolean playWhenReady, int state) {
293
- String stateString;
294
- Map<String, Object> info = new HashMap<String, Object>() {
295
- {
296
- put("fromPlayerId", playerId);
297
- put("currentTime", String.valueOf(player.getCurrentPosition() / 1000));
298
- }
299
- };
300
-
301
- switch (state) {
302
- case ExoPlayer.STATE_IDLE:
303
- stateString = "ExoPlayer.STATE_IDLE -";
304
- Toast.makeText(context, "Video Url not found", Toast.LENGTH_SHORT).show();
305
- playerExit();
306
- break;
307
- case ExoPlayer.STATE_BUFFERING:
308
- stateString = "ExoPlayer.STATE_BUFFERING -";
309
- Pbar.setVisibility(View.VISIBLE);
310
- break;
311
- case ExoPlayer.STATE_READY:
312
- stateString = "ExoPlayer.STATE_READY -";
313
- Pbar.setVisibility(View.GONE);
314
- playerReady = true;
315
- if (!showControls) {
316
- styledPlayerView.setUseController(false);
317
- } else {
318
- styledPlayerView.setUseController(true);
319
- }
320
- linearLayout.setVisibility(View.INVISIBLE);
321
- Log.v(TAG, "**** in ExoPlayer.STATE_READY firstReadyToPlay " + firstReadyToPlay);
322
-
323
- if (firstReadyToPlay) {
324
- firstReadyToPlay = false;
325
- NotificationCenter.defaultCenter().postNotification("playerItemReady", info);
326
- play();
327
- Log.v(TAG, "**** in ExoPlayer.STATE_READY firstReadyToPlay player.isPlaying" + player.isPlaying());
328
- player.seekTo(currentWindow, playbackPosition);
329
-
330
- // We show progress bar, position and duration only when the video is not live
331
- if (!player.isCurrentMediaItemLive()) {
332
- exo_progress.setVisibility(View.VISIBLE);
333
- exo_position.setVisibility(View.VISIBLE);
334
- exo_duration.setVisibility(View.VISIBLE);
335
- exo_label_separation.setVisibility(View.VISIBLE);
336
- styledPlayerView.setShowFastForwardButton(true);
337
- styledPlayerView.setShowRewindButton(true);
338
- } else {
339
- live_text.setVisibility(View.VISIBLE);
340
- }
341
- } else {
342
- Log.v(TAG, "**** in ExoPlayer.STATE_READY isPlaying " + player.isPlaying());
343
- if (player.isPlaying()) {
344
- Log.v(TAG, "**** in ExoPlayer.STATE_READY going to notify playerItemPlay ");
345
- NotificationCenter.defaultCenter().postNotification("playerItemPlay", info);
346
- resizeBtn.setVisibility(View.VISIBLE);
347
-
348
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipEnabled) {
349
- pipBtn.setVisibility(View.VISIBLE);
350
- }
351
- } else {
352
- Log.v(TAG, "**** in ExoPlayer.STATE_READY going to notify playerItemPause ");
353
- NotificationCenter.defaultCenter().postNotification("playerItemPause", info);
354
- }
355
- }
356
- break;
357
- case ExoPlayer.STATE_ENDED:
358
- stateString = "ExoPlayer.STATE_ENDED -";
359
- Log.v(TAG, "**** in ExoPlayer.STATE_ENDED going to notify playerItemEnd ");
360
-
361
- player.seekTo(0);
362
- player.setVolume(curVolume);
363
- player.setPlayWhenReady(false);
364
- if (exitOnEnd) {
365
- releasePlayer();
366
- /*
367
- Activity mAct = getActivity();
368
- int mOrient = mAct.getRequestedOrientation();
369
- if (mOrient == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
370
- mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
371
- }
372
- */
373
- NotificationCenter.defaultCenter().postNotification("playerItemEnd", info);
374
- }
375
- break;
376
- default:
377
- stateString = "UNKNOWN_STATE -";
378
- break;
379
- }
380
- }
381
- };
382
-
383
- if (isTV) {
384
- Toast.makeText(context, "Device is a TV ", Toast.LENGTH_SHORT).show();
385
- }
386
-
387
- if (!isInternal) {
388
- uri = Uri.parse(videoPath);
389
- sturi = subTitle != null ? Uri.parse(subTitle) : null;
390
-
391
- stForeColor = subTitleOptions.has("foregroundColor") ? subTitleOptions.getString("foregroundColor") : "rgba(255,255,255,1)";
392
- stBackColor = subTitleOptions.has("backgroundColor") ? subTitleOptions.getString("backgroundColor") : "rgba(0,0,0,1)";
393
- stFontSize = subTitleOptions.has("fontSize") ? subTitleOptions.getInteger("fontSize") : 16;
394
- // get video type
395
- vType = getVideoType(uri);
396
- Log.v(TAG, "display url: " + uri);
397
- Log.v(TAG, "display subtitle url: " + sturi);
398
- Log.v(TAG, "display isTV: " + isTV);
399
- Log.v(TAG, "display vType: " + vType);
400
- }
401
- if (uri != null || isInternal) {
402
- // go fullscreen
403
- getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
404
- getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
405
- if (savedInstanceState != null) {
406
- mCurrentPosition = savedInstanceState.getInt(PLAYBACK_TIME);
407
- }
408
-
409
- getActivity()
410
- .runOnUiThread(
411
- new Runnable() {
412
- @Override
413
- public void run() {
414
- // Set the onKey listener
415
- view.setFocusableInTouchMode(true);
416
- view.requestFocus();
417
- view.setOnKeyListener(
418
- new View.OnKeyListener() {
419
- @Override
420
- public boolean onKey(View v, int keyCode, KeyEvent event) {
421
- if (event.getAction() == KeyEvent.ACTION_UP) {
422
- Log.v(TAG, "$$$$ onKey " + keyCode + " $$$$");
423
- if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME) {
424
- Log.v(TAG, "$$$$ Going to backpress $$$$");
425
- backPressed();
426
- } else if (isTV) {
427
- long videoPosition = (player == null) ? 0 : player.getCurrentPosition();
428
- switch (keyCode) {
429
- case KeyEvent.KEYCODE_DPAD_RIGHT:
430
- fastForward(videoPosition, 1);
431
- break;
432
- case KeyEvent.KEYCODE_DPAD_LEFT:
433
- rewind(videoPosition, 1);
434
- break;
435
- case KeyEvent.KEYCODE_DPAD_CENTER:
436
- play_pause();
437
- break;
438
- case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
439
- fastForward(videoPosition, 2);
440
- break;
441
- case KeyEvent.KEYCODE_MEDIA_REWIND:
442
- rewind(videoPosition, 2);
443
- break;
444
- }
445
- }
446
- return true;
447
- } else {
448
- return false;
449
- }
450
- }
451
- }
452
- );
453
-
454
- // initialize the player
455
- initializePlayer();
456
-
457
- closeBtn.setOnClickListener(
458
- new View.OnClickListener() {
459
- @Override
460
- public void onClick(View view) {
461
- playerExit();
462
- }
463
- }
464
- );
465
- pipBtn.setOnClickListener(
466
- new View.OnClickListener() {
467
- @Override
468
- public void onClick(View view) {
469
- pictureInPictureMode();
470
- }
471
- }
472
- );
473
- resizeBtn.setOnClickListener(
474
- new View.OnClickListener() {
475
- @Override
476
- public void onClick(View view) {
477
- resizePressed();
478
- }
479
- }
480
- );
481
- }
482
- }
483
- );
484
- } else {
485
- Log.d(TAG, "Video path wrong or type not supported");
486
- Toast.makeText(context, "Video path wrong or type not supported", Toast.LENGTH_SHORT).show();
487
- }
488
- adjustAspectRatio();
489
- return view;
490
- }
491
-
492
- /**
493
- * Sets the cast image in playerView when it is connected to a cast device
494
- */
495
- private class setCastImage extends AsyncTask<Void, Void, Bitmap> {
496
-
497
- protected Bitmap doInBackground(Void... params) {
498
- final String image = artwork;
499
- if (image != "") {
500
- try {
501
- URL url = new URL(image);
502
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
503
- connection.setDoInput(true);
504
- connection.connect();
505
- InputStream input = connection.getInputStream();
506
- Bitmap myBitmap = BitmapFactory.decodeStream(input);
507
- return myBitmap;
508
- } catch (IOException e) {
509
- e.printStackTrace();
510
- return null;
511
- }
512
- } else {
513
- return null;
514
- }
515
- }
516
-
517
- @Override
518
- protected void onPostExecute(Bitmap result) {
519
- cast_image.setImageBitmap(result);
520
- }
521
- }
522
-
523
- /**
524
- * Show controller
525
- */
526
- public void showController() {
527
- styledPlayerView.showController();
528
- }
529
-
530
- /**
531
- * isControllerIsFullyVisible
532
- */
533
- public boolean isControllerIsFullyVisible() {
534
- return styledPlayerView.isControllerFullyVisible();
535
- }
536
-
537
- /**
538
- * Check if player operations are safe to perform
539
- */
540
- private boolean isPlayerOperationSafe() {
541
- return !isFragmentDestroyed &&
542
- player != null &&
543
- getActivity() != null &&
544
- !getActivity().isFinishing() &&
545
- !getActivity().isDestroyed() &&
546
- isAdded();
547
- }
548
-
549
- /**
550
- * Perform backPressed Action
551
- */
552
- private void backPressed() {
553
- if (isCastSession) {
554
- playerExit();
555
- return;
556
- }
557
- if (
558
- !isInPictureInPictureMode &&
559
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
560
- packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) &&
561
- isPIPModeeEnabled &&
562
- pipEnabled &&
563
- playerReady // <- playerReady: this prevents a crash if the user presses back before the player is ready (when enters in pip mode and tries to get the aspect ratio)
564
- ) {
565
- pictureInPictureMode();
566
- } else {
567
- playerExit();
568
- }
569
- }
570
-
571
- private void resizePressed() {
572
- if (resizeStatus == AspectRatioFrameLayout.RESIZE_MODE_FIT) {
573
- styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
574
- resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_FILL;
575
- resizeBtn.setImageResource(R.drawable.ic_zoom);
576
- } else if (resizeStatus == AspectRatioFrameLayout.RESIZE_MODE_FILL) {
577
- styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_ZOOM);
578
- resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
579
- resizeBtn.setImageResource(R.drawable.ic_fit);
580
- } else {
581
- styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
582
- resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_FIT;
583
- resizeBtn.setImageResource(R.drawable.ic_expand);
584
- }
585
- }
586
-
587
- public void playerExit() {
588
- if (player != null) {
589
- Map<String, Object> info = new HashMap<String, Object>() {
590
- {
591
- put("dismiss", "1");
592
- put("currentTime", getCurrentTime());
593
- }
594
- };
595
- player.seekTo(0);
596
- player.setVolume(curVolume);
597
- releasePlayer();
598
- /*
599
- Activity mAct = getActivity();
600
- int mOrient = mAct.getRequestedOrientation();
601
- if (mOrient == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
602
- mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
603
- }
604
- */
605
- // We control if the user lock the screen when the player is in pip mode
606
- try {
607
- NotificationCenter.defaultCenter().postNotification("playerFullscreenDismiss", info);
608
- } catch (Exception e) {
609
- Log.e(TAG, "Error in posting notification");
610
- }
611
- }
612
- }
613
-
614
- /**
615
- * Perform pictureInPictureMode Action
616
- */
617
- private void pictureInPictureMode() {
618
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
619
- // Store activity reference to avoid multiple getActivity() calls and check for null
620
- // Fragment may be detached from activity during transitions or configuration changes
621
- Activity activity = getActivity();
622
- if (activity == null) {
623
- Log.v(TAG, "pictureInPictureMode: activity is null");
624
- return;
625
- }
626
- styledPlayerView.setUseController(false);
627
- styledPlayerView.setControllerAutoShow(false);
628
- linearLayout.setVisibility(View.INVISIBLE);
629
- Log.v(TAG, "PIP break 1");
630
- // require android O or higher
631
- if (
632
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
633
- ) {
634
- pictureInPictureParams = new PictureInPictureParams.Builder();
635
- // setup aspect ratio of the PIP window; fall back to 16:9 for audio-only or not-yet-decoded video
636
- VideoSize videoSize = player.getVideoSize();
637
- if (videoSize != VideoSize.UNKNOWN) {
638
- pictureInPictureParams.setAspectRatio(new Rational(videoSize.width, videoSize.height));
639
- } else {
640
- pictureInPictureParams.setAspectRatio(new Rational(16, 9));
641
- }
642
-
643
- // Catch IllegalStateException that occurs when:
644
- // 1. Activity doesn't have android:supportsPictureInPicture="true" in manifest
645
- // 2. Device doesn't support PIP (even though PackageManager check passed)
646
- // 3. PIP is disabled in system settings
647
- // This prevents crashes on devices with PIP hardware support but missing app configuration
648
- try {
649
- activity.enterPictureInPictureMode(pictureInPictureParams.build());
650
- } catch (IllegalStateException e) {
651
- Log.v(TAG, "pictureInPictureMode: failed to enter PIP", e);
652
- return;
653
- }
654
- Log.v(TAG, "PIP break 2");
655
- } else {
656
- try {
657
- activity.enterPictureInPictureMode();
658
- } catch (IllegalStateException e) {
659
- Log.v(TAG, "pictureInPictureMode: failed to enter PIP", e);
660
- return;
661
- }
662
- Log.v(TAG, "PIP break 3");
663
- }
664
- isInPictureInPictureMode = activity.isInPictureInPictureMode();
665
- if (sturi != null) {
666
- setSubtitle(true);
667
- }
668
- if (player != null) play();
669
-
670
- handler.postDelayed(mRunnable, 100);
671
- Log.v(TAG, "PIP break 4");
672
- } else {
673
- Log.v(TAG, "pictureInPictureMode: doesn't support PIP");
674
- }
675
- }
676
-
677
- @RequiresApi(api = Build.VERSION_CODES.N)
678
- private void checkPIPPermission() {
679
- isPIPModeeEnabled = isInPictureInPictureMode;
680
- if (!isInPictureInPictureMode) {
681
- backPressed();
682
- }
683
- }
684
-
685
- /**
686
- * Perform onStart Action
687
- */
688
- @Override
689
- public void onStart() {
690
- super.onStart();
691
- //if (chromecast && castContext != null) mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
692
-
693
- if (Util.SDK_INT >= 24) {
694
- if (styledPlayerView != null) {
695
- // If cast is playing then it doesn't start the local player once get backs from background
696
- if (castContext != null && chromecast && castPlayer.isCastSessionAvailable()) return;
697
-
698
- initializePlayer();
699
- if (player.getCurrentPosition() != 0) {
700
- firstReadyToPlay = false;
701
- play();
702
- }
703
- } else {
704
- getActivity().finishAndRemoveTask();
705
- }
706
- }
707
- }
708
-
709
- /**
710
- * Perform onStop Action
711
- */
712
- @Override
713
- public void onStop() {
714
- super.onStop();
715
- boolean isAppBackground = false;
716
- if (bkModeEnabled) isAppBackground = isApplicationSentToBackground(context);
717
- if (isInPictureInPictureMode) {
718
- linearLayout.setVisibility(View.VISIBLE);
719
- playerExit();
720
- getActivity().finishAndRemoveTask();
721
- }
722
- }
723
-
724
- /**
725
- * Perform onDestroy Action
726
- */
727
- @Override
728
- public void onDestroy() {
729
- isFragmentDestroyed = true;
730
-
731
- // Release player before calling super to ensure proper cleanup order
732
- releasePlayer();
733
-
734
- // Clean up handler
735
- if (mainHandler != null) {
736
- mainHandler.removeCallbacksAndMessages(null);
737
- mainHandler = null;
738
- }
739
-
740
- if (chromecast) mRouter.removeCallback(mCallback);
741
-
742
- super.onDestroy();
743
- }
744
-
745
- /**
746
- * Perform onDetach Action
747
- */
748
- @Override
749
- public void onDetach() {
750
- isFragmentDestroyed = true;
751
- super.onDetach();
752
- }
753
-
754
- /**
755
- * Perform onPause Action
756
- */
757
- @Override
758
- public void onPause() {
759
- super.onPause();
760
- if (chromecast) castContext.removeCastStateListener(castStateListener);
761
- boolean isAppBackground = false;
762
- if (bkModeEnabled) isAppBackground = isApplicationSentToBackground(context);
763
-
764
- if (!isInPictureInPictureMode) {
765
- if (Util.SDK_INT < 24) {
766
- if (player != null) player.setPlayWhenReady(false);
767
- releasePlayer();
768
- } else {
769
- if (isAppBackground) {
770
- if (player != null) {
771
- if (player.isPlaying()) play();
772
- }
773
- } else {
774
- pause();
775
- }
776
- }
777
- } else {
778
- if (linearLayout.getVisibility() == View.VISIBLE) {
779
- linearLayout.setVisibility(View.INVISIBLE);
780
- }
781
- if ((isInPictureInPictureMode || isAppBackground) && player != null) play();
782
- }
783
- }
784
-
785
- public void releasePlayer() {
786
- if (isFragmentDestroyed) {
787
- Log.w("VideoPlayer", "Fragment already destroyed, skipping player release");
788
- return;
789
- }
790
-
791
- if (player != null) {
792
- try {
793
- // Ensure release called from main thread
794
- // If current thread is main, release, if not post message to main
795
- if (Looper.myLooper() == Looper.getMainLooper()) {
796
- releasePlayerInternal();
797
- } else if (mainHandler != null) {
798
- mainHandler.post(() -> releasePlayerInternal());
799
- } else {
800
- Log.w("VideoPlayer", "Cannot release player from background thread without a handler. Player may not be released properly.");
801
- }
802
- } catch (Exception e) {
803
- Log.e("VideoPlayer", "Error releasing player", e);
804
- // Force cleanup
805
- player = null;
806
- if (castPlayer != null) {
807
- castPlayer = null;
808
- }
809
- }
810
- }
811
- }
812
-
813
- private void releasePlayerInternal() {
814
- if (player != null && !isFragmentDestroyed) {
815
- try {
816
- playWhenReady = player.getPlayWhenReady();
817
- playbackPosition = player.getCurrentPosition();
818
- currentWindow = player.getCurrentWindowIndex();
819
-
820
- // Media3 Migration Note: MediaSession cleanup changed
821
- // Old: MediaSessionConnector.setPlayer(null) + MediaSessionCompat.setActive(false)
822
- // New: MediaSession.release() - properly releases all resources and removes session ID
823
- // Set to null after release to allow recreation with same ID on next initialization
824
- if (mediaSession != null) {
825
- mediaSession.release();
826
- mediaSession = null;
827
- }
828
-
829
- player.setRepeatMode(Player.REPEAT_MODE_OFF);
830
- player.removeListener(listener);
831
- player.stop(); // Add this line to properly stop the player
832
- player.release();
833
- } catch (Exception e) {
834
- Log.e("VideoPlayer", "Error in releasePlayerInternal", e);
835
- } finally {
836
- player = null;
837
- }
838
-
839
- showSystemUI();
840
- resetVariables();
841
- }
842
-
843
- // Move chromecast handling outside the player null check
844
- // and add null check for castPlayer
845
- if (chromecast && castPlayer != null) {
846
- try {
847
- castPlayer.release();
848
- } catch (Exception e) {
849
- Log.e("VideoPlayer", "Error releasing cast player", e);
850
- } finally {
851
- castPlayer = null;
852
- }
853
- }
854
- }
855
-
856
- /**
857
- * Perform onResume Action
858
- */
859
- @Override
860
- public void onResume() {
861
- super.onResume();
862
- //if (chromecast && castContext != null) castContext.addCastStateListener(castStateListener);
863
- if (!isInPictureInPictureMode) {
864
- hideSystemUi();
865
- if ((Util.SDK_INT < 24 || player == null)) {
866
- initializePlayer();
867
- }
868
- } else {
869
- isInPictureInPictureMode = false;
870
- if (
871
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
872
- ) {
873
- if (!showControls) {
874
- styledPlayerView.setUseController(false);
875
- } else {
876
- styledPlayerView.setUseController(true);
877
- }
878
- }
879
- if (sturi != null) {
880
- setSubtitle(false);
881
- }
882
- }
883
- }
884
-
885
- /**
886
- * Hide System UI
887
- */
888
- @SuppressLint("InlinedApi")
889
- private void hideSystemUi() {
890
- if (styledPlayerView != null) styledPlayerView.setSystemUiVisibility(
891
- View.SYSTEM_UI_FLAG_LOW_PROFILE |
892
- View.SYSTEM_UI_FLAG_FULLSCREEN |
893
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
894
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
895
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
896
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
897
- );
898
- }
899
-
900
- /**
901
- * Leave the fullsreen mode and reset the status bar to visible
902
- */
903
- private void showSystemUI() {
904
- getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
905
- getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
906
- getActivity().getWindow().getDecorView().setSystemUiVisibility(View.VISIBLE);
907
- }
908
-
909
- /**
910
- * Initialize the player
911
- */
912
- private void initializePlayer() {
913
- if (player == null) {
914
- ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
915
- trackSelector = new DefaultTrackSelector(context, videoTrackSelectionFactory);
916
- LoadControl loadControl = new DefaultLoadControl();
917
-
918
- // Media3 Migration Note: DefaultBandwidthMeter now configured automatically by default
919
- player =
920
- new ExoPlayer.Builder(context)
921
- .setSeekBackIncrementMs(10000)
922
- .setSeekForwardIncrementMs(10000)
923
- .setTrackSelector(trackSelector)
924
- .setLoadControl(loadControl)
925
- .build();
926
- }
927
-
928
- styledPlayerView.setPlayer(player);
929
-
930
- MediaSource mediaSource;
931
- if (!isInternal) {
932
- if (videoPath.substring(0, 21).equals("file:///android_asset") || videoPath.substring(0, 15).equals("content://media")) {
933
- mediaSource = buildAssetMediaSource(uri);
934
- } else {
935
- mediaSource = buildHttpMediaSource();
936
- }
937
- } else {
938
- Uri videoUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoId);
939
- mediaSource = buildInternalMediaSource(videoUri);
940
- }
941
-
942
- if (mediaSource != null) {
943
- player.setAudioAttributes(AudioAttributes.DEFAULT, true);
944
- player.addListener(listener);
945
- player.prepare(mediaSource, false, false);
946
- if (loopOnEnd) {
947
- player.setRepeatMode(player.REPEAT_MODE_ONE);
948
- } else {
949
- player.setRepeatMode(player.REPEAT_MODE_OFF);
950
- }
951
- }
952
- Map<String, Object> info = new HashMap<String, Object>() {
953
- {
954
- put("fromPlayerId", playerId);
955
- }
956
- };
957
- if (sturi != null) {
958
- setSubtitle(false);
959
- }
960
- // Media3 Migration Note: MediaSessionConnector replaced with MediaSession.Builder; null check prevents duplicate session ID
961
- if (mediaSession == null) {
962
- mediaSession = new MediaSession.Builder(context, player)
963
- .setId("capacitorvideoplayer")
964
- .build();
965
- }
966
-
967
- NotificationCenter.defaultCenter().postNotification("initializePlayer", info);
968
- }
969
-
970
- private void setSubtitle(boolean transparent) {
971
- int foreground;
972
- int background;
973
- if (!transparent) {
974
- foreground = Color.WHITE;
975
- background = Color.BLACK;
976
- if (stForeColor.length() > 4 && stForeColor.substring(0, 4).equals("rgba")) {
977
- foreground = getColorFromRGBA(stForeColor);
978
- }
979
- if (stBackColor.length() > 4 && stBackColor.substring(0, 4).equals("rgba")) {
980
- background = getColorFromRGBA(stBackColor);
981
- }
982
- } else {
983
- foreground = Color.TRANSPARENT;
984
- background = Color.TRANSPARENT;
985
- }
986
- styledPlayerView
987
- .getSubtitleView()
988
- .setStyle(
989
- new CaptionStyleCompat(foreground, background, Color.TRANSPARENT, CaptionStyleCompat.EDGE_TYPE_NONE, Color.WHITE, null)
990
- );
991
- styledPlayerView.getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, stFontSize);
992
- styledPlayerView.setShowSubtitleButton(true);
993
- }
994
-
995
- /**
996
- * Build the Asset MediaSource
997
- */
998
- private MediaSource buildAssetMediaSource(Uri uri) {
999
- MediaSource mediaSource = null;
1000
- // Media3 Migration Note: DefaultDataSourceFactory replaced with DefaultDataSource.Factory
1001
- DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
1002
- mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1003
- // Get the subtitles if any
1004
- if (sturi != null) {
1005
- mediaSource = getSubTitle(mediaSource, sturi, dataSourceFactory);
1006
- }
1007
- return mediaSource;
1008
- }
1009
-
1010
- /**
1011
- * Build the Internal MediaSource
1012
- */
1013
- private MediaSource buildInternalMediaSource(Uri uri) {
1014
- DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
1015
- return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1016
- }
1017
-
1018
- /**
1019
- * Build the Http MediaSource
1020
- * @return MediaSource
1021
- */
1022
- private MediaSource buildHttpMediaSource() {
1023
- MediaSource mediaSource = null;
1024
-
1025
- DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();
1026
- httpDataSourceFactory.setUserAgent("jeep-exoplayer-plugin");
1027
- httpDataSourceFactory.setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS);
1028
- httpDataSourceFactory.setReadTimeoutMs(1800000);
1029
- httpDataSourceFactory.setAllowCrossProtocolRedirects(true);
1030
-
1031
- // If headers is not null and has data we pass them to the HttpDataSourceFactory
1032
- if (headers != null && headers.length() > 0) {
1033
- // We map the headers(JSObject) to a Map<String, String>
1034
- Map<String, String> headersMap = new HashMap<String, String>();
1035
- for (int i = 0; i < headers.names().length(); i++) {
1036
- try {
1037
- headersMap.put(headers.names().getString(i), headers.get(headers.names().getString(i)).toString());
1038
- } catch (JSONException e) {
1039
- e.printStackTrace();
1040
- }
1041
- }
1042
- httpDataSourceFactory.setDefaultRequestProperties(headersMap);
1043
- }
1044
-
1045
- DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, httpDataSourceFactory);
1046
-
1047
- if (
1048
- vType.equals("mp4") ||
1049
- vType.equals("webm") ||
1050
- vType.equals("ogv") ||
1051
- vType.equals("3gp") ||
1052
- vType.equals("flv") ||
1053
- vType.equals("")
1054
- ) {
1055
- mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1056
- } else if (vType.equals("dash") || vType.equals("mpd")) {
1057
- /* adaptive streaming Dash stream */
1058
- DashMediaSource.Factory mediaSourceFactory = new DashMediaSource.Factory(dataSourceFactory);
1059
- mediaSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(uri));
1060
- } else if (vType.equals("m3u8")) {
1061
- mediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1062
- } else if (vType.equals("ism")) {
1063
- mediaSource = new SsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1064
- }
1065
- // Get the subtitles if any
1066
- if (sturi != null) {
1067
- mediaSource = getSubTitle(mediaSource, sturi, dataSourceFactory);
1068
- }
1069
- return mediaSource;
1070
- }
1071
-
1072
- /**
1073
- * Save instance state
1074
- */
1075
- @Override
1076
- public void onSaveInstanceState(Bundle outState) {
1077
- super.onSaveInstanceState(outState);
1078
-
1079
- // Save the current playback position (in milliseconds) to the
1080
- // instance state bundle.
1081
- if (player != null) {
1082
- outState.putInt(PLAYBACK_TIME, (int) player.getCurrentPosition());
1083
- }
1084
- }
1085
-
1086
- private int getColorFromRGBA(String rgbaColor) {
1087
- int ret = 0;
1088
- String color = rgbaColor.substring(rgbaColor.indexOf("(") + 1, rgbaColor.indexOf(")"));
1089
- List<String> colors = Arrays.asList(color.split(","));
1090
- if (colors.size() == 4) {
1091
- ret =
1092
- (Math.round(Float.parseFloat(colors.get(3).trim()) * 255) & 0xff) << 24 |
1093
- (Integer.parseInt(colors.get(0).trim()) & 0xff) << 16 |
1094
- (Integer.parseInt(colors.get(1).trim()) & 0xff) << 8 |
1095
- (Integer.parseInt(colors.get(2).trim()) & 0xff);
1096
- }
1097
- return ret;
1098
- }
1099
-
1100
- private MediaSource getSubTitle(MediaSource mediaSource, Uri sturi, DataSource.Factory dataSourceFactory) {
1101
- // Create mediaSource with subtitle
1102
- MediaSource[] mediaSources = new MediaSource[2];
1103
- mediaSources[0] = mediaSource;
1104
- String mimeType = getMimeType(sturi);
1105
-
1106
- // We get the language label from the language code
1107
- String languageLabel = Locale.forLanguageTag(language).getDisplayLanguage();
1108
- MediaItem.SubtitleConfiguration subConfig = new MediaItem.SubtitleConfiguration.Builder(sturi)
1109
- .setMimeType(mimeType)
1110
- .setUri(sturi)
1111
- .setId(subTitle)
1112
- .setLabel(languageLabel)
1113
- .setRoleFlags(C.ROLE_FLAG_CAPTION)
1114
- .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
1115
- .setLanguage(language)
1116
- .build();
1117
-
1118
- SingleSampleMediaSource subtitleSource = new SingleSampleMediaSource.Factory(dataSourceFactory)
1119
- .createMediaSource(subConfig, C.TIME_UNSET);
1120
-
1121
- mediaSources[1] = subtitleSource;
1122
-
1123
- mediaSource = new MergingMediaSource(mediaSources);
1124
- return mediaSource;
1125
- }
1126
-
1127
- private String getMimeType(Uri sturi) {
1128
- String lastSegment = sturi.getLastPathSegment();
1129
- String extension = lastSegment.substring(lastSegment.lastIndexOf(".") + 1);
1130
- String mimeType = "";
1131
- if (extension.equals("vtt")) {
1132
- mimeType = MimeTypes.TEXT_VTT;
1133
- } else if (extension.equals("srt")) {
1134
- mimeType = MimeTypes.APPLICATION_SUBRIP;
1135
- } else if (extension.equals("ssa") || extension.equals("ass")) {
1136
- mimeType = MimeTypes.TEXT_SSA;
1137
- } else if (extension.equals("ttml") || extension.equals("dfxp") || extension.equals("xml")) {
1138
- mimeType = MimeTypes.APPLICATION_TTML;
1139
- }
1140
- return mimeType;
1141
- }
1142
-
1143
- /**
1144
- * Fast Forward TV
1145
- */
1146
- private void fastForward(long position, int times) {
1147
- if (position < mDuration - videoStep) {
1148
- if (player.isPlaying()) {
1149
- player.setPlayWhenReady(false);
1150
- }
1151
- player.seekTo(position + (long) times * videoStep);
1152
- play();
1153
- }
1154
- }
1155
-
1156
- /**
1157
- * Rewind TV
1158
- */
1159
- private void rewind(long position, int times) {
1160
- if (position > videoStep) {
1161
- if (player.isPlaying()) {
1162
- player.setPlayWhenReady(false);
1163
- }
1164
- player.seekTo(position - (long) times * videoStep);
1165
- play();
1166
- }
1167
- }
1168
-
1169
- /**
1170
- * Play Pause TV
1171
- */
1172
- private void play_pause() {
1173
- player.setPlayWhenReady(!player.isPlaying());
1174
- }
1175
-
1176
- /**
1177
- * Check if the player is playing
1178
- * @return boolean
1179
- */
1180
- public boolean isPlaying() {
1181
- return player.isPlaying();
1182
- }
1183
-
1184
- /**
1185
- * Start the player
1186
- */
1187
- public void play() {
1188
- if (isFragmentDestroyed || player == null) {
1189
- Log.w("VideoPlayer", "Cannot play - fragment destroyed or player null");
1190
- return;
1191
- }
1192
-
1193
- try {
1194
- // Ensure we're on the main thread
1195
- if (Looper.myLooper() != Looper.getMainLooper()) {
1196
- if (mainHandler != null) {
1197
- mainHandler.post(() -> playInternal());
1198
- }
1199
- return;
1200
- }
1201
-
1202
- playInternal();
1203
- } catch (Exception e) {
1204
- Log.e("VideoPlayer", "Error during play operation", e);
1205
- }
1206
- }
1207
-
1208
- private void playInternal() {
1209
- if (player != null && !isFragmentDestroyed) {
1210
- try {
1211
- PlaybackParameters param = new PlaybackParameters(videoRate);
1212
- player.setPlaybackParameters(param);
1213
-
1214
- /* If the user start the cast before the player is ready and playing, then the video will start
1215
- in the device and chromecast at the same time. This is to avoid that behaviour.*/
1216
- if (!isCastSession) {
1217
- player.setPlayWhenReady(true);
1218
- }
1219
- } catch (Exception e) {
1220
- Log.e("VideoPlayer", "Error in playInternal", e);
1221
- }
1222
- }
1223
- }
1224
-
1225
- /**
1226
- * Pause the player
1227
- */
1228
- public void pause() {
1229
- if (!isPlayerOperationSafe()) {
1230
- return;
1231
- }
1232
-
1233
- try {
1234
- if (Looper.myLooper() != Looper.getMainLooper()) {
1235
- if (mainHandler != null) {
1236
- mainHandler.post(() -> pauseInternal());
1237
- }
1238
- return;
1239
- }
1240
- pauseInternal();
1241
- } catch (Exception e) {
1242
- Log.e("VideoPlayer", "Error pausing player", e);
1243
- }
1244
- }
1245
-
1246
- private void pauseInternal() {
1247
- if (player != null && !isFragmentDestroyed) {
1248
- try {
1249
- player.setPlayWhenReady(false);
1250
- } catch (Exception e) {
1251
- Log.e("VideoPlayer", "Error in pauseInternal", e);
1252
- }
1253
- }
1254
- }
1255
-
1256
- /**
1257
- * Get the player duration
1258
- * @return int in seconds
1259
- */
1260
- public int getDuration() {
1261
- return player.getDuration() == UNKNOWN_TIME ? 0 : (int) (player.getDuration() / 1000);
1262
- }
1263
-
1264
- /**
1265
- * Get the player current position
1266
- * @return int in seconds
1267
- */
1268
- public int getCurrentTime() {
1269
- if (player == null) {
1270
- return 0;
1271
- }
1272
- return player.getCurrentPosition() == UNKNOWN_TIME ? 0 : (int) (player.getCurrentPosition() / 1000);
1273
- }
1274
-
1275
- /**
1276
- * Set the player current position
1277
- * @param timeSecond int
1278
- */
1279
- public void setCurrentTime(int timeSecond) {
1280
- if (!isPlayerOperationSafe()) {
1281
- Log.w("VideoPlayer", "Cannot set current time - player operation not safe");
1282
- return;
1283
- }
1284
-
1285
- try {
1286
- if (isInPictureInPictureMode) {
1287
- styledPlayerView.setUseController(false);
1288
- linearLayout.setVisibility(View.INVISIBLE);
1289
- }
1290
- long seekPosition = player.getCurrentPosition() == UNKNOWN_TIME
1291
- ? 0
1292
- : Math.min(Math.max(0, timeSecond * 1000), player.getDuration());
1293
- player.seekTo(seekPosition);
1294
- } catch (Exception e) {
1295
- Log.e("VideoPlayer", "Error setting current time", e);
1296
- }
1297
- }
1298
-
1299
- /**
1300
- * Return the player volume
1301
- * @return float
1302
- */
1303
- public float getVolume() {
1304
- return player.getVolume();
1305
- }
1306
-
1307
- /**
1308
- * Set the player volume
1309
- * @param _volume float range 0,1
1310
- */
1311
- public void setVolume(float _volume) {
1312
- float volume = Math.min(Math.max(0, _volume), 1L);
1313
- player.setVolume(volume);
1314
- }
1315
-
1316
- /**
1317
- * Return the player rate
1318
- * @return float
1319
- */
1320
- public float getRate() {
1321
- return videoRate;
1322
- }
1323
-
1324
- /**
1325
- * Set the player rate
1326
- * @param _rate float range [0.25f, 0.5f, 0.75f, 1f, 2f, 4f]
1327
- */
1328
- public void setRate(float _rate) {
1329
- videoRate = _rate;
1330
- PlaybackParameters param = new PlaybackParameters(videoRate);
1331
- player.setPlaybackParameters(param);
1332
- }
1333
-
1334
- /**
1335
- * Switch Off/On the player volume
1336
- * @param _isMuted boolean
1337
- */
1338
- public void setMuted(boolean _isMuted) {
1339
- isMuted = _isMuted;
1340
- if (isMuted) {
1341
- curVolume = player.getVolume();
1342
- player.setVolume(0L);
1343
- } else {
1344
- player.setVolume(curVolume);
1345
- }
1346
- }
1347
-
1348
- /**
1349
- * Check if the player is muted
1350
- * @return boolean
1351
- */
1352
- public boolean getMuted() {
1353
- return isMuted;
1354
- }
1355
-
1356
- /**
1357
- * Apply white color to MediaRouteButton
1358
- * @param button
1359
- */
1360
- private void mediaRouteButtonColorWhite(MediaRouteButton button) {
1361
- if (button == null) return;
1362
- Context castContext = new ContextThemeWrapper(getContext(), androidx.mediarouter.R.style.Theme_MediaRouter);
1363
-
1364
- TypedArray a = castContext.obtainStyledAttributes(
1365
- null,
1366
- androidx.mediarouter.R.styleable.MediaRouteButton,
1367
- androidx.mediarouter.R.attr.mediaRouteButtonStyle,
1368
- 0
1369
- );
1370
- Drawable drawable = a.getDrawable(androidx.mediarouter.R.styleable.MediaRouteButton_externalRouteEnabledDrawable);
1371
- a.recycle();
1372
- DrawableCompat.setTint(drawable, getContext().getResources().getColor(R.color.white));
1373
- drawable.setState(button.getDrawableState());
1374
- button.setRemoteIndicatorDrawable(drawable);
1375
- }
1376
-
1377
- /**
1378
- * Get video Type from Uri
1379
- * @param uri
1380
- * @return video type
1381
- */
1382
- private String getVideoType(Uri uri) {
1383
- String ret = null;
1384
- Object obj = uri.getLastPathSegment();
1385
- String lastSegment = (obj == null) ? "" : uri.getLastPathSegment();
1386
- for (String type : supportedFormat) {
1387
- if (ret != null) break;
1388
- if (lastSegment.length() > 0 && lastSegment.contains(type)) ret = type;
1389
- if (ret == null) {
1390
- List<String> segments = uri.getPathSegments();
1391
- if (segments.size() > 0) {
1392
- String segment;
1393
- if (segments.get(segments.size() - 1).equals("manifest")) {
1394
- segment = segments.get(segments.size() - 2);
1395
- } else {
1396
- segment = segments.get(segments.size() - 1);
1397
- }
1398
- for (String sType : supportedFormat) {
1399
- if (segment.contains(sType)) {
1400
- ret = sType;
1401
- break;
1402
- }
1403
- }
1404
- }
1405
- }
1406
- }
1407
- ret = (ret != null) ? ret : "";
1408
- return ret;
1409
- }
1410
-
1411
- /**
1412
- * Reset Variables for multiple runs
1413
- */
1414
- private void resetVariables() {
1415
- vType = null;
1416
- styledPlayerView = null;
1417
- playWhenReady = true;
1418
- firstReadyToPlay = true;
1419
- isEnded = false;
1420
- currentWindow = 0;
1421
- playbackPosition = 0;
1422
- uri = null;
1423
- isMuted = false;
1424
- curVolume = (float) 0.5;
1425
- mCurrentPosition = 0;
1426
- }
1427
-
1428
- /**
1429
- * Check if the application has been sent to the background
1430
- * @param context
1431
- * @return boolean
1432
- */
1433
- public boolean isApplicationSentToBackground(final Context context) {
1434
- int pid = android.os.Process.myPid();
1435
- ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
1436
- List<ActivityManager.RunningAppProcessInfo> procInfos = am.getRunningAppProcesses();
1437
- if (procInfos != null) {
1438
- for (ActivityManager.RunningAppProcessInfo appProcess : procInfos) {
1439
- if (appProcess.pid == pid) {
1440
- return true;
1441
- }
1442
- }
1443
- }
1444
- return false;
1445
- }
1446
-
1447
- /**
1448
- * Function to initialize the cast service and everything related to it
1449
- * @return void
1450
- */
1451
- private void initializeCastService() {
1452
- Executor executor = Executors.newSingleThreadExecutor();
1453
- Task<CastContext> task = CastContext.getSharedInstance(context, executor);
1454
-
1455
- task.addOnCompleteListener(new OnCompleteListener<CastContext>() {
1456
- @Override
1457
- public void onComplete(Task<CastContext> task) {
1458
- if (task.isSuccessful()) {
1459
- castContext = task.getResult();
1460
- castPlayer = new CastPlayer(castContext);
1461
- mRouter = MediaRouter.getInstance(context);
1462
- mSelector =
1463
- new MediaRouteSelector.Builder()
1464
- .addControlCategories(Arrays.asList(MediaControlIntent.CATEGORY_LIVE_AUDIO, MediaControlIntent.CATEGORY_LIVE_VIDEO))
1465
- .build();
1466
-
1467
- mediaRouteButtonColorWhite(mediaRouteButton);
1468
- if (castContext != null && castContext.getCastState() != CastState.NO_DEVICES_AVAILABLE) mediaRouteButton.setVisibility(
1469
- View.VISIBLE
1470
- );
1471
-
1472
- castStateListener =
1473
- state -> {
1474
- if (state == CastState.NO_DEVICES_AVAILABLE) {
1475
- mediaRouteButton.setVisibility(View.GONE);
1476
- } else {
1477
- if (mediaRouteButton.getVisibility() == View.GONE) {
1478
- mediaRouteButton.setVisibility(View.VISIBLE);
1479
- }
1480
- }
1481
- };
1482
- CastButtonFactory.setUpMediaRouteButton(context, mediaRouteButton);
1483
-
1484
- MediaMetadata movieMetadata;
1485
- if (artwork != "") {
1486
- movieMetadata = new MediaMetadata.Builder()
1487
- .setTitle(title)
1488
- .setSubtitle(smallTitle)
1489
- .setMediaType(MediaMetadata.MEDIA_TYPE_MOVIE)
1490
- .setArtworkUri(Uri.parse(artwork))
1491
- .build();
1492
- new setCastImage().execute();
1493
- } else {
1494
- movieMetadata = new MediaMetadata.Builder()
1495
- .setTitle(title)
1496
- .setSubtitle(smallTitle)
1497
- .build();
1498
- }
1499
- mediaItem =
1500
- new MediaItem.Builder()
1501
- .setUri(videoPath)
1502
- .setMimeType(MimeTypes.VIDEO_UNKNOWN)
1503
- .setMediaMetadata(movieMetadata)
1504
- .build();
1505
-
1506
- castPlayer.setSessionAvailabilityListener(
1507
- new SessionAvailabilityListener() {
1508
- @Override
1509
- public void onCastSessionAvailable() {
1510
- isCastSession = true;
1511
- final Long videoPosition = player.getCurrentPosition();
1512
- if (pipEnabled) {
1513
- pipBtn.setVisibility(View.GONE);
1514
- }
1515
- resizeBtn.setVisibility(View.GONE);
1516
- player.setPlayWhenReady(false);
1517
- cast_image.setVisibility(View.VISIBLE);
1518
- castPlayer.setMediaItem(mediaItem, videoPosition);
1519
- styledPlayerView.setPlayer(castPlayer);
1520
- styledPlayerView.setControllerShowTimeoutMs(0);
1521
- styledPlayerView.setControllerHideOnTouch(false);
1522
- //We perform a click because for some weird reason, the layout is black until the user clicks on it
1523
- styledPlayerView.performClick();
1524
- }
1525
-
1526
- @Override
1527
- public void onCastSessionUnavailable() {
1528
- isCastSession = false;
1529
- final Long videoPosition = castPlayer.getCurrentPosition();
1530
- if (pipEnabled) {
1531
- pipBtn.setVisibility(View.VISIBLE);
1532
- }
1533
- resizeBtn.setVisibility(View.VISIBLE);
1534
- cast_image.setVisibility(View.GONE);
1535
- styledPlayerView.setPlayer(player);
1536
- player.setPlayWhenReady(true);
1537
- player.seekTo(videoPosition);
1538
- styledPlayerView.setControllerShowTimeoutMs(3000);
1539
- styledPlayerView.setControllerHideOnTouch(true);
1540
- }
1541
- }
1542
- );
1543
-
1544
- castPlayer.addListener(
1545
- new Player.Listener() {
1546
- @Override
1547
- public void onPlayerStateChanged(boolean playWhenReady, int state) {
1548
- Map<String, Object> info = new HashMap<String, Object>() {
1549
- {
1550
- put("fromPlayerId", playerId);
1551
- put("currentTime", String.valueOf(player.getCurrentPosition() / 1000));
1552
- }
1553
- };
1554
- switch (state) {
1555
- case CastPlayer.STATE_READY:
1556
- if (castPlayer.isPlaying()) {
1557
- NotificationCenter.defaultCenter().postNotification("playerItemPlay", info);
1558
- } else {
1559
- NotificationCenter.defaultCenter().postNotification("playerItemPause", info);
1560
- }
1561
- break;
1562
- default:
1563
- break;
1564
- }
1565
- }
1566
- }
1567
- );
1568
-
1569
- castContext.addCastStateListener(castStateListener);
1570
- mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
1571
-
1572
- } else {
1573
- Exception e = task.getException();
1574
- e.printStackTrace();
1575
- }
1576
- }
1577
- });
1578
- }
1579
-
1580
- private final class EmptyCallback extends MediaRouter.Callback {}
1581
-
1582
- @Override
1583
- public void onConfigurationChanged(Configuration newConfig) {
1584
- super.onConfigurationChanged(newConfig);
1585
- adjustAspectRatio();
1586
- }
1587
-
1588
- private void adjustAspectRatio() {
1589
- if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
1590
- styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
1591
- } else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
1592
- styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
1593
- }
1594
- }
1595
- }
1
+ package com.jeep.plugin.capacitor.capacitorvideoplayer;
2
+
3
+ import android.annotation.SuppressLint;
4
+ import android.app.Activity;
5
+ import android.app.ActivityManager;
6
+ import android.app.PictureInPictureParams;
7
+ import android.content.ContentUris;
8
+ import android.content.Context;
9
+ import android.content.pm.ActivityInfo;
10
+ import android.content.pm.PackageManager;
11
+ import android.content.res.Configuration;
12
+ import android.content.res.TypedArray;
13
+ import android.graphics.Bitmap;
14
+ import android.graphics.BitmapFactory;
15
+ import android.graphics.Color;
16
+ import android.graphics.drawable.Drawable;
17
+ import android.net.Uri;
18
+ import android.os.AsyncTask;
19
+ import android.os.Build;
20
+ import android.os.Bundle;
21
+ import android.os.Handler;
22
+ import android.os.Looper;
23
+ import android.provider.MediaStore;
24
+ import android.support.v4.media.session.MediaSessionCompat;
25
+ import android.util.Log;
26
+ import android.util.Rational;
27
+ import android.util.TypedValue;
28
+ import android.view.KeyEvent;
29
+ import android.view.LayoutInflater;
30
+ import android.view.View;
31
+ import android.view.ViewGroup;
32
+ import android.view.WindowManager;
33
+ import android.widget.ImageButton;
34
+ import android.widget.ImageView;
35
+ import android.widget.LinearLayout;
36
+ import android.widget.ProgressBar;
37
+ import android.widget.TextView;
38
+ import android.widget.Toast;
39
+ import androidx.annotation.RequiresApi;
40
+ import androidx.appcompat.view.ContextThemeWrapper;
41
+ import androidx.constraintlayout.widget.ConstraintLayout;
42
+ import androidx.core.graphics.drawable.DrawableCompat;
43
+ import androidx.fragment.app.Fragment;
44
+ import androidx.mediarouter.app.MediaRouteButton;
45
+ import androidx.mediarouter.media.MediaControlIntent;
46
+ import androidx.mediarouter.media.MediaRouteSelector;
47
+ import androidx.mediarouter.media.MediaRouter;
48
+ import com.getcapacitor.JSObject;
49
+ import androidx.media3.common.C;
50
+ import androidx.media3.exoplayer.DefaultLoadControl;
51
+ import androidx.media3.exoplayer.ExoPlayer;
52
+ import androidx.media3.exoplayer.LoadControl;
53
+ import androidx.media3.common.MediaItem;
54
+ import androidx.media3.common.MediaMetadata;
55
+ import androidx.media3.common.PlaybackParameters;
56
+ import androidx.media3.common.Player;
57
+ import androidx.media3.common.AudioAttributes;
58
+ import androidx.media3.cast.CastPlayer;
59
+ import androidx.media3.cast.SessionAvailabilityListener;
60
+ import androidx.media3.session.MediaSession;
61
+ import androidx.media3.exoplayer.source.MediaSource;
62
+ import androidx.media3.exoplayer.source.MergingMediaSource;
63
+ import androidx.media3.exoplayer.source.ProgressiveMediaSource;
64
+ import androidx.media3.exoplayer.source.SingleSampleMediaSource;
65
+ import androidx.media3.exoplayer.dash.DashMediaSource;
66
+ import androidx.media3.exoplayer.hls.HlsMediaSource;
67
+ import androidx.media3.exoplayer.smoothstreaming.SsMediaSource;
68
+ import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
69
+ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
70
+ import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
71
+ import androidx.media3.exoplayer.trackselection.TrackSelector;
72
+ import androidx.media3.ui.AspectRatioFrameLayout;
73
+ import androidx.media3.ui.CaptionStyleCompat;
74
+ import androidx.media3.ui.DefaultTimeBar;
75
+ import androidx.media3.ui.PlayerControlView;
76
+ import androidx.media3.ui.PlayerView;
77
+ import androidx.media3.datasource.DataSource;
78
+ import androidx.media3.datasource.DefaultDataSource;
79
+ import androidx.media3.datasource.DefaultHttpDataSource;
80
+ import androidx.media3.common.VideoSize;
81
+ import androidx.media3.common.MimeTypes;
82
+ import androidx.media3.common.util.Util;
83
+ import com.google.android.gms.cast.framework.CastButtonFactory;
84
+ import com.google.android.gms.cast.framework.CastContext;
85
+ import com.google.android.gms.cast.framework.CastState;
86
+ import com.google.android.gms.cast.framework.CastStateListener;
87
+ import com.google.android.gms.tasks.OnCompleteListener;
88
+ import com.google.android.gms.tasks.Task;
89
+ import com.jeep.plugin.capacitor.capacitorvideoplayer.Notifications.NotificationCenter;
90
+ import java.io.IOException;
91
+ import java.io.InputStream;
92
+ import java.net.HttpURLConnection;
93
+ import java.net.URL;
94
+ import java.util.Arrays;
95
+ import java.util.HashMap;
96
+ import java.util.List;
97
+ import java.util.Locale;
98
+ import java.util.Map;
99
+ import java.util.concurrent.Executor;
100
+ import java.util.concurrent.Executors;
101
+ import org.json.JSONException;
102
+
103
+ public class FullscreenExoPlayerFragment extends Fragment {
104
+
105
+ public String videoPath;
106
+ public Float videoRate;
107
+ public String playerId;
108
+ public String subTitle;
109
+ public String language;
110
+ public JSObject subTitleOptions;
111
+ public JSObject headers;
112
+ public Boolean isTV;
113
+ public Boolean isInternal;
114
+ public Long videoId;
115
+ public Boolean exitOnEnd;
116
+ public Boolean loopOnEnd;
117
+ public Boolean pipEnabled;
118
+ public Boolean bkModeEnabled;
119
+ public Boolean showControls;
120
+ public String displayMode = "all";
121
+ public String title;
122
+ public String smallTitle;
123
+ public String accentColor;
124
+ public Boolean chromecast;
125
+ public String artwork;
126
+
127
+ private static final String TAG = FullscreenExoPlayerFragment.class.getName();
128
+ public static final long UNKNOWN_TIME = -1L;
129
+ private final List<String> supportedFormat = Arrays.asList(
130
+ new String[] { "mp4", "webm", "ogv", "3gp", "flv", "dash", "mpd", "m3u8", "ism", "ytube", "" }
131
+ );
132
+ private Player.Listener listener;
133
+ private PlayerView styledPlayerView;
134
+ private String vType = null;
135
+ private static ExoPlayer player;
136
+ private boolean playWhenReady = true;
137
+ private boolean firstReadyToPlay = true;
138
+ private boolean isEnded = false;
139
+ private int currentWindow = 0;
140
+ private long playbackPosition = 0;
141
+ private Uri uri = null;
142
+ private Uri sturi = null;
143
+ private ProgressBar Pbar;
144
+ private View view;
145
+ private ImageButton closeBtn;
146
+ private ImageButton pipBtn;
147
+ private ImageButton resizeBtn;
148
+ private ConstraintLayout constLayout;
149
+ private LinearLayout linearLayout;
150
+ private TextView header_tv;
151
+ private TextView header_below;
152
+ private static ImageView cast_image;
153
+ private DefaultTimeBar exo_progress;
154
+ private TextView exo_position;
155
+ private TextView exo_duration;
156
+ private TextView exo_label_separation;
157
+ private TextView live_text;
158
+ private Context context;
159
+ private boolean isMuted = false;
160
+ private float curVolume = (float) 0.5;
161
+ private String stForeColor = "";
162
+ private String stBackColor = "";
163
+ private Integer stFontSize = 16;
164
+ private boolean isInPictureInPictureMode = false;
165
+ private TrackSelector trackSelector;
166
+ // Current playback position (in milliseconds).
167
+ private int mCurrentPosition;
168
+ private int mDuration;
169
+ private static final int videoStep = 10000;
170
+ private boolean isCastSession = false;
171
+
172
+ // Tag for the instance state bundle.
173
+ private static final String PLAYBACK_TIME = "play_time";
174
+
175
+ private PictureInPictureParams.Builder pictureInPictureParams;
176
+ private MediaSession mediaSession;
177
+ private PlayerControlView.VisibilityListener visibilityListener;
178
+ private PackageManager packageManager;
179
+ private Boolean isPIPModeeEnabled = true;
180
+ final Handler handler = new Handler();
181
+ final Runnable mRunnable = new Runnable() {
182
+ @RequiresApi(api = Build.VERSION_CODES.N)
183
+ public void run() {
184
+ checkPIPPermission();
185
+ }
186
+ };
187
+
188
+ private Integer resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_FIT;
189
+ private MediaRouteButton mediaRouteButton;
190
+ private CastContext castContext;
191
+ private CastPlayer castPlayer;
192
+ private MediaItem mediaItem;
193
+ private MediaRouter mRouter;
194
+ private MediaRouter.Callback mCallback = new EmptyCallback();
195
+ private MediaRouteSelector mSelector;
196
+ private CastStateListener castStateListener = null;
197
+ private Boolean playerReady = false;
198
+
199
+ // Thread safety and lifecycle management
200
+ private boolean isFragmentDestroyed = false;
201
+ private Handler mainHandler;
202
+
203
+ /**
204
+ * Create Fragment View
205
+ * @param inflater
206
+ * @param container
207
+ * @param savedInstanceState
208
+ * @return View
209
+ */
210
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
211
+
212
+ context = requireContext();
213
+
214
+ packageManager = context.getPackageManager();
215
+
216
+ // Initialize thread safety components
217
+ mainHandler = new Handler(Looper.getMainLooper());
218
+ isFragmentDestroyed = false;
219
+
220
+ view = inflater.inflate(R.layout.fragment_fs_exoplayer, container, false);
221
+ constLayout = view.findViewById(R.id.fsExoPlayer);
222
+ linearLayout = view.findViewById(R.id.linearLayout);
223
+ styledPlayerView = view.findViewById(R.id.videoViewId);
224
+ header_tv = view.findViewById(R.id.header_tv);
225
+ header_below = view.findViewById(R.id.header_below);
226
+ Pbar = view.findViewById(R.id.indeterminateBar);
227
+ exo_progress = view.findViewById(R.id.exo_progress);
228
+ exo_progress.setVisibility(View.GONE);
229
+ exo_position = view.findViewById(R.id.exo_position);
230
+ exo_position.setVisibility(View.GONE);
231
+ exo_duration = view.findViewById(R.id.exo_duration);
232
+ exo_duration.setVisibility(View.GONE);
233
+ exo_label_separation = view.findViewById(R.id.exo_label_separation);
234
+ exo_label_separation.setVisibility(View.GONE);
235
+ live_text = view.findViewById(R.id.live_text);
236
+ resizeBtn = view.findViewById(R.id.exo_resize);
237
+ cast_image = view.findViewById(R.id.cast_image);
238
+ mediaRouteButton = view.findViewById(R.id.media_route_button);
239
+ styledPlayerView.setShowPreviousButton(false);
240
+ styledPlayerView.setShowNextButton(false);
241
+ styledPlayerView.setShowFastForwardButton(false);
242
+ styledPlayerView.setShowRewindButton(false);
243
+
244
+ Activity mAct = requireActivity();
245
+ if (displayMode.equals("landscape")) {
246
+ mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
247
+ }
248
+ if (displayMode.equals("portrait")) {
249
+ mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
250
+ }
251
+ if (!showControls) {
252
+ styledPlayerView.setUseController(false);
253
+ } else {
254
+ styledPlayerView.setUseController(true);
255
+ }
256
+
257
+ if (!chromecast) {
258
+ mediaRouteButton.setVisibility(View.GONE);
259
+ } else {
260
+ initializeCastService();
261
+ }
262
+
263
+ if (title != "") {
264
+ header_tv.setText(title);
265
+ }
266
+ if (smallTitle != "") {
267
+ header_below.setText(smallTitle);
268
+ }
269
+ if (accentColor != "") {
270
+ Pbar.getIndeterminateDrawable().setColorFilter(Color.parseColor(accentColor), android.graphics.PorterDuff.Mode.MULTIPLY);
271
+ exo_progress.setPlayedColor(Color.parseColor(accentColor));
272
+ exo_progress.setScrubberColor(Color.parseColor(accentColor));
273
+ }
274
+
275
+ closeBtn = view.findViewById(R.id.exo_close);
276
+ pipBtn = view.findViewById(R.id.exo_pip);
277
+ styledPlayerView.requestFocus();
278
+ linearLayout.setVisibility(View.INVISIBLE);
279
+ styledPlayerView.setControllerShowTimeoutMs(3000);
280
+ styledPlayerView.setControllerVisibilityListener(
281
+ new PlayerView.ControllerVisibilityListener() {
282
+ @Override
283
+ public void onVisibilityChanged(int visibility) {
284
+ linearLayout.setVisibility(visibility);
285
+ }
286
+ }
287
+ );
288
+
289
+ listener =
290
+ new Player.Listener() {
291
+ @Override
292
+ public void onPlayerStateChanged(boolean playWhenReady, int state) {
293
+ String stateString;
294
+ Map<String, Object> info = new HashMap<String, Object>() {
295
+ {
296
+ put("fromPlayerId", playerId);
297
+ put("currentTime", String.valueOf(player.getCurrentPosition() / 1000));
298
+ }
299
+ };
300
+
301
+ switch (state) {
302
+ case ExoPlayer.STATE_IDLE:
303
+ stateString = "ExoPlayer.STATE_IDLE -";
304
+ Toast.makeText(context, "Video Url not found", Toast.LENGTH_SHORT).show();
305
+ playerExit();
306
+ break;
307
+ case ExoPlayer.STATE_BUFFERING:
308
+ stateString = "ExoPlayer.STATE_BUFFERING -";
309
+ Pbar.setVisibility(View.VISIBLE);
310
+ break;
311
+ case ExoPlayer.STATE_READY:
312
+ stateString = "ExoPlayer.STATE_READY -";
313
+ Pbar.setVisibility(View.GONE);
314
+ playerReady = true;
315
+ if (!showControls) {
316
+ styledPlayerView.setUseController(false);
317
+ } else {
318
+ styledPlayerView.setUseController(true);
319
+ }
320
+ linearLayout.setVisibility(View.INVISIBLE);
321
+ Log.v(TAG, "**** in ExoPlayer.STATE_READY firstReadyToPlay " + firstReadyToPlay);
322
+
323
+ if (firstReadyToPlay) {
324
+ firstReadyToPlay = false;
325
+ NotificationCenter.defaultCenter().postNotification("playerItemReady", info);
326
+ play();
327
+ Log.v(TAG, "**** in ExoPlayer.STATE_READY firstReadyToPlay player.isPlaying" + player.isPlaying());
328
+ player.seekTo(currentWindow, playbackPosition);
329
+
330
+ // We show progress bar, position and duration only when the video is not live
331
+ if (!player.isCurrentMediaItemLive()) {
332
+ exo_progress.setVisibility(View.VISIBLE);
333
+ exo_position.setVisibility(View.VISIBLE);
334
+ exo_duration.setVisibility(View.VISIBLE);
335
+ exo_label_separation.setVisibility(View.VISIBLE);
336
+ styledPlayerView.setShowFastForwardButton(true);
337
+ styledPlayerView.setShowRewindButton(true);
338
+ } else {
339
+ live_text.setVisibility(View.VISIBLE);
340
+ }
341
+ } else {
342
+ Log.v(TAG, "**** in ExoPlayer.STATE_READY isPlaying " + player.isPlaying());
343
+ if (player.isPlaying()) {
344
+ Log.v(TAG, "**** in ExoPlayer.STATE_READY going to notify playerItemPlay ");
345
+ NotificationCenter.defaultCenter().postNotification("playerItemPlay", info);
346
+ resizeBtn.setVisibility(View.VISIBLE);
347
+
348
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipEnabled) {
349
+ pipBtn.setVisibility(View.VISIBLE);
350
+ }
351
+ } else {
352
+ Log.v(TAG, "**** in ExoPlayer.STATE_READY going to notify playerItemPause ");
353
+ NotificationCenter.defaultCenter().postNotification("playerItemPause", info);
354
+ }
355
+ }
356
+ break;
357
+ case ExoPlayer.STATE_ENDED:
358
+ stateString = "ExoPlayer.STATE_ENDED -";
359
+ Log.v(TAG, "**** in ExoPlayer.STATE_ENDED going to notify playerItemEnd ");
360
+
361
+ player.seekTo(0);
362
+ player.setVolume(curVolume);
363
+ player.setPlayWhenReady(false);
364
+ if (exitOnEnd) {
365
+ releasePlayer();
366
+ /*
367
+ Activity mAct = getActivity();
368
+ int mOrient = mAct.getRequestedOrientation();
369
+ if (mOrient == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
370
+ mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
371
+ }
372
+ */
373
+ NotificationCenter.defaultCenter().postNotification("playerItemEnd", info);
374
+ }
375
+ break;
376
+ default:
377
+ stateString = "UNKNOWN_STATE -";
378
+ break;
379
+ }
380
+ }
381
+ };
382
+
383
+ if (isTV) {
384
+ Toast.makeText(context, "Device is a TV ", Toast.LENGTH_SHORT).show();
385
+ }
386
+
387
+ if (!isInternal) {
388
+ uri = Uri.parse(videoPath);
389
+ sturi = subTitle != null ? Uri.parse(subTitle) : null;
390
+
391
+ stForeColor = subTitleOptions.has("foregroundColor") ? subTitleOptions.getString("foregroundColor") : "rgba(255,255,255,1)";
392
+ stBackColor = subTitleOptions.has("backgroundColor") ? subTitleOptions.getString("backgroundColor") : "rgba(0,0,0,1)";
393
+ stFontSize = subTitleOptions.has("fontSize") ? subTitleOptions.getInteger("fontSize") : 16;
394
+ // get video type
395
+ vType = getVideoType(uri);
396
+ Log.v(TAG, "display url: " + uri);
397
+ Log.v(TAG, "display subtitle url: " + sturi);
398
+ Log.v(TAG, "display isTV: " + isTV);
399
+ Log.v(TAG, "display vType: " + vType);
400
+ }
401
+ if (uri != null || isInternal) {
402
+ // go fullscreen
403
+ getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
404
+ getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
405
+ if (savedInstanceState != null) {
406
+ mCurrentPosition = savedInstanceState.getInt(PLAYBACK_TIME);
407
+ }
408
+
409
+ getActivity()
410
+ .runOnUiThread(
411
+ new Runnable() {
412
+ @Override
413
+ public void run() {
414
+ // Set the onKey listener
415
+ view.setFocusableInTouchMode(true);
416
+ view.requestFocus();
417
+ view.setOnKeyListener(
418
+ new View.OnKeyListener() {
419
+ @Override
420
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
421
+ if (event.getAction() == KeyEvent.ACTION_UP) {
422
+ Log.v(TAG, "$$$$ onKey " + keyCode + " $$$$");
423
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME) {
424
+ Log.v(TAG, "$$$$ Going to backpress $$$$");
425
+ backPressed();
426
+ } else if (isTV) {
427
+ long videoPosition = (player == null) ? 0 : player.getCurrentPosition();
428
+ switch (keyCode) {
429
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
430
+ fastForward(videoPosition, 1);
431
+ break;
432
+ case KeyEvent.KEYCODE_DPAD_LEFT:
433
+ rewind(videoPosition, 1);
434
+ break;
435
+ case KeyEvent.KEYCODE_DPAD_CENTER:
436
+ play_pause();
437
+ break;
438
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
439
+ fastForward(videoPosition, 2);
440
+ break;
441
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
442
+ rewind(videoPosition, 2);
443
+ break;
444
+ }
445
+ }
446
+ return true;
447
+ } else {
448
+ return false;
449
+ }
450
+ }
451
+ }
452
+ );
453
+
454
+ // initialize the player
455
+ initializePlayer();
456
+
457
+ closeBtn.setOnClickListener(
458
+ new View.OnClickListener() {
459
+ @Override
460
+ public void onClick(View view) {
461
+ playerExit();
462
+ }
463
+ }
464
+ );
465
+ pipBtn.setOnClickListener(
466
+ new View.OnClickListener() {
467
+ @Override
468
+ public void onClick(View view) {
469
+ pictureInPictureMode();
470
+ }
471
+ }
472
+ );
473
+ resizeBtn.setOnClickListener(
474
+ new View.OnClickListener() {
475
+ @Override
476
+ public void onClick(View view) {
477
+ resizePressed();
478
+ }
479
+ }
480
+ );
481
+ }
482
+ }
483
+ );
484
+ } else {
485
+ Log.d(TAG, "Video path wrong or type not supported");
486
+ Toast.makeText(context, "Video path wrong or type not supported", Toast.LENGTH_SHORT).show();
487
+ }
488
+ adjustAspectRatio();
489
+ return view;
490
+ }
491
+
492
+ /**
493
+ * Sets the cast image in playerView when it is connected to a cast device
494
+ */
495
+ private class setCastImage extends AsyncTask<Void, Void, Bitmap> {
496
+
497
+ protected Bitmap doInBackground(Void... params) {
498
+ final String image = artwork;
499
+ if (image != "") {
500
+ try {
501
+ URL url = new URL(image);
502
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
503
+ connection.setDoInput(true);
504
+ connection.connect();
505
+ InputStream input = connection.getInputStream();
506
+ Bitmap myBitmap = BitmapFactory.decodeStream(input);
507
+ return myBitmap;
508
+ } catch (IOException e) {
509
+ e.printStackTrace();
510
+ return null;
511
+ }
512
+ } else {
513
+ return null;
514
+ }
515
+ }
516
+
517
+ @Override
518
+ protected void onPostExecute(Bitmap result) {
519
+ cast_image.setImageBitmap(result);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Show controller
525
+ */
526
+ public void showController() {
527
+ styledPlayerView.showController();
528
+ }
529
+
530
+ /**
531
+ * isControllerIsFullyVisible
532
+ */
533
+ public boolean isControllerIsFullyVisible() {
534
+ return styledPlayerView.isControllerFullyVisible();
535
+ }
536
+
537
+ /**
538
+ * Check if player operations are safe to perform
539
+ */
540
+ private boolean isPlayerOperationSafe() {
541
+ return !isFragmentDestroyed &&
542
+ player != null &&
543
+ getActivity() != null &&
544
+ !getActivity().isFinishing() &&
545
+ !getActivity().isDestroyed() &&
546
+ isAdded();
547
+ }
548
+
549
+ /**
550
+ * Perform backPressed Action
551
+ */
552
+ private void backPressed() {
553
+ if (isCastSession) {
554
+ playerExit();
555
+ return;
556
+ }
557
+ if (
558
+ !isInPictureInPictureMode &&
559
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
560
+ packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) &&
561
+ isPIPModeeEnabled &&
562
+ pipEnabled &&
563
+ playerReady // <- playerReady: this prevents a crash if the user presses back before the player is ready (when enters in pip mode and tries to get the aspect ratio)
564
+ ) {
565
+ pictureInPictureMode();
566
+ } else {
567
+ playerExit();
568
+ }
569
+ }
570
+
571
+ private void resizePressed() {
572
+ if (resizeStatus == AspectRatioFrameLayout.RESIZE_MODE_FIT) {
573
+ styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
574
+ resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_FILL;
575
+ resizeBtn.setImageResource(R.drawable.ic_zoom);
576
+ } else if (resizeStatus == AspectRatioFrameLayout.RESIZE_MODE_FILL) {
577
+ styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_ZOOM);
578
+ resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
579
+ resizeBtn.setImageResource(R.drawable.ic_fit);
580
+ } else {
581
+ styledPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
582
+ resizeStatus = AspectRatioFrameLayout.RESIZE_MODE_FIT;
583
+ resizeBtn.setImageResource(R.drawable.ic_expand);
584
+ }
585
+ }
586
+
587
+ public void playerExit() {
588
+ if (player != null) {
589
+ Map<String, Object> info = new HashMap<String, Object>() {
590
+ {
591
+ put("dismiss", "1");
592
+ put("currentTime", getCurrentTime());
593
+ }
594
+ };
595
+ player.seekTo(0);
596
+ player.setVolume(curVolume);
597
+ releasePlayer();
598
+ /*
599
+ Activity mAct = getActivity();
600
+ int mOrient = mAct.getRequestedOrientation();
601
+ if (mOrient == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
602
+ mAct.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
603
+ }
604
+ */
605
+ // We control if the user lock the screen when the player is in pip mode
606
+ try {
607
+ NotificationCenter.defaultCenter().postNotification("playerFullscreenDismiss", info);
608
+ } catch (Exception e) {
609
+ Log.e(TAG, "Error in posting notification");
610
+ }
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Perform pictureInPictureMode Action
616
+ */
617
+ private void pictureInPictureMode() {
618
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
619
+ // Store activity reference to avoid multiple getActivity() calls and check for null
620
+ // Fragment may be detached from activity during transitions or configuration changes
621
+ Activity activity = getActivity();
622
+ if (activity == null) {
623
+ Log.v(TAG, "pictureInPictureMode: activity is null");
624
+ return;
625
+ }
626
+ styledPlayerView.setUseController(false);
627
+ styledPlayerView.setControllerAutoShow(false);
628
+ linearLayout.setVisibility(View.INVISIBLE);
629
+ Log.v(TAG, "PIP break 1");
630
+ // require android O or higher
631
+ if (
632
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
633
+ ) {
634
+ pictureInPictureParams = new PictureInPictureParams.Builder();
635
+ // setup aspect ratio of the PIP window; fall back to 16:9 for audio-only or not-yet-decoded video
636
+ VideoSize videoSize = player.getVideoSize();
637
+ if (videoSize != VideoSize.UNKNOWN) {
638
+ pictureInPictureParams.setAspectRatio(new Rational(videoSize.width, videoSize.height));
639
+ } else {
640
+ pictureInPictureParams.setAspectRatio(new Rational(16, 9));
641
+ }
642
+
643
+ // Catch IllegalStateException that occurs when:
644
+ // 1. Activity doesn't have android:supportsPictureInPicture="true" in manifest
645
+ // 2. Device doesn't support PIP (even though PackageManager check passed)
646
+ // 3. PIP is disabled in system settings
647
+ // This prevents crashes on devices with PIP hardware support but missing app configuration
648
+ try {
649
+ activity.enterPictureInPictureMode(pictureInPictureParams.build());
650
+ } catch (IllegalStateException e) {
651
+ Log.v(TAG, "pictureInPictureMode: failed to enter PIP", e);
652
+ return;
653
+ }
654
+ Log.v(TAG, "PIP break 2");
655
+ } else {
656
+ try {
657
+ activity.enterPictureInPictureMode();
658
+ } catch (IllegalStateException e) {
659
+ Log.v(TAG, "pictureInPictureMode: failed to enter PIP", e);
660
+ return;
661
+ }
662
+ Log.v(TAG, "PIP break 3");
663
+ }
664
+ isInPictureInPictureMode = activity.isInPictureInPictureMode();
665
+ if (sturi != null) {
666
+ setSubtitle(true);
667
+ }
668
+ if (player != null) play();
669
+
670
+ handler.postDelayed(mRunnable, 100);
671
+ Log.v(TAG, "PIP break 4");
672
+ } else {
673
+ Log.v(TAG, "pictureInPictureMode: doesn't support PIP");
674
+ }
675
+ }
676
+
677
+ @RequiresApi(api = Build.VERSION_CODES.N)
678
+ private void checkPIPPermission() {
679
+ isPIPModeeEnabled = isInPictureInPictureMode;
680
+ if (!isInPictureInPictureMode) {
681
+ backPressed();
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Perform onStart Action
687
+ */
688
+ @Override
689
+ public void onStart() {
690
+ super.onStart();
691
+ //if (chromecast && castContext != null) mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
692
+
693
+ if (Util.SDK_INT >= 24) {
694
+ if (styledPlayerView != null) {
695
+ // If cast is playing then it doesn't start the local player once get backs from background
696
+ if (castContext != null && chromecast && castPlayer.isCastSessionAvailable()) return;
697
+
698
+ initializePlayer();
699
+ if (player.getCurrentPosition() != 0) {
700
+ firstReadyToPlay = false;
701
+ play();
702
+ }
703
+ } else {
704
+ getActivity().finishAndRemoveTask();
705
+ }
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Perform onStop Action
711
+ */
712
+ @Override
713
+ public void onStop() {
714
+ super.onStop();
715
+ boolean isAppBackground = false;
716
+ if (bkModeEnabled) isAppBackground = isApplicationSentToBackground(context);
717
+ if (isInPictureInPictureMode) {
718
+ linearLayout.setVisibility(View.VISIBLE);
719
+ playerExit();
720
+ getActivity().finishAndRemoveTask();
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Perform onDestroy Action
726
+ */
727
+ @Override
728
+ public void onDestroy() {
729
+ isFragmentDestroyed = true;
730
+
731
+ // Release player before calling super to ensure proper cleanup order
732
+ releasePlayer();
733
+
734
+ // Clean up handler
735
+ if (mainHandler != null) {
736
+ mainHandler.removeCallbacksAndMessages(null);
737
+ mainHandler = null;
738
+ }
739
+
740
+ if (chromecast) mRouter.removeCallback(mCallback);
741
+
742
+ super.onDestroy();
743
+ }
744
+
745
+ /**
746
+ * Perform onDetach Action
747
+ */
748
+ @Override
749
+ public void onDetach() {
750
+ isFragmentDestroyed = true;
751
+ super.onDetach();
752
+ }
753
+
754
+ /**
755
+ * Perform onPause Action
756
+ */
757
+ @Override
758
+ public void onPause() {
759
+ super.onPause();
760
+ if (chromecast) castContext.removeCastStateListener(castStateListener);
761
+ boolean isAppBackground = false;
762
+ if (bkModeEnabled) isAppBackground = isApplicationSentToBackground(context);
763
+
764
+ if (!isInPictureInPictureMode) {
765
+ if (Util.SDK_INT < 24) {
766
+ if (player != null) player.setPlayWhenReady(false);
767
+ releasePlayer();
768
+ } else {
769
+ if (isAppBackground) {
770
+ if (player != null) {
771
+ if (player.isPlaying()) play();
772
+ }
773
+ } else {
774
+ pause();
775
+ }
776
+ }
777
+ } else {
778
+ if (linearLayout.getVisibility() == View.VISIBLE) {
779
+ linearLayout.setVisibility(View.INVISIBLE);
780
+ }
781
+ if ((isInPictureInPictureMode || isAppBackground) && player != null) play();
782
+ }
783
+ }
784
+
785
+ public void releasePlayer() {
786
+ if (isFragmentDestroyed) {
787
+ Log.w("VideoPlayer", "Fragment already destroyed, skipping player release");
788
+ return;
789
+ }
790
+
791
+ if (player != null) {
792
+ try {
793
+ // Ensure release called from main thread
794
+ // If current thread is main, release, if not post message to main
795
+ if (Looper.myLooper() == Looper.getMainLooper()) {
796
+ releasePlayerInternal();
797
+ } else if (mainHandler != null) {
798
+ mainHandler.post(() -> releasePlayerInternal());
799
+ } else {
800
+ Log.w("VideoPlayer", "Cannot release player from background thread without a handler. Player may not be released properly.");
801
+ }
802
+ } catch (Exception e) {
803
+ Log.e("VideoPlayer", "Error releasing player", e);
804
+ // Force cleanup
805
+ player = null;
806
+ if (castPlayer != null) {
807
+ castPlayer = null;
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ private void releasePlayerInternal() {
814
+ if (player != null && !isFragmentDestroyed) {
815
+ try {
816
+ playWhenReady = player.getPlayWhenReady();
817
+ playbackPosition = player.getCurrentPosition();
818
+ currentWindow = player.getCurrentWindowIndex();
819
+
820
+ // Media3 Migration Note: MediaSession cleanup changed
821
+ // Old: MediaSessionConnector.setPlayer(null) + MediaSessionCompat.setActive(false)
822
+ // New: MediaSession.release() - properly releases all resources and removes session ID
823
+ // Set to null after release to allow recreation with same ID on next initialization
824
+ if (mediaSession != null) {
825
+ mediaSession.release();
826
+ mediaSession = null;
827
+ }
828
+
829
+ player.setRepeatMode(Player.REPEAT_MODE_OFF);
830
+ player.removeListener(listener);
831
+ player.stop(); // Add this line to properly stop the player
832
+ player.release();
833
+ } catch (Exception e) {
834
+ Log.e("VideoPlayer", "Error in releasePlayerInternal", e);
835
+ } finally {
836
+ player = null;
837
+ }
838
+
839
+ showSystemUI();
840
+ resetVariables();
841
+ }
842
+
843
+ // Move chromecast handling outside the player null check
844
+ // and add null check for castPlayer
845
+ if (chromecast && castPlayer != null) {
846
+ try {
847
+ castPlayer.release();
848
+ } catch (Exception e) {
849
+ Log.e("VideoPlayer", "Error releasing cast player", e);
850
+ } finally {
851
+ castPlayer = null;
852
+ }
853
+ }
854
+ }
855
+
856
+ /**
857
+ * Perform onResume Action
858
+ */
859
+ @Override
860
+ public void onResume() {
861
+ super.onResume();
862
+ //if (chromecast && castContext != null) castContext.addCastStateListener(castStateListener);
863
+ if (!isInPictureInPictureMode) {
864
+ hideSystemUi();
865
+ if ((Util.SDK_INT < 24 || player == null)) {
866
+ initializePlayer();
867
+ }
868
+ } else {
869
+ isInPictureInPictureMode = false;
870
+ if (
871
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
872
+ ) {
873
+ if (!showControls) {
874
+ styledPlayerView.setUseController(false);
875
+ } else {
876
+ styledPlayerView.setUseController(true);
877
+ }
878
+ }
879
+ if (sturi != null) {
880
+ setSubtitle(false);
881
+ }
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Hide System UI
887
+ */
888
+ @SuppressLint("InlinedApi")
889
+ private void hideSystemUi() {
890
+ if (styledPlayerView != null) styledPlayerView.setSystemUiVisibility(
891
+ View.SYSTEM_UI_FLAG_LOW_PROFILE |
892
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
893
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
894
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
895
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
896
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
897
+ );
898
+ }
899
+
900
+ /**
901
+ * Leave the fullsreen mode and reset the status bar to visible
902
+ */
903
+ private void showSystemUI() {
904
+ getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
905
+ getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
906
+ getActivity().getWindow().getDecorView().setSystemUiVisibility(View.VISIBLE);
907
+ }
908
+
909
+ /**
910
+ * Initialize the player
911
+ */
912
+ private void initializePlayer() {
913
+ if (player == null) {
914
+ ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
915
+ trackSelector = new DefaultTrackSelector(context, videoTrackSelectionFactory);
916
+ LoadControl loadControl = new DefaultLoadControl();
917
+
918
+ // Media3 Migration Note: DefaultBandwidthMeter now configured automatically by default
919
+ player =
920
+ new ExoPlayer.Builder(context)
921
+ .setSeekBackIncrementMs(10000)
922
+ .setSeekForwardIncrementMs(10000)
923
+ .setTrackSelector(trackSelector)
924
+ .setLoadControl(loadControl)
925
+ .build();
926
+ }
927
+
928
+ styledPlayerView.setPlayer(player);
929
+
930
+ MediaSource mediaSource;
931
+ if (!isInternal) {
932
+ if (videoPath.substring(0, 21).equals("file:///android_asset") || videoPath.substring(0, 15).equals("content://media")) {
933
+ mediaSource = buildAssetMediaSource(uri);
934
+ } else {
935
+ mediaSource = buildHttpMediaSource();
936
+ }
937
+ } else {
938
+ Uri videoUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoId);
939
+ mediaSource = buildInternalMediaSource(videoUri);
940
+ }
941
+
942
+ if (mediaSource != null) {
943
+ player.setAudioAttributes(AudioAttributes.DEFAULT, true);
944
+ player.addListener(listener);
945
+ player.prepare(mediaSource, false, false);
946
+ if (loopOnEnd) {
947
+ player.setRepeatMode(player.REPEAT_MODE_ONE);
948
+ } else {
949
+ player.setRepeatMode(player.REPEAT_MODE_OFF);
950
+ }
951
+ }
952
+ Map<String, Object> info = new HashMap<String, Object>() {
953
+ {
954
+ put("fromPlayerId", playerId);
955
+ }
956
+ };
957
+ if (sturi != null) {
958
+ setSubtitle(false);
959
+ }
960
+ // Media3 Migration Note: MediaSessionConnector replaced with MediaSession.Builder; null check prevents duplicate session ID
961
+ if (mediaSession == null) {
962
+ mediaSession = new MediaSession.Builder(context, player)
963
+ .setId("capacitorvideoplayer")
964
+ .build();
965
+ }
966
+
967
+ NotificationCenter.defaultCenter().postNotification("initializePlayer", info);
968
+ }
969
+
970
+ private void setSubtitle(boolean transparent) {
971
+ int foreground;
972
+ int background;
973
+ if (!transparent) {
974
+ foreground = Color.WHITE;
975
+ background = Color.BLACK;
976
+ if (stForeColor.length() > 4 && stForeColor.substring(0, 4).equals("rgba")) {
977
+ foreground = getColorFromRGBA(stForeColor);
978
+ }
979
+ if (stBackColor.length() > 4 && stBackColor.substring(0, 4).equals("rgba")) {
980
+ background = getColorFromRGBA(stBackColor);
981
+ }
982
+ } else {
983
+ foreground = Color.TRANSPARENT;
984
+ background = Color.TRANSPARENT;
985
+ }
986
+ styledPlayerView
987
+ .getSubtitleView()
988
+ .setStyle(
989
+ new CaptionStyleCompat(foreground, background, Color.TRANSPARENT, CaptionStyleCompat.EDGE_TYPE_NONE, Color.WHITE, null)
990
+ );
991
+ styledPlayerView.getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, stFontSize);
992
+ styledPlayerView.setShowSubtitleButton(true);
993
+ }
994
+
995
+ /**
996
+ * Build the Asset MediaSource
997
+ */
998
+ private MediaSource buildAssetMediaSource(Uri uri) {
999
+ MediaSource mediaSource = null;
1000
+ // Media3 Migration Note: DefaultDataSourceFactory replaced with DefaultDataSource.Factory
1001
+ DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
1002
+ mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1003
+ // Get the subtitles if any
1004
+ if (sturi != null) {
1005
+ mediaSource = getSubTitle(mediaSource, sturi, dataSourceFactory);
1006
+ }
1007
+ return mediaSource;
1008
+ }
1009
+
1010
+ /**
1011
+ * Build the Internal MediaSource
1012
+ */
1013
+ private MediaSource buildInternalMediaSource(Uri uri) {
1014
+ DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
1015
+ return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1016
+ }
1017
+
1018
+ /**
1019
+ * Build the Http MediaSource
1020
+ * @return MediaSource
1021
+ */
1022
+ private MediaSource buildHttpMediaSource() {
1023
+ MediaSource mediaSource = null;
1024
+
1025
+ DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();
1026
+ httpDataSourceFactory.setUserAgent("jeep-exoplayer-plugin");
1027
+ httpDataSourceFactory.setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS);
1028
+ httpDataSourceFactory.setReadTimeoutMs(1800000);
1029
+ httpDataSourceFactory.setAllowCrossProtocolRedirects(true);
1030
+
1031
+ // If headers is not null and has data we pass them to the HttpDataSourceFactory
1032
+ if (headers != null && headers.length() > 0) {
1033
+ // We map the headers(JSObject) to a Map<String, String>
1034
+ Map<String, String> headersMap = new HashMap<String, String>();
1035
+ for (int i = 0; i < headers.names().length(); i++) {
1036
+ try {
1037
+ headersMap.put(headers.names().getString(i), headers.get(headers.names().getString(i)).toString());
1038
+ } catch (JSONException e) {
1039
+ e.printStackTrace();
1040
+ }
1041
+ }
1042
+ httpDataSourceFactory.setDefaultRequestProperties(headersMap);
1043
+ }
1044
+
1045
+ DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, httpDataSourceFactory);
1046
+
1047
+ if (
1048
+ vType.equals("mp4") ||
1049
+ vType.equals("webm") ||
1050
+ vType.equals("ogv") ||
1051
+ vType.equals("3gp") ||
1052
+ vType.equals("flv") ||
1053
+ vType.equals("")
1054
+ ) {
1055
+ mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1056
+ } else if (vType.equals("dash") || vType.equals("mpd")) {
1057
+ /* adaptive streaming Dash stream */
1058
+ DashMediaSource.Factory mediaSourceFactory = new DashMediaSource.Factory(dataSourceFactory);
1059
+ mediaSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(uri));
1060
+ } else if (vType.equals("m3u8")) {
1061
+ mediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1062
+ } else if (vType.equals("ism")) {
1063
+ mediaSource = new SsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
1064
+ }
1065
+ // Get the subtitles if any
1066
+ if (sturi != null) {
1067
+ mediaSource = getSubTitle(mediaSource, sturi, dataSourceFactory);
1068
+ }
1069
+ return mediaSource;
1070
+ }
1071
+
1072
+ /**
1073
+ * Save instance state
1074
+ */
1075
+ @Override
1076
+ public void onSaveInstanceState(Bundle outState) {
1077
+ super.onSaveInstanceState(outState);
1078
+
1079
+ // Save the current playback position (in milliseconds) to the
1080
+ // instance state bundle.
1081
+ if (player != null) {
1082
+ outState.putInt(PLAYBACK_TIME, (int) player.getCurrentPosition());
1083
+ }
1084
+ }
1085
+
1086
+ private int getColorFromRGBA(String rgbaColor) {
1087
+ int ret = 0;
1088
+ String color = rgbaColor.substring(rgbaColor.indexOf("(") + 1, rgbaColor.indexOf(")"));
1089
+ List<String> colors = Arrays.asList(color.split(","));
1090
+ if (colors.size() == 4) {
1091
+ ret =
1092
+ (Math.round(Float.parseFloat(colors.get(3).trim()) * 255) & 0xff) << 24 |
1093
+ (Integer.parseInt(colors.get(0).trim()) & 0xff) << 16 |
1094
+ (Integer.parseInt(colors.get(1).trim()) & 0xff) << 8 |
1095
+ (Integer.parseInt(colors.get(2).trim()) & 0xff);
1096
+ }
1097
+ return ret;
1098
+ }
1099
+
1100
+ private MediaSource getSubTitle(MediaSource mediaSource, Uri sturi, DataSource.Factory dataSourceFactory) {
1101
+ // Create mediaSource with subtitle
1102
+ MediaSource[] mediaSources = new MediaSource[2];
1103
+ mediaSources[0] = mediaSource;
1104
+ String mimeType = getMimeType(sturi);
1105
+
1106
+ // We get the language label from the language code
1107
+ String languageLabel = Locale.forLanguageTag(language).getDisplayLanguage();
1108
+ MediaItem.SubtitleConfiguration subConfig = new MediaItem.SubtitleConfiguration.Builder(sturi)
1109
+ .setMimeType(mimeType)
1110
+ .setUri(sturi)
1111
+ .setId(subTitle)
1112
+ .setLabel(languageLabel)
1113
+ .setRoleFlags(C.ROLE_FLAG_CAPTION)
1114
+ .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
1115
+ .setLanguage(language)
1116
+ .build();
1117
+
1118
+ SingleSampleMediaSource subtitleSource = new SingleSampleMediaSource.Factory(dataSourceFactory)
1119
+ .createMediaSource(subConfig, C.TIME_UNSET);
1120
+
1121
+ mediaSources[1] = subtitleSource;
1122
+
1123
+ mediaSource = new MergingMediaSource(mediaSources);
1124
+ return mediaSource;
1125
+ }
1126
+
1127
+ private String getMimeType(Uri sturi) {
1128
+ String lastSegment = sturi.getLastPathSegment();
1129
+ String extension = lastSegment.substring(lastSegment.lastIndexOf(".") + 1);
1130
+ String mimeType = "";
1131
+ if (extension.equals("vtt")) {
1132
+ mimeType = MimeTypes.TEXT_VTT;
1133
+ } else if (extension.equals("srt")) {
1134
+ mimeType = MimeTypes.APPLICATION_SUBRIP;
1135
+ } else if (extension.equals("ssa") || extension.equals("ass")) {
1136
+ mimeType = MimeTypes.TEXT_SSA;
1137
+ } else if (extension.equals("ttml") || extension.equals("dfxp") || extension.equals("xml")) {
1138
+ mimeType = MimeTypes.APPLICATION_TTML;
1139
+ }
1140
+ return mimeType;
1141
+ }
1142
+
1143
+ /**
1144
+ * Fast Forward TV
1145
+ */
1146
+ private void fastForward(long position, int times) {
1147
+ if (position < mDuration - videoStep) {
1148
+ if (player.isPlaying()) {
1149
+ player.setPlayWhenReady(false);
1150
+ }
1151
+ player.seekTo(position + (long) times * videoStep);
1152
+ play();
1153
+ }
1154
+ }
1155
+
1156
+ /**
1157
+ * Rewind TV
1158
+ */
1159
+ private void rewind(long position, int times) {
1160
+ if (position > videoStep) {
1161
+ if (player.isPlaying()) {
1162
+ player.setPlayWhenReady(false);
1163
+ }
1164
+ player.seekTo(position - (long) times * videoStep);
1165
+ play();
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Play Pause TV
1171
+ */
1172
+ private void play_pause() {
1173
+ player.setPlayWhenReady(!player.isPlaying());
1174
+ }
1175
+
1176
+ /**
1177
+ * Check if the player is playing
1178
+ * @return boolean
1179
+ */
1180
+ public boolean isPlaying() {
1181
+ return player.isPlaying();
1182
+ }
1183
+
1184
+ /**
1185
+ * Start the player
1186
+ */
1187
+ public void play() {
1188
+ if (isFragmentDestroyed || player == null) {
1189
+ Log.w("VideoPlayer", "Cannot play - fragment destroyed or player null");
1190
+ return;
1191
+ }
1192
+
1193
+ try {
1194
+ // Ensure we're on the main thread
1195
+ if (Looper.myLooper() != Looper.getMainLooper()) {
1196
+ if (mainHandler != null) {
1197
+ mainHandler.post(() -> playInternal());
1198
+ }
1199
+ return;
1200
+ }
1201
+
1202
+ playInternal();
1203
+ } catch (Exception e) {
1204
+ Log.e("VideoPlayer", "Error during play operation", e);
1205
+ }
1206
+ }
1207
+
1208
+ private void playInternal() {
1209
+ if (player != null && !isFragmentDestroyed) {
1210
+ try {
1211
+ PlaybackParameters param = new PlaybackParameters(videoRate);
1212
+ player.setPlaybackParameters(param);
1213
+
1214
+ /* If the user start the cast before the player is ready and playing, then the video will start
1215
+ in the device and chromecast at the same time. This is to avoid that behaviour.*/
1216
+ if (!isCastSession) {
1217
+ player.setPlayWhenReady(true);
1218
+ }
1219
+ } catch (Exception e) {
1220
+ Log.e("VideoPlayer", "Error in playInternal", e);
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ /**
1226
+ * Pause the player
1227
+ */
1228
+ public void pause() {
1229
+ if (!isPlayerOperationSafe()) {
1230
+ return;
1231
+ }
1232
+
1233
+ try {
1234
+ if (Looper.myLooper() != Looper.getMainLooper()) {
1235
+ if (mainHandler != null) {
1236
+ mainHandler.post(() -> pauseInternal());
1237
+ }
1238
+ return;
1239
+ }
1240
+ pauseInternal();
1241
+ } catch (Exception e) {
1242
+ Log.e("VideoPlayer", "Error pausing player", e);
1243
+ }
1244
+ }
1245
+
1246
+ private void pauseInternal() {
1247
+ if (player != null && !isFragmentDestroyed) {
1248
+ try {
1249
+ player.setPlayWhenReady(false);
1250
+ } catch (Exception e) {
1251
+ Log.e("VideoPlayer", "Error in pauseInternal", e);
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ /**
1257
+ * Get the player duration
1258
+ * @return int in seconds
1259
+ */
1260
+ public int getDuration() {
1261
+ return player.getDuration() == UNKNOWN_TIME ? 0 : (int) (player.getDuration() / 1000);
1262
+ }
1263
+
1264
+ /**
1265
+ * Get the player current position
1266
+ * @return int in seconds
1267
+ */
1268
+ public int getCurrentTime() {
1269
+ if (player == null) {
1270
+ return 0;
1271
+ }
1272
+ return player.getCurrentPosition() == UNKNOWN_TIME ? 0 : (int) (player.getCurrentPosition() / 1000);
1273
+ }
1274
+
1275
+ /**
1276
+ * Set the player current position
1277
+ * @param timeSecond int
1278
+ */
1279
+ public void setCurrentTime(int timeSecond) {
1280
+ if (!isPlayerOperationSafe()) {
1281
+ Log.w("VideoPlayer", "Cannot set current time - player operation not safe");
1282
+ return;
1283
+ }
1284
+
1285
+ try {
1286
+ if (isInPictureInPictureMode) {
1287
+ styledPlayerView.setUseController(false);
1288
+ linearLayout.setVisibility(View.INVISIBLE);
1289
+ }
1290
+ long seekPosition = player.getCurrentPosition() == UNKNOWN_TIME
1291
+ ? 0
1292
+ : Math.min(Math.max(0, timeSecond * 1000), player.getDuration());
1293
+ player.seekTo(seekPosition);
1294
+ } catch (Exception e) {
1295
+ Log.e("VideoPlayer", "Error setting current time", e);
1296
+ }
1297
+ }
1298
+
1299
+ /**
1300
+ * Return the player volume
1301
+ * @return float
1302
+ */
1303
+ public float getVolume() {
1304
+ return player.getVolume();
1305
+ }
1306
+
1307
+ /**
1308
+ * Set the player volume
1309
+ * @param _volume float range 0,1
1310
+ */
1311
+ public void setVolume(float _volume) {
1312
+ float volume = Math.min(Math.max(0, _volume), 1L);
1313
+ player.setVolume(volume);
1314
+ }
1315
+
1316
+ /**
1317
+ * Return the player rate
1318
+ * @return float
1319
+ */
1320
+ public float getRate() {
1321
+ return videoRate;
1322
+ }
1323
+
1324
+ /**
1325
+ * Set the player rate
1326
+ * @param _rate float range [0.25f, 0.5f, 0.75f, 1f, 2f, 4f]
1327
+ */
1328
+ public void setRate(float _rate) {
1329
+ videoRate = _rate;
1330
+ PlaybackParameters param = new PlaybackParameters(videoRate);
1331
+ player.setPlaybackParameters(param);
1332
+ }
1333
+
1334
+ /**
1335
+ * Switch Off/On the player volume
1336
+ * @param _isMuted boolean
1337
+ */
1338
+ public void setMuted(boolean _isMuted) {
1339
+ isMuted = _isMuted;
1340
+ if (isMuted) {
1341
+ curVolume = player.getVolume();
1342
+ player.setVolume(0L);
1343
+ } else {
1344
+ player.setVolume(curVolume);
1345
+ }
1346
+ }
1347
+
1348
+ /**
1349
+ * Check if the player is muted
1350
+ * @return boolean
1351
+ */
1352
+ public boolean getMuted() {
1353
+ return isMuted;
1354
+ }
1355
+
1356
+ /**
1357
+ * Apply white color to MediaRouteButton
1358
+ * @param button
1359
+ */
1360
+ private void mediaRouteButtonColorWhite(MediaRouteButton button) {
1361
+ if (button == null) return;
1362
+ Context castContext = new ContextThemeWrapper(getContext(), androidx.mediarouter.R.style.Theme_MediaRouter);
1363
+
1364
+ TypedArray a = castContext.obtainStyledAttributes(
1365
+ null,
1366
+ androidx.mediarouter.R.styleable.MediaRouteButton,
1367
+ androidx.mediarouter.R.attr.mediaRouteButtonStyle,
1368
+ 0
1369
+ );
1370
+ Drawable drawable = a.getDrawable(androidx.mediarouter.R.styleable.MediaRouteButton_externalRouteEnabledDrawable);
1371
+ a.recycle();
1372
+ DrawableCompat.setTint(drawable, getContext().getResources().getColor(R.color.white));
1373
+ drawable.setState(button.getDrawableState());
1374
+ button.setRemoteIndicatorDrawable(drawable);
1375
+ }
1376
+
1377
+ /**
1378
+ * Get video Type from Uri
1379
+ * @param uri
1380
+ * @return video type
1381
+ */
1382
+ private String getVideoType(Uri uri) {
1383
+ String ret = null;
1384
+ Object obj = uri.getLastPathSegment();
1385
+ String lastSegment = (obj == null) ? "" : uri.getLastPathSegment();
1386
+ for (String type : supportedFormat) {
1387
+ if (ret != null) break;
1388
+ if (lastSegment.length() > 0 && lastSegment.contains(type)) ret = type;
1389
+ if (ret == null) {
1390
+ List<String> segments = uri.getPathSegments();
1391
+ if (segments.size() > 0) {
1392
+ String segment;
1393
+ if (segments.get(segments.size() - 1).equals("manifest")) {
1394
+ segment = segments.get(segments.size() - 2);
1395
+ } else {
1396
+ segment = segments.get(segments.size() - 1);
1397
+ }
1398
+ for (String sType : supportedFormat) {
1399
+ if (segment.contains(sType)) {
1400
+ ret = sType;
1401
+ break;
1402
+ }
1403
+ }
1404
+ }
1405
+ }
1406
+ }
1407
+ ret = (ret != null) ? ret : "";
1408
+ return ret;
1409
+ }
1410
+
1411
+ /**
1412
+ * Reset Variables for multiple runs
1413
+ */
1414
+ private void resetVariables() {
1415
+ vType = null;
1416
+ styledPlayerView = null;
1417
+ playWhenReady = true;
1418
+ firstReadyToPlay = true;
1419
+ isEnded = false;
1420
+ currentWindow = 0;
1421
+ playbackPosition = 0;
1422
+ uri = null;
1423
+ isMuted = false;
1424
+ curVolume = (float) 0.5;
1425
+ mCurrentPosition = 0;
1426
+ }
1427
+
1428
+ /**
1429
+ * Check if the application has been sent to the background
1430
+ * @param context
1431
+ * @return boolean
1432
+ */
1433
+ public boolean isApplicationSentToBackground(final Context context) {
1434
+ int pid = android.os.Process.myPid();
1435
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
1436
+ List<ActivityManager.RunningAppProcessInfo> procInfos = am.getRunningAppProcesses();
1437
+ if (procInfos != null) {
1438
+ for (ActivityManager.RunningAppProcessInfo appProcess : procInfos) {
1439
+ if (appProcess.pid == pid) {
1440
+ return true;
1441
+ }
1442
+ }
1443
+ }
1444
+ return false;
1445
+ }
1446
+
1447
+ /**
1448
+ * Function to initialize the cast service and everything related to it
1449
+ * @return void
1450
+ */
1451
+ private void initializeCastService() {
1452
+ Executor executor = Executors.newSingleThreadExecutor();
1453
+ Task<CastContext> task = CastContext.getSharedInstance(context, executor);
1454
+
1455
+ task.addOnCompleteListener(new OnCompleteListener<CastContext>() {
1456
+ @Override
1457
+ public void onComplete(Task<CastContext> task) {
1458
+ if (task.isSuccessful()) {
1459
+ castContext = task.getResult();
1460
+ castPlayer = new CastPlayer(castContext);
1461
+ mRouter = MediaRouter.getInstance(context);
1462
+ mSelector =
1463
+ new MediaRouteSelector.Builder()
1464
+ .addControlCategories(Arrays.asList(MediaControlIntent.CATEGORY_LIVE_AUDIO, MediaControlIntent.CATEGORY_LIVE_VIDEO))
1465
+ .build();
1466
+
1467
+ mediaRouteButtonColorWhite(mediaRouteButton);
1468
+ if (castContext != null && castContext.getCastState() != CastState.NO_DEVICES_AVAILABLE) mediaRouteButton.setVisibility(
1469
+ View.VISIBLE
1470
+ );
1471
+
1472
+ castStateListener =
1473
+ state -> {
1474
+ if (state == CastState.NO_DEVICES_AVAILABLE) {
1475
+ mediaRouteButton.setVisibility(View.GONE);
1476
+ } else {
1477
+ if (mediaRouteButton.getVisibility() == View.GONE) {
1478
+ mediaRouteButton.setVisibility(View.VISIBLE);
1479
+ }
1480
+ }
1481
+ };
1482
+ CastButtonFactory.setUpMediaRouteButton(context, mediaRouteButton);
1483
+
1484
+ MediaMetadata movieMetadata;
1485
+ if (artwork != "") {
1486
+ movieMetadata = new MediaMetadata.Builder()
1487
+ .setTitle(title)
1488
+ .setSubtitle(smallTitle)
1489
+ .setMediaType(MediaMetadata.MEDIA_TYPE_MOVIE)
1490
+ .setArtworkUri(Uri.parse(artwork))
1491
+ .build();
1492
+ new setCastImage().execute();
1493
+ } else {
1494
+ movieMetadata = new MediaMetadata.Builder()
1495
+ .setTitle(title)
1496
+ .setSubtitle(smallTitle)
1497
+ .build();
1498
+ }
1499
+ mediaItem =
1500
+ new MediaItem.Builder()
1501
+ .setUri(videoPath)
1502
+ .setMimeType(MimeTypes.VIDEO_UNKNOWN)
1503
+ .setMediaMetadata(movieMetadata)
1504
+ .build();
1505
+
1506
+ castPlayer.setSessionAvailabilityListener(
1507
+ new SessionAvailabilityListener() {
1508
+ @Override
1509
+ public void onCastSessionAvailable() {
1510
+ isCastSession = true;
1511
+ final Long videoPosition = player.getCurrentPosition();
1512
+ if (pipEnabled) {
1513
+ pipBtn.setVisibility(View.GONE);
1514
+ }
1515
+ resizeBtn.setVisibility(View.GONE);
1516
+ player.setPlayWhenReady(false);
1517
+ cast_image.setVisibility(View.VISIBLE);
1518
+ castPlayer.setMediaItem(mediaItem, videoPosition);
1519
+ styledPlayerView.setPlayer(castPlayer);
1520
+ styledPlayerView.setControllerShowTimeoutMs(0);
1521
+ styledPlayerView.setControllerHideOnTouch(false);
1522
+ //We perform a click because for some weird reason, the layout is black until the user clicks on it
1523
+ styledPlayerView.performClick();
1524
+ }
1525
+
1526
+ @Override
1527
+ public void onCastSessionUnavailable() {
1528
+ isCastSession = false;
1529
+ final Long videoPosition = castPlayer.getCurrentPosition();
1530
+ if (pipEnabled) {
1531
+ pipBtn.setVisibility(View.VISIBLE);
1532
+ }
1533
+ resizeBtn.setVisibility(View.VISIBLE);
1534
+ cast_image.setVisibility(View.GONE);
1535
+ styledPlayerView.setPlayer(player);
1536
+ player.setPlayWhenReady(true);
1537
+ player.seekTo(videoPosition);
1538
+ styledPlayerView.setControllerShowTimeoutMs(3000);
1539
+ styledPlayerView.setControllerHideOnTouch(true);
1540
+ }
1541
+ }
1542
+ );
1543
+
1544
+ castPlayer.addListener(
1545
+ new Player.Listener() {
1546
+ @Override
1547
+ public void onPlayerStateChanged(boolean playWhenReady, int state) {
1548
+ Map<String, Object> info = new HashMap<String, Object>() {
1549
+ {
1550
+ put("fromPlayerId", playerId);
1551
+ put("currentTime", String.valueOf(player.getCurrentPosition() / 1000));
1552
+ }
1553
+ };
1554
+ switch (state) {
1555
+ case CastPlayer.STATE_READY:
1556
+ if (castPlayer.isPlaying()) {
1557
+ NotificationCenter.defaultCenter().postNotification("playerItemPlay", info);
1558
+ } else {
1559
+ NotificationCenter.defaultCenter().postNotification("playerItemPause", info);
1560
+ }
1561
+ break;
1562
+ default:
1563
+ break;
1564
+ }
1565
+ }
1566
+ }
1567
+ );
1568
+
1569
+ castContext.addCastStateListener(castStateListener);
1570
+ mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
1571
+
1572
+ } else {
1573
+ Exception e = task.getException();
1574
+ e.printStackTrace();
1575
+ }
1576
+ }
1577
+ });
1578
+ }
1579
+
1580
+ private final class EmptyCallback extends MediaRouter.Callback {}
1581
+
1582
+ @Override
1583
+ public void onConfigurationChanged(Configuration newConfig) {
1584
+ super.onConfigurationChanged(newConfig);
1585
+ adjustAspectRatio();
1586
+ }
1587
+
1588
+ private void adjustAspectRatio() {
1589
+ styledPlayerView.setResizeMode(resizeStatus);
1590
+ }
1591
+ }