een-api-toolkit 0.3.20 → 0.3.22

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 (33) hide show
  1. package/.claude/agents/docs-accuracy-reviewer.md +146 -0
  2. package/.claude/agents/een-auth-agent.md +168 -0
  3. package/.claude/agents/een-devices-agent.md +294 -0
  4. package/.claude/agents/een-events-agent.md +375 -0
  5. package/.claude/agents/een-media-agent.md +256 -0
  6. package/.claude/agents/een-setup-agent.md +126 -0
  7. package/.claude/agents/een-users-agent.md +239 -0
  8. package/.claude/agents/test-runner.md +144 -0
  9. package/CHANGELOG.md +138 -30
  10. package/docs/AI-CONTEXT.md +169 -1700
  11. package/docs/ai-reference/AI-AUTH.md +288 -0
  12. package/docs/ai-reference/AI-DEVICES.md +569 -0
  13. package/docs/ai-reference/AI-EVENTS.md +1745 -0
  14. package/docs/ai-reference/AI-MEDIA.md +974 -0
  15. package/docs/ai-reference/AI-SETUP.md +267 -0
  16. package/docs/ai-reference/AI-USERS.md +255 -0
  17. package/examples/vue-event-subscriptions/package-lock.json +8 -1
  18. package/examples/vue-event-subscriptions/package.json +1 -0
  19. package/examples/vue-event-subscriptions/src/App.vue +1 -41
  20. package/examples/vue-event-subscriptions/src/composables/useHlsPlayer.ts +272 -0
  21. package/examples/vue-event-subscriptions/src/main.ts +3 -3
  22. package/examples/vue-event-subscriptions/src/stores/connection.ts +101 -0
  23. package/examples/vue-event-subscriptions/src/stores/mediaSession.ts +79 -0
  24. package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +349 -88
  25. package/examples/vue-event-subscriptions/src/views/Logout.vue +6 -0
  26. package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +0 -13
  27. package/examples/vue-events/package-lock.json +8 -1
  28. package/examples/vue-events/package.json +1 -0
  29. package/examples/vue-events/src/components/EventsModal.vue +269 -47
  30. package/examples/vue-events/src/composables/useHlsPlayer.ts +272 -0
  31. package/examples/vue-events/src/stores/mediaSession.ts +79 -0
  32. package/package.json +10 -2
  33. package/scripts/setup-agents.ts +116 -0
@@ -1,36 +1,38 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
2
+ import { ref, computed, onMounted, watch } from 'vue'
3
3
  import { useRoute } from 'vue-router'
4
4
  import {
5
5
  listEventSubscriptions,
6
- connectToEventSubscription,
6
+ getRecordedImage,
7
7
  type EventSubscription,
8
- type SSEConnection,
9
- type SSEConnectionStatus,
10
- type SSEEvent,
11
- type EenError
8
+ type SSEEvent
12
9
  } from 'een-api-toolkit'
10
+ import { useConnectionStore } from '../stores/connection'
11
+ import { useHlsPlayer } from '../composables/useHlsPlayer'
13
12
 
14
13
  const route = useRoute()
14
+ const connectionStore = useConnectionStore()
15
+
16
+ // Initialize HLS player composable
17
+ const hlsPlayer = useHlsPlayer()
18
+ const { videoUrl, videoError, loadingVideo, loadVideo, resetVideo } = hlsPlayer
15
19
 
16
20
  // Subscriptions state
17
21
  const subscriptions = ref<EventSubscription[]>([])
18
22
  const selectedSubscriptionId = ref<string | null>(null)
19
23
  const loadingSubscriptions = ref(false)
20
24
 
21
- // Connection state
22
- const connection = ref<SSEConnection | null>(null)
23
- const connectionStatus = ref<SSEConnectionStatus>('disconnected')
24
- const connectionError = ref<EenError | null>(null)
25
-
26
- // Events state
27
- const events = ref<SSEEvent[]>([])
28
- const maxEvents = 100
29
-
30
25
  // Modal state
31
26
  const selectedEvent = ref<SSEEvent | null>(null)
32
27
  const showModal = ref(false)
33
28
 
