apache / cordova-plugin-media

Apache Cordova Media Plugin
https://cordova.apache.org/
Apache License 2.0
388 stars 765 forks source link

Can't play mp3 - No such file / Failed to connect to localhost #401

Open nbruley opened 6 days ago

nbruley commented 6 days ago

Bug Report

Problem

I cannot play mp3 files with the plugin apart from those on my online server.

What is expected to happen?

Play mp3 file. I believe this worked a long time ago but no longer works now...

What does actually happen?

Get error:

/storage/emulated/0/Android/data/com.hablamedecristo.himnario2/files/test.mp3: open failed: ENOENT (No such file or directory)

Note: when trying to access an mp3 file with cordova.file.applicationStorageDirectory or https://localhost/test.mp3, I get the error "java.net.ConnectException: Failed to connect to localhost/127.0.0.1:443"

Information

Command or Code

I have test.mp3 saved in the android/platform_www directory. I also can download to cordova.file.applicationStorageDirectory via the cordova-plugin-file-transfer plugin.

function playDownloadedAudio (fileTransferWebUrl, thisElement) {
    my_media = new Media("test.mp3",
        function () //success;
        { }, 
        function (error) //error
        { 
            alertOK(translate('Error in playback. Please contact support. \n Code: ','text') + error.code + '\n '+translate('Message:','text')+' '+error.message + '\n');
            playing = false;
        }
    );
    my_media.play();
}

config.xml

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.hablamedecristo.himnario2" version="6.9.0" versionCode="77" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>X</name>
    <description>
        X
    </description>
    <author email="X" href="X">
        X
    </author>
    <content src="index.html" />
    <preference name="android-installLocation" value="preferExternal" />
    <preference name="AndroidPersistentFileLocation" value="Compatibility" /> <!-- use "Internal" instead of "Compatibility" for new apps -->
    <preference name="android-targetSdkVersion" value="34" />
    <preference name="android-compileSdkVersion" value="34" />
    <preference name="android-minSdkVersion" value="23" />
    <preference name="AndroidGradlePluginVersion" value="8.5.1" />
    <preference name="loadUrlTimeoutValue" value="700000" />
    <preference name="scheme" value="https" />
    <preference name="hostname" value="localhost" />
    <plugin name="cordova-plugin-media" spec="7.0.0" />
    <plugin name="cordova-plugin-insomnia" />
    <plugin name="cordova-plugin-file" />
    <plugin name="cordova-plugin-email-composer" />
    <!--<plugin name="cordova-plugin-wkwebview-file-xhr" spec="3.1.1" />-->
    <plugin name="cordova-plugin-file-transfer" />
    <icon src="icon.png" />
    <allow-navigation href="*" />
    <access origin="*" />
    <access origin="file:///*" />
    <access origin="cdvfile://*" />
    <allow-intent href="file:///*" />
    <allow-intent href="cdvfile://*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="mailto:*" />

    <!-- not sure if these are needed  https://developer.android.com/training/data-storage/shared/media#kotlin -->
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

    <platform name="android">
        <edit-config file="app/src/main/AndroidManifest.xml" mode="merge" target="/manifest/application">
            <application android:usesCleartextTraffic="true" />
        </edit-config>
        <allow-intent href="market:*" />
        <preference name="FadeSplashScreenDuration" value="0"/>
        <preference name="SplashScreenDelay" value="0" />
        <preference name="AndroidWindowSplashScreenBackground" value="#9D9D9C" />
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
    </platform>
</widget>

Content Security Policy

<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: cdvfile: http://mysite.com http://mysite2.com http://mysite3.com 'unsafe-eval'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' https://www.googletagmanager.com data:">

Environment, Platform, Device

Android Target SDK = 34 Galaxy Tab A7, Android 12

Version information

Cordova version 12.0.0 cordova-media-plugin version 7.0.0 cordova-plugin-file version 8.1.0 cordova-plugin-file-transfer version 2.0.0 Android Studio Koala 2024.1.1 Patch 2

Checklist

breautek commented 2 days ago

There's a few things to unpack here, most of which doesn't make much sense:

/storage/emulated/0/Android/data/com.hablamedecristo.himnario2/files/test.mp3

This path suggests that you have your test.mp3 stored in the cordova.file.dataDirectory and that your application is installed on an "SD Card" (a virtual sd card, not a physical one, hence /storage/emulated/...). OR you're using cordova.file.externalApplicationStorageDirectory. I'm going to refer to this as "External" storage here on out.

There are limitations in using external storage through File APIs, though I do not think they apply to externalApplicationStorageDirectory paths. It's worth testing using cordova.file.dataDirectory with your app installed on android's Internal storage to see if that makes a difference.

Note: when trying to access an mp3 file with cordova.file.applicationStorageDirectory

applicationStorageDirectory is suppose to be the app install directory, which where your apk is unpacked. This directory itself is suppose to be read only. This should point to /storage/emulated/0/Android/data/com.hablamedecristo.himnario2. cordova.file.dataDirectory should point to /storage/emulated/0/Android/data/com.hablamedecristo.himnario2/files.

I also can download to cordova.file.applicationStorageDirectory via the cordova-plugin-file-transfer plugin.

As I stated above, applicationStorageDirectory is suppose to be a read-only directory, therefore you shouldn't be able to download files to it.

I get the error "java.net.ConnectException: Failed to connect to localhost/127.0.0.1:443"

If you're using the file transfer plugin to "download" from localhost, you're receiving this error because in this context, localhost is the WebViewAssetLoader, it's only accessible from within the webview itself. The file transfer plugin however uses the native network stack, which is external of the webview. The file transfer plugin is intended to be used for external resources, not to download from the app itself.

I have test.mp3 saved in the android/platform_www directory.

the platform_www directory is something that cordova manages in preparing platform-specific web assets. It's not intended for developers to manually add content to it. If you want to bundle assets, use <cordova-project-root>/www directory instead. Manually adding content inside platform_www (or anything inside the platforms/ directory in general) is not recommended.

Moving forward....

Because there is a lot of weird constructs in the information you've provided, we will need a sample reproduction app that demonstrates the issue. This will help isolate the bug from your application code and allows others to easily reproduce the issue. If the issue cannot be reproduced in a minimal sample application, then it suggests that the bug doesn't rely in the media plugin.

nbruley commented 1 day ago

I must say, much of this doesn't make sense to me either. That's why I'm posting here.

One thing I will say is my app was created quite a long time ago, and at that time it was advantageous to install to SD card if so I used installLocation of "prefer external". My AndroidPersistentFileLocation is now set to "compatibility" which I think puts the app data in a different location than I would do if I were creating the app now (with internal setting). If that is the case, creating a new app might not reproduce this exact problem. Would Android default to looking in ApplicationStorageDirectory given my config.xml settings and latest changes to Android permissions?

I tried applicationStorageDirectory just because I wondered if that would be the www folder. [https://cordova.apache.org/docs/en/dev/reference/cordova-plugin-file/index.html] states that this location is r/w but applicationDirectory is read only. Where are you getting your information about applicationStorageDirectory being read only?

I am using the file-transfer plugin to download from an online web location to the device. I can see a file produced on the device when I download to externalRootDirectory because apparently the emulated SD card is the internal Music directory on my device. Likewise, I do not get a download error when downloading to applicationStorageDirectory even though I obviously can't see the file anywhere. However, when trying to play a (missing) file which I never downloaded to applicationStorageDirectory, I get a media plugin error that file was not found, whereas if I download it first, I get the Logcat error "failed to connect to localhost" and an undefined error from the media plugin.

The errors I'm experiencing are not related to the file-transfer plugin. That plugin works fine. The error occurs when I try to play a file I downloaded with the media plugin.

I understand that using the platform_www directory is not recommended; however, unlike iOS, I've never been able to get the files from the www folder to update the platform_www when rebuilding the app. Maybe you can enlighten me on which step I'm missing, but that's another story and irrelevant to this issue.

Perhaps you can respond to the above before I go about trying to figure out how to create a sample reproduction app? I appreciate your time. I do think that creating a new app from scratch with internal instead of external settings would be worthwhile and would potentially solve my problem for future apps.

breautek commented 1 day ago

Where are you getting your information about applicationStorageDirectory being read only?

My apologies, I got confused between applicationStorageDirectory and applicationDirectory. You're right that applicationStorageDirectory is a writable path.

I understand that using the platform_www directory is not recommended; however, unlike iOS, I've never been able to get the files from the www folder to update the platform_www when rebuilding the app. Maybe you can enlighten me on which step I'm missing, but that's another story and irrelevant to this issue.

You should have a <cordova-project-root>/www directory that contains your index.html and other web assets. Cordova copies this entire directory and places it in the platform's assets. For android, that would be inside /platforms/android/app/src/main/assets/.

I am using the file-transfer plugin to download from an online web location to the device. I can see a file produced on the device when I download to externalRootDirectory because apparently the emulated SD card is the internal Music directory on my device.

Android has became much more stricter in attempt to provide greater privacy to the end-user. Since API 29 accessing the external file system (e.g. anything inside /storage/emulated/...) has severe limitations when using the File API. Content is suppose to be accesesd via the MediaStore, which Apache does not have a plugin for, but there are third-party plugins available. Though I'm not sure what happens if the application is installed on the external filesystem/sd card. I do believe the application-specific directories on the external storage is still freely accessible. On API 29+ device, writing files to externalRootDirectory should not work, but applicationStorageDirectory should work.

However, when trying to play a (missing) file which I never downloaded to applicationStorageDirectory, I get a media plugin error that file was not found, whereas if I download it first, I get the Logcat error "failed to connect to localhost" and an undefined error from the media plugin.

This part I'm not sure if I'm completely understanding.

Suppose your loaded document is at <cordova-project>/www/index.html. Everything inside the www directory becomes app assets, which in the webview this maps to https://localhost/. I'm not sure on top of my head what the path looks like on device, but it leads to a readonly path, where the app install is. So keep note of this, we'll come back to this detail in a moment.

It sounds like you're using the file transfer plugin to download a mp3 to applicationStorageDirectory which is a directory completely outside of the app's asset directory. It's on a separate data partition that is writable.

my_media = new Media("test.mp3", ...

So now we have this code snippet, that relatively loads in test.mp3. This will be relative to the loaded documented. So if the loaded document is https://localhost/index.html, then the above will attempt to load https://localhost/test.mp3 and this is looking for test.mp3 in the read-only assets directory, which wouldn't exist, and is certainly not the same directory as applicationStorageDirectory.

If you want to use the downloaded file, you'll need to get the FileEntry object using the file plugin. With the FileEntry you can use .toURL() to get a DOM-usable URL. The URL will look something like https://localhost/__cdvfile__xxxx/test.mp3. I've mentioned previously about limitations with accessing the external storage with the File API, but as long as the file is stored within an app directory (e.g. /storage/emulated/0/<app-id>/...) then I believe it will still work.

An example/untested code might look like:

let path = cordova.file.applicationStorageDirectory + "/test.mp3";

window.resolveLocalFilesystemURI(path, function(fileEntry) {
  let media = new Media(fileEntry.toURL());
  ...
}, onError);

Let me know if this helps.

nbruley commented 1 day ago

You should have a /www directory that contains your index.html and other web assets. Cordova copies this entire directory and places it in the platform's assets. For android, that would be inside /platforms/android/app/src/main/assets/. Thank you for this helpful information. It would appear that things in the platform_www folder get written to assets\www if present, which is why my method was working, but adding them to the root www directory also does appear to work, now that I know the platform_www directory isn't where the files are finally referenced. There was potentially a phonegap update issue back in the day with www that isn't an issue with Android Studio / command line. Even when placing assets in the root www folder, however, they still do not transfer to the assets\www folder when building in Android Studio; I still have to build via command line to make that happen.

On API 29+ device, writing files to externalRootDirectory should not work I am confident that for my Android 12 device (API 31/32), this writes to Internal storage, maybe because I don't have an SD card. I have found documentation that there is an emulated SD location.

then the above will attempt to load https://localhost/test.mp3 and this is looking for test.mp3 in the read-only assets directory, which wouldn't exist Wouldn't https://localhost/test.mp3 exist if I place it in the root www folder and the test.mp3 ends up in assets/www?

If you want to use the downloaded file, you'll need to get the FileEntry object using the file plugin. With the FileEntry you can use .toURL() to get a DOM-usable URL. The URL will look something like https://localhost/__cdvfile__xxxx/test.mp3. I've mentioned previously about limitations with accessing the external storage with the File API, but as long as the file is stored within an app directory (e.g. /storage/emulated/0//...) then I believe it will still work.

When using the toURL with value for example "https://localhost/__cdvfile_files__/A_casa_vete_y_cuenta_alli.mp3" I get this error:

java.net.ConnectException: Failed to connect to localhost/127.0.0.1:443
at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:147)
at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:542)
at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:106)
at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:30)
at android.media.MediaHTTPConnection.seekTo(MediaHTTPConnection.java:306)
at android.media.MediaHTTPConnection.getMIMEType(MediaHTTPConnection.java:499)
at android.media.IMediaHTTPConnection$Stub.onTransact(IMediaHTTPConnection.java:161)
at android.os.Binder.execTransactInternal(Binder.java:1220)
at android.os.Binder.execTransact(Binder.java:1179)

I suspect any https://localhost URL would give me this error.

breautek commented 1 day ago

I suspect any https://localhost/ URL would give me this error.

You're suspicion is right. It seems like the plugin is passing through the https://localhost url through to the underlaying media APIs, which uses the java network stack to attempt to request it, but the endpoint is not hittable by anything outside of the webview. It's definitely a problem.

In fact I don't think the URL is being loaded through the webview, I think the URL is just being passed via the media plugin...

Given that information, then I think this should work

let path = cordova.file.applicationStorageDirectory + "/test.mp3";
let media = new Media(path);

You may want to double check the value of cordova.file.applicationStorageDirectory, I forget if it contains a trailing slash or not, so I'm not sure if the leading slash is necessary when appending "/test.mp3".

breautek commented 1 day ago

Alternatively, you can probably use the native browser feature as well instead of the media plugin.

let path = cordova.file.applicationStorageDirectory + "/test.mp3";

window.resolveLocalFilesystemURI(path, function(fileEntry) {
  let audio = new Audio(fileEntry.toURL());
  audio.play();
  ...
}, onError);

As an example, or you can create a audio node via document.createElement('audio') to get the browser audio player UI that can be appended to the DOM somewhere.

The .toURL() should work in this case because Audio should be passed through the webview / WebViewAssetLoader where https://localhost/__cdvfile_files__/A_casa_vete_y_cuenta_alli.mp3 will get parsed and remapped appropriately, over the browser's network stack.

Depending on your use cases, the usage of the media plugin might not be necessary. The HTML5 Audio/Video APIs are widely supported nowadays.