MOBILE-3490 core: Record audio in app if no recording app installed
parent
b67ea14abb
commit
384c4372fe
|
@ -214,6 +214,11 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
</config-file>
|
</config-file>
|
||||||
|
<config-file parent="/*" target="res/xml/config.xml">
|
||||||
|
<feature name="Media">
|
||||||
|
<param name="android-package" value="org.apache.cordova.media.AudioHandler" />
|
||||||
|
</feature>
|
||||||
|
</config-file>
|
||||||
</platform>
|
</platform>
|
||||||
<platform name="ios">
|
<platform name="ios">
|
||||||
<resource-file src="GoogleService-Info.plist" />
|
<resource-file src="GoogleService-Info.plist" />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "moodlemobile",
|
"name": "moodlemobile",
|
||||||
"version": "3.9.2",
|
"version": "3.9.3",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -172,6 +172,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/local-notifications/-/local-notifications-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/local-notifications/-/local-notifications-4.20.0.tgz",
|
||||||
"integrity": "sha512-Ht/0zau8/2+G/bH/okXXhhWB6YrkCNL2QxVJHQ2dophXFGxQPOZAN3CKWhuQSjfbr76fa2nvQXF6jsXLpIR/ng=="
|
"integrity": "sha512-Ht/0zau8/2+G/bH/okXXhhWB6YrkCNL2QxVJHQ2dophXFGxQPOZAN3CKWhuQSjfbr76fa2nvQXF6jsXLpIR/ng=="
|
||||||
},
|
},
|
||||||
|
"@ionic-native/media": {
|
||||||
|
"version": "4.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ionic-native/media/-/media-4.20.0.tgz",
|
||||||
|
"integrity": "sha512-uhuTvy7MT6zFMSTDX/0aIrGu8IeRGi2FWJbWE+6o5wttAeVA6hNISSbtj4OQZhL3sUXYNCczDayV1VsOcXbdUg=="
|
||||||
|
},
|
||||||
"@ionic-native/media-capture": {
|
"@ionic-native/media-capture": {
|
||||||
"version": "4.20.0",
|
"version": "4.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-4.20.0.tgz",
|
||||||
|
@ -3169,9 +3174,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"compare-func": {
|
"compare-func": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.4.tgz",
|
||||||
"integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=",
|
"integrity": "sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"array-ify": "^1.0.0",
|
"array-ify": "^1.0.0",
|
||||||
"dot-prop": "^3.0.0"
|
"dot-prop": "^3.0.0"
|
||||||
|
@ -3728,6 +3733,11 @@
|
||||||
"version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836",
|
"version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836",
|
||||||
"from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle"
|
"from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle"
|
||||||
},
|
},
|
||||||
|
"cordova-plugin-media": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cordova-plugin-media/-/cordova-plugin-media-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-UQPFlpk1zL4BY44zGi8RVmYCvcKBCN4Dyf8ovxqGYCC8zR1yhbTRWYDdO9vJdERwbfgWV7+z7FMWiSUfqWm9bQ=="
|
||||||
|
},
|
||||||
"cordova-plugin-media-capture": {
|
"cordova-plugin-media-capture": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.3.tgz",
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
"@ionic-native/in-app-browser": "^4.20.0",
|
"@ionic-native/in-app-browser": "^4.20.0",
|
||||||
"@ionic-native/keyboard": "^4.20.0",
|
"@ionic-native/keyboard": "^4.20.0",
|
||||||
"@ionic-native/local-notifications": "^4.20.0",
|
"@ionic-native/local-notifications": "^4.20.0",
|
||||||
|
"@ionic-native/media": "^4.20.0",
|
||||||
"@ionic-native/media-capture": "^4.20.0",
|
"@ionic-native/media-capture": "^4.20.0",
|
||||||
"@ionic-native/network": "^4.20.0",
|
"@ionic-native/network": "^4.20.0",
|
||||||
"@ionic-native/push": "^4.20.0",
|
"@ionic-native/push": "^4.20.0",
|
||||||
|
@ -105,6 +106,7 @@
|
||||||
"cordova-plugin-ionic-keyboard": "2.1.3",
|
"cordova-plugin-ionic-keyboard": "2.1.3",
|
||||||
"cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle",
|
"cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle",
|
||||||
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
|
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
|
||||||
|
"cordova-plugin-media": "^5.0.3",
|
||||||
"cordova-plugin-media-capture": "^3.0.3",
|
"cordova-plugin-media-capture": "^3.0.3",
|
||||||
"cordova-plugin-network-information": "^2.0.2",
|
"cordova-plugin-network-information": "^2.0.2",
|
||||||
"cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist",
|
"cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist",
|
||||||
|
@ -213,7 +215,10 @@
|
||||||
"cordova-plugin-wkwebview-cookies": {},
|
"cordova-plugin-wkwebview-cookies": {},
|
||||||
"cordova-plugin-qrscanner": {},
|
"cordova-plugin-qrscanner": {},
|
||||||
"cordova-plugin-chooser": {},
|
"cordova-plugin-chooser": {},
|
||||||
"cordova-plugin-wkuserscript": {}
|
"cordova-plugin-wkuserscript": {},
|
||||||
|
"cordova-plugin-media": {
|
||||||
|
"KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"main": "desktop/electron.js",
|
"main": "desktop/electron.js",
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { Globalization } from '@ionic-native/globalization';
|
||||||
import { InAppBrowser } from '@ionic-native/in-app-browser';
|
import { InAppBrowser } from '@ionic-native/in-app-browser';
|
||||||
import { Keyboard } from '@ionic-native/keyboard';
|
import { Keyboard } from '@ionic-native/keyboard';
|
||||||
import { LocalNotifications } from '@ionic-native/local-notifications';
|
import { LocalNotifications } from '@ionic-native/local-notifications';
|
||||||
|
import { Media } from '@ionic-native/media';
|
||||||
import { MediaCapture } from '@ionic-native/media-capture';
|
import { MediaCapture } from '@ionic-native/media-capture';
|
||||||
import { Network } from '@ionic-native/network';
|
import { Network } from '@ionic-native/network';
|
||||||
import { Push } from '@ionic-native/push';
|
import { Push } from '@ionic-native/push';
|
||||||
|
@ -196,6 +197,7 @@ export const IONIC_NATIVE_PROVIDERS = [
|
||||||
return appProvider.isMobile() ? new MediaCapture() : new MediaCaptureMock(captureHelper);
|
return appProvider.isMobile() ? new MediaCapture() : new MediaCaptureMock(captureHelper);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Media,
|
||||||
{
|
{
|
||||||
provide: Network,
|
provide: Network,
|
||||||
deps: [Platform],
|
deps: [Platform],
|
||||||
|
|
|
@ -24,9 +24,18 @@
|
||||||
<canvas *ngIf="isImage" class="core-webcam-image-canvas" #imgCanvas></canvas>
|
<canvas *ngIf="isImage" class="core-webcam-image-canvas" #imgCanvas></canvas>
|
||||||
<img *ngIf="isImage" [hidden]="!hasCaptured" class="core-webcam-image" alt="{{ 'core.capturedimage' | translate }}" #previewImage>
|
<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. -->
|
<!-- Recording audio. -->
|
||||||
<div *ngIf="isAudio" class="core-audio-record-container">
|
<div *ngIf="isAudio" class="core-audio-record-container">
|
||||||
<canvas [hidden]="hasCaptured" class="core-audio-canvas" #streamAudio></canvas>
|
<!-- Canvas to show audio waves when recording audio in desktop. -->
|
||||||
|
<canvas [hidden]="hasCaptured || isCordovaAudioCapture" class="core-audio-canvas" #streamAudio></canvas>
|
||||||
|
|
||||||
|
<!-- Button to start/stop in mobile devices. -->
|
||||||
|
<button ion-button icon-only clear *ngIf="!hasCaptured && isCordovaAudioCapture" (click)="actionClicked()" [attr.aria-label]="title">
|
||||||
|
<ion-icon *ngIf="!isCapturing" name="microphone"></ion-icon>
|
||||||
|
<ion-icon *ngIf="isCapturing" name="square"></ion-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Audio player to listen to the result. -->
|
||||||
<audio [hidden]="!hasCaptured" class="core-audio-captured" controls #previewAudio></audio>
|
<audio [hidden]="!hasCaptured" class="core-audio-captured" controls #previewAudio></audio>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +46,7 @@
|
||||||
<ion-row *ngIf="readyToCapture">
|
<ion-row *ngIf="readyToCapture">
|
||||||
<ion-col></ion-col>
|
<ion-col></ion-col>
|
||||||
<ion-col text-center>
|
<ion-col text-center>
|
||||||
<button ion-button icon-only clear *ngIf="!hasCaptured" (click)="actionClicked()" [attr.aria-label]="title">
|
<button ion-button icon-only clear *ngIf="!hasCaptured && !isCordovaAudioCapture" (click)="actionClicked()" [attr.aria-label]="title">
|
||||||
<ion-icon *ngIf="!isCapturing && isAudio" name="microphone"></ion-icon>
|
<ion-icon *ngIf="!isCapturing && isAudio" name="microphone"></ion-icon>
|
||||||
<ion-icon *ngIf="!isCapturing && isVideo" name="videocam"></ion-icon>
|
<ion-icon *ngIf="!isCapturing && isVideo" name="videocam"></ion-icon>
|
||||||
<ion-icon *ngIf="isImage" name="camera"></ion-icon>
|
<ion-icon *ngIf="isImage" name="camera"></ion-icon>
|
||||||
|
|
|
@ -23,9 +23,34 @@ ion-app.app-root page-core-emulator-capture-media {
|
||||||
.core-audio-captured {
|
.core-audio-captured {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
height: 120px;
|
||||||
|
width: 120px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 120px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audio, video, img {
|
audio {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
video, img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
|
|
|
@ -13,15 +13,20 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
||||||
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
|
import { IonicPage, ViewController, NavParams, Platform } from 'ionic-angular';
|
||||||
|
import { CoreApp } from '@providers/app';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
import { CoreFileProvider } from '@providers/file';
|
||||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
|
||||||
|
import { FileEntry } from '@ionic-native/file';
|
||||||
import { MediaFile } from '@ionic-native/media-capture';
|
import { MediaFile } from '@ionic-native/media-capture';
|
||||||
|
import { Media, MediaObject } from '@ionic-native/media';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page to capture media in browser or desktop.
|
* Page to capture media in browser or desktop, or to capture audio in mobile devices.
|
||||||
*/
|
*/
|
||||||
@IonicPage({ segment: 'core-emulator-capture-media' })
|
@IonicPage({ segment: 'core-emulator-capture-media' })
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -45,6 +50,7 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
isCapturing: boolean; // Whether it's capturing.
|
isCapturing: boolean; // Whether it's capturing.
|
||||||
maxTime: number; // The max time to capture.
|
maxTime: number; // The max time to capture.
|
||||||
resetChrono: boolean; // Boolean to reset the chrono.
|
resetChrono: boolean; // Boolean to reset the chrono.
|
||||||
|
isCordovaAudioCapture: boolean; // Whether it's capturing audio using Cordova plugin.
|
||||||
|
|
||||||
protected type: string; // The type to capture: audio, video, image, captureimage.
|
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 isCaptureImage: boolean; // To identify if it's capturing an image using media capture plugin (instead of camera).
|
||||||
|
@ -60,9 +66,20 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
protected mediaBlob: Blob; // A Blob where the captured data is stored.
|
protected mediaBlob: Blob; // A Blob where the captured data is stored.
|
||||||
protected localMediaStream: MediaStream;
|
protected localMediaStream: MediaStream;
|
||||||
|
|
||||||
constructor(private viewCtrl: ViewController, params: NavParams, private domUtils: CoreDomUtilsProvider,
|
// Variables for Cordova Media capture.
|
||||||
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
|
protected mediaFile: MediaObject;
|
||||||
private textUtils: CoreTextUtilsProvider, private cdr: ChangeDetectorRef) {
|
protected filePath: string;
|
||||||
|
protected fileEntry: FileEntry;
|
||||||
|
|
||||||
|
constructor(protected viewCtrl: ViewController,
|
||||||
|
params: NavParams,
|
||||||
|
protected domUtils: CoreDomUtilsProvider,
|
||||||
|
protected timeUtils: CoreTimeUtilsProvider,
|
||||||
|
protected fileProvider: CoreFileProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider,
|
||||||
|
protected cdr: ChangeDetectorRef,
|
||||||
|
protected plaform: Platform,
|
||||||
|
protected media: Media) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.type = params.get('type');
|
this.type = params.get('type');
|
||||||
this.maxTime = params.get('maxTime');
|
this.maxTime = params.get('maxTime');
|
||||||
|
@ -79,12 +96,52 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initVariables();
|
this.initVariables();
|
||||||
|
|
||||||
|
if (this.isCordovaAudioCapture) {
|
||||||
|
this.initCordovaMediaPlugin();
|
||||||
|
} else {
|
||||||
|
this.initHtmlCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init recording with Cordova media plugin.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when ready.
|
||||||
|
*/
|
||||||
|
protected async initCordovaMediaPlugin(): Promise<void> {
|
||||||
|
this.filePath = this.getFilePath();
|
||||||
|
let absolutePath = this.textUtils.concatenatePaths(this.fileProvider.getBasePathInstant(), this.filePath);
|
||||||
|
|
||||||
|
if (this.plaform.is('ios')) {
|
||||||
|
// In iOS we need to remove the file:// part.
|
||||||
|
absolutePath = absolutePath.replace(/^file:\/\//, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First create the file.
|
||||||
|
this.fileEntry = await this.fileProvider.createFile(this.filePath);
|
||||||
|
|
||||||
|
// Now create the media instance.
|
||||||
|
this.mediaFile = this.media.create(absolutePath);
|
||||||
|
this.readyToCapture = true;
|
||||||
|
this.previewMedia = this.previewAudio.nativeElement;
|
||||||
|
} catch (error) {
|
||||||
|
this.dismissWithError(-1, error.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init HTML recorder, for desktop apps.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected initHtmlCapture(): Promise<void> {
|
||||||
const constraints = {
|
const constraints = {
|
||||||
video: this.isAudio ? false : { facingMode: this.facingMode },
|
video: this.isAudio ? false : { facingMode: this.facingMode },
|
||||||
audio: !this.isImage
|
audio: !this.isImage
|
||||||
};
|
};
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
|
return navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
|
||||||
let chunks = [];
|
let chunks = [];
|
||||||
this.localMediaStream = stream;
|
this.localMediaStream = stream;
|
||||||
|
|
||||||
|
@ -254,6 +311,13 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
this.isImage = true;
|
this.isImage = true;
|
||||||
this.title = 'core.captureimage';
|
this.title = 'core.captureimage';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isCordovaAudioCapture = CoreApp.instance.isMobile() && this.isAudio;
|
||||||
|
|
||||||
|
if (this.isCordovaAudioCapture) {
|
||||||
|
this.extension = this.plaform.is('ios') ? 'wav' : 'aac';
|
||||||
|
this.returnDataUrl = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -269,7 +333,14 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
// Start the capture.
|
// Start the capture.
|
||||||
this.isCapturing = true;
|
this.isCapturing = true;
|
||||||
this.resetChrono = false;
|
this.resetChrono = false;
|
||||||
this.mediaRecorder.start();
|
|
||||||
|
if (this.isCordovaAudioCapture) {
|
||||||
|
this.mediaFile.startRecord();
|
||||||
|
this.previewMedia.src = '';
|
||||||
|
} else {
|
||||||
|
this.mediaRecorder && this.mediaRecorder.start();
|
||||||
|
}
|
||||||
|
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
} else {
|
} else {
|
||||||
// Get the image from the video and set it to the canvas, using video width/height.
|
// Get the image from the video and set it to the canvas, using video width/height.
|
||||||
|
@ -299,6 +370,11 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
cancel(): void {
|
cancel(): void {
|
||||||
// Send a "cancelled" error like the Cordova plugin does.
|
// Send a "cancelled" error like the Cordova plugin does.
|
||||||
this.dismissWithError(3, 'Canceled.', 'Camera cancelled');
|
this.dismissWithError(3, 'Canceled.', 'Camera cancelled');
|
||||||
|
|
||||||
|
if (this.isCordovaAudioCapture) {
|
||||||
|
// Delete the tmp file.
|
||||||
|
this.fileProvider.removeFile(this.filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -341,7 +417,7 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Done capturing, write the file.
|
* Done capturing, write the file.
|
||||||
*/
|
*/
|
||||||
done(): void {
|
async done(): Promise<void> {
|
||||||
if (this.returnDataUrl) {
|
if (this.returnDataUrl) {
|
||||||
// Return the image as a base64 string.
|
// Return the image as a base64 string.
|
||||||
this.dismissWithData(this.imgCanvas.nativeElement.toDataURL(this.mimetype, this.quality));
|
this.dismissWithData(this.imgCanvas.nativeElement.toDataURL(this.mimetype, this.quality));
|
||||||
|
@ -349,64 +425,98 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.mediaBlob) {
|
if (!this.mediaBlob && !this.isCordovaAudioCapture) {
|
||||||
// Shouldn't happen.
|
// Shouldn't happen.
|
||||||
this.domUtils.showErrorModal('Please capture the media first.');
|
this.domUtils.showErrorModal('Please capture the media first.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the file and return it.
|
let fileEntry = this.fileEntry;
|
||||||
const fileName = this.type + '_' + this.timeUtils.readableTimestamp() + '.' + this.extension,
|
const loadingModal = this.domUtils.showModalLoading();
|
||||||
path = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 'media/' + fileName),
|
|
||||||
loadingModal = this.domUtils.showModalLoading();
|
try {
|
||||||
|
if (!this.isCordovaAudioCapture) {
|
||||||
|
// Capturing in browser. Write the blob in a file.
|
||||||
|
if (!this.mediaBlob) {
|
||||||
|
// Shouldn't happen.
|
||||||
|
throw new Error('Please capture the media first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEntry = await this.fileProvider.writeFile(this.getFilePath(), this.mediaBlob);
|
||||||
|
}
|
||||||
|
|
||||||
this.fileProvider.writeFile(path, this.mediaBlob).then((fileEntry) => {
|
|
||||||
if (this.isImage && !this.isCaptureImage) {
|
if (this.isImage && !this.isCaptureImage) {
|
||||||
this.dismissWithData(fileEntry.toURL());
|
this.dismissWithData(fileEntry.toURL());
|
||||||
} else {
|
} else {
|
||||||
// The capture plugin should return a MediaFile, not a FileEntry. Convert it.
|
// The capture plugin should return a MediaFile, not a FileEntry. Convert it.
|
||||||
return this.fileProvider.getMetadata(fileEntry).then((metadata) => {
|
const metadata = await this.fileProvider.getMetadata(fileEntry);
|
||||||
const mediaFile: MediaFile = {
|
|
||||||
name: fileEntry.name,
|
|
||||||
fullPath: fileEntry.fullPath,
|
|
||||||
type: null,
|
|
||||||
lastModifiedDate: metadata.modificationTime,
|
|
||||||
size: metadata.size,
|
|
||||||
getFormatData: (successFn, errorFn): void => {
|
|
||||||
// Nothing to do.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.dismissWithData([mediaFile]);
|
let mimetype = null;
|
||||||
});
|
if (this.extension) {
|
||||||
|
mimetype = CoreMimetypeUtils.instance.getMimeType(this.extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaFile: MediaFile = {
|
||||||
|
name: fileEntry.name,
|
||||||
|
fullPath: fileEntry.nativeURL || fileEntry.fullPath,
|
||||||
|
type: mimetype,
|
||||||
|
lastModifiedDate: metadata.modificationTime,
|
||||||
|
size: metadata.size,
|
||||||
|
getFormatData: (successFn, errorFn): void => {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dismissWithData([mediaFile]);
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
} catch (err) {
|
||||||
this.domUtils.showErrorModal(err);
|
this.domUtils.showErrorModal(err);
|
||||||
}).finally(() => {
|
} finally {
|
||||||
loadingModal.dismiss();
|
loadingModal.dismiss();
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path to the file where the media will be stored.
|
||||||
|
*
|
||||||
|
* @return Path.
|
||||||
|
*/
|
||||||
|
protected getFilePath(): string {
|
||||||
|
const fileName = this.type + '_' + this.timeUtils.readableTimestamp() + '.' + this.extension;
|
||||||
|
|
||||||
|
return this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 'media/' + fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop capturing. Only for video and audio.
|
* Stop capturing. Only for video and audio.
|
||||||
*/
|
*/
|
||||||
stopCapturing(): void {
|
stopCapturing(): void {
|
||||||
this.streamVideo && this.streamVideo.nativeElement.pause();
|
|
||||||
this.audioDrawer && this.audioDrawer.stop();
|
|
||||||
this.mediaRecorder && this.mediaRecorder.stop();
|
|
||||||
this.isCapturing = false;
|
this.isCapturing = false;
|
||||||
this.hasCaptured = true;
|
this.hasCaptured = true;
|
||||||
|
|
||||||
|
if (this.isCordovaAudioCapture) {
|
||||||
|
this.mediaFile.stopRecord();
|
||||||
|
this.previewMedia.src = this.fileProvider.convertFileSrc(this.fileEntry.toURL());
|
||||||
|
} else {
|
||||||
|
this.streamVideo && this.streamVideo.nativeElement.pause();
|
||||||
|
this.audioDrawer && this.audioDrawer.stop();
|
||||||
|
this.mediaRecorder && this.mediaRecorder.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page destroyed.
|
* Page destroyed.
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
const tracks = this.localMediaStream.getTracks();
|
this.mediaFile && this.mediaFile.release();
|
||||||
tracks.forEach((track) => {
|
|
||||||
track.stop();
|
if (this.localMediaStream) {
|
||||||
});
|
const tracks = this.localMediaStream.getTracks();
|
||||||
|
tracks.forEach((track) => {
|
||||||
|
track.stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
this.streamVideo && this.streamVideo.nativeElement.pause();
|
this.streamVideo && this.streamVideo.nativeElement.pause();
|
||||||
this.previewMedia && this.previewMedia.pause();
|
this.previewMedia && this.previewMedia.pause();
|
||||||
this.audioDrawer && this.audioDrawer.stop();
|
this.audioDrawer && this.audioDrawer.stop();
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Platform } from 'ionic-angular';
|
import { Platform, ModalController } from 'ionic-angular';
|
||||||
import { Camera, CameraOptions } from '@ionic-native/camera';
|
import { Camera, CameraOptions } from '@ionic-native/camera';
|
||||||
import { MediaCapture, MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture';
|
import { MediaCapture, MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
@ -53,11 +53,19 @@ export class CoreFileUploaderProvider {
|
||||||
onAudioCapture: Subject<boolean> = new Subject<boolean>();
|
onAudioCapture: Subject<boolean> = new Subject<boolean>();
|
||||||
onVideoCapture: Subject<boolean> = new Subject<boolean>();
|
onVideoCapture: Subject<boolean> = new Subject<boolean>();
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, private textUtils: CoreTextUtilsProvider,
|
constructor(logger: CoreLoggerProvider,
|
||||||
private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider,
|
protected fileProvider: CoreFileProvider,
|
||||||
private mimeUtils: CoreMimetypeUtilsProvider, private filepoolProvider: CoreFilepoolProvider,
|
protected textUtils: CoreTextUtilsProvider,
|
||||||
private platform: Platform, private translate: TranslateService, private mediaCapture: MediaCapture,
|
protected utils: CoreUtilsProvider,
|
||||||
private camera: Camera) {
|
protected sitesProvider: CoreSitesProvider,
|
||||||
|
protected timeUtils: CoreTimeUtilsProvider,
|
||||||
|
protected mimeUtils: CoreMimetypeUtilsProvider,
|
||||||
|
protected filepoolProvider: CoreFilepoolProvider,
|
||||||
|
protected platform: Platform,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected mediaCapture: MediaCapture,
|
||||||
|
protected camera: Camera,
|
||||||
|
protected modalCtrl: ModalController) {
|
||||||
this.logger = logger.getInstance('CoreFileUploaderProvider');
|
this.logger = logger.getInstance('CoreFileUploaderProvider');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +118,29 @@ export class CoreFileUploaderProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record an audio file without using an external app.
|
||||||
|
*
|
||||||
|
* @return Promise resolved with the file.
|
||||||
|
*/
|
||||||
|
captureAudioInApp(): Promise<MediaFile> {
|
||||||
|
return new Promise((resolve, reject): any => {
|
||||||
|
const params = {
|
||||||
|
type: 'audio',
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal = this.modalCtrl.create('CoreEmulatorCaptureMediaPage', params, { enableBackdropDismiss: false });
|
||||||
|
modal.present();
|
||||||
|
modal.onDidDismiss((data: any, role: string) => {
|
||||||
|
if (role == 'success') {
|
||||||
|
resolve(data[0]);
|
||||||
|
} else {
|
||||||
|
reject(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the video recorder application and return information about captured video clip files.
|
* Start the video recorder application and return information about captured video clip files.
|
||||||
*
|
*
|
||||||
|
@ -215,13 +246,14 @@ export class CoreFileUploaderProvider {
|
||||||
*/
|
*/
|
||||||
getMediaUploadOptions(mediaFile: MediaFile): CoreFileUploaderOptions {
|
getMediaUploadOptions(mediaFile: MediaFile): CoreFileUploaderOptions {
|
||||||
const options: CoreFileUploaderOptions = {};
|
const options: CoreFileUploaderOptions = {};
|
||||||
let filename = mediaFile.name,
|
let filename = mediaFile.name;
|
||||||
split;
|
|
||||||
|
|
||||||
// Add a timestamp to the filename to make it unique.
|
if (!filename.match(/_\d{14}(\..*)?$/)) {
|
||||||
split = filename.split('.');
|
// Add a timestamp to the filename to make it unique.
|
||||||
split[0] += '_' + this.timeUtils.readableTimestamp();
|
const split = filename.split('.');
|
||||||
filename = split.join('.');
|
split[0] += '_' + this.timeUtils.readableTimestamp();
|
||||||
|
filename = split.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
options.fileName = filename;
|
options.fileName = filename;
|
||||||
options.deleteAfterUpload = true;
|
options.deleteAfterUpload = true;
|
||||||
|
|
|
@ -455,37 +455,37 @@ export class CoreFileUploaderHelperProvider {
|
||||||
/**
|
/**
|
||||||
* Treat a capture audio/video error.
|
* Treat a capture audio/video error.
|
||||||
*
|
*
|
||||||
* @param error Error returned by the Cordova plugin. Can be a string or an object.
|
* @param error Error returned by the Cordova plugin.
|
||||||
* @param defaultMessage Key of the default message to show.
|
* @param defaultMessage Key of the default message to show.
|
||||||
* @return Rejected promise. If it doesn't have an error message it means it was cancelled.
|
* @return Rejected promise.
|
||||||
*/
|
*/
|
||||||
protected treatCaptureError(error: any, defaultMessage: string): Promise<any> {
|
protected treatCaptureError(error: any, defaultMessage: string): void {
|
||||||
// Cancelled or error. If cancelled, error is an object with code = 3.
|
// Cancelled or error. If cancelled, error is an object with code = 3.
|
||||||
if (error) {
|
if (error) {
|
||||||
if (typeof error === 'string') {
|
if (error.code != 3) {
|
||||||
this.logger.error('Error while recording audio/video: ' + error);
|
// Error, not cancelled.
|
||||||
if (error.indexOf('No Activity found') > -1) {
|
this.logger.error('Error while recording audio/video', error);
|
||||||
// User doesn't have an app to do this.
|
|
||||||
return Promise.reject(this.translate.instant('core.fileuploader.errornoapp'));
|
const message = this.isNoAppError(error) ? this.translate.instant('core.fileuploader.errornoapp') :
|
||||||
} else {
|
(error.message || this.translate.instant(defaultMessage));
|
||||||
return Promise.reject(this.translate.instant(defaultMessage));
|
|
||||||
}
|
throw new Error(message);
|
||||||
} else {
|
} else {
|
||||||
if (error.code != 3) {
|
throw this.domUtils.createCanceledError();
|
||||||
// Error, not cancelled.
|
|
||||||
this.logger.error('Error while recording audio/video', error);
|
|
||||||
|
|
||||||
const message = error.code == 20 ? this.translate.instant('core.fileuploader.errornoapp') :
|
|
||||||
(error.message || this.translate.instant(defaultMessage));
|
|
||||||
|
|
||||||
return Promise.reject(message);
|
|
||||||
} else {
|
|
||||||
return Promise.reject(this.domUtils.createCanceledError());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(null);
|
throw new Error('Error capturing media');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a capture error is because there is no app to capture.
|
||||||
|
*
|
||||||
|
* @param error Error.
|
||||||
|
* @return Whether it's because there is no app.
|
||||||
|
*/
|
||||||
|
protected isNoAppError(error: any): boolean {
|
||||||
|
return error && error.code == 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -522,41 +522,57 @@ export class CoreFileUploaderHelperProvider {
|
||||||
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
|
* @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
uploadAudioOrVideo(isAudio: boolean, maxSize: number, upload?: boolean, mimetypes?: string[]): Promise<any> {
|
async uploadAudioOrVideo(isAudio: boolean, maxSize: number, upload?: boolean, mimetypes?: string[]): Promise<any> {
|
||||||
this.logger.debug('Trying to record a ' + (isAudio ? 'audio' : 'video') + ' file');
|
this.logger.debug('Trying to record a ' + (isAudio ? 'audio' : 'video') + ' file');
|
||||||
|
|
||||||
const options = { limit: 1, mimetypes: mimetypes },
|
|
||||||
promise = isAudio ? this.fileUploaderProvider.captureAudio(options) : this.fileUploaderProvider.captureVideo(options);
|
|
||||||
|
|
||||||
// The mimetypes param is only for desktop apps, the Cordova plugin doesn't support it.
|
// The mimetypes param is only for desktop apps, the Cordova plugin doesn't support it.
|
||||||
return promise.then((medias) => {
|
const captureOptions = { limit: 1, mimetypes: mimetypes };
|
||||||
// We used limit 1, we only want 1 media.
|
let media: MediaFile;
|
||||||
const media: MediaFile = medias[0];
|
|
||||||
let path = media.fullPath;
|
|
||||||
const error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
|
|
||||||
|
|
||||||
if (error) {
|
try {
|
||||||
return Promise.reject(error);
|
const medias = isAudio ? await this.fileUploaderProvider.captureAudio(captureOptions) :
|
||||||
}
|
await this.fileUploaderProvider.captureVideo(captureOptions);
|
||||||
|
|
||||||
// Make sure the path has the protocol. In iOS it doesn't.
|
media = medias[0]; // We used limit 1, we only want 1 media.
|
||||||
if (this.appProvider.isMobile() && path.indexOf('file://') == -1) {
|
} catch (error) {
|
||||||
path = 'file://' + path;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = this.fileUploaderProvider.getMediaUploadOptions(media);
|
if (isAudio && this.isNoAppError(error) && this.appProvider.isMobile() &&
|
||||||
|
(!this.platform.is('android') || this.platform.version().major < 10)) {
|
||||||
|
// No app to record audio, fallback to capture it ourselves.
|
||||||
|
// In Android it will only be done in Android 9 or lower because there's a bug in the plugin.
|
||||||
|
try {
|
||||||
|
media = await this.fileUploaderProvider.captureAudioInApp();
|
||||||
|
} catch (error) {
|
||||||
|
this.treatCaptureError(error, 'core.fileuploader.errorcapturingaudio'); // Throw the right error.
|
||||||
|
}
|
||||||
|
|
||||||
if (upload) {
|
|
||||||
return this.uploadFile(path, maxSize, true, options);
|
|
||||||
} else {
|
} else {
|
||||||
// Copy or move the file to our temporary folder.
|
const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo';
|
||||||
return this.copyToTmpFolder(path, true, maxSize, undefined, options);
|
|
||||||
}
|
|
||||||
}, (error) => {
|
|
||||||
const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo';
|
|
||||||
|
|
||||||
return this.treatCaptureError(error, defaultError);
|
this.treatCaptureError(error, defaultError); // Throw the right error.
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = media.fullPath;
|
||||||
|
const error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the path has the protocol. In iOS it doesn't.
|
||||||
|
if (this.appProvider.isMobile() && path.indexOf('file://') == -1) {
|
||||||
|
path = 'file://' + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = this.fileUploaderProvider.getMediaUploadOptions(media);
|
||||||
|
|
||||||
|
if (upload) {
|
||||||
|
return this.uploadFile(path, maxSize, true, options);
|
||||||
|
} else {
|
||||||
|
// Copy or move the file to our temporary folder.
|
||||||
|
return this.copyToTmpFolder(path, true, maxSize, undefined, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue