MOBILE-3585 emulator: Add mocks of media services
parent
f581dbcc7c
commit
4bb7f0e97f
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
};
|
|
@ -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],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
}
|
|
@ -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…
Reference in New Issue