@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,598 @@
1
+ package run.granite.video
2
+
3
+ import android.content.Context
4
+ import android.view.View
5
+ import io.mockk.*
6
+ import org.junit.After
7
+ import org.junit.Before
8
+ import org.junit.Test
9
+ import org.junit.runner.RunWith
10
+ import org.robolectric.RobolectricTestRunner
11
+ import org.robolectric.RuntimeEnvironment
12
+ import org.robolectric.annotation.Config
13
+ import run.granite.video.provider.*
14
+ import org.junit.Assert.*
15
+
16
+ /**
17
+ * GraniteVideoView tests using Robolectric for Android view testing.
18
+ * These tests require a real Android environment provided by Robolectric.
19
+ */
20
+ @RunWith(RobolectricTestRunner::class)
21
+ @Config(sdk = [28])
22
+ class GraniteVideoViewRobolectricTest {
23
+
24
+ private lateinit var context: Context
25
+ private lateinit var mockProvider: GraniteVideoProvider
26
+ private lateinit var mockPlayerView: View
27
+ private lateinit var mockEventListener: GraniteVideoEventListener
28
+
29
+ @Before
30
+ fun setUp() {
31
+ context = RuntimeEnvironment.getApplication()
32
+ mockProvider = mockk(relaxed = true)
33
+ mockPlayerView = View(context)
34
+ mockEventListener = mockk(relaxed = true)
35
+
36
+ every { mockProvider.createPlayerView(any()) } returns mockPlayerView
37
+ every { mockProvider.providerId } returns "test-provider"
38
+ every { mockProvider.providerName } returns "Test Provider"
39
+
40
+ GraniteVideoRegistry.clear()
41
+ }
42
+
43
+ @After
44
+ fun tearDown() {
45
+ clearAllMocks()
46
+ GraniteVideoRegistry.clear()
47
+ }
48
+
49
+ // ============================================================
50
+ // Initialization Tests
51
+ // ============================================================
52
+
53
+ @Test
54
+ fun `view created with provider factory should use the provided factory`() {
55
+ val view = GraniteVideoView(
56
+ context = context,
57
+ providerFactory = { mockProvider }
58
+ )
59
+
60
+ assertEquals(mockProvider, view.currentProvider)
61
+ }
62
+
63
+ @Test
64
+ fun `view should set delegate on provider`() {
65
+ val view = GraniteVideoView(
66
+ context = context,
67
+ providerFactory = { mockProvider }
68
+ )
69
+
70
+ verify { mockProvider.delegate = view }
71
+ }
72
+
73
+ @Test
74
+ fun `view should create player view on initialization`() {
75
+ GraniteVideoView(
76
+ context = context,
77
+ providerFactory = { mockProvider }
78
+ )
79
+
80
+ verify { mockProvider.createPlayerView(context) }
81
+ }
82
+
83
+ @Test
84
+ fun `view created with default provider from registry should use registry provider`() {
85
+ GraniteVideoRegistry.registerFactory("default") { mockProvider }
86
+ GraniteVideoRegistry.setDefaultProvider("default")
87
+
88
+ val view = GraniteVideoView(context = context)
89
+
90
+ assertEquals(mockProvider, view.currentProvider)
91
+ }
92
+
93
+ @Test
94
+ fun `view created with no registry and no factory should have a fallback provider`() {
95
+ val view = GraniteVideoView(context = context)
96
+
97
+ assertNotNull(view.currentProvider)
98
+ }
99
+
100
+ // ============================================================
101
+ // setSource Tests
102
+ // ============================================================
103
+
104
+ @Test
105
+ fun `setSource with valid source should call loadSource on provider`() {
106
+ val view = GraniteVideoView(
107
+ context = context,
108
+ providerFactory = { mockProvider }
109
+ )
110
+
111
+ val source = mapOf(
112
+ "uri" to "https://example.com/video.mp4",
113
+ "type" to "mp4",
114
+ "startPosition" to 10.0,
115
+ "headers" to mapOf("Authorization" to "Bearer token")
116
+ )
117
+ view.setSource(source)
118
+
119
+ verify {
120
+ mockProvider.loadSource(match<GraniteVideoSource> {
121
+ it.uri == "https://example.com/video.mp4" &&
122
+ it.type == "mp4" &&
123
+ it.startPosition == 10.0 &&
124
+ it.headers?.get("Authorization") == "Bearer token"
125
+ })
126
+ }
127
+ }
128
+
129
+ @Test
130
+ fun `setSource with null source should not call loadSource`() {
131
+ val view = GraniteVideoView(
132
+ context = context,
133
+ providerFactory = { mockProvider }
134
+ )
135
+
136
+ view.setSource(null)
137
+
138
+ verify(exactly = 0) { mockProvider.loadSource(any()) }
139
+ }
140
+
141
+ @Test
142
+ fun `setSource when not paused should call play after loading`() {
143
+ val view = GraniteVideoView(
144
+ context = context,
145
+ providerFactory = { mockProvider }
146
+ )
147
+ view.setPaused(false)
148
+ clearMocks(mockProvider, answers = false)
149
+ every { mockProvider.createPlayerView(any()) } returns mockPlayerView
150
+
151
+ view.setSource(mapOf("uri" to "https://example.com/video.mp4"))
152
+
153
+ verifyOrder {
154
+ mockProvider.loadSource(any())
155
+ mockProvider.play()
156
+ }
157
+ }
158
+
159
+ // ============================================================
160
+ // Playback Control Tests
161
+ // ============================================================
162
+
163
+ @Test
164
+ fun `setPaused(true) should call pause on provider`() {
165
+ val view = GraniteVideoView(
166
+ context = context,
167
+ providerFactory = { mockProvider }
168
+ )
169
+
170
+ view.setPaused(true)
171
+
172
+ verify { mockProvider.pause() }
173
+ }
174
+
175
+ @Test
176
+ fun `setPaused(false) should call play on provider`() {
177
+ val view = GraniteVideoView(
178
+ context = context,
179
+ providerFactory = { mockProvider }
180
+ )
181
+
182
+ view.setPaused(false)
183
+
184
+ verify { mockProvider.play() }
185
+ }
186
+
187
+ @Test
188
+ fun `setMuted should call setMuted on provider`() {
189
+ val view = GraniteVideoView(
190
+ context = context,
191
+ providerFactory = { mockProvider }
192
+ )
193
+
194
+ view.setMuted(true)
195
+
196
+ verify { mockProvider.setMuted(true) }
197
+ }
198
+
199
+ @Test
200
+ fun `setVolume should call setVolume on provider`() {
201
+ val view = GraniteVideoView(
202
+ context = context,
203
+ providerFactory = { mockProvider }
204
+ )
205
+
206
+ view.setVolume(0.5f)
207
+
208
+ verify { mockProvider.setVolume(0.5f) }
209
+ }
210
+
211
+ @Test
212
+ fun `setRate should call setRate on provider`() {
213
+ val view = GraniteVideoView(
214
+ context = context,
215
+ providerFactory = { mockProvider }
216
+ )
217
+
218
+ view.setRate(1.5f)
219
+
220
+ verify { mockProvider.setRate(1.5f) }
221
+ }
222
+
223
+ @Test
224
+ fun `setRepeat should call setRepeat on provider`() {
225
+ val view = GraniteVideoView(
226
+ context = context,
227
+ providerFactory = { mockProvider }
228
+ )
229
+
230
+ view.setRepeat(true)
231
+
232
+ verify { mockProvider.setRepeat(true) }
233
+ }
234
+
235
+ @Test
236
+ fun `seek should call seek on provider`() {
237
+ val view = GraniteVideoView(
238
+ context = context,
239
+ providerFactory = { mockProvider }
240
+ )
241
+
242
+ view.seek(30.0, 0.5)
243
+
244
+ verify { mockProvider.seek(30.0, 0.5) }
245
+ }
246
+
247
+ // ============================================================
248
+ // Resize Mode Tests
249
+ // ============================================================
250
+
251
+ @Test
252
+ fun `setResizeMode cover should call setResizeMode with COVER`() {
253
+ val view = GraniteVideoView(
254
+ context = context,
255
+ providerFactory = { mockProvider }
256
+ )
257
+
258
+ view.setResizeMode("cover")
259
+
260
+ verify { mockProvider.setResizeMode(GraniteVideoResizeMode.COVER) }
261
+ }
262
+
263
+ @Test
264
+ fun `setResizeMode stretch should call setResizeMode with STRETCH`() {
265
+ val view = GraniteVideoView(
266
+ context = context,
267
+ providerFactory = { mockProvider }
268
+ )
269
+
270
+ view.setResizeMode("stretch")
271
+
272
+ verify { mockProvider.setResizeMode(GraniteVideoResizeMode.STRETCH) }
273
+ }
274
+
275
+ @Test
276
+ fun `setResizeMode none should call setResizeMode with NONE`() {
277
+ val view = GraniteVideoView(
278
+ context = context,
279
+ providerFactory = { mockProvider }
280
+ )
281
+
282
+ view.setResizeMode("none")
283
+
284
+ verify { mockProvider.setResizeMode(GraniteVideoResizeMode.NONE) }
285
+ }
286
+
287
+ @Test
288
+ fun `setResizeMode contain should call setResizeMode with CONTAIN`() {
289
+ val view = GraniteVideoView(
290
+ context = context,
291
+ providerFactory = { mockProvider }
292
+ )
293
+
294
+ view.setResizeMode("contain")
295
+
296
+ verify { mockProvider.setResizeMode(GraniteVideoResizeMode.CONTAIN) }
297
+ }
298
+
299
+ // ============================================================
300
+ // Controls Tests
301
+ // ============================================================
302
+
303
+ @Test
304
+ fun `setControls should call setControlsEnabled on provider`() {
305
+ val view = GraniteVideoView(
306
+ context = context,
307
+ providerFactory = { mockProvider }
308
+ )
309
+
310
+ view.setControls(true)
311
+
312
+ verify { mockProvider.setControlsEnabled(true) }
313
+ }
314
+
315
+ @Test
316
+ fun `setFullscreen should call setFullscreen on provider`() {
317
+ val view = GraniteVideoView(
318
+ context = context,
319
+ providerFactory = { mockProvider }
320
+ )
321
+
322
+ view.setFullscreen(true)
323
+
324
+ verify { mockProvider.setFullscreen(true, true) }
325
+ }
326
+
327
+ @Test
328
+ fun `setPictureInPicture should call setPictureInPictureEnabled on provider`() {
329
+ val view = GraniteVideoView(
330
+ context = context,
331
+ providerFactory = { mockProvider }
332
+ )
333
+
334
+ view.setPictureInPicture(true)
335
+
336
+ verify { mockProvider.setPictureInPictureEnabled(true) }
337
+ }
338
+
339
+ // ============================================================
340
+ // Buffer Config Tests
341
+ // ============================================================
342
+
343
+ @Test
344
+ fun `setBufferConfig with valid config should call setBufferConfig on provider`() {
345
+ val view = GraniteVideoView(
346
+ context = context,
347
+ providerFactory = { mockProvider }
348
+ )
349
+
350
+ val config = mapOf(
351
+ "minBufferMs" to 10000,
352
+ "maxBufferMs" to 30000,
353
+ "bufferForPlaybackMs" to 2000
354
+ )
355
+ view.setBufferConfig(config)
356
+
357
+ verify {
358
+ mockProvider.setBufferConfig(match<GraniteVideoBufferConfig> {
359
+ it.minBufferMs == 10000 &&
360
+ it.maxBufferMs == 30000 &&
361
+ it.bufferForPlaybackMs == 2000
362
+ })
363
+ }
364
+ }
365
+
366
+ @Test
367
+ fun `setBufferConfig with null should not call setBufferConfig`() {
368
+ val view = GraniteVideoView(
369
+ context = context,
370
+ providerFactory = { mockProvider }
371
+ )
372
+
373
+ view.setBufferConfig(null)
374
+
375
+ verify(exactly = 0) { mockProvider.setBufferConfig(any()) }
376
+ }
377
+
378
+ // ============================================================
379
+ // Delegate Events Tests
380
+ // ============================================================
381
+
382
+ @Test
383
+ fun `onLoadStart should forward to eventListener`() {
384
+ val view = GraniteVideoView(
385
+ context = context,
386
+ providerFactory = { mockProvider }
387
+ )
388
+ view.eventListener = mockEventListener
389
+
390
+ view.onLoadStart(true, "mp4", "https://example.com/video.mp4")
391
+
392
+ verify { mockEventListener.onLoadStart(true, "mp4", "https://example.com/video.mp4") }
393
+ }
394
+
395
+ @Test
396
+ fun `onLoad should forward to eventListener`() {
397
+ val view = GraniteVideoView(
398
+ context = context,
399
+ providerFactory = { mockProvider }
400
+ )
401
+ view.eventListener = mockEventListener
402
+
403
+ val data = GraniteVideoLoadData(
404
+ currentTime = 0.0,
405
+ duration = 60.0,
406
+ naturalWidth = 1920.0,
407
+ naturalHeight = 1080.0,
408
+ orientation = "landscape"
409
+ )
410
+ view.onLoad(data)
411
+
412
+ verify { mockEventListener.onLoad(data) }
413
+ }
414
+
415
+ @Test
416
+ fun `onError should forward to eventListener`() {
417
+ val view = GraniteVideoView(
418
+ context = context,
419
+ providerFactory = { mockProvider }
420
+ )
421
+ view.eventListener = mockEventListener
422
+
423
+ val error = GraniteVideoErrorData(
424
+ code = 1001,
425
+ domain = "ExoPlayer",
426
+ localizedDescription = "Test error",
427
+ errorString = "ERROR_TEST"
428
+ )
429
+ view.onError(error)
430
+
431
+ verify { mockEventListener.onError(error) }
432
+ }
433
+
434
+ @Test
435
+ fun `onProgress should forward to eventListener`() {
436
+ val view = GraniteVideoView(
437
+ context = context,
438
+ providerFactory = { mockProvider }
439
+ )
440
+ view.eventListener = mockEventListener
441
+
442
+ val data = GraniteVideoProgressData(
443
+ currentTime = 10.0,
444
+ playableDuration = 20.0,
445
+ seekableDuration = 60.0
446
+ )
447
+ view.onProgress(data)
448
+
449
+ verify { mockEventListener.onProgress(data) }
450
+ }
451
+
452
+ @Test
453
+ fun `onEnd should forward to eventListener`() {
454
+ val view = GraniteVideoView(
455
+ context = context,
456
+ providerFactory = { mockProvider }
457
+ )
458
+ view.eventListener = mockEventListener
459
+
460
+ view.onEnd()
461
+
462
+ verify { mockEventListener.onEnd() }
463
+ }
464
+
465
+ @Test
466
+ fun `onBuffer should forward to eventListener`() {
467
+ val view = GraniteVideoView(
468
+ context = context,
469
+ providerFactory = { mockProvider }
470
+ )
471
+ view.eventListener = mockEventListener
472
+
473
+ view.onBuffer(true)
474
+
475
+ verify { mockEventListener.onBuffer(true) }
476
+ }
477
+
478
+ @Test
479
+ fun `onPlaybackStateChanged should forward to eventListener`() {
480
+ val view = GraniteVideoView(
481
+ context = context,
482
+ providerFactory = { mockProvider }
483
+ )
484
+ view.eventListener = mockEventListener
485
+
486
+ view.onPlaybackStateChanged(true, false, true)
487
+
488
+ verify { mockEventListener.onPlaybackStateChanged(true, false, true) }
489
+ }
490
+
491
+ // ============================================================
492
+ // Provider Selection Tests
493
+ // ============================================================
494
+
495
+ @Test
496
+ fun `default provider set before view creation should be used`() {
497
+ val mockProvider2 = mockk<GraniteVideoProvider>(relaxed = true)
498
+ val mockPlayerView2 = View(context)
499
+ every { mockProvider2.createPlayerView(any()) } returns mockPlayerView2
500
+ every { mockProvider2.providerId } returns "provider2"
501
+
502
+ GraniteVideoRegistry.registerFactory("provider1") { mockProvider }
503
+ GraniteVideoRegistry.registerFactory("provider2") { mockProvider2 }
504
+ GraniteVideoRegistry.setDefaultProvider("provider2")
505
+
506
+ val view = GraniteVideoView(context = context)
507
+
508
+ assertEquals(mockProvider2, view.currentProvider)
509
+ }
510
+
511
+ @Test
512
+ fun `different default providers for different views`() {
513
+ val mockProvider2 = mockk<GraniteVideoProvider>(relaxed = true)
514
+ val mockPlayerView2 = View(context)
515
+ every { mockProvider2.createPlayerView(any()) } returns mockPlayerView2
516
+ every { mockProvider2.providerId } returns "provider2"
517
+
518
+ GraniteVideoRegistry.registerFactory("provider1") { mockProvider }
519
+ GraniteVideoRegistry.registerFactory("provider2") { mockProvider2 }
520
+
521
+ GraniteVideoRegistry.setDefaultProvider("provider1")
522
+ val view1 = GraniteVideoView(context = context)
523
+
524
+ GraniteVideoRegistry.setDefaultProvider("provider2")
525
+ val view2 = GraniteVideoView(context = context)
526
+
527
+ assertEquals(mockProvider, view1.currentProvider)
528
+ assertEquals(mockProvider2, view2.currentProvider)
529
+ }
530
+
531
+ // ============================================================
532
+ // Commands Tests
533
+ // ============================================================
534
+
535
+ @Test
536
+ fun `pauseCommand should call pause on provider`() {
537
+ val view = GraniteVideoView(
538
+ context = context,
539
+ providerFactory = { mockProvider }
540
+ )
541
+
542
+ view.pauseCommand()
543
+
544
+ verify { mockProvider.pause() }
545
+ }
546
+
547
+ @Test
548
+ fun `resumeCommand should call play on provider`() {
549
+ val view = GraniteVideoView(
550
+ context = context,
551
+ providerFactory = { mockProvider }
552
+ )
553
+
554
+ view.resumeCommand()
555
+
556
+ verify { mockProvider.play() }
557
+ }
558
+
559
+ @Test
560
+ fun `seekCommand should call seek on provider`() {
561
+ val view = GraniteVideoView(
562
+ context = context,
563
+ providerFactory = { mockProvider }
564
+ )
565
+
566
+ view.seekCommand(30.0, 0.1)
567
+
568
+ verify { mockProvider.seek(30.0, 0.1) }
569
+ }
570
+
571
+ @Test
572
+ fun `setVolumeCommand should call setVolume on provider`() {
573
+ val view = GraniteVideoView(
574
+ context = context,
575
+ providerFactory = { mockProvider }
576
+ )
577
+
578
+ view.setVolumeCommand(0.7f)
579
+
580
+ verify { mockProvider.setVolume(0.7f) }
581
+ }
582
+
583
+ @Test
584
+ fun `setSourceCommand should call loadSource with uri`() {
585
+ val view = GraniteVideoView(
586
+ context = context,
587
+ providerFactory = { mockProvider }
588
+ )
589
+
590
+ view.setSourceCommand("https://example.com/video.mp4")
591
+
592
+ verify {
593
+ mockProvider.loadSource(match<GraniteVideoSource> {
594
+ it.uri == "https://example.com/video.mp4"
595
+ })
596
+ }
597
+ }
598
+ }