Ionic camera with overlays - camera view is black

In my Angular 13 + Ionic 6 I have a camera with overlays page using:

import { CameraPreview, CameraPreviewOptions, CameraPreviewPictureOptions } from '@capacitor-community/camera-preview';

The page is working, and capturing images also works, only the camera view is not showing when run on Xiomu Rednote 8. On the screen. Instead, while the camera is OPEN, I see a black background on the screen, behind the visible overlays. It seems as if the camera preview is overwritten by an opaque black background, or something like that, although I do have:

ion-content {
  --background: transparent !important;
}

So, the user experience is: a black screen with overlays buttons, fully functional, including the button capturing an image, and the image is actually captured OK and can be further processed/displayed.

The basic approach is from this guide: Creating a Custom Camera Preview Overlay with Ionic & Capacitor - YouTube

When run on Samsung Galaxy A32, this problem does not exist.

HOWEVER:

The Xiaomi Rednote 8 is always running my app in DARK THEME, regardless of my device settings (LIGHT / DARK) and even when I have these in my app.component:

document.body.setAttribute('data-theme', 'light');
document.body.classList.toggle('dark', false);

The Samsung Galaxy A32 is allways running my app in LIGHT THEME, regardless of my device settings (LIGHT / DARK).

So, the “hiding” camera preview may be associated with a DARK mode (which should not happen anyway - it should work in both modes), OR, it may be associated with the XIomi Rednote 8 (here, too, it should work anyway…).
I’m not sure how to proceed with this from here.

For completeness, here’s more code from the component:

home.page.html

<ion-content>
  <div id="cameraPreview" class="cameraPreview">
    <div *ngIf="cameraActive">
      
      <ion-button [routerLink]="['/helpers']" id="helpers-icon" class="button-icon-top" fill="clear">
        <div>
          <ion-icon name="people-outline" slot="icon-only" size="large" class="buttons-icon"></ion-icon>
          <label class="button-label">Helpers</label>
        </div>
      </ion-button>
      <ion-button (click)="maintainMyInvitations()" id="my-invitations-icon" class="button-icon-top" *ngIf="isHelper" fill="clear">
        <div>
          <ion-icon name="receipt-outline" slot="icon-only" size="large" class="buttons-icon"></ion-icon>
          <label class="button-label">My Invitations</label>
        </div>
      </ion-button>
      <ion-button (click)="maintainSettings()" id="settings-icon" class="button-icon-top" fill="clear">
        <div>
          <ion-icon name="settings" slot="icon-only" size="large" class="buttons-icon"></ion-icon>
          <label class="button-label">Settings</label>
        </div>
      </ion-button>

      <ion-button (click)="captureImage()" id="capture-button" fill="clear">
        <div>
          <ion-icon name="radio-button-off" slot="icon-only" id="capture-icon"></ion-icon>
        </div>
      </ion-button>

      <ion-button (click)="provideFeedback()" id="feedback-icon" class="button-icon-bottom" fill="clear">
        <div>
          <ion-icon name="document-text-outline" slot="icon-only" size="large" class="buttons-icon"></ion-icon>
          <label class="button-label">Feedback</label>
        </div>
      </ion-button>
    </div>
  </div>
</div>
</ion-content>

home.page.scss

ion-content {
  --background: transparent !important;
}

.button-label {
  display: block;
  font-size: 50%;
}

.button-icon-top {
  width: 90px;
  height: 90px;  
  margin-top: 10px;
  .button-inner {
    flex-flow: column;

    .icon {
      //margin-top: 10px;
      font-size: 15em;
    }
  }
}

.button-icon-bottom {
  position: absolute;
  top: calc(100% - 5.5em);
  height: 5em;
  .button-inner {
    flex-flow: column;

    .icon {
      //margin-bottom: 10px;
      font-size: 1.5em;
    }
  }
}



.overlay {
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 10;
}

.cameraPreview {
  display: flex;
  position: absolute;
  width: 100%;
  height: 100%;
}

//If overlaying an image on top of the camera preview
.image-overlay {
  z-index: 1;
  position: absolute;
  left: 25%;
  top: 25%;
  width: 50%;
}

#my-invitations-icon {
  position: absolute;
  //bottom: 30px;
  left: calc(50% - 45px);

  z-index: 11;
}
#helpers-icon {
  position: absolute;
  //bottom: 30px;
  left: 10px;

  z-index: 11;
}
#settings-icon {
  position: absolute;
  //bottom: 30px;
  left: calc(100% - 100px);
  
  z-index: 11;
}

