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.
Files changed (129) hide show
  1. package/.github/workflows/ci.yml +253 -0
  2. package/ANDROID_TV_IMPLEMENTATION.md +313 -0
  3. package/COMPLETION_STATUS.md +165 -0
  4. package/CONTRIBUTING.md +376 -0
  5. package/FINAL_STATUS_REPORT.md +170 -0
  6. package/FRAMEWORK_REVIEW.md +247 -0
  7. package/IMPROVEMENTS_SUMMARY.md +168 -0
  8. package/LICENSE +21 -0
  9. package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
  10. package/PAYWALL_RENTAL_FLOW.md +499 -0
  11. package/PLATFORM_SETUP_GUIDE.md +1636 -0
  12. package/README.md +315 -0
  13. package/RUN_LOCALLY.md +151 -0
  14. package/apps/demo/cast-sender-min.html +173 -0
  15. package/apps/demo/custom-player.html +883 -0
  16. package/apps/demo/demo.html +990 -0
  17. package/apps/demo/enhanced-player.html +3556 -0
  18. package/apps/demo/index.html +159 -0
  19. package/apps/rental-api/.env.example +24 -0
  20. package/apps/rental-api/README.md +23 -0
  21. package/apps/rental-api/migrations/001_init.sql +35 -0
  22. package/apps/rental-api/migrations/002_videos.sql +10 -0
  23. package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
  24. package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
  25. package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
  26. package/apps/rental-api/package-lock.json +2045 -0
  27. package/apps/rental-api/package.json +33 -0
  28. package/apps/rental-api/scripts/run-migration.js +42 -0
  29. package/apps/rental-api/scripts/update-video-currency.js +21 -0
  30. package/apps/rental-api/scripts/update-video-price.js +19 -0
  31. package/apps/rental-api/src/config.ts +14 -0
  32. package/apps/rental-api/src/db.ts +10 -0
  33. package/apps/rental-api/src/routes/cashfree.ts +167 -0
  34. package/apps/rental-api/src/routes/pesapal.ts +92 -0
  35. package/apps/rental-api/src/routes/rentals.ts +242 -0
  36. package/apps/rental-api/src/routes/webhooks.ts +73 -0
  37. package/apps/rental-api/src/server.ts +41 -0
  38. package/apps/rental-api/src/services/entitlements.ts +45 -0
  39. package/apps/rental-api/src/services/payments.ts +22 -0
  40. package/apps/rental-api/tsconfig.json +17 -0
  41. package/check-urls.ps1 +74 -0
  42. package/comparison-report.md +181 -0
  43. package/docs/PAYWALL.md +95 -0
  44. package/docs/PLAYER_UI_VISIBILITY.md +431 -0
  45. package/docs/README.md +7 -0
  46. package/docs/SYSTEM_ARCHITECTURE.md +612 -0
  47. package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
  48. package/examples/android/JavaSampleApp/MainActivity.java +641 -0
  49. package/examples/android/JavaSampleApp/activity_main.xml +226 -0
  50. package/examples/android/SampleApp/MainActivity.kt +430 -0
  51. package/examples/ios/SampleApp/ViewController.swift +337 -0
  52. package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
  53. package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
  54. package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
  55. package/jest.config.js +33 -0
  56. package/jitpack.yml +5 -0
  57. package/lerna.json +35 -0
  58. package/package.json +69 -0
  59. package/packages/PLATFORM_STATUS.md +163 -0
  60. package/packages/android/build.gradle +135 -0
  61. package/packages/android/src/main/AndroidManifest.xml +36 -0
  62. package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
  63. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
  64. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
  65. package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
  66. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
  67. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
  68. package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
  69. package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
  70. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
  71. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
  72. package/packages/core/package.json +34 -0
  73. package/packages/core/src/BasePlayer.ts +250 -0
  74. package/packages/core/src/VideoPlayer.ts +237 -0
  75. package/packages/core/src/VideoPlayerFactory.ts +145 -0
  76. package/packages/core/src/index.ts +20 -0
  77. package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
  78. package/packages/core/src/interfaces.ts +240 -0
  79. package/packages/core/src/utils/EventEmitter.ts +66 -0
  80. package/packages/core/src/utils/PlatformDetector.ts +300 -0
  81. package/packages/core/tsconfig.json +20 -0
  82. package/packages/enact/package.json +51 -0
  83. package/packages/enact/src/VideoPlayer.js +365 -0
  84. package/packages/enact/src/adapters/TizenAdapter.js +354 -0
  85. package/packages/enact/src/index.js +82 -0
  86. package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
  87. package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
  88. package/packages/ios/GETTING_STARTED.md +100 -0
  89. package/packages/ios/Package.swift +35 -0
  90. package/packages/ios/README.md +84 -0
  91. package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
  92. package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
  93. package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
  94. package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
  95. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
  96. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
  97. package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
  98. package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
  99. package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
  100. package/packages/ios/build_framework.sh +55 -0
  101. package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
  102. package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
  103. package/packages/react-native/package.json +51 -0
  104. package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
  105. package/packages/react-native/src/VideoPlayer.tsx +224 -0
  106. package/packages/react-native/src/index.ts +28 -0
  107. package/packages/react-native/src/utils/EventEmitter.ts +66 -0
  108. package/packages/react-native/tsconfig.json +31 -0
  109. package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
  110. package/packages/roku/package.json +44 -0
  111. package/packages/roku/source/VideoPlayer.brs +231 -0
  112. package/packages/roku/source/main.brs +28 -0
  113. package/packages/web/GETTING_STARTED.md +292 -0
  114. package/packages/web/jest.config.js +28 -0
  115. package/packages/web/jest.setup.ts +110 -0
  116. package/packages/web/package.json +50 -0
  117. package/packages/web/src/SecureVideoPlayer.ts +1164 -0
  118. package/packages/web/src/WebPlayer.ts +3110 -0
  119. package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
  120. package/packages/web/src/index.ts +14 -0
  121. package/packages/web/src/paywall/PaywallController.ts +215 -0
  122. package/packages/web/src/react/WebPlayerView.tsx +177 -0
  123. package/packages/web/tsconfig.json +23 -0
  124. package/packages/web/webpack.config.js +45 -0
  125. package/server.js +131 -0
  126. package/server.py +84 -0
  127. package/test-urls.ps1 +97 -0
  128. package/test-video-urls.ps1 +87 -0
  129. package/tsconfig.json +39 -0
