@granite-js/video 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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/GraniteVideo.podspec +72 -0
  3. package/android/README.md +232 -0
  4. package/android/build.gradle +117 -0
  5. package/android/gradle.properties +8 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/run/granite/video/GraniteVideoModule.kt +70 -0
  8. package/android/src/main/java/run/granite/video/GraniteVideoPackage.kt +43 -0
  9. package/android/src/main/java/run/granite/video/GraniteVideoView.kt +384 -0
  10. package/android/src/main/java/run/granite/video/GraniteVideoViewManager.kt +318 -0
  11. package/android/src/main/java/run/granite/video/event/GraniteVideoEvents.kt +273 -0
  12. package/android/src/main/java/run/granite/video/event/VideoEventDispatcher.kt +66 -0
  13. package/android/src/main/java/run/granite/video/event/VideoEventListenerAdapter.kt +157 -0
  14. package/android/src/main/java/run/granite/video/provider/GraniteVideoProvider.kt +346 -0
  15. package/android/src/media3/AndroidManifest.xml +9 -0
  16. package/android/src/media3/java/run/granite/video/provider/media3/ExoPlayerProvider.kt +386 -0
  17. package/android/src/media3/java/run/granite/video/provider/media3/Media3ContentProvider.kt +29 -0
  18. package/android/src/media3/java/run/granite/video/provider/media3/Media3Initializer.kt +25 -0
  19. package/android/src/media3/java/run/granite/video/provider/media3/factory/ExoPlayerFactory.kt +32 -0
  20. package/android/src/media3/java/run/granite/video/provider/media3/factory/MediaSourceFactory.kt +61 -0
  21. package/android/src/media3/java/run/granite/video/provider/media3/factory/TrackSelectorFactory.kt +26 -0
  22. package/android/src/media3/java/run/granite/video/provider/media3/factory/VideoSurfaceFactory.kt +62 -0
  23. package/android/src/media3/java/run/granite/video/provider/media3/listener/ExoPlayerEventListener.kt +104 -0
  24. package/android/src/media3/java/run/granite/video/provider/media3/scheduler/ProgressScheduler.kt +56 -0
  25. package/android/src/test/java/run/granite/video/GraniteVideoViewRobolectricTest.kt +598 -0
  26. package/android/src/test/java/run/granite/video/event/VideoEventListenerAdapterTest.kt +319 -0
  27. package/android/src/test/java/run/granite/video/helpers/FakeGraniteVideoProvider.kt +161 -0
  28. package/android/src/test/java/run/granite/video/helpers/TestProgressScheduler.kt +42 -0
  29. package/android/src/test/java/run/granite/video/provider/GraniteVideoRegistryTest.kt +232 -0
  30. package/android/src/test/java/run/granite/video/provider/ProviderContractTest.kt +174 -0
  31. package/android/src/test/java/run/granite/video/provider/media3/listener/ExoPlayerEventListenerTest.kt +243 -0
  32. package/android/src/test/resources/kotest.properties +2 -0
  33. package/dist/module/GraniteVideo.js +458 -0
  34. package/dist/module/GraniteVideo.js.map +1 -0
  35. package/dist/module/GraniteVideoNativeComponent.ts +265 -0
  36. package/dist/module/index.js +7 -0
  37. package/dist/module/index.js.map +1 -0
  38. package/dist/module/package.json +1 -0
  39. package/dist/module/types.js +4 -0
  40. package/dist/module/types.js.map +1 -0
  41. package/dist/typescript/GraniteVideo.d.ts +12 -0
  42. package/dist/typescript/GraniteVideoNativeComponent.d.ts +189 -0
  43. package/dist/typescript/index.d.ts +5 -0
  44. package/dist/typescript/types.d.ts +328 -0
  45. package/ios/GraniteVideoComponentsProvider.h +10 -0
  46. package/ios/GraniteVideoProvider.swift +280 -0
  47. package/ios/GraniteVideoView.h +15 -0
  48. package/ios/GraniteVideoView.mm +661 -0
  49. package/ios/Providers/AVPlayerProvider.swift +541 -0
  50. package/package.json +106 -0
  51. package/src/GraniteVideo.tsx +575 -0
  52. package/src/GraniteVideoNativeComponent.ts +265 -0
  53. package/src/index.ts +8 -0
  54. package/src/types.ts +464 -0
@@ -0,0 +1,386 @@
1
+ package run.granite.video.provider.media3
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.view.View
6
+ import android.view.SurfaceView
7
+ import android.view.TextureView
8
+ import android.widget.FrameLayout
9
+ import android.graphics.Color
10
+ import androidx.media3.common.Player
11
+ import androidx.media3.common.util.UnstableApi
12
+ import androidx.media3.datasource.DefaultDataSource
13
+ import androidx.media3.datasource.DefaultHttpDataSource
14
+ import androidx.media3.exoplayer.ExoPlayer
15
+ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
16
+ import androidx.media3.exoplayer.DefaultLoadControl
17
+ import run.granite.video.provider.GraniteVideoAudioOutput
18
+ import run.granite.video.provider.GraniteVideoBufferConfig
19
+ import run.granite.video.provider.GraniteVideoDelegate
20
+ import run.granite.video.provider.GraniteVideoProvider
21
+ import run.granite.video.provider.GraniteVideoResizeMode
22
+ import run.granite.video.provider.GraniteVideoSelectedTrack
23
+ import run.granite.video.provider.GraniteVideoSource
24
+ import run.granite.video.provider.GraniteVideoProgressData
25
+ import run.granite.video.provider.media3.factory.DefaultExoPlayerFactory
26
+ import run.granite.video.provider.media3.factory.DefaultMediaSourceFactory
27
+ import run.granite.video.provider.media3.factory.DefaultTrackSelectorFactory
28
+ import run.granite.video.provider.media3.factory.DefaultVideoSurfaceFactory
29
+ import run.granite.video.provider.media3.factory.ExoPlayerFactory
30
+ import run.granite.video.provider.media3.factory.MediaSourceFactory
31
+ import run.granite.video.provider.media3.factory.TrackSelectorFactory
32
+ import run.granite.video.provider.media3.factory.VideoSurfaceFactory
33
+ import run.granite.video.provider.media3.listener.ExoPlayerEventListener
34
+ import run.granite.video.provider.media3.listener.PlaybackStateProvider
35
+ import run.granite.video.provider.media3.scheduler.HandlerProgressScheduler
36
+ import run.granite.video.provider.media3.scheduler.ProgressScheduler
37
+
38
+ /**
39
+ * Built-in ExoPlayer Provider (Default, using AndroidX Media3)
40
+ *
41
+ * This provider uses dependency injection for all external dependencies,
42
+ * making it fully testable. Default implementations are provided for
43
+ * production use.
44
+ */
45
+ @UnstableApi
46
+ class ExoPlayerProvider(
47
+ private val exoPlayerFactory: ExoPlayerFactory = DefaultExoPlayerFactory(),
48
+ private val videoSurfaceFactory: VideoSurfaceFactory = DefaultVideoSurfaceFactory(),
49
+ private val mediaSourceFactory: MediaSourceFactory = DefaultMediaSourceFactory(),
50
+ private val progressScheduler: ProgressScheduler = HandlerProgressScheduler(),
51
+ private val trackSelectorFactory: TrackSelectorFactory = DefaultTrackSelectorFactory()
52
+ ) : GraniteVideoProvider, PlaybackStateProvider {
53
+
54
+ // Provider Identification
55
+ override val providerId: String = "media3"
56
+ override val providerName: String = "Media3 ExoPlayer"
57
+
58
+ // Properties
59
+ override var delegate: GraniteVideoDelegate? = null
60
+
61
+ private var player: ExoPlayer? = null
62
+ private var playerView: FrameLayout? = null
63
+ private var surfaceView: SurfaceView? = null
64
+ private var textureView: TextureView? = null
65
+ private var context: Context? = null
66
+ private var trackSelector: DefaultTrackSelector? = null
67
+ private var eventListener: ExoPlayerEventListener? = null
68
+
69
+ private var _isPlaying: Boolean = false
70
+ private var _isSeeking: Boolean = false
71
+ private var _shouldRepeat: Boolean = false
72
+ private var _resizeMode: GraniteVideoResizeMode = GraniteVideoResizeMode.CONTAIN
73
+ private var _useTextureView: Boolean = false
74
+ private var _useSecureView: Boolean = false
75
+ private var _playInBackground: Boolean = false
76
+ private var _volume: Float = 1.0f
77
+ private var _muted: Boolean = false
78
+ private var _rate: Float = 1.0f
79
+ private var _shutterColor: Int = Color.BLACK
80
+
81
+ // PlaybackStateProvider implementation
82
+ override val isPlaying: Boolean
83
+ get() = _isPlaying
84
+
85
+ override val isSeeking: Boolean
86
+ get() = _isSeeking
87
+
88
+ override val isLooping: Boolean
89
+ get() = _shouldRepeat
90
+
91
+ override val currentTime: Double
92
+ get() = (player?.currentPosition ?: 0L) / 1000.0
93
+
94
+ override val duration: Double
95
+ get() = (player?.duration ?: 0L) / 1000.0
96
+
97
+ // Required - View Creation
98
+ override fun createPlayerView(context: Context): View {
99
+ this.context = context
100
+
101
+ playerView = videoSurfaceFactory.createContainer(context).apply {
102
+ setBackgroundColor(_shutterColor)
103
+ }
104
+
105
+ // Initialize player
106
+ trackSelector = trackSelectorFactory.create(context)
107
+ player = exoPlayerFactory.create(context, trackSelector!!)
108
+
109
+ // Create and attach event listener
110
+ eventListener = ExoPlayerEventListener(
111
+ delegateProvider = { delegate },
112
+ stateProvider = this,
113
+ onPlayingChanged = { isPlaying -> _isPlaying = isPlaying },
114
+ onVideoSizeChanged = { _, _ -> /* Handled by listener */ }
115
+ )
116
+ player?.addListener(eventListener!!)
117
+
118
+ // Create surface/texture view
119
+ setupVideoSurface(context)
120
+
121
+ return playerView!!
122
+ }
123
+
124
+ private fun setupVideoSurface(context: Context) {
125
+ // Remove existing views
126
+ surfaceView?.let { playerView?.removeView(it) }
127
+ textureView?.let { playerView?.removeView(it) }
128
+
129
+ if (_useTextureView) {
130
+ textureView = videoSurfaceFactory.createTextureView(context)
131
+ player?.setVideoTextureView(textureView)
132
+ playerView?.addView(textureView)
133
+ } else {
134
+ surfaceView = videoSurfaceFactory.createSurfaceView(context)
135
+ player?.setVideoSurfaceView(surfaceView)
136
+ playerView?.addView(surfaceView)
137
+ }
138
+ }
139
+
140
+ // Required - Source Loading
141
+ override fun loadSource(source: GraniteVideoSource) {
142
+ val uri = source.uri ?: return
143
+ val ctx = context ?: return
144
+
145
+ delegate?.onLoadStart(
146
+ isNetwork = uri.startsWith("http"),
147
+ type = source.type ?: detectMediaType(uri),
148
+ uri = uri
149
+ )
150
+
151
+ // Create data source factory with headers
152
+ val httpDataSourceFactory = DefaultHttpDataSource.Factory().apply {
153
+ source.headers?.let { headers ->
154
+ setDefaultRequestProperties(headers)
155
+ }
156
+ }
157
+
158
+ val dataSourceFactory = DefaultDataSource.Factory(ctx, httpDataSourceFactory)
159
+
160
+ // Create media source using factory
161
+ val mediaSource = mediaSourceFactory.create(source, dataSourceFactory)
162
+
163
+ player?.setMediaSource(mediaSource)
164
+ player?.prepare()
165
+
166
+ // Seek to start position if specified
167
+ if (source.startPosition > 0) {
168
+ player?.seekTo(source.startPosition.toLong())
169
+ }
170
+ }
171
+
172
+ override fun unload() {
173
+ progressScheduler.cancel()
174
+ player?.stop()
175
+ player?.clearMediaItems()
176
+ }
177
+
178
+ // Required - Playback Control
179
+ override fun play() {
180
+ player?.play()
181
+ _isPlaying = true
182
+ startProgressUpdates()
183
+ delegate?.onPlaybackStateChanged(isPlaying = true, isSeeking = false, isLooping = _shouldRepeat)
184
+ }
185
+
186
+ override fun pause() {
187
+ player?.pause()
188
+ _isPlaying = false
189
+ progressScheduler.cancel()
190
+ delegate?.onPlaybackStateChanged(isPlaying = false, isSeeking = false, isLooping = _shouldRepeat)
191
+ }
192
+
193
+ override fun seek(time: Double, tolerance: Double) {
194
+ _isSeeking = true
195
+ delegate?.onPlaybackStateChanged(isPlaying = _isPlaying, isSeeking = true, isLooping = _shouldRepeat)
196
+
197
+ val positionMs = (time * 1000).toLong()
198
+ player?.seekTo(positionMs)
199
+
200
+ delegate?.onSeek(currentTime = currentTime, seekTime = time)
201
+ _isSeeking = false
202
+ delegate?.onPlaybackStateChanged(isPlaying = _isPlaying, isSeeking = false, isLooping = _shouldRepeat)
203
+ }
204
+
205
+ // Optional - Volume
206
+ override fun setVolume(volume: Float) {
207
+ _volume = volume
208
+ player?.volume = if (_muted) 0f else volume
209
+ delegate?.onVolumeChange(volume)
210
+ }
211
+
212
+ override fun setMuted(muted: Boolean) {
213
+ _muted = muted
214
+ player?.volume = if (muted) 0f else _volume
215
+ }
216
+
217
+ // Optional - Rate
218
+ override fun setRate(rate: Float) {
219
+ _rate = rate
220
+ player?.setPlaybackSpeed(rate)
221
+ delegate?.onPlaybackRateChange(rate)
222
+ }
223
+
224
+ // Optional - Repeat
225
+ override fun setRepeat(shouldRepeat: Boolean) {
226
+ _shouldRepeat = shouldRepeat
227
+ player?.repeatMode = if (shouldRepeat) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
228
+ }
229
+
230
+ // Optional - Resize Mode
231
+ override fun setResizeMode(mode: GraniteVideoResizeMode) {
232
+ _resizeMode = mode
233
+ // ExoPlayer handles resize mode differently - would need AspectRatioFrameLayout
234
+ }
235
+
236
+ // Optional - Background Playback
237
+ override fun setPlayInBackground(enabled: Boolean) {
238
+ _playInBackground = enabled
239
+ }
240
+
241
+ override fun setPlayWhenInactive(enabled: Boolean) {
242
+ // Similar to playInBackground for Android
243
+ }
244
+
245
+ // Optional - Audio Output
246
+ override fun setAudioOutput(output: GraniteVideoAudioOutput) {
247
+ // Would require AudioManager configuration
248
+ }
249
+
250
+ // Optional - Fullscreen
251
+ override fun setFullscreen(fullscreen: Boolean, animated: Boolean) {
252
+ if (fullscreen) {
253
+ delegate?.onFullscreenPlayerWillPresent()
254
+ delegate?.onFullscreenPlayerDidPresent()
255
+ } else {
256
+ delegate?.onFullscreenPlayerWillDismiss()
257
+ delegate?.onFullscreenPlayerDidDismiss()
258
+ }
259
+ }
260
+
261
+ // Optional - Controls
262
+ override fun setControlsEnabled(enabled: Boolean) {
263
+ // Would need to add/remove control views
264
+ delegate?.onControlsVisibilityChanged(enabled)
265
+ }
266
+
267
+ // Optional - Buffer Config
268
+ override fun setBufferConfig(config: GraniteVideoBufferConfig) {
269
+ val ctx = context ?: return
270
+
271
+ val loadControl = DefaultLoadControl.Builder()
272
+ .setBufferDurationsMs(
273
+ config.minBufferMs,
274
+ config.maxBufferMs,
275
+ config.bufferForPlaybackMs,
276
+ config.bufferForPlaybackAfterRebufferMs
277
+ )
278
+ .setBackBuffer(config.backBufferDurationMs, true)
279
+ .build()
280
+
281
+ // Note: Would need to rebuild player with new load control
282
+ }
283
+
284
+ override fun setMaxBitRate(bitRate: Int) {
285
+ trackSelector?.setParameters(
286
+ trackSelector!!.buildUponParameters()
287
+ .setMaxVideoBitrate(bitRate)
288
+ )
289
+ }
290
+
291
+ // Optional - Track Selection
292
+ override fun setSelectedAudioTrack(track: GraniteVideoSelectedTrack) {
293
+ // Would use trackSelector to select audio track
294
+ }
295
+
296
+ override fun setSelectedTextTrack(track: GraniteVideoSelectedTrack) {
297
+ // Would use trackSelector to select text track
298
+ }
299
+
300
+ override fun setSelectedVideoTrack(type: String, value: Int) {
301
+ // Would use trackSelector to select video track
302
+ }
303
+
304
+ // Optional - View Type
305
+ override fun setUseTextureView(useTexture: Boolean) {
306
+ if (_useTextureView != useTexture) {
307
+ _useTextureView = useTexture
308
+ context?.let { setupVideoSurface(it) }
309
+ }
310
+ }
311
+
312
+ override fun setUseSecureView(useSecure: Boolean) {
313
+ _useSecureView = useSecure
314
+ if (useSecure) {
315
+ surfaceView?.setSecure(true)
316
+ }
317
+ }
318
+
319
+ // Optional - Shutter
320
+ override fun setShutterColor(color: Int) {
321
+ _shutterColor = color
322
+ playerView?.setBackgroundColor(color)
323
+ }
324
+
325
+ override fun setHideShutterView(hide: Boolean) {
326
+ playerView?.setBackgroundColor(if (hide) Color.TRANSPARENT else _shutterColor)
327
+ }
328
+
329
+ // Optional - Cache Management
330
+ override fun clearCache() {
331
+ // Would need cache implementation
332
+ }
333
+
334
+ // Optional - Codec Support
335
+ override fun isCodecSupported(mimeType: String, width: Int, height: Int): Boolean {
336
+ // Would check MediaCodecList
337
+ return true
338
+ }
339
+
340
+ override fun isHEVCSupported(): Boolean {
341
+ return isCodecSupported("video/hevc", 1920, 1080)
342
+ }
343
+
344
+ override fun getWidevineLevel(): Int {
345
+ // Would check Widevine security level
346
+ return 1
347
+ }
348
+
349
+ // Private helpers
350
+ private fun detectMediaType(uri: String): String {
351
+ return when {
352
+ uri.contains(".m3u8") -> "hls"
353
+ uri.contains(".mpd") -> "dash"
354
+ uri.contains(".ism") -> "smoothstreaming"
355
+ uri.contains(".mp4") -> "mp4"
356
+ uri.contains(".webm") -> "webm"
357
+ else -> "unknown"
358
+ }
359
+ }
360
+
361
+ private fun startProgressUpdates() {
362
+ progressScheduler.schedule(250) {
363
+ player?.let { p ->
364
+ val progressData = GraniteVideoProgressData(
365
+ currentTime = p.currentPosition / 1000.0,
366
+ playableDuration = p.bufferedPosition / 1000.0,
367
+ seekableDuration = p.duration / 1000.0
368
+ )
369
+ delegate?.onProgress(progressData)
370
+ }
371
+ }
372
+ }
373
+
374
+ // Cleanup
375
+ override fun release() {
376
+ progressScheduler.cancel()
377
+ eventListener?.let { player?.removeListener(it) }
378
+ player?.release()
379
+ player = null
380
+ eventListener = null
381
+ surfaceView = null
382
+ textureView = null
383
+ playerView = null
384
+ context = null
385
+ }
386
+ }
@@ -0,0 +1,29 @@
1
+ package run.granite.video.provider.media3
2
+
3
+ import android.content.ContentProvider
4
+ import android.content.ContentValues
5
+ import android.database.Cursor
6
+ import android.net.Uri
7
+
8
+ /**
9
+ * ContentProvider for automatic Media3 initialization.
10
+ *
11
+ * This provider is automatically instantiated by Android before Application.onCreate(),
12
+ * ensuring Media3 ExoPlayer is registered as the default video provider early in the app lifecycle.
13
+ *
14
+ * No manual registration required - just include this module in your build.
15
+ */
16
+ class Media3ContentProvider : ContentProvider() {
17
+
18
+ override fun onCreate(): Boolean {
19
+ Media3Initializer.initialize()
20
+ return true
21
+ }
22
+
23
+ // Required ContentProvider methods - not used for initialization
24
+ override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? = null
25
+ override fun getType(uri: Uri): String? = null
26
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
27
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
28
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
29
+ }
@@ -0,0 +1,25 @@
1
+ package run.granite.video.provider.media3
2
+
3
+ import run.granite.video.BuildConfig
4
+ import run.granite.video.provider.GraniteVideoRegistry
5
+
6
+ /**
7
+ * Initializes Media3 ExoPlayer as the default video provider.
8
+ *
9
+ * Called by GraniteVideoPackage when USE_MEDIA3 is enabled.
10
+ */
11
+ object Media3Initializer {
12
+ private var initialized = false
13
+
14
+ /**
15
+ * Initialize Media3 provider and register it as default.
16
+ * Safe to call multiple times - only executes once.
17
+ */
18
+ fun initialize() {
19
+ if (initialized) return
20
+ initialized = true
21
+
22
+ GraniteVideoRegistry.registerFactory("media3") { ExoPlayerProvider() }
23
+ GraniteVideoRegistry.setDefaultProvider("media3")
24
+ }
25
+ }
@@ -0,0 +1,32 @@
1
+ package run.granite.video.provider.media3.factory
2
+
3
+ import android.content.Context
4
+ import androidx.media3.common.util.UnstableApi
5
+ import androidx.media3.exoplayer.ExoPlayer
6
+ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
7
+
8
+ /**
9
+ * Factory interface for creating ExoPlayer instances.
10
+ * Abstracted to allow for testability.
11
+ */
12
+ interface ExoPlayerFactory {
13
+ /**
14
+ * Create an ExoPlayer instance.
15
+ * @param context The Android context.
16
+ * @param trackSelector The track selector to use.
17
+ * @return A new ExoPlayer instance.
18
+ */
19
+ fun create(context: Context, trackSelector: DefaultTrackSelector): ExoPlayer
20
+ }
21
+
22
+ /**
23
+ * Default implementation that creates a standard ExoPlayer.
24
+ */
25
+ @UnstableApi
26
+ class DefaultExoPlayerFactory : ExoPlayerFactory {
27
+ override fun create(context: Context, trackSelector: DefaultTrackSelector): ExoPlayer {
28
+ return ExoPlayer.Builder(context)
29
+ .setTrackSelector(trackSelector)
30
+ .build()
31
+ }
32
+ }
@@ -0,0 +1,61 @@
1
+ package run.granite.video.provider.media3.factory
2
+
3
+ import android.net.Uri
4
+ import androidx.media3.common.MediaItem
5
+ import androidx.media3.common.util.UnstableApi
6
+ import androidx.media3.datasource.DataSource
7
+ import androidx.media3.exoplayer.dash.DashMediaSource
8
+ import androidx.media3.exoplayer.hls.HlsMediaSource
9
+ import androidx.media3.exoplayer.smoothstreaming.SsMediaSource
10
+ import androidx.media3.exoplayer.source.MediaSource
11
+ import androidx.media3.exoplayer.source.ProgressiveMediaSource
12
+ import run.granite.video.provider.GraniteVideoSource
13
+
14
+ /**
15
+ * Factory interface for creating MediaSource instances.
16
+ * Abstracted to allow for testability.
17
+ */
18
+ interface MediaSourceFactory {
19
+ /**
20
+ * Create a MediaSource for the given video source.
21
+ * @param source The video source configuration.
22
+ * @param dataSourceFactory The data source factory to use.
23
+ * @return A new MediaSource.
24
+ */
25
+ fun create(source: GraniteVideoSource, dataSourceFactory: DataSource.Factory): MediaSource
26
+ }
27
+
28
+ /**
29
+ * Default implementation that creates appropriate MediaSource based on content type.
30
+ */
31
+ @UnstableApi
32
+ class DefaultMediaSourceFactory : MediaSourceFactory {
33
+
34
+ override fun create(source: GraniteVideoSource, dataSourceFactory: DataSource.Factory): MediaSource {
35
+ val uri = Uri.parse(source.uri ?: "")
36
+ val type = source.type ?: inferType(uri)
37
+
38
+ val mediaItem = MediaItem.fromUri(uri)
39
+
40
+ return when (type.lowercase()) {
41
+ "hls", "m3u8" -> HlsMediaSource.Factory(dataSourceFactory)
42
+ .createMediaSource(mediaItem)
43
+ "dash", "mpd" -> DashMediaSource.Factory(dataSourceFactory)
44
+ .createMediaSource(mediaItem)
45
+ "ss", "ism", "smoothstreaming" -> SsMediaSource.Factory(dataSourceFactory)
46
+ .createMediaSource(mediaItem)
47
+ else -> ProgressiveMediaSource.Factory(dataSourceFactory)
48
+ .createMediaSource(mediaItem)
49
+ }
50
+ }
51
+
52
+ private fun inferType(uri: Uri): String {
53
+ val path = uri.path?.lowercase() ?: ""
54
+ return when {
55
+ path.endsWith(".m3u8") -> "hls"
56
+ path.endsWith(".mpd") -> "dash"
57
+ path.contains(".ism") -> "ss"
58
+ else -> "progressive"
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,26 @@
1
+ package run.granite.video.provider.media3.factory
2
+
3
+ import android.content.Context
4
+ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
5
+
6
+ /**
7
+ * Factory interface for creating TrackSelector instances.
8
+ * Abstracted to allow for testability.
9
+ */
10
+ interface TrackSelectorFactory {
11
+ /**
12
+ * Create a DefaultTrackSelector instance.
13
+ * @param context The Android context.
14
+ * @return A new DefaultTrackSelector instance.
15
+ */
16
+ fun create(context: Context): DefaultTrackSelector
17
+ }
18
+
19
+ /**
20
+ * Default implementation that creates a standard DefaultTrackSelector.
21
+ */
22
+ class DefaultTrackSelectorFactory : TrackSelectorFactory {
23
+ override fun create(context: Context): DefaultTrackSelector {
24
+ return DefaultTrackSelector(context)
25
+ }
26
+ }
@@ -0,0 +1,62 @@
1
+ package run.granite.video.provider.media3.factory
2
+
3
+ import android.content.Context
4
+ import android.view.SurfaceView
5
+ import android.view.TextureView
6
+ import android.view.View
7
+ import android.widget.FrameLayout
8
+
9
+ /**
10
+ * Factory interface for creating video surface views.
11
+ * Abstracted to allow for testability.
12
+ */
13
+ interface VideoSurfaceFactory {
14
+ /**
15
+ * Create a SurfaceView for video rendering.
16
+ * @param context The Android context.
17
+ * @return A new SurfaceView.
18
+ */
19
+ fun createSurfaceView(context: Context): SurfaceView
20
+
21
+ /**
22
+ * Create a TextureView for video rendering.
23
+ * @param context The Android context.
24
+ * @return A new TextureView.
25
+ */
26
+ fun createTextureView(context: Context): TextureView
27
+
28
+ /**
29
+ * Create a container FrameLayout for the video player.
30
+ * @param context The Android context.
31
+ * @return A new FrameLayout.
32
+ */
33
+ fun createContainer(context: Context): FrameLayout
34
+ }
35
+
36
+ /**
37
+ * Default implementation that creates standard Android views.
38
+ */
39
+ class DefaultVideoSurfaceFactory : VideoSurfaceFactory {
40
+
41
+ override fun createSurfaceView(context: Context): SurfaceView {
42
+ return SurfaceView(context).apply {
43
+ layoutParams = FrameLayout.LayoutParams(
44
+ FrameLayout.LayoutParams.MATCH_PARENT,
45
+ FrameLayout.LayoutParams.MATCH_PARENT
46
+ )
47
+ }
48
+ }
49
+
50
+ override fun createTextureView(context: Context): TextureView {
51
+ return TextureView(context).apply {
52
+ layoutParams = FrameLayout.LayoutParams(
53
+ FrameLayout.LayoutParams.MATCH_PARENT,
54
+ FrameLayout.LayoutParams.MATCH_PARENT
55
+ )
56
+ }
57
+ }
58
+
59
+ override fun createContainer(context: Context): FrameLayout {
60
+ return FrameLayout(context)
61
+ }
62
+ }