#capture-button {
  position: absolute;
  //bottom: 30px;
  left: calc(50% - 60px);
  top: calc(100% - 220px);
  width: 120px;
  height: 120px;
  z-index: 11;
  .button-inner {
    flex-flow: column;

  }
}

#capture-icon {
  font-size: 10em;
  font-weight: bold;
}

.buttons-icon {
  font-size: 14em;
  margin-bottom: 5px;
}


#repeat-button {
  width: 50px;
  height: 50px;
  left: calc(50% - 25px);
  margin-top: 10px;
  margin-bottom: 10px;
}

#repeat-icon {
  font-size: 10em;
}


#get-help-button {
  margin-top: 20px;
  width: 200px;
  left: calc(50% - 100px);
  height: 60px;
}

home.page.ts (abbreviated)

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
//import { Plugins } from '@capacitor/core';

import { environment } from '../../environments/environment';
import { get, set, remove } from '../services/storage.service';
import { AwsFileUploadService } from '../services/aws-file-upload.service'

import { CameraPreview, CameraPreviewPlugin, CameraPreviewOptions, CameraPreviewPictureOptions, CameraPosition } from '@capacitor-community/camera-preview';
import { TigergraphService } from '../services/tigergraph.service';
import { Dialogs, DialogType } from '../Utilities/dialogs';
import { Services } from '../Utilities/general-service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})

export class HomePage implements OnInit {
  image = null; //Base64 to save
  imagePreview = null; //For displaying on screen
  cameraActive = false;
  chatText: string = '';
  person: {};
  learner: {};
  isHelper = false;
  helpersExists: boolean = false;

  constructor(public tgdb: TigergraphService, private dialogs: Dialogs, private services: Services, private router: Router, private aws: AwsFileUploadService) {}

  ngOnInit() {
    this.dialogs.closeLoader();
    this.openCamera();

    // BUNCH OF DB STUFF COMES HERE
  }

  openCamera() {
    this.imagePreview = null;
    this.image = null;
    const cameraPreviewOptions: CameraPreviewOptions = {
      position: 'rear',
      toBack: true,
      disableAudio: true,
      lockAndroidOrientation: true,
      enableZoom: true,
      parent: "cameraPreview",
      className: "cameraPreview"
    };
    CameraPreview.start(cameraPreviewOptions);
    this.cameraActive = true;
  }

  async stopCamera() {
    console.log('Stopping Camera');
    await CameraPreview.stop();
    this.cameraActive = false;
  }

  async captureImage() {
    const CameraPreviewPictureOptions: CameraPreviewPictureOptions = {
      quality: 50
    }

    const result = await CameraPreview.capture(CameraPreviewPictureOptions)

    this.imagePreview = 'data:image/jpeg;base64,' + result.value;
    this.image = result.value;

    this.stopCamera();

  }
}

What am I missing?

(I tried posting this to StackOverflow 5 days ago - not even a comment from anyone.
I sure hope I can get a hint here from someone :slight_smile: )

Thank you!

Apparently this is a known issue:

OK, here’s an update on findings:
I think I understand what’s going on here, but there are “side effects” to the intuitive workaround I could come up with.

The black shield is coming from the variables.scss here:

