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,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
|
+
}
|