MOBILE-2312 emulator: Emulate Camera and MediaCapture

main
Dani Palou 2018-01-08 09:21:51 +01:00
parent 57d4d9d8ff
commit 556085e13c
9 changed files with 920 additions and 2 deletions

View File

@ -15,6 +15,8 @@
import { NgModule } from '@angular/core';
import { Platform } from 'ionic-angular';
// Ionic Native services.
import { Camera } from '@ionic-native/camera';
import { Clipboard } from '@ionic-native/clipboard';
import { File } from '@ionic-native/file';
import { FileTransfer } from '@ionic-native/file-transfer';
@ -22,22 +24,27 @@ import { Globalization } from '@ionic-native/globalization';
import { InAppBrowser } from '@ionic-native/in-app-browser';
import { Keyboard } from '@ionic-native/keyboard';
import { LocalNotifications } from '@ionic-native/local-notifications';
import { MediaCapture } from '@ionic-native/media-capture';
import { Network } from '@ionic-native/network';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { SQLite } from '@ionic-native/sqlite';
import { Zip } from '@ionic-native/zip';
// Services that Mock Ionic Native in browser an desktop.
import { CameraMock } from './providers/camera';
import { ClipboardMock } from './providers/clipboard';
import { FileMock } from './providers/file';
import { FileTransferMock } from './providers/file-transfer';
import { GlobalizationMock } from './providers/globalization';
import { InAppBrowserMock } from './providers/inappbrowser';
import { LocalNotificationsMock } from './providers/local-notifications';
import { MediaCaptureMock } from './providers/media-capture';
import { NetworkMock } from './providers/network';
import { ZipMock } from './providers/zip';
import { CoreEmulatorHelperProvider } from './providers/helper';
import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper';
import { CoreAppProvider } from '../../providers/app';
import { CoreFileProvider } from '../../providers/file';
import { CoreTextUtilsProvider } from '../../providers/utils/text';
@ -46,6 +53,15 @@ import { CoreUrlUtilsProvider } from '../../providers/utils/url';
import { CoreUtilsProvider } from '../../providers/utils/utils';
import { CoreInitDelegate } from '../../providers/init';
/**
* This module handles the emulation of Cordova plugins in browser and desktop.
*
* It includes the "mock" of all the Ionic Native services that should be supported in browser and desktop,
* otherwise those features would only work in a Cordova environment.
*
* This module also determines if the app should use the original service or the mock. In each of the "useFactory"
* functions we check if the app is running in mobile or not, and then provide the right service to use.
*/
@NgModule({
declarations: [
],
@ -53,6 +69,14 @@ import { CoreInitDelegate } from '../../providers/init';
],
providers: [
CoreEmulatorHelperProvider,
CoreEmulatorCaptureHelperProvider,
{
provide: Camera,
deps: [CoreAppProvider, CoreEmulatorCaptureHelperProvider],
useFactory: (appProvider: CoreAppProvider, captureHelper: CoreEmulatorCaptureHelperProvider) => {
return appProvider.isMobile() ? new Camera() : new CameraMock(captureHelper);
}
},
{
provide: Clipboard,
deps: [CoreAppProvider],
@ -99,6 +123,13 @@ import { CoreInitDelegate } from '../../providers/init';
return appProvider.isMobile() ? new LocalNotifications() : new LocalNotificationsMock(appProvider, utils);
}
},
{
provide: MediaCapture,
deps: [CoreAppProvider, CoreEmulatorCaptureHelperProvider],
useFactory: (appProvider: CoreAppProvider, captureHelper: CoreEmulatorCaptureHelperProvider) => {
return appProvider.isMobile() ? new MediaCapture() : new MediaCaptureMock(captureHelper);
}
},
{
provide: Network,
deps: [Platform],

View File

@ -0,0 +1,55 @@
<ion-header>
<ion-navbar>
<ion-buttons start>
<button ion-button (click)="cancel()">{{ 'core.cancel' | translate }}</button>
</ion-buttons>
<ion-title>{{ title }}</ion-title>
<ion-buttons end>
<button ion-button *ngIf="hasCaptured" (click)="done()">{{ 'core.done' | translate }}</button>
</ion-buttons>
</ion-navbar>
</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>
<!-- Canvas to show audio waves when recording audio and audio player to listen to the result. -->
<div *ngIf="isAudio" class="core-audio-record-container">
<canvas [hidden]="hasCaptured" class="core-audio-canvas" #streamAudio></canvas>
<audio [hidden]="!hasCaptured" class="core-audio-captured" controls #previewAudio></audio>
</div>
</div>
</core-loading>
</ion-content>
<ion-footer>
<ion-row *ngIf="readyToCapture">
<ion-col></ion-col>
<ion-col text-center>
<button ion-button icon-only clear *ngIf="!hasCaptured" (click)="actionClicked()" [attr.aria-label]="title">
<ion-icon *ngIf="!isCapturing && isAudio" name="microphone"></ion-icon>
<ion-icon *ngIf="!isCapturing && isVideo" name="videocam"></ion-icon>
<ion-icon *ngIf="isImage" name="camera"></ion-icon>
<ion-icon *ngIf="isCapturing" name="square"></ion-icon>
</button>
<button ion-button icon-only clear *ngIf="hasCaptured" (click)="discard()" [attr.aria-label]="'core.discard' | translate">
<ion-icon name="trash"></ion-icon>
</button>
</ion-col>
<ion-col padding text-right class="chrono-container">
<core-chrono *ngIf="!isImage" [hidden]="hasCaptured" [running]="isCapturing" [reset]="resetChrono" [endTime]="maxTime" (onEnd)="stopCapturing()"></core-chrono>
</ion-col>
</ion-row>
</ion-footer>

View File

@ -0,0 +1,31 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { CoreEmulatorCaptureMediaPage } from './capture-media';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../../components/components.module';
@NgModule({
declarations: [
CoreEmulatorCaptureMediaPage
],
imports: [
CoreComponentsModule,
IonicPageModule.forChild(CoreEmulatorCaptureMediaPage),
TranslateModule.forChild()
]
})
export class CoreEmulatorCaptureMediaPageModule {}

View File

@ -0,0 +1,68 @@
page-core-emulator-capture-media {
ion-content {
.core-av-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 0;
clear: both;
.core-webcam-image-canvas {
display: none;
}
.core-audio-record-container {
width: 100%;
height: 100%;
.core-audio-canvas {
width: 100%;
height: 100%;
}
.core-audio-captured {
width: 100%;
}
}
audio, 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);
}
}
}
.scroll-content {
// We're modifying the height of the footer, the padding-bottom of the scroll needs to change too.
margin-bottom: 44px !important;
}
}
ion-footer {
background-color: $gray;
border-top: 1px solid $gray-dark;
.col {
padding: 0;
.icon.ion-md-trash, .icon.ion-ios-trash {
color: $red;
}
}
.chrono-container {
line-height: 24px;
}
}
}

