diff --git a/packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts b/packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts
new file mode 100644
index 000000000000..e87df3fb908a
--- /dev/null
+++ b/packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts
@@ -0,0 +1,349 @@
+import { html } from "lit";
+import "@ui5/webcomponents-icons/dist/camera.js";
+import "../../src/BarcodeScannerDialog.js";
+import type BarcodeScannerDialog from "../../src/BarcodeScannerDialog.js";
+
+describe("BarcodeScannerDialog", () => {
+ let handleScanSuccess: (event: CustomEvent) => void;
+ let handleScanError: (event: CustomEvent) => void;
+
+ beforeEach(() => {
+ cy.mount(html`
+
+ Open Scanner Dialog
+
+
+
+
+
+ `);
+
+ cy.get("#dlgScan").as("dialog");
+ cy.get("#btnScan").as("button");
+ cy.get("@dialog")
+ .shadow()
+ .find(".ui5-barcode-scanner-dialog-video")
+ .as("videoElement");
+
+ // Add event listener to the button to open the dialog
+ cy.document().then(doc => {
+ const dlgScan = doc.querySelector("#dlgScan")!;
+ const btnScan = doc.querySelector("#btnScan")!;
+
+ btnScan.addEventListener("click", () => {
+ dlgScan.open = true;
+ });
+ });
+ });
+
+ afterEach(() => {
+ // Remove event listeners if they were added
+ cy.document().then(doc => {
+ const dlgScan = doc.querySelector("#dlgScan")!;
+ if (handleScanSuccess) {
+ dlgScan.removeEventListener("scan-success", handleScanSuccess as EventListener);
+ handleScanSuccess = undefined!;
+ }
+ if (handleScanError) {
+ dlgScan.removeEventListener("scan-error", handleScanError as EventListener);
+ handleScanError = undefined!;
+ }
+
+ // Clear the scan result and error
+ const scanResultElement = doc.querySelector("#scanResult")!;
+ const scanErrorElement = doc.querySelector("#scanError")!;
+ scanResultElement.textContent = "";
+ scanErrorElement.textContent = "";
+ });
+ });
+
+ it("should open and close the dialog", () => {
+ // Before clicking, the dialog should not be open
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("not.have.attr", "open");
+
+ // Click the button to open the dialog
+ cy.get("@button").realClick();
+
+ // Wait for the video to be ready
+ cy.get("@videoElement").should($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
+ });
+
+ // Assert that the dialog is open
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("have.attr", "open");
+
+ // Close the dialog using the close button
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog [slot=footer] ui5-button")
+ .realClick();
+
+ // Verify the dialog is closed
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("not.have.attr", "open");
+ });
+
+ it("closes barcode scanner dialog with escape key", () => {
+ // Click the button to open the dialog
+ cy.get("@button").realClick();
+
+ // Wait for the video to be ready
+ cy.get("@videoElement").should($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
+ });
+
+ // Assert that the dialog is open
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("have.attr", "open");
+
+ // Simulate pressing the Escape key
+ cy.get("@dialog").realPress("Escape");
+
+ // Verify the dialog is closed
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("not.have.attr", "open");
+ });
+
+ it("opens barcode scanner dialog and checks video stream", () => {
+ // Click the button to open the dialog
+ cy.get("@button").realClick();
+
+ // Wait for the video to be ready
+ cy.get("@videoElement").should($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
+ });
+
+ // Assert that the dialog is open
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("have.attr", "open");
+
+ // Check that video.srcObject is not null
+ cy.get("@videoElement").should($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ // eslint-disable-next-line no-unused-expressions
+ expect(videoEl.srcObject, "Video srcObject should not be null").to.not.be.null;
+ });
+
+ // Get the overlay canvas element
+ cy.get("@dialog")
+ .shadow()
+ .find(".ui5-barcode-scanner-dialog-overlay")
+ .as("overlayElement")
+ .should("be.visible");
+
+ // Check that the canvas dimensions match the video element's dimensions
+ cy.get("@videoElement").then($video => {
+ cy.get("@overlayElement").then($canvas => {
+ expect(
+ ($canvas[0] as HTMLCanvasElement).width,
+ "Canvas width should match video width",
+ ).to.equal($video[0].clientWidth);
+ expect(
+ ($canvas[0] as HTMLCanvasElement).height,
+ "Canvas height should match video height",
+ ).to.equal($video[0].clientHeight);
+ });
+ });
+ });
+
+ it("stops media tracks when the dialog is closed", () => {
+ // Click the button to open the dialog
+ cy.get("@button").realClick();
+
+ // Wait for the video to be ready
+ cy.get("@videoElement").should($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
+ });
+
+ // Assert that the dialog is open
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("have.attr", "open");
+
+ // Save the media stream
+ cy.get("@videoElement").then($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ cy.wrap(videoEl.srcObject).as("mediaStream");
+ });
+
+ // Close the dialog using the close button
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog [slot=footer] ui5-button")
+ .realClick();
+
+ // Verify the dialog is closed
+ cy.get("@dialog")
+ .shadow()
+ .find("ui5-dialog")
+ .should("not.have.attr", "open");
+
+ // Check that media tracks are stopped
+ cy.get("@mediaStream").then(currentSubject => {
+ const mediaStream = currentSubject as unknown as MediaStream;
+ const tracksStopped = mediaStream
+ .getTracks()
+ .every(track => track.readyState === "ended");
+ // eslint-disable-next-line no-unused-expressions
+ expect(tracksStopped, "All media tracks should be stopped").to.be.true;
+ });
+
+ // Check that video srcObject is cleared
+ cy.get("@videoElement").then($video => {
+ const videoEl = $video[0] as HTMLVideoElement;
+ // eslint-disable-next-line no-unused-expressions
+ expect(videoEl.srcObject, "Video srcObject should be null").to.be.null;
+ });
+ });
+
+ it("displays the busy indicator while loading", () => {
+ // Stub the _getUserPermission method to simulate delay
+ cy.get("@dialog").then($dialog => {
+ const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
+
+ const stub = cy
+ .stub(dlgScan, "_getUserPermission")
+ .callsFake(() => new Promise(() => { })); // Never resolves
+ cy.wrap(stub).as("getUserPermissionStub");
+ });
+
+ // Open the dialog
+ cy.get("@button").realClick();
+
+ // Check that the busy indicator is visible
+ cy.get("@dialog")
+ .shadow()
+ .find(".ui5-barcode-scanner-dialog-busy")
+ .should("be.visible");
+
+ // Restore the stub
+ cy.get("@getUserPermissionStub").then(stub => {
+ (stub as unknown as sinon.SinonStub).restore();
+ });
+ });
+
+ it("handles scan success event", () => {
+ // Define the event handler function
+ handleScanSuccess = (event: CustomEvent) => {
+ const detail = event.detail;
+ const scanResultElement = document.querySelector("#scanResult")!;
+ scanResultElement.textContent = detail.text;
+ };
+
+ // Get the scanResult element
+ cy.get("#scanResult").as("scanResult");
+
+ // Add event listener to display scan result
+ cy.get("@dialog").then($dialog => {
+ const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
+ dlgScan.addEventListener("scan-success", handleScanSuccess as EventListener);
+ });
+
+ // Open the dialog
+ cy.get("@button").realClick();
+
+ // Simulate scan success
+ cy.get("@dialog").then($dialog => {
+ const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
+
+ // Simulate the scan success
+ dlgScan.fireEvent("scan-success", {
+ text: "mocked-scan-result",
+ rawBytes: new Uint8Array(),
+ });
+ });
+
+ // Check that the scan result is displayed
+ cy.get("@scanResult").should("have.text", "mocked-scan-result");
+ });
+
+ it("handles scan error event", () => {
+ // Define the event handler function
+ handleScanError = (event: CustomEvent) => {
+ const detail = event.detail;
+ const scanErrorElement = document.querySelector("#scanError")!;
+ scanErrorElement.textContent = detail.message;
+ };
+
+ // Get the scanError element
+ cy.get("#scanError").as("scanError");
+
+ // Add event listener to display scan error
+ cy.get("@dialog").then($dialog => {
+ const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
+ dlgScan.addEventListener("scan-error", handleScanError as EventListener);
+ });
+
+ // Open the dialog
+ cy.get("@button").realClick();
+
+ // Simulate scan error
+ cy.get("@dialog").then($dialog => {
+ const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
+
+ // Simulate the scan error
+ dlgScan.fireEvent("scan-error", {
+ message: "mocked-scan-error",
+ });
+ });
+
+ // Check that the scan error is displayed
+ cy.get("@scanError").should("have.text", "mocked-scan-error");
+ });
+
+ it("handles permission denied error", () => {
+ // Define the event handler function
+ handleScanError = (event: CustomEvent) => {
+ const detail = event.detail;
+ const scanErrorElement = document.querySelector("#scanError")!;
+ scanErrorElement.textContent = detail.message;
+ };
+
+ // Get the scanError element
+ cy.get("#scanError").as("scanError");
+
+ // Stub getUserMedia to reject with "Permission denied"
+ cy.window().then(win => {
+ const stub = cy
+ .stub(win.navigator.mediaDevices, "getUserMedia")
+ .rejects(new DOMException("Permission denied", "NotAllowedError"));
+ cy.wrap(stub).as("getUserMediaStub");
+ });
+
+ // Add event listener to display scan error
+ cy.get("@dialog").then($dialog => {
+ const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
+ dlgScan.addEventListener("scan-error", handleScanError as EventListener);
+ });
+
+ // Open the dialog
+ cy.get("@button").realClick();
+
+ // Check that the scan error is displayed
+ cy.get("@scanError").should("contain.text", "Permission denied");
+
+ // Restore the stub
+ cy.get("@getUserMediaStub").then(stub => {
+ (stub as unknown as sinon.SinonStub).restore();
+ });
+ });
+});
diff --git a/packages/fiori/src/BarcodeScannerDialog.hbs b/packages/fiori/src/BarcodeScannerDialog.hbs
index 00f29115596b..a1c293ee9499 100644
--- a/packages/fiori/src/BarcodeScannerDialog.hbs
+++ b/packages/fiori/src/BarcodeScannerDialog.hbs
@@ -1,19 +1,11 @@
-
+
-
+
+
-
-
+
\ No newline at end of file
diff --git a/packages/fiori/src/BarcodeScannerDialog.ts b/packages/fiori/src/BarcodeScannerDialog.ts
index cedaa80e845f..8b6f61a97363 100644
--- a/packages/fiori/src/BarcodeScannerDialog.ts
+++ b/packages/fiori/src/BarcodeScannerDialog.ts
@@ -8,7 +8,9 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
+import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
import type { Result, Exception } from "@zxing/library/esm5/index.js";
+import type { Interval } from "@ui5/webcomponents-base/dist/types.js";
// eslint-disable-next-line import/no-extraneous-dependencies
import ZXing from "@ui5/webcomponents-fiori/dist/ssr-zxing.js";
@@ -86,6 +88,7 @@ type BarcodeScannerDialogScanErrorEventDetail = {
/**
* Fired when the user closes the component.
+ * @since 2.0.0
* @public
*/
@event("close", {
@@ -101,12 +104,12 @@ type BarcodeScannerDialogScanErrorEventDetail = {
@event("scan-success", {
detail: {
/**
- * @public
- */
+ * @public
+ */
text: { type: String },
/**
- * @public
- */
+ * @public
+ */
rawBytes: { type: Object },
},
bubbles: true,
@@ -120,8 +123,8 @@ type BarcodeScannerDialogScanErrorEventDetail = {
@event("scan-error", {
detail: {
/**
- * @public
- */
+ * @public
+ */
message: { type: String },
},
bubbles: true,
@@ -134,12 +137,12 @@ class BarcodeScannerDialog extends UI5Element {
* @public
* @default false
* @since 1.24.0
- */
+ */
@property({ type: Boolean })
open = false;
/**
- * Indicates whether a loading indicator should be displayed in the dialog.
+ * Indicates whether a loading indicator should be displayed while the scanner is loading.
* @default false
* @private
*/
@@ -147,59 +150,84 @@ class BarcodeScannerDialog extends UI5Element {
loading = false;
/**
- * Indicates whether the user has granted permissions to use the camera.
+ * Indicates whether the scanner is ready to scan.
* @default false
* @private
*/
@property({ type: Boolean, noAttribute: true })
- permissionsGranted = false;
+ isReadyToScan = false;
- _codeReader: InstanceType;
dialog?: Dialog;
@i18n("@ui5/webcomponents-fiori")
static i18nBundle: I18nBundle;
+ _codeReader: InstanceType;
+ _tempCanvas!: HTMLCanvasElement;
+ _scanInterval!: Interval | null;
+ _handleVideoPlayingBound: () => void;
+ _handleCaptureRegionBound: () => void;
constructor() {
super();
this._codeReader = new BrowserMultiFormatReader();
+ this._handleVideoPlayingBound = this._handleVideoPlaying.bind(this);
+ this._handleCaptureRegionBound = this._handleDrawCaptureRegion.bind(this);
}
- onAfterRendering() {
- if (this.open) {
- if (this.loading) {
- return;
- }
-
- if (!this._hasGetUserMedia()) {
- this.fireDecoratorEvent("scan-error", { message: "getUserMedia() is not supported by your browser" });
- return;
- }
-
- if (!this.permissionsGranted) {
- this.loading = true;
- }
-
- this._getUserPermission()
- .then(() => {
- this.permissionsGranted = true;
- })
- .catch(err => {
- this.fireDecoratorEvent("scan-error", { message: err });
- this.loading = false;
- });
- } else {
+ static async onDefine() {
+ BarcodeScannerDialog.i18nBundle = await getI18nBundle("@ui5/webcomponents-fiori");
+ }
+
+ async onAfterRendering() {
+ if (!this._hasGetUserMedia()) {
+ this.fireDecoratorEvent("scan-error", { message: "getUserMedia() is not supported by your browser" });
+ return;
+ }
+
+ if (!this.open || this.loading) {
+ return;
+ }
+
+ if (!this.isReadyToScan) {
+ this.loading = true;
+ }
+
+ const video = this._getVideoElement();
+ if (video.srcObject) {
+ return;
+ }
+
+ try {
+ const stream = await this._getUserPermission();
+ video.addEventListener("loadeddata", this._handleVideoPlayingBound);
+ video.srcObject = stream;
+ } catch (error) {
+ this.fireDecoratorEvent("scan-error", { message: (error as Error).message });
this.loading = false;
}
}
+ onEnterDOM() {
+ super.onEnterDOM();
+ window.addEventListener("resize", this._handleCaptureRegionBound);
+ }
+
+ onExitDOM() {
+ super.onExitDOM();
+ window.removeEventListener("resize", this._handleCaptureRegionBound);
+ }
+
get _open() {
- return this.open && this.permissionsGranted;
+ return this.open && this.isReadyToScan;
}
- /**
- * PRIVATE METHODS
- */
+ get _cancelButtonText() {
+ return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_CANCEL_BUTTON_TXT);
+ }
+
+ get _busyIndicatorText() {
+ return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_LOADING_TXT);
+ }
_hasGetUserMedia() {
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
@@ -213,48 +241,220 @@ class BarcodeScannerDialog extends UI5Element {
return this.shadowRoot!.querySelector(".ui5-barcode-scanner-dialog-video")!;
}
- _closeDialog() {
- this.open = false;
+ _getOverlayCanvasElement() {
+ return this.shadowRoot!.querySelector(".ui5-barcode-scanner-dialog-overlay")!;
}
- _fireCloseEvent() {
- this.open = false;
- this.fireDecoratorEvent("close");
- }
+ /**
+ * CALCULATIONS
+ *
+ * The following methods are used to calculate the capture region
+ * and draw it on the overlay canvas.
+ * The capture region is a square area in the center of the video element
+ * where the barcode scanning is performed.
+ * The region is defined as a proportion of the video element's dimensions.
+ * The overlay canvas is used to draw a semi-transparent black overlay
+ * over the video element and a red border around the capture region.
+ * The overlay canvas is updated on every frame to ensure the capture region is always visible.
+ * The capture region is used to crop the video frame and extract the barcode image.
+ * The extracted image is then processed by the zxing-js library to decode the barcode.
+ */
- _startReader() {
- this._decodeFromCamera();
+ _calculateCaptureRegion(clientWidth: number, clientHeight: number) {
+ // Define the maximum scan size as a proportion of the video element
+ const maxScanProportion = 0.66666667; // 2:3
+ // Calculate maximum square dimension based on video dimensions and max proportion
+ const maxScanDimension = Math.min(clientWidth, clientHeight) * maxScanProportion;
+ // Calculate offset to center the square scan region
+ const xOffset = (clientWidth - maxScanDimension) / 2;
+ const yOffset = (clientHeight - maxScanDimension) / 2;
+ // Calculate the width and height of the scan region
+ const scanWidth = Math.floor(maxScanDimension);
+ const scanHeight = Math.floor(maxScanDimension);
+
+ return {
+ scanHeight,
+ scanWidth,
+ xOffset,
+ yOffset,
+ };
}
- _resetReader() {
+ _drawCaptureRegion() {
const videoElement = this._getVideoElement();
- videoElement.pause();
- this._codeReader.reset();
+ const canvasElement = this._getOverlayCanvasElement();
+ const context = canvasElement.getContext("2d")!;
+
+ const videoClientWidth = videoElement.clientWidth;
+ const videoClientHeight = videoElement.clientHeight;
+
+ // Set canvas dimensions to match the video element's dimensions
+ canvasElement.width = videoClientWidth;
+ canvasElement.height = videoClientHeight;
+
+ // Clear the canvas
+ context.clearRect(0, 0, videoClientWidth, videoClientHeight);
+ // Calculate the capture region
+ const captureRegion = this._calculateCaptureRegion(videoClientWidth, videoClientHeight);
+
+ // Draw a semi-transparent black overlay over the video
+ context.fillStyle = "rgba(0, 0, 0, 0.5)";
+ context.fillRect(0, 0, videoClientWidth, videoClientHeight);
+ context.clearRect(captureRegion.xOffset, captureRegion.yOffset, captureRegion.scanWidth, captureRegion.scanHeight);
+
+ // Draw red border around the capture region
+ context.strokeStyle = "red";
+ context.lineWidth = 1;
+ context.strokeRect(captureRegion.xOffset, captureRegion.yOffset, captureRegion.scanWidth, captureRegion.scanHeight);
+
+ // Display the overlay
+ canvasElement.style.display = "block";
}
- _decodeFromCamera() {
- const videoElement = this._getVideoElement();
- this._codeReader.decodeFromVideoDevice(null, videoElement, (result: Result, err?: Exception) => {
- this.loading = false;
- if (result) {
- this.fireDecoratorEvent("scan-success",
- {
- text: result.getText(),
- rawBytes: result.getRawBytes(),
- });
- }
- if (err && !(err instanceof NotFoundException)) {
- this.fireDecoratorEvent("scan-error", { message: err.message });
- }
- }).catch((err: Error) => this.fireDecoratorEvent("scan-error", { message: err.message }));
+ _getTempCanvasElement() {
+ if (!this._tempCanvas) {
+ this._tempCanvas = document.createElement("canvas");
+ }
+ return this._tempCanvas;
}
- get _cancelButtonText() {
- return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_CANCEL_BUTTON_TXT);
+ _captureFrame() {
+ const video = this._getVideoElement();
+ const tempCanvas = this._getTempCanvasElement();
+ const context = tempCanvas.getContext("2d")!;
+
+ const videoWidth = video.videoWidth;
+ const videoHeight = video.videoHeight;
+ const clientWidth = video.clientWidth;
+ const clientHeight = video.clientHeight;
+
+ const captureRegion = this._calculateCaptureRegion(clientWidth, clientHeight);
+
+ // Calculate the ratio of videoSize to clientSize
+ const ratioX = videoWidth / clientWidth;
+ const ratioY = videoHeight / clientHeight;
+ const scale = Math.min(ratioX, ratioY);
+
+ // Calculate the scaled capture region
+ const scaledXOffset = captureRegion.xOffset * scale;
+ const scaledYOffset = captureRegion.yOffset * scale;
+ const scaledScanWidth = captureRegion.scanWidth * scale;
+ const scaledScanHeight = captureRegion.scanHeight * scale;
+
+ // Set canvas dimensions to match the capture region dimensions
+ tempCanvas.width = captureRegion.scanWidth;
+ tempCanvas.height = captureRegion.scanHeight;
+
+ // Clear the canvas
+ context.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
+
+ // Correct positioning if aspect ratios are different
+ const positionX = (videoWidth - clientWidth * scale) / 2;
+ const positionY = (videoHeight - clientHeight * scale) / 2;
+
+ // Calculate final source position considering the video element's offset
+ const finalXOffset = scaledXOffset + positionX;
+ const finalYOffset = scaledYOffset + positionY;
+
+ // Draw the portion of the video on the canvas
+ context.drawImage(
+ video,
+ finalXOffset, finalYOffset, scaledScanWidth, scaledScanHeight, // Source rectangle
+ 0, 0, tempCanvas.width, tempCanvas.height, // Destination rectangle
+ );
+
+ return tempCanvas;
}
- get _busyIndicatorText() {
- return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_LOADING_TXT);
+ /**
+ * HANDLERS
+ */
+
+ async _processFrame() {
+ try {
+ const canvas = this._captureFrame();
+ const dataUrl = canvas.toDataURL();
+ const result = await this._codeReader.decodeFromImageUrl(dataUrl);
+
+ this._handleScanSuccess(result);
+ } catch (error) {
+ this._handleScanError(error as Exception);
+ }
+ }
+
+ _handleScanSuccess(result: Result) {
+ this.fireDecoratorEvent("scan-success", {
+ text: result.getText(),
+ rawBytes: result.getRawBytes(),
+ });
+ }
+
+ _handleScanError(error: Exception) {
+ if (error instanceof NotFoundException) {
+ return;
+ }
+
+ this.fireDecoratorEvent("scan-error", { message: error.message });
+ }
+
+ _handleVideoPlaying() {
+ const FRAME_PROCESSING_INTERVAL = 200; // 5 frames per second
+
+ this.loading = false;
+ this.isReadyToScan = true;
+
+ // Wait for the next animation frame before drawing the capture region
+ requestAnimationFrame(() => {
+ this._drawCaptureRegion();
+ });
+
+ // Ensure any existing interval is cleared before setting a new one
+ if (this._scanInterval) {
+ clearInterval(this._scanInterval);
+ }
+
+ this._scanInterval = setInterval(() => {
+ this._processFrame();
+ }, FRAME_PROCESSING_INTERVAL);
+ }
+
+ _handleDrawCaptureRegion() {
+ this._drawCaptureRegion();
+ }
+
+ _closeDialog() {
+ this._resetReader();
+ this.open = false;
+ this.fireDecoratorEvent("close");
+ }
+
+ _resetReader() {
+ const video = this._getVideoElement();
+ video.pause();
+
+ if (video.srcObject) {
+ const stream = video.srcObject as MediaStream;
+ const tracks = stream.getTracks();
+ tracks.forEach(track => track.stop());
+ }
+
+ video.srcObject = null;
+ video.removeEventListener("loadeddata", this._handleVideoPlayingBound);
+
+ if (this._scanInterval) {
+ clearInterval(this._scanInterval);
+ this._scanInterval = null;
+ }
+
+ if (this._codeReader) {
+ this._codeReader.reset();
+ }
+
+ const overlay = this._getOverlayCanvasElement();
+ overlay.style.display = "none";
+
+ // Reset the ready state
+ this.isReadyToScan = false;
}
}
diff --git a/packages/fiori/src/themes/BarcodeScannerDialog.css b/packages/fiori/src/themes/BarcodeScannerDialog.css
index ed9d417658ef..a78877072a1e 100644
--- a/packages/fiori/src/themes/BarcodeScannerDialog.css
+++ b/packages/fiori/src/themes/BarcodeScannerDialog.css
@@ -1,34 +1,45 @@
-.ui5-barcode-scanner-dialog-root::part(content) {
- padding: .4375rem;
-}
-
-/* video */
-.ui5-barcode-scanner-dialog-video-wrapper,
-.ui5-barcode-scanner-dialog-video {
- height:100%;
- width: 100%;
-}
-
-.ui5-barcode-scanner-dialog-video {
- object-fit: cover;
-}
-
-/* footer */
-.ui5-barcode-scanner-dialog-footer {
- display: flex;
- justify-content: flex-end;
- width: 100%;
-}
-
-/* busy indicator */
-.ui5-barcode-scanner-dialog-busy {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- z-index: 1;
-}
-
-.ui5-barcode-scanner-dialog-busy:not([active]) {
- display: none;
-}
+.ui5-barcode-scanner-dialog-root::part(content) {
+ padding: .4375rem;
+}
+
+.ui5-barcode-scanner-dialog-video-wrapper {
+ position: relative;
+}
+
+/* video */
+.ui5-barcode-scanner-dialog-video-wrapper,
+.ui5-barcode-scanner-dialog-video,
+.ui5-barcode-scanner-dialog-overlay {
+ height: 100%;
+ width: 100%;
+}
+
+.ui5-barcode-scanner-dialog-video {
+ object-fit: cover;
+}
+
+.ui5-barcode-scanner-dialog-overlay {
+ display: none;
+ position: absolute;
+ inset: 0;
+}
+
+/* footer */
+.ui5-barcode-scanner-dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+}
+
+/* busy indicator */
+.ui5-barcode-scanner-dialog-busy {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1;
+}
+
+.ui5-barcode-scanner-dialog-busy:not([active]) {
+ display: none;
+}
\ No newline at end of file
diff --git a/packages/fiori/test/specs/BarcodeScannerDialog.spec.js b/packages/fiori/test/specs/BarcodeScannerDialog.spec.js
deleted file mode 100644
index bb0dc937637d..000000000000
--- a/packages/fiori/test/specs/BarcodeScannerDialog.spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { assert } from "chai";
-
-describe("BarcodeScannerDialog Behavior", () => {
- before(async () => {
- await browser.url(`test/pages/BarcodeScannerDialog.html`);
- });
-
- it("fires scan-error when no permissions granted", async () => {
- // Setup: deny permissions to access the camera
- await browser.setPermissions({ name: 'camera' }, 'denied');
- const btnScan = await browser.$("#btnScan"),
- scanError = await browser.$("#scanError");
-
- await btnScan.click();
-
- await browser.waitUntil(async () => {
- return (await scanError.getText()).length > 0;
- }, 25000, "expect scan-error output");
-
- // assert
- const scanErrorText = await scanError.getText();
- assert.ok(scanErrorText.length, "fires scan-error when no permissions");
- });
-
-});