een-api-toolkit 0.1.13 → 0.2.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.
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "@een/live-video-web-sdk": "^1.10.2",
12
12
  "een-api-toolkit": "file:../..",
13
- "pinia": "^2.1.7",
13
+ "pinia": "^3.0.4",
14
14
  "vue": "^3.4.0",
15
15
  "vue-router": "^4.2.0"
16
16
  },
@@ -24,21 +24,22 @@
24
24
  }
25
25
  },
26
26
  "../..": {
27
- "version": "0.1.10",
27
+ "version": "0.1.13",
28
28
  "license": "MIT",
29
29
  "devDependencies": {
30
30
  "@playwright/test": "^1.57.0",
31
- "@types/node": "^22.0.0",
32
- "@typescript-eslint/eslint-plugin": "^6.21.0",
33
- "@typescript-eslint/parser": "^6.21.0",
31
+ "@types/node": "^25.0.3",
32
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
33
+ "@typescript-eslint/parser": "^8.51.0",
34
34
  "@vitejs/plugin-vue": "^6.0.0",
35
- "@vue/tsconfig": "^0.5.0",
35
+ "@vue/tsconfig": "^0.8.1",
36
36
  "dotenv": "^17.2.3",
37
- "eslint": "^8.56.0",
38
- "eslint-plugin-vue": "^9.20.0",
37
+ "eslint": "^9.39.2",
38
+ "eslint-plugin-vue": "^10.6.2",
39
+ "globals": "^17.0.0",
39
40
  "husky": "^9.1.7",
40
41
  "jsdom": "^27.4.0",
41
- "pinia": "^2.1.7",
42
+ "pinia": "^3.0.4",
42
43
  "tsx": "^4.21.0",
43
44
  "typedoc": "^0.28.15",
44
45
  "typedoc-plugin-markdown": "^4.9.0",
@@ -997,6 +998,30 @@
997
998
  "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
998
999
  "license": "MIT"
999
1000
  },
1001
+ "node_modules/@vue/devtools-kit": {
1002
+ "version": "7.7.9",
1003
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
1004
+ "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
1005
+ "license": "MIT",
1006
+ "dependencies": {
1007
+ "@vue/devtools-shared": "^7.7.9",
1008
+ "birpc": "^2.3.0",
1009
+ "hookable": "^5.5.3",
1010
+ "mitt": "^3.0.1",
1011
+ "perfect-debounce": "^1.0.0",
1012
+ "speakingurl": "^14.0.1",
1013
+ "superjson": "^2.2.2"
1014
+ }
1015
+ },
1016
+ "node_modules/@vue/devtools-shared": {
1017
+ "version": "7.7.9",
1018
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
1019
+ "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
1020
+ "license": "MIT",
1021
+ "dependencies": {
1022
+ "rfdc": "^1.4.1"
1023
+ }
1024
+ },
1000
1025
  "node_modules/@vue/language-core": {
1001
1026
  "version": "3.2.1",
1002
1027
  "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.1.tgz",
@@ -1070,6 +1095,30 @@
1070
1095
  "dev": true,
1071
1096
  "license": "MIT"
1072
1097
  },
1098
+ "node_modules/birpc": {
1099
+ "version": "2.9.0",
1100
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
1101
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
1102
+ "license": "MIT",
1103
+ "funding": {
1104
+ "url": "https://github.com/sponsors/antfu"
1105
+ }
1106
+ },
1107
+ "node_modules/copy-anything": {
1108
+ "version": "4.0.5",
1109
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
1110
+ "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
1111
+ "license": "MIT",
1112
+ "dependencies": {
1113
+ "is-what": "^5.2.0"
1114
+ },
1115
+ "engines": {
1116
+ "node": ">=18"
1117
+ },
1118
+ "funding": {
1119
+ "url": "https://github.com/sponsors/mesqueeb"
1120
+ }
1121
+ },
1073
1122
  "node_modules/csstype": {
1074
1123
  "version": "3.2.3",
1075
1124
  "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1186,6 +1235,24 @@
1186
1235
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1187
1236
  }
1188
1237
  },
1238
+ "node_modules/hookable": {
1239
+ "version": "5.5.3",
1240
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
1241
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
1242
+ "license": "MIT"
1243
+ },
1244
+ "node_modules/is-what": {
1245
+ "version": "5.5.0",
1246
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
1247
+ "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
1248
+ "license": "MIT",
1249
+ "engines": {
1250
+ "node": ">=18"
1251
+ },
1252
+ "funding": {
1253
+ "url": "https://github.com/sponsors/mesqueeb"
1254
+ }
1255
+ },
1189
1256
  "node_modules/magic-string": {
1190
1257
  "version": "0.30.21",
1191
1258
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1195,6 +1262,12 @@
1195
1262
  "@jridgewell/sourcemap-codec": "^1.5.5"
1196
1263
  }
