MOBILE-3490 core: Record audio in app if no recording app installed
parent
b67ea14abb
commit
384c4372fe
|
@ -214,6 +214,11 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
</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 name="ios">
|
||||
<resource-file src="GoogleService-Info.plist" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "moodlemobile",
|
||||
"version": "3.9.2",
|
||||
"version": "3.9.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -172,6 +172,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@ionic-native/local-notifications/-/local-notifications-4.20.0.tgz",
|
||||
"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": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/media-capture/-/media-capture-4.20.0.tgz",
|
||||
|
@ -3169,9 +3174,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"compare-func": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz",
|
||||
"integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.4.tgz",
|
||||
"integrity": "sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==",
|
||||
"requires": {
|
||||
"array-ify": "^1.0.0",
|
||||
"dot-prop": "^3.0.0"
|
||||
|
@ -3728,6 +3733,11 @@
|
|||
"version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836",
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"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/keyboard": "^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/network": "^4.20.0",
|
||||
"@ionic-native/push": "^4.20.0",
|
||||
|
@ -105,6 +106,7 @@
|
|||
"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-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-network-information": "^2.0.2",
|
||||
"cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist",
|
||||
|
@ -213,7 +215,10 @@
|
|||
"cordova-plugin-wkwebview-cookies": {},
|
||||
"cordova-plugin-qrscanner": {},
|
||||
"cordova-plugin-chooser": {},
|
||||
"cordova-plugin-wkuserscript": {}
|
||||
"cordova-plugin-wkuserscript": {},
|
||||
"cordova-plugin-media": {
|
||||
"KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "desktop/electron.js",
|
||||
|
@ -276,4 +281,4 @@
|
|||
"engines": {
|
||||
"node": ">=11.x"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ 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 { Media } from '@ionic-native/media';
|
||||
import { MediaCapture } from '@ionic-native/media-capture';
|
||||
import { Network } from '@ionic-native/network';
|
||||
import { Push } from '@ionic-native/push';
|
||||
|
@ -196,6 +197,7 @@ export const IONIC_NATIVE_PROVIDERS = [
|
|||
return appProvider.isMobile() ? new MediaCapture() : new MediaCaptureMock(captureHelper);
|
||||
}
|
||||
},
|
||||
Media,
|
||||
{
|
||||
provide: Network,
|
||||
deps: [Platform],
|
||||
|
|
|
@ -24,9 +24,18 @@
|
|||
<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. -->
|
||||
<!-- Recording audio. -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,7 +46,7 @@
|
|||
<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">
|
||||
<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 && isVideo" name="videocam"></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 {
|
||||
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%;
|
||||
height: 100%;
|
||||
display: table-cell;
|
||||
|
|
|
@ -13,15 +13,20 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
|
||||
import { FileEntry } from '@ionic-native/file';
|
||||
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' })
|
||||
@Component({
|
||||
|
@ -45,6 +50,7 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
|||
isCapturing: boolean; // Whether it's capturing.
|
||||
maxTime: number; // The max time to capture.
|
||||
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 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 localMediaStream: MediaStream;
|
||||
|
||||
constructor(private viewCtrl: ViewController, params: NavParams, private domUtils: CoreDomUtilsProvider,
|
||||
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
|
||||
private textUtils: CoreTextUtilsProvider, private cdr: ChangeDetectorRef) {
|
||||
// Variables for Cordova Media capture.
|
||||
protected mediaFile: MediaObject;
|
||||
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.type = params.get('type');
|
||||
this.maxTime = params.get('maxTime');
|
||||
|
@ -79,12 +96,52 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
|||
ngOnInit(): void {
|
||||
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 = {
|
||||
video: this.isAudio ? false : { facingMode: this.facingMode },
|
||||
audio: !this.isImage
|
||||
};
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
|
||||
return navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
|
||||
let chunks = [];
|
||||
this.localMediaStream = stream;
|
||||
|
||||
|
@ -254,6 +311,13 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
|||
this.isImage = true;
|
||||
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.
|
||||
this.isCapturing = true;
|
||||
this.resetChrono = false;
|
||||
this.mediaRecorder.start();
|
||||
|
||||
if (this.isCordovaAudioCapture) {
|
||||
this.mediaFile.startRecord();
|
||||
this.previewMedia.src = '';
|
||||
} else {
|
||||
this.mediaRecorder && this.mediaRecorder.start();
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
} else {
|
||||
// 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 {
|
||||
// Send a "cancelled" error like the Cordova plugin does.
|
||||
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(): void {
|
||||
async done(): Promise<void> {
|
||||
if (this.returnDataUrl) {
|
||||
// Return the image as a base64 string.
|
||||
this.dismissWithData(this.imgCanvas.nativeElement.toDataURL(this.mimetype, this.quality));
|
||||
|
@ -349,64 +425,98 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this.mediaBlob) {
|
||||
if (!this.mediaBlob && !this.isCordovaAudioCapture) {
|
||||
// Shouldn't happen.
|
||||
this.domUtils.showErrorModal('Please capture the media first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the file and return it.
|
||||
const fileName = this.type + '_' + this.timeUtils.readableTimestamp() + '.' + this.extension,
|
||||
path = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 'media/' + fileName),
|
||||
loadingModal = this.domUtils.showModalLoading();
|
||||
let fileEntry = this.fileEntry;
|
||||
const 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) {
|
||||
this.dismissWithData(fileEntry.toURL());
|
||||
} else {
|
||||
// The capture plugin should return a MediaFile, not a FileEntry. Convert it.
|
||||
return this.fileProvider.getMetadata(fileEntry).then((metadata) => {
|
||||
const mediaFile: MediaFile = {
|
||||
name: fileEntry.name,
|
||||
fullPath: fileEntry.fullPath,
|
||||
type: null,
|
||||
lastModifiedDate: metadata.modificationTime,
|
||||
size: metadata.size,
|
||||
getFormatData: (successFn, errorFn): void => {
|
||||
// Nothing to do.
|
||||
}
|
||||
};
|
||||
const metadata = await this.fileProvider.getMetadata(fileEntry);
|
||||
|
||||
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);
|
||||
}).finally(() => {
|
||||
} finally {
|
||||
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.
|
||||
*/
|
||||
stopCapturing(): void {
|
||||
this.streamVideo && this.streamVideo.nativeElement.pause();
|
||||
this.audioDrawer && this.audioDrawer.stop();
|
||||
this.mediaRecorder && this.mediaRecorder.stop();
|
||||
this.isCapturing = false;
|
||||
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.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
const tracks = this.localMediaStream.getTracks();
|
||||
tracks.forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
this.mediaFile && this.mediaFile.release();
|
||||
|
||||
if (this.localMediaStream) {
|
||||
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();
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Platform } from 'ionic-angular';
|
||||
import { Platform, ModalController } from 'ionic-angular';
|
||||
import { Camera, CameraOptions } from '@ionic-native/camera';
|
||||
import { MediaCapture, MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
@ -53,11 +53,19 @@ export class CoreFileUploaderProvider {
|
|||
onAudioCapture: Subject<boolean> = new Subject<boolean>();
|
||||
onVideoCapture: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private utils: CoreUtilsProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider,
|
||||
private mimeUtils: CoreMimetypeUtilsProvider, private filepoolProvider: CoreFilepoolProvider,
|
||||
private platform: Platform, private translate: TranslateService, private mediaCapture: MediaCapture,
|
||||
private camera: Camera) {
|
||||
constructor(logger: CoreLoggerProvider,
|
||||
protected fileProvider: CoreFileProvider,
|
||||
protected textUtils: CoreTextUtilsProvider,
|
||||
protected utils: CoreUtilsProvider,
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -215,13 +246,14 @@ export class CoreFileUploaderProvider {
|
|||
*/
|
||||
getMediaUploadOptions(mediaFile: MediaFile): CoreFileUploaderOptions {
|
||||
const options: CoreFileUploaderOptions = {};
|
||||
let filename = mediaFile.name,
|
||||
split;
|
||||
let filename = mediaFile.name;
|
||||
|
||||
// Add a timestamp to the filename to make it unique.
|
||||
split = filename.split('.');
|
||||
split[0] += '_' + this.timeUtils.readableTimestamp();
|
||||
filename = split.join('.');
|
||||
if (!filename.match(/_\d{14}(\..*)?$/)) {
|
||||
// Add a timestamp to the filename to make it unique.
|
||||
const split = filename.split('.');
|
||||
split[0] += '_' + this.timeUtils.readableTimestamp();
|
||||
filename = split.join('.');
|
||||
}
|
||||
|
||||
options.fileName = filename;
|
||||
options.deleteAfterUpload = true;
|
||||
|
|
|
@ -455,37 +455,37 @@ export class CoreFileUploaderHelperProvider {
|
|||
/**
|
||||
* 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.
|
||||
* @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.
|
||||
if (error) {
|
||||
if (typeof error === 'string') {
|
||||
this.logger.error('Error while recording audio/video: ' + error);
|
||||
if (error.indexOf('No Activity found') > -1) {
|
||||
// User doesn't have an app to do this.
|
||||
return Promise.reject(this.translate.instant('core.fileuploader.errornoapp'));
|
||||
} else {
|
||||
return Promise.reject(this.translate.instant(defaultMessage));
|
||||
}
|
||||
if (error.code != 3) {
|
||||
// Error, not cancelled.
|
||||
this.logger.error('Error while recording audio/video', error);
|
||||
|
||||
const message = this.isNoAppError(error) ? this.translate.instant('core.fileuploader.errornoapp') :
|
||||
(error.message || this.translate.instant(defaultMessage));
|
||||
|
||||
throw new Error(message);
|
||||
} else {
|
||||
if (error.code != 3) {
|
||||
// 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());
|
||||
}
|
||||
throw 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.
|
||||
* @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');
|
||||
|
||||
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.
|
||||
return promise.then((medias) => {
|
||||
// We used limit 1, we only want 1 media.
|
||||
const media: MediaFile = medias[0];
|
||||
let path = media.fullPath;
|
||||
const error = this.fileUploaderProvider.isInvalidMimetype(mimetypes, path); // Verify that the mimetype is supported.
|
||||
const captureOptions = { limit: 1, mimetypes: mimetypes };
|
||||
let media: MediaFile;
|
||||
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
try {
|
||||
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.
|
||||
if (this.appProvider.isMobile() && path.indexOf('file://') == -1) {
|
||||
path = 'file://' + path;
|
||||
}
|
||||
media = medias[0]; // We used limit 1, we only want 1 media.
|
||||
} catch (error) {
|
||||
|
||||
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 {
|
||||
// Copy or move the file to our temporary folder.
|
||||
return this.copyToTmpFolder(path, true, maxSize, undefined, options);
|
||||
}
|
||||
}, (error) => {
|
||||
const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo';
|
||||
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