Save Audio from HTML5 Video (WebM/OGG) — own/allowed content only

Добавляет кнопку рядом с <video> для записи и сохранения аудио как WebM/OGG. Только для вашего или разрешённого контента без DRM.

// ==UserScript==
// @name         Save Audio from HTML5 Video (WebM/OGG) — own/allowed content only
// @namespace    https://greasyfork.org/users/your-name
// @license MIT
// @version      1.0
// @description  Добавляет кнопку рядом с <video> для записи и сохранения аудио как WebM/OGG. Только для вашего или разрешённого контента без DRM.
// @match        *://*/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const BTN_CLASS = "gf-save-audio-btn";

  const getSupportedMime = () => {
    const types = [
      "audio/webm;codecs=opus",
      "audio/ogg;codecs=opus",
      "audio/webm",
      "audio/ogg"
    ];
    return types.find(t => typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(t)) || null;
  };

  const sanitize = (s) => s.replace(/[\\/:*?"<>|]+/g, "_").trim();

  const makeButton = (video) => {
    if (video.dataset.gfsabInit === "1") return;
    video.dataset.gfsabInit = "1";

    // Контейнер под кнопки
    const bar = document.createElement("div");
    bar.style.display = "flex";
    bar.style.gap = "8px";
    bar.style.alignItems = "center";
    bar.style.margin = "6px 0";
    bar.style.font = "14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial";

    const btn = document.createElement("button");
    btn.textContent = "Сохранить аудио";
    btn.className = BTN_CLASS;
    Object.assign(btn.style, {
      padding: "6px 10px",
      borderRadius: "8px",
      border: "1px solid #ccc",
      background: "#f7f7f7",
      cursor: "pointer"
    });

    const hint = document.createElement("span");
    hint.textContent = "Запись идёт в реальном времени; видео должно проигрываться.";
    hint.style.opacity = "0.8";

    bar.appendChild(btn);
    bar.appendChild(hint);

    // Вставляем панель сразу после видео
    if (video.parentElement) {
      video.parentElement.insertBefore(bar, video.nextSibling);
    } else {
      video.insertAdjacentElement("afterend", bar);
    }

    let recorder = null;
    let chunks = [];
    let stopping = false;

    const stopAndSave = () => {
      if (recorder && recorder.state !== "inactive" && !stopping) {
        stopping = true;
        recorder.stop();
      }
    };

    btn.addEventListener("click", async () => {
      // Если уже пишем — останавливаем
      if (recorder && recorder.state === "recording") {
        stopAndSave();
        return;
      }

      const mime = getSupportedMime();
      if (!mime) {
        alert("MediaRecorder не поддерживает аудио-форматы (WebM/OGG) в этом браузере.");
        return;
      }

      const capture = video.captureStream || video.mozCaptureStream;
      if (!capture) {
        alert("Этот браузер/страница не поддерживает captureStream для видео.");
        return;
      }

      const stream = capture.call(video);
      const audioTracks = stream.getAudioTracks();
      if (!audioTracks || audioTracks.length === 0) {
        alert("У видео не обнаружена аудио-дорожка или она недоступна (возможны ограничения CORS/DRM).");
        return;
      }

      const audioStream = new MediaStream([audioTracks[0]]);
      chunks = [];
      stopping = false;

      try {
        recorder = new MediaRecorder(audioStream, { mimeType: mime });
      } catch (e) {
        console.error(e);
        alert("Не удалось запустить MediaRecorder.");
        return;
      }

      recorder.ondataavailable = (ev) => {
        if (ev.data && ev.data.size > 0) chunks.push(ev.data);
      };

      recorder.onstop = () => {
        try {
          const blob = new Blob(chunks, { type: mime });
          const url = URL.createObjectURL(blob);
          const title = sanitize(document.title || "audio");
          const ext = mime.includes("ogg") ? "ogg" : "webm";
          const ts = new Date().toISOString().replace(/[:.]/g, "-");
          const a = document.createElement("a");
          a.href = url;
          a.download = `${title}-${ts}.${ext}`;
          document.body.appendChild(a);
          a.click();
          a.remove();
          setTimeout(() => URL.revokeObjectURL(url), 5000);
        } finally {
          btn.textContent = "Сохранить аудио";
        }
      };

      // Авто-стоп в конце
      const onEnded = () => stopAndSave();
      video.addEventListener("ended", onEnded, { once: true });

      recorder.start();
      btn.textContent = "Стоп и сохранить аудио";

      // Если видео на паузе — пробуем запустить воспроизведение
      try {
        if (video.paused) await video.play();
      } catch {
        // Браузер может требовать пользовательского взаимодействия
      }
    });
  };

  const scan = () => document.querySelectorAll("video").forEach(makeButton);
  scan();

  // Отслеживаем динамически добавленные видео
  const mo = new MutationObserver(() => scan());
  mo.observe(document.documentElement, { childList: true, subtree: true });
})();