Wilkolicious / twitchAdSkip

141 stars 11 forks source link

Volume slider not in sync with volume after reset #5

Open Wilkolicious opened 3 years ago

ToxiClay commented 3 years ago

Confirmed still an issue at 0200 4Nov.

twitchAdSkip: Video player observer attached
twitchAdSkip: Volume modified to: '0.46'.
twitchAdSkip: Volume modified to: '0.53'.
twitchAdSkip: Volume modified to: '0.67'.
twitchAdSkip: Volume modified to: '0.33'.
twitchAdSkip: Volume modified to: '0.61'.
twitchAdSkip: Found ad node at: [data-test-selector="ad-banner-default-text"]
twitchAdSkip: Finding video node to post-fix volume.
twitchAdSkip: Volume before reset: 0.61
twitchAdSkip: Triggering FFZ reset button...
twitchAdSkip: Fixing volume to original value of '0.61' after interval of '2000' ms
twitchAdSkip: Post-fixed volume from reset val of '0.10000000149011612' -> '0.61'

Visual volume slider was at 0, then it moved visually back to where I had it set, but the volume was definitely not where I had it set. It sounded like the volume was still down at the reset val instead of back up at 61%.

garbb commented 3 years ago

I was playing with my own script and found a weird workaround that works for me to sync the volume slider. Basically, read the pre-reset volume value from the volume slider control, click the reset player button to reset the player, then dispatch mouseleave event to mute button, then click mute button twice to mute/unmute. Seems to sync up the volume slider control to the actual player volume.

const dblclick = new MouseEvent('dblclick');
const mouseleave = new MouseEvent('mouseleave', {'bubbles': true})

const videoPlayer = document.querySelector('[data-a-target="video-player"] video');
const volumeSlider = document.querySelector("[id^='player-volume-slider']");
const muteButton = document.querySelector('[data-a-target="player-mute-unmute-button"]');
const ffzResetButton = document.querySelector('[data-a-target="ffz-player-reset-button"]');

let currentVolumeFromControl = volumeSlider.value;
ffzResetButton.dispatchEvent(dblclick);
videoPlayer.volume = currentVolumeFromControl;
muteButton.dispatchEvent(mouseleave);

setTimeout(() => {
    //mute
    muteButton.click()
}, 500);

setTimeout(() => {
    //unmute
    muteButton.click()
}, 1000);
Wilkolicious commented 3 years ago

@garbb That's a good find with the mute button. A manual double click (~100ms between clicks?) on the mute button seems to return the slider to the correct position after video player FFZ refresh. I don't think we need to touch the volume slider at all which is great for simplifying things. I'd prefer to keep the amount of race conditions present in the logic to a minimum where possible. That includes reading the vol slider after ad detection - Twitch may fix the slider value in the future or an slower/faster PC may read it too late.

I'll test stripping out any volume slider logic and err towards using a quick mute/unmute.

garbb commented 3 years ago

The mute/unmute trick works for me when the tab is active, but I have noticed that sometimes if the twitch tab is in the background (another tab is active in the same window) then the player will remain muted. Maybe some timing issue with the unmute click? Maybe worth testing.

Wilkolicious commented 3 years ago

So I've tested two scenarios through the devtools console:

  1. That the mute/unmute syncs the slider to the actual volume: First, set the slider to 0.5
    document.querySelector('video').volume = 1;
    document.querySelector('[data-a-target="player-volume-slider"]').value = 0.5;

Then execute the mute "double click":

muteBtn = document.querySelector('[data-a-target="player-mute-unmute-button"]');

muteBtn.dispatchEvent(new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
    view: window
  }));
setTimeout(() => {
  muteBtn.dispatchEvent(new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
    view: window
  }));
}, 100);

