19

In both Chrome (v72, W10) and Opera, the following snippet very occasionally does not seem to run the attached end listener to the SpeechSynthesisUtterance, maybe 1 out of 50 times the snippet is run. (Sorry, in the original version of this, it could be reproduced much more easily - now, creating the utterance on button click looks to have made the bug much more rare)

button.onclick = () => {
  console.log('start script');
  button.disabled = true;
  const utt = new SpeechSynthesisUtterance('e');
  utt.addEventListener('end', () => {
    console.log('end event triggered');
  });

  // just for debugging completeness, no errors seem to be thrown though
  utt.addEventListener('error', (err) => {
    console.log('err', err)
  });

  speechSynthesis.speak(utt);
  setTimeout(() => {
    console.log('finished?');
  }, 1500);
};
<button id="button">click</button>

From what I've seen, if the end event ever activates, it will always activate within a given pageload, which is why I disable the button in the above snippet. (you'll have to rerun the snippet many times to see the problem)

You can reproduce it more readily if you run the below snippet in Chrome (72 on W10) with autoplay restrictions disabled. (go to chrome://flags/, change Autoplay policy to No user gesture is required).

(In Opera, it seems to be similarly difficult to reproduce as in the first snippet, unfortunately)

console.log('start script');
function say(text) {
  const utt = new SpeechSynthesisUtterance(text);
  utt.addEventListener('end', () => console.log('end: ' + text));
  
  // just for debugging completeness, no errors seem to be thrown though
  utt.addEventListener('error', (err) => {
    console.log('err on ' + text + ', ', err)
  });
  
  speechSynthesis.speak(utt);
}

say('foo');
say('bar');

Firefox (56) does not have this issue as far as I can see - in it, the end listener always fires properly.

Am I somehow not attaching the listener sufficiently properly, or is this a Chromium bug?

Snow
  • 3,820
  • 3
  • 13
  • 39
  • Oops! It looks like Opera hasn't implemented the autoplay restrictions yet, and I had a custom setting on my Chrome that permitted autoplay, so I didn't see the error. Triggering the utterance on click still [results in the bug](https://i.stack.imgur.com/uHWlX.png), though unfortunately it's a lot less reproducible. – Snow Mar 08 '19 at 05:06
  • didn't you use "onend" instead of "utt.addEventListener('end', () => {" – Udara Kasun Mar 08 '19 at 06:24
  • @UdaraKasun No, I always used `addEventListener` for the `end` event, you can see the edit history, but I tried it and it looks like using `.onend =` behaves the same. – Snow Mar 08 '19 at 06:35
  • Able to reproduce in Chrome 72.0.3626.81. Maybe 1 in 20 or so and doesn't seem to happen when the dev console is open. – Ouroborus Mar 08 '19 at 08:38
  • @Kaiido reproduced on Mojave, chrome v 72.0.3626.121 – WilliamNHarvey Mar 08 '19 at 19:15

1 Answers1

8

Edit/Update: @Ouroborus pointed out that this is indeed an open Chromium bug


I fired up Sawbuck and started attempts to reproduce this. When the issue occurs, I consistently see gc activity happening between the 'start script' and 'finished?' logs.

Example of success:

enter image description here

Example of failure:

enter image description here

So, it seems like it may be that the gc process is interfering with the end event being delivered.

To further test this theory I started chrome with the --js-flags="--expose-gc" flag which enables the v8 gc function, allowing one to force garbage collection.

If I modify your test code and add window.gc() before console.log('start script'), I can no longer reproduce the issue (>50 attempts). It's possible this is because it reduces/eliminates the chance that gc occurs during the speech utterance.

It seems that you may be able to prevent the SpeechSynthesisUtterance object from being gc'd by console.log-ing it. This does seem to result in consistent delivery of the event. Obviously preventing their collection is probably not ideal if you're creating a great number of these objects:

button.onclick = () => {
  console.log('start script');
  button.disabled = true;
  const utt = new SpeechSynthesisUtterance('e');
  
  // Prevent garbage collection of utt object
  console.log(utt);

  utt.addEventListener('end', () => {
    console.log('end event triggered');
  });

  // just for debugging completeness, no errors seem to be thrown though
  utt.addEventListener('error', (err) => {
    console.log('err', err)
  });

  speechSynthesis.speak(utt);
  setTimeout(() => {
    console.log('finished?');
  }, 1500);
};
<button id="button">click</button>
cody
  • 11,045
  • 3
  • 21
  • 36
  • 3
    https://bugs.chromium.org/p/chromium/issues/detail?id=509488 Workaround is similar to what you suggest: Store the `SpeechSynthesisUtterance` object somewhere more permanent so that it doesn't get collected. – Ouroborus Mar 11 '19 at 17:31