@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.
- package/CHANGELOG.md +7 -0
- package/GraniteVideo.podspec +72 -0
- package/android/README.md +232 -0
- package/android/build.gradle +117 -0
- package/android/gradle.properties +8 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/run/granite/video/GraniteVideoModule.kt +70 -0
- package/android/src/main/java/run/granite/video/GraniteVideoPackage.kt +43 -0
- package/android/src/main/java/run/granite/video/GraniteVideoView.kt +384 -0
- package/android/src/main/java/run/granite/video/GraniteVideoViewManager.kt +318 -0
- package/android/src/main/java/run/granite/video/event/GraniteVideoEvents.kt +273 -0
- package/android/src/main/java/run/granite/video/event/VideoEventDispatcher.kt +66 -0
- package/android/src/main/java/run/granite/video/event/VideoEventListenerAdapter.kt +157 -0
- package/android/src/main/java/run/granite/video/provider/GraniteVideoProvider.kt +346 -0
- package/android/src/media3/AndroidManifest.xml +9 -0
- package/android/src/media3/java/run/granite/video/provider/media3/ExoPlayerProvider.kt +386 -0
- package/android/src/media3/java/run/granite/video/provider/media3/Media3ContentProvider.kt +29 -0
- package/android/src/media3/java/run/granite/video/provider/media3/Media3Initializer.kt +25 -0
- package/android/src/media3/java/run/granite/video/provider/media3/factory/ExoPlayerFactory.kt +32 -0
- package/android/src/media3/java/run/granite/video/provider/media3/factory/MediaSourceFactory.kt +61 -0
- package/android/src/media3/java/run/granite/video/provider/media3/factory/TrackSelectorFactory.kt +26 -0
- package/android/src/media3/java/run/granite/video/provider/media3/factory/VideoSurfaceFactory.kt +62 -0
- package/android/src/media3/java/run/granite/video/provider/media3/listener/ExoPlayerEventListener.kt +104 -0
- package/android/src/media3/java/run/granite/video/provider/media3/scheduler/ProgressScheduler.kt +56 -0
- package/android/src/test/java/run/granite/video/GraniteVideoViewRobolectricTest.kt +598 -0
- package/android/src/test/java/run/granite/video/event/VideoEventListenerAdapterTest.kt +319 -0
- package/android/src/test/java/run/granite/video/helpers/FakeGraniteVideoProvider.kt +161 -0
- package/android/src/test/java/run/granite/video/helpers/TestProgressScheduler.kt +42 -0
- package/android/src/test/java/run/granite/video/provider/GraniteVideoRegistryTest.kt +232 -0
- package/android/src/test/java/run/granite/video/provider/ProviderContractTest.kt +174 -0
- package/android/src/test/java/run/granite/video/provider/media3/listener/ExoPlayerEventListenerTest.kt +243 -0
- package/android/src/test/resources/kotest.properties +2 -0
- package/dist/module/GraniteVideo.js +458 -0
- package/dist/module/GraniteVideo.js.map +1 -0
- package/dist/module/GraniteVideoNativeComponent.ts +265 -0
- package/dist/module/index.js +7 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/types.js +4 -0
- package/dist/module/types.js.map +1 -0
- package/dist/typescript/GraniteVideo.d.ts +12 -0
- package/dist/typescript/GraniteVideoNativeComponent.d.ts +189 -0
- package/dist/typescript/index.d.ts +5 -0
- package/dist/typescript/types.d.ts +328 -0
- package/ios/GraniteVideoComponentsProvider.h +10 -0
- package/ios/GraniteVideoProvider.swift +280 -0
- package/ios/GraniteVideoView.h +15 -0
- package/ios/GraniteVideoView.mm +661 -0
- package/ios/Providers/AVPlayerProvider.swift +541 -0
- package/package.json +106 -0
- package/src/GraniteVideo.tsx +575 -0
- package/src/GraniteVideoNativeComponent.ts +265 -0
- package/src/index.ts +8 -0
- 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
|
+
}
|
package/android/src/media3/java/run/granite/video/provider/media3/factory/MediaSourceFactory.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/media3/java/run/granite/video/provider/media3/factory/TrackSelectorFactory.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/media3/java/run/granite/video/provider/media3/factory/VideoSurfaceFactory.kt
ADDED
|
@@ -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
|
+
}
|