/*
   * Material Design Dark Theme
   * -------------------------------------------
   */

  .md body {
    --ion-background-color: #121212;
    --ion-background-color-rgb: 18,18,18;

On the camera page itself, setting the background to transparent, overlays a transparent layer, effectively revealing the… black shield: --ion-background-color: #121212; - this is the opaque shield we see when the camera opens!

Changing the above to be transparent:
--ion-background-color: transparent;

…reveals the camera at last!

However, this globally affects the whole application. I notices two unwanted side-effects so far:

  1. Alerts are rendered with a transparent background.
  2. During routing away from the camera page, as I close the camera, there is a fraction of a second the application “disappears”, revealing the Android DESKTOP (with all my app icons), until the target route is painted. Basically the app background is now… transparent, until the next page comes along.

#1 can be mitigated by setting a background to the alerts class.
#2 - I thought I could play with a dynamic CSS class on the page level, that sets a black background as soon as I close the camera, and sets a transparent background as soon as I open the camera - but it did not solve the “disappearing” app experience.

Anyway, the above is not considered elegant or consistent solution in my mind, as it creates a global problem for a single page to work and it may lend itself to other issues for dark/light or customized themes.

I hope that the issue is now better understood for Ionic engineers to come up with a robust solution to this problem: how to use the camera-preview on a single page of an app, in line with the global theming infrastructure and implementation.

Happy to report on resolution.
Hinted from this (other) capacitor plugin, I did the following:

Added the following to the global.scss file:

body.camera-active {
    --background: transparent;
    --ion-background-color: transparent;
  }

And I inject this class into the body of the DOM upon opening the camera:

document.querySelector('body').classList.add('camera-active');

Upon closing the camera, I remove this class:

document.querySelector('body').classList.remove('camera-active');

And that’s it!

1 Like

Did you make it work with ios or android?

Try adding this to global.scss:

scss

CopyEdit

body.camera-active {
  --background: transparent;
  --ion-background-color: transparent;
}

Then, add the class when the camera is active:

typescript

CopyEdit

document.body.classList.add('camera-active');

It should resolve the black screen issue.

Im using ionic 7 with capacitor with angular

this is my code

step9.page.html

<ion-header>
  <ion-toolbar>
    <ion-title>camera</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div id="cameraPreview" class="cameraPreview">
    <div *ngIf="cameraActive">
      <img src="assets/icon/guide.png" class="image-overlay">
      <ion-button (click)="stopCamera()" expand="full" id="close">
        <ion-icon name="close-circle" slot="icon-only"></ion-icon>
      </ion-button>
      <ion-button (click)="captureImage()" expand="full" id="capture">
        <ion-icon name="camera" slot="icon-only"></ion-icon>
      </ion-button>
      <ion-button (click)="flipCamera()" expand="full" id="flip">
        <ion-icon name="repeat" slot="icon-only"></ion-icon>
      </ion-button>
    </div>
  </div>
  <ion-img [src]="image" *ngIf="image && !cameraActive"></ion-img>
  <ion-button (click)="openCamera()" expand="full" *ngIf="!cameraActive">Open Camera</ion-button>
  <ion-button (click)="stopCamera()" expand="full" *ngIf="cameraActive">Open Camera</ion-button>
</ion-content>

step9.page.scss

ion-content {
    --background: transparent !important;
  }
  .overlay {
    position: absolute;
    width: 100%;
    height: 100%;
    z-index: 10;
  }
  
  .cameraPreview {
    display: flex;
    position: absolute;
    width: 100%;
    height: 100%;
  }
  
  .image-overlay {
    z-index: 1;
    position: absolute;
    left: 25%;
    top: 25%;
    width: 50%;
  }
  
  #capture {
    position: absolute;
    bottom: 30px;
    left: calc(50% - 25px);
    width: 50px;
    height: 50px;
    z-index: 11;
  }
  #close {
    position: absolute;
    bottom: 30px;
    left: calc(50% - 175px);
    width: 50px;
    height: 50px;
    z-index: 11;
  }
  #flip {
    position: absolute;
    bottom: 30px;
    left: calc(50% + 125px);
    width: 50px;
    height: 50px;
    z-index: 11;
  }
  
  #close::part(native) {
    border-radius: 30px;
  }
  #capture::part(native) {
    border-radius: 30px;
  }
  #flip::part(native) {
    border-radius: 30px;
  }

Step9.page.ts

import { Component, OnInit } from '@angular/core';
import { ToastService } from '../../../services/toast/toast.service';
import { LoadingController } from '@ionic/angular';
import { Router } from '@angular/router';
import { CameraPreview, CameraPreviewOptions, CameraPreviewPictureOptions } from '@capacitor-community/camera-preview';


@Component({
  selector: 'app-step9',
  templateUrl: './step9.page.html',
  styleUrls: ['./step9.page.scss'],
})
export class Step9Page implements OnInit {

  parametro : any;
  fileTransfer : any;
  image: any  = null; //Base64 to save
  imagePreview: any = null; //For displaying on screen
  cameraActive = false;

  constructor(
    private loadingController: LoadingController,
    private toastService: ToastService,
    private router: Router
  ) { }

  ngOnInit() {
    this.openCamera();

  }

  flipCamera() {
    CameraPreview.flip()

  }

  openCamera() {
    const cameraPreviewOptions: CameraPreviewOptions = {
      position: 'rear',
      disableAudio: true,
      height: 1920,
      width: 1080
    };
    CameraPreview.start(cameraPreviewOptions);
    document.body.classList.add('camera-active');

  }

  async stopCamera() {
    console.log('Stopping Camera');
    await CameraPreview.stop();
    this.cameraActive = false;
  }

  async captureImage() {
    const CameraPreviewPictureOptions: CameraPreviewPictureOptions = {
      quality: 50
    }

    const result = await CameraPreview.capture(CameraPreviewPictureOptions)

    this.imagePreview = 'data:image/jpeg;base64,' + result.value;
    this.image = result.value;

    this.stopCamera();

  }

}

global.scss

  body.camera-active {
    --background: transparent;
    --ion-background-color: transparent;
  }

unfortunately it doesnt work