MediaPlugin does not save audio recording into file [ionic-native]

Hi,
(I read all related articles and google, no solution for this found)

My code is at below.
Logic:

  1. define where the audio file should be located;
  2. click a button to start recording, after 5secs stop recording.

Deploy to an android v5.1.1 tablet and the code hits all console.log message successfully.

Problem:

  • The audio file, namely record.m4a has 0 byte size after the code is done. and the audioObject.getDuration() is -1, indicating no audio content is saved.
  • The recorded content, however, is saved in local -> internal storage (a path that I can view in File tool in Android without root) with name temprecording-xxxxxxx.3gp (the xxxxxxx system assigned digital numbers)

I tried both file.dataStorage and file.cacheStorage, same symptom.

Any idea why this happens?

constructor(public navCtrl: NavController, private media: MediaPlugin, private file: File, private platform: Platform) {
    this.platform.ready().then(() => {
      if (!this.platform.is('cordova')) {
        return false;
      }

      if (this.platform.is('ios')) {
        this.storageDirectory = file.documentsDirectory.replace(/^file:\/\//, '');
      }
      else if (this.platform.is('android')) {
        this.storageDirectory = file.dataDirectory;
      }
      else {
        // future usage for more platform support
        return false;
      }
    });
  }

  record() {
    const fileName = 'record.m4a';

    this.file.createFile(this.storageDirectory, fileName, true).then(() => {
      let audioObject: MediaObject = this.media.create(this.storageDirectory + fileName);

      audioObject.startRecord();
      console.log('cache dir: ' + this.file.cacheDirectory);
      console.log(`start recording ${fileName}`);
      window.setTimeout(() => {
        audioObject.stopRecord();
        console.log('duration: ' + audioObject.getDuration());
        audioObject.release();
        console.log(`done recording ${fileName}`);
        /** Do something with the record file and then delete */
        // this.file.removeFile(this.file.tempDirectory, 'record.m4a');
      }, 5000);
    });
  }

Hello,

1 - on android it saves a .3gp file, on ios .wav or a .m4a and on windows a .mp3, i recommend you to set the filename according to the device type

2 - i tested only on android and i wasn’t able to access the dataDirectory so i used externalDataDirectory instead

3 - for recording even on android you have to remove the “file://” so try to change from:

      else if (this.platform.is('android')) {
        this.storageDirectory = file.dataDirectory;
      }

to:

      else if (this.platform.is('android')) {
        this.storageDirectory = file.externalDataDirectory.replace(/file:\/\//g, '');
      }

4 - even after it started to save correctly the file, allways was getting the -1 duration

Firstly, thanks a lot for your help!

corresponding comments on your 4 points:

  1. I have actually tried all suffix: 3gp, m4a, mp3, aac, all with same symptom.

  2. Why is that you cannot access the dataDirectory?
    with adb shell -> run-as [package name] -> cd files -> ls -la, I can see below.
    -rw------- u0_a139 u0_a139 0 2017-05-27 00:41 record.3gp. I assume this means my code has r/w permission on this 3gp file (you can see its size is 0, and it was created by this.file.createFile instead of startRecord, stopRecord)

  3. are you sure of this? trancate file:// is due to a bug for iOS. Even if I do it for android, error is thrown in this.file.createFile because it does not recognize the truncated path.

  4. could you please check your referred ‘correctly saved file’ size, does it really have content or just zero?
    I do not have a external storage, cannot verify it. I cannot either assume that all users will have it.

p.s. I also tried with cordova-plugin-media directly, both latest version and older versions, all with the same problem.

I am now really uncertain whether it is my device or what caused this…

1 Like

Hello :smiley:

I am not using createFile, i am usign directly the media.create method and i dont have a external storage like an sd card, but externalDataDirectory is accessible (i’ve changed to external after i saw a guy saying that only external was working to him).

If you look on the media plugin android native code, you will see that, when you execute the startRecord, creates a .3gp file (the file you can see on the file tool), when you call the stopRecord method, it will try to save to destination file (specified on media.create), the problem is, the code check if the file destination start with a “/”, if not, then the file name is prepended with the cache directory, so if you try to save to file://xxxx.3gp (all directories from native File starts with file://) the code will try to save on /data/data/com.package.your/cache/file://xxxx.3gp.

So your file still with size 0, simply because the media plugin is trying to save in another destination, this is why you have to remove the initial “file://”. (the .replace(/file:///g, ‘’) will do it)

this is the android code where the filename is prepended

    ...
    public void moveFile(String file) {
       if (!file.startsWith("/")) {
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file;
            } else {
                file = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/" + file;
            }
        }
       ...
    }
    ...

so i would recommend you to change your code to:

  private fileName: string;

  constructor(public navCtrl: NavController, private media: MediaPlugin, private file: File, private platform: Platform) {
    this.platform.ready().then(() => {
      if (!this.platform.is('cordova')) {
        return false;
      }

      if (this.platform.is('ios')) {
        this.fileName = file.documentsDirectory.replace(/file:\/\//g, '') + 'record.m4a';
      }
      else if (this.platform.is('android')) {
        this.fileName = file.externalDataDirectory.replace(/file:\/\//g, '') + 'record.3gp';
      }
      else {
        // future usage for more platform support
        return false;
      }
    });
  }

  record() {
    let audioObject: MediaObject = this.media.create(this.fileName);

    audioObject.startRecord();
    console.log('cache dir: ' + this.file.cacheDirectory);
    console.log('start recording' + this.fileName);
    setTimeout(() => {
      audioObject.stopRecord();
      console.log('duration: ' + audioObject.getDuration());
      audioObject.release();
      console.log('done recording' + this.fileName);
      /** Do something with the record file and then delete */
      // this.file.removeFile(this.file.tempDirectory, 'record.m4a');
    }, 5000);
  }
3 Likes

Pure evil! It really works here!! Thank you so much eltonfaust, saved me from a big depression.

Still one remaining point if I may:
I confirm that saving the audio file to dataStorage will fail with same symptom (i.e. file saved as temprecording in root).
But why? I understand that the app has write permission to it.

btw. ‘.m4a’ suffix file also works in my Android tablet. it playbacks well.

I realy didn’t tried to find the reason to not save with dataStorage, sorry.

About the m4a file extension, when it saves the record, doesn’t convert the file, just saves a 3gp file with a m4a sufix, the file is still being played probably because the android audio player uses the file mime type instead of its sufix to corectly play the audio file, maybe if you try to play the file on somewhere else, can crash the player or simply dont play the file…

If you send the recorded file to a server you could use something like the ffmpeg to convert the audio…

Thank eltonfaust. I am on the way to do ffmpeg on server :).

UPDATE:

I guess Media has been updated with this matter because there is no need to .replace(/file:\/\//g, '') anymore. In fact, adding replace will cause the saving issue…

Another update to media and this error is popping up again for me ( on android, with plugin version 4.1.0 ). Trying to save to a path that begins with “file///” throws an encoding error and trying to save to a path that starts with “/” gives me an aborted error.

thanks @eltonfaust this works for me

Hi @fdambrosio @eltonfaust @danielle113
Are your plugin similar to these?
"@ionic-native/file": “^4.3.3”,
"@ionic-native/media": “^4.3.3”,

Mine doesn’t seem to work.

if (cordova.platformId === 'android') {
        this.filePath = this.file.externalDataDirectory.replace(/file:\/\//g, '') + 'audioRecord.3gp'
      } else if (cordova.platformId === 'ios') {
        this.filePath = this.file.documentsDirectory.replace(/^file:\/\//, '') + 'audioRecord.m4a'
      }
      console.log('this.filePath: ', this.filePath)
      console.log('android filePath: ', this.file.externalDataDirectory.replace(/file:\/\//g, '') + 'audioRecord.3gp')
      this.audioFile = this.media.create(this.filePath);
      this.audioFile.onSuccess.subscribe(() => console.log('Action is successful'));
      this.audioFile.onError.subscribe(error => console.log('Error!', error));
      this.audioFile.onStatusUpdate.subscribe(status => console.log('status is ', status));

      if (this.audioFile) {
        this.isRecording = true;
        this.audioFile.startRecord()
        //   .then(()=> {
        //   console.log()
        // });
        console.log('audio startRecord audioFile:', this.audioFile);

        setTimeout(() => {
          this.audioFile.stopRecord();
          console.log('duration: ' + this.audioFile.getDuration());
          this.audioFile.release();
        }, 5000);
      }
}

I get Error! Object {code: 1} via onError subscription once I startRecord.

@eltonfaust,

I am also sttuck in this , I see your code in this thread but i did not get where do i need to use moveFile() method,

Can you please send me some sample code for this

Thanks a lot

This bug is found in version 5.0.2.
The bug has been fixed at this pull request:

This fix allows you to record to app-specific folders e.g. files or cache.

You will still need to remove the “file://” as per eltonfaust’s explanation: e.g.

tempFilename = this.file.dataDirectory + 'filename.m4a';
tempFilename.replace(/file:\/\//g, ''); // yes this is still necessary

see details at
https://issues.apache.org/jira/browse/CB-12849

I spent a lot of hours figuring out it was a bug before looking into the Apache JIRA bug tracker for this plugin. Should have done that earlier.

You will need to pull the change into the plugins folder.

Hello everyone,

I have the same problem on Android with version 5.0.2.

@bobmintons i have followed your link and i have applied the modifications into the code of the plugin, but the bug is still here.

Below, my code :

var fileName = ‘test.3gp’;
var filePathAudio = this.file.dataDirectory.replace(/file:///g, ‘’) + fileName;
var fileAudio1 = this.media.create(filePathAudio);

fileAudio1.onStatusUpdate.subscribe((status) => {
alert(status);
if(status==this.media.MEDIA_RUNNING) {
alert(“it s running”);
}
});

fileAudio1.onError.subscribe((error) => {
this.safetynApi.displayAlert(‘fileAudio1 :’+ error);
this.safetynApi.displayAlert(error);
});

fileAudio1.startRecord();

This simple code returns the error code 1 as @Jun711

Could you help me please?

Best regards

Hi,

Make sure you force the plugin to rebuild. Try a clean build in AndroidStudio. Or, remove the android platform and readd it.
Make sure the config.xml points to the correct version of the Media plugin code, matches the version in plugin.xml.

In project root/config.xml
<plugin name=“cordova-plugin-media” spec="~5.0.2-dev" />

In plugin/cordova-media-plugin
<plugin xmlns=“http://apache.org/cordova/ns/plugins/1.0
xmlns:android=“http://schemas.android.com/apk/res/android
id=“cordova-plugin-media”
version=“5.0.3-dev”>

This is the code I use:

startRecord( ms: number ){
       var rec: MediaObject;
       let fn = this.tempFileName('pc_'); //TODO: debug. pass user name 
       rec  = this.media.create(fn);

       let timerId;
       let timeInterval = 200;
       let elapsed = 0;

       rec.onError.subscribe( error => {
           console.log('Start recording error:', error);
           rec.release();
       });

       rec.onStatusUpdate.subscribe(status => {
           console.log('recordingMediaStatus: ', MEDIA_STATUS[status]);

           if (status == MEDIA_STATUS.RUNNING){
               
               timerId = setInterval( function() {
                   elapsed += timeInterval;
                   console.log('Elapsed time (s):', elapsed/1000);
               }, timeInterval)
           }
           else if (status == MEDIA_STATUS.PAUSED){
               console.log('Elapsed time (s):', elapsed/1000);
               clearInterval(timerId);
           }
           else if (status == MEDIA_STATUS.STOPPED){

               // clean up
               rec.release();          
               // stop timer
               clearInterval(timerId);
           }
       });

       rec.startRecord();

       setTimeout(function() {
           rec.stopRecord();
           console.log('DEBUG: stopRecord 6s timeout');
       }, ms);
   }

   private tempFileName( usr: string ){
       let now = new Date();
       let fn = usr + now.getDate() + now.getMonth() + now.getFullYear() + now.getHours() 
                + now.getMinutes()+ now.getSeconds()+'.m4a';
       fn = this.file.dataDirectory + fn;
       //fn = fn.replace(/file:\/\//g, ''); // Remove the file:// prefix. **Not needed**
       console.log(`DEBUG: current_recording tempfilename = ${fn}`);
       return fn;
   }

Hi everyone, I found this solution

// Create root file -> my_file.mp3
this.file.createFile(this.file.externalApplicationStorageDirectory, 'my_file.mp3', true).then(() => {
  // Get file created -> my_file.mp3
  let audioObject: MediaObject = this.media.create(this.file.externalApplicationStorageDirectory.replace(/file:\/\//g, '') + 'my_file.mp3');
  audioObject.release();
  // Start Record
  audioObject.startRecord();

  // This method is very important to know your execution process
  // In my case the recording interface is not shown as in IOS
  audioObject.onStatusUpdate.forEach(n => {
    switch (n) {
        case MEDIA_STATUS.RUNNING: // return code 2 = recording audio
          // recording
        break;
        case MEDIA_STATUS.STOPPED: // return code 4 = stopped audio
            this.file.readAsDataURL(this.file.externalApplicationStorageDirectory, 'my_file.mp3').then(audioo => {
              // base64 audio
            });
            this.loading.hide();
        break;
    }

  });

  window.setTimeout(() => audioObject.stopRecord(), 9000);
});
DOCS https://www.npmjs.com/package/cordova-plugin-media

Media.MEDIA_NONE = 0;
Media.MEDIA_STARTING = 1;
Media.MEDIA_RUNNING = 2;
Media.MEDIA_PAUSED = 3;
Media.MEDIA_STOPPED = 4;

I hope it helps many. :grinning:

Hello! I have a problem with this code :frowning:
My android studio terminal says:

V/Capacitor/Plugin: To native (Cordova plugin): callbackId: File1300072470, service: File, action: resolveLocalFileSystemURI, actionArgs: ["file:\/\/\/storage\/emulated\/0\/Android\/data\/io.ionic.starter\/"]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: File1300072471, service: File, action: getFile, actionArgs: ["cdvfile:\/\/localhost\/sdcard\/Android\/data\/io.ionic.starter\/","my_file.mp3",{"create":true}]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: INVALID, service: Media, action: create, actionArgs: ["fa04d338-82b1-8f27-d3cc-362a4d60aa03","\/storage\/emulated\/0\/Android\/data\/io.ionic.starter\/my_file.mp3"]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: INVALID, service: Media, action: release, actionArgs: ["fa04d338-82b1-8f27-d3cc-362a4d60aa03"]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: INVALID, service: Media, action: startRecordingAudio, actionArgs: ["fa04d338-82b1-8f27-d3cc-362a4d60aa03","\/storage\/emulated\/0\/Android\/data\/io.ionic.starter\/my_file.mp3"]
W/System.err: java.io.FileNotFoundException: /storage/emulated/0/tmprecording-1622781222190.3gp: open failed: EPERM (Operation not permitted)
        at libcore.io.IoBridge.open(IoBridge.java:492)
        at java.io.RandomAccessFile.<init>(RandomAccessFile.java:289)
        at java.io.RandomAccessFile.<init>(RandomAccessFile.java:152)
        at android.media.MediaRecorder.prepare(MediaRecorder.java:1091)
        at org.apache.cordova.media.AudioPlayer.startRecording(AudioPlayer.java:161)
        at org.apache.cordova.media.AudioHandler.startRecordingAudio(AudioHandler.java:287)
        at org.apache.cordova.media.AudioHandler.promptForRecord(AudioHandler.java:543)
W/System.err:     at org.apache.cordova.media.AudioHandler.execute(AudioHandler.java:118)
        at org.apache.cordova.CordovaPlugin.execute(CordovaPlugin.java:98)
        at org.apache.cordova.PluginManager.exec(PluginManager.java:132)
        at com.getcapacitor.MessageHandler.callCordovaPluginMethod(MessageHandler.java:107)
        at com.getcapacitor.MessageHandler.postMessage(MessageHandler.java:51)
W/System.err:     at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:335)
        at android.os.Looper.loop(Looper.java:183)
        at android.os.HandlerThread.run(HandlerThread.java:67)
    Caused by: android.system.ErrnoException: open failed: EPERM (Operation not permitted)
        at libcore.io.Linux.open(Native Method)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
W/System.err:     at libcore.io.BlockGuardOs.open(BlockGuardOs.java:254)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7542)
        at libcore.io.IoBridge.open(IoBridge.java:478)
    	... 15 more
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: INVALID, service: Media, action: stopRecordingAudio, actionArgs: ["fa04d338-82b1-8f27-d3cc-362a4d60aa03"]
V/ActivityThread: Recovering failed rename /storage/emulated/0/tmprecording-1622781222190.3gp to /storage/emulated/0/Android/data/io.ionic.starter/my_file.mp3
E/ActivityThread: Rename recovery failed 
    android.system.ErrnoException: rename failed: EXDEV (Cross-device link)
        at libcore.io.Linux.rename(Native Method)
        at libcore.io.ForwardingOs.rename(ForwardingOs.java:190)
        at libcore.io.BlockGuardOs.rename(BlockGuardOs.java:350)
        at libcore.io.ForwardingOs.rename(ForwardingOs.java:190)
        at android.app.ActivityThread$AndroidOs.rename(ActivityThread.java:7581)
        at java.io.UnixFileSystem.rename(UnixFileSystem.java:368)
        at java.io.File.renameTo(File.java:1412)
        at org.apache.cordova.media.AudioPlayer.moveFile(AudioPlayer.java:204)
        at org.apache.cordova.media.AudioPlayer.stopRecording(AudioPlayer.java:311)
        at org.apache.cordova.media.AudioHandler.stopRecordingAudio(AudioHandler.java:298)
        at org.apache.cordova.media.AudioHandler.execute(AudioHandler.java:121)
        at org.apache.cordova.CordovaPlugin.execute(CordovaPlugin.java:98)
        at org.apache.cordova.PluginManager.exec(PluginManager.java:132)
        at com.getcapacitor.MessageHandler.callCordovaPluginMethod(MessageHandler.java:107)
        at com.getcapacitor.MessageHandler.postMessage(MessageHandler.java:51)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:335)
        at android.os.Looper.loop(Looper.java:183)
        at android.os.HandlerThread.run(HandlerThread.java:67)
E/AudioPlayer: /storage/emulated/0/tmprecording-1622781222190.3gp: open failed: ENOENT (No such file or directory)
    java.io.FileNotFoundException: /storage/emulated/0/tmprecording-1622781222190.3gp: open failed: ENOENT (No such file or directory)
        at libcore.io.IoBridge.open(IoBridge.java:492)
        at java.io.FileInputStream.<init>(FileInputStream.java:160)
        at org.apache.cordova.media.AudioPlayer.moveFile(AudioPlayer.java:216)
        at org.apache.cordova.media.AudioPlayer.stopRecording(AudioPlayer.java:311)
        at org.apache.cordova.media.AudioHandler.stopRecordingAudio(AudioHandler.java:298)
        at org.apache.cordova.media.AudioHandler.execute(AudioHandler.java:121)
        at org.apache.cordova.CordovaPlugin.execute(CordovaPlugin.java:98)
        at org.apache.cordova.PluginManager.exec(PluginManager.java:132)
        at com.getcapacitor.MessageHandler.callCordovaPluginMethod(MessageHandler.java:107)
        at com.getcapacitor.MessageHandler.postMessage(MessageHandler.java:51)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:335)
        at android.os.Looper.loop(Looper.java:183)
        at android.os.HandlerThread.run(HandlerThread.java:67)
     Caused by: android.system.ErrnoException: open failed: ENOENT (No such file or directory)
        at libcore.io.Linux.open(Native Method)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at libcore.io.BlockGuardOs.open(BlockGuardOs.java:254)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7542)
        at libcore.io.IoBridge.open(IoBridge.java:478)
        at java.io.FileInputStream.<init>(FileInputStream.java:160) 
        at org.apache.cordova.media.AudioPlayer.moveFile(AudioPlayer.java:216) 
        at org.apache.cordova.media.AudioPlayer.stopRecording(AudioPlayer.java:311) 
        at org.apache.cordova.media.AudioHandler.stopRecordingAudio(AudioHandler.java:298) 
        at org.apache.cordova.media.AudioHandler.execute(AudioHandler.java:121) 
        at org.apache.cordova.CordovaPlugin.execute(CordovaPlugin.java:98) 
        at org.apache.cordova.PluginManager.exec(PluginManager.java:132) 
        at com.getcapacitor.MessageHandler.callCordovaPluginMethod(MessageHandler.java:107) 
        at com.getcapacitor.MessageHandler.postMessage(MessageHandler.java:51) 
        at android.os.MessageQueue.nativePollOnce(Native Method) 
        at android.os.MessageQueue.next(MessageQueue.java:335) 
        at android.os.Looper.loop(Looper.java:183) 
        at android.os.HandlerThread.run(HandlerThread.java:67) 
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: File1300072472, service: File, action: resolveLocalFileSystemURI, actionArgs: ["file:\/\/\/storage\/emulated\/0\/Android\/data\/io.ionic.starter\/"]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: File1300072473, service: File, action: getFile, actionArgs: ["cdvfile:\/\/localhost\/sdcard\/Android\/data\/io.ionic.starter\/","my_file.mp3",{"create":false}]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: File1300072474, service: File, action: getFileMetadata, actionArgs: ["cdvfile:\/\/localhost\/sdcard\/Android\/data\/io.ionic.starter\/my_file.mp3"]
V/Capacitor/Plugin: To native (Cordova plugin): callbackId: File1300072475, service: File, action: readAsDataURL, actionArgs: ["cdvfile:\/\/localhost\/sdcard\/Android\/data\/io.ionic.starter\/my_file.mp3",0,0]
I/OpenGLRenderer: Davey! duration=788ms; Flags=0, IntendedVsync=16375330415818, Vsync=16375380415816, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=16375385185100, AnimationStart=16375385223500, PerformTraversalsStart=16375517780200, DrawStart=16375519755500, SyncQueued=16375546274700, SyncStart=16375707910300, IssueDrawCommandsStart=16375708735700, SwapBuffers=16376175747900, FrameCompleted=16376280105700, DequeueBufferDuration=1111700, QueueBufferDuration=5395300, GpuCompleted=16367851817200, 

The problem is in that “callbackId: INVALID” I guess…but I don’t get how to solve this issue :c

Thanks for everyone for your support on this community