MOBILE-2312 emulator: Emulate Camera and MediaCapture
parent
57d4d9d8ff
commit
556085e13c
|
@ -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],
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue