Skip to content

Instantly share code, notes, and snippets.

@jdnichollsc
Last active July 17, 2021 05:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdnichollsc/9d450ee36d9d75b201c01549f8f36fd7 to your computer and use it in GitHub Desktop.
Save jdnichollsc/9d450ee36d9d75b201c01549f8f36fd7 to your computer and use it in GitHub Desktop.
Record audio (Cordova/Ionic) using cordova-plugin-media and cordova-plugin-file
import { Media, MediaObject } from '@ionic-native/media';
import { File as NativeFile, FileEntry } from '@ionic-native/file';
import { delay, getBlobFromFileEntry } from './utils'
// Detect platform device and select extension (Cordova platform, etc)
// Android
const AUDIO_EXTENSION = '.mp3'
// iOS
const AUDIO_EXTENSION = '.m4a'
export class PageDemo {
private audioFile: FileEntry
private audioMedia: MediaObject
// private audioMediaSrc: string
async onStartRecording() {
const path = NativeFile.dataDirectory;
const name = `record_${(new Date()).getTime() + AUDIO_EXTENSION}`;
this.audioFile = await NativeFile.createFile(path, name, true);
// Deprecated versions
// this.audioMediaSrc = this.audioFile.nativeURL.replace(/^file:[\/]+/, '');
// this.audioMedia = Media.create(this.audioMediaSrc);
this.audioMedia = Media.create(this.audioFile.nativeURL);
this.audioMedia.startRecord();
}
async onStopRecording() {
if (this.audioFile) {
try {
this.audioMedia.stopRecord();
// It requires a little delay to get media file data
await delay(200);
const blob = await getBlobFromFileEntry(this.audioFile);
// SAVE AUDIO HERE!
// uploadFile(this.audioFile.name, blob)
} finally {
this.audioFile = null;
}
}
}
}
import { FileEntry } from '@ionic-native/file';
export const getBlobFromFileEntry = (fileEntry: FileEntry): Promise<Blob> => {
return new Promise((resolve, reject) => {
fileEntry.file((file) => {
const reader = new FileReader();
reader.onloadend = function(e) {
try {
const { result: buffer } = e.target
const blob = new Blob(
[new Uint8Array(buffer as any)],
{ type: file.type }
);
resolve(blob);
} catch (error) {
reject(error);
}
};
reader.onabort = (ev) => reject(ev);
reader.onerror = (ev) => reject(ev);
reader.readAsArrayBuffer(file);
}, reject)
})
}
export const delay = (time: number) =>
new Promise(resolve => setTimeout(() => resolve(), time))
@Mehulkb
Copy link

Mehulkb commented Oct 12, 2020

core.js:4197 ERROR Error: Uncaught (in promise): TypeError: ionic_native_file__WEBPACK_IMPORTED_MODULE_3_.File.createFile is not a function
TypeError: ionic_native_file__WEBPACK_IMPORTED_MODULE_3_.File.createFile is not a function

It is throwing this error. Do you have a complete working code of this snippet?

@jdnichollsc
Copy link
Author

@ToschaA
Copy link

ToschaA commented Apr 6, 2021

Hey @jdnichollsc !

For me it says: "CreateDataFile failed", I am using NativeFile.dataDirectory as path but still get the error.
Is your code maybe only for Android, because I am developing for mainly iOS.
Appreciate if you could help me with this error! This is exactly what I need!

@jdnichollsc
Copy link
Author

This example is inspired in my enterprise WebComponent with StencilJS/Cordova here, but I already have an example in Production working from iOS & Android using Ionic 4/5, the code is similar 🙂
I can help you with those kinds of issues if you are a sponsor, check the below repository if you have any concern https://github.com/proyecto26/sponsors

Best,
Juan

@ToschaA
Copy link

ToschaA commented Apr 6, 2021

This example is inspired in my enterprise WebComponent with StencilJS/Cordova here, but I already have an example in Production working from iOS & Android using Ionic 4/5, the code is similar 🙂
I can help you with those kinds of issues if you are a sponsor, check the below repository if you have any concern https://github.com/proyecto26/sponsors

Best,
Juan

Alright, thanks! The only thing I am having problems with is to convert the mediaObject into a File that uploadable to firebase Storage. I think it is so strange how I feel like I am the only having wanting to achieve this nowadays. All the solutions are outdated but yours it seems like. What is the most simplest way, according to you, to convert MediaObject to Blob/File (I know you have already done that above, but I can't get it to work, although I have copied and pasted and made it suitable to my context).
Anything is appreciated!

@jdnichollsc
Copy link
Author

jdnichollsc commented Apr 6, 2021

Oh ok, I did that integration in another project with Ionic/Angular and Firebase too, something like this:

import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { Media, MediaObject } from '@ionic-native/media/ngx';
import { File as NativeFile, FileEntry } from '@ionic-native/file/ngx';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs';

import { MessageService } from '../api/message.service';
import { getBlobFromFileEntry, isImage, delay } from '../utils'

@Component({
  selector: 'app-chat',
  templateUrl: './chat.page.html',
  styleUrls: ['./chat.page.scss'],
})
export class ChatPage {
  audioExtension = this.platform.is('ios') ? '.m4a' : '.mp3';
  audioFile: FileEntry;
  audioMedia: MediaObject;
  audioMediaSrc = '';
  FILE_TYPES = {
    TEXT: 'Text',
    FILE: 'File',
    AUDIO: 'Audio'
  };
  uploadPercent: Observable<number> = null;
  
  constructor(
    private readonly media: Media,
    private readonly platform: Platform,
    // firebase
    private readonly db: AngularFirestore,
    private readonly storage: AngularFireStorage,
    private readonly file: NativeFile,
    private readonly messageService: MessageService,
  ) { }

  async uploadFile(
    fileName: string,
    data: Blob | File,
    type: this.FileType.FILE | this.FileType.AUDIO,
  ) {
    const filePath = `${this.connectionPath}/${fileName}`;
    const fileRef = this.storage.ref(filePath);
    // Firebase Storage
    const task = this.storage.upload(filePath, data);
    this.uploadPercent = task.percentageChanges();
    await task.snapshotChanges().toPromise();
    const fileUrl = await fileRef.getDownloadURL().toPromise();
    const metadata = await fileRef.getMetadata().toPromise();
  
    // Firebase Database for a chat or something like that
    await this.messageService.addMessage({
        type,
        fileUrl,
        fileMetadata: {
          name: metadata.name,
          size: metadata.size,
          type: metadata.contentType,
          path: metadata.fullPath,
          lastModified: metadata.updated
        }
      }
    );
    this.uploadPercent = null;
  }

  async startRecordAudio() {
    if (this.audioFile || !this.platform.is('cordova')) return
    let path;
    const name = `record_${(new Date().getTime()) + this.audioExtension}`;
    if(this.platform.is("ios")){
      path = this.file.tempDirectory;
      this.audioFile = await this.file.createFile(path, name, true);
      this.audioMedia = this.media.create(this.file.tempDirectory.replace(/^file:\/\//, '') + name);
    } else {
      path = this.file.dataDirectory;
      this.audioFile = await this.file.createFile(path, name, true);
      this.audioMediaSrc = this.audioFile.nativeURL.replace(/^file:[\/]+/, '');
      this.audioMedia = this.media.create(this.audioFile.nativeURL);
    }
    this.audioMedia.startRecord();
  }

  async stopRecordAudio(detail: Blob) {
    if (this.platform.is('cordova')) {
      if (!this.audioFile) return
      try {
        this.audioMedia.stopRecord();
        await delay(200);
        const blob = await getBlobFromFileEntry(this.audioFile);
        this.uploadFile(this.audioFile.name, blob, FileType.AUDIO);
      } finally {
        this.audioFile = null
      }
    } else {
      const audioName = new Date().toTimeString() + '.webm';
      this.uploadFile(audioName, detail, FileType.AUDIO);
    }
  }
}

Hope this can help you to have a better idea :)
Cheers!

@jdnichollsc
Copy link
Author

Check that example and let me know :)

@ToschaA
Copy link

ToschaA commented Apr 7, 2021

Check that example and let me know :)