1197
1264
  },
1265
+ "node_modules/mitt": {
1266
+ "version": "3.0.1",
1267
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
1268
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
1269
+ "license": "MIT"
1270
+ },
1198
1271
  "node_modules/muggle-string": {
1199
1272
  "version": "0.4.1",
1200
1273
  "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -1227,6 +1300,12 @@
1227
1300
  "dev": true,
1228
1301
  "license": "MIT"
1229
1302
  },
1303
+ "node_modules/perfect-debounce": {
1304
+ "version": "1.0.0",
1305
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
1306
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
1307
+ "license": "MIT"
1308
+ },
1230
1309
  "node_modules/picocolors": {
1231
1310
  "version": "1.1.1",
1232
1311
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1247,20 +1326,19 @@
1247
1326
  }
1248
1327
  },
1249
1328
  "node_modules/pinia": {
1250
- "version": "2.3.1",
1251
- "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
1252
- "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
1329
+ "version": "3.0.4",
1330
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
1331
+ "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
1253
1332
  "license": "MIT",
1254
1333
  "dependencies": {
1255
- "@vue/devtools-api": "^6.6.3",
1256
- "vue-demi": "^0.14.10"
1334
+ "@vue/devtools-api": "^7.7.7"
1257
1335
  },
1258
1336
  "funding": {
1259
1337
  "url": "https://github.com/sponsors/posva"
1260
1338
  },
1261
1339
  "peerDependencies": {
1262
- "typescript": ">=4.4.4",
1263
- "vue": "^2.7.0 || ^3.5.11"
1340
+ "typescript": ">=4.5.0",
1341
+ "vue": "^3.5.11"
1264
1342
  },
1265
1343
  "peerDependenciesMeta": {
1266
1344
  "typescript": {
@@ -1268,6 +1346,15 @@
1268
1346
  }
1269
1347
  }
1270
1348
  },
1349
+ "node_modules/pinia/node_modules/@vue/devtools-api": {
1350
+ "version": "7.7.9",
1351
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
1352
+ "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
1353
+ "license": "MIT",
1354
+ "dependencies": {
1355
+ "@vue/devtools-kit": "^7.7.9"
1356
+ }
1357
+ },
1271
1358
  "node_modules/playwright": {
1272
1359
  "version": "1.57.0",
1273
1360
  "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
@@ -1328,6 +1415,12 @@
1328
1415
  "node": "^10 || ^12 || >=14"
1329
1416
  }
1330
1417
  },
1418
+ "node_modules/rfdc": {
1419
+ "version": "1.4.1",
1420
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
1421
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
1422
+ "license": "MIT"
1423
+ },
1331
1424
  "node_modules/rollup": {
1332
1425
  "version": "4.54.0",
1333
1426
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
@@ -1379,6 +1472,27 @@
1379
1472
  "node": ">=0.10.0"
1380
1473
  }
1381
1474
  },
1475
+ "node_modules/speakingurl": {
1476
+ "version": "14.0.1",
1477
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
1478
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
1479
+ "license": "BSD-3-Clause",
1480
+ "engines": {
1481
+ "node": ">=0.10.0"
1482
+ }
1483
+ },
1484
+ "node_modules/superjson": {
1485
+ "version": "2.2.6",
1486
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
1487
+ "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
1488
+ "license": "MIT",
1489
+ "dependencies": {
1490
+ "copy-anything": "^4"
1491
+ },
1492
+ "engines": {
1493
+ "node": ">=16"
1494
+ }
1495
+ },
1382
1496
  "node_modules/tinyglobby": {
1383
1497
  "version": "0.2.15",
1384
1498
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -1528,32 +1642,6 @@
1528
1642
  }
1529
1643
  }
1530
1644
  },
1531
- "node_modules/vue-demi": {
1532
- "version": "0.14.10",
1533
- "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
1534
- "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
1535
- "hasInstallScript": true,
1536
- "license": "MIT",
1537
- "bin": {
1538
- "vue-demi-fix": "bin/vue-demi-fix.js",
1539
- "vue-demi-switch": "bin/vue-demi-switch.js"
1540
- },
1541
- "engines": {
1542
- "node": ">=12"
1543
- },
1544
- "funding": {
1545
- "url": "https://github.com/sponsors/antfu"
1546
- },
1547
- "peerDependencies": {
1548
- "@vue/composition-api": "^1.0.0-rc.1",
1549
- "vue": "^3.0.0-0 || ^2.6.0"
1550
- },
1551
- "peerDependenciesMeta": {
1552
- "@vue/composition-api": {
1553
- "optional": true
1554
- }
1555
- }
1556
- },
1557
1645
  "node_modules/vue-router": {
1558
1646
  "version": "4.6.4",
1559
1647
  "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "stop": "npx kill-port 3333",
7
+ "stop": "lsof -ti :3333 2>/dev/null | xargs -r kill -9 || echo 'Port 3333 is free'",
8
8
  "dev": "npm run stop && vite",
9
9
  "build": "vue-tsc && vite build",
10
10
  "preview": "vite preview",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "@een/live-video-web-sdk": "^1.10.2",
16
16
  "een-api-toolkit": "file:../..",
17
- "pinia": "^2.1.7",
17
+ "pinia": "^3.0.4",
18
18
  "vue": "^3.4.0",
19
19
  "vue-router": "^4.2.0"
20
20
  },
