forked from EVOgeek/Vmeda.Online
		
	MOBILE-3585 emulator: Add mocks of media services
This commit is contained in:
		
							parent
							
								
									f581dbcc7c
								
							
						
					
					
						commit
						4bb7f0e97f
					
				
							
								
								
									
										60
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										60
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -2115,6 +2115,36 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/camera": { | ||||
|       "version": "5.29.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/camera/-/camera-5.29.0.tgz", | ||||
|       "integrity": "sha512-JOmFb2eWeh8zZWu2JlNVRbhcSvOcwiTSdoabEfGtw0ITXs0FzuRmzAQgF2PQGyPA8844wkr3T5IUhcMpYxW6UQ==", | ||||
|       "requires": { | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/cordova": { | ||||
|           "version": "0.0.34", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", | ||||
|           "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/chooser": { | ||||
|       "version": "5.29.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/chooser/-/chooser-5.29.0.tgz", | ||||
|       "integrity": "sha512-1/+zr+SbijWqd0FomOh83aQb8vqH2qO2CAlgX2FyjJuK4fgt3BF9GMXpzTjkd/qrHO9rbxUMFAcrQAv/HAVNiA==", | ||||
|       "requires": { | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/cordova": { | ||||
|           "version": "0.0.34", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", | ||||
|           "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/clipboard": { | ||||
|       "version": "5.28.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-5.28.0.tgz", | ||||
| @ -2247,6 +2277,36 @@ | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/media": { | ||||
|       "version": "5.29.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/media/-/media-5.29.0.tgz", | ||||
|       "integrity": "sha512-XC8MtrbeR0X0I6B0FABStc2mSAmgIQidaRjFqP4jBAElAwjZC7PHwaDyyVJUOR1Rx5Nest46hZAU6jpAPZ8+pw==", | ||||
|       "requires": { | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/cordova": { | ||||
|           "version": "0.0.34", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", | ||||
|           "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/media-capture": { | ||||
|       "version": "5.29.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-5.29.0.tgz", | ||||
|       "integrity": "sha512-5NdTXQGbrpXLeeLbI+cGQaeNmpmOrPC9vgX4jvUT6whUdDXGZ93wLT1/eeRj208czNiqbdetjG8Dji3OJZ5MKA==", | ||||
|       "requires": { | ||||
|         "@types/cordova": "^0.0.34" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/cordova": { | ||||
|           "version": "0.0.34", | ||||
|           "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", | ||||
|           "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@ionic-native/network": { | ||||
|       "version": "5.28.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/network/-/network-5.28.0.tgz", | ||||
|  | ||||
| @ -38,6 +38,8 @@ | ||||
|     "@angular/platform-browser": "~10.0.0", | ||||
|     "@angular/platform-browser-dynamic": "~10.0.0", | ||||
|     "@angular/router": "~10.0.0", | ||||
|     "@ionic-native/camera": "^5.29.0", | ||||
|     "@ionic-native/chooser": "^5.29.0", | ||||
|     "@ionic-native/clipboard": "^5.28.0", | ||||
|     "@ionic-native/core": "^5.0.0", | ||||
|     "@ionic-native/device": "^5.28.0", | ||||
| @ -51,6 +53,8 @@ | ||||
|     "@ionic-native/ionic-webview": "^5.28.0", | ||||
|     "@ionic-native/keyboard": "^5.28.0", | ||||
|     "@ionic-native/local-notifications": "^5.28.0", | ||||
|     "@ionic-native/media": "^5.29.0", | ||||
|     "@ionic-native/media-capture": "^5.29.0", | ||||
|     "@ionic-native/network": "^5.28.0", | ||||
|     "@ionic-native/push": "^5.28.0", | ||||
|     "@ionic-native/qr-scanner": "^5.28.0", | ||||
|  | ||||
							
								
								
									
										30
									
								
								src/app/classes/errors/captureerror.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/classes/errors/captureerror.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreError } from './error'; | ||||
| 
 | ||||
| /** | ||||
|  * Capture error. | ||||
|  */ | ||||
| export class CoreCaptureError extends CoreError { | ||||
| 
 | ||||
|     code: number; | ||||
| 
 | ||||
|     constructor(code: number, message?: string) { | ||||
|         super(message); | ||||
| 
 | ||||
|         this.code = code; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,66 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title>{{ title | translate }}</ion-title> | ||||
| 
 | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button (click)="cancel()">{{ 'core.cancel' | translate }}</ion-button> | ||||
|             <ion-button *ngIf="hasCaptured" (click)="done()">{{ 'core.done' | translate }}</ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="readyToCapture"> | ||||
|         <div class="core-av-wrapper"> | ||||
|             <!-- Video stream for image and video. --> | ||||
|             <video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video> | ||||
| 
 | ||||
|             <!-- For video recording, use 2 videos and show/hide them because a CSS rule caused problems with the controls. --> | ||||
|             <video *ngIf="isVideo" [hidden]="!hasCaptured" class="core-webcam-video-captured" controls #previewVideo></video> | ||||
| 
 | ||||
|             <!-- Canvas to treat the image and an img to show the result. --> | ||||
|             <canvas *ngIf="isImage" class="core-webcam-image-canvas" #imgCanvas></canvas> | ||||
|             <img *ngIf="isImage" [hidden]="!hasCaptured" class="core-webcam-image" alt="{{ 'core.capturedimage' | translate }}" | ||||
|                 #previewImage> | ||||
| 
 | ||||
|             <!-- Recording audio. --> | ||||
|             <div *ngIf="isAudio" class="core-audio-record-container"> | ||||
|                 <!-- Canvas to show audio waves when recording audio in browser. --> | ||||
|                 <canvas [hidden]="hasCaptured || isCordovaAudioCapture" class="core-audio-canvas" #streamAudio></canvas> | ||||
| 
 | ||||
|                 <!-- Button to start/stop in mobile devices. --> | ||||
|                 <ion-button fill="clear" *ngIf="!hasCaptured && isCordovaAudioCapture" (click)="actionClicked()" | ||||
|                     [attr.aria-label]="title"> | ||||
|                     <ion-icon *ngIf="!isCapturing" name="fa-microphone" slot="icon-only"></ion-icon> | ||||
|                     <ion-icon *ngIf="isCapturing" name="fa-square" slot="icon-only"></ion-icon> | ||||
|                 </ion-button> | ||||
| 
 | ||||
|                 <!-- Audio player to listen to the result. --> | ||||
|                 <audio [hidden]="!hasCaptured" class="core-audio-captured" controls #previewAudio></audio> | ||||
|             </div> | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| 
 | ||||
| <ion-footer *ngIf="readyToCapture"> | ||||
|     <ion-row> | ||||
|         <ion-col></ion-col> | ||||
|         <ion-col class="ion-text-center"> | ||||
|             <ion-button fill="clear" *ngIf="!hasCaptured && !isCordovaAudioCapture" (click)="actionClicked()" | ||||
|                 [attr.aria-label]="title"> | ||||
|                 <ion-icon *ngIf="!isCapturing && isAudio" name="fa-microphone" slot="icon-only"></ion-icon> | ||||
|                 <ion-icon *ngIf="!isCapturing && isVideo" name="fa-video" slot="icon-only"></ion-icon> | ||||
|                 <ion-icon *ngIf="isImage" name="fa-camera" slot="icon-only"></ion-icon> | ||||
|                 <ion-icon *ngIf="isCapturing" name="fa-square" slot="icon-only"></ion-icon> | ||||
|             </ion-button> | ||||
|             <ion-button fill="clear" *ngIf="hasCaptured" (click)="discard()" [attr.aria-label]="'core.discard' | translate"> | ||||
|                 <ion-icon color="danger" name="fa-trash" slot="icon-only"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-col> | ||||
|         <ion-col class="ion-padding ion-text-end chrono-container"> | ||||
|             <core-chrono *ngIf="!isImage" [hidden]="hasCaptured" [running]="isCapturing" [reset]="resetChrono" [endTime]="maxTime" | ||||
|                 (onEnd)="stopCapturing()"> | ||||
|             </core-chrono> | ||||
|         </ion-col> | ||||
|     </ion-row> | ||||
| </ion-footer> | ||||
| 
 | ||||
| @ -0,0 +1,71 @@ | ||||
| :host { | ||||
|     .core-av-wrapper { | ||||
|         // @todo: For some reason it takes a while to apply these styles, first it's displayed too big and then it's resized. | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
| 
 | ||||
|         .core-webcam-image-canvas { | ||||
|             display: none; | ||||
|         } | ||||
| 
 | ||||
|         .core-audio-record-container { | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             position: relative; | ||||
| 
 | ||||
|             .core-audio-canvas { | ||||
|                 width: 100%; | ||||
|                 height: 100%; | ||||
|             } | ||||
| 
 | ||||
|             .core-audio-captured { | ||||
|                 width: 100%; | ||||
|             } | ||||
| 
 | ||||
|             .button { | ||||
|                 position: absolute; | ||||
|                 top: 0; | ||||
|                 left: 0; | ||||
|                 bottom: 0; | ||||
|                 right: 0; | ||||
|                 margin: auto; | ||||
|                 height: 120px; | ||||
|                 width: 120px; | ||||
| 
 | ||||
|                 .icon { | ||||
|                     font-size: 120px; | ||||
|                     width: auto; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             audio { | ||||
|                 position: absolute; | ||||
|                 top: 0; | ||||
|                 left: 0; | ||||
|                 bottom: 0; | ||||
|                 right: 0; | ||||
|                 margin: auto; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         video, img { | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             display: table-cell; | ||||
|             text-align: center; | ||||
|             vertical-align: middle; | ||||
|             object-fit: contain; | ||||
| 
 | ||||
|             &.core-webcam-stream { | ||||
|                 -webkit-transform: scaleX(-1); | ||||
|                 transform: scaleX(-1); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     ion-footer { | ||||
|         background-color: var(--gray); | ||||
|         border-top: 1px solid var(--gray-dark); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										588
									
								
								src/app/core/emulator/components/capture-media/capture-media.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										588
									
								
								src/app/core/emulator/components/capture-media/capture-media.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,588 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef, Input } from '@angular/core'; | ||||
| import { MediaObject } from '@ionic-native/media/ngx'; | ||||
| import { FileEntry } from '@ionic-native/file/ngx'; | ||||
| import { MediaFile } from '@ionic-native/media-capture/ngx'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFile, CoreFileProvider } from '@services/file'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { Platform, ModalController, Media, Translate } from '@singletons/core.singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreCaptureError } from '@classes/errors/captureerror'; | ||||
| import { CoreCanceledError } from '@classes/errors/cancelederror'; | ||||
| 
 | ||||
| /** | ||||
|  * Page to capture media in browser, or to capture audio in mobile devices. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-emulator-capture-media', | ||||
|     templateUrl: 'capture-media.html', | ||||
|     styleUrls: ['capture-media.scss'], | ||||
| }) | ||||
| export class CoreEmulatorCaptureMediaComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @Input() type?: 'audio' | 'video' | 'image' | 'captureimage'; | ||||
|     @Input() maxTime?: number; // Max time to capture.
 | ||||
|     @Input() facingMode?: string; // Camera facing mode.
 | ||||
|     @Input() mimetype?: string; | ||||
|     @Input() extension?: string; | ||||
|     @Input() quality?: number; // Only for images.
 | ||||
|     @Input() returnDataUrl?: boolean; // Whether it should return a data img. Only for images.
 | ||||
| 
 | ||||
|     @ViewChild('streamVideo') streamVideo?: ElementRef; | ||||
|     @ViewChild('previewVideo') previewVideo?: ElementRef; | ||||
|     @ViewChild('imgCanvas') imgCanvas?: ElementRef; | ||||
|     @ViewChild('previewImage') previewImage?: ElementRef; | ||||
|     @ViewChild('streamAudio') streamAudio?: ElementRef; | ||||
|     @ViewChild('previewAudio') previewAudio?: ElementRef; | ||||
| 
 | ||||
|     title?: string; // The title of the page.
 | ||||
|     isAudio?: boolean; // Whether it should capture audio.
 | ||||
|     isVideo?: boolean; // Whether it should capture video.
 | ||||
|     isImage?: boolean; // Whether it should capture image.
 | ||||
|     readyToCapture?: boolean; // Whether it's ready to capture.
 | ||||
|     hasCaptured?: boolean; // Whether it has captured something.
 | ||||
|     isCapturing?: boolean; // Whether it's capturing.
 | ||||
|     resetChrono?: boolean; // Boolean to reset the chrono.
 | ||||
|     isCordovaAudioCapture?: boolean; // Whether it's capturing audio using Cordova plugin.
 | ||||
| 
 | ||||
|     protected isCaptureImage?: boolean; // To identify if it's capturing an image using media capture plugin (instead of camera).
 | ||||
|     protected mediaRecorder?: MediaRecorder; // To record video/audio.
 | ||||
|     protected previewMedia?: HTMLAudioElement | HTMLVideoElement; // The element to preview the audio/video captured.
 | ||||
|     protected mediaBlob?: Blob; // A Blob where the captured data is stored.
 | ||||
|     protected localMediaStream?: MediaStream; | ||||
|     protected audioDrawer?: {start: () => void; stop: () => void }; // To start/stop the display of audio sound.
 | ||||
| 
 | ||||
|     // Variables for Cordova Media capture.
 | ||||
|     protected mediaFile?: MediaObject; | ||||
|     protected filePath?: string; | ||||
|     protected fileEntry?: FileEntry; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected changeDetectorRef: ChangeDetectorRef, | ||||
|     ) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.initVariables(); | ||||
| 
 | ||||
|         if (this.isCordovaAudioCapture) { | ||||
|             this.initCordovaMediaPlugin(); | ||||
|         } else { | ||||
|             this.initHtmlCapture(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize some variables based on the params. | ||||
|      */ | ||||
|     protected initVariables(): void { | ||||
|         this.facingMode = this.facingMode || 'environment'; | ||||
|         this.quality = this.quality || 0.92; | ||||
| 
 | ||||
|         if (this.type == 'captureimage') { | ||||
|             this.isCaptureImage = true; | ||||
|             this.type = 'image'; | ||||
|         } | ||||
| 
 | ||||
|         // Initialize some data based on the type of media to capture.
 | ||||
|         if (this.type == 'video') { | ||||
|             this.isVideo = true; | ||||
|             this.title = 'core.capturevideo'; | ||||
|         } else if (this.type == 'audio') { | ||||
|             this.isAudio = true; | ||||
|             this.title = 'core.captureaudio'; | ||||
|         } else if (this.type == 'image') { | ||||
|             this.isImage = true; | ||||
|             this.title = 'core.captureimage'; | ||||
|         } | ||||
| 
 | ||||
|         this.isCordovaAudioCapture = CoreApp.instance.isMobile() && this.isAudio; | ||||
| 
 | ||||
|         if (this.isCordovaAudioCapture) { | ||||
|             this.extension = Platform.instance.is('ios') ? 'wav' : 'aac'; | ||||
|             this.returnDataUrl = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init recording with Cordova media plugin. | ||||
|      * | ||||
|      * @return Promise resolved when ready. | ||||
|      */ | ||||
|     protected async initCordovaMediaPlugin(): Promise<void> { | ||||
|         this.filePath = this.getFilePath(); | ||||
|         let absolutePath = CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getBasePathInstant(), this.filePath); | ||||
| 
 | ||||
|         if (Platform.instance.is('ios')) { | ||||
|             // In iOS we need to remove the file:// part.
 | ||||
|             absolutePath = absolutePath.replace(/^file:\/\//, ''); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // First create the file.
 | ||||
|             this.fileEntry = await CoreFile.instance.createFile(this.filePath); | ||||
| 
 | ||||
|             // Now create the media instance.
 | ||||
|             this.mediaFile = Media.instance.create(absolutePath); | ||||
|             this.readyToCapture = true; | ||||
|             this.previewMedia = this.previewAudio?.nativeElement; | ||||
|         } catch (error) { | ||||
|             this.dismissWithError(-1, error.message || error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init HTML recorder for browser | ||||
|      * . | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async initHtmlCapture(): Promise<void> { | ||||
|         const constraints = { | ||||
|             video: this.isAudio ? false : { facingMode: this.facingMode }, | ||||
|             audio: !this.isImage, | ||||
|         }; | ||||
| 
 | ||||
|         try { | ||||
|             const stream = await navigator.mediaDevices.getUserMedia(constraints); | ||||
| 
 | ||||
|             let chunks: Blob[] = []; | ||||
|             this.localMediaStream = stream; | ||||
| 
 | ||||
|             if (!this.isImage) { | ||||
|                 if (this.isVideo) { | ||||
|                     this.previewMedia = this.previewVideo?.nativeElement; | ||||
|                 } else { | ||||
|                     this.previewMedia = this.previewAudio?.nativeElement; | ||||
|                     this.initAudioDrawer(this.localMediaStream); | ||||
|                     this.audioDrawer?.start(); | ||||
|                 } | ||||
| 
 | ||||
|                 this.mediaRecorder = new MediaRecorder(this.localMediaStream, { mimeType: this.mimetype }); | ||||
| 
 | ||||
|                 // When video or audio is recorded, add it to the list of chunks.
 | ||||
|                 this.mediaRecorder.ondataavailable = (e): void => { | ||||
|                     if (e.data.size > 0) { | ||||
|                         chunks.push(e.data); | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 // When recording stops, create a Blob element with the recording and set it to the video or audio.
 | ||||
|                 this.mediaRecorder.onstop = (): void => { | ||||
|                     this.mediaBlob = new Blob(chunks); | ||||
|                     chunks = []; | ||||
| 
 | ||||
|                     if (this.previewMedia) { | ||||
|                         this.previewMedia.src = window.URL.createObjectURL(this.mediaBlob); | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             if (!this.isImage && !this.isVideo) { | ||||
|                 // It's ready to capture.
 | ||||
|                 this.readyToCapture = true; | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!this.streamVideo) { | ||||
|                 throw new CoreError('Video element not found.'); | ||||
|             } | ||||
| 
 | ||||
|             let hasLoaded = false; | ||||
| 
 | ||||
|             // If stream isn't ready in a while, show error.
 | ||||
|             const waitTimeout = window.setTimeout(() => { | ||||
|                 if (!hasLoaded) { | ||||
|                     // Show error.
 | ||||
|                     hasLoaded = true; | ||||
|                     this.dismissWithError(-1, 'Cannot connect to webcam.'); | ||||
|                 } | ||||
|             }, 10000); | ||||
| 
 | ||||
|             // Listen for stream ready to display the stream.
 | ||||
|             this.streamVideo.nativeElement.onloadedmetadata = (): void => { | ||||
|                 if (hasLoaded) { | ||||
|                     // Already loaded or timeout triggered, stop.
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 hasLoaded = true; | ||||
|                 clearTimeout(waitTimeout); | ||||
|                 this.readyToCapture = true; | ||||
|                 this.streamVideo!.nativeElement.onloadedmetadata = null; | ||||
|                 // Force change detection. Angular doesn't detect these async operations.
 | ||||
|                 this.changeDetectorRef.detectChanges(); | ||||
|             }; | ||||
| 
 | ||||
|             // Set the stream as the source of the video.
 | ||||
|             if ('srcObject' in this.streamVideo.nativeElement) { | ||||
|                 this.streamVideo.nativeElement.srcObject = this.localMediaStream; | ||||
|             } else { | ||||
|                 // Fallback for old browsers.
 | ||||
|                 // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject#Examples
 | ||||
|                 this.streamVideo.nativeElement.src = window.URL.createObjectURL(this.localMediaStream); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             this.dismissWithError(-1, error.message || error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the audio drawer. This code has been extracted from MDN's example on MediaStream Recording: | ||||
|      * https://github.com/mdn/web-dictaphone
 | ||||
|      * | ||||
|      * @param stream Stream returned by getUserMedia. | ||||
|      */ | ||||
|     protected initAudioDrawer(stream: MediaStream): void { | ||||
|         if (!this.streamAudio) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let skip = true; | ||||
|         let running = false; | ||||
| 
 | ||||
|         // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|         const audioCtx = new (window.AudioContext || (<any> window).webkitAudioContext)(); | ||||
|         const canvasCtx = this.streamAudio.nativeElement.getContext('2d'); | ||||
|         const source = audioCtx.createMediaStreamSource(stream); | ||||
|         const analyser = audioCtx.createAnalyser(); | ||||
|         const bufferLength = analyser.frequencyBinCount; | ||||
|         const dataArray = new Uint8Array(bufferLength); | ||||
|         const width = this.streamAudio.nativeElement.width; | ||||
|         const height = this.streamAudio.nativeElement.height; | ||||
|         const drawAudio = (): void => { | ||||
|             if (!running) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Update the draw every animation frame.
 | ||||
|             requestAnimationFrame(drawAudio); | ||||
| 
 | ||||
|             // Skip half of the frames to improve performance, shouldn't affect the smoothness.
 | ||||
|             skip = !skip; | ||||
|             if (skip) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const sliceWidth = width / bufferLength; | ||||
|             let x = 0; | ||||
| 
 | ||||
|             analyser.getByteTimeDomainData(dataArray); | ||||
| 
 | ||||
|             canvasCtx.fillStyle = 'rgb(200, 200, 200)'; | ||||
|             canvasCtx.fillRect(0, 0, width, height); | ||||
| 
 | ||||
|             canvasCtx.lineWidth = 1; | ||||
|             canvasCtx.strokeStyle = 'rgb(0, 0, 0)'; | ||||
| 
 | ||||
|             canvasCtx.beginPath(); | ||||
| 
 | ||||
|             for (let i = 0; i < bufferLength; i++) { | ||||
|                 const v = dataArray[i] / 128.0; | ||||
|                 const y = v * height / 2; | ||||
| 
 | ||||
|                 if (i === 0) { | ||||
|                     canvasCtx.moveTo(x, y); | ||||
|                 } else { | ||||
|                     canvasCtx.lineTo(x, y); | ||||
|                 } | ||||
| 
 | ||||
|                 x += sliceWidth; | ||||
|             } | ||||
| 
 | ||||
|             canvasCtx.lineTo(width, height / 2); | ||||
|             canvasCtx.stroke(); | ||||
|         }; | ||||
| 
 | ||||
|         analyser.fftSize = 2048; | ||||
|         source.connect(analyser); | ||||
| 
 | ||||
|         this.audioDrawer = { | ||||
|             start: (): void => { | ||||
|                 if (running) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 running = true; | ||||
|                 drawAudio(); | ||||
|             }, | ||||
|             stop: (): void => { | ||||
|                 running = false; | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Main action clicked: record or stop recording. | ||||
|      */ | ||||
|     async actionClicked(): Promise<void> { | ||||
|         if (this.isCapturing) { | ||||
|             // It's capturing, stop.
 | ||||
|             this.stopCapturing(); | ||||
|             this.changeDetectorRef.detectChanges(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.isImage) { | ||||
|             // Start the capture.
 | ||||
|             this.isCapturing = true; | ||||
|             this.resetChrono = false; | ||||
| 
 | ||||
|             if (this.isCordovaAudioCapture) { | ||||
|                 this.mediaFile?.startRecord(); | ||||
|                 if (this.previewMedia) { | ||||
|                     this.previewMedia.src = ''; | ||||
|                 } | ||||
|             } else { | ||||
|                 this.mediaRecorder?.start(); | ||||
|             } | ||||
| 
 | ||||
|             this.changeDetectorRef.detectChanges(); | ||||
|         } else { | ||||
|             if (!this.imgCanvas) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Get the image from the video and set it to the canvas, using video width/height.
 | ||||
|             const width = this.streamVideo?.nativeElement.videoWidth; | ||||
|             const height = this.streamVideo?.nativeElement.videoHeight; | ||||
|             const loadingModal = await CoreDomUtils.instance.showModalLoading(); | ||||
| 
 | ||||
| 
 | ||||
|             this.imgCanvas.nativeElement.width = width; | ||||
|             this.imgCanvas.nativeElement.height = height; | ||||
|             this.imgCanvas.nativeElement.getContext('2d').drawImage(this.streamVideo?.nativeElement, 0, 0, width, height); | ||||
| 
 | ||||
|             // Convert the image to blob and show it in an image element.
 | ||||
|             this.imgCanvas.nativeElement.toBlob((blob) => { | ||||
|                 loadingModal.dismiss(); | ||||
| 
 | ||||
|                 this.mediaBlob = blob; | ||||
|                 this.previewImage?.nativeElement.setAttribute('src', window.URL.createObjectURL(this.mediaBlob)); | ||||
|                 this.hasCaptured = true; | ||||
|             }, this.mimetype, this.quality); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User cancelled. | ||||
|      */ | ||||
|     async cancel(): Promise<void> { | ||||
|         if (this.hasCaptured) { | ||||
|             try { | ||||
|                 await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit')); | ||||
|             } catch { | ||||
|                 // Canceled.
 | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Send a "cancelled" error like the Cordova plugin does.
 | ||||
|         this.dismissWithCanceledError('Canceled.', 'Camera cancelled'); | ||||
| 
 | ||||
|         if (this.isCordovaAudioCapture && this.filePath) { | ||||
|             // Delete the tmp file.
 | ||||
|             CoreFile.instance.removeFile(this.filePath); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Discard the captured media. | ||||
|      */ | ||||
|     discard(): void { | ||||
|         this.previewMedia?.pause(); | ||||
|         this.streamVideo?.nativeElement.play(); | ||||
|         this.audioDrawer?.start(); | ||||
| 
 | ||||
|         this.hasCaptured = false; | ||||
|         this.isCapturing = false; | ||||
|         this.resetChrono = true; | ||||
|         delete this.mediaBlob; | ||||
|         this.changeDetectorRef.detectChanges(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the modal, returning some data (success). | ||||
|      * | ||||
|      * @param data Data to return. | ||||
|      */ | ||||
|     dismissWithData(data?: [MediaFile] | string): void { | ||||
|         ModalController.instance.dismiss(data, 'success'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the modal, returning an error. | ||||
|      * | ||||
|      * @param code Error code. Will not be used if it's a Camera capture. | ||||
|      * @param message Error message. | ||||
|      * @param cameraMessage A specific message to use if it's a Camera capture. If not set, message will be used. | ||||
|      */ | ||||
|     dismissWithCanceledError(message: string, cameraMessage?: string): void { | ||||
|         const isCamera = this.isImage && !this.isCaptureImage; | ||||
|         const error = isCamera ? new CoreCanceledError(cameraMessage || message) : new CoreCaptureError(3, message); | ||||
| 
 | ||||
|         ModalController.instance.dismiss(error, 'error'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the modal, returning an error. | ||||
|      * | ||||
|      * @param code Error code. Will not be used if it's a Camera capture. | ||||
|      * @param message Error message. | ||||
|      * @param cameraMessage A specific message to use if it's a Camera capture. If not set, message will be used. | ||||
|      */ | ||||
|     dismissWithError(code: number, message: string, cameraMessage?: string): void { | ||||
|         const isCamera = this.isImage && !this.isCaptureImage; | ||||
|         const error = isCamera ? new CoreError(cameraMessage || message) : new CoreCaptureError(code, message); | ||||
| 
 | ||||
|         ModalController.instance.dismiss(error, 'error'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Done capturing, write the file. | ||||
|      */ | ||||
|     async done(): Promise<void> { | ||||
|         if (this.returnDataUrl) { | ||||
|             // Return the image as a base64 string.
 | ||||
|             this.dismissWithData((<HTMLCanvasElement> this.imgCanvas?.nativeElement).toDataURL(this.mimetype, this.quality)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.mediaBlob && !this.isCordovaAudioCapture) { | ||||
|             // Shouldn't happen.
 | ||||
|             CoreDomUtils.instance.showErrorModal('Please capture the media first.'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let fileEntry = this.fileEntry; | ||||
|         const loadingModal = await CoreDomUtils.instance.showModalLoading(); | ||||
| 
 | ||||
|         try { | ||||
|             if (!this.isCordovaAudioCapture) { | ||||
|                 // Capturing in browser. Write the blob in a file.
 | ||||
|                 if (!this.mediaBlob) { | ||||
|                     // Shouldn't happen.
 | ||||
|                     throw new Error('Please capture the media first.'); | ||||
|                 } | ||||
| 
 | ||||
|                 fileEntry = await CoreFile.instance.writeFile(this.getFilePath(), this.mediaBlob); | ||||
|             } | ||||
| 
 | ||||
|             if (!fileEntry) { | ||||
|                 throw new CoreError('File not found.'); | ||||
|             } | ||||
| 
 | ||||
|             if (this.isImage && !this.isCaptureImage) { | ||||
|                 this.dismissWithData(fileEntry.toURL()); | ||||
|             } else { | ||||
|                 // The capture plugin should return a MediaFile, not a FileEntry. Convert it.
 | ||||
|                 const metadata = await CoreFile.instance.getMetadata(fileEntry); | ||||
| 
 | ||||
|                 let mimetype: string | undefined; | ||||
|                 if (this.extension) { | ||||
|                     mimetype = CoreMimetypeUtils.instance.getMimeType(this.extension); | ||||
|                 } | ||||
| 
 | ||||
|                 const mediaFile: MediaFile = { | ||||
|                     name: fileEntry.name, | ||||
|                     fullPath: fileEntry.nativeURL || fileEntry.fullPath, | ||||
|                     type: mimetype || '', | ||||
|                     lastModifiedDate: metadata.modificationTime, | ||||
|                     size: metadata.size, | ||||
|                     getFormatData: (): void => { | ||||
|                         // Nothing to do.
 | ||||
|                     }, | ||||
|                 }; | ||||
| 
 | ||||
|                 this.dismissWithData([mediaFile]); | ||||
|             } | ||||
|         } catch (err) { | ||||
|             CoreDomUtils.instance.showErrorModal(err); | ||||
|         } finally { | ||||
|             loadingModal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get path to the file where the media will be stored. | ||||
|      * | ||||
|      * @return Path. | ||||
|      */ | ||||
|     protected getFilePath(): string { | ||||
|         const fileName = this.type + '_' + CoreTimeUtils.instance.readableTimestamp() + '.' + this.extension; | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'media/' + fileName); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stop capturing. Only for video and audio. | ||||
|      */ | ||||
|     stopCapturing(): void { | ||||
|         this.isCapturing = false; | ||||
|         this.hasCaptured = true; | ||||
| 
 | ||||
|         if (this.isCordovaAudioCapture) { | ||||
|             this.mediaFile?.stopRecord(); | ||||
|             if (this.previewMedia && this.fileEntry) { | ||||
|                 this.previewMedia.src = CoreFile.instance.convertFileSrc(this.fileEntry.toURL()); | ||||
|             } | ||||
|         } else { | ||||
|             this.streamVideo && this.streamVideo.nativeElement.pause(); | ||||
|             this.audioDrawer && this.audioDrawer.stop(); | ||||
|             this.mediaRecorder && this.mediaRecorder.stop(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.mediaFile?.release(); | ||||
| 
 | ||||
|         if (this.localMediaStream) { | ||||
|             const tracks = this.localMediaStream.getTracks(); | ||||
|             tracks.forEach((track) => { | ||||
|                 track.stop(); | ||||
|             }); | ||||
|         } | ||||
|         this.streamVideo?.nativeElement.pause(); | ||||
|         this.previewMedia?.pause(); | ||||
|         this.audioDrawer?.stop(); | ||||
|         delete this.mediaBlob; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type CaptureMediaComponentInputs = { | ||||
|     type: 'audio' | 'video' | 'image' | 'captureimage'; | ||||
|     maxTime?: number; // Max time to capture.
 | ||||
|     facingMode?: string; // Camera facing mode.
 | ||||
|     mimetype?: string; | ||||
|     extension?: string; | ||||
|     quality?: number; // Only for images.
 | ||||
|     returnDataUrl?: boolean; // Whether it should return a data img. Only for images.
 | ||||
| }; | ||||
							
								
								
									
										41
									
								
								src/app/core/emulator/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/app/core/emulator/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| import { CoreComponentsModule } from '@app/components/components.module'; | ||||
| import { CoreDirectivesModule } from '@app/directives/directives.module'; | ||||
| import { CorePipesModule } from '@app/pipes/pipes.module'; | ||||
| import { CoreEmulatorCaptureMediaComponent } from './capture-media/capture-media'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreEmulatorCaptureMediaComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule.forRoot(), | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         CoreEmulatorCaptureMediaComponent, | ||||
|     ], | ||||
| }) | ||||
| export class CoreEmulatorComponentsModule {} | ||||
| @ -17,8 +17,12 @@ import { Platform } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreInitDelegate } from '@services/init'; | ||||
| import { CoreEmulatorHelperProvider } from './services/helper'; | ||||
| import { CoreEmulatorCaptureHelperProvider } from './services/capture.helper'; | ||||
| import { CoreEmulatorComponentsModule } from './components/components.module'; | ||||
| 
 | ||||
| // Ionic Native services.
 | ||||
| import { Camera } from '@ionic-native/camera/ngx'; | ||||
| import { Chooser } from '@ionic-native/chooser/ngx'; | ||||
| import { Clipboard } from '@ionic-native/clipboard/ngx'; | ||||
| import { Device } from '@ionic-native/device/ngx'; | ||||
| import { Diagnostic } from '@ionic-native/diagnostic/ngx'; | ||||
| @ -31,6 +35,8 @@ import { InAppBrowser } from '@ionic-native/in-app-browser/ngx'; | ||||
| import { WebView } from '@ionic-native/ionic-webview/ngx'; | ||||
| import { Keyboard } from '@ionic-native/keyboard/ngx'; | ||||
| import { LocalNotifications } from '@ionic-native/local-notifications/ngx'; | ||||
| import { Media } from '@ionic-native/media/ngx'; | ||||
| import { MediaCapture } from '@ionic-native/media-capture/ngx'; | ||||
| import { Network } from '@ionic-native/network/ngx'; | ||||
| import { Push } from '@ionic-native/push/ngx'; | ||||
| import { QRScanner } from '@ionic-native/qr-scanner/ngx'; | ||||
| @ -41,12 +47,14 @@ import { WebIntent } from '@ionic-native/web-intent/ngx'; | ||||
| import { Zip } from '@ionic-native/zip/ngx'; | ||||
| 
 | ||||
| // Mock services.
 | ||||
| import { CameraMock } from './services/camera'; | ||||
| import { ClipboardMock } from './services/clipboard'; | ||||
| import { FileMock } from './services/file'; | ||||
| import { FileOpenerMock } from './services/file-opener'; | ||||
| import { FileTransferMock } from './services/file-transfer'; | ||||
| import { GeolocationMock } from './services/geolocation'; | ||||
| import { InAppBrowserMock } from './services/inappbrowser'; | ||||
| import { MediaCaptureMock } from './services/media-capture'; | ||||
| import { NetworkMock } from './services/network'; | ||||
| import { ZipMock } from './services/zip'; | ||||
| 
 | ||||
| @ -63,9 +71,17 @@ import { ZipMock } from './services/zip'; | ||||
|     declarations: [ | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreEmulatorComponentsModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         CoreEmulatorHelperProvider, | ||||
|         CoreEmulatorCaptureHelperProvider, | ||||
|         { | ||||
|             provide: Camera, | ||||
|             deps: [Platform], | ||||
|             useFactory: (platform: Platform): Camera => platform.is('cordova') ? new Camera() : new CameraMock(), | ||||
|         }, | ||||
|         Chooser, | ||||
|         { | ||||
|             provide: Clipboard, | ||||
|             deps: [Platform], // Use platform instead of AppProvider to prevent errors with singleton injection.
 | ||||
| @ -101,6 +117,16 @@ import { ZipMock } from './services/zip'; | ||||
|         }, | ||||
|         Keyboard, | ||||
|         LocalNotifications, | ||||
|         { | ||||
|             provide: Media, | ||||
|             deps: [], | ||||
|             useFactory: (): Media => new Media(), | ||||
|         }, | ||||
|         { | ||||
|             provide: MediaCapture, | ||||
|             deps: [Platform], | ||||
|             useFactory: (platform: Platform): MediaCapture => platform.is('cordova') ? new MediaCapture() : new MediaCaptureMock(), | ||||
|         }, | ||||
|         { | ||||
|             provide: Network, | ||||
|             deps: [Platform], | ||||
|  | ||||
							
								
								
									
										47
									
								
								src/app/core/emulator/services/camera.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/app/core/emulator/services/camera.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Camera, CameraOptions } from '@ionic-native/camera/ngx'; | ||||
| 
 | ||||
| import { CoreEmulatorCaptureHelper } from './capture.helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Emulates the Cordova Camera plugin in browser. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CameraMock extends Camera { | ||||
| 
 | ||||
|     /** | ||||
|      * Remove intermediate image files that are kept in temporary storage after calling camera.getPicture. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|     cleanup(): Promise<any> { | ||||
|         // This function is iOS only, nothing to do.
 | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Take a picture. | ||||
|      * | ||||
|      * @param options Options that you want to pass to the camera. | ||||
|      * @return Promise resolved when captured. | ||||
|      */ | ||||
|     getPicture(options: CameraOptions): Promise<string> { | ||||
|         return CoreEmulatorCaptureHelper.instance.captureMedia('image', options); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										218
									
								
								src/app/core/emulator/services/capture.helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								src/app/core/emulator/services/capture.helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,218 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreCanceledError } from '@/app/classes/errors/cancelederror'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CameraOptions } from '@ionic-native/camera/ngx'; | ||||
| import { CaptureAudioOptions, CaptureImageOptions, CaptureVideoOptions, MediaFile } from '@ionic-native/media-capture/ngx'; | ||||
| 
 | ||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; | ||||
| import { makeSingleton, ModalController } from '@singletons/core.singletons'; | ||||
| import { CaptureMediaComponentInputs, CoreEmulatorCaptureMediaComponent } from '../components/capture-media/capture-media'; | ||||
| 
 | ||||
| /** | ||||
|  * Helper service with some features to capture media (image, audio, video). | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreEmulatorCaptureHelperProvider { | ||||
| 
 | ||||
|     protected possibleAudioMimeTypes = { | ||||
|         'audio/webm': 'weba', | ||||
|         'audio/ogg': 'ogg', | ||||
|     }; | ||||
| 
 | ||||
|     protected possibleVideoMimeTypes = { | ||||
|         'video/webm;codecs=vp9': 'webm', | ||||
|         'video/webm;codecs=vp8': 'webm', | ||||
|         'video/ogg': 'ogv', | ||||
|     }; | ||||
| 
 | ||||
|     videoMimeType?: string; | ||||
|     audioMimeType?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Capture media (image, audio, video). | ||||
|      * | ||||
|      * @param type Type of media: image, audio, video. | ||||
|      * @param options Optional options. | ||||
|      * @return Promise resolved when captured, rejected if error. | ||||
|      */ | ||||
|     captureMedia(type: 'image', options?: MockCameraOptions): Promise<string>; | ||||
|     captureMedia(type: 'captureimage', options?: MockCaptureImageOptions): Promise<MediaFile[]>; | ||||
|     captureMedia(type: 'audio', options?: MockCaptureAudioOptions): Promise<MediaFile[]>; | ||||
|     captureMedia(type: 'video', options?: MockCaptureVideoOptions): Promise<MediaFile[]>; | ||||
|     async captureMedia( | ||||
|         type: 'image' | 'captureimage' | 'audio' | 'video', | ||||
|         options?: MockCameraOptions | MockCaptureImageOptions | MockCaptureAudioOptions | MockCaptureVideoOptions, | ||||
|     ): Promise<MediaFile[] | string> { | ||||
|         options = options || {}; | ||||
| 
 | ||||
|         // Build the params to send to the modal.
 | ||||
|         const params: CaptureMediaComponentInputs = { | ||||
|             type: type, | ||||
|         }; | ||||
| 
 | ||||
|         // Initialize some data based on the type of media to capture.
 | ||||
|         if (type == 'video') { | ||||
|             const mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes); | ||||
|             params.mimetype = mimeAndExt.mimetype; | ||||
|             params.extension = mimeAndExt.extension; | ||||
|         } else if (type == 'audio') { | ||||
|             const mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes); | ||||
|             params.mimetype = mimeAndExt.mimetype; | ||||
|             params.extension = mimeAndExt.extension; | ||||
|         } else if (type == 'image') { | ||||
|             if ('sourceType' in options && options.sourceType !== undefined && options.sourceType != 1) { | ||||
|                 return Promise.reject('This source type is not supported in browser.'); | ||||
|             } | ||||
| 
 | ||||
|             if ('cameraDirection' in options && options.cameraDirection == 1) { | ||||
|                 params.facingMode = 'user'; | ||||
|             } | ||||
| 
 | ||||
|             if ('encodingType' in options && options.encodingType == 1) { | ||||
|                 params.mimetype = 'image/png'; | ||||
|                 params.extension = 'png'; | ||||
|             } else { | ||||
|                 params.mimetype = 'image/jpeg'; | ||||
|                 params.extension = 'jpeg'; | ||||
|             } | ||||
| 
 | ||||
|             if ('quality' in options && options.quality !== undefined && options.quality >= 0 && options.quality <= 100) { | ||||
|                 params.quality = options.quality / 100; | ||||
|             } | ||||
| 
 | ||||
|             if ('destinationType' in options && options.destinationType == 0) { | ||||
|                 params.returnDataUrl = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ('duration' in options && options.duration) { | ||||
|             params.maxTime = options.duration * 1000; | ||||
|         } | ||||
| 
 | ||||
|         const modal = await ModalController.instance.create({ | ||||
|             component: CoreEmulatorCaptureMediaComponent, | ||||
|             cssClass: 'core-modal-fullscreen', | ||||
|             componentProps: params, | ||||
|         }); | ||||
| 
 | ||||
|         modal.present(); | ||||
| 
 | ||||
|         const result = await modal.onDidDismiss(); | ||||
| 
 | ||||
|         if (result.role == 'success') { | ||||
|             return result.data; | ||||
|         } else { | ||||
|             throw result.data; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the mimetype and extension to capture media. | ||||
|      * | ||||
|      * @param type Type of media: image, audio, video. | ||||
|      * @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported. | ||||
|      * @return An object with mimetype and extension to use. | ||||
|      */ | ||||
|     protected getMimeTypeAndExtension(type: string, mimetypes?: string[]): { extension?: string; mimetype?: string } { | ||||
|         const result: { extension?: string; mimetype?: string } = {}; | ||||
| 
 | ||||
|         if (mimetypes?.length) { | ||||
|             // Search for a supported mimetype.
 | ||||
|             for (let i = 0; i < mimetypes.length; i++) { | ||||
|                 const mimetype = mimetypes[i]; | ||||
|                 const matches = mimetype.match(new RegExp('^' + type + '/')); | ||||
| 
 | ||||
|                 if (matches?.length && window.MediaRecorder.isTypeSupported(mimetype)) { | ||||
|                     result.mimetype = mimetype; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (result.mimetype) { | ||||
|             // Found a supported mimetype in the mimetypes array, get the extension.
 | ||||
|             result.extension = CoreMimetypeUtils.instance.getExtension(result.mimetype); | ||||
|         } else if (type == 'video') { | ||||
|             // No mimetype found, use default extension.
 | ||||
|             result.mimetype = this.videoMimeType; | ||||
|             result.extension = this.possibleVideoMimeTypes[result.mimetype!]; | ||||
|         } else if (type == 'audio') { | ||||
|             // No mimetype found, use default extension.
 | ||||
|             result.mimetype = this.audioMimeType; | ||||
|             result.extension = this.possibleAudioMimeTypes[result.mimetype!]; | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init the getUserMedia function, using a deprecated function as fallback if the new one doesn't exist. | ||||
|      * | ||||
|      * @return Whether the function is supported. | ||||
|      */ | ||||
|     protected initGetUserMedia(): boolean { | ||||
|         return !!navigator.mediaDevices.getUserMedia; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the mimetypes to use when capturing. | ||||
|      */ | ||||
|     protected initMimeTypes(): void { | ||||
|         // Determine video and audio mimetype to use.
 | ||||
|         for (const mimeType in this.possibleVideoMimeTypes) { | ||||
|             if (window.MediaRecorder.isTypeSupported(mimeType)) { | ||||
|                 this.videoMimeType = mimeType; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (const mimeType in this.possibleAudioMimeTypes) { | ||||
|             if (window.MediaRecorder.isTypeSupported(mimeType)) { | ||||
|                 this.audioMimeType = mimeType; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the Mocks that need it. | ||||
|      * | ||||
|      * @return Promise resolved when loaded. | ||||
|      */ | ||||
|     load(): Promise<void> { | ||||
|         if (typeof window.MediaRecorder != 'undefined' && this.initGetUserMedia()) { | ||||
|             this.initMimeTypes(); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class CoreEmulatorCaptureHelper extends makeSingleton(CoreEmulatorCaptureHelperProvider) {} | ||||
| 
 | ||||
| export interface MockCameraOptions extends CameraOptions { | ||||
|     mimetypes?: string[]; // Allowed mimetypes.
 | ||||
| } | ||||
| export interface MockCaptureImageOptions extends CaptureImageOptions { | ||||
|     mimetypes?: string[]; // Allowed mimetypes.
 | ||||
| } | ||||
| export interface MockCaptureAudioOptions extends CaptureAudioOptions { | ||||
|     mimetypes?: string[]; // Allowed mimetypes.
 | ||||
| } | ||||
| export interface MockCaptureVideoOptions extends CaptureVideoOptions { | ||||
|     mimetypes?: string[]; // Allowed mimetypes.
 | ||||
| } | ||||
							
								
								
									
										62
									
								
								src/app/core/emulator/services/media-capture.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/app/core/emulator/services/media-capture.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { | ||||
|     MediaCapture, | ||||
|     CaptureAudioOptions, | ||||
|     CaptureImageOptions, | ||||
|     CaptureVideoOptions, | ||||
|     MediaFile, | ||||
| } from '@ionic-native/media-capture/ngx'; | ||||
| 
 | ||||
| import { CoreEmulatorCaptureHelper } from './capture.helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Emulates the Cordova MediaCapture plugin in browser. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class MediaCaptureMock extends MediaCapture { | ||||
| 
 | ||||
|     /** | ||||
|      * Start the audio recorder application and return information about captured audio clip files. | ||||
|      * | ||||
|      * @param options Options. | ||||
|      * @return Promise resolved when captured. | ||||
|      */ | ||||
|     captureAudio(options: CaptureAudioOptions): Promise<MediaFile[]> { | ||||
|         return CoreEmulatorCaptureHelper.instance.captureMedia('audio', options); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start the camera application and return information about captured image files. | ||||
|      * | ||||
|      * @param options Options. | ||||
|      * @return Promise resolved when captured. | ||||
|      */ | ||||
|     captureImage(options: CaptureImageOptions): Promise<MediaFile[]> { | ||||
|         return CoreEmulatorCaptureHelper.instance.captureMedia('captureimage', options); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Start the video recorder application and return information about captured video clip files. | ||||
|      * | ||||
|      * @param options Options. | ||||
|      * @return Promise resolved when captured. | ||||
|      */ | ||||
|     captureVideo(options: CaptureVideoOptions): Promise<MediaFile[]> { | ||||
|         return CoreEmulatorCaptureHelper.instance.captureMedia('video', options); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user