Senti
Senti

Reputation: 339

Audio not working in Android WebView using shouldInterceptRequest()

My Android app is showing an html5 e-book in a WebView.
I have a zipped file containing an e-book with all its resources: text, images and audio (mp3 files).
In order to unzip the book I use shouldInterceptRequest(), which intercepts the file:///... requests, and returns the data via a WebResourceResponse object. The code works fine for text and images.
When I get to audio resources, I get runtime errors, and the audio file is not played.
Note: I do see the unzipped file is returned with the correct size (about 10MB).

Error messages I get:
cr_MediaResourceGetter File does not exist
cr_MediaResourceGetter Unable to configure metadata extractor

My HTML code for the audio :

<div xmlns="http://www.w3.org/1999/xhtml">
  <p style="text-align:center;margin:0px;">
    <audio controls="controls" src="../Audio/01-AudioTrack-01.mp3">Your browser does not support the audio tag.</audio>
    <br />
  </p>
</div>

My Android Code:

    setWebViewClient(new WebViewClient()
    {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, final String url)
    {
        String urlWithoutAnchor = URLUtil.stripAnchor(url); 
        String fileName = urlWithoutAnchor;

        try {
            byte [] resource = tbxPool.getResource(fileName); 
         /* SIMPLE VERSION without calling setResponseHeaders():
            return new WebResourceResponse(mimeType, "UTF-8", new ByteArrayInputStream(resource)); 
            */
            WebResourceResponse returnedMediaResource = new WebResourceResponse(mimeType, "UTF-8", new ByteArrayInputStream(resource));
            if (mimeType.toLowerCase().startsWith("audio")) {
                Map<String, String> responseHeaders = new HashMap<String, String>();

                responseHeaders.put("Content-Type", mimeType);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//2CLEAN
                    returnedMediaResource.setResponseHeaders(responseHeaders); 
                    Logger.v(TAG, "Response Headers added to audio resource");
                }
                else {
                    //TODO: Handle else for API<21. Toast?
                }
            }                               
            return returnedMediaResource;
        } catch (IOException e) {
            Logger.e(TAG, "failed to load resource "+fileName,e);
            return null;
        }
    }
}

Environment
Android 6.0.1 (Nexus 5) Android System WebView version 47

Requirement Clarification
The audio is to play in browser like an html5 document should, without laucnhing external player.

Question:
What am I doing wrong?! Many Thanks in advance!

Upvotes: 2

Views: 2319

Answers (1)

Senti
Senti

Reputation: 339

The workaround I found to this problem is not elegant, but it's the only one that worked for me: Write the audio file to sd card :(.
Stage 1): When shouldInterceptRequest() is called with a chapter url.
The chapter is intercepted first (before the other chapter resources (images, audio, fonts, ..) are intercepted.
When the chapter is intercepted we search the html for the <audio> tag. If found, we replace the relative path (e.g. SRC="../Audio/abc.mp3") with an absolute path (e.g. SRC="/storage/tmp/abc.mp3")

Stage 2): When shouldInterceptRequest() is called with an audio url.
Your attention. Like all workarounds this is a bit tricky (but works!):
After Stage 1) the audio url will be an absolute url (the absolute url is what is now written in the modified html).
We now have to do 2 things:
a) read the audio file from the zipped epub.
To do this we need to "fool" the code, and read the audio file from its original zipped relative url, e.g. "../Audio/abc.mp3" in our example (although shouldInterceptRequest has been called with "/storage/tmp/abc.mp3").
b) After reading the zipped audio file, write it to the storage (sdcard)

Stage 3) When shouldInterceptRequest() is called with a chapter url, We delete the temp audio files Note: If you follow the code, you will see this is Step 0) in shouldInterceptRequest(), executed before stage 1), but I found it clearer explained as above.

if (isChapterFile(mimeType)) {  
    deleteTempFiles(); // this line is stage 3)  
        changeAudioPathsInHtml(tzis); // this line is stage 1)

This is the code:

setWebViewClient(new WebViewClient()
{
    private String tmpPath = TbxApplication.getAppPath(null) + "/tmp/"; 
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, final String url) 
    {
        Logger.d(TAG, "in shouldInterceptRequest for " + url);  
        String urlWithoutAnchor = URLUtil.stripAnchor(url); 
        String mimeType = StringUtils.getFileMimeType(urlWithoutAnchor);
        String urlWithoutBase; //the url stripped from leading 'epubBaseUrl' (base url for example:"file:///storage/.../123456.tbx")

            if (isAudioFile(mimeType)) { //write AUDIO file to phone storage. See AUDIO WORKAROUND DOCUMENTATION    
                String storagePath = StringUtils.truncateFileScheme(url);  //WebView calls shoudlInterceptRequest() with "file://..."
                try {
                    String oEBPSAudioPath = storagePathToOEBPSAudioPath(storagePath); //e.g. change"/storage/tmp" to "OEBPS/Audio/abc.mp3"
                    byte[] audioBytes = tbxPool.getMedia(oEBPSAudioPath); 
                    FileUtils.writeByteArrayToFile(audioBytes, storagePath); //TODO: To be strict, write in separate thread
                    Logger.d(TAG, String.format("%s written to %s", oEBPSAudioPath, storagePath));
                    return null;//webView will read resource from file
                                //Note: return new WebResourceResponse("audio/mpeg", "UTF-8", new ByteArrayInputStream(audioBytes));
                                //did NOT work,so we had to change html for audio to point to local storage & write to disk
                                //see AUDIO WORKAROUND DOCUMENTATION in this file
                } catch (Exception e) {
                    Logger.e(TAG,e.getMessage());
                    return null;
                } 
            } 
            .....
            else {
                if (isChapterFile(mimeType)) { //This is a CHAPTER
                    deleteTempFiles(); //Loading a new chapter. Delete previous chapter audio files. See AUDIO WORKAROUND DOCUMENTATION in this file
                    InputStream htmlWithChangedAudioPaths = changeAudioPathsInHtml(tzis); //see AUDIO WORKAROUND DOCUMENTATION in this file
                    WebResourceResponse webResourceResponse = new WebResourceResponse(mimeType, "UTF-8", htmlWithChangedAudioPaths);
                    return webResourceResponse; 
                }

        //Changes relative paths of audio files, to absolute paths on storage
        //see AUDIO WORKAROUND DOCUMENTATION in this file
        private InputStream changeAudioPathsInHtml(InputStream inputStream) {
            String inputString = StringUtils.inputStreamToString(inputStream);
            String outputString = inputString.replaceAll("\"../Audio/", "\"" + tmpPath);// e.g. SRC="../Audio/abc.mp3" ==>SRC="/sdcard/tmp/abc.mp3"                                                                              //where '*' stands for multiple whitespaces would be more elegant
            return StringUtils.stringToInputStream(outputString); 
        }

        /** Example:
         * storagePath="/storage/tmp/abc.mp3
         * Returns: "OEBPS/Audio/abc.mp3"*/
        private String storagePathToOEBPSAudioPath(String storagePath){
            String fileName = StringUtils.getFileName(storagePath);
            String tbxOEBPSAudioPath = "OEBPS/Audio/" + fileName;
            return tbxOEBPSAudioPath; 
        }

public static void writeByteArrayToFile(byte[] byteArray, String outPath) { 
    try {
        File file = new File(outPath);
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(byteArray);
        fos.close();
    } catch (IOException e) {
        Logger.e(TAG, String.format("Could not write %s", outPath));
    }
}

Upvotes: 2

Related Questions