You are the man! I'll try it right away!

@ToschaA
Copy link

ToschaA commented Apr 7, 2021

@jdnichollsc

Hey again!

So first of all, thanks for the demo. Now the "CreateDataFile failed" error disappears. Although, now there is something wrong with the getBlobFromFileEntry, which I copied from the code you posted first, do you use the same? I can show you my code flow, it is quite similar to yours!

// Start recording
 async startRecord() {
    var path: string = "";
    if(this.platform.is("ios")){
      path = this.file.tempDirectory;
      this.audioFileName = new Date().getTime() + ".m4a";
      this.audioFile = await this.file.createFile(path, this.audioFileName, true);
      this.audioMedia = this.media.create(this.file.tempDirectory.replace(/^file:\/\//, '') + this.audioFileName);
    } else {
      path = this.file.dataDirectory;
      this.audioFileName = new Date().getTime() + ".mp3";
      this.audioFile = await this.file.createFile(path, this.audioFileName, true);
      //this.audioMediaSrc = this.audioFile.nativeURL.replace(/^file:[\/]+/, '');
      this.audioMedia = this.media.create(this.audioFile.nativeURL);
    }

    this.audioMedia.startRecord();
    this.isRecordingAudio = true;
  }



  // Stop recording
  async stopRecord() {
    this.audioMedia.stopRecord();
    this.isRecordingAudio = false;
    console.log("Has stopped recording");
    
    // Upload the new recording
    await this.delay(200);
    console.log("About to start converting to Blob");
    const blob = await this.getBlobFromFileEntry(this.audioFile);
    console.log("Finished Converting to Blob");
    this.guideService.uploadFilesToFs([blob]).then(files => {
      this.audioData = {name: files[0].name, url: files[0].url}
      console.log("Finished uploading (url result): " + this.audioData.url);
    });

    
  }

  getBlobFromFileEntry(fileEntry: FileEntry): Promise<Blob> {
    return new Promise((resolve, reject) => {
      console.log("About to resolve file from fiven FileEntry");
      fileEntry.file((file) => {
        console.log("Resolved file");
        const reader = new FileReader();
        reader.onloadend = function(e) {
          console.log("Reader has loaded");
          try {
            const { result: buffer } = e.target
            const blob = new Blob(
              [new Uint8Array(buffer as any)],
              { type: file.type }
            );
            console.log(blob);
            resolve(blob);
          } catch (error) {
            reject(error);
          }
        };
        reader.onabort = (ev) => reject(ev);
        reader.onerror = (ev) => reject(ev);
        reader.readAsArrayBuffer(file);
      }, reject)
    });
  }

The console.logs are there to debug, how far it came in the process. When running on iOS, it stops at console.log("Resolved file") so there seems to be something wrong with reader.onloadend? What do you think?

@ToschaA
Copy link

ToschaA commented Apr 7, 2021

I solved managed to solve the errors and am now able to upload the audio file to firebase Storage.
Appreciate the help very much!

Now the code looks like:

// Audio recording management

  // Start recording
  async startRecord() {
    var path: string = "";
    if(this.platform.is("ios")){
      path = this.file.tempDirectory;
      this.audioFileName = new Date().getTime() + ".m4a";
      this.audioFile = await this.file.createFile(path, this.audioFileName, true);
      this.audioMedia = this.media.create(this.file.tempDirectory.replace(/^file:\/\//, '') + this.audioFileName);
    } else {
      path = this.file.dataDirectory;
      this.audioFileName = new Date().getTime() + ".mp3";
      this.audioFile = await this.file.createFile(path, this.audioFileName, true);
      //this.audioMediaSrc = this.audioFile.nativeURL.replace(/^file:[\/]+/, '');
      this.audioMedia = this.media.create(this.audioFile.nativeURL);
    }

    this.audioMedia.startRecord();
    this.isRecordingAudio = true;
  }
  // Stop recording
  async stopRecord() {
    this.audioMedia.stopRecord();
    this.isRecordingAudio = false;
    
    // Upload the new recording
    await this.delay(200);
    this.audioFile.file(async (file) => {
      const blob = await this.getBlobFromFile(file);
      this.guideService.uploadFilesToFs([blob]).then(files => {
        this.audioData = {name: files[0].name, url: files[0].url}
      });
    })
  }
  // Convert File to Blob
  getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {
      let fileReader = this.mainService.getFileReader();

      fileReader.onloadend = function() {
        console.log(this.result);
        var blob = new Blob([this.result], { type: file.type })
        resolve(blob);
      };

      fileReader.onerror = function(err) {
        reject(err);
      };

      fileReader.readAsArrayBuffer(file);
    });
  }

Because I am using capacitor I needed to fetch the FileReader instance in a different way. So this.mainService.getFileReader() translates to:

getFileReader(): FileReader {
    const fileReader = new FileReader();
    const zoneOriginalInstance = (fileReader as any)["__zone_symbol__originalInstance"];
    return zoneOriginalInstance || fileReader;
  }

Thanks again! :)

@jdnichollsc
Copy link
Author

jdnichollsc commented Apr 7, 2021

Awesome, thanks for letting me know! <3

@Harshk16
Copy link

Harshk16 commented May 24, 2021

@jdnichollsc, @ToschaA

I am trying to implement the same solution but it's not working for, can you please help me out, below i am adding the code with the issue, Please let me, i will be grateful. Thank you

this.audioFile = await this.file.createFile(this.filePath, this.record.FileName, true);
      this.mediaObject = this.media.create(this.audioFile.nativeURL);
async uploadFile() {
    await delayTimer(200);
    this.audioFile.file(async file => {
        const blob =  await this.getBlobFromFile(file) // fIle return of  IFile not File
    })
  }
getBlobFromFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {
      let fileReader = this.utils.getFileReader();

      fileReader.onloadend = function() {
        console.log(this.result);
        var blob = new Blob([this.result], { type: file.type })
        resolve(blob);
      };
      fileReader.onerror = function(err) {
        reject(err);
      };
      fileReader.readAsArrayBuffer(file);  //(parameter) file: FileArgument of type 'File' is not assignable to parameter of type 'Blob'.
                                                                Type 'File' is missing the following properties from type 'Blob': size, type, arrayBuffer, slice, and 2 
                                                                 
    });
  }
  getFileReader(): FileReader {
    const fileReader = new FileReader();
    const zoneOriginalInstance = (fileReader as any)["__zone_symbol__originalInstance"];
    return zoneOriginalInstance || fileReader;
  }

