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": { |     "@ionic-native/clipboard": { | ||||||
|       "version": "5.28.0", |       "version": "5.28.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-5.28.0.tgz", |       "resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-5.28.0.tgz", | ||||||
| @ -2247,6 +2277,36 @@ | |||||||
|         "@types/cordova": "^0.0.34" |         "@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": { |     "@ionic-native/network": { | ||||||
|       "version": "5.28.0", |       "version": "5.28.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/network/-/network-5.28.0.tgz", |       "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": "~10.0.0", | ||||||
|     "@angular/platform-browser-dynamic": "~10.0.0", |     "@angular/platform-browser-dynamic": "~10.0.0", | ||||||
|     "@angular/router": "~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/clipboard": "^5.28.0", | ||||||
|     "@ionic-native/core": "^5.0.0", |     "@ionic-native/core": "^5.0.0", | ||||||
|     "@ionic-native/device": "^5.28.0", |     "@ionic-native/device": "^5.28.0", | ||||||
| @ -51,6 +53,8 @@ | |||||||
|     "@ionic-native/ionic-webview": "^5.28.0", |     "@ionic-native/ionic-webview": "^5.28.0", | ||||||
|     "@ionic-native/keyboard": "^5.28.0", |     "@ionic-native/keyboard": "^5.28.0", | ||||||
|     "@ionic-native/local-notifications": "^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/network": "^5.28.0", | ||||||
|     "@ionic-native/push": "^5.28.0", |     "@ionic-native/push": "^5.28.0", | ||||||
|     "@ionic-native/qr-scanner": "^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 { CoreInitDelegate } from '@services/init'; | ||||||
| import { CoreEmulatorHelperProvider } from './services/helper'; | import { CoreEmulatorHelperProvider } from './services/helper'; | ||||||
|  | import { CoreEmulatorCaptureHelperProvider } from './services/capture.helper'; | ||||||
|  | import { CoreEmulatorComponentsModule } from './components/components.module'; | ||||||
| 
 | 
 | ||||||
| // Ionic Native services.
 | // 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 { Clipboard } from '@ionic-native/clipboard/ngx'; | ||||||
| import { Device } from '@ionic-native/device/ngx'; | import { Device } from '@ionic-native/device/ngx'; | ||||||
| import { Diagnostic } from '@ionic-native/diagnostic/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 { WebView } from '@ionic-native/ionic-webview/ngx'; | ||||||
| import { Keyboard } from '@ionic-native/keyboard/ngx'; | import { Keyboard } from '@ionic-native/keyboard/ngx'; | ||||||
| import { LocalNotifications } from '@ionic-native/local-notifications/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 { Network } from '@ionic-native/network/ngx'; | ||||||
| import { Push } from '@ionic-native/push/ngx'; | import { Push } from '@ionic-native/push/ngx'; | ||||||
| import { QRScanner } from '@ionic-native/qr-scanner/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'; | import { Zip } from '@ionic-native/zip/ngx'; | ||||||
| 
 | 
 | ||||||
| // Mock services.
 | // Mock services.
 | ||||||
|  | import { CameraMock } from './services/camera'; | ||||||
| import { ClipboardMock } from './services/clipboard'; | import { ClipboardMock } from './services/clipboard'; | ||||||
| import { FileMock } from './services/file'; | import { FileMock } from './services/file'; | ||||||
| import { FileOpenerMock } from './services/file-opener'; | import { FileOpenerMock } from './services/file-opener'; | ||||||
| import { FileTransferMock } from './services/file-transfer'; | import { FileTransferMock } from './services/file-transfer'; | ||||||
| import { GeolocationMock } from './services/geolocation'; | import { GeolocationMock } from './services/geolocation'; | ||||||
| import { InAppBrowserMock } from './services/inappbrowser'; | import { InAppBrowserMock } from './services/inappbrowser'; | ||||||
|  | import { MediaCaptureMock } from './services/media-capture'; | ||||||
| import { NetworkMock } from './services/network'; | import { NetworkMock } from './services/network'; | ||||||
| import { ZipMock } from './services/zip'; | import { ZipMock } from './services/zip'; | ||||||
| 
 | 
 | ||||||
| @ -63,9 +71,17 @@ import { ZipMock } from './services/zip'; | |||||||
|     declarations: [ |     declarations: [ | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|  |         CoreEmulatorComponentsModule, | ||||||
|     ], |     ], | ||||||
|     providers: [ |     providers: [ | ||||||
|         CoreEmulatorHelperProvider, |         CoreEmulatorHelperProvider, | ||||||
|  |         CoreEmulatorCaptureHelperProvider, | ||||||
|  |         { | ||||||
|  |             provide: Camera, | ||||||
|  |             deps: [Platform], | ||||||
|  |             useFactory: (platform: Platform): Camera => platform.is('cordova') ? new Camera() : new CameraMock(), | ||||||
|  |         }, | ||||||
|  |         Chooser, | ||||||
|         { |         { | ||||||
|             provide: Clipboard, |             provide: Clipboard, | ||||||
|             deps: [Platform], // Use platform instead of AppProvider to prevent errors with singleton injection.
 |             deps: [Platform], // Use platform instead of AppProvider to prevent errors with singleton injection.
 | ||||||
| @ -101,6 +117,16 @@ import { ZipMock } from './services/zip'; | |||||||
|         }, |         }, | ||||||
|         Keyboard, |         Keyboard, | ||||||
|         LocalNotifications, |         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, |             provide: Network, | ||||||
|             deps: [Platform], |             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