Merge branch 'master' into master

This commit is contained in:
th-ch
2023-01-14 15:02:01 +01:00
committed by GitHub
27 changed files with 877 additions and 220 deletions

View File

@ -1,5 +1,5 @@
const applyCompressor = () => {
const audioContext = new AudioContext();
const applyCompressor = (e) => {
const audioContext = e.detail.audioContext;
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50;
@ -8,10 +8,11 @@ const applyCompressor = () => {
compressor.attack.value = 0;
compressor.release.value = 0.25;
const source = audioContext.createMediaElementSource(document.querySelector("video"));
source.connect(compressor);
e.detail.audioSource.connect(compressor);
compressor.connect(audioContext.destination);
};
module.exports = () => document.addEventListener('apiLoaded', applyCompressor, { once: true, passive: true });
module.exports = () =>
document.addEventListener("audioCanPlay", applyCompressor, {
passive: true,
});

View File

@ -4,8 +4,8 @@ const { dialog, app } = require("electron");
const registerCallback = require("../../providers/song-info");
// Application ID registered by @xn-oah
const clientId = "942539762227630162";
// Application ID registered by @Zo-Bro-23
const clientId = "1043858434585526382";
/**
* @typedef {Object} Info

View File

@ -51,7 +51,7 @@ function sendToaster(songInfo) {
//download image and get path
let imgSrc = notificationImage(songInfo, true);
toDelete = {
appID: is.dev() ? undefined : "com.github.th-ch.youtube-music",
appID: "com.github.th-ch.youtube-music",
title: songInfo.title || "Playing",
message: songInfo.artist,
id: parseInt(Math.random() * 1000000, 10),

View File

@ -47,6 +47,7 @@ const togglePiP = async () => {
win.webContents.on("before-input-event", blockShortcutsInPiP);
win.setMaximizable(false);
win.setFullScreenable(false);
runAdaptors();
@ -62,6 +63,7 @@ const togglePiP = async () => {
}
} else {
win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
win.setMaximizable(true);
win.setFullScreenable(true);
runAdaptors();

View File

@ -10,6 +10,21 @@ const pipButton = ElementFromFile(
templatePath(__dirname, "picture-in-picture.html")
);
// will also clone
function replaceButton(query, button) {
const svg = button.querySelector("#icon svg").cloneNode(true);
button.replaceWith(button.cloneNode(true));
button.remove();
const newButton = $(query);
newButton.querySelector("#icon").appendChild(svg);
return newButton;
}
function cloneButton(query) {
replaceButton(query, $(query));
return $(query);
}
const observer = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
@ -30,9 +45,6 @@ global.togglePictureInPicture = () => {
const listenForToggle = () => {
const originalExitButton = $(".exit-fullscreen-button");
const clonedExitButton = originalExitButton.cloneNode(true);
clonedExitButton.onclick = () => togglePictureInPicture();
const appLayout = $("ytmusic-app-layout");
const expandMenu = $('#expanding-menu');
const middleControls = $('.middle-controls');
@ -44,7 +56,7 @@ const listenForToggle = () => {
ipcRenderer.on('pip-toggle', (_, isPip) => {
if (isPip) {
$(".exit-fullscreen-button").replaceWith(clonedExitButton);
replaceButton(".exit-fullscreen-button", originalExitButton).onclick = () => togglePictureInPicture();
player.onDoubleClick_ = () => {};
expandMenu.onmouseleave = () => middleControls.click();
if (!playerPage.playerPageOpen_) {
@ -67,10 +79,8 @@ function observeMenu(options) {
"apiLoaded",
() => {
listenForToggle();
const minButton = $(".player-minimize-button");
// remove native listeners
minButton.replaceWith(minButton.cloneNode(true));
$(".player-minimize-button").onclick = () => {
cloneButton(".player-minimize-button").onclick = () => {
global.togglePictureInPicture();
setTimeout(() => $('#player').click());
};

View File

@ -1,37 +1,112 @@
const hark = require("hark/hark.bundle.js");
module.exports = () => {
module.exports = (options) => {
let isSilent = false;
let hasAudioStarted = false;
document.addEventListener("apiLoaded", () => {
const video = document.querySelector("video");
const speechEvents = hark(video, {
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
interval: 2, // ms
});
const skipSilence = () => {
if (isSilent && !video.paused) {
video.currentTime += 0.2; // in s
}
};
const smoothing = 0.1;
const threshold = -100; // dB (-100 = absolute silence, 0 = loudest)
const interval = 2; // ms
const history = 10;
const speakingHistory = Array(history).fill(0);
speechEvents.on("speaking", function () {
isSilent = false;
});
document.addEventListener(
"audioCanPlay",
(e) => {
const video = document.querySelector("video");
const audioContext = e.detail.audioContext;
const sourceNode = e.detail.audioSource;
speechEvents.on("stopped_speaking", function () {
if (!(video.paused || video.seeking || video.ended)) {
isSilent = true;
// Use an audio analyser similar to Hark
// https://github.com/otalk/hark/blob/master/hark.bundle.js
const analyser = audioContext.createAnalyser();
analyser.fftSize = 512;
analyser.smoothingTimeConstant = smoothing;
const fftBins = new Float32Array(analyser.frequencyBinCount);
sourceNode.connect(analyser);
analyser.connect(audioContext.destination);
const looper = () => {
setTimeout(() => {
const currentVolume = getMaxVolume(analyser, fftBins);
let history = 0;
if (currentVolume > threshold && isSilent) {
// trigger quickly, short history
for (
let i = speakingHistory.length - 3;
i < speakingHistory.length;
i++
) {
history += speakingHistory[i];
}
if (history >= 2) {
// Not silent
isSilent = false;
hasAudioStarted = true;
}
} else if (currentVolume < threshold && !isSilent) {
for (let i = 0; i < speakingHistory.length; i++) {
history += speakingHistory[i];
}
if (history == 0) {
// Silent
if (
!(
video.paused ||
video.seeking ||
video.ended ||
video.muted ||
video.volume === 0
)
) {
isSilent = true;
skipSilence();
}
}
}
speakingHistory.shift();
speakingHistory.push(0 + (currentVolume > threshold));
looper();
}, interval);
};
looper();
const skipSilence = () => {
if (options.onlySkipBeginning && hasAudioStarted) {
return;
}
if (isSilent && !video.paused) {
video.currentTime += 0.2; // in s
}
};
video.addEventListener("play", function () {
hasAudioStarted = false;
skipSilence();
}
});
});
video.addEventListener("play", function () {
skipSilence();
});
video.addEventListener("seeked", function () {
skipSilence();
});
});
video.addEventListener("seeked", function () {
hasAudioStarted = false;
skipSilence();
});
},
{
passive: true,
}
);
};
function getMaxVolume(analyser, fftBins) {
var maxVolume = -Infinity;
analyser.getFloatFrequencyData(fftBins);
for (var i = 4, ii = fftBins.length; i < ii; i++) {
if (fftBins[i] > maxVolume && fftBins[i] < 0) {
maxVolume = fftBins[i];
}
}
return maxVolume;
}

View File

@ -63,7 +63,7 @@ module.exports = (win) => {
// If the page is ready, register the callback
win.once("ready-to-show", () => {
controls = [previous, playPause, next, like, dislike];
controls = [previous, playPause, next, dislike, like];
// Register the callback
registerCallback((songInfo) => {

View File

@ -2,6 +2,10 @@
align-items: unset !important;
}
#main-panel {
position: relative;
}
.video-switch-button {
z-index: 999;
box-sizing: border-box;

View File

@ -38,7 +38,7 @@ function setup(e) {
player = $('ytmusic-player');
video = $('video');
$('ytmusic-player-page').prepend(switchButtonDiv);
$('#main-panel').append(switchButtonDiv);
if (options.hideVideo) {
$('.video-switch-button-checkbox').checked = false;
@ -58,6 +58,21 @@ function setup(e) {
video.addEventListener('srcChanged', videoStarted);
observeThumbnail();
switch (options.align) {
case "right": {
switchButtonDiv.style.left = "calc(100% - 240px)";
return;
}
case "middle": {
switchButtonDiv.style.left = "calc(50% - 120px)";
return;
}
default:
case "left": {
switchButtonDiv.style.left = "0px";
}
}
}
function changeDisplay(showVideo) {

View File

@ -33,6 +33,38 @@ module.exports = (win, options) => [
},
]
},
{
label: "Alignment",
submenu: [
{
label: "Left",
type: "radio",
checked: options.align === 'left',
click: () => {
options.align = 'left';
setMenuOptions("video-toggle", options);
}
},
{
label: "Middle",
type: "radio",
checked: options.align === 'middle',
click: () => {
options.align = 'middle';
setMenuOptions("video-toggle", options);
}
},
{
label: "Right",
type: "radio",
checked: options.align === 'right',
click: () => {
options.align = 'right';
setMenuOptions("video-toggle", options);
}
},
]
},
{
label: "Force Remove Video Tab",
type: "checkbox",

View File

@ -0,0 +1,6 @@
const { injectCSS } = require("../utils");
const path = require("path");
module.exports = (win, options) => {
injectCSS(win.webContents, path.join(__dirname, "empty-player.css"));
};

View File

@ -0,0 +1,9 @@
#player {
margin: 0 !important;
background: black;
}
#song-image,
#song-video {
display: none !important;
}

View File

@ -0,0 +1,61 @@
const defaultConfig = require("../../config/defaults");
module.exports = (options) => {
const optionsWithDefaults = {
...defaultConfig.plugins.visualizer,
...options,
};
const VisualizerType = require(`./visualizers/${optionsWithDefaults.type}`);
document.addEventListener(
"audioCanPlay",
(e) => {
const video = document.querySelector("video");
const visualizerContainer = document.querySelector("#player");
let canvas = document.getElementById("visualizer");
if (!canvas) {
canvas = document.createElement("canvas");
canvas.id = "visualizer";
canvas.style.position = "absolute";
canvas.style.background = "black";
visualizerContainer.append(canvas);
}
const resizeCanvas = () => {
canvas.width = visualizerContainer.clientWidth;
canvas.height = visualizerContainer.clientHeight;
};
resizeCanvas();
const gainNode = e.detail.audioContext.createGain();
gainNode.gain.value = 1.25;
e.detail.audioSource.connect(gainNode);
const visualizer = new VisualizerType(
e.detail.audioContext,
e.detail.audioSource,
visualizerContainer,
canvas,
gainNode,
video.captureStream(),
optionsWithDefaults[optionsWithDefaults.type]
);
const resizeVisualizer = (width, height) => {
resizeCanvas();
visualizer.resize(width, height);
};
resizeVisualizer(canvas.width, canvas.height);
const visualizerContainerObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
resizeVisualizer(entry.contentRect.width, entry.contentRect.height);
});
});
visualizerContainerObserver.observe(visualizerContainer);
visualizer.render();
},
{ passive: true }
);
};

View File

@ -0,0 +1,23 @@
const { readdirSync } = require("fs");
const path = require("path");
const { setMenuOptions } = require("../../config/plugins");
const visualizerTypes = readdirSync(path.join(__dirname, "visualizers")).map(
(filename) => path.parse(filename).name
);
module.exports = (win, options) => [
{
label: "Type",
submenu: visualizerTypes.map((visualizerType) => ({
label: visualizerType,
type: "radio",
checked: options.type === visualizerType,
click: () => {
options.type = visualizerType;
setMenuOptions("visualizer", options);
},
})),
},
];

View File

@ -0,0 +1,46 @@
const butterchurn = require("butterchurn");
const butterchurnPresets = require("butterchurn-presets");
const presets = butterchurnPresets.getPresets();
class ButterchurnVisualizer {
constructor(
audioContext,
audioSource,
visualizerContainer,
canvas,
audioNode,
stream,
options
) {
this.visualizer = butterchurn.default.createVisualizer(
audioContext,
canvas,
{
width: canvas.width,
height: canvas.height,
}
);
const preset = presets[options.preset];
this.visualizer.loadPreset(preset, options.blendTimeInSeconds);
this.visualizer.connectAudio(audioNode);
this.renderingFrequencyInMs = options.renderingFrequencyInMs;
}
resize(width, height) {
this.visualizer.setRendererSize(width, height);
}
render() {
const renderVisualizer = () => {
requestAnimationFrame(() => renderVisualizer());
this.visualizer.render();
};
setTimeout(renderVisualizer(), this.renderingFrequencyInMs);
}
}
module.exports = ButterchurnVisualizer;

View File

@ -0,0 +1,33 @@
const Vudio = require("vudio/umd/vudio");
class VudioVisualizer {
constructor(
audioContext,
audioSource,
visualizerContainer,
canvas,
audioNode,
stream,
options
) {
this.visualizer = new Vudio(stream, canvas, {
width: canvas.width,
height: canvas.height,
// Visualizer config
...options,
});
}
resize(width, height) {
this.visualizer.setOption({
width: width,
height: height,
});
}
render() {
this.visualizer.dance();
}
}
module.exports = VudioVisualizer;

View File

@ -0,0 +1,31 @@
const { Wave } = require("@foobar404/wave");
class WaveVisualizer {
constructor(
audioContext,
audioSource,
visualizerContainer,
canvas,
audioNode,
stream,
options
) {
this.visualizer = new Wave(
{ context: audioContext, source: audioSource },
canvas
);
options.animations.forEach((animation) => {
this.visualizer.addAnimation(
eval(`new this.visualizer.animations.${animation.type}(
${JSON.stringify(animation.config)}
)`)
);
});
}
resize(width, height) {}
render() {}
}
module.exports = WaveVisualizer;