安卓4.0.4 WebView使用<audio>标签和本地资源文件时出现MediaPlayer错误(1,-2147483648)

6
我是Android新手,一直在尝试在WebView浏览器中使用HTML5的
3个回答

6
我已经尝试了三种方法来解决这个问题,每一种方法都是其他方法的变体,并且都涉及将原始音频文件从内部“资产”目录(默认MediaPlayer无法访问)复制到可以访问的目录。各种目标目录在链接中描述:http://developer.android.com/guide/topics/data/data-storage.html。其中使用了以下三种:
  • 内部存储[/data/data/your.package.name/files; 即context.getFilesDir()]
  • 外部应用程序存储[/mnt/sdcard/Android/data/package_name/files/Music; 即context.getExternalFilesDir(null)]
  • 外部通用存储[/mnt/sdcard/temp; 即Environment.getExternalStorageDirectory() + "/temp"]
在所有情况下,文件被分配“全局可读”权限以使它们对默认MediaPlayer可访问。首选第一种方法(内部),因为文件作为包本身的一部分并且可以从设备的“管理应用程序”应用程序中的“清除数据”选项中删除,并且在卸载应用程序时会自动删除。第二种方法类似于第一种方法,但仅在卸载应用程序时删除文件,依赖于外部存储,并需要在清单中指定写入外部存储的权限。第三种方法是最不受欢迎的;它类似于第二种方法,但在卸载应用程序时不会清除音频文件。每个code标签都提供了两个path标签,第一个使用Android路径,第二个使用原始路径,以便在将HTML文件合并到Android应用程序之前可以通过Chrome浏览器检查它们。 内部存储解决方案 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);
    }

    ...
}

对应HTML标签

   <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>

外部通用存储解决方案

使用此解决方案时,尝试使用“onDestroy()”方法清理复制到“/mnt/sdcard/temp”目录的临时文件,但在实践中,该方法似乎从未执行过(尝试了两个都被调用的“onStop()”和“onPause()”,但它们过早地删除了文件)。这里给出关于“onDestroy()”的讨论,确认它的代码可能不会执行:

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

清单条目

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

Java 代码

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()

相关的 HTML 标签

   <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>

外部应用存储解决方案

我不会在此列出代码,但它基本上是上面所示解决方案的混合体。请注意,Android必须查看文件扩展名并决定将文件存储在何处;无论您指定目录"context.getExternalFilesDir(null)"还是"context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)",在这两种情况下,文件都将被写入目录"/mnt/sdcard/Android/data/package_name/files/Music"。

最终评论

虽然上述方法可行,但存在缺点。首先,相同的数据被复制,因此存储空间加倍。其次,相同的MediaPlayer用于所有声音文件,因此播放一个文件会中断前一个文件,这样就无法在前景音频上播放背景音频。当我尝试使用此处描述的解决方案时,我能够同时播放多个音频文件:

Android:使用WebView播放资产声音

我认为更好的解决方案是使用Javascript函数来启动音频,进而调用Android Java方法启动自己的MediaPlayer(我可能最终会尝试一下)。


1
在我的情况下,错误是由于媒体播放器在资产目录下本地存储的音频文件没有文件权限造成的。尝试将音频存储到/mnt/sdCARD目录中。

你是说在“onCreate”期间,我应该将音频文件从“file:///android_asset/audio/”目录复制到“/mnt/sdcard/some_unique_dir_name/”,并将我的<audio>标签源更改为“/mnt/sdcard/some_unique_dir_name/some_audio_file.mp3”吗?如果这样做可以解决问题,我可以看到它可能会创建其他问题,比如选择一个不会与其他应用程序潜在干扰的唯一子目录名称,可能的空间限制以及在应用程序突然终止而没有析构函数调用时进行清理。 - VicTorn
我只想确认一下,我已经测试了建议的技术,它确实有效。 - VicTorn
我想确认一下,我已经测试了建议的技术,它确实有效。为了进行实验,我通过Eclipse DDMS的文件浏览器加载了一些声音文件到“/mnt/sdcard/…”下,并在我的<audio><source>标签中使用相同的路径引用这些文件。我无法直接使用“android_asset”文件。我也一直在尝试下面链接中建议的方法,但是到目前为止还没有成功。完成/移动后,将发布我最终使用的解决方案(可能尝试Android“data”目录)。 http://www.weston-fl.com/blog/?p=2988 - VicTorn
我已经成功实现了http://www.weston-fl.com/blog/?p=2988建议的方法。当我清理完通过实验造成的混乱后,将发布解决方案。这种方法的美妙之处在于文件存储在应用程序包中而不是外部存储器中。唯一的缺陷是,在编写HTML“src”值时需要知道特定的Android包名称(构成路径的一部分),这可能会影响自动化HTML生成器的结构(不能仅仅假设所有HTML集合都像“mnt/sdcard/temp/…”)。可以列出两种方法。 - VicTorn

0

小心,暴力方法

我假设您想要在网页内播放音频而不是直接播放。我不确定以下内容是否有帮助,但当我无法控制HTML(即从某些第三方站点加载)时,我仍然需要控制其启动行为。例如,Vimeo目前似乎忽略WebView的即时版本中的“autoplay”URI参数。因此,为了让视频在页面加载时播放,我采用了专利的“插入手臂大约到肘部”的方法。在您的情况下,隐藏标签(根据上面引用的帖子),然后模拟单击具有不在WebView完全加载之前对页面进行检测的好处。然后,您可以注入JS代码(该代码源自此处:在Android Webview中,我能否修改网页的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");
            }
    }

使用直接的JS或者模拟点击,你可以对你的页面进行仪器化。为了仅仅定位一个小标签位置而加载jQuery似乎有些笨重,因此也许你可以使用以下方法(这是从这里无耻地借鉴来的如何在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');";
    }

既然我已经告诉你一些尴尬的事情,那么我就坦白地承认,我使用Chrome浏览器在第三方页面上寻找我想要以这种方式检测的东西。由于Android上没有开发者工具(至少目前还没有),我会调整UserAgent,具体方法可以参考这里http://www.technipages.com/google-chrome-change-user-agent-string.html。然后我会泡杯咖啡(液体,而不是Rails Gem),并且在其他人的代码中胡闹。


谢谢Ted的回复。我已经考虑过一些解决办法,比如在页面加载时创建和控制自己的MediaPlayer来提供背景音乐,并使用CSS隐藏/伪装<a>锚标签以使用此处描述的技术播放“单击”触发的声音:https://dev59.com/mOo6XIcBkEYKwwoYPSL7...接下来... - VicTorn
但在走丑陋的解决方法之前,我首先想知道我的测试示例中是否存在根本性的错误/缺失或Android尚未正确实现用于播放本地存储的音频文件的<audio>标签。 这似乎是一个相当成熟的产品,如果基于其中WebView的Chrome浏览器可以轻松处理本地音频,而其最接近的竞争对手iPhone也可以做到。 此外,我手机上的浏览器应用程序可以像这里演示的那样处理音频:http://www.w3schools.com/html5/tryit.asp?filename=tryhtml5_audio - VicTorn
最终,似乎这里还有其他人遇到了同样的问题,但没有解决方案:https://dev59.com/qmzds4cB2Jgan1znWkVn - VicTorn
这个解决方案就像我在车里掉落的三明治一样(毛茸茸且不可口),没有任何争议。我也同意你的评论,认为你应该能够做到。然而,截至4.1版本,仍然有一些奇怪的东西似乎表明了相反的情况。例如,如果您播放一个HTML5视频流,然后销毁包含它的WebView,与该流相关的MediaPlayer将继续缓冲该流,并且不会释放套接字。桌面浏览器在视图被销毁时会进行整理(例如,在OSX上的Chrome),但是4.1中的Chrome客户端似乎没有这样做。 - Ted Collins
因此,即使您尝试在启动时播放它,您可能仍需要注意在退出时停止流。大多数流HTML5/mp4/h.264(这里使用这些词作为近义词)的“通用”播放器都有“停止”和“卸载”功能(尽管4.1版的Chrome客户端不会关注“卸载”)。因此,在您的活动的“onStop”回调期间(以及根据所需的行为,也许是“onPause”,如果设置了“isFinishing()”),您可能需要“停止”流。我猜想有一种非常规方法可以“正确”地开始流。停止流是另一回事。 - Ted Collins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接