29
+ // Lightbox state
30
+ const showLightbox = ref(false)
31
+ const lightboxImageUrl = ref<string | null>(null)
32
+ const loadingImage = ref(false)
33
+ const imageError = ref<string | null>(null)
34
+ const showVideo = ref(false)
35
+
34
36
  const selectedSubscription = computed(() => {
35
37
  return subscriptions.value.find(s => s.id === selectedSubscriptionId.value)
36
38
  })
@@ -41,8 +43,13 @@ const canConnect = computed(() => {
41
43
  return !!selectedSubscription.value.deliveryConfig.sseUrl
42
44
  })
43
45
 
44
- const isConnected = computed(() => connectionStatus.value === 'connected')
45
- const isConnecting = computed(() => connectionStatus.value === 'connecting')
46
+ // Use store computed values
47
+ const isConnected = computed(() => connectionStore.isConnected)
48
+ const isConnecting = computed(() => connectionStore.isConnecting)
49
+ const connectionStatus = computed(() => connectionStore.connectionStatus)
50
+ const connectionError = computed(() => connectionStore.connectionError)
51
+ const events = computed(() => connectionStore.events)
52
+ const maxEvents = connectionStore.maxEvents
46
53
 
47
54
  async function loadSubscriptions() {
48
55
  loadingSubscriptions.value = true
@@ -58,51 +65,52 @@ async function loadSubscriptions() {
58
65
  loadingSubscriptions.value = false
59
66
  }
60
67
 
61
- function connect() {
68
+ async function connect() {
62
69
  if (!canConnect.value || !selectedSubscription.value) return
63
70
 
64
- const sseUrl = selectedSubscription.value.deliveryConfig.type === 'serverSentEvents.v1'
65
- ? selectedSubscription.value.deliveryConfig.sseUrl
66
- : undefined
67
-
68
- if (!sseUrl) return
69
-
70
- connectionError.value = null
71
- events.value = []
72
-
73
- const result = connectToEventSubscription(sseUrl, {
74
- onEvent: (event) => {
75
- // Add new event at the beginning, limit to maxEvents
76
- // Using unshift + pop is more efficient than spread operator for large arrays
77
- events.value.unshift(event)
78
- if (events.value.length > maxEvents) {
79
- events.value.pop()
80
- }
81
- },
82
- onError: (error) => {
83
- connectionError.value = { code: 'NETWORK_ERROR', message: error.message }
84
- },
85
- onStatusChange: (status) => {
86
- connectionStatus.value = status
87
- }
88
- })
71
+ const subscriptionId = selectedSubscription.value.id
89
72
 
90
- if (result.error) {
91
- connectionError.value = result.error
92
- } else {
93
- connection.value = result.data
73
+ // Reload all subscriptions to get fresh SSE URLs
74
+ // SSE URLs become invalid after disconnecting
75
+ await loadSubscriptions()
76
+
77
+ // Find the subscription in the refreshed list
78
+ const freshSubscription = subscriptions.value.find(s => s.id === subscriptionId)
79
+ if (!freshSubscription) {
80
+ connectionStore.setConnectionError({ code: 'NOT_FOUND', message: 'Subscription no longer exists' })
81
+ return
82
+ }
83
+
84
+ if (freshSubscription.deliveryConfig.type !== 'serverSentEvents.v1') {
85
+ connectionStore.setConnectionError({ code: 'VALIDATION_ERROR', message: 'Subscription is not an SSE type' })
86
+ return
94
87
  }
88
+
89
+ const sseUrl = freshSubscription.deliveryConfig.sseUrl
90
+ if (!sseUrl) {
91
+ connectionStore.setConnectionError({ code: 'VALIDATION_ERROR', message: 'No SSE URL available' })
92
+ return
93
+ }
94
+
95
+ connectionStore.connect(subscriptionId, sseUrl)
95
96
  }
96
97
 
97
98
  function disconnect() {
98
- if (connection.value) {
99
- connection.value.close()
100
- connection.value = null
99
+ // Get the subscription ID before disconnecting
100
+ const disconnectedId = connectionStore.connectedSubscriptionId
101
+
102
+ connectionStore.disconnect()
103
+
104
+ // Remove the subscription from the list since SSE URLs are single-use
105
+ // and cannot be reconnected
106
+ if (disconnectedId) {
107
+ subscriptions.value = subscriptions.value.filter(s => s.id !== disconnectedId)
108
+ selectedSubscriptionId.value = null
101
109
  }
102
110
  }
103
111
 
104
112
  function clearEvents() {
105
- events.value = []
113
+ connectionStore.clearEvents()
106
114
  }
107
115
 
108
116
  function formatTimestamp(timestamp: string): string {
@@ -138,8 +146,12 @@ function closeModal() {
138
146
  }
139
147
 
140
148
  function handleKeyDown(e: KeyboardEvent) {
141
- if (e.key === 'Escape' && showModal.value) {
142
- closeModal()
149
+ if (e.key === 'Escape') {
150
+ if (showLightbox.value) {
151
+ closeLightbox()
152
+ } else if (showModal.value) {
153
+ closeModal()
154
+ }
143
155
  }
144
156
  }
145
157
 
@@ -149,28 +161,109 @@ function handleModalBackdropClick(e: MouseEvent) {
149
161
  }
150
162
  }
151
163
 
164
+ // Extract the image URL from event data
165
+ function getEventImageUrl(event: SSEEvent): string | null {
166
+ if (!event.data) return null
167
+ const fullFrameData = event.data.find(d => d.type === 'een.fullFrameImageUrl.v1')
168
+ if (fullFrameData && typeof fullFrameData.httpsUrl === 'string') {
169
+ return fullFrameData.httpsUrl
170
+ }
171
+ return null
172
+ }
173
+
174
+ // Check if event has media URLs
175
+ function hasMediaUrls(event: SSEEvent): boolean {
176
+ return getEventImageUrl(event) !== null
177
+ }
178
+
179
+ // Handle image click (preview or HD)
180
+ // Uses event.actorId and event.startTimestamp directly instead of parsing URL
181
+ async function handleImageClick(quality: 'preview' | 'main' = 'preview') {
182
+ if (!selectedEvent.value) return
183
+
184
+ // Use event properties directly for reliability
185
+ const deviceId = selectedEvent.value.actorId
186
+ const timestamp = selectedEvent.value.startTimestamp
187
+
188
+ if (!deviceId || !timestamp) {
189
+ return
190
+ }
191
+
192
+ loadingImage.value = true
193
+ imageError.value = null
194
+ lightboxImageUrl.value = null
195
+ showLightbox.value = true
196
+ showVideo.value = false
197
+
198
+ // Use the toolkit's getRecordedImage function
199
+ const result = await getRecordedImage({
200
+ deviceId,
201
+ type: quality,
202
+ timestamp__gte: timestamp
203
+ })
204
+
205
+ if (result.error) {
206
+ imageError.value = result.error.message
207
+ } else if (result.data?.imageData) {
208
+ lightboxImageUrl.value = result.data.imageData
209
+ } else {
210
+ imageError.value = 'No image data returned'
211
+ }
212
+
213
+ loadingImage.value = false
214
+ }
215
+
216
+ // Handle video click
217
+ // Uses event.actorId and event.startTimestamp directly instead of parsing URL
218
+ async function handleVideoClick() {
219
+ if (!selectedEvent.value) return
220
+
221
+ // Use event properties directly for reliability
222
+ const deviceId = selectedEvent.value.actorId
223
+ const timestamp = selectedEvent.value.startTimestamp
224
+
225
+ if (!deviceId || !timestamp) {
226
+ return
227
+ }
228
+
229
+ showVideo.value = true
230
+ showLightbox.value = true
231
+
232
+ // Use the composable to load and play video
233
+ await loadVideo(deviceId, timestamp)
234
+ }
235
+
236
+ // Close lightbox
237
+ function closeLightbox() {
238
+ showLightbox.value = false
239
+ lightboxImageUrl.value = null
240
+ imageError.value = null
241
+ // Cleanup video if it was playing
242
+ resetVideo()
243
+ showVideo.value = false
244
+ }
245
+
152
246
  // Load subscriptions on mount
153
247
  onMounted(async () => {
154
248
  await loadSubscriptions()
155
249
 
156
- // Check for subscriptionId in query params
157
- const queryId = route.query.subscriptionId as string | undefined
158
- if (queryId) {
159
- selectedSubscriptionId.value = queryId
250
+ // If already connected, set the selected subscription to match
251
+ if (connectionStore.connectedSubscriptionId) {
252
+ selectedSubscriptionId.value = connectionStore.connectedSubscriptionId
253
+ } else {
254
+ // Check for subscriptionId in query params
255
+ const queryId = route.query.subscriptionId as string | undefined
256
+ if (queryId) {
257
+ selectedSubscriptionId.value = queryId
258
+ }
160
259
  }
161
260
  })
162
261
 
163
- // Clean up connection on unmount
164
- onUnmounted(() => {
165
- disconnect()
166
- })
167
-
168
- // Auto-disconnect when changing subscription
169
- watch(selectedSubscriptionId, () => {
170
- if (connection.value) {
262
+ // Auto-disconnect when changing subscription (but not if selecting the already connected one)
263
+ watch(selectedSubscriptionId, (newId) => {
264
+ if (newId && newId !== connectionStore.connectedSubscriptionId && connectionStore.isConnected) {
171
265
  disconnect()
172
266
  }
173
- events.value = []
174
267
  })
175
268
  </script>
176
269
 
@@ -283,6 +376,12 @@ watch(selectedSubscriptionId, () => {
283
376
  <li>Maximum {{ maxEvents }} events are displayed (oldest removed first)</li>
284
377
  <li>Click on an event card to view detailed information</li>
285
378
  </ul>
379
+ <h4>Important</h4>
380
+ <ul class="warning-list">
381
+ <li>SSE URLs are single-use. Once disconnected, the subscription cannot be reconnected.</li>
382
+ <li>To receive events again after disconnecting, create a new subscription.</li>
383
+ <li>Subscriptions have a 15-minute TTL and expire if not connected.</li>
384
+ </ul>
286
385
  </div>
287
386
 
288
387
  <!-- Event Details Modal -->
@@ -297,7 +396,30 @@ watch(selectedSubscriptionId, () => {
297
396
  <div class="modal-content" role="document">
298
397
  <div class="modal-header">
299
398
  <h3 id="modal-title">{{ formatEventType(selectedEvent.type) }}</h3>
300
- <button class="modal-close" @click="closeModal" aria-label="Close modal">&times;</button>
399
+ <div class="modal-header-buttons">
400
+ <button
401
+ v-if="hasMediaUrls(selectedEvent)"
402
+ class="image-button"
403
+ @click="handleImageClick('preview')"
404
+ >
405
+ Preview
406
+ </button>
407
+ <button
408
+ v-if="hasMediaUrls(selectedEvent)"
409
+ class="image-button image-button-hd"
410
+ @click="handleImageClick('main')"
411
+ >
412
+ HD Image
413
+ </button>
414
+ <button
415
+ v-if="hasMediaUrls(selectedEvent)"
416
+ class="image-button image-button-video"
417
+ @click="handleVideoClick"
418
+ >
419
+ Video
420
+ </button>
421
+ <button class="close-button" @click="closeModal" aria-label="Close modal">&times;</button>
422
+ </div>
301
423
  </div>
302
424
  <div class="modal-body">
303
425
  <div class="modal-section">
@@ -339,6 +461,39 @@ watch(selectedSubscriptionId, () => {
339
461
  </div>
340
462
  </div>
341
463
  </div>
464
+
465
+ </div>
466
+
467
+ <!-- Image/Video Lightbox -->
468
+ <div v-if="showLightbox" class="lightbox-overlay" @click.self="closeLightbox">
469
+ <div class="lightbox-content">
470
+ <button class="lightbox-close" @click="closeLightbox">&times;</button>
471
+ <!-- Video mode -->
472
+ <template v-if="showVideo">
473
+ <div v-if="loadingVideo" class="lightbox-loading">Loading video...</div>
474
+ <div v-else-if="videoError" class="lightbox-error">{{ videoError }}</div>
475
+ <video
476
+ v-else-if="videoUrl"
477
+ :ref="(el) => hlsPlayer.videoRef.value = el as HTMLVideoElement | null"
478
+ class="lightbox-video"
479
+ controls
480
+ autoplay
481
+ muted
482
+ playsinline
483
+ />
484
+ </template>
485
+ <!-- Image mode -->
486
+ <template v-else>
487
+ <div v-if="loadingImage" class="lightbox-loading">Loading image...</div>
488
+ <div v-else-if="imageError" class="lightbox-error">{{ imageError }}</div>
489
+ <img
490
+ v-else-if="lightboxImageUrl"
491
+ :src="lightboxImageUrl"
492
+ alt="Event image"
493
+ class="lightbox-image"
494
+ />
495
+ </template>
496
+ </div>
342
497
  </div>
343
498
  </div>
344
499
  </template>
@@ -498,8 +653,8 @@ h2 {
498
653
  position: fixed;
499
654
  top: 0;
500
655
  left: 0;
501
- width: 100%;
502
- height: 100%;
656
+ right: 0;
657
+ bottom: 0;
503
658
  background: rgba(0, 0, 0, 0.5);
504
659
  display: flex;
505
660
  align-items: center;
@@ -509,49 +664,77 @@ h2 {
509
664
 
510
665
  .modal-content {
511
666
  background: white;
512
- border-radius: 12px;
513
- width: 90%;
514
- max-width: 600px;
667
+ border-radius: 8px;
668
+ width: 80%;
515
669
  max-height: 80vh;
516
670
  overflow: hidden;
517
671
  display: flex;
518
672
  flex-direction: column;
519
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
673
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
520
674
  }
521
675
 
522
676
  .modal-header {
523
677
  display: flex;
524
678
  justify-content: space-between;
525
679
  align-items: center;
526
- padding: 20px;
527
- border-bottom: 1px solid #e2e3e5;
528
- background: #f8f9fa;
680
+ padding: 16px 20px;
681
+ border-bottom: 1px solid #eee;
529
682
  }
530
683
 
531
684
  .modal-header h3 {
532
685
  margin: 0;
533
- color: #42b883;
686
+ font-size: 1.1rem;
687
+ color: #333;
534
688
  }
535
689
 
536
- .modal-close {
690
+ .modal-header-buttons {
691
+ display: flex;
692
+ align-items: center;
693
+ gap: 10px;
694
+ }
695
+
696
+ .image-button {
697
+ padding: 6px 14px;
698
+ background: #42b883;
699
+ color: white;
700
+ border: none;
701
+ border-radius: 4px;
702
+ cursor: pointer;
703
+ font-size: 0.85rem;
704
+ font-weight: 500;
705
+ }
706
+
707
+ .image-button:hover {
708
+ background: #3aa876;
709
+ }
710
+
711
+ .image-button-hd {
712
+ background: #3b82f6;
713
+ }
714
+
715
+ .image-button-hd:hover {
716
+ background: #2563eb;
717
+ }
718
+
719
+ .image-button-video {
720
+ background: #9b59b6;
721
+ }
722
+
723
+ .image-button-video:hover {
724
+ background: #8e44ad;
725
+ }
726
+
727
+ .close-button {
537
728
  background: none;
538
729
  border: none;
539
- font-size: 28px;
730
+ font-size: 1.5rem;
540
731
  cursor: pointer;
541
732
  color: #666;
542
733
  padding: 0;
543
734
  line-height: 1;
544
- width: 36px;
545
- height: 36px;
546
- display: flex;
547
- align-items: center;
548
- justify-content: center;
549
- border-radius: 50%;
550
- transition: background-color 0.15s ease;
551
735
  }
552
736
 
553
- .modal-close:hover {
554
- background: #e2e3e5;
737
+ .close-button:hover {
555
738
  color: #333;
556
739
  }
557
740
 
@@ -637,4 +820,82 @@ h2 {
637
820
  .help-section li {
638
821
  margin-bottom: 5px;
639
822
  }
823
+
824
+ .help-section .warning-list {
825
+ color: #856404;
826
+ background: #fff3cd;
827
+ padding: 10px 10px 10px 30px;
828
+ border-radius: 4px;
829
+ margin-top: 10px;
830
+ }
831
+
832
+ .help-section .warning-list li {
833
+ margin-bottom: 4px;
834
+ }
835
+
836
+ /* Lightbox styles */
837
+ .lightbox-overlay {
838
+ position: fixed;
839
+ top: 0;
840
+ left: 0;
841
+ right: 0;
842
+ bottom: 0;
843
+ background: rgba(0, 0, 0, 0.9);
844
+ display: flex;
845
+ align-items: center;
846
+ justify-content: center;
847
+ z-index: 1100;
848
+ }
849
+
850
+ .lightbox-content {
851
+ position: relative;
852
+ max-width: 90vw;
853
+ max-height: 90vh;
854
+ display: flex;
855
+ align-items: center;
856
+ justify-content: center;
857
+ }
858
+
859
+ .lightbox-close {
860
+ position: absolute;
861
+ top: -40px;
862
+ right: 0;
863
+ background: none;
864
+ border: none;
865
+ color: white;
866
+ font-size: 2rem;
867
+ cursor: pointer;
868
+ padding: 5px 10px;
869
+ line-height: 1;
870
+ }
871
+
872
+ .lightbox-close:hover {
873
+ color: #ccc;
874
+ }
875
+
876
+ .lightbox-image {
877
+ max-width: 90vw;
878
+ max-height: 90vh;
879
+ object-fit: contain;
880
+ border-radius: 4px;
881
+ }
882
+
883
+ .lightbox-video {
884
+ max-width: 90vw;
885
+ max-height: 90vh;
886
+ width: 100%;
887
+ background: #000;
888
+ border-radius: 4px;
889
+ }
890
+
891
+ .lightbox-loading,
892
+ .lightbox-error {
893
+ color: white;
894
+ font-size: 1.1rem;
895
+ padding: 40px;
896
+ }
897
+
898
+ .lightbox-error {
899
+ color: #ff6b6b;
900
+ }
640
901
  </style>
@@ -2,12 +2,18 @@
2
2
  import { onMounted, ref } from 'vue'
3
3
  import { useRouter } from 'vue-router'
4
4
  import { revokeToken } from 'een-api-toolkit'
5
+ import { useConnectionStore } from '../stores/connection'
5
6
 
6
7
  const router = useRouter()
8
+ const connectionStore = useConnectionStore()
7
9
  const processing = ref(true)
8
10
  const error = ref<string | null>(null)
9
11
 
10
12
  onMounted(async () => {
13
+ // Close any active SSE connection and clear events
14
+ connectionStore.disconnect()
15
+ connectionStore.clearEvents()
16
+
11
17
  const result = await revokeToken()
12
18
 
13
19
  if (result.error) {
@@ -172,13 +172,6 @@ function getDeliveryType(sub: EventSubscription): string {
172
172
  return sub.deliveryConfig.type
173
173
  }
174
174
 
175
- function getSseUrl(sub: EventSubscription): string | undefined {
176
- if (sub.deliveryConfig.type === 'serverSentEvents.v1') {
177
- return sub.deliveryConfig.sseUrl
178
- }
179
- return undefined
180
- }
181
-
182
175
  onMounted(async () => {
183
176
  await Promise.all([
184
177
  fetchSubscriptions(),
@@ -267,12 +260,6 @@ onMounted(async () => {
267
260
  <td>{{ sub.subscriptionConfig?.lifeCycle || '-' }}</td>
268
261
  <td>{{ sub.subscriptionConfig?.timeToLiveSeconds ? `${sub.subscriptionConfig.timeToLiveSeconds}s` : '-' }}</td>
269
262
  <td class="actions">
270
- <router-link
271
- v-if="getSseUrl(sub)"
272
- :to="{ path: '/live', query: { subscriptionId: sub.id } }"
273
- >
274
- <button class="secondary small">Listen</button>
275
- </router-link>
276
263
  <button
277
264
  class="danger small"
278
265
  @click="handleDelete(sub.id)"
@@ -9,6 +9,7 @@
9
9
  "version": "0.0.1",
10
10
  "dependencies": {
11
11
  "een-api-toolkit": "file:../..",
12
+ "hls.js": "^1.6.15",
12
13
  "pinia": "^3.0.4",
13
14
  "vue": "^3.4.0",
14
15
  "vue-router": "^4.2.0"
@@ -23,7 +24,7 @@
23
24
  }
24
25
  },
25
26
  "../..": {
26
- "version": "0.3.11",
27
+ "version": "0.3.20",
27
28
  "license": "MIT",
28
29
  "devDependencies": {
29
30
  "@eslint/js": "^9.39.2",
@@ -1276,6 +1277,12 @@
1276
1277
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1277
1278
  }
1278
1279
  },
1280
+ "node_modules/hls.js": {
1281
+ "version": "1.6.15",
1282
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
1283
+ "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
1284
+ "license": "Apache-2.0"
1285
+ },
1279
1286
  "node_modules/hookable": {
1280
1287
  "version": "5.5.3",
1281
1288
  "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "een-api-toolkit": "file:../..",
16
+ "hls.js": "^1.6.15",
16
17
  "pinia": "^3.0.4",
17
18
  "vue": "^3.4.0",
18
19
  "vue-router": "^4.2.0"