View File

@ -0,0 +1,402 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { CoreFileProvider } from '../../../../providers/file';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreTimeUtilsProvider } from '../../../../providers/utils/time';
/**
* Page to capture media in browser or desktop.
*/
@IonicPage()
@Component({
selector: 'page-core-emulator-capture-media',
templateUrl: 'capture-media.html',
})
export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
@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.
maxTime: number; // The max time to capture.
resetChrono: boolean; // Boolean to reset the chrono.
protected type: string; // The type to capture: audio, video, image, captureimage.
protected isCaptureImage: boolean; // To identify if it's capturing an image using media capture plugin (instead of camera).
protected returnDataUrl: boolean; // Whether it should return a data img. Only if isImage.
protected facingMode: string; // Camera facing mode.
protected mimetype: string;
protected extension: string;
protected window: any; // Cast window to "any" because some of the properties used aren't in the window spec.
protected mediaRecorder; // To record video/audio.
protected audioDrawer; // To start/stop the display of audio sound.
protected quality; // Image only.
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;
constructor(private viewCtrl: ViewController, params: NavParams, private domUtils: CoreDomUtilsProvider,
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
private textUtils: CoreTextUtilsProvider, private cdr: ChangeDetectorRef) {
this.window = window;
this.type = params.get('type');
this.maxTime = params.get('maxTime');
this.facingMode = params.get('facingMode') || 'environment';
this.mimetype = params.get('mimetype');
this.extension = params.get('extension');
this.quality = params.get('quality') || 0.92;
this.returnDataUrl = !!params.get('returnDataUrl');
}
/**
* Component being initialized.
*/
ngOnInit() {
this.initVariables();
let constraints = {
video: this.isAudio ? false : {facingMode: this.facingMode},
audio: !this.isImage
};
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
let chunks = [];
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 this.window.MediaRecorder(this.localMediaStream, {mimeType: this.mimetype});
// When video or audio is recorded, add it to the list of chunks.
this.mediaRecorder.ondataavailable = (e) => {
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 = () => {
this.mediaBlob = new Blob(chunks);
chunks = [];
this.previewMedia.src = window.URL.createObjectURL(this.mediaBlob);
};
}
if (this.isImage || this.isVideo) {
let hasLoaded = false,
waitTimeout;
// Listen for stream ready to display the stream.
this.streamVideo.nativeElement.onloadedmetadata = () => {
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.cdr.detectChanges();
};
// Set the stream as the source of the video.
this.streamVideo.nativeElement.src = window.URL.createObjectURL(this.localMediaStream);
// If stream isn't ready in a while, show error.
waitTimeout = setTimeout(() => {
if (!hasLoaded) {
// Show error.
hasLoaded = true;
this.dismissWithError(-1, 'Cannot connect to webcam.');
}
}, 10000);
} else {
// It's ready to capture.
this.readyToCapture = true;
}
}).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 {MediaStream} stream Stream returned by getUserMedia.
*/
protected initAudioDrawer(stream: MediaStream) : void {
let audioCtx = new (this.window.AudioContext || this.window.webkitAudioContext)(),
canvasCtx = this.streamAudio.nativeElement.getContext('2d'),
source = audioCtx.createMediaStreamSource(stream),
analyser = audioCtx.createAnalyser(),
bufferLength = analyser.frequencyBinCount,
dataArray = new Uint8Array(bufferLength),
width = this.streamAudio.nativeElement.width,
height = this.streamAudio.nativeElement.height,
running = false,
skip = true,
drawAudio = () => {
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;
}
let sliceWidth = width / bufferLength,
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++) {
let v = dataArray[i] / 128.0,
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: () => {
if (running) {
return;
}
running = true;
drawAudio();
},
stop: () => {
running = false;
}
};
}
/**
* Initialize some variables based on the params.
*/
protected initVariables() {
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';
}
}
/**
* Main action clicked: record or stop recording.
*/
actionClicked() : void {
if (this.isCapturing) {
// It's capturing, stop.
this.stopCapturing();
this.cdr.detectChanges();
} else {
if (!this.isImage) {
// Start the capture.
this.isCapturing = true;
this.resetChrono = false;
this.mediaRecorder.start();
this.cdr.detectChanges();
} else {
// Get the image from the video and set it to the canvas, using video width/height.
let width = this.streamVideo.nativeElement.videoWidth,
height = this.streamVideo.nativeElement.videoHeight;
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.
let loadingModal = this.domUtils.showModalLoading();
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.
*/
cancel() : void {
// Send a "cancelled" error like the Cordova plugin does.
this.dismissWithError(3, 'Canceled.', 'Camera cancelled');
}
/**
* Discard the captured media.
*/
discard() : void {
this.previewMedia && this.previewMedia.pause();
this.streamVideo && this.streamVideo.nativeElement.play();
this.audioDrawer && this.audioDrawer.start();
this.hasCaptured = false;
this.isCapturing = false;
this.resetChrono = true;
delete this.mediaBlob;
this.cdr.detectChanges();
};
/**
* Close the modal, returning some data (success).
*
* @param {any} data Data to return.
*/
dismissWithData(data: any) : void {
this.viewCtrl.dismiss(data, 'success');
}
/**
* Close the modal, returning an error.
*
* @param {number} code Error code. Will not be used if it's a Camera capture.
* @param {string} message Error message.
* @param {string} [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 {
let isCamera = this.isImage && !this.isCaptureImage,
error = isCamera ? (cameraMessage || message) : {code: code, message: message};
this.viewCtrl.dismiss(error, 'error');
}
/**
* Done capturing, write the file.
*/
done() : void {
if (this.returnDataUrl) {
// Return the image as a base64 string.
this.dismissWithData(this.imgCanvas.nativeElement.toDataURL(this.mimetype, this.quality));
return;
}
if (!this.mediaBlob) {
// Shouldn't happen.
this.domUtils.showErrorModal('Please capture the media first.');
return;
}
// Create the file and return it.
let fileName = this.type + '_' + this.timeUtils.readableTimestamp() + '.' + this.extension,
path = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, 'media/' + fileName);
let loadingModal = this.domUtils.showModalLoading();
this.fileProvider.writeFile(path, this.mediaBlob).then((fileEntry) => {
if (this.isImage && !this.isCaptureImage) {
this.dismissWithData(fileEntry.toURL());
} else {
// The capture plugin returns a MediaFile, not a FileEntry. The only difference is that
// it supports a new function that won't be supported in desktop.
fileEntry.getFormatData = (successFn, errorFn) => {};
this.dismissWithData([fileEntry]);
}
}).catch((err) => {
this.domUtils.showErrorModal(err);
}).finally(() => {
loadingModal.dismiss();
});
};
/**
* Stop capturing. Only for video and audio.
*/
stopCapturing() : void {
this.streamVideo && this.streamVideo.nativeElement.pause();
this.audioDrawer && this.audioDrawer.stop();
this.mediaRecorder && this.mediaRecorder.stop();
this.isCapturing = false;
this.hasCaptured = true;
};
/**
* Page destroyed.
*/
ngOnDestroy() : void {
const tracks = this.localMediaStream.getTracks();
tracks.forEach((track) => {
track.stop();
});
this.streamVideo && this.streamVideo.nativeElement.pause();
this.previewMedia && this.previewMedia.pause();
this.audioDrawer && this.audioDrawer.stop();
delete this.mediaBlob;
}
}

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
/**
* Emulates the Cordova Camera plugin in desktop apps and in browser.
*/
@Injectable()
export class CameraMock extends Camera {
constructor(private captureHelper: CoreEmulatorCaptureHelperProvider) {
super();
}
/**
* Remove intermediate image files that are kept in temporary storage after calling camera.getPicture.
*
* @return {Promise<any>} Promise resolved when done.
*/
cleanup() : Promise<any> {
// iOS only, nothing to do.
return Promise.resolve();
}
/**
* Take a picture.
*
* @param {CameraOptions} options Options that you want to pass to the camera.
* @return {Promise<any>} Promise resolved when captured.
*/
getPicture(options: CameraOptions) : Promise<any> {
return this.captureHelper.captureMedia('image', options);
}
}

View File

@ -0,0 +1,222 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { ModalController, Modal } from 'ionic-angular';
import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
/**
* 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'
};
protected win: any;
videoMimeType: string;
audioMimeType: string;
constructor(private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider,
private modalCtrl: ModalController) {
// Convert the window to "any" type because some of the variables used (like MediaRecorder) aren't in the window spec.
this.win = <any>window;
}
/**
* Capture media (image, audio, video).
*
* @param {String} type Type of media: image, audio, video.
* @param {Function} successCallback Function called when media taken.
* @param {Function} errorCallback Function called when error or cancel.
* @param {Object} [options] Optional options.
* @return {Void}
*/
captureMedia(type: string, options: any) : Promise<any> {
options = options || {};
try {
// Build the params to send to the modal.
let deferred = this.utils.promiseDefer(),
params: any = {
type: type
},
mimeAndExt,
modal: Modal;
// Initialize some data based on the type of media to capture.
if (type == 'video') {
mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes);
params.mimetype = mimeAndExt.mimetype;
params.extension = mimeAndExt.extension;
} else if (type == 'audio') {
mimeAndExt = this.getMimeTypeAndExtension(type, options.mimetypes);
params.mimetype = mimeAndExt.mimetype;
params.extension = mimeAndExt.extension;
} else if (type == 'image') {
if (typeof options.sourceType != 'undefined' && options.sourceType != 1) {
return Promise.reject('This source type is not supported in desktop.');
}
if (options.cameraDirection == 1) {
params.facingMode = 'user';
}
if (options.encodingType == 1) {
params.mimetype = 'image/png';
params.extension = 'png';
} else {
params.mimetype = 'image/jpeg';
params.extension = 'jpeg';
}
if (options.quality >= 0 && options.quality <= 100) {
params.quality = options.quality / 100;
}
if (options.destinationType == 0) {
params.returnDataUrl = true;
}
}
if (options.duration) {
params.maxTime = options.duration * 1000;
}
modal = this.modalCtrl.create('CoreEmulatorCaptureMediaPage', params);
modal.present();
modal.onDidDismiss((data: any, role: string) => {
if (role == 'success') {
deferred.resolve(data);
} else {
deferred.reject(data);
}
});
return deferred.promise;
} catch(ex) {
return Promise.reject(ex.toString());
}
}
/**
* Get the mimetype and extension to capture media.
*
* @param {string} type Type of media: image, audio, video.
* @param {string[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {{extension: string, mimetype: string}} An object with mimetype and extension to use.
*/
protected getMimeTypeAndExtension(type: string, mimetypes) : {extension: string, mimetype: string} {
var result: any = {};
if (mimetypes && mimetypes.length) {
// Search for a supported mimetype.
for (let i = 0; i < mimetypes.length; i++) {
let mimetype = mimetypes[i],
matches = mimetype.match(new RegExp('^' + type + '/'));
if (matches && matches.length && this.win.MediaRecorder.isTypeSupported(mimetype)) {
result.mimetype = mimetype;
break;
}
}
}
if (result.mimetype) {
// Found a supported mimetype in the mimetypes array, get the extension.
result.extension = this.mimeUtils.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 {boolean} Whether the function is supported.
*/
protected initGetUserMedia() : boolean {
let nav = <any>navigator;
// Check if there is a function to get user media.
if (typeof nav.mediaDevices == 'undefined') {
nav.mediaDevices = {};
}
if (!nav.mediaDevices.getUserMedia) {
// New function doesn't exist, check if the deprecated function is supported.
nav.getUserMedia = nav.getUserMedia || nav.webkitGetUserMedia || nav.mozGetUserMedia || nav.msGetUserMedia;
if (nav.getUserMedia) {
// Deprecated function exists, support the new function using the deprecated one.
navigator.mediaDevices.getUserMedia = (constraints) => {
let deferred = this.utils.promiseDefer();
nav.getUserMedia(constraints, deferred.resolve, deferred.reject);
return deferred.promise;
};
} else {
return false;
}
}
return true;
}
/**
* Initialize the mimetypes to use when capturing.
*/
protected initMimeTypes() : void {
// Determine video and audio mimetype to use.
for (let mimeType in this.possibleVideoMimeTypes) {
if (this.win.MediaRecorder.isTypeSupported(mimeType)) {
this.videoMimeType = mimeType;
break;
}
}
for (let mimeType in this.possibleAudioMimeTypes) {
if (this.win.MediaRecorder.isTypeSupported(mimeType)) {
this.audioMimeType = mimeType;
break;
}
}
}
/**
* Load the Mocks that need it.
*
* @return {Promise<void>} Promise resolved when loaded.
*/
load() : Promise<void> {
if (typeof this.win.MediaRecorder != 'undefined' && this.initGetUserMedia()) {
this.initMimeTypes();
}
return Promise.resolve();
}
}

View File

@ -19,9 +19,10 @@ import { File } from '@ionic-native/file';
import { LocalNotifications } from '@ionic-native/local-notifications';
import { CoreInitDelegate, CoreInitHandler } from '../../../providers/init';
import { FileTransferErrorMock } from './file-transfer';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
/**
* Emulates the Cordova Zip plugin in desktop apps and in browser.
* Helper service for the emulator feature. It also acts as an init handler.
*/
@Injectable()
export class CoreEmulatorHelperProvider implements CoreInitHandler {
@ -30,7 +31,8 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler {
blocking = true;
constructor(private file: File, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider,
initDelegate: CoreInitDelegate, private localNotif: LocalNotifications) {}
initDelegate: CoreInitDelegate, private localNotif: LocalNotifications,
private captureHelper: CoreEmulatorCaptureHelperProvider) {}
/**
* Load the Mocks that need it.
@ -44,6 +46,7 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler {
this.fileProvider.setHTMLBasePath(basePath);
}));
promises.push((<any>this.localNotif).load());
promises.push(this.captureHelper.load());
(<any>window).FileTransferError = FileTransferErrorMock;

View File

@ -0,0 +1,58 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@ionic-native/media-capture';
import { CoreEmulatorCaptureHelperProvider } from './capture-helper';
/**
* Emulates the Cordova MediaCapture plugin in desktop apps and in browser.
*/
@Injectable()
export class MediaCaptureMock extends MediaCapture {
constructor(private captureHelper: CoreEmulatorCaptureHelperProvider) {
super();
}
/**
* Start the audio recorder application and return information about captured audio clip files.
*
* @param {CaptureAudioOptions} options Options.
* @return {Promise<any>} Promise resolved when captured.
*/
captureAudio(options: CaptureAudioOptions) : Promise<any> {
return this.captureHelper.captureMedia('audio', options);
}
/**
* Start the camera application and return information about captured image files.
*
* @param {CaptureImageOptions} options Options.
* @return {Promise<any>} Promise resolved when captured.
*/
captureImage(options: CaptureImageOptions) : Promise<any> {
return this.captureHelper.captureMedia('captureimage', options);
}
/**
* Start the video recorder application and return information about captured video clip files.
*
* @param {CaptureVideoOptions} options Options.
* @return {Promise<any>} Promise resolved when captured.
*/
captureVideo(options: CaptureVideoOptions) : Promise<any> {
return this.captureHelper.captureMedia('video', options);
}
}