@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,319 @@
1
+ package run.granite.video.event
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+ import io.kotest.core.spec.style.FunSpec
7
+ import io.kotest.matchers.shouldBe
8
+ import io.mockk.*
9
+ import run.granite.video.provider.GraniteVideoErrorData
10
+ import run.granite.video.provider.GraniteVideoLoadData
11
+ import run.granite.video.provider.GraniteVideoProgressData
12
+
13
+ class VideoEventListenerAdapterTest : FunSpec({
14
+
15
+ lateinit var mockDispatcher: VideoEventDispatcher
16
+ lateinit var mockWritableMap: WritableMap
17
+ lateinit var adapter: VideoEventListenerAdapter
18
+ val viewId = 123
19
+ val surfaceId = 1
20
+
21
+ beforeTest {
22
+ mockDispatcher = mockk(relaxed = true)
23
+ mockWritableMap = mockk(relaxed = true)
24
+
25
+ // Mock static Arguments.createMap() to return our mock
26
+ mockkStatic(Arguments::class)
27
+ every { Arguments.createMap() } returns mockWritableMap
28
+
29
+ // Mock getSurfaceId() to return test surfaceId
30
+ every { mockDispatcher.getSurfaceId() } returns surfaceId
31
+
32
+ adapter = VideoEventListenerAdapter(
33
+ dispatcher = mockDispatcher,
34
+ viewIdProvider = { viewId }
35
+ )
36
+ }
37
+
38
+ afterTest {
39
+ clearAllMocks()
40
+ unmockkStatic(Arguments::class)
41
+ }
42
+
43
+ // ============================================================
44
+ // onLoadStart Tests
45
+ // ============================================================
46
+
47
+ test("onLoadStart should dispatch topVideoLoadStart event") {
48
+ adapter.onLoadStart(true, "mp4", "https://example.com/video.mp4")
49
+
50
+ verify {
51
+ mockDispatcher.dispatchEvent(match<GraniteVideoLoadStartEvent> {
52
+ it.getEventName() == "topVideoLoadStart" && it.viewTag == viewId
53
+ })
54
+ }
55
+ }
56
+
57
+ // ============================================================
58
+ // onLoad Tests
59
+ // ============================================================
60
+
61
+ test("onLoad should dispatch topVideoLoad event") {
62
+ val data = GraniteVideoLoadData(
63
+ currentTime = 0.0,
64
+ duration = 60.0,
65
+ naturalWidth = 1920.0,
66
+ naturalHeight = 1080.0,
67
+ orientation = "landscape"
68
+ )
69
+
70
+ adapter.onLoad(data)
71
+
72
+ verify {
73
+ mockDispatcher.dispatchEvent(match<GraniteVideoLoadEvent> {
74
+ it.getEventName() == "topVideoLoad" && it.viewTag == viewId
75
+ })
76
+ }
77
+ }
78
+
79
+ // ============================================================
80
+ // onError Tests
81
+ // ============================================================
82
+
83
+ test("onError should dispatch topVideoError event") {
84
+ val error = GraniteVideoErrorData(
85
+ code = 1001,
86
+ domain = "ExoPlayer",
87
+ localizedDescription = "Test error",
88
+ errorString = "ERROR_TEST"
89
+ )
90
+
91
+ adapter.onError(error)
92
+
93
+ verify {
94
+ mockDispatcher.dispatchEvent(match<GraniteVideoErrorEvent> {
95
+ it.getEventName() == "topVideoError" && it.viewTag == viewId
96
+ })
97
+ }
98
+ }
99
+
100
+ // ============================================================
101
+ // onProgress Tests
102
+ // ============================================================
103
+
104
+ test("onProgress should dispatch topVideoProgress event") {
105
+ val data = GraniteVideoProgressData(
106
+ currentTime = 10.0,
107
+ playableDuration = 20.0,
108
+ seekableDuration = 60.0
109
+ )
110
+
111
+ adapter.onProgress(data)
112
+
113
+ verify {
114
+ mockDispatcher.dispatchEvent(match<GraniteVideoProgressEvent> {
115
+ it.getEventName() == "topVideoProgress" && it.viewTag == viewId
116
+ })
117
+ }
118
+ }
119
+
120
+ // ============================================================
121
+ // onSeek Tests
122
+ // ============================================================
123
+
124
+ test("onSeek should dispatch topVideoSeek event") {
125
+ adapter.onSeek(5.0, 10.0)
126
+
127
+ verify {
128
+ mockDispatcher.dispatchEvent(match<GraniteVideoSeekEvent> {
129
+ it.getEventName() == "topVideoSeek" && it.viewTag == viewId
130
+ })
131
+ }
132
+ }
133
+
134
+ // ============================================================
135
+ // onEnd Tests
136
+ // ============================================================
137
+
138
+ test("onEnd should dispatch topVideoEnd event") {
139
+ adapter.onEnd()
140
+
141
+ verify {
142
+ mockDispatcher.dispatchEvent(match<GraniteVideoEndEvent> {
143
+ it.getEventName() == "topVideoEnd" && it.viewTag == viewId
144
+ })
145
+ }
146
+ }
147
+
148
+ // ============================================================
149
+ // onBuffer Tests
150
+ // ============================================================
151
+
152
+ test("onBuffer should dispatch topVideoBuffer event") {
153
+ adapter.onBuffer(true)
154
+
155
+ verify {
156
+ mockDispatcher.dispatchEvent(match<GraniteVideoBufferEvent> {
157
+ it.getEventName() == "topVideoBuffer" && it.viewTag == viewId
158
+ })
159
+ }
160
+ }
161
+
162
+ // ============================================================
163
+ // onBandwidthUpdate Tests
164
+ // ============================================================
165
+
166
+ test("onBandwidthUpdate should dispatch topVideoBandwidthUpdate event") {
167
+ adapter.onBandwidthUpdate(5000000.0, 1920, 1080)
168
+
169
+ verify {
170
+ mockDispatcher.dispatchEvent(match<GraniteVideoBandwidthUpdateEvent> {
171
+ it.getEventName() == "topVideoBandwidthUpdate" && it.viewTag == viewId
172
+ })
173
+ }
174
+ }
175
+
176
+ // ============================================================
177
+ // onPlaybackStateChanged Tests
178
+ // ============================================================
179
+
180
+ test("onPlaybackStateChanged should dispatch topVideoPlaybackStateChanged event") {
181
+ adapter.onPlaybackStateChanged(true, false, false)
182
+
183
+ verify {
184
+ mockDispatcher.dispatchEvent(match<GraniteVideoPlaybackStateChangedEvent> {
185
+ it.getEventName() == "topVideoPlaybackStateChanged" && it.viewTag == viewId
186
+ })
187
+ }
188
+ }
189
+
190
+ // ============================================================
191
+ // onPlaybackRateChange Tests
192
+ // ============================================================
193
+
194
+ test("onPlaybackRateChange should dispatch topVideoPlaybackRateChange event") {
195
+ adapter.onPlaybackRateChange(1.5f)
196
+
197
+ verify {
198
+ mockDispatcher.dispatchEvent(match<GraniteVideoPlaybackRateChangeEvent> {
199
+ it.getEventName() == "topVideoPlaybackRateChange" && it.viewTag == viewId
200
+ })
201
+ }
202
+ }
203
+
204
+ // ============================================================
205
+ // onVolumeChange Tests
206
+ // ============================================================
207
+
208
+ test("onVolumeChange should dispatch topVideoVolumeChange event") {
209
+ adapter.onVolumeChange(0.5f)
210
+
211
+ verify {
212
+ mockDispatcher.dispatchEvent(match<GraniteVideoVolumeChangeEvent> {
213
+ it.getEventName() == "topVideoVolumeChange" && it.viewTag == viewId
214
+ })
215
+ }
216
+ }
217
+
218
+ // ============================================================
219
+ // onIdle Tests
220
+ // ============================================================
221
+
222
+ test("onIdle should dispatch topVideoIdle event") {
223
+ adapter.onIdle()
224
+
225
+ verify {
226
+ mockDispatcher.dispatchEvent(match<GraniteVideoIdleEvent> {
227
+ it.getEventName() == "topVideoIdle" && it.viewTag == viewId
228
+ })
229
+ }
230
+ }
231
+
232
+ // ============================================================
233
+ // onReadyForDisplay Tests
234
+ // ============================================================
235
+
236
+ test("onReadyForDisplay should dispatch topVideoReadyForDisplay event") {
237
+ adapter.onReadyForDisplay()
238
+
239
+ verify {
240
+ mockDispatcher.dispatchEvent(match<GraniteVideoReadyForDisplayEvent> {
241
+ it.getEventName() == "topVideoReadyForDisplay" && it.viewTag == viewId
242
+ })
243
+ }
244
+ }
245
+
246
+ // ============================================================
247
+ // Fullscreen Events Tests
248
+ // ============================================================
249
+
250
+ test("onFullscreenPlayerWillPresent should dispatch topVideoFullscreenPlayerWillPresent event") {
251
+ adapter.onFullscreenPlayerWillPresent()
252
+
253
+ verify {
254
+ mockDispatcher.dispatchEvent(match<GraniteVideoFullscreenPlayerWillPresentEvent> {
255
+ it.getEventName() == "topVideoFullscreenPlayerWillPresent" && it.viewTag == viewId
256
+ })
257
+ }
258
+ }
259
+
260
+ test("onFullscreenPlayerDidPresent should dispatch topVideoFullscreenPlayerDidPresent event") {
261
+ adapter.onFullscreenPlayerDidPresent()
262
+
263
+ verify {
264
+ mockDispatcher.dispatchEvent(match<GraniteVideoFullscreenPlayerDidPresentEvent> {
265
+ it.getEventName() == "topVideoFullscreenPlayerDidPresent" && it.viewTag == viewId
266
+ })
267
+ }
268
+ }
269
+
270
+ // ============================================================
271
+ // PiP Tests
272
+ // ============================================================
273
+
274
+ test("onPictureInPictureStatusChanged should dispatch topVideoPictureInPictureStatusChanged event") {
275
+ adapter.onPictureInPictureStatusChanged(true)
276
+
277
+ verify {
278
+ mockDispatcher.dispatchEvent(match<GraniteVideoPictureInPictureStatusChangedEvent> {
279
+ it.getEventName() == "topVideoPictureInPictureStatusChanged" && it.viewTag == viewId
280
+ })
281
+ }
282
+ }
283
+
284
+ // ============================================================
285
+ // Aspect Ratio Tests
286
+ // ============================================================
287
+
288
+ test("onAspectRatioChanged should dispatch topVideoAspectRatio event") {
289
+ adapter.onAspectRatioChanged(16.0, 9.0)
290
+
291
+ verify {
292
+ mockDispatcher.dispatchEvent(match<GraniteVideoAspectRatioEvent> {
293
+ it.getEventName() == "topVideoAspectRatio" && it.viewTag == viewId
294
+ })
295
+ }
296
+ }
297
+
298
+ // ============================================================
299
+ // Dynamic viewId Tests
300
+ // ============================================================
301
+
302
+ test("viewId changes dynamically should use updated viewId for each dispatch") {
303
+ var currentViewId = 100
304
+
305
+ val dynamicAdapter = VideoEventListenerAdapter(
306
+ dispatcher = mockDispatcher,
307
+ viewIdProvider = { currentViewId }
308
+ )
309
+
310
+ dynamicAdapter.onEnd()
311
+ currentViewId = 200
312
+ dynamicAdapter.onEnd()
313
+
314
+ verifyOrder {
315
+ mockDispatcher.dispatchEvent(match<GraniteVideoEndEvent> { it.viewTag == 100 })
316
+ mockDispatcher.dispatchEvent(match<GraniteVideoEndEvent> { it.viewTag == 200 })
317
+ }
318
+ }
319
+ })
@@ -0,0 +1,161 @@
1
+ package run.granite.video.helpers
2
+
3
+ import android.content.Context
4
+ import android.view.View
5
+ import io.mockk.mockk
6
+ import run.granite.video.provider.*
7
+
8
+ class FakeGraniteVideoProvider(
9
+ override val providerId: String = "fake",
10
+ override val providerName: String = "Fake Provider"
11
+ ) : GraniteVideoProvider {
12
+
13
+ override var delegate: GraniteVideoDelegate? = null
14
+
15
+ var playCount = 0
16
+ private set
17
+ var pauseCount = 0
18
+ private set
19
+ var seekCount = 0
20
+ private set
21
+ var lastLoadedSource: GraniteVideoSource? = null
22
+ private set
23
+ var lastSeekTime: Double = 0.0
24
+ private set
25
+
26
+ private var _isPlaying = false
27
+ private var _currentTime = 0.0
28
+ private var _duration = 0.0
29
+ private var _volume = 1.0f
30
+ private var _muted = false
31
+ private var _rate = 1.0f
32
+ private var _repeat = false
33
+
34
+ override val currentTime: Double get() = _currentTime
35
+ override val duration: Double get() = _duration
36
+ override val isPlaying: Boolean get() = _isPlaying
37
+
38
+ override fun createPlayerView(context: Context): View = mockk(relaxed = true)
39
+
40
+ override fun loadSource(source: GraniteVideoSource) {
41
+ lastLoadedSource = source
42
+ _duration = 0.0
43
+ _currentTime = source.startPosition
44
+ delegate?.onLoadStart(
45
+ isNetwork = source.uri?.startsWith("http") == true,
46
+ type = source.type ?: "unknown",
47
+ uri = source.uri ?: ""
48
+ )
49
+ }
50
+
51
+ override fun unload() {
52
+ _isPlaying = false
53
+ _currentTime = 0.0
54
+ _duration = 0.0
55
+ lastLoadedSource = null
56
+ }
57
+
58
+ override fun play() {
59
+ playCount++
60
+ _isPlaying = true
61
+ delegate?.onPlaybackStateChanged(isPlaying = true, isSeeking = false, isLooping = _repeat)
62
+ }
63
+
64
+ override fun pause() {
65
+ pauseCount++
66
+ _isPlaying = false
67
+ delegate?.onPlaybackStateChanged(isPlaying = false, isSeeking = false, isLooping = _repeat)
68
+ }
69
+
70
+ override fun seek(time: Double, tolerance: Double) {
71
+ seekCount++
72
+ lastSeekTime = time
73
+ val previousTime = _currentTime
74
+ _currentTime = time
75
+ delegate?.onSeek(currentTime = previousTime, seekTime = time)
76
+ }
77
+
78
+ override fun setVolume(volume: Float) {
79
+ _volume = volume
80
+ delegate?.onVolumeChange(volume)
81
+ }
82
+
83
+ override fun setMuted(muted: Boolean) {
84
+ _muted = muted
85
+ delegate?.onVolumeChange(if (muted) 0f else _volume)
86
+ }
87
+
88
+ override fun setRate(rate: Float) {
89
+ _rate = rate
90
+ delegate?.onPlaybackRateChange(rate)
91
+ }
92
+
93
+ override fun setRepeat(shouldRepeat: Boolean) {
94
+ _repeat = shouldRepeat
95
+ }
96
+
97
+ // Test helpers
98
+ fun simulateLoad(duration: Double, width: Double = 1920.0, height: Double = 1080.0) {
99
+ _duration = duration
100
+ delegate?.onLoad(
101
+ GraniteVideoLoadData(
102
+ currentTime = _currentTime,
103
+ duration = duration,
104
+ naturalWidth = width,
105
+ naturalHeight = height,
106
+ orientation = if (width > height) "landscape" else "portrait"
107
+ )
108
+ )
109
+ }
110
+
111
+ fun simulateProgress(currentTime: Double) {
112
+ _currentTime = currentTime
113
+ delegate?.onProgress(
114
+ GraniteVideoProgressData(
115
+ currentTime = currentTime,
116
+ playableDuration = _duration,
117
+ seekableDuration = _duration
118
+ )
119
+ )
120
+ }
121
+
122
+ fun simulateEnd() {
123
+ _isPlaying = false
124
+ delegate?.onEnd()
125
+ }
126
+
127
+ fun simulateError(code: Int, message: String) {
128
+ delegate?.onError(
129
+ GraniteVideoErrorData(
130
+ code = code,
131
+ domain = "Fake",
132
+ localizedDescription = message,
133
+ errorString = message
134
+ )
135
+ )
136
+ }
137
+
138
+ fun simulateBuffer(isBuffering: Boolean) {
139
+ delegate?.onBuffer(isBuffering)
140
+ }
141
+
142
+ override fun release() {
143
+ reset()
144
+ }
145
+
146
+ fun reset() {
147
+ playCount = 0
148
+ pauseCount = 0
149
+ seekCount = 0
150
+ lastLoadedSource = null
151
+ lastSeekTime = 0.0
152
+ _isPlaying = false
153
+ _currentTime = 0.0
154
+ _duration = 0.0
155
+ _volume = 1.0f
156
+ _muted = false
157
+ _rate = 1.0f
158
+ _repeat = false
159
+ delegate = null
160
+ }
161
+ }
@@ -0,0 +1,42 @@
1
+ package run.granite.video.helpers
2
+
3
+ import run.granite.video.provider.media3.scheduler.ProgressScheduler
4
+
5
+ class TestProgressScheduler : ProgressScheduler {
6
+ private var action: (() -> Unit)? = null
7
+ private var _intervalMs: Long = 0
8
+
9
+ var scheduledCount = 0
10
+ private set
11
+ var cancelCount = 0
12
+ private set
13
+
14
+ val intervalMs: Long get() = _intervalMs
15
+ val isScheduled: Boolean get() = action != null
16
+
17
+ override fun schedule(intervalMs: Long, action: () -> Unit) {
18
+ this.action = action
19
+ this._intervalMs = intervalMs
20
+ scheduledCount++
21
+ }
22
+
23
+ override fun cancel() {
24
+ action = null
25
+ cancelCount++
26
+ }
27
+
28
+ fun tick() {
29
+ action?.invoke()
30
+ }
31
+
32
+ fun tick(times: Int) {
33
+ repeat(times) { tick() }
34
+ }
35
+
36
+ fun reset() {
37
+ action = null
38
+ _intervalMs = 0
39
+ scheduledCount = 0
40
+ cancelCount = 0
41
+ }
42
+ }