Audio_Error

@jdnichollsc
Copy link
Author

That error is clear, type is wrong :)

@gregor-srdic
Copy link

gregor-srdic commented Jul 17, 2021

Hi guys, great example, thanks, but I am running into a strange problem (although I am setting the file path manualy as above):
W/System.err: java.io.FileNotFoundException: /storage/emulated/0/tmprecording-1626497965957.3gp: open failed: EPERM (Operation not permitted)
Adding android:requestLegacyExternalStorage="true" solved the problem, but I would need to avoid that.
Any idea? Thanks!

@jdnichollsc
Copy link
Author

Why are you using that format? 3gp. Do you know if that codec is supported by that platform? Did you request the permissions for recording audio?

@gregor-srdic
Copy link

I am not using any specific format, it seems like it is the default. This is my entire code, I didn't manually request permissions, but the dialog for recording and file access does popup.

@Injectable()
export class MediaService {
  private readonly logger = new Logger(this.constructor.name);

  constructor(private readonly media: Media, private readonly file: File) {}

  async recordAudio(): Promise<void> {
    const path = this.file.dataDirectory;
    const name = 'recording.mp3';
    const file = await this.file.createFile(path, name, true);
    console.log('file native url', file.nativeURL);

    const mediaFile: MediaObject = this.media.create(file.nativeURL);
    mediaFile.startRecord();
    await wait(10000);
    this.logger.debug('recording stopped');
    mediaFile.stopRecord();
    await wait(1000);
    this.logger.debug('playing started');
    mediaFile.play();
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment