unified-video-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnifiedVideoPlayer.java
|
|
3
|
+
* Unified Video Framework - Android Native SDK (Java Version)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.unifiedvideo.player;
|
|
7
|
+
|
|
8
|
+
import android.content.Context;
|
|
9
|
+
import android.net.Uri;
|
|
10
|
+
import android.os.Handler;
|
|
11
|
+
import android.os.Looper;
|
|
12
|
+
import android.util.Log;
|
|
13
|
+
import android.view.Surface;
|
|
14
|
+
import android.view.View;
|
|
15
|
+
import android.view.ViewGroup;
|
|
16
|
+
import android.widget.FrameLayout;
|
|
17
|
+
|
|
18
|
+
import com.unifiedvideo.player.services.PlayerHolder;
|
|
19
|
+
import com.unifiedvideo.player.services.PlaybackService;
|
|
20
|
+
|
|
21
|
+
import androidx.annotation.NonNull;
|
|
22
|
+
import androidx.annotation.Nullable;
|
|
23
|
+
|
|
24
|
+
import com.google.android.exoplayer2.C;
|
|
25
|
+
import com.google.android.exoplayer2.ExoPlayer;
|
|
26
|
+
import com.google.android.exoplayer2.LoadControl;
|
|
27
|
+
import com.google.android.exoplayer2.MediaItem;
|
|
28
|
+
import com.google.android.exoplayer2.PlaybackException;
|
|
29
|
+
import com.google.android.exoplayer2.PlaybackParameters;
|
|
30
|
+
import com.google.android.exoplayer2.Player;
|
|
31
|
+
import com.google.android.exoplayer2.Timeline;
|
|
32
|
+
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
|
33
|
+
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
|
34
|
+
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
|
35
|
+
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
|
|
36
|
+
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
|
37
|
+
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
|
38
|
+
import com.google.android.exoplayer2.source.LoadEventInfo;
|
|
39
|
+
import com.google.android.exoplayer2.source.MediaLoadData;
|
|
40
|
+
import com.google.android.exoplayer2.source.MediaSource;
|
|
41
|
+
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
|
42
|
+
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
|
43
|
+
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
|
44
|
+
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
|
45
|
+
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
46
|
+
import com.google.android.exoplayer2.ui.PlayerView;
|
|
47
|
+
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
|
48
|
+
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
|
49
|
+
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
|
50
|
+
import com.google.android.exoplayer2.util.MimeTypes;
|
|
51
|
+
import com.google.android.exoplayer2.util.Util;
|
|
52
|
+
import com.google.android.exoplayer2.video.VideoSize;
|
|
53
|
+
|
|
54
|
+
import java.util.ArrayList;
|
|
55
|
+
import java.util.HashMap;
|
|
56
|
+
import java.util.List;
|
|
57
|
+
import java.util.Map;
|
|
58
|
+
import java.util.UUID;
|
|
59
|
+
import java.util.concurrent.TimeUnit;
|
|
60
|
+
|
|
61
|
+
// New imports
|
|
62
|
+
import android.app.Activity;
|
|
63
|
+
import android.app.PictureInPictureParams;
|
|
64
|
+
import android.app.RemoteAction;
|
|
65
|
+
import android.os.Build;
|
|
66
|
+
import android.util.Rational;
|
|
67
|
+
import android.app.PendingIntent;
|
|
68
|
+
import android.content.Intent;
|
|
69
|
+
import android.graphics.drawable.Icon;
|
|
70
|
+
|
|
71
|
+
import com.unifiedvideo.player.analytics.AnalyticsProvider;
|
|
72
|
+
import com.unifiedvideo.player.cast.CastManager;
|
|
73
|
+
import com.unifiedvideo.player.cast.CastManager.SubtitleItem;
|
|
74
|
+
import com.unifiedvideo.player.overlay.WatermarkOverlayView;
|
|
75
|
+
|
|
76
|
+
import androidx.appcompat.widget.AppCompatButton;
|
|
77
|
+
import androidx.appcompat.widget.AppCompatTextView;
|
|
78
|
+
import androidx.mediarouter.app.MediaRouteButton;
|
|
79
|
+
import com.google.android.gms.cast.framework.CastButtonFactory;
|
|
80
|
+
import com.google.android.gms.cast.framework.CastSession;
|
|
81
|
+
import com.google.android.gms.cast.framework.SessionManagerListener;
|
|
82
|
+
|
|
83
|
+
import android.view.Gravity;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Main Unified Video Player class for Android (Java implementation)
|
|
87
|
+
*/
|
|
88
|
+
public class UnifiedVideoPlayer {
|
|
89
|
+
|
|
90
|
+
private static final String TAG = "UnifiedVideoPlayer";
|
|
91
|
+
|
|
92
|
+
// Player components
|
|
93
|
+
private ExoPlayer exoPlayer;
|
|
94
|
+
private View playerView;
|
|
95
|
+
private ViewGroup container;
|
|
96
|
+
private PlayerConfiguration configuration;
|
|
97
|
+
private MediaSourceInfo currentSource;
|
|
98
|
+
private DefaultTrackSelector trackSelector;
|
|
99
|
+
|
|
100
|
+
// Overlay
|
|
101
|
+
private WatermarkOverlayView watermarkOverlay;
|
|
102
|
+
|
|
103
|
+
// Analytics
|
|
104
|
+
private final List<AnalyticsProvider> analyticsProviders = new ArrayList<>();
|
|
105
|
+
|
|
106
|
+
// Cast
|
|
107
|
+
private CastManager castManager;
|
|
108
|
+
private MediaRouteButton castButton;
|
|
109
|
+
private AppCompatButton stopCastBtn;
|
|
110
|
+
private AppCompatTextView subtitleBtn;
|
|
111
|
+
private SessionManagerListener<CastSession> castSessionListener;
|
|
112
|
+
|
|
113
|
+
// Context
|
|
114
|
+
private final Context context;
|
|
115
|
+
|
|
116
|
+
// Handler for progress updates
|
|
117
|
+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
118
|
+
private Runnable updateProgressHandler;
|
|
119
|
+
private Runnable watermarkRandomizer;
|
|
120
|
+
|
|
121
|
+
// State properties
|
|
122
|
+
private PlayerState state = PlayerState.IDLE;
|
|
123
|
+
private boolean isPlaying = false;
|
|
124
|
+
private long duration = 0;
|
|
125
|
+
private long currentPosition = 0;
|
|
126
|
+
private long bufferedPosition = 0;
|
|
127
|
+
private float volume = 1.0f;
|
|
128
|
+
|
|
129
|
+
// Event listeners
|
|
130
|
+
private PlayerEventListener eventListener;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Player states
|
|
134
|
+
*/
|
|
135
|
+
public enum PlayerState {
|
|
136
|
+
IDLE,
|
|
137
|
+
LOADING,
|
|
138
|
+
READY,
|
|
139
|
+
PLAYING,
|
|
140
|
+
PAUSED,
|
|
141
|
+
BUFFERING,
|
|
142
|
+
SEEKING,
|
|
143
|
+
ENDED,
|
|
144
|
+
ERROR
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Event listener interface
|
|
149
|
+
*/
|
|
150
|
+
public interface PlayerEventListener {
|
|
151
|
+
void onReady();
|
|
152
|
+
void onPlay();
|
|
153
|
+
void onPause();
|
|
154
|
+
void onTimeUpdate(long currentTime);
|
|
155
|
+
void onBuffering(boolean isBuffering);
|
|
156
|
+
void onSeek(long position);
|
|
157
|
+
void onEnded();
|
|
158
|
+
void onError(Exception error);
|
|
159
|
+
void onLoadedMetadata(Map<String, Object> metadata);
|
|
160
|
+
void onVolumeChange(float volume);
|
|
161
|
+
void onStateChange(PlayerState state);
|
|
162
|
+
void onProgress(long bufferedPosition);
|
|
163
|
+
void onVideoSizeChanged(int width, int height);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Constructor
|
|
168
|
+
* @param context Android context
|
|
169
|
+
*/
|
|
170
|
+
public UnifiedVideoPlayer(@NonNull Context context) {
|
|
171
|
+
this.context = context.getApplicationContext();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Initialize the player with container and configuration
|
|
176
|
+
* @param container ViewGroup to hold the player view
|
|
177
|
+
* @param configuration Player configuration (optional)
|
|
178
|
+
*/
|
|
179
|
+
public void initialize(@NonNull ViewGroup container, @Nullable PlayerConfiguration configuration) {
|
|
180
|
+
this.container = container;
|
|
181
|
+
this.configuration = configuration != null ? configuration : new PlayerConfiguration.Builder().build();
|
|
182
|
+
|
|
183
|
+
setupPlayer();
|
|
184
|
+
applyConfiguration();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Set event listener
|
|
189
|
+
* @param listener Event listener implementation
|
|
190
|
+
*/
|
|
191
|
+
public void setEventListener(PlayerEventListener listener) {
|
|
192
|
+
this.eventListener = listener;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Setup the player
|
|
197
|
+
*/
|
|
198
|
+
private void setupPlayer() {
|
|
199
|
+
// Create track selector for adaptive streaming
|
|
200
|
+
trackSelector = new DefaultTrackSelector(context);
|
|
201
|
+
trackSelector.setParameters(
|
|
202
|
+
trackSelector.buildUponParameters()
|
|
203
|
+
.setMaxVideoSizeSd()
|
|
204
|
+
.build()
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Create player
|
|
208
|
+
exoPlayer = new ExoPlayer.Builder(context)
|
|
209
|
+
.setTrackSelector(trackSelector)
|
|
210
|
+
.build();
|
|
211
|
+
// Expose player to services (background & PiP actions)
|
|
212
|
+
PlayerHolder.setPlayer(exoPlayer);
|
|
213
|
+
|
|
214
|
+
// Add listeners
|
|
215
|
+
exoPlayer.addListener(playerEventListener);
|
|
216
|
+
exoPlayer.addAnalyticsListener(analyticsListener);
|
|
217
|
+
|
|
218
|
+
// Create player view
|
|
219
|
+
if (configuration.useStyledControls) {
|
|
220
|
+
StyledPlayerView styledPlayerView = new StyledPlayerView(context);
|
|
221
|
+
styledPlayerView.setPlayer(exoPlayer);
|
|
222
|
+
styledPlayerView.setUseController(configuration.controls);
|
|
223
|
+
styledPlayerView.setControllerShowTimeoutMs(3000);
|
|
224
|
+
styledPlayerView.setControllerHideOnTouch(true);
|
|
225
|
+
styledPlayerView.setShowBuffering(StyledPlayerView.SHOW_BUFFERING_WHEN_PLAYING);
|
|
226
|
+
playerView = styledPlayerView;
|
|
227
|
+
} else {
|
|
228
|
+
PlayerView simplePlayerView = new PlayerView(context);
|
|
229
|
+
simplePlayerView.setPlayer(exoPlayer);
|
|
230
|
+
simplePlayerView.setUseController(configuration.controls);
|
|
231
|
+
simplePlayerView.setControllerShowTimeoutMs(3000);
|
|
232
|
+
simplePlayerView.setControllerHideOnTouch(true);
|
|
233
|
+
playerView = simplePlayerView;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add to container
|
|
237
|
+
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
|
|
238
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
239
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
240
|
+
);
|
|
241
|
+
playerView.setLayoutParams(layoutParams);
|
|
242
|
+
container.addView(playerView);
|
|
243
|
+
|
|
244
|
+
// Add watermark overlay on top (optional by default)
|
|
245
|
+
watermarkOverlay = new WatermarkOverlayView(context);
|
|
246
|
+
watermarkOverlay.setLayoutParams(new FrameLayout.LayoutParams(
|
|
247
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
248
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
249
|
+
));
|
|
250
|
+
watermarkOverlay.setAlphaFactor(0.3f);
|
|
251
|
+
container.addView(watermarkOverlay);
|
|
252
|
+
|
|
253
|
+
// Add Cast button (top-right)
|
|
254
|
+
castButton = new MediaRouteButton(context);
|
|
255
|
+
FrameLayout.LayoutParams castLp = new FrameLayout.LayoutParams(
|
|
256
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
257
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
258
|
+
);
|
|
259
|
+
castLp.gravity = Gravity.TOP | Gravity.END;
|
|
260
|
+
castLp.topMargin = 16; castLp.rightMargin = 16;
|
|
261
|
+
castButton.setLayoutParams(castLp);
|
|
262
|
+
try { CastButtonFactory.setUpMediaRouteButton(context, castButton); } catch (Exception ignored) {}
|
|
263
|
+
container.addView(castButton);
|
|
264
|
+
|
|
265
|
+
// Stop Cast pill button (hidden until session active)
|
|
266
|
+
stopCastBtn = new AppCompatButton(context);
|
|
267
|
+
stopCastBtn.setText("Stop Casting");
|
|
268
|
+
FrameLayout.LayoutParams stopLp = new FrameLayout.LayoutParams(
|
|
269
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
270
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
271
|
+
);
|
|
272
|
+
stopLp.gravity = Gravity.TOP | Gravity.END;
|
|
273
|
+
stopLp.topMargin = 72; stopLp.rightMargin = 16;
|
|
274
|
+
stopCastBtn.setLayoutParams(stopLp);
|
|
275
|
+
stopCastBtn.setVisibility(View.GONE);
|
|
276
|
+
stopCastBtn.setOnClickListener(v -> { if (castManager != null) castManager.stopCasting(); });
|
|
277
|
+
container.addView(stopCastBtn);
|
|
278
|
+
|
|
279
|
+
// Subtitle toggle button (cycles through tracks)
|
|
280
|
+
subtitleBtn = new AppCompatTextView(context);
|
|
281
|
+
subtitleBtn.setText("CC");
|
|
282
|
+
subtitleBtn.setPadding(20, 10, 20, 10);
|
|
283
|
+
subtitleBtn.setBackgroundColor(0x66000000);
|
|
284
|
+
subtitleBtn.setTextColor(0xFFFFFFFF);
|
|
285
|
+
FrameLayout.LayoutParams subLp = new FrameLayout.LayoutParams(
|
|
286
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
287
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
288
|
+
);
|
|
289
|
+
subLp.gravity = Gravity.TOP | Gravity.END;
|
|
290
|
+
subLp.topMargin = 128; subLp.rightMargin = 16;
|
|
291
|
+
subtitleBtn.setLayoutParams(subLp);
|
|
292
|
+
subtitleBtn.setOnClickListener(v -> cycleSubtitle());
|
|
293
|
+
container.addView(subtitleBtn);
|
|
294
|
+
|
|
295
|
+
// Start progress updates
|
|
296
|
+
startProgressUpdates();
|
|
297
|
+
|
|
298
|
+
// Schedule watermark movement
|
|
299
|
+
scheduleWatermark();
|
|
300
|
+
|
|
301
|
+
updateState(PlayerState.IDLE);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Apply configuration to the player
|
|
306
|
+
*/
|
|
307
|
+
private void applyConfiguration() {
|
|
308
|
+
if (configuration == null) return;
|
|
309
|
+
|
|
310
|
+
exoPlayer.setVolume(configuration.volume);
|
|
311
|
+
exoPlayer.setPlaybackParameters(new PlaybackParameters(configuration.playbackSpeed));
|
|
312
|
+
|
|
313
|
+
if (configuration.loop) {
|
|
314
|
+
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
|
|
315
|
+
} else {
|
|
316
|
+
exoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (configuration.muted) {
|
|
320
|
+
exoPlayer.setVolume(0f);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (configuration.debug) {
|
|
324
|
+
enableDebugLogging();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Load media source
|
|
330
|
+
* @param source Media source information
|
|
331
|
+
*/
|
|
332
|
+
public void load(@NonNull MediaSourceInfo source) {
|
|
333
|
+
currentSource = source;
|
|
334
|
+
updateState(PlayerState.LOADING);
|
|
335
|
+
|
|
336
|
+
Uri uri = Uri.parse(source.url);
|
|
337
|
+
MediaSource mediaSource = createMediaSource(uri, source);
|
|
338
|
+
|
|
339
|
+
exoPlayer.setMediaSource(mediaSource);
|
|
340
|
+
exoPlayer.prepare();
|
|
341
|
+
|
|
342
|
+
// Apply start time if configured
|
|
343
|
+
if (configuration.startTime > 0) {
|
|
344
|
+
seekTo(configuration.startTime);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Auto-play if configured
|
|
348
|
+
if (configuration.autoPlay) {
|
|
349
|
+
play();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Load subtitles if provided
|
|
353
|
+
if (source.subtitles != null && !source.subtitles.isEmpty()) {
|
|
354
|
+
loadSubtitles(source.subtitles);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Load media from URL
|
|
360
|
+
* @param url Media URL
|
|
361
|
+
*/
|
|
362
|
+
public void load(@NonNull String url) {
|
|
363
|
+
MediaSourceInfo source = new MediaSourceInfo(url);
|
|
364
|
+
load(source);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create ExoPlayer MediaSource
|
|
369
|
+
*/
|
|
370
|
+
private MediaSource createMediaSource(Uri uri, MediaSourceInfo source) {
|
|
371
|
+
DefaultDataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
|
|
372
|
+
|
|
373
|
+
// Configure DRM if needed
|
|
374
|
+
DrmSessionManagerProvider drmSessionManagerProvider = null;
|
|
375
|
+
if (source.drm != null) {
|
|
376
|
+
drmSessionManagerProvider = createDrmSessionManagerProvider(source.drm);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri);
|
|
380
|
+
|
|
381
|
+
// Add DRM configuration
|
|
382
|
+
if (source.drm != null) {
|
|
383
|
+
MediaItem.DrmConfiguration drmConfig = new MediaItem.DrmConfiguration.Builder(getDrmUuid(source.drm.type))
|
|
384
|
+
.setLicenseUri(source.drm.licenseUrl)
|
|
385
|
+
.setMultiSession(source.drm.multiSession)
|
|
386
|
+
.setForceDefaultLicenseUri(source.drm.forceDefaultLicenseUri)
|
|
387
|
+
.setLicenseRequestHeaders(source.drm.headers != null ? source.drm.headers : new HashMap<>())
|
|
388
|
+
.build();
|
|
389
|
+
mediaItemBuilder.setDrmConfiguration(drmConfig);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Attach side-loaded subtitles if provided
|
|
393
|
+
if (source.subtitles != null && !source.subtitles.isEmpty()) {
|
|
394
|
+
List<MediaItem.SubtitleConfiguration> subs = new ArrayList<>();
|
|
395
|
+
for (SubtitleTrack st : source.subtitles) {
|
|
396
|
+
MediaItem.SubtitleConfiguration subCfg = new MediaItem.SubtitleConfiguration.Builder(Uri.parse(st.url))
|
|
397
|
+
.setMimeType(st.mimeType != null ? st.mimeType : MimeTypes.TEXT_VTT)
|
|
398
|
+
.setLanguage(st.language)
|
|
399
|
+
.setLabel(st.label)
|
|
400
|
+
.build();
|
|
401
|
+
subs.add(subCfg);
|
|
402
|
+
}
|
|
403
|
+
mediaItemBuilder.setSubtitleConfigurations(subs);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
MediaItem mediaItem = mediaItemBuilder.build();
|
|
407
|
+
|
|
408
|
+
// Create appropriate media source based on type
|
|
409
|
+
String type = source.type != null ? source.type : MediaSourceInfo.detectType(source.url);
|
|
410
|
+
|
|
411
|
+
switch (type) {
|
|
412
|
+
case "hls":
|
|
413
|
+
HlsMediaSource.Factory hlsFactory = new HlsMediaSource.Factory(dataSourceFactory);
|
|
414
|
+
if (drmSessionManagerProvider != null) {
|
|
415
|
+
hlsFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
|
416
|
+
}
|
|
417
|
+
return hlsFactory.createMediaSource(mediaItem);
|
|
418
|
+
|
|
419
|
+
case "dash":
|
|
420
|
+
DashMediaSource.Factory dashFactory = new DashMediaSource.Factory(dataSourceFactory);
|
|
421
|
+
if (drmSessionManagerProvider != null) {
|
|
422
|
+
dashFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
|
423
|
+
}
|
|
424
|
+
return dashFactory.createMediaSource(mediaItem);
|
|
425
|
+
|
|
426
|
+
case "smoothstreaming":
|
|
427
|
+
SsMediaSource.Factory ssFactory = new SsMediaSource.Factory(dataSourceFactory);
|
|
428
|
+
if (drmSessionManagerProvider != null) {
|
|
429
|
+
ssFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
|
430
|
+
}
|
|
431
|
+
return ssFactory.createMediaSource(mediaItem);
|
|
432
|
+
|
|
433
|
+
default:
|
|
434
|
+
ProgressiveMediaSource.Factory progressiveFactory = new ProgressiveMediaSource.Factory(dataSourceFactory);
|
|
435
|
+
if (drmSessionManagerProvider != null) {
|
|
436
|
+
progressiveFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
|
437
|
+
}
|
|
438
|
+
return progressiveFactory.createMediaSource(mediaItem);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Create DRM Session Manager Provider
|
|
444
|
+
*/
|
|
445
|
+
private DrmSessionManagerProvider createDrmSessionManagerProvider(DRMConfiguration drm) {
|
|
446
|
+
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(
|
|
447
|
+
drm.licenseUrl,
|
|
448
|
+
new DefaultHttpDataSource.Factory()
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (drm.headers != null) {
|
|
452
|
+
for (Map.Entry<String, String> entry : drm.headers.entrySet()) {
|
|
453
|
+
drmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue());
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return new DrmSessionManagerProvider() {
|
|
458
|
+
@Override
|
|
459
|
+
public DrmSessionManager get(MediaItem mediaItem) {
|
|
460
|
+
DefaultDrmSessionManager drmSessionManager = new DefaultDrmSessionManager.Builder()
|
|
461
|
+
.setUuidAndExoMediaDrmProvider(
|
|
462
|
+
getDrmUuid(drm.type),
|
|
463
|
+
FrameworkMediaDrm.DEFAULT_PROVIDER
|
|
464
|
+
)
|
|
465
|
+
.build(drmCallback);
|
|
466
|
+
|
|
467
|
+
drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, new byte[0]);
|
|
468
|
+
return drmSessionManager;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get DRM UUID from type
|
|
475
|
+
*/
|
|
476
|
+
private UUID getDrmUuid(String drmType) {
|
|
477
|
+
switch (drmType.toLowerCase()) {
|
|
478
|
+
case "widevine":
|
|
479
|
+
return C.WIDEVINE_UUID;
|
|
480
|
+
case "playready":
|
|
481
|
+
return C.PLAYREADY_UUID;
|
|
482
|
+
case "clearkey":
|
|
483
|
+
return C.CLEARKEY_UUID;
|
|
484
|
+
default:
|
|
485
|
+
return C.WIDEVINE_UUID;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Load subtitles
|
|
491
|
+
*/
|
|
492
|
+
private void loadSubtitles(List<SubtitleTrack> subtitles) {
|
|
493
|
+
// Subtitles are attached via MediaItem subtitle configurations in createMediaSource.
|
|
494
|
+
for (SubtitleTrack subtitle : subtitles) {
|
|
495
|
+
Log.d(TAG, "Subtitle configured: " + subtitle.label + " (" + subtitle.language + ")");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Playback Control Methods
|
|
500
|
+
|
|
501
|
+
public void play() {
|
|
502
|
+
exoPlayer.play();
|
|
503
|
+
isPlaying = true;
|
|
504
|
+
updateState(PlayerState.PLAYING);
|
|
505
|
+
if (configuration != null && configuration.allowBackgroundPlayback) {
|
|
506
|
+
try { PlaybackService.start(context); } catch (Exception ignored) {}
|
|
507
|
+
}
|
|
508
|
+
if (eventListener != null) eventListener.onPlay();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
public void pause() {
|
|
512
|
+
exoPlayer.pause();
|
|
513
|
+
isPlaying = false;
|
|
514
|
+
updateState(PlayerState.PAUSED);
|
|
515
|
+
if (configuration != null && configuration.allowBackgroundPlayback) {
|
|
516
|
+
try { PlaybackService.start(context); } catch (Exception ignored) {}
|
|
517
|
+
}
|
|
518
|
+
if (eventListener != null) eventListener.onPause();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
public void stop() {
|
|
522
|
+
exoPlayer.stop();
|
|
523
|
+
exoPlayer.seekTo(0);
|
|
524
|
+
isPlaying = false;
|
|
525
|
+
updateState(PlayerState.IDLE);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public void togglePlayPause() {
|
|
529
|
+
if (isPlaying) {
|
|
530
|
+
pause();
|
|
531
|
+
} else {
|
|
532
|
+
play();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
public void seekTo(long position) {
|
|
537
|
+
updateState(PlayerState.SEEKING);
|
|
538
|
+
exoPlayer.seekTo(position);
|
|
539
|
+
if (eventListener != null) eventListener.onSeek(position);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
public void seekForward(int seconds) {
|
|
543
|
+
long newPosition = currentPosition + (seconds * 1000);
|
|
544
|
+
seekTo(Math.min(newPosition, duration));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
public void seekBackward(int seconds) {
|
|
548
|
+
long newPosition = currentPosition - (seconds * 1000);
|
|
549
|
+
seekTo(Math.max(newPosition, 0));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Volume Control
|
|
553
|
+
|
|
554
|
+
public void setVolume(float volume) {
|
|
555
|
+
float clampedVolume = Math.max(0f, Math.min(1f, volume));
|
|
556
|
+
this.volume = clampedVolume;
|
|
557
|
+
exoPlayer.setVolume(clampedVolume);
|
|
558
|
+
if (eventListener != null) eventListener.onVolumeChange(clampedVolume);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
public void mute() {
|
|
562
|
+
exoPlayer.setVolume(0f);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
public void unmute() {
|
|
566
|
+
exoPlayer.setVolume(volume);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
public void toggleMute() {
|
|
570
|
+
if (exoPlayer.getVolume() == 0f) {
|
|
571
|
+
unmute();
|
|
572
|
+
} else {
|
|
573
|
+
mute();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Playback Speed
|
|
578
|
+
|
|
579
|
+
public void setPlaybackSpeed(float speed) {
|
|
580
|
+
exoPlayer.setPlaybackParameters(new PlaybackParameters(speed));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
public float getPlaybackSpeed() {
|
|
584
|
+
return exoPlayer.getPlaybackParameters().speed;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Quality Selection
|
|
588
|
+
|
|
589
|
+
public void setVideoQuality(String quality) {
|
|
590
|
+
DefaultTrackSelector.Parameters.Builder parametersBuilder = trackSelector.buildUponParameters();
|
|
591
|
+
|
|
592
|
+
switch (quality) {
|
|
593
|
+
case "auto":
|
|
594
|
+
trackSelector.setParameters(parametersBuilder.clearVideoSizeConstraints().build());
|
|
595
|
+
break;
|
|
596
|
+
case "hd":
|
|
597
|
+
trackSelector.setParameters(
|
|
598
|
+
parametersBuilder
|
|
599
|
+
.setMaxVideoSize(1920, 1080)
|
|
600
|
+
.setMinVideoSize(1280, 720)
|
|
601
|
+
.build()
|
|
602
|
+
);
|
|
603
|
+
break;
|
|
604
|
+
case "sd":
|
|
605
|
+
trackSelector.setParameters(parametersBuilder.setMaxVideoSizeSd().build());
|
|
606
|
+
break;
|
|
607
|
+
case "low":
|
|
608
|
+
trackSelector.setParameters(
|
|
609
|
+
parametersBuilder.setMaxVideoSize(854, 480).build()
|
|
610
|
+
);
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// State Management
|
|
616
|
+
|
|
617
|
+
private void updateState(PlayerState newState) {
|
|
618
|
+
state = newState;
|
|
619
|
+
if (eventListener != null) eventListener.onStateChange(newState);
|
|
620
|
+
trackAnalytics("statechange", new HashMap<String, Object>() {{ put("state", newState.name()); }});
|
|
621
|
+
|
|
622
|
+
if (configuration.debug) {
|
|
623
|
+
Log.d(TAG, "State changed to: " + newState);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Progress Updates
|
|
628
|
+
|
|
629
|
+
private void startProgressUpdates() {
|
|
630
|
+
updateProgressHandler = new Runnable() {
|
|
631
|
+
@Override
|
|
632
|
+
public void run() {
|
|
633
|
+
if (exoPlayer != null) {
|
|
634
|
+
currentPosition = exoPlayer.getCurrentPosition();
|
|
635
|
+
bufferedPosition = exoPlayer.getBufferedPosition();
|
|
636
|
+
duration = exoPlayer.getDuration();
|
|
637
|
+
|
|
638
|
+
if (eventListener != null) {
|
|
639
|
+
eventListener.onTimeUpdate(currentPosition);
|
|
640
|
+
eventListener.onProgress(bufferedPosition);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
mainHandler.postDelayed(this, 100);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
mainHandler.post(updateProgressHandler);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private void stopProgressUpdates() {
|
|
650
|
+
if (updateProgressHandler != null) {
|
|
651
|
+
mainHandler.removeCallbacks(updateProgressHandler);
|
|
652
|
+
}
|
|
653
|
+
if (watermarkRandomizer != null) {
|
|
654
|
+
mainHandler.removeCallbacks(watermarkRandomizer);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private void scheduleWatermark() {
|
|
659
|
+
if (watermarkOverlay == null) return;
|
|
660
|
+
watermarkRandomizer = new Runnable() {
|
|
661
|
+
@Override
|
|
662
|
+
public void run() {
|
|
663
|
+
try {
|
|
664
|
+
watermarkOverlay.randomize();
|
|
665
|
+
} catch (Exception ignored) {}
|
|
666
|
+
mainHandler.postDelayed(this, 5000);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
mainHandler.postDelayed(watermarkRandomizer, 5000);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Player Event Listener
|
|
673
|
+
|
|
674
|
+
private final Player.Listener playerEventListener = new Player.Listener() {
|
|
675
|
+
@Override
|
|
676
|
+
public void onPlaybackStateChanged(int playbackState) {
|
|
677
|
+
switch (playbackState) {
|
|
678
|
+
case Player.STATE_IDLE:
|
|
679
|
+
updateState(PlayerState.IDLE);
|
|
680
|
+
break;
|
|
681
|
+
case Player.STATE_BUFFERING:
|
|
682
|
+
updateState(PlayerState.BUFFERING);
|
|
683
|
+
if (eventListener != null) eventListener.onBuffering(true);
|
|
684
|
+
break;
|
|
685
|
+
case Player.STATE_READY:
|
|
686
|
+
if (state == PlayerState.LOADING || state == PlayerState.BUFFERING) {
|
|
687
|
+
updateState(PlayerState.READY);
|
|
688
|
+
if (eventListener != null) {
|
|
689
|
+
eventListener.onReady();
|
|
690
|
+
emitLoadedMetadata();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (state == PlayerState.BUFFERING && eventListener != null) {
|
|
694
|
+
eventListener.onBuffering(false);
|
|
695
|
+
}
|
|
696
|
+
if (exoPlayer.isPlaying()) {
|
|
697
|
+
updateState(PlayerState.PLAYING);
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
case Player.STATE_ENDED:
|
|
701
|
+
updateState(PlayerState.ENDED);
|
|
702
|
+
if (eventListener != null) eventListener.onEnded();
|
|
703
|
+
|
|
704
|
+
if (configuration.loop) {
|
|
705
|
+
seekTo(0);
|
|
706
|
+
play();
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
@Override
|
|
713
|
+
public void onIsPlayingChanged(boolean isPlaying) {
|
|
714
|
+
UnifiedVideoPlayer.this.isPlaying = isPlaying;
|
|
715
|
+
if (isPlaying) {
|
|
716
|
+
updateState(PlayerState.PLAYING);
|
|
717
|
+
trackAnalytics("play", null);
|
|
718
|
+
} else if (state == PlayerState.PLAYING) {
|
|
719
|
+
updateState(PlayerState.PAUSED);
|
|
720
|
+
trackAnalytics("pause", null);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
@Override
|
|
725
|
+
public void onPlayerError(PlaybackException error) {
|
|
726
|
+
updateState(PlayerState.ERROR);
|
|
727
|
+
if (eventListener != null) eventListener.onError(error);
|
|
728
|
+
trackAnalytics("error", new HashMap<String, Object>() {{ put("message", error.getMessage()); }});
|
|
729
|
+
|
|
730
|
+
if (configuration.debug) {
|
|
731
|
+
Log.e(TAG, "Player error: " + error.getMessage(), error);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
@Override
|
|
736
|
+
public void onVideoSizeChanged(VideoSize videoSize) {
|
|
737
|
+
if (eventListener != null) {
|
|
738
|
+
eventListener.onVideoSizeChanged(videoSize.width, videoSize.height);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
@Override
|
|
743
|
+
public void onRenderedFirstFrame() {
|
|
744
|
+
Log.d(TAG, "First frame rendered");
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// Analytics Listener
|
|
749
|
+
|
|
750
|
+
private final AnalyticsListener analyticsListener = new AnalyticsListener() {
|
|
751
|
+
@Override
|
|
752
|
+
public void onLoadCompleted(AnalyticsListener.EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
|
|
753
|
+
Log.d(TAG, "Load completed: " + loadEventInfo.dataSpec.uri);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
@Override
|
|
757
|
+
public void onBandwidthEstimate(AnalyticsListener.EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
|
|
758
|
+
if (configuration.debug) {
|
|
759
|
+
Log.d(TAG, "Bandwidth estimate: " + bitrateEstimate + " bps");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
private void emitLoadedMetadata() {
|
|
765
|
+
Map<String, Object> metadata = new HashMap<>();
|
|
766
|
+
|
|
767
|
+
if (exoPlayer != null) {
|
|
768
|
+
metadata.put("duration", exoPlayer.getDuration());
|
|
769
|
+
|
|
770
|
+
if (exoPlayer.getVideoFormat() != null) {
|
|
771
|
+
metadata.put("width", exoPlayer.getVideoFormat().width);
|
|
772
|
+
metadata.put("height", exoPlayer.getVideoFormat().height);
|
|
773
|
+
metadata.put("frameRate", exoPlayer.getVideoFormat().frameRate);
|
|
774
|
+
metadata.put("bitrate", exoPlayer.getVideoFormat().bitrate);
|
|
775
|
+
metadata.put("codec", exoPlayer.getVideoFormat().codecs != null ? exoPlayer.getVideoFormat().codecs : "");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (exoPlayer.getAudioFormat() != null) {
|
|
779
|
+
metadata.put("audioChannels", exoPlayer.getAudioFormat().channelCount);
|
|
780
|
+
metadata.put("audioSampleRate", exoPlayer.getAudioFormat().sampleRate);
|
|
781
|
+
metadata.put("audioCodec", exoPlayer.getAudioFormat().codecs != null ? exoPlayer.getAudioFormat().codecs : "");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (eventListener != null) {
|
|
786
|
+
eventListener.onLoadedMetadata(metadata);
|
|
787
|
+
}
|
|
788
|
+
trackAnalytics("loadedmetadata", metadata);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Surface Control (for advanced use cases)
|
|
792
|
+
|
|
793
|
+
public void setVideoSurface(Surface surface) {
|
|
794
|
+
exoPlayer.setVideoSurface(surface);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
public void clearVideoSurface() {
|
|
798
|
+
exoPlayer.clearVideoSurface();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Debug
|
|
802
|
+
|
|
803
|
+
private void enableDebugLogging() {
|
|
804
|
+
Log.d(TAG, "Debug mode enabled");
|
|
805
|
+
Log.d(TAG, "Configuration: " + configuration);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Lifecycle Management
|
|
809
|
+
|
|
810
|
+
public void onResume() {
|
|
811
|
+
if (exoPlayer != null && state == PlayerState.PLAYING) {
|
|
812
|
+
exoPlayer.play();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Enter Picture-in-Picture mode (requires host Activity with android:supportsPictureInPicture="true"). */
|
|
817
|
+
public boolean enterPictureInPicture(@NonNull Activity activity) {
|
|
818
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
819
|
+
try {
|
|
820
|
+
PendingIntent playPi = PendingIntent.getBroadcast(activity, 200,
|
|
821
|
+
new Intent("com.unifiedvideo.player.ACTION_PLAY"), Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0);
|
|
822
|
+
PendingIntent pausePi = PendingIntent.getBroadcast(activity, 201,
|
|
823
|
+
new Intent("com.unifiedvideo.player.ACTION_PAUSE"), Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0);
|
|
824
|
+
|
|
825
|
+
RemoteAction playAction = new RemoteAction(
|
|
826
|
+
Icon.createWithResource(activity, android.R.drawable.ic_media_play),
|
|
827
|
+
"Play",
|
|
828
|
+
"Play",
|
|
829
|
+
playPi
|
|
830
|
+
);
|
|
831
|
+
RemoteAction pauseAction = new RemoteAction(
|
|
832
|
+
Icon.createWithResource(activity, android.R.drawable.ic_media_pause),
|
|
833
|
+
"Pause",
|
|
834
|
+
"Pause",
|
|
835
|
+
pausePi
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
|
839
|
+
.setAspectRatio(new Rational(16, 9))
|
|
840
|
+
.setActions(java.util.Arrays.asList(playAction, pauseAction))
|
|
841
|
+
.build();
|
|
842
|
+
activity.enterPictureInPictureMode(params);
|
|
843
|
+
return true;
|
|
844
|
+
} catch (Exception e) {
|
|
845
|
+
Log.w(TAG, "PiP entry failed", e);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
public void onPause() {
|
|
852
|
+
if (!configuration.allowBackgroundPlayback && exoPlayer != null) {
|
|
853
|
+
exoPlayer.pause();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
public void onStop() {
|
|
858
|
+
if (!configuration.allowBackgroundPlayback && exoPlayer != null) {
|
|
859
|
+
exoPlayer.stop();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Cleanup
|
|
864
|
+
|
|
865
|
+
public void release() {
|
|
866
|
+
stopProgressUpdates();
|
|
867
|
+
try { PlaybackService.stop(context); } catch (Exception ignored) {}
|
|
868
|
+
|
|
869
|
+
if (exoPlayer != null) {
|
|
870
|
+
exoPlayer.removeListener(playerEventListener);
|
|
871
|
+
exoPlayer.removeAnalyticsListener(analyticsListener);
|
|
872
|
+
exoPlayer.release();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (playerView != null && container != null) {
|
|
876
|
+
container.removeView(playerView);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
exoPlayer = null;
|
|
880
|
+
playerView = null;
|
|
881
|
+
container = null;
|
|
882
|
+
|
|
883
|
+
updateState(PlayerState.IDLE);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Getters
|
|
887
|
+
|
|
888
|
+
public PlayerState getState() {
|
|
889
|
+
return state;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
public boolean isPlaying() {
|
|
893
|
+
return isPlaying;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
public long getDuration() {
|
|
897
|
+
return duration;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
public long getCurrentPosition() {
|
|
901
|
+
return currentPosition;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
public long getBufferedPosition() {
|
|
905
|
+
return bufferedPosition;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
public float getVolume() {
|
|
909
|
+
return volume;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Theming & Watermark
|
|
913
|
+
public void setThemeColors(int accentStart, int accentEnd) {
|
|
914
|
+
if (watermarkOverlay != null) {
|
|
915
|
+
watermarkOverlay.setAccentColors(accentStart, accentEnd);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
public void setWatermarkEnabled(boolean enabled) {
|
|
920
|
+
if (watermarkOverlay != null) {
|
|
921
|
+
watermarkOverlay.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Analytics wiring
|
|
926
|
+
public void addAnalyticsProvider(AnalyticsProvider provider) {
|
|
927
|
+
if (provider != null) analyticsProviders.add(provider);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
public void setAnalyticsProviders(List<AnalyticsProvider> providers) {
|
|
931
|
+
analyticsProviders.clear();
|
|
932
|
+
if (providers != null) analyticsProviders.addAll(providers);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private void trackAnalytics(String event, Map<String, Object> data) {
|
|
936
|
+
if (analyticsProviders.isEmpty()) return;
|
|
937
|
+
Map<String, Object> payload = new HashMap<>();
|
|
938
|
+
if (data != null) payload.putAll(data);
|
|
939
|
+
payload.put("timestamp", System.currentTimeMillis());
|
|
940
|
+
payload.put("position", currentPosition);
|
|
941
|
+
payload.put("duration", duration);
|
|
942
|
+
for (AnalyticsProvider p : analyticsProviders) {
|
|
943
|
+
try {
|
|
944
|
+
p.track(event, payload);
|
|
945
|
+
} catch (Exception ignored) {}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Casting API
|
|
950
|
+
public void enableCasting() {
|
|
951
|
+
if (castManager == null) castManager = new CastManager();
|
|
952
|
+
if (castSessionListener == null) {
|
|
953
|
+
castSessionListener = new SessionManagerListener<CastSession>() {
|
|
954
|
+
@Override public void onSessionStarted(CastSession session, String s) { updateCastUi(true); }
|
|
955
|
+
@Override public void onSessionResumed(CastSession session, boolean b) { updateCastUi(true); }
|
|
956
|
+
@Override public void onSessionEnded(CastSession session, int i) { updateCastUi(false); }
|
|
957
|
+
@Override public void onSessionSuspended(CastSession session, int i) { updateCastUi(false); }
|
|
958
|
+
@Override public void onSessionStarting(CastSession session) {}
|
|
959
|
+
@Override public void onSessionResuming(CastSession session, String s) {}
|
|
960
|
+
@Override public void onSessionEnding(CastSession session) {}
|
|
961
|
+
@Override public void onSessionResumeFailed(CastSession session, int i) {}
|
|
962
|
+
@Override public void onSessionStartFailed(CastSession session, int i) { updateCastUi(false); }
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
castManager.addSessionManagerListener(castSessionListener);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private void updateCastUi(boolean connected) {
|
|
969
|
+
if (stopCastBtn != null) {
|
|
970
|
+
stopCastBtn.post(() -> stopCastBtn.setVisibility(connected ? View.VISIBLE : View.GONE));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
public boolean isCastingAvailable() {
|
|
975
|
+
if (castManager == null) return false;
|
|
976
|
+
return castManager.hasSession();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
public void castCurrentMedia() {
|
|
980
|
+
if (castManager == null || currentSource == null) return;
|
|
981
|
+
String ct = inferContentType(currentSource);
|
|
982
|
+
List<SubtitleItem> subs = null;
|
|
983
|
+
if (currentSource.subtitles != null && !currentSource.subtitles.isEmpty()) {
|
|
984
|
+
subs = new ArrayList<>();
|
|
985
|
+
for (SubtitleTrack st : currentSource.subtitles) {
|
|
986
|
+
subs.add(new SubtitleItem(st.url, st.language, st.label, st.mimeType));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
castManager.startCasting(currentSource.url, ct, currentSource.metadata != null ? String.valueOf(currentSource.metadata.get("title")) : null, subs, null);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private String inferContentType(MediaSourceInfo src) {
|
|
993
|
+
String u = src.url != null ? src.url.toLowerCase() : "";
|
|
994
|
+
if (u.endsWith(".m3u8")) return "application/x-mpegURL";
|
|
995
|
+
if (u.endsWith(".mpd")) return "application/dash+xml";
|
|
996
|
+
if (u.endsWith(".webm")) return "video/webm";
|
|
997
|
+
return "video/mp4";
|
|
998
|
+
}
|
|
999
|
+
// Subtitle selection helpers
|
|
1000
|
+
private int subtitleIndex = -1; // -1 off
|
|
1001
|
+
private void cycleSubtitle() {
|
|
1002
|
+
if (currentSource == null || currentSource.subtitles == null || currentSource.subtitles.isEmpty()) {
|
|
1003
|
+
// toggle off/on no-op
|
|
1004
|
+
disableSubtitles();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
subtitleIndex++;
|
|
1008
|
+
if (subtitleIndex >= currentSource.subtitles.size()) subtitleIndex = -1; // off
|
|
1009
|
+
if (subtitleIndex == -1) {
|
|
1010
|
+
disableSubtitles();
|
|
1011
|
+
} else {
|
|
1012
|
+
SubtitleTrack st = currentSource.subtitles.get(subtitleIndex);
|
|
1013
|
+
setSubtitleLanguage(st.language);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
public void setSubtitleLanguage(String language) {
|
|
1018
|
+
try {
|
|
1019
|
+
exoPlayer.setTrackSelectionParameters(
|
|
1020
|
+
exoPlayer.getTrackSelectionParameters().buildUpon()
|
|
1021
|
+
.setPreferredTextLanguage(language)
|
|
1022
|
+
.setSelectUndeterminedTextLanguage(true)
|
|
1023
|
+
.build()
|
|
1024
|
+
);
|
|
1025
|
+
} catch (Exception ignored) {}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
public void disableSubtitles() {
|
|
1029
|
+
try {
|
|
1030
|
+
exoPlayer.setTrackSelectionParameters(
|
|
1031
|
+
exoPlayer.getTrackSelectionParameters().buildUpon()
|
|
1032
|
+
.setPreferredTextLanguage(null)
|
|
1033
|
+
.build()
|
|
1034
|
+
);
|
|
1035
|
+
} catch (Exception ignored) {}
|
|
1036
|
+
}
|
|
1037
|
+
}
|