@@ -1,18 +1,39 @@
1
1
  import { defineConfig, devices } from '@playwright/test'
2
+ import dotenv from 'dotenv'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ // Load .env files: parent .env first, then local .env overrides parent values
9
+ dotenv.config({ path: path.resolve(__dirname, '../../.env') })
10
+ dotenv.config({ path: path.resolve(__dirname, '.env'), override: true })
11
+
12
+ const redirectUri = process.env.VITE_REDIRECT_URI || 'http://127.0.0.1:3333'
13
+ if (!redirectUri.startsWith('http://127.0.0.1:') && !redirectUri.startsWith('http://localhost:')) {
14
+ throw new Error('VITE_REDIRECT_URI must use localhost or 127.0.0.1 for security')
15
+ }
16
+ const baseURL = redirectUri
17
+
18
+ // Export for use in test files
19
+ export { baseURL }
2
20
 
3
21
  export default defineConfig({
4
22
  testDir: './e2e',
23
+ testMatch: '**/*.spec.ts',
5
24
  fullyParallel: false,
6
25
  forbidOnly: !!process.env.CI,
7
- retries: process.env.CI ? 2 : 0,
26
+ retries: 0,
27
+ maxFailures: 1,
8
28
  workers: 1,
9
- reporter: 'html',
10
- timeout: 60000,
29
+ reporter: [['html', { open: 'never' }]],
30
+ timeout: 30000,
11
31
  use: {
12
- baseURL: 'http://127.0.0.1:3333',
32
+ baseURL,
13
33
  trace: 'on-first-retry',
14
- screenshot: 'only-on-failure'
34
+ video: 'retain-on-failure'
15
35
  },
36
+ outputDir: './e2e-results/',
16
37
  projects: [
17
38
  {
18
39
  name: 'chromium',
@@ -21,8 +42,8 @@ export default defineConfig({
21
42
  ],
22
43
  webServer: {
23
44
  command: 'npm run dev',
24
- url: 'http://127.0.0.1:3333',
45
+ url: baseURL,
25
46
  reuseExistingServer: !process.env.CI,
26
- timeout: 120000
47
+ timeout: 30000
27
48
  }
28
49
  })
@@ -0,0 +1,187 @@
1
+ # EEN API Toolkit - Vue Media Example
2
+
3
+ A Vue 3 example demonstrating how to fetch live and recorded images from EEN cameras using the een-api-toolkit.
4
+
5
+ ![Media Screenshot](media-screenshot.png)
6
+
7
+ ## Features Demonstrated
8
+
9
+ - OAuth authentication flow (login, callback, logout)
10
+ - Protected routes with navigation guards
11
+ - `getCameras()` function for listing cameras
12
+ - `getLiveImage()` function for fetching live preview images
13
+ - `getRecordedImage()` function for fetching recorded images with navigation
14
+ - Camera selection with persistence across pages
15
+ - Auto-refresh functionality for live images
16
+ - Time-based navigation for recorded images (prev/next)
17
+ - Image timestamp display
18
+
19
+ ## APIs Used
20
+
21
+ - `getCameras()` - List available cameras
22
+ - `getLiveImage()` - Fetch live preview image as base64
23
+ - `getRecordedImage()` - Fetch recorded image at specific timestamp
24
+ - `useAuthStore()` - Authentication state management
25
+ - `initEenToolkit()` - Toolkit initialization
26
+
27
+ ## Setup
28
+
29
+ ### Prerequisites
30
+
31
+ 1. **Start the OAuth proxy** (required for authentication):
32
+
33
+ The OAuth proxy is a separate project that handles token management securely.
34
+ Clone and run it from: https://github.com/klaushofrichter/een-oauth-proxy
35
+
36
+ ```bash
37
+ # In a separate terminal, from the een-oauth-proxy directory
38
+ npm install
39
+ npm run dev
40
+ ```
41
+
42
+ The proxy should be running at `http://localhost:8787`.
43
+
44
+ ### Example Setup
45
+
46
+ All commands below should be run from this example directory (`examples/vue-media/`):
47
+
48
+ 2. Copy the environment file:
49
+ ```bash
50
+ # From examples/vue-media/
51
+ cp .env.example .env
52
+ ```
53
+
54
+ 3. Edit `.env` with your EEN credentials:
55
+ ```env
56
+ VITE_EEN_CLIENT_ID=your-client-id
57
+ VITE_PROXY_URL=http://localhost:8787
58
+ # DO NOT change the redirect URI - EEN IDP only permits this URL
59
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
60
+ ```
61
+
62
+ 4. Install dependencies and start:
63
+ ```bash
64
+ # From examples/vue-media/
65
+ npm install
66
+ npm run dev
67
+ ```
68
+
69
+ 5. Open http://127.0.0.1:3333 in your browser.
70
+
71
+ **Important:** The EEN Identity Provider only permits `http://127.0.0.1:3333` as the OAuth redirect URI. Do not use `localhost` or other ports.
72
+
73
+ **Note:** Development and testing was done on macOS. The `npm run stop` command uses `lsof`, which is not available on Windows. Windows users should manually stop any process on port 3333 or use `npx kill-port 3333` instead.
74
+
75
+ ## Project Structure
76
+
77
+ ```
78
+ src/
79
+ ├── main.ts # App entry, toolkit initialization
80
+ ├── App.vue # Root component with navigation
81
+ ├── router/
82
+ │ └── index.ts # Vue Router with auth guards
83
+ └── views/
84
+ ├── Home.vue # Home page with login prompt
85
+ ├── Login.vue # OAuth login redirect
86
+ ├── Callback.vue # OAuth callback handler
87
+ ├── LiveCamera.vue # Live image viewer with auto-refresh
88
+ ├── RecordedImage.vue # Recorded image viewer with navigation
89
+ └── Logout.vue # Logout handler
90
+ ```
91
+
92
+ ## Key Code Examples
93
+
94
+ ### Fetching Live Images (LiveCamera.vue)
95
+
96
+ ```typescript
97
+ import { getLiveImage, type LiveImageParams } from 'een-api-toolkit'
98
+
99
+ async function fetchLiveImage() {
100
+ const result = await getLiveImage(selectedCameraId.value, {
101
+ type: 'preview'
102
+ })
103
+
104
+ if (result.error) {
105
+ error.value = result.error.message
106
+ } else {
107
+ imageData.value = result.data.image
108
+ timestamp.value = result.data.timestamp
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### Auto-Refresh for Live Images
114
+
115
+ ```typescript
116
+ let refreshInterval: number | null = null
117
+
118
+ function startAutoRefresh() {
119
+ refreshInterval = window.setInterval(() => {
120
+ fetchLiveImage()
121
+ }, 2000) // Refresh every 2 seconds
122
+ }
123
+
124
+ function stopAutoRefresh() {
125
+ if (refreshInterval) {
126
+ clearInterval(refreshInterval)
127
+ refreshInterval = null
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### Fetching Recorded Images (RecordedImage.vue)
133
+
134
+ ```typescript
135
+ import { getRecordedImage, type RecordedImageParams } from 'een-api-toolkit'
136
+
137
+ async function fetchRecordedImage() {
138
+ const result = await getRecordedImage(selectedCameraId.value, {
139
+ timestamp__gte: selectedTimestamp.value,
140
+ type: 'preview'
141
+ })
142
+
143
+ if (result.error) {
144
+ error.value = result.error.message
145
+ } else {
146
+ imageData.value = result.data.image
147
+ actualTimestamp.value = result.data.timestamp
148
+ prevToken.value = result.data.prevToken
149
+ nextToken.value = result.data.nextToken
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Navigating Recorded Images
155
+
156
+ ```typescript
157
+ async function navigateNext() {
158
+ if (!nextToken.value) return
159
+
160
+ const result = await getRecordedImage(selectedCameraId.value, {
161
+ next: nextToken.value,
162
+ type: 'preview'
163
+ })
164
+
165
+ if (!result.error) {
166
+ imageData.value = result.data.image
167
+ actualTimestamp.value = result.data.timestamp
168
+ prevToken.value = result.data.prevToken
169
+ nextToken.value = result.data.nextToken
170
+ }
171
+ }
172
+ ```
173
+
174
+ ### Displaying Images
175
+
176
+ ```vue
177
+ <template>
178
+ <img
179
+ v-if="imageData"
180
+ :src="`data:image/jpeg;base64,${imageData}`"
181
+ alt="Camera image"
182
+ />
183
+ <p v-if="timestamp">
184
+ Timestamp: {{ new Date(timestamp).toLocaleString() }}
185
+ </p>
186
+ </template>
187
+ ```