6

I'm new to Android and have been trying to get the HTML5 <audio> tag to work in a WebView browser but keep getting MediaPlayer Error (1, -2147483648). The file I'm trying to play resides below the "assets" directory. I've tried referencing a file in the "res/raw" directory, but with the same result.

To verify that the files could be found and played, as part of my tests I created a variation of the code where the sound would be triggered through an <a> tag and would be processed by a WebViewClient, using the suggestions here:

Android: Playing an Asset Sound Using WebView

It worked (although I had to trim off the leading "file:///android_asset" from the URL), but using anchors is not how I want the pages to operate. I'd like a background sound to play when the page opens and other sounds to be triggered through Javascript when certain <div> tags are clicked. I've read elsewhere that Android now supports tags, but I've had no luck with them and I'm using the latest SDK.

I've created a stripped down test page to experiment with, details of which are shown below. I've looked all over for a solution with no luck. I don't know what's missing (my preference is to avoid any add-ons if possible and work with Android alone).

Assets Directory Layout

assets
 > audio
   > a-00099954.mp3
   > a-00099954.ogg
 > image
 > script
 > style
 > video
 audioTest.html

Java Code

package com.test.audiotag;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebView;

public class MainActivity extends Activity
{
    private WebView localBrowser;

   @Override
   public void onCreate(Bundle savedInstanceState)
   {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       localBrowser = (WebView)findViewById(R.id.localbrowser);
       localBrowser.getSettings().setJavaScriptEnabled(true);
       localBrowser.loadUrl("file:///android_asset/audioTest.html");
   }
}

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android      ="http://schemas.android.com/apk/res/android"
          package            ="com.test.audiotag"
          android:versionCode="1"
          android:versionName="1.0">
    <uses-sdk android:minSdkVersion="14" />
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name             =".MainActivity"
                  android:label            ="@string/app_name"
                  android:screenOrientation="portrait">
            <intent-filter>
                <action   android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

HTML Page

<!DOCTYPE html>
<html>
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
   <meta name="viewport" content="width=300, height=400">
   <style type="text/css">
    #centre_all
    {
       -webkit-box-flex  : 0;
       position          : relative;
       background-color  : #D0D000;
       border            : 2px dotted red;
       -webkit-box-shadow: 0 0 5px red;
    }
   </style>
</head>
<body>
   <div id="centre_all" style="width:300px;height:400px;">
     <audio controls="controls" autoplay="autoplay">
        <source src="audio/a-00099954.mp3" type="audio/mpeg" />
        <source src="audio/a-00099954.ogg" type="audio/ogg"/>
        &#160;
     </audio>
   </div>
</body>
</html>
Community
  • 1
  • 1
VicTorn
  • 191
  • 2
  • 7

3 Answers3

6

I've experimented with 3 approaches to addressing this problem, each of which is a variation of the others, and each involve copying the original audio files from the internal "assets" directory (apparently not accessible to the default MediaPlayer) to a directory which can be accessed. The various target directories are described at link:

http://developer.android.com/guide/topics/data/data-storage.html

of which these three were used:

  • internal storage [ /data/data/your.package.name/files; ie. context.getFilesDir() ]
  • external application storage [ /mnt/sdcard/Android/data/package_name/files/Music; ie. context.getExternalFilesDir(null) ]
  • external general storage [ /mnt/sdcard/temp; ie. Environment.getExternalStorageDirectory() + "/temp" ]

In all cases, the files were assigned "world readable" privileges to make them accessible to the default MediaPlayer. The first approach (internal) is preferred because the files are incorporated as part of the package itself and can be removed from the "Clear data" option through the device's "Manage Apps" application and are automatically removed when the application is uninstalled. The second approach is similar to the first, but the files are only removed when the application is uninstalled, is dependent on external storage, and requires privileges to write to external storage specified in the manifest. The third approach is the least favourable; it is similar to the second, but there is no cleanup of the audio files when the application is uninstalled. Two tags were provided for each tag, the first uses the Android path, and the second uses the original path so that the HTML files can be examined through a Chrome browser before being incorporated in the Android App.

Internal Storage Solution

Java Code

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    localBrowser = (WebView)findViewById(R.id.localbrowser);
    localBrowser.getSettings().setJavaScriptEnabled(true);
    localBrowser.setWebViewClient(new WebViewClient());
    localBrowser.setWebChromeClient(new WebChromeClient());
    ...

    Context context  = localBrowser.getContext();
    File    filesDir = context.getFilesDir();
    Log.d("FILE PATH", filesDir.getAbsolutePath());
    if (filesDir.exists())
    {
        filesDir.setReadable(true, false);
        try
        {
            String[] audioFiles = context.getAssets().list("audio");
            if (audioFiles != null)
            {
                byte[]           buffer;
                int              length;
                InputStream      inStream;
                FileOutputStream outStream;
                for (int i=0; i<audioFiles.length; i++)
                {
                    inStream  = context.getAssets().open(
                                "audio/" + audioFiles[i] );
                    outStream = context.openFileOutput(audioFiles[i],
                                        Context.MODE_WORLD_READABLE);
                    buffer    = new byte[8192];
                    while ((length=inStream.read(buffer)) > 0)
                    {
                        outStream.write(buffer, 0, length);
                    }
                    // Close the streams
                    inStream.close();
                    outStream.flush();
                    outStream.close();
                }
            }
        }
        catch (Exception e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    // Feedback
    String[] fileList = context().fileList();
    Log.d("FILE LIST",  "--------------");
    for (String fileName : fileList)
    {
        Log.d("- FILE", fileName);
    }

    ...
}

Corresponding HTML Tags

   <div id="centre_all" style="width:300px;height:400px;">
     <audio controls="controls" autoplay="autoplay">
        <source src="/data/data/com.test.audiotag/files/a-00099954.mp3" type="audio/mpeg" />
        <source src="audio/a-00099954.mp3" type="audio/mpeg" />
        &#160;
     </audio>
   </div>

External General Storage Solution

With this solution, attempted to use "onDestroy()" method to cleanup temporary files copied to "/mnt/sdcard/temp" directory when done but in practise, the method never seemed to executed (tried both "onStop()" and "onPause()" which were called, but they prematurely removed the files). A discussion of "onDestroy()" given here confirms that it's code may not execute:

http://developer.android.com/reference/android/app/Activity.html#onDestroy()

Manifest Entry

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Java Code

private String[] audioList;

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    localBrowser = (WebView)findViewById(R.id.localbrowser);
    localBrowser.getSettings().setJavaScriptEnabled(true);
    localBrowser.setWebViewClient(new WebViewClient());
    localBrowser.setWebChromeClient(new WebChromeClient());
    ...

    Context context = localBrowser.getContext();
    File    dirRoot = new File(Environment.getExternalStorageDirectory().toString());
    Log.d("ROOT DIR PATH", dirRoot.getAbsolutePath());
    if (dirRoot.exists())
    {
        File dirTemp = new File(dirRoot.getAbsolutePath() + "/temp");
        if (!dirTemp.exists())
        {
            if (!dirTemp.mkdir())
            {
                Log.e("AUDIO DIR PATH", "FAILED TO CREATE " +
                                        dirTemp.getAbsolutePath());
            }
        }
        else
        {
            Log.d("AUDIO DIR PATH", dirTemp.getAbsolutePath());
        }
        if (dirTemp.exists())
        {
            dirTemp.setReadable(true, false);
            dirTemp.setWritable(true);
            try
            {
                String[] audioFiles = context.getAssets().list("audio");
                if (audioFiles != null)
                {
                    byte[]           buffer;
                    int              length;
                    InputStream      inStream;
                    FileOutputStream outStream;

                    audioList = new String[audioFiles.length];
                    for (int i=0; i<audioFiles.length; i++)
                    {
                        inStream     = context.getAssets().open(
                                       "audio/" + audioFiles[i] );
                        audioList[i] = dirTemp.getAbsolutePath() + "/" +
                                       audioFiles[i];
                        outStream    = new FileOutputStream(audioList[i]);
                        buffer       = new byte[8192];
                        while ( (length=inStream.read(buffer)) > 0)
                        {
                            outStream.write(buffer, 0, length);
                        }
                        // Close the streams
                        inStream.close();
                        outStream.flush();
                        outStream.close();
                        //audioFile = new File(audioList[i]);
                        //audioFile.deleteOnExit();
                    }
                }
            }
            catch (Exception e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        // Feedback
        String[] fileList = dirTemp.list();
        Log.d("FILE LIST",  "--------------");
        for (String fileName : fileList)
        {
            Log.d("- FILE", fileName);
        }
    }

    ...
}   // onCreate()

@Override
public void onDestroy()
{
    for (String audioFile : audioList)
    {
        File hFile = new File(audioFile);
        if (hFile.delete())
        {
            Log.d("DELETE FILE", audioFile);
        }
        else
        {
            Log.d("DELETE FILE FAILED", audioFile);
        }
    }
    super.onDestroy();
}   // onDestroy()

Corresponding HTML Tags

   <div id="centre_all" style="width:300px;height:400px;">
     <audio controls="controls" autoplay="autoplay">
        <source src="/mnt/sdcard/temp/a-00099954.mp3" type="audio/mpeg" />
        <source src="audio/a-00099954.mp3" type="audio/mpeg" />
        &#160;
     </audio>
   </div>

External Application Storage Solution

I won't list the code here, but it's essentially a blend of the solutions shown above. Note that Android must look at the file extensions and decide where to store the files; whether you specify directory "context.getExternalFilesDir(null)" or "context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)", in both cases, the files will be written to directory "/mnt/sdcard/Android/data/package_name/files/Music".

Final Comments

While the above approach works, there are shortcomings. First, the same data is duplicated and therefore doubles the storage space. Second, the same MediaPlayer is used for all sound files so the playing of one will interrupt its predecessor which prevents being able to play background audio over which separate foreground audio is played. When I experimented with the solution described here, I was able to play multiple audio files simultaneously:

Android: Playing an Asset Sound Using WebView

I think a better solution would be one that uses a Javascript function to launch the audio that in turn calls an Android Java method that starts its own MediaPlayer (I may eventually experiment with that).

Community
  • 1
  • 1
VicTorn
  • 191
  • 2
  • 7
1

In my case the error was due to the mediaplayer not having file permissions on the locally stored audio under assets directory. Try storing the audio onto the /mnt/sdCARD directory.

droiding
  • 41
  • 1
  • Are you saying that during "onCreate" I should copy the audio files from "file:///android_asset/audio/" directory to "/mnt/sdcard/some_unique_dir_name/", and change my – VicTorn Sep 14 '12 at 10:54
  • I just want to acknowledge that I've tested the suggested technique and it does work. – VicTorn Sep 16 '12 at 11:59
  • I just want to acknowledge that I've tested the suggested technique and it does work. For the sake of experimenting, I loaded a few sound files below "/mnt/sdcard/..." through the File Explorer of Eclipse DDMS and referenced those files using the same path in my – VicTorn Sep 16 '12 at 12:10
  • I've managed to get the approach suggested by: http://www.weston-fl.com/blog/?p=2988 to work. When I've cleaned up the mess I've made through experimenting, will post solution. The beauty of this approach is that the files get stored with the application package and not in external storage. The one shortfall is that you need to know your specific Android package name (forms part of the path) when coding the HTML "src" values which may affect how automated HTML generators are structured (can't just assume something like "mnt/sdcard/temp/..." for all HTML collections). May list both approaches. – VicTorn Sep 16 '12 at 13:36
0

Beware, Brute Force Approach

I'm assuming that you want the audio played inside a web page instead of directly. I am not sure the following will help, but I have hit similar situations when I do not control the HTML (i.e. loaded from some third party site), and I still need to control its behavior on launch. For example, Vimeo appears (at this point) to ignore the "autoplay" uri parameter in the instant version of WebView. So, to get a video to play when the page loads I employed the patented 'insert arms approximately up to the elbows' approach. In your case, hiding a tag (per the post you cite above), and then simulating a click has the benefit of not instrumenting the page before the WebView has completely loaded. Then, you inject JS code (Which is adopted from here: In Android Webview, am I able to modify a webpage's DOM?):

    mWebView.setWebViewClient(new CustomVimeoViewClient(){

            @Override
            public void onPageFinished(WebView view, String url) {
                    super.onPageFinished(view, url);
                    String injection = injectPageMonitor();
                    if(injection != null) {
                        Log.d(TAG, "  Injecting . . .");
                        view.loadUrl(injection);
                    }
                    Log.d(TAG, "Page is loaded");
            }
    }

Using either direct JS, or else simulating clicks, you can instrument your page. Loading jQuery seems a bit ham-fisted for just one little tag location, so perhaps you might use the following (Which is shamelessly lifted from here How to Get Element By Class in JavaScript?):

    private String injectPageMonitor() {
    return( "javascript:" +
        "function eventFire(el, etype){ if (el.fireEvent) { (el.fireEvent('on' + etype)); } else { var evObj = document.createEvent('Events'); evObj.initEvent(etype, true, false); el.dispatchEvent(evObj); }}" +
        "eventFire(document.querySelectorAll('.some_class_or_another')[0], 'click');";
    }

And as long as I am already telling you embarrassing things, I will round out by confessing that I use Chrome to poke around in third-party pages to find things I want to instrument in this way. Since there are no developer tools on Android (yet), I adjust the UserAgent, described here http://www.technipages.com/google-chrome-change-user-agent-string.html. I then procure coffee (the liquid, not the Rails Gem), and poke about in other people's code causing mischief.

Community
  • 1
  • 1
Ted Collins
  • 710
  • 2
  • 8
  • 15
  • Thank you Ted for the reply. I've thought about workarounds like creating and controlling my own MediaPlayer when the page loads to provide the background sound, and hiding/disguising anchor tags via CSS to play "on click" triggered sounds using the technique described here: http://stackoverflow.com/questions/10966245/android-playing-an-asset-sound-using-webview ... continued ... – VicTorn Sep 08 '12 at 13:00
  • but before travelling down the ugly workaround route, I'd first really like to know if there is something fundamentally wrong/missing in what I've done in my test example or if Android hasn't yet properly implemented – VicTorn Sep 08 '12 at 13:01
  • Finally, it seems someone else here has run into the same problem without resolution: http://stackoverflow.com/questions/9162022/playing-sound-in-webview-from-javascript – VicTorn Sep 08 '12 at 13:02
  • No argument here. . . this solution is about like the sandwich I dropped on the floor of my car (hairy and unappetizing). I also agree with your comment that you *should* be able to do it. As of 4.1, however, there are still some oddments that *seem* to suggest otherwise. For example, if you play an HTML5 video stream, and then destroy the WebView containing it, the MediaPlayer attached to the stream continues to buffer the stream, and won't release the sockets. Where a desktop browser will tidy up when a view is destroyed (e.g. Chrome on OSX), the Chrome Client in 4.1 appears to not do that. – Ted Collins Sep 10 '12 at 12:56
  • Ergo, even if you tackle getting it to play on launch, you may need to pay attention to stopping the train on exit. Most "universal" players that stream HTML5/mp4/h.264 (using these as approximate synonyms here), have 'stop' and 'unload' (though, again, 4.1's Chrome client doesn't pay attention to 'unload'). So, you may need to 'stop' the stream during the 'onStop' callback of your activity (and, perhaps 'onPause' if the 'isFinishing()' is set, depending on your desired behavior). I'm guessing that there is a non-hairy way to 'properly' start the stream. Stopping the stream is another matter. – Ted Collins Sep 10 '12 at 13:05