diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts
index d4724b600..1701696d3 100644
--- a/src/core/emulator/emulator.module.ts
+++ b/src/core/emulator/emulator.module.ts
@@ -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],
diff --git a/src/core/emulator/pages/capture-media/capture-media.html b/src/core/emulator/pages/capture-media/capture-media.html
new file mode 100644
index 000000000..a6fc6dbe1
--- /dev/null
+++ b/src/core/emulator/pages/capture-media/capture-media.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/emulator/pages/capture-media/capture-media.module.ts b/src/core/emulator/pages/capture-media/capture-media.module.ts
new file mode 100644
index 000000000..133671a75
--- /dev/null
+++ b/src/core/emulator/pages/capture-media/capture-media.module.ts
@@ -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 {}
diff --git a/src/core/emulator/pages/capture-media/capture-media.scss b/src/core/emulator/pages/capture-media/capture-media.scss
new file mode 100644
index 000000000..58bb5d1a2
--- /dev/null
+++ b/src/core/emulator/pages/capture-media/capture-media.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/core/emulator/pages/capture-media/capture-media.ts b/src/core/emulator/pages/capture-media/capture-media.ts
new file mode 100644
index 000000000..ba257c29f
--- /dev/null
+++ b/src/core/emulator/pages/capture-media/capture-media.ts
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/core/emulator/providers/camera.ts b/src/core/emulator/providers/camera.ts
new file mode 100644
index 000000000..681fe1ca6
--- /dev/null
+++ b/src/core/emulator/providers/camera.ts
@@ -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} Promise resolved when done.
+ */
+ cleanup() : Promise {
+ // 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} Promise resolved when captured.
+ */
+ getPicture(options: CameraOptions) : Promise {
+ return this.captureHelper.captureMedia('image', options);
+ }
+}
diff --git a/src/core/emulator/providers/capture-helper.ts b/src/core/emulator/providers/capture-helper.ts
new file mode 100644
index 000000000..3d03605ba
--- /dev/null
+++ b/src/core/emulator/providers/capture-helper.ts
@@ -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 = 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 {
+ 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 = 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} Promise resolved when loaded.
+ */
+ load() : Promise {
+ if (typeof this.win.MediaRecorder != 'undefined' && this.initGetUserMedia()) {
+ this.initMimeTypes();
+ }
+
+ return Promise.resolve();
+ }
+}
diff --git a/src/core/emulator/providers/helper.ts b/src/core/emulator/providers/helper.ts
index abfd542fa..1a6e2fa86 100644
--- a/src/core/emulator/providers/helper.ts
+++ b/src/core/emulator/providers/helper.ts
@@ -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((this.localNotif).load());
+ promises.push(this.captureHelper.load());
(window).FileTransferError = FileTransferErrorMock;
diff --git a/src/core/emulator/providers/media-capture.ts b/src/core/emulator/providers/media-capture.ts
new file mode 100644
index 000000000..55a8b65ce
--- /dev/null
+++ b/src/core/emulator/providers/media-capture.ts
@@ -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} Promise resolved when captured.
+ */
+ captureAudio(options: CaptureAudioOptions) : Promise {
+ return this.captureHelper.captureMedia('audio', options);
+ }
+
+ /**
+ * Start the camera application and return information about captured image files.
+ *
+ * @param {CaptureImageOptions} options Options.
+ * @return {Promise} Promise resolved when captured.
+ */
+ captureImage(options: CaptureImageOptions) : Promise {
+ 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} Promise resolved when captured.
+ */
+ captureVideo(options: CaptureVideoOptions) : Promise {
+ return this.captureHelper.captureMedia('video', options);
+ }
+}