|
30 | 30 | chrome.runtime.sendMessage(chrome.runtime.id, { "getSettings": true }, function(response){ // {"state":isExtensionOn,"streamID":channel, "settings":settings} |
31 | 31 | if ("settings" in response){ |
32 | 32 | settings = response.settings; |
| 33 | + applyAudioPickerSetting(); |
33 | 34 | } |
34 | 35 | if ("state" in response){ |
35 | 36 | isExtensionOn = response.state; |
|
63 | 64 | if (typeof request === "object"){ |
64 | 65 | if ("settings" in request){ |
65 | 66 | settings = request.settings; |
66 | | - |
| 67 | + applyAudioPickerSetting(); |
67 | 68 | } |
68 | 69 | if ("state" in request){ |
69 | 70 | isExtensionOn = request.state; |
|
75 | 76 | document.getElementById("startupbutton").style.display = "none"; |
76 | 77 | } |
77 | 78 | } |
| 79 | + applyAudioPickerSetting(); |
78 | 80 | } |
79 | 81 | sendResponse(true); |
80 | 82 | return; |
|
231 | 233 | }, 2000); |
232 | 234 | } |
233 | 235 |
|
| 236 | + const AUDIO_OUTPUT_BUTTON_ID = "ssn-audio-output-picker"; |
| 237 | + const AUDIO_OUTPUT_PANEL_ID = "ssn-audio-output-panel"; |
| 238 | + const AUDIO_OUTPUT_SELECT_ID = "ssn-audio-output-select"; |
| 239 | + const AUDIO_OUTPUT_STATUS_ID = "ssn-audio-output-status"; |
| 240 | + let isPickingAudioOutput = false; |
| 241 | + let hasVisibilityListeners = false; |
| 242 | + |
| 243 | + function removeAudioOutputButton() { |
| 244 | + const existing = document.getElementById(AUDIO_OUTPUT_BUTTON_ID); |
| 245 | + if (existing) { |
| 246 | + existing.remove(); |
| 247 | + } |
| 248 | + |
| 249 | + const panel = document.getElementById(AUDIO_OUTPUT_PANEL_ID); |
| 250 | + if (panel) { |
| 251 | + panel.remove(); |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + function getActiveVideo() { |
| 256 | + return document.querySelector(".html5-main-video") || document.querySelector("video"); |
| 257 | + } |
| 258 | + |
| 259 | + function renderAudioOutputPanel(outputs, selectedId) { |
| 260 | + let panel = document.getElementById(AUDIO_OUTPUT_PANEL_ID); |
| 261 | + if (!panel) { |
| 262 | + panel = document.createElement("div"); |
| 263 | + panel.id = AUDIO_OUTPUT_PANEL_ID; |
| 264 | + panel.style = "position: fixed; right: 18px; bottom: 70px; z-index: 2147483647; background: #0f0f0f; color: #fff; border: 1px solid #3ea6ff; border-radius: 10px; padding: 10px; box-shadow: 0 6px 14px rgba(0,0,0,0.35); font-size: 13px; font-family: inherit; min-width: 240px;"; |
| 265 | + |
| 266 | + const header = document.createElement("div"); |
| 267 | + header.style = "display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px;"; |
| 268 | + const title = document.createElement("span"); |
| 269 | + title.textContent = "Audio output"; |
| 270 | + const close = document.createElement("button"); |
| 271 | + close.type = "button"; |
| 272 | + close.textContent = "✕"; |
| 273 | + close.style = "background: transparent; border: none; color: #fff; cursor: pointer; font-size: 14px; padding: 0 4px;"; |
| 274 | + close.addEventListener("click", () => panel.remove()); |
| 275 | + header.appendChild(title); |
| 276 | + header.appendChild(close); |
| 277 | + panel.appendChild(header); |
| 278 | + |
| 279 | + const select = document.createElement("select"); |
| 280 | + select.id = AUDIO_OUTPUT_SELECT_ID; |
| 281 | + select.style = "width: 100%; margin-bottom: 6px; background: #121212; color: #fff; border: 1px solid #3ea6ff; border-radius: 6px; padding: 6px;"; |
| 282 | + panel.appendChild(select); |
| 283 | + |
| 284 | + const status = document.createElement("div"); |
| 285 | + status.id = AUDIO_OUTPUT_STATUS_ID; |
| 286 | + status.style = "opacity: 0.85;"; |
| 287 | + panel.appendChild(status); |
| 288 | + |
| 289 | + (document.body || document.documentElement).appendChild(panel); |
| 290 | + } |
| 291 | + |
| 292 | + const selectEl = panel.querySelector("#" + AUDIO_OUTPUT_SELECT_ID); |
| 293 | + const statusEl = panel.querySelector("#" + AUDIO_OUTPUT_STATUS_ID); |
| 294 | + |
| 295 | + selectEl.innerHTML = ""; |
| 296 | + const defaultOpt = document.createElement("option"); |
| 297 | + defaultOpt.value = "default"; |
| 298 | + defaultOpt.textContent = "System default"; |
| 299 | + selectEl.appendChild(defaultOpt); |
| 300 | + |
| 301 | + outputs.forEach((device, idx) => { |
| 302 | + const opt = document.createElement("option"); |
| 303 | + opt.value = device.deviceId; |
| 304 | + opt.textContent = device.label || `Audio output ${idx + 1}`; |
| 305 | + selectEl.appendChild(opt); |
| 306 | + }); |
| 307 | + |
| 308 | + if (selectedId && selectEl.querySelector(`option[value="${selectedId}"]`)) { |
| 309 | + selectEl.value = selectedId; |
| 310 | + } else { |
| 311 | + selectEl.value = "default"; |
| 312 | + } |
| 313 | + |
| 314 | + statusEl.textContent = selectEl.value === "default" ? "Using system default" : `Using ${selectEl.options[selectEl.selectedIndex].textContent}`; |
| 315 | + |
| 316 | + selectEl.onchange = () => { |
| 317 | + const chosenId = selectEl.value; |
| 318 | + statusEl.textContent = "Switching..."; |
| 319 | + applyAudioOutput(chosenId, outputs).then(() => { |
| 320 | + const label = selectEl.options[selectEl.selectedIndex].textContent; |
| 321 | + statusEl.textContent = chosenId === "default" ? "Using system default" : `Using ${label}`; |
| 322 | + }).catch((err) => { |
| 323 | + console.warn("Failed to switch audio output", err); |
| 324 | + statusEl.textContent = err && err.message ? err.message : "Failed to switch output"; |
| 325 | + }); |
| 326 | + }; |
| 327 | + |
| 328 | + return { panel, selectEl, statusEl }; |
| 329 | + } |
| 330 | + |
| 331 | + async function applyAudioOutput(deviceId, outputs) { |
| 332 | + const video = getActiveVideo(); |
| 333 | + if (!video) { |
| 334 | + throw new Error("No video element found on this page."); |
| 335 | + } |
| 336 | + if (typeof video.setSinkId !== "function") { |
| 337 | + throw new Error("Audio output selection is not supported in this browser."); |
| 338 | + } |
| 339 | + await video.setSinkId(deviceId || "default"); |
| 340 | + } |
| 341 | + |
| 342 | + async function listAudioOutputsWithPermission() { |
| 343 | + if (!navigator.mediaDevices) { |
| 344 | + throw new Error("Media devices API is unavailable."); |
| 345 | + } |
| 346 | + |
| 347 | + let devices = []; |
| 348 | + try { |
| 349 | + devices = await navigator.mediaDevices.enumerateDevices(); |
| 350 | + } catch (err) { |
| 351 | + console.warn("enumerateDevices failed before permission", err); |
| 352 | + } |
| 353 | + |
| 354 | + const hasAudioOutputs = devices.some((d) => d.kind === "audiooutput" && d.deviceId); |
| 355 | + if (!hasAudioOutputs) { |
| 356 | + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| 357 | + stream.getTracks().forEach((track) => track.stop()); |
| 358 | + devices = await navigator.mediaDevices.enumerateDevices(); |
| 359 | + } |
| 360 | + |
| 361 | + return devices.filter((d) => d.kind === "audiooutput" && d.deviceId); |
| 362 | + } |
| 363 | + |
| 364 | + function isFullPlayerMode() { |
| 365 | + if (document.fullscreenElement) { |
| 366 | + return true; |
| 367 | + } |
| 368 | + const player = document.querySelector(".html5-video-player"); |
| 369 | + return !!(player && (player.classList.contains("ytp-fullscreen") || player.classList.contains("ytp-big-mode"))); |
| 370 | + } |
| 371 | + |
| 372 | + function syncAudioPickerVisibility() { |
| 373 | + const hide = isFullPlayerMode(); |
| 374 | + const button = document.getElementById(AUDIO_OUTPUT_BUTTON_ID); |
| 375 | + const panel = document.getElementById(AUDIO_OUTPUT_PANEL_ID); |
| 376 | + if (button) { |
| 377 | + button.style.display = hide ? "none" : "block"; |
| 378 | + } |
| 379 | + if (panel) { |
| 380 | + panel.style.display = hide ? "none" : ""; |
| 381 | + } |
| 382 | + } |
| 383 | + |
| 384 | + function applyAudioPickerSetting() { |
| 385 | + const onWatchPage = window.location.href.startsWith("https://www.youtube.com/watch"); |
| 386 | + if (!isExtensionOn || !settings.youtubeAudioPicker || !onWatchPage) { |
| 387 | + removeAudioOutputButton(); |
| 388 | + return; |
| 389 | + } |
| 390 | + ensureAudioOutputButton(); |
| 391 | + } |
| 392 | + |
| 393 | + async function pickAudioOutputForCurrentVideo(buttonEl) { |
| 394 | + if (isPickingAudioOutput) { |
| 395 | + return; |
| 396 | + } |
| 397 | + |
| 398 | + const video = getActiveVideo(); |
| 399 | + if (!video) { |
| 400 | + throw new Error("No video element found on this page."); |
| 401 | + } |
| 402 | + if (typeof video.setSinkId !== "function") { |
| 403 | + throw new Error("Audio output selection is not supported in this browser."); |
| 404 | + } |
| 405 | + if (!navigator.mediaDevices) { |
| 406 | + throw new Error("Media devices API is unavailable."); |
| 407 | + } |
| 408 | + |
| 409 | + isPickingAudioOutput = true; |
| 410 | + const defaultLabel = "Pick audio output"; |
| 411 | + buttonEl.disabled = true; |
| 412 | + buttonEl.textContent = "Requesting..."; |
| 413 | + |
| 414 | + try { |
| 415 | + let selectedDeviceId = null; |
| 416 | + if (navigator.mediaDevices.selectAudioOutput) { |
| 417 | + try { |
| 418 | + const selection = await navigator.mediaDevices.selectAudioOutput(); |
| 419 | + selectedDeviceId = selection && selection.deviceId; |
| 420 | + } catch (err) { |
| 421 | + console.warn("selectAudioOutput failed or was dismissed", err); |
| 422 | + } |
| 423 | + } |
| 424 | + |
| 425 | + const outputs = await listAudioOutputsWithPermission(); |
| 426 | + |
| 427 | + if (selectedDeviceId) { |
| 428 | + await applyAudioOutput(selectedDeviceId, outputs); |
| 429 | + buttonEl.textContent = selectedDeviceId === "default" ? "Using system default" : "Audio output set"; |
| 430 | + renderAudioOutputPanel(outputs, selectedDeviceId); |
| 431 | + return; |
| 432 | + } |
| 433 | + |
| 434 | + if (!outputs.length) { |
| 435 | + throw new Error("No audio outputs exposed by the browser."); |
| 436 | + } |
| 437 | + |
| 438 | + const panel = renderAudioOutputPanel(outputs); |
| 439 | + // Immediately apply the current selection (default) to refresh status. |
| 440 | + await applyAudioOutput(panel.selectEl.value, outputs); |
| 441 | + panel.statusEl.textContent = panel.selectEl.value === "default" ? "Using system default" : `Using ${panel.selectEl.options[panel.selectEl.selectedIndex].textContent}`; |
| 442 | + buttonEl.textContent = "Picker ready"; |
| 443 | + } finally { |
| 444 | + isPickingAudioOutput = false; |
| 445 | + buttonEl.disabled = false; |
| 446 | + setTimeout(() => { |
| 447 | + if (buttonEl && buttonEl.isConnected) { |
| 448 | + buttonEl.textContent = defaultLabel; |
| 449 | + } |
| 450 | + }, 2000); |
| 451 | + } |
| 452 | + } |
| 453 | + |
| 454 | + function ensureAudioOutputButton() { |
| 455 | + if (document.getElementById(AUDIO_OUTPUT_BUTTON_ID)) { |
| 456 | + syncAudioPickerVisibility(); |
| 457 | + return; |
| 458 | + } |
| 459 | + |
| 460 | + const button = document.createElement("button"); |
| 461 | + button.id = AUDIO_OUTPUT_BUTTON_ID; |
| 462 | + button.type = "button"; |
| 463 | + button.textContent = "Pick audio output"; |
| 464 | + button.title = "Select where this tab's audio should play"; |
| 465 | + button.style = "position: fixed; right: 18px; bottom: 18px; z-index: 2147483647; background: #0f0f0f; color: #fff; border: 1px solid #3ea6ff; border-radius: 10px; padding: 10px 12px; box-shadow: 0 6px 14px rgba(0,0,0,0.35); cursor: pointer; font-size: 13px; font-family: inherit;"; |
| 466 | + button.addEventListener("click", function (event) { |
| 467 | + event.preventDefault(); |
| 468 | + pickAudioOutputForCurrentVideo(button).catch((err) => { |
| 469 | + console.warn("Audio output selection failed", err); |
| 470 | + button.textContent = err && err.message ? err.message : "Selection failed"; |
| 471 | + setTimeout(() => { |
| 472 | + if (button && button.isConnected) { |
| 473 | + button.textContent = "Pick audio output"; |
| 474 | + } |
| 475 | + }, 2500); |
| 476 | + }); |
| 477 | + }); |
| 478 | + |
| 479 | + (document.body || document.documentElement).appendChild(button); |
| 480 | + syncAudioPickerVisibility(); |
| 481 | + |
| 482 | + if (!hasVisibilityListeners) { |
| 483 | + hasVisibilityListeners = true; |
| 484 | + document.addEventListener("fullscreenchange", syncAudioPickerVisibility, false); |
| 485 | + window.addEventListener("resize", syncAudioPickerVisibility, false); |
| 486 | + } |
| 487 | + } |
| 488 | + |
234 | 489 | function preStartup(){ |
235 | 490 |
|
236 | | - if (!isExtensionOn){return;} |
| 491 | + applyAudioPickerSetting(); |
| 492 | + |
| 493 | + if (!isExtensionOn){ |
| 494 | + return; |
| 495 | + } |
237 | 496 |
|
238 | 497 | if (!window.location.href.startsWith("https://www.youtube.com/watch")){ |
239 | 498 | return; |
|
333 | 592 |
|
334 | 593 |
|
335 | 594 |
|
336 | | - |
337 | | - |
338 | | - |
339 | | - |
340 | | - |
341 | | - |
342 | | - |
343 | | - |
344 | | - |
|
0 commit comments