Result: Works - slider returns to correct position (i.e. all the way to the right indicating fully on, or 1

  1. Perform the steps above, but:
    • set the volume slider in Twitch tab
    • switch to a new tab in the same window
    • execute the double click
    • return to Twitch tab

Result: Works - slider returns to correct position (i.e. all the way to the right indicating fully on, or 1

Going to have to be wary of race conditions with these timeouts however. I've tested putting a version of the above code in the script, but it broke the volume persistence after FFZ refresh. Seems it might need the first timeout.

simple-hacker commented 3 years ago

What I think I found out with the volume was that Twitch would normalise the video element volume.

So before an ad, the video player volume value would be 0.8 When an ad is triggered the volume value would be normalised to say 0.44848 so that the ad and stream volume would be about the same. Each ad had a different normalising value, like The Boys ad was louder than the Playstation ad. After the ad finished, the video element volume value would still be 0.44848, but the stream audio would sound like 0.8 and so it's get normalised back. So basically the stream audio would sound the same before and after an ad, but the video element volume value would be different once an ad has been played. The volume slider value would still be 0.8 throughout.

I don't know what the normalising function is, but I did find that Twitch periodically kept note of the stream_loudness. If you check localStorage for "video_ads.stream_loudness" you'll see a json object like {"loudness":-29.025481476458374,"timestamp":1604528264282} This gets updated fairly frequently.

I think if you set the video volume to the slider value, this may still be incorrect because it's still been normalised to the ad volume. I think the best bet would be to trigger a volume change with a click event to the slider, but would have to click say ±0.1 and then ±0.1 again, because clicking at the same value doesn't do anything fire a change event. I think on a volume change, Twitch then renormalises the stream volume to the slider value.

Take this with a pinch of salt though because I was still trying to wrap my head around what was happening.

Wilkolicious commented 3 years ago

Thanks @simple-hacker, very useful investigative notes! I'll have a play with a slider click + change event. Have a feeling it won't work.

One thing I've noticed with the mute/unmute workaround, is that the volume will be set to Twitch's normalised volume if the mute/unmute button is clicked too soon after FFZ refresh. It's a matter of trial & error atm - 2 seconds is too short, 10 seconds works sometimes. Seems Twtich's app sets the volume in its components or logic after some time (some event?), or doesn't sync internally meaning that the unmute volume is still set to the normalised volume.

ToxiClay commented 3 years ago

One thing I've noticed with the mute/unmute workaround, is that the volume will be set to Twitch's normalised volume if the mute/unmute button is clicked too soon after FFZ refresh. It's a matter of trial & error atm - 2 seconds is too short, 10 seconds works sometimes. Seems Twtich's app sets the volume in its components or logic after some time (some event?), or doesn't sync internally meaning that the unmute volume is still set to the normalised volume.

Dragging the volume slider immediately resets the actual speaker volume to what it's supposed to be. Is there a way to simulate that in your script? Simply setting the volume appears to move the slider but not actually adjust it, possibly due to the normalization that simple-hacker mentions.

simple-hacker commented 3 years ago

I also found I was setting the volume too quickly, before FFZ had a chance to refresh. Yeah 2 seconds was touch and go with myself, but then I had to take in to consideration that people have slower pcs and internet than I do so we could never get a true amount.

I did start playing around with a MO on the src of video. On a player refresh the src would change from src="blob:https://www.twitch.tv/6dd193a6-ed72-434f-ad8e-ad3a244993df" to null and then to blob:https://www.twitch.tv/6f14a495-933b-4db0-8a1c-abeb983c3876

So when the src changed from null to a new blob the video player should be fully refreshed. Here's where I got to (as a starting point)

` const attachVideoMutationObserver = function(videoPlayerElement) {

    const originalSrc = videoPlayerElement.getAttribute('src');

    let options = {
        attributes: true,
        attributeFilter: ["src"],
        attributeOldValue: true
    }

    const videoObserver = new MutationObserver(function(mutations) {
        mutations.forEach(mutation => {
            console.log('Original src: '+originalSrc);
            console.log('Mutation Old Value: '+mutation.oldValue);
            let newSrc = videoPlayerElement.getAttribute('src');
            console.log('Mutation New Value: '+newSrc);
            if (mutation.oldValue == null && originalSrc !== newSrc) {
                console.log('Video player reloaded.  Refreshing volume...');
                // let volumeSlider = document.querySelector('[data-a-target="player-volume-slider"]');
                let videoPlayerElement = document.querySelector('video');
                if (videoPlayerElement) {
                    console.log('Current volume slider: '+videoPlayerElement.volume);
                    // volumeSlider.value = volumeSlider.value;
                    setInterval(() => {
                        console.log('New volume slider: '+videoPlayerElement.volume);
                    }, 250);
                }
            }
        });
    });

    videoObserver.observe(videoPlayerElement, options);
}

`

What I found is that programmatically changing the volumeSlider.value wouldn't actually change the value of the volumeSlider, though volumeSlider.defaultValue would, but the volume doesn't actually change and the slider is the same (but value is what you set defaultValue to), but then something on Twitch's end would reset the volume value to a second later.

subz390 commented 3 years ago

Not sure whether this helps, but in the process of testing I found that I wasn't able to programically unmute or set the volume when returning back to the stream after the ads without granting permissions in the browser (Firefox). In my script I just mute and unmute, and the levels stay the same when returning back to the stream so I don't touch the volume.

image

For the Observer I watch div[data-test-selector="sad-overlay"], and in the callback mute and unmute when the node is added and removed.

// rough idea...
if (mutation.addedNodes.length > 0) {
        mainVideo.muted = true
}
else if (mutation.removedNodes.length > 0) {
        mainVideo.muted = false
}

Not sure how this will work across a FFZ reset - my script is still a work in progress - but I thought I'd chip in when I stumbled across this repo.