@@ -0,0 +1,707 @@
1
+ /**
2
+ * UnifiedVideoPlayer.kt
3
+ * Unified Video Framework - Android Native SDK
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
+ import com.google.android.exoplayer2.*
18
+ import com.google.android.exoplayer2.analytics.AnalyticsListener
19
+ import com.google.android.exoplayer2.drm.*
20
+ import com.google.android.exoplayer2.source.MediaSource
21
+ import com.google.android.exoplayer2.source.ProgressiveMediaSource
22
+ import com.google.android.exoplayer2.source.dash.DashMediaSource
23
+ import com.google.android.exoplayer2.source.hls.HlsMediaSource
24
+ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource
25
+ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
26
+ import com.google.android.exoplayer2.ui.PlayerView
27
+ import com.google.android.exoplayer2.ui.StyledPlayerView
28
+ import com.google.android.exoplayer2.upstream.DefaultDataSource
29
+ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
30
+ import com.google.android.exoplayer2.util.MimeTypes
31
+ import com.google.android.exoplayer2.util.Util
32
+ import com.google.android.exoplayer2.video.VideoSize
33
+ import java.util.UUID
34
+
35
+ // Player Configuration
36
+ data class PlayerConfiguration(
37
+ val autoPlay: Boolean = false,
38
+ val controls: Boolean = true,
39
+ val muted: Boolean = false,
40
+ val loop: Boolean = false,
41
+ val preload: String = "auto",
42
+ val startTime: Long = 0,
43
+ val playbackSpeed: Float = 1.0f,
44
+ val volume: Float = 1.0f,
45
+ val debug: Boolean = false,
46
+ val useStyledControls: Boolean = true,
47
+ val allowBackgroundPlayback: Boolean = false
48
+ ) {
49
+ class Builder {
50
+ private var autoPlay: Boolean = false
51
+ private var controls: Boolean = true
52
+ private var muted: Boolean = false
53
+ private var loop: Boolean = false
54
+ private var preload: String = "auto"
55
+ private var startTime: Long = 0
56
+ private var playbackSpeed: Float = 1.0f
57
+ private var volume: Float = 1.0f
58
+ private var debug: Boolean = false
59
+ private var useStyledControls: Boolean = true
60
+ private var allowBackgroundPlayback: Boolean = false
61
+
62
+ fun setAutoPlay(autoPlay: Boolean) = apply { this.autoPlay = autoPlay }
63
+ fun setControls(controls: Boolean) = apply { this.controls = controls }
64
+ fun setMuted(muted: Boolean) = apply { this.muted = muted }
65
+ fun setLoop(loop: Boolean) = apply { this.loop = loop }
66
+ fun setPreload(preload: String) = apply { this.preload = preload }
67
+ fun setStartTime(startTime: Long) = apply { this.startTime = startTime }
68
+ fun setPlaybackSpeed(speed: Float) = apply { this.playbackSpeed = speed }
69
+ fun setVolume(volume: Float) = apply { this.volume = volume }
70
+ fun setDebug(debug: Boolean) = apply { this.debug = debug }
71
+ fun setUseStyledControls(styled: Boolean) = apply { this.useStyledControls = styled }
72
+ fun setAllowBackgroundPlayback(allow: Boolean) = apply { this.allowBackgroundPlayback = allow }
73
+
74
+ fun build() = PlayerConfiguration(
75
+ autoPlay, controls, muted, loop, preload, startTime,
76
+ playbackSpeed, volume, debug, useStyledControls, allowBackgroundPlayback
77
+ )
78
+ }
79
+ }
80
+
81
+ // Media Source
82
+ data class MediaSource(
83
+ val url: String,
84
+ val type: String? = null,
85
+ val drm: DRMConfiguration? = null,
86
+ val metadata: Map<String, Any>? = null,
87
+ val subtitles: List<SubtitleTrack>? = null
88
+ ) {
89
+ companion object {
90
+ fun detectType(url: String): String {
91
+ return when {
92
+ url.contains(".m3u8") -> "hls"
93
+ url.contains(".mpd") -> "dash"
94
+ url.contains(".ism") -> "smoothstreaming"
95
+ url.contains(".mp4") -> "mp4"
96
+ url.contains(".webm") -> "webm"
97
+ url.contains(".mkv") -> "mkv"
98
+ else -> "mp4"
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ // DRM Configuration
105
+ data class DRMConfiguration(
106
+ val type: String, // widevine, playready, clearkey
107
+ val licenseUrl: String,
108
+ val headers: Map<String, String>? = null,
109
+ val multiSession: Boolean = false,
110
+ val forceDefaultLicenseUri: Boolean = false
111
+ )
112
+
113
+ // Subtitle Track
114
+ data class SubtitleTrack(
115
+ val url: String,
116
+ val language: String,
117
+ val label: String,
118
+ val kind: String = "subtitles", // subtitles, captions
119
+ val mimeType: String = MimeTypes.TEXT_VTT
120
+ )
121
+
122
+ // Player State
123
+ enum class PlayerState {
124
+ IDLE,
125
+ LOADING,
126
+ READY,
127
+ PLAYING,
128
+ PAUSED,
129
+ BUFFERING,
130
+ SEEKING,
131
+ ENDED,
132
+ ERROR
133
+ }
134
+
135
+ // Main Player Class
136
+ class UnifiedVideoPlayer(private val context: Context) {
137
+
138
+ companion object {
139
+ private const val TAG = "UnifiedVideoPlayer"
140
+ }
141
+
142
+ // Properties
143
+ private var exoPlayer: ExoPlayer? = null
144
+ private var playerView: View? = null
145
+ private var container: ViewGroup? = null
146
+ private var configuration: PlayerConfiguration? = null
147
+ private var currentSource: MediaSource? = null
148
+ private var trackSelector: DefaultTrackSelector? = null
149
+
150
+ private val mainHandler = Handler(Looper.getMainLooper())
151
+ private var updateProgressHandler: Runnable? = null
152
+
153
+ // State properties
154
+ var state: PlayerState = PlayerState.IDLE
155
+ private set
156
+
157
+ var isPlaying: Boolean = false
158
+ private set
159
+
160
+ var duration: Long = 0
161
+ private set
162
+
163
+ var currentPosition: Long = 0
164
+ private set
165
+
166
+ var bufferedPosition: Long = 0
167
+ private set
168
+
169
+ var volume: Float = 1.0f
170
+ private set
171
+
172
+ // Event callbacks
173
+ var onReady: (() -> Unit)? = null
174
+ var onPlay: (() -> Unit)? = null
175
+ var onPause: (() -> Unit)? = null
176
+ var onTimeUpdate: ((Long) -> Unit)? = null
177
+ var onBuffering: ((Boolean) -> Unit)? = null
178
+ var onSeek: ((Long) -> Unit)? = null
179
+ var onEnded: (() -> Unit)? = null
180
+ var onError: ((Exception) -> Unit)? = null
181
+ var onLoadedMetadata: ((Map<String, Any>) -> Unit)? = null
182
+ var onVolumeChange: ((Float) -> Unit)? = null
183
+ var onStateChange: ((PlayerState) -> Unit)? = null
184
+ var onProgress: ((Long) -> Unit)? = null
185
+ var onVideoSizeChanged: ((Int, Int) -> Unit)? = null
186
+
187
+ // Initialization
188
+ fun initialize(
189
+ container: ViewGroup,
190
+ configuration: PlayerConfiguration? = null
191
+ ) {
192
+ this.container = container
193
+ this.configuration = configuration ?: PlayerConfiguration()
194
+
195
+ setupPlayer()
196
+ applyConfiguration()
197
+ }
198
+
199
+ private fun setupPlayer() {
200
+ // Create track selector for adaptive streaming
201
+ trackSelector = DefaultTrackSelector(context).apply {
202
+ setParameters(buildUponParameters().setMaxVideoSizeSd())
203
+ }
204
+
205
+ // Create player
206
+ exoPlayer = ExoPlayer.Builder(context)
207
+ .setTrackSelector(trackSelector!!)
208
+ .build()
209
+ .apply {
210
+ addListener(playerEventListener)
211
+ addAnalyticsListener(analyticsListener)
212
+ }
213
+
214
+ // Create player view
215
+ playerView = if (configuration?.useStyledControls == true) {
216
+ StyledPlayerView(context).apply {
217
+ player = exoPlayer
218
+ useController = configuration?.controls ?: true
219
+ controllerShowTimeoutMs = 3000
220
+ controllerHideOnTouch = true
221
+ setShowBuffering(StyledPlayerView.SHOW_BUFFERING_WHEN_PLAYING)
222
+ }
223
+ } else {
224
+ PlayerView(context).apply {
225
+ player = exoPlayer
226
+ useController = configuration?.controls ?: true
227
+ controllerShowTimeoutMs = 3000
228
+ controllerHideOnTouch = true
229
+ }
230
+ }
231
+
232
+ // Add to container
233
+ playerView?.layoutParams = FrameLayout.LayoutParams(
234
+ FrameLayout.LayoutParams.MATCH_PARENT,
235
+ FrameLayout.LayoutParams.MATCH_PARENT
236
+ )
237
+ container?.addView(playerView)
238
+
239
+ // Start progress updates
240
+ startProgressUpdates()
241
+
242
+ updateState(PlayerState.IDLE)
243
+ }
244
+
245
+ private fun applyConfiguration() {
246
+ val config = configuration ?: return
247
+
248
+ exoPlayer?.apply {
249
+ setVolume(config.volume)
250
+ playbackParameters = PlaybackParameters(config.playbackSpeed)
251
+ repeatMode = if (config.loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
252
+
253
+ if (config.muted) {
254
+ setVolume(0f)
255
+ }
256
+ }
257
+
258
+ if (config.debug) {
259
+ enableDebugLogging()
260
+ }
261
+ }
262
+
263
+ // Loading Content
264
+ fun load(source: MediaSource) {
265
+ currentSource = source
266
+ updateState(PlayerState.LOADING)
267
+
268
+ val uri = Uri.parse(source.url)
269
+ val mediaSource = createMediaSource(uri, source)
270
+
271
+ exoPlayer?.apply {
272
+ setMediaSource(mediaSource)
273
+ prepare()
274
+
275
+ // Apply start time if configured
276
+ configuration?.startTime?.let { startTime ->
277
+ if (startTime > 0) {
278
+ seekTo(startTime)
279
+ }
280
+ }
281
+
282
+ // Auto-play if configured
283
+ if (configuration?.autoPlay == true) {
284
+ play()
285
+ }
286
+ }
287
+
288
+ // Load subtitles if provided
289
+ source.subtitles?.let { loadSubtitles(it) }
290
+ }
291
+
292
+ fun load(url: String) {
293
+ val source = MediaSource(
294
+ url = url,
295
+ type = MediaSource.detectType(url)
296
+ )
297
+ load(source)
298
+ }
299
+
300
+ private fun createMediaSource(uri: Uri, source: MediaSource): com.google.android.exoplayer2.source.MediaSource {
301
+ val dataSourceFactory = DefaultDataSource.Factory(context)
302
+
303
+ // Configure DRM if needed
304
+ val drmSessionManagerProvider = source.drm?.let { drm ->
305
+ createDrmSessionManagerProvider(drm)
306
+ }
307
+
308
+ val mediaItem = MediaItem.Builder()
309
+ .setUri(uri)
310
+ .apply {
311
+ source.drm?.let { drm ->
312
+ setDrmConfiguration(
313
+ MediaItem.DrmConfiguration.Builder(getDrmUuid(drm.type))
314
+ .setLicenseUri(drm.licenseUrl)
315
+ .setMultiSession(drm.multiSession)
316
+ .setForceDefaultLicenseUri(drm.forceDefaultLicenseUri)
317
+ .apply {
318
+ drm.headers?.let { headers ->
319
+ setLicenseRequestHeaders(headers)
320
+ }
321
+ }
322
+ .build()
323
+ )
324
+ }
325
+ }
326
+ .build()
327
+
328
+ // Create appropriate media source based on type
329
+ val type = source.type ?: MediaSource.detectType(source.url)
330
+
331
+ return when (type) {
332
+ "hls" -> HlsMediaSource.Factory(dataSourceFactory)
333
+ .setDrmSessionManagerProvider(drmSessionManagerProvider)
334
+ .createMediaSource(mediaItem)
335
+
336
+ "dash" -> DashMediaSource.Factory(dataSourceFactory)
337
+ .setDrmSessionManagerProvider(drmSessionManagerProvider)
338
+ .createMediaSource(mediaItem)
339
+
340
+ "smoothstreaming" -> SsMediaSource.Factory(dataSourceFactory)
341
+ .setDrmSessionManagerProvider(drmSessionManagerProvider)
342
+ .createMediaSource(mediaItem)
343
+
344
+ else -> ProgressiveMediaSource.Factory(dataSourceFactory)
345
+ .setDrmSessionManagerProvider(drmSessionManagerProvider)
346
+ .createMediaSource(mediaItem)
347
+ }
348
+ }
349
+
350
+ private fun createDrmSessionManagerProvider(drm: DRMConfiguration): DrmSessionManagerProvider {
351
+ val drmCallback = HttpMediaDrmCallback(
352
+ drm.licenseUrl,
353
+ DefaultHttpDataSource.Factory()
354
+ ).apply {
355
+ drm.headers?.forEach { (key, value) ->
356
+ setKeyRequestProperty(key, value)
357
+ }
358
+ }
359
+
360
+ return DrmSessionManagerProvider { mediaItem ->
361
+ val drmSessionManager = DefaultDrmSessionManager.Builder()
362
+ .setUuidAndExoMediaDrmProvider(
363
+ getDrmUuid(drm.type),
364
+ FrameworkMediaDrm.DEFAULT_PROVIDER
365
+ )
366
+ .build(drmCallback)
367
+
368
+ drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, emptyByteArray())
369
+ drmSessionManager
370
+ }
371
+ }
372
+
373
+ private fun getDrmUuid(drmType: String): UUID {
374
+ return when (drmType.lowercase()) {
375
+ "widevine" -> C.WIDEVINE_UUID
376
+ "playready" -> C.PLAYREADY_UUID
377
+ "clearkey" -> C.CLEARKEY_UUID
378
+ else -> C.WIDEVINE_UUID
379
+ }
380
+ }
381
+
382
+ private fun loadSubtitles(subtitles: List<SubtitleTrack>) {
383
+ // ExoPlayer handles subtitles through MediaItem configuration
384
+ // This would be implemented with side-loaded subtitle tracks
385
+ subtitles.forEach { subtitle ->
386
+ Log.d(TAG, "Loading subtitle: ${subtitle.label} (${subtitle.language})")
387
+ // Implementation would add subtitle tracks to the media source
388
+ }
389
+ }
390
+
391
+ // Playback Control
392
+ fun play() {
393
+ exoPlayer?.play()
394
+ isPlaying = true
395
+ updateState(PlayerState.PLAYING)
396
+ onPlay?.invoke()
397
+ }
398
+
399
+ fun pause() {
400
+ exoPlayer?.pause()
401
+ isPlaying = false
402
+ updateState(PlayerState.PAUSED)
403
+ onPause?.invoke()
404
+ }
405
+
406
+ fun stop() {
407
+ exoPlayer?.apply {
408
+ stop()
409
+ seekTo(0)
410
+ }
411
+ isPlaying = false
412
+ updateState(PlayerState.IDLE)
413
+ }
414
+
415
+ fun togglePlayPause() {
416
+ if (isPlaying) {
417
+ pause()
418
+ } else {
419
+ play()
420
+ }
421
+ }
422
+
423
+ fun seekTo(position: Long) {
424
+ updateState(PlayerState.SEEKING)
425
+ exoPlayer?.seekTo(position)
426
+ onSeek?.invoke(position)
427
+ }
428
+
429
+ fun seekForward(seconds: Int = 10) {
430
+ val newPosition = currentPosition + (seconds * 1000)
431
+ seekTo(minOf(newPosition, duration))
432
+ }
433
+
434
+ fun seekBackward(seconds: Int = 10) {
435
+ val newPosition = currentPosition - (seconds * 1000)
436
+ seekTo(maxOf(newPosition, 0))
437
+ }
438
+
439
+ // Volume Control
440
+ fun setVolume(volume: Float) {
441
+ val clampedVolume = volume.coerceIn(0f, 1f)
442
+ this.volume = clampedVolume
443
+ exoPlayer?.volume = clampedVolume
444
+ onVolumeChange?.invoke(clampedVolume)
445
+ }
446
+
447
+ fun mute() {
448
+ exoPlayer?.volume = 0f
449
+ }
450
+
451
+ fun unmute() {
452
+ exoPlayer?.volume = volume
453
+ }
454
+
455
+ fun toggleMute() {
456
+ exoPlayer?.let {
457
+ if (it.volume == 0f) {
458
+ unmute()
459
+ } else {
460
+ mute()
461
+ }
462
+ }
463
+ }
464
+
465
+ // Playback Speed
466
+ fun setPlaybackSpeed(speed: Float) {
467
+ exoPlayer?.setPlaybackSpeed(speed)
468
+ }
469
+
470
+ fun getPlaybackSpeed(): Float {
471
+ return exoPlayer?.playbackParameters?.speed ?: 1.0f
472
+ }
473
+
474
+ // Quality Selection
475
+ fun setVideoQuality(quality: String) {
476
+ when (quality) {
477
+ "auto" -> trackSelector?.setParameters(
478
+ trackSelector!!.buildUponParameters().clearVideoSizeConstraints()
479
+ )
480
+ "hd" -> trackSelector?.setParameters(
481
+ trackSelector!!.buildUponParameters()
482
+ .setMaxVideoSize(1920, 1080)
483
+ .setMinVideoSize(1280, 720)
484
+ )
485
+ "sd" -> trackSelector?.setParameters(
486
+ trackSelector!!.buildUponParameters().setMaxVideoSizeSd()
487
+ )
488
+ "low" -> trackSelector?.setParameters(
489
+ trackSelector!!.buildUponParameters()
490
+ .setMaxVideoSize(854, 480)
491
+ )
492
+ }
493
+ }
494
+
495
+ // State Management
496
+ private fun updateState(newState: PlayerState) {
497
+ state = newState
498
+ onStateChange?.invoke(newState)
499
+
500
+ if (configuration?.debug == true) {
501
+ Log.d(TAG, "State changed to: $newState")
502
+ }
503
+ }
504
+
505
+ // Progress Updates
506
+ private fun startProgressUpdates() {
507
+ updateProgressHandler = object : Runnable {
508
+ override fun run() {
509
+ exoPlayer?.let {
510
+ currentPosition = it.currentPosition
511
+ bufferedPosition = it.bufferedPosition
512
+ duration = it.duration
513
+
514
+ onTimeUpdate?.invoke(currentPosition)
515
+ onProgress?.invoke(bufferedPosition)
516
+ }
517
+ mainHandler.postDelayed(this, 100)
518
+ }
519
+ }
520
+ updateProgressHandler?.let { mainHandler.post(it) }
521
+ }
522
+
523
+ private fun stopProgressUpdates() {
524
+ updateProgressHandler?.let { mainHandler.removeCallbacks(it) }
525
+ }
526
+
527
+ // Player Event Listener
528
+ private val playerEventListener = object : Player.Listener {
529
+
530
+ override fun onPlaybackStateChanged(playbackState: Int) {
531
+ when (playbackState) {
532
+ Player.STATE_IDLE -> updateState(PlayerState.IDLE)
533
+ Player.STATE_BUFFERING -> {
534
+ updateState(PlayerState.BUFFERING)
535
+ onBuffering?.invoke(true)
536
+ }
537
+ Player.STATE_READY -> {
538
+ if (state == PlayerState.LOADING || state == PlayerState.BUFFERING) {
539
+ updateState(PlayerState.READY)
540
+ onReady?.invoke()
541
+ emitLoadedMetadata()
542
+ }
543
+ if (state == PlayerState.BUFFERING) {
544
+ onBuffering?.invoke(false)
545
+ }
546
+ if (exoPlayer?.isPlaying == true) {
547
+ updateState(PlayerState.PLAYING)
548
+ }
549
+ }
550
+ Player.STATE_ENDED -> {
551
+ updateState(PlayerState.ENDED)
552
+ onEnded?.invoke()
553
+
554
+ if (configuration?.loop == true) {
555
+ seekTo(0)
556
+ play()
557
+ }
558
+ }
559
+ }
560
+ }
561
+
562
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
563
+ this@UnifiedVideoPlayer.isPlaying = isPlaying
564
+ if (isPlaying) {
565
+ updateState(PlayerState.PLAYING)
566
+ } else if (state == PlayerState.PLAYING) {
567
+ updateState(PlayerState.PAUSED)
568
+ }
569
+ }
570
+
571
+ override fun onPlayerError(error: PlaybackException) {
572
+ updateState(PlayerState.ERROR)
573
+ onError?.invoke(error)
574
+
575
+ if (configuration?.debug == true) {
576
+ Log.e(TAG, "Player error: ${error.message}", error)
577
+ }
578
+ }
579
+
580
+ override fun onVideoSizeChanged(videoSize: VideoSize) {
581
+ onVideoSizeChanged?.invoke(videoSize.width, videoSize.height)
582
+ }
583
+
584
+ override fun onRenderedFirstFrame() {
585
+ Log.d(TAG, "First frame rendered")
586
+ }
587
+ }
588
+
589
+ // Analytics Listener
590
+ private val analyticsListener = object : AnalyticsListener {
591
+ override fun onLoadCompleted(
592
+ eventTime: AnalyticsListener.EventTime,
593
+ loadEventInfo: LoadEventInfo,
594
+ mediaLoadData: MediaLoadData
595
+ ) {
596
+ Log.d(TAG, "Load completed: ${loadEventInfo.dataSpec.uri}")
597
+ }
598
+
599
+ override fun onBandwidthEstimate(
600
+ eventTime: AnalyticsListener.EventTime,
601
+ totalLoadTimeMs: Int,
602
+ totalBytesLoaded: Long,
603
+ bitrateEstimate: Long
604
+ ) {
605
+ if (configuration?.debug == true) {
606
+ Log.d(TAG, "Bandwidth estimate: $bitrateEstimate bps")
607
+ }
608
+ }
609
+ }
610
+
611
+ private fun emitLoadedMetadata() {
612
+ val metadata = mutableMapOf<String, Any>()
613
+
614
+ exoPlayer?.let { player ->
615
+ metadata["duration"] = player.duration
616
+
617
+ player.videoFormat?.let { format ->
618
+ metadata["width"] = format.width
619
+ metadata["height"] = format.height
620
+ metadata["frameRate"] = format.frameRate
621
+ metadata["bitrate"] = format.bitrate
622
+ metadata["codec"] = format.codecs ?: ""
623
+ }
624
+
625
+ player.audioFormat?.let { format ->
626
+ metadata["audioChannels"] = format.channelCount
627
+ metadata["audioSampleRate"] = format.sampleRate
628
+ metadata["audioCodec"] = format.codecs ?: ""
629
+ }
630
+ }
631
+
632
+ onLoadedMetadata?.invoke(metadata)
633
+ }
634
+
635
+ // Surface Control (for advanced use cases)
636
+ fun setVideoSurface(surface: Surface?) {
637
+ exoPlayer?.setVideoSurface(surface)
638
+ }
639
+
640
+ fun clearVideoSurface() {
641
+ exoPlayer?.clearVideoSurface()
642
+ }
643
+
644
+ // Picture-in-Picture Support
645
+ fun enterPictureInPicture(): Boolean {
646
+ // Implementation depends on Activity context
647
+ // This would trigger PiP mode if supported
648
+ return false
649
+ }
650
+
651
+ // Debug
652
+ private fun enableDebugLogging() {
653
+ Log.d(TAG, "Debug mode enabled")
654
+ Log.d(TAG, "Configuration: $configuration")
655
+ }
656
+
657
+ // Lifecycle Management
658
+ fun onResume() {
659
+ exoPlayer?.let {
660
+ if (state == PlayerState.PLAYING) {
661
+ it.play()
662
+ }
663
+ }
664
+ }
665
+
666
+ fun onPause() {
667
+ if (configuration?.allowBackgroundPlayback == false) {
668
+ exoPlayer?.pause()
669
+ }
670
+ }
671
+
672
+ fun onStop() {
673
+ if (configuration?.allowBackgroundPlayback == false) {
674
+ exoPlayer?.stop()
675
+ }
676
+ }
677
+
678
+ // Cleanup
679
+ fun release() {
680
+ stopProgressUpdates()
681
+
682
+ exoPlayer?.apply {
683
+ removeListener(playerEventListener)
684
+ removeAnalyticsListener(analyticsListener)
685
+ release()
686
+ }
687
+
688
+ playerView?.let {
689
+ container?.removeView(it)
690
+ }
691
+
692
+ exoPlayer = null
693
+ playerView = null
694
+ container = null
695
+
696
+ updateState(PlayerState.IDLE)
697
+ }
698
+ }
699
+
700
+ // Extension functions for easier use
701
+ fun ExoPlayer.isBuffering(): Boolean {
702
+ return playbackState == Player.STATE_BUFFERING
703
+ }
704
+
705
+ fun ExoPlayer.hasEnded(): Boolean {
706
+ return playbackState == Player.STATE_ENDED
707
+ }
@@ -0,0 +1,9 @@
1
+ package com.unifiedvideo.player.analytics;
2
+
3
+ import java.util.Map;
4
+
5
+ public interface AnalyticsProvider {
6
+ String getName();
7
+ void track(String event, Map<String, Object> data);
8
+ }
9
+