biometry-sdk 1.1.1 → 1.2.1

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/README.md CHANGED
@@ -98,7 +98,76 @@ Custom slots allow you to style and customize UI elements, loading, success, and
98
98
  ```
99
99
 
100
100
  ### Process Video Component
101
- Coming soon...
101
+ The Process Video component allows developers to record, upload, and process video directly within their applications through Biometry services to check liveness and authorize user.
102
+
103
+ ### Integration
104
+ **Option 1: Install via npm**
105
+
106
+ 1. To include the component in your project, install the biometry-sdk package:
107
+ ```bash
108
+ npm install biometry-sdk
109
+ ```
110
+
111
+ 2. After installation, import the component into your project:
112
+ ```javascript
113
+ // index.js
114
+ import './node_modules/biometry-sdk/dist/components/process-video.js';
115
+ ```
116
+
117
+ 3. Include the component in your HTML:
118
+ ```html
119
+ <script type="module" src="./index.js"></script>
120
+ <process-video ...></process-video>
121
+ ```
122
+
123
+ **Option 2: Import directly via CDN**
124
+
125
+ You can skip the npm installation and include the component directly in your HTML:
126
+ ```html
127
+ <script type="module" src="https://cdn.jsdelivr.net/npm/biometry-sdk/dist/components/process-video.js"></script>
128
+ <process-video ...></process-video>
129
+ ```
130
+
131
+ ### Usage
132
+
133
+ **Basic Usage**
134
+ ```html
135
+ <process-video
136
+ api-key="your-api-key"
137
+ user-fullname="Lionel Messi"
138
+ ></process-video>
139
+ ```
140
+
141
+ **Advanced Usage**
142
+ ```html
143
+ <process-video
144
+ api-key="eyJhb...apikey"
145
+ user-fullname="John Doe Uulu"
146
+ >
147
+ <!-- Custom video element -->
148
+ <video slot="video" muted playsinline style="border-radius: 1rem;"></video>
149
+
150
+ <!-- Custom buttons -->
151
+ <button slot="record-button">Custom Record</button>
152
+ <button slot="stop-button">Custom Stop</button>
153
+
154
+ <!-- Custom file input -->
155
+ <input slot="file-input" type="file" accept="video/*" />
156
+
157
+ <!-- Custom submit button -->
158
+ <button slot="submit-button">Custom Submit</button>
159
+
160
+ <!-- Custom messages -->
161
+ <div slot="loading">Processing...</div>
162
+ <div slot="error">An error occurred. Please try again.</div>
163
+ <div slot="success">Video submitted successfully!</div>
164
+ </process-video>
165
+ ```
166
+
167
+ **Notes**
168
+ - By default, all elements are functional without customization. Replace slots only if customization is required.
169
+
170
+ - To regenerate the video preview or handle custom actions, use JavaScript to interact with the provided slots or the component's public methods.
102
171
 
103
172
  ## License
104
173
 
@@ -0,0 +1,51 @@
1
+ export declare class ProcessVideoComponent extends HTMLElement {
2
+ private sdk;
3
+ private apiKey;
4
+ private phrase;
5
+ private previewStream;
6
+ private recordedChunks;
7
+ private mediaRecorder;
8
+ private videoFile;
9
+ private timerInterval;
10
+ private recordingTimeout;
11
+ private videoElement;
12
+ private fileInput;
13
+ private recordButton;
14
+ private stopButton;
15
+ private submitButton;
16
+ private errorState;
17
+ private timeLimit;
18
+ constructor();
19
+ private initializeSDK;
20
+ connectedCallback(): void;
21
+ disconnectedCallback(): void;
22
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
23
+ private generateDefaultPhrase;
24
+ initializeUI(): void;
25
+ private attachSlotListeners;
26
+ private getSlotElement;
27
+ replaceSlotContent(slotName: string, content: string | HTMLElement): void;
28
+ removeSlotListener(slotName: string, event: string, callback: EventListener): void;
29
+ private toggleState;
30
+ private convertPhraseToWords;
31
+ private setupPreview;
32
+ private startTimer;
33
+ private stopTimer;
34
+ startRecording(): Promise<void>;
35
+ stopRecording(): void;
36
+ private handleFileUpload;
37
+ handleSubmit(): Promise<void>;
38
+ static get observedAttributes(): string[];
39
+ get userFullname(): string | null;
40
+ set userFullname(value: string | null);
41
+ get isRecording(): boolean;
42
+ get currentPhrase(): string;
43
+ get videoDuration(): number | null;
44
+ get currentFile(): File | null;
45
+ get currentStream(): MediaStream | null;
46
+ set sdkInstance(newSdk: any);
47
+ get videoElementRef(): HTMLVideoElement;
48
+ get fileInputRef(): HTMLInputElement;
49
+ get recordingTimeLimit(): number;
50
+ set recordingTimeLimit(value: number);
51
+ }
@@ -0,0 +1,452 @@
1
+ import { BiometrySDK } from "../sdk.js";
2
+ export class ProcessVideoComponent extends HTMLElement {
3
+ constructor() {
4
+ super();
5
+ this.apiKey = null;
6
+ this.previewStream = null;
7
+ this.recordedChunks = [];
8
+ this.mediaRecorder = null;
9
+ this.videoFile = null;
10
+ this.timerInterval = null;
11
+ this.recordingTimeout = null;
12
+ this.errorState = null;
13
+ this.timeLimit = 30;
14
+ this.phrase = this.generateDefaultPhrase();
15
+ // Attach shadow DOM and initialize UI
16
+ this.attachShadow({ mode: 'open' });
17
+ this.apiKey = this.getAttribute('api-key');
18
+ this.initializeSDK();
19
+ this.initializeUI();
20
+ }
21
+ initializeSDK() {
22
+ if (this.apiKey) {
23
+ this.sdk = new BiometrySDK(this.apiKey);
24
+ }
25
+ else {
26
+ this.toggleState('error');
27
+ console.error('API key is required to initialize the SDK.');
28
+ }
29
+ }
30
+ connectedCallback() {
31
+ if (this.apiKey) {
32
+ this.initializeSDK();
33
+ }
34
+ else {
35
+ console.error('API key is required.');
36
+ }
37
+ }
38
+ disconnectedCallback() {
39
+ this.stopRecording();
40
+ if (this.previewStream) {
41
+ this.previewStream.getTracks().forEach(track => track.stop());
42
+ }
43
+ }
44
+ attributeChangedCallback(name, oldValue, newValue) {
45
+ if (name === 'api-key' && newValue !== oldValue) {
46
+ this.apiKey = newValue;
47
+ this.initializeSDK();
48
+ }
49
+ }
50
+ generateDefaultPhrase() {
51
+ return Math.random().toString().slice(2, 10); // 8-digit random phrase
52
+ }
53
+ initializeUI() {
54
+ const phraseDisplay = this.phrase
55
+ .split("")
56
+ .map((digit) => `<span class="digit">${digit}</span>`)
57
+ .join(" ");
58
+ this.shadowRoot.innerHTML = `
59
+ <style>
60
+ :host {
61
+ display: block;
62
+ font-family: Arial, sans-serif;
63
+ --primary-color: #007bff;
64
+ --secondary-color: #6c757d;
65
+ --button-bg: var(--primary-color);
66
+ --button-text-color: #fff;
67
+ --input-border-color: var(--secondary-color);
68
+ --input-focus-border-color: var(--primary-color);
69
+ --spacing: 16px;
70
+ --button-padding: 10px 20px;
71
+ --border-radius: 4px;
72
+ }
73
+
74
+ .container {
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: center;
78
+ justify-content: center;
79
+ gap: var(--spacing);
80
+ padding: var(--spacing);
81
+ max-width: 500px;
82
+ margin: 0 auto;
83
+ text-align: center;
84
+ }
85
+
86
+ .video-wrapper {
87
+ position: relative;
88
+ display: inline-block;
89
+ }
90
+
91
+ #timer-overlay {
92
+ position: absolute;
93
+ top: 10px;
94
+ left: 10px;
95
+ background-color: rgba(0, 0, 0, 0.7);
96
+ color: white;
97
+ padding: 5px 10px;
98
+ border-radius: 4px;
99
+ font-family: Arial, sans-serif;
100
+ font-size: 14px;
101
+ }
102
+
103
+ video {
104
+ max-width: 100%;
105
+ border-radius: var(--border-radius);
106
+ }
107
+
108
+ input[type="text"], input[type="file"] {
109
+ padding: var(--button-padding);
110
+ border: 1px solid var(--input-border-color);
111
+ border-radius: var(--border-radius);
112
+ width: 100%;
113
+ max-width: 100%;
114
+ }
115
+
116
+ input[type="text"]:focus, input[type="file"]:focus {
117
+ outline: none;
118
+ border-color: var(--input-focus-border-color);
119
+ }
120
+
121
+ .hidden {
122
+ display: none;
123
+ }
124
+
125
+ .phrase-display {
126
+ font-size: 24px;
127
+ font-weight: bold;
128
+ display: flex;
129
+ gap: 8px;
130
+ justify-content: center;
131
+ }
132
+ .digit {
133
+ padding: 4px;
134
+ border: 1px solid #ccc;
135
+ border-radius: 4px;
136
+ text-align: center;
137
+ width: 24px;
138
+ }
139
+
140
+ </style>
141
+ <div class="container">
142
+ <slot name="video">
143
+ <div class="video-wrapper">
144
+ <video id="video-preview" muted autoplay></video>
145
+ <div id="timer-overlay" class="hidden">00:00</div>
146
+ </div>
147
+ </slot>
148
+ <slot name="phrase-display">
149
+ <div class="phrase-display">
150
+ ${phraseDisplay}
151
+ </div>
152
+ </slot>
153
+ <slot name="record-button">
154
+ <button id="record-button">Start Recording</button>
155
+ </slot>
156
+ <slot name="stop-button">
157
+ <button id="stop-button" disabled>Stop Recording</button>
158
+ </slot>
159
+ <slot name="file-input">
160
+ <input type="file" accept="video/*" id="file-input" />
161
+ </slot>
162
+ <slot name="submit-button">
163
+ <button id="submit-button">Submit Video</button>
164
+ </slot>
165
+ <slot name="loading">
166
+ <div class="message">Loading...</div>
167
+ </slot>
168
+ <slot name="error">
169
+ <div class="message error">An error occurred</div>
170
+ </slot>
171
+ <slot name="success">
172
+ <div class="message success">Video submitted successfully!</div>
173
+ </slot>
174
+ </div>
175
+ `;
176
+ this.attachSlotListeners();
177
+ this.setupPreview();
178
+ this.toggleState(null);
179
+ }
180
+ attachSlotListeners() {
181
+ const videoSlot = this.shadowRoot.querySelector('slot[name="video"]');
182
+ const recordButtonSlot = this.shadowRoot.querySelector('slot[name="record-button"]');
183
+ const stopButtonSlot = this.shadowRoot.querySelector('slot[name="stop-button"]');
184
+ const fileInputSlot = this.shadowRoot.querySelector('slot[name="file-input"]');
185
+ const submitButtonSlot = this.shadowRoot.querySelector('slot[name="submit-button"]');
186
+ this.videoElement = this.getSlotElement(videoSlot, '#video-preview', HTMLVideoElement);
187
+ this.recordButton = this.getSlotElement(recordButtonSlot, '#record-button', HTMLButtonElement);
188
+ this.stopButton = this.getSlotElement(stopButtonSlot, '#stop-button', HTMLButtonElement);
189
+ this.fileInput = this.getSlotElement(fileInputSlot, '#file-input', HTMLInputElement);
190
+ this.submitButton = this.getSlotElement(submitButtonSlot, '#submit-button', HTMLButtonElement);
191
+ if (this.fileInput) {
192
+ this.fileInput.addEventListener('change', (e) => this.handleFileUpload(e));
193
+ }
194
+ if (this.recordButton) {
195
+ this.recordButton.addEventListener("click", () => this.startRecording());
196
+ }
197
+ if (this.stopButton) {
198
+ this.stopButton.addEventListener("click", () => this.stopRecording());
199
+ }
200
+ if (this.submitButton) {
201
+ this.submitButton.addEventListener("click", () => this.handleSubmit());
202
+ }
203
+ }
204
+ getSlotElement(slot, fallbackSelector, elementType) {
205
+ const assignedElements = slot.assignedElements();
206
+ return (assignedElements.length > 0 ? assignedElements[0] : null) || this.shadowRoot.querySelector(fallbackSelector);
207
+ }
208
+ replaceSlotContent(slotName, content) {
209
+ const slot = this.shadowRoot.querySelector(`slot[name="${slotName}"]`);
210
+ if (slot) {
211
+ if (typeof content === 'string') {
212
+ slot.innerHTML = content;
213
+ }
214
+ else if (content instanceof HTMLElement) {
215
+ slot.innerHTML = '';
216
+ slot.appendChild(content);
217
+ }
218
+ }
219
+ }
220
+ removeSlotListener(slotName, event, callback) {
221
+ const slot = this.shadowRoot.querySelector(`slot[name="${slotName}"]`);
222
+ if (slot) {
223
+ const assignedNodes = slot.assignedElements();
224
+ assignedNodes.forEach((node) => {
225
+ node.removeEventListener(event, callback);
226
+ });
227
+ }
228
+ }
229
+ toggleState(state) {
230
+ const states = ['loading', 'success', 'error'];
231
+ states.forEach((slotName) => {
232
+ const slot = this.shadowRoot.querySelector(`slot[name="${slotName}"]`);
233
+ if (slot) {
234
+ slot.style.display = slotName === state ? 'block' : 'none';
235
+ }
236
+ });
237
+ }
238
+ convertPhraseToWords(phrase) {
239
+ const digitWords = [
240
+ "zero", "one", "two", "three", "four",
241
+ "five", "six", "seven", "eight", "nine"
242
+ ];
243
+ return phrase
244
+ .split("")
245
+ .map((digit) => digitWords[parseInt(digit, 10)])
246
+ .join(" ");
247
+ }
248
+ async setupPreview() {
249
+ try {
250
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
251
+ this.previewStream = stream;
252
+ this.videoElement.srcObject = stream;
253
+ this.videoElement.controls = false;
254
+ this.videoElement.play();
255
+ }
256
+ catch (error) {
257
+ this.toggleState('error');
258
+ console.error('Error setting up video preview:', error);
259
+ }
260
+ }
261
+ async startTimer() {
262
+ const timerOverlay = this.shadowRoot.querySelector('#timer-overlay');
263
+ timerOverlay.textContent = '00:00';
264
+ timerOverlay.classList.remove('hidden');
265
+ let seconds = 0;
266
+ this.timerInterval = setInterval(() => {
267
+ seconds++;
268
+ const minutes = Math.floor(seconds / 60);
269
+ const remainingSeconds = seconds % 60;
270
+ timerOverlay.textContent = `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
271
+ }, 1000);
272
+ this.recordingTimeout = setTimeout(() => {
273
+ this.stopRecording();
274
+ }, this.timeLimit * 1000);
275
+ }
276
+ async stopTimer() {
277
+ if (this.recordingTimeout) {
278
+ clearTimeout(this.recordingTimeout);
279
+ }
280
+ if (this.timerInterval) {
281
+ clearInterval(this.timerInterval);
282
+ this.timerInterval = null;
283
+ }
284
+ const timerOverlay = this.shadowRoot.querySelector('#timer-overlay');
285
+ timerOverlay.classList.add('hidden');
286
+ }
287
+ async startRecording() {
288
+ if (!window.MediaRecorder) {
289
+ console.error('MediaRecorder API is not supported in this browser');
290
+ return;
291
+ }
292
+ try {
293
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
294
+ console.log('Recording already in progress.');
295
+ return;
296
+ }
297
+ if (!this.previewStream) {
298
+ console.log('Initializing preview stream...');
299
+ this.previewStream = await navigator.mediaDevices.getUserMedia({
300
+ video: true,
301
+ audio: true,
302
+ });
303
+ }
304
+ this.videoElement.muted = true;
305
+ this.videoElement.srcObject = this.previewStream;
306
+ this.videoElement.currentTime = 0;
307
+ await this.videoElement.play();
308
+ this.mediaRecorder = new MediaRecorder(this.previewStream);
309
+ this.recordedChunks = [];
310
+ this.mediaRecorder.ondataavailable = (event) => {
311
+ if (event.data.size > 0) {
312
+ this.recordedChunks.push(event.data);
313
+ }
314
+ };
315
+ this.mediaRecorder.onstop = () => {
316
+ const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
317
+ const videoURL = URL.createObjectURL(blob);
318
+ this.videoFile = new File([blob], 'recorded_video.webm', { type: 'video/webm' });
319
+ this.videoElement.src = videoURL;
320
+ this.videoElement.controls = true;
321
+ this.videoElement.play();
322
+ this.videoElement.muted = false;
323
+ this.stopTimer();
324
+ };
325
+ this.mediaRecorder.start();
326
+ this.startTimer();
327
+ this.recordButton.disabled = true;
328
+ this.stopButton.disabled = false;
329
+ this.videoElement.controls = false;
330
+ }
331
+ catch (error) {
332
+ console.error('Error starting video recording:', error);
333
+ }
334
+ }
335
+ stopRecording() {
336
+ try {
337
+ if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') {
338
+ console.log('No recording in progress.');
339
+ return;
340
+ }
341
+ this.mediaRecorder.stop();
342
+ if (this.previewStream) {
343
+ this.previewStream.getTracks().forEach(track => track.stop());
344
+ }
345
+ this.videoElement.srcObject = null;
346
+ this.videoElement.src = '';
347
+ this.videoElement.controls = false;
348
+ this.recordButton.disabled = false;
349
+ this.stopButton.disabled = true;
350
+ this.mediaRecorder = null;
351
+ this.recordedChunks = [];
352
+ this.previewStream = null;
353
+ }
354
+ catch (error) {
355
+ console.error('Error stopping video recording:', error);
356
+ }
357
+ }
358
+ handleFileUpload(event) {
359
+ var _a, _b;
360
+ const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
361
+ if ((_b = file === null || file === void 0 ? void 0 : file.type) === null || _b === void 0 ? void 0 : _b.startsWith('video/')) {
362
+ if (file.size > 100 * 1024 * 1024) { // 100MB limit
363
+ this.toggleState('error');
364
+ console.error('File size exceeds limit of 100MB');
365
+ return;
366
+ }
367
+ this.videoFile = file;
368
+ this.videoElement.src = URL.createObjectURL(file);
369
+ this.videoElement.play();
370
+ }
371
+ else {
372
+ this.toggleState('error');
373
+ console.error('Please select a valid video file.');
374
+ }
375
+ }
376
+ async handleSubmit() {
377
+ if (!this.videoFile) {
378
+ this.toggleState('error');
379
+ console.error('No video file to submit.');
380
+ return;
381
+ }
382
+ if (!this.apiKey || !this.userFullname) {
383
+ this.toggleState('error');
384
+ console.error('API key and user fullname must be provided.');
385
+ return;
386
+ }
387
+ this.toggleState('loading');
388
+ try {
389
+ const phraseInWords = this.convertPhraseToWords(this.phrase);
390
+ const result = await this.sdk.processVideo(this.videoFile, phraseInWords, this.userFullname);
391
+ console.log('Response from processVideo:', result);
392
+ this.toggleState('success');
393
+ }
394
+ catch (error) {
395
+ this.toggleState('error');
396
+ console.error('Error submitting video:', error);
397
+ }
398
+ }
399
+ static get observedAttributes() {
400
+ return ['api-key', 'user-fullname'];
401
+ }
402
+ get userFullname() {
403
+ return this.getAttribute('user-fullname');
404
+ }
405
+ set userFullname(value) {
406
+ if (value) {
407
+ this.setAttribute('user-fullname', value);
408
+ }
409
+ else {
410
+ this.removeAttribute('user-fullname');
411
+ }
412
+ }
413
+ get isRecording() {
414
+ var _a;
415
+ return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) === 'recording';
416
+ }
417
+ get currentPhrase() {
418
+ return this.phrase;
419
+ }
420
+ get videoDuration() {
421
+ var _a;
422
+ return ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.duration) || null;
423
+ }
424
+ get currentFile() {
425
+ return this.videoFile;
426
+ }
427
+ get currentStream() {
428
+ return this.previewStream;
429
+ }
430
+ set sdkInstance(newSdk) {
431
+ this.sdk = newSdk;
432
+ }
433
+ get videoElementRef() {
434
+ return this.videoElement;
435
+ }
436
+ get fileInputRef() {
437
+ return this.fileInput;
438
+ }
439
+ get recordingTimeLimit() {
440
+ return this.timeLimit;
441
+ }
442
+ set recordingTimeLimit(value) {
443
+ this.timeLimit = value;
444
+ if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
445
+ if (this.recordingTimeout) {
446
+ clearTimeout(this.recordingTimeout);
447
+ }
448
+ this.recordingTimeout = setTimeout(() => this.stopRecording(), this.timeLimit * 1000);
449
+ }
450
+ }
451
+ }
452
+ customElements.define('process-video', ProcessVideoComponent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biometry-sdk",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [