goldfire / howler.js

Javascript audio library for the modern web.
https://howlerjs.com
MIT License
23.29k stars 2.21k forks source link

In iPhone, it loss all sounds after answering a phone call #671

Open aswind opened 7 years ago

aswind commented 7 years ago

I used howler.js in a HTML5 game, and find an issue in ios (we test it in iPhone 5 / 5S / 6 / 6S, with Chrome and Safari):

When a player is playing the game, and someone call in. If player refuses to answer the phone call, everything is OK. But if the player answers the phone, when he finishes the phone call and the game resumes, all the sounds in the game will be lost. All sounds will no longer appear until refresh the webpage.

In Android, there is no such a issue.

goldfire commented 7 years ago

Do you know if playing a sound inside user interaction (such as a click) causes the sound to start again? I'm wondering if the sound gets re-locked after the phone call.

aswind commented 7 years ago

It is a slot game, when player tap SPIN button, the button sound and spin sound will be played. After answering a phone call, all sounds are lost, no matter how you tap the SPIN button.

aswind commented 7 years ago

Yes, after phone call, I click buttons to play sounds, but no sound appearing. I run the demo pages in your website with same iPhone, after phone call, all sounds are lost too.

lewiji commented 7 years ago

I'm having a similar problem with losing sound completely intermittently on iOS when the device is put to sleep and woken back up, wondering if this is the same general issue - same problem where even user initiated sounds are lost. iPad Air 2, iOS 10.2.1, howler v2.0.3

agalyan commented 7 years ago

Hi guys, is there any chance to get this annoying issue fixed in the near feature ?

goldfire commented 7 years ago

I am able to reproduce, but so far I haven't found a workaround. I'm happy to get a fix implemented if anyone can find a solution.

fregante commented 7 years ago

You'll probably have to re-enable the context every so often: https://github.com/goldfire/howler.js/blob/754770312ab8ca3e97cfcdf309d72bf837392906/src/howler.core.js#L278

yi1yang commented 6 years ago

Same issue and we tried to use _enableMobileAudio to enable the audio but it does not work.

An interesting thing is this issue exists in CreateJS as well, unsolved.

https://github.com/CreateJS/SoundJS/issues/264

DFortun81 commented 6 years ago

I was able to fix this issue in our game by turning off autoSuspend and also preventing autoResume from being activated on iOS and Safari. In the native Objective C/Swift code, when the app went to the background, I told Howler to _suspend (essentially a version of _autoSuspend without the timeout) and when the app regained focus, I told Howler to _resume. (essentially a version of _autoResume without the iOS/Safari User Agent check that I was using to prevent it from doing anything on iOS)

Note that I'm doing the user agent check only once when Howler is initialized and caching that to a simple boolean. User Agent parsing can get quite expensive otherwise.

The issue I had discovered was that autoResume would immediately trigger after the process was supposed to go into the background. While the app is not in focus, it redirects any active context's output source to one that's internally set as silent. (I think that's the best guess I have for why this bug exists.)

k8w commented 6 years ago

The same problem.. anyone have solution ?

yi1yang commented 6 years ago

Hi Mr. Fortune,

Would you mind sharing a little piece of code regarding this?

I told Howler to _suspend (essentially a version of _autoSuspend without the timeout) and when the app regained focus, I told Howler to _resume. (essentially a version of _autoResume without the iOS/Safari User Agent check that I was using to prevent it from doing anything on iOS)

We tried everything we could to solve this problem, but seems like when the phone call coming and make the safari background thread, there is no event we can catch to take any action. Maybe there is a smart way that we cant get it now.

Thanks bro!

DFortun81 commented 6 years ago

Certainly. Assuming this is for an iOS app that uses WKWebView and that you have a public reference to your webView (I'm using self.webView, if you're using something else, replace where I'm using self.webView with what you have.) and some way of telling your ViewController that the web context is loaded and can accept JavaScript injections. (I use self.contextLoaded for this. I set it to true shortly after loading my HTML data into the webview.) At the top of your AppDelegate.m file, add this import statement so that you can use the extension methods provided later:

#import "ViewController (Howler).h"

In that same file, implement the following methods if they don't exist already, if they do, add the additional lines to those methods:

- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    [self.viewController SuspendAudio];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    [self.viewController SuspendAudio];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    [self.viewController ResumeAudio];
}

Now in your ViewController, you need to implement the two methods used above. First, create a new Header file called "ViewController (Howler).h". This file will extend your existing ViewController and keep these modifications out of your main ViewController header for clarity.

#import "ViewController.h"

@interface ViewController (Howler)
-(void) ResumeAudio;
-(void) SuspendAudio;
-(void) Inject:(NSString *)javaScript;
@end

Now create the "ViewController (Howler).m" file and on the right side bar in XCode, attach the file to your project in the "Target Membership" section. This this side bar isn't visible, there's a button in the top right of the interface that will make that appear.

#import "ViewController (Howler).h"

@implementation ViewController (Howler)
-(void) ResumeAudio
{
    [self Inject:@"if(typeof Howler != 'undefined') { Howler._resume(); }"];
}
-(void) SuspendAudio
{
    [self Inject:@"if(typeof Howler != 'undefined') { Howler._suspend(); }"];
}

// This is a helper method for injecting JavaScript into your webView. You really only need this if you don't already have a way to inject JavaScript into your webview.
-(void) Inject:(NSString *)javaScript
{
    if(self.contextLoaded) {
        //NSLog(javaScript);
        [self.webView evaluateJavaScript:javaScript completionHandler:nil];
    }
}
@end

Now we need to tweak Howler's code a bit. Look for the section in the init function where self.autoSuspend is set to true. You're going want to set that to false by default. self.autoSuspend = false; Now that this has been dealt with, we need to implement the _suspend method. I'm not using the latest release of Howler, so your _suspend function might have some changes in it: (it's essentially just the body of the _suspendTimer setTimeout function without the check for autoSuspend.)

    _suspend: function() {
        var self = this;
        if (!self.ctx || typeof self.ctx.suspend === 'undefined' || !Howler.usingWebAudio) {
            return;
        }

        if (self._suspendTimer) {
            clearTimeout(self._suspendTimer);
            self._suspendTimer = null;
        }
        self.state = 'suspending';
        self.ctx.suspend().then(function() {
          self.state = 'suspended';

          if (self._resumeAfterSuspend) {
            delete self._resumeAfterSuspend;
            self._autoResume();
          }
        });
      return self;
    },

Now we need to implement the _resume method. This is more or less the same as _autoResume except that _autoResume has been given some additional boolean logic that we want to bypass on iOS by calling it directly in our application.

* Automatically resume the Web Audio AudioContext when a new sound is played.
* @return {Howler}
*/
_autoResume: function() {
    // Make sure the AudioContext isn't suspended, and resume it if it is.
    if (isFirefox || (this._webAudio && !(isiOS || isSafari))) return this._resume();
    return this;
},
_resume: function() {
    var self = this;

    if (!self.ctx || typeof self.ctx.resume === 'undefined' || !Howler.usingWebAudio) {
        return;
    }

    if (self.state === 'running' && self._suspendTimer) {
        clearTimeout(self._suspendTimer);
        self._suspendTimer = null;
    }
    else if (self.state === 'suspended') {
        self.ctx.resume().then(function() {
        self.state = 'running';

        // Emit to all Howls that the audio has resumed.
        for (var i=0; i<self._howls.length; i++) {
            self._howls[i]._emit('resume');
        }
        });

        if (self._suspendTimer) {
            clearTimeout(self._suspendTimer);
            self._suspendTimer = null;
        }
    }
    else if (self.state === 'suspending') {
        self._resumeAfterSuspend = true;
    }

    return self;
}

Notice that I'm using "isFirefox", "isiOS" and "isSafari". These variables are cached at the top of howler.js in my copy.

var ua = navigator.userAgent.toLowerCase();
var isIE = !!ua.match(/msie|trident/);
var isEdge = !!ua.match(/edge/);
var isAndroid = ua.indexOf("android") > -1;
var isChrome = ua.indexOf("chrome") > -1;
var isFirefox = ua.indexOf("firefox") > -1;
var isiOS = !!navigator.platform.match(/(iPhone|iPod|iPad)/i);
var isSafari = !(isiOS || isAndroid || isChrome || isEdge || isIE) && window.safari !== undefined;

If you are ONLY using HowlerJS inside of a WebView in iOS, you can leave the user agent checks out and also comment out the body of _autoResume. This would be a slight optimization.

yi1yang commented 6 years ago

Hi Mr. Fortune,

Thanks for your detailed explanation and code sharing. We are using this howler.js for a HTML5 game which is running in the Safari and its a bit different from running in an App because there is no events (as much as I know at the moment) that we can catch when there is a incoming call. So, this solution might not work for us and the bug is still pending.

Anyway, thanks again for your help, i think your solution is great for app+webview stuff.

Thanks Bro!

k8w commented 6 years ago

@yi1yang I found a way to detect this. if you have a playing sound, you can check by its seek(), for example:

let sound = new Howl({ ... });
sound.play();
let oldPos = sound.seek();
setTimeout(()=>{
    if(sound.seek() === oldPos){
        // Sound not play as expected !!
        window.once('touchstart', ()=>{
            sound.pause();  
            ctx.suspend();
            ctx.resume();
            sound.play();            
        })
    }
}, 500)
kurozael commented 6 years ago

Hi @goldfire this issue is still occuring on iPhones for me with the latest Howler in 2018, I'm wondering if a proper solution can be included in the next patch?

nanek commented 6 years ago

I had a similar issue that I fixed with https://github.com/goldfire/howler.js/pull/928

kurozael commented 6 years ago

Thanks @nanek I'll give this a go!

SHISME commented 5 years ago

Anyone has some solution about this problem?

digitaloranges commented 5 years ago

Any movement on this?

SHISME commented 5 years ago

hello, every one ,our team found that when we use UIWebview, the app have this problem, after we use WkWebview recently ,the problem was resolved, so i think it may be a UIWebview bug

ringcrl commented 5 years ago
  // Because some devices seem to break the audioContext instance from hardware
  // the previous audioContext instance needs to be closed and reinitialized when used again
  if (Howler.ctx) {
    Howler.ctx.close();
    delete Howler.ctx;
  }