☰

Sleazy Fork is available in English.

fastlnker Ult

Detect and play video streams (HLS, DASH, MP4, WebM) with improved performance and features + color controls (brightness, contrast, saturation, gamma)

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         fastlnker Ult
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Detect and play video streams (HLS, DASH, MP4, WebM) with improved performance and features + color controls (brightness, contrast, saturation, gamma)
// @author       tae
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        unsafeWindow
// ==/UserScript==

(function () {
  'use strict';

  /* =========================
     CONSTANTS & CONFIG
  ========================= */
  const CONFIG = {
    UI_DEBOUNCE: 500,
    UPDATE_THROTTLE: 250,
    URL_DEDUP_WINDOW: 5000,
    MIN_URL_LENGTH: 10,
    VARIANT_CACHE_SIZE: 50,
  };

  /* =========================
     UTILITIES
  ========================= */
  function debounce(func, delay) {
    let timeout;
    return function (...args) {
      const context = this;
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(context, args), delay);
    };
  }

  function throttle(func, limit) {
    let inThrottle;
    let lastResult;
    return function (...args) {
      const context = this;
      if (!inThrottle) {
        inThrottle = true;
        setTimeout(() => (inThrottle = false), limit);
        lastResult = func.apply(context, args);
      }
      return lastResult;
    };
  }

  /* =========================
     GAMMA FILTER (SVG)
     Gamma is not available as a CSS filter function, so we use an
     SVG feComponentTransfer with type="gamma" and reference it from
     the video element via filter: url(#id).
  ========================= */
  class GammaFilter {
    constructor() {
      const NS = 'http://www.w3.org/2000/svg';

      this.id = 'faststream-gamma-' + Math.random().toString(36).slice(2, 9);

      const svg = document.createElementNS(NS, 'svg');
      svg.setAttribute('width', '0');
      svg.setAttribute('height', '0');
      svg.setAttribute('aria-hidden', 'true');
      svg.setAttribute('focusable', 'false');
      svg.style.cssText =
        'position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;';

      const filter = document.createElementNS(NS, 'filter');
      filter.setAttribute('id', this.id);
      filter.setAttribute('x', '0');
      filter.setAttribute('y', '0');
      filter.setAttribute('width', '100%');
      filter.setAttribute('height', '100%');
      filter.setAttribute('color-interpolation-filters', 'sRGB');

      const transfer = document.createElementNS(NS, 'feComponentTransfer');

      this.funcs = [];
      ['R', 'G', 'B'].forEach((channel) => {
        const fn = document.createElementNS(NS, 'feFunc' + channel);
        fn.setAttribute('type', 'gamma');
        fn.setAttribute('amplitude', '1');
        fn.setAttribute('exponent', '1');
        fn.setAttribute('offset', '0');
        transfer.appendChild(fn);
        this.funcs.push(fn);
      });

      filter.appendChild(transfer);
      svg.appendChild(filter);

      this.svg = svg;
      this.filter = filter;

      // Attach to the main document so the URL fragment reference
      // resolves reliably regardless of Shadow DOM boundaries.
      document.documentElement.appendChild(svg);
    }

    setExponent(value) {
      const v = String(value);
      this.funcs.forEach((fn) => fn.setAttribute('exponent', v));
    }

    destroy() {
      if (this.svg && this.svg.parentNode) {
        this.svg.parentNode.removeChild(this.svg);
      }
    }
  }

  /* =========================
     M3U8 PARSER
  ========================= */
  class M3U8Parser {
    static async fetch(url) {
      try {
        const response = await fetch(url);
        return await response.text();
      } catch (e) {
        console.error('FastStream: Failed to fetch m3u8:', e);
        return null;
      }
    }

    static parse(content) {
      const variants = [];
      const lines = content.split('\n');

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();

        if (line.startsWith('#EXT-X-STREAM-INF')) {
          const resolution = this.extractResolution(line);
          const bandwidth = this.extractBandwidth(line);
          const nextLine = lines[i + 1]?.trim();

          if (nextLine && !nextLine.startsWith('#')) {
            variants.push({
              resolution,
              bandwidth,
              url: nextLine,
              label: this.createLabel(resolution, bandwidth),
            });
          }
        }
      }
      return variants;
    }

    static extractResolution(line) {
      const match = line.match(/RESOLUTION=(\d+)x(\d+)/);
      return match ? { width: parseInt(match[1]), height: parseInt(match[2]) } : null;
    }

    static extractBandwidth(line) {
      const match = line.match(/BANDWIDTH=(\d+)/);
      return match ? parseInt(match[1]) : null;
    }

    static createLabel(resolution, bandwidth) {
      if (resolution) {
        const height = resolution.height;
        const label = `${height}p`;
        return bandwidth ? `${label} (${(bandwidth / 1000).toFixed(0)} kbps)` : label;
      }
      return 'Unknown';
    }
  }

  /* =========================
     URL ANALYZER
  ========================= */
  class URLAnalyzer {
    static getType(url) {
      const cleanUrl = url.split('?')[0].toLowerCase();

      if (cleanUrl.includes('m3u8')) return 'HLS';
      if (cleanUrl.includes('mpd')) return 'DASH';
      if (cleanUrl.endsWith('.mp4')) return 'MP4';
      if (cleanUrl.endsWith('.webm')) return 'WebM';
      if (cleanUrl.endsWith('.ts')) return 'TS';
      if (cleanUrl.endsWith('.m3u')) return 'M3U';
      if (cleanUrl.includes('playlist')) return 'Playlist';

      return 'Stream';
    }

    static getQuality(url) {
      // Extract quality from URL patterns
      const patterns = [
        /(\d{3,4})p/i,
        /quality[=_]([^&]+)/i,
        /res[=_]([^&]+)/i,
        /bitrate[=_](\d+)/i,
      ];

      for (const pattern of patterns) {
        const match = url.match(pattern);
        if (match) return match[1];
      }
      return null;
    }

    static isDirectPlayable(url) {
      const type = this.getType(url);
      // MP4, WebM, HLS, and DASH can be played in most video players
      return ['MP4', 'WebM', 'HLS', 'DASH', 'M3U'].includes(type);
    }

    static isLikelyMasterPlaylist(url) {
      // Master playlists typically have these characteristics
      const urlLower = url.toLowerCase();
      return (
        (urlLower.includes('m3u8') || urlLower.includes('master')) &&
        (urlLower.includes('master') ||
          urlLower.includes('variant') ||
          urlLower.includes('playlist'))
      );
    }

    static isLikelySegment(url) {
      // Segments are usually .ts files or have numeric identifiers
      const urlLower = url.toLowerCase();
      return (
        urlLower.endsWith('.ts') ||
        /segment\d+/i.test(urlLower) ||
        /-\d+\.m3u8/i.test(urlLower)
      );
    }

    static getRank(type, url) {
      // Rank URLs by preference for external players
      const urlLower = url.toLowerCase();

      // Segments get lowest priority
      if (this.isLikelySegment(url)) return 10;

      switch (type) {
        case 'MP4':
        case 'WebM':
          return 1; // Direct playable files - highest priority
        case 'HLS':
        case 'DASH':
          // Master playlists ranked higher than variants
          return this.isLikelyMasterPlaylist(url) ? 2 : 3;
        case 'M3U':
          return 2;
        case 'Playlist':
          return 4;
        case 'TS':
          return 8; // Very low priority
        default:
          return 6;
      }
    }
  }

  /* =========================
     DETECTOR
  ========================= */
  class Detector {
    constructor() {
      this.videos = new Map(); // Map of video elements to their data
      this.detectedUrls = new Map(); // Map of URL -> detection data
      this.playingSources = new Set();
      this.variantCache = new Map();
      this.networkUrls = new Map(); // Track network-detected URLs separately
      this.uiUpdateDebounced = debounce(this.updateUI.bind(this), CONFIG.UI_DEBOUNCE);
      this.init();
    }

    init() {
      this.interceptNetwork();
      this.setupMutationObserver();
      this.initialScan();
    }

    interceptNetwork() {
      const origFetch = unsafeWindow.fetch;
      unsafeWindow.fetch = async (...args) => {
        try {
          const url = args[0];
          if (url && typeof url === 'string') {
            this.checkNetworkUrl(url);
          }
        } catch (e) {
          console.warn('FastStream: Error in fetch interception:', e);
        }
        return origFetch.apply(this, args);
      };

      const origOpen = unsafeWindow.XMLHttpRequest.prototype.open;
      unsafeWindow.XMLHttpRequest.prototype.open = function (method, url) {
        try {
          if (url && typeof url === 'string') {
            this.checkNetworkUrl(url);
          }
        } catch (e) {
          console.warn('FastStream: Error in XMLHttpRequest interception:', e);
        }
        return origOpen.apply(this, arguments);
      }.bind(this);
    }

    setupMutationObserver() {
      const videoObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => {
              if (node.nodeName === 'VIDEO') {
                this.attach(node);
              } else if (node.querySelectorAll) {
                node.querySelectorAll('video').forEach((video) => this.attach(video));
              }
            });
            mutation.removedNodes.forEach((node) => {
              if (node.nodeName === 'VIDEO') {
                this.detach(node);
              } else if (node.querySelectorAll) {
                node.querySelectorAll('video').forEach((video) => this.detach(video));
              }
            });
          }
        });
        this.uiUpdateDebounced();
      });

      videoObserver.observe(document.body, { childList: true, subtree: true });
    }

    initialScan() {
      document.querySelectorAll('video').forEach((v) => this.attach(v));
      this.uiUpdateDebounced();
    }

    checkNetworkUrl(url) {
      // Filter for media URLs
      if (!/m3u8|mpd|\.mp4|\.webm|\.ts|manifest|playlist|\.m3u/i.test(url)) return;

      // Skip blob URLs and data URLs
      if (url.startsWith('blob:') || url.startsWith('data:')) return;

      // Avoid duplicates
      if (
        this.networkUrls.has(url) &&
        Date.now() - this.networkUrls.get(url).timestamp < CONFIG.URL_DEDUP_WINDOW
      ) {
        return;
      }

      const type = URLAnalyzer.getType(url);
      const isPlayable = URLAnalyzer.isDirectPlayable(url);
      const rank = URLAnalyzer.getRank(type, url);

      this.networkUrls.set(url, {
        url,
        type,
        isPlaying: false,
        timestamp: Date.now(),
        fromVideo: false,
        fromNetwork: true,
        isPlayable,
        rank,
      });

      this.detectedUrls.set(url, this.networkUrls.get(url));
      this.uiUpdateDebounced();
    }

    attach(video) {
      if (this.videos.has(video)) return;

      const update = () => {
        this.extractSources(video);
        this.updatePlayingStatus(video);
        this.uiUpdateDebounced();
      };

      const throttledUpdate = throttle(update, CONFIG.UPDATE_THROTTLE);

      const eventListeners = {
        play: update,
        loadedmetadata: update,
        loadstart: update,
        playing: update,
        pause: update,
        ended: update,
        timeupdate: throttledUpdate,
        srcchange: update,
      };

      for (const event in eventListeners) {
        video.addEventListener(event, eventListeners[event], true);
      }

      this.videos.set(video, { el: video, listeners: eventListeners });

      // Override src setter
      const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'src');
      if (descriptor) {
        const originalSet = descriptor.set;
        const originalGet = descriptor.get;

        Object.defineProperty(video, 'src', {
          set(value) {
            if (originalSet) originalSet.call(this, value);
            this.dispatchEvent(new Event('srcchange'));
          },
          get() {
            return originalGet ? originalGet.call(this) : '';
          },
          configurable: true,
        });
      }

      update();
    }

    detach(video) {
      const videoData = this.videos.get(video);
      if (videoData) {
        for (const event in videoData.listeners) {
          video.removeEventListener(event, videoData.listeners[event], true);
        }
        this.videos.delete(video);

        // Remove video-specific sources
        this.detectedUrls.forEach((data, url) => {
          if (data.fromVideo && data.videoElement === video) {
            this.detectedUrls.delete(url);
          }
        });

        this.uiUpdateDebounced();
      }
    }

    extractSources(video) {
      if (video.currentSrc) {
        this.addVideoSource(video.currentSrc, video);
      } else if (video.src) {
        this.addVideoSource(video.src, video);
      }

      video.querySelectorAll('source').forEach((s) => {
        if (s.src) {
          this.addVideoSource(s.src, video);
        }
      });
    }

    addVideoSource(url, videoElement) {
      if (url.startsWith('blob:') || url.startsWith('data:')) return;

      const type = URLAnalyzer.getType(url);
      const isPlayable = URLAnalyzer.isDirectPlayable(url);
      const rank = URLAnalyzer.getRank(type, url);

      const sourceData = {
        url,
        type,
        isPlaying: false,
        timestamp: Date.now(),
        fromVideo: true,
        videoElement,
        isPlayable,
        rank,
      };

      this.detectedUrls.set(url, sourceData);
    }

    updatePlayingStatus(video) {
      const currentlyPlaying = new Set();

      if (!video.paused && video.currentTime > 0 && !video.ended) {
        if (video.currentSrc) currentlyPlaying.add(video.currentSrc);
        else if (video.src) currentlyPlaying.add(video.src);

        video.querySelectorAll('source').forEach((s) => {
          if (s.src) currentlyPlaying.add(s.src);
        });
      }

      this.detectedUrls.forEach((data, url) => {
        data.isPlaying = currentlyPlaying.has(url);
      });
    }

    getSortedUniqueSources() {
      const uniqueSources = Array.from(this.detectedUrls.values());

      // Filter low-value URLs
      const filteredSources = uniqueSources.filter(
        (s) => s.url.length > CONFIG.MIN_URL_LENGTH && !s.url.startsWith('blob:')
      );

      // Sort by rank, then playing status, then timestamp
      filteredSources.sort((a, b) => {
        if (a.rank !== b.rank) return a.rank - b.rank;
        if (a.isPlaying !== b.isPlaying) return b.isPlaying - a.isPlaying;
        return b.timestamp - a.timestamp;
      });

      // Deduplicate URLs
      const seenUrls = new Set();
      const finalSources = [];

      for (const source of filteredSources) {
        let simplifiedUrl = source.url;

        if (source.type === 'HLS' || source.type === 'DASH' || source.type === 'M3U') {
          simplifiedUrl = source.url.split('?')[0];
        }

        if (!seenUrls.has(simplifiedUrl)) {
          seenUrls.add(simplifiedUrl);
          finalSources.push(source);
        }
      }

      return finalSources;
    }

    async setQualityVariants(url) {
      if (url.includes('m3u8') && !this.variantCache.has(url)) {
        const content = await M3U8Parser.fetch(url);
        if (content) {
          const variants = M3U8Parser.parse(content);
          this.variantCache.set(url, variants);

          // Limit cache size
          if (this.variantCache.size > CONFIG.VARIANT_CACHE_SIZE) {
            const firstKey = this.variantCache.keys().next().value;
            this.variantCache.delete(firstKey);
          }

          return variants;
        }
      }
      return this.variantCache.get(url) || [];
    }

    getQualityVariants(url) {
      return this.variantCache.get(url) || [];
    }

    updateUI() {
      window.fastUI?.update();
    }
  }

  /* =========================
     UI + PLAYER
  ========================= */
  class UI {
    constructor(detector) {
      this.d = detector;
      this.hls = null;
      this.dash = null;
      this.playerExpanded = false;
      this.currentZoom = 1;
      this.shadowRoot = null;

      // ---- Color adjustment state ----
      this.color = {
        brightness: 1, // 1 = 100%
        contrast: 1, // 1 = 100%
        saturation: 1, // 1 = 100%
        gamma: 1, // 1 = neutral
      };
      this.gammaFilter = new GammaFilter();

      this.build();
    }

    build() {
      // Create a shadow DOM host to isolate UI from page styles
      this.host = document.createElement('div');
      this.host.id = 'faststream-host-' + Math.random().toString(36).substr(2, 9);
      this.host.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 2147483647;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        all: initial;
      `;

      // Attach to document root, not body (safer from ad injections)
      document.documentElement.appendChild(this.host);

      // Create shadow DOM for style isolation
      this.shadowRoot = this.host.attachShadow({ mode: 'open' });

      // Add global styles to shadow DOM
      const style = document.createElement('style');
      style.textContent = `
        * {
          box-sizing: border-box;
          margin: 0;
          padding: 0;
        }

        button {
          font-family: inherit;
        }

        #faststream-btn {
          width: 75px;
          height: 75px;
          border-radius: 50%;
          border: none;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: #fff;
          font-size: 24px;
          cursor: pointer;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
          transition: transform 0.2s, box-shadow 0.2s;
          z-index: 2147483647;
          position: relative;
        }

        #faststream-btn:hover {
          transform: scale(1.1);
          box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
        }

        #faststream-btn:active {
          transform: scale(0.95);
        }

        #faststream-panel {
          display: none;
          position: fixed;
          bottom: 85px;
          right: 20px;
          width: 550px;
          max-height: 650px;
          background: #fff;
          border-radius: 12px;
          overflow: hidden;
          box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
          flex-direction: column;
          z-index: 2147483646;
        }

        #faststream-panel.open {
          display: flex;
        }

        #faststream-player-container {
          display: none;
          background: #000;
          border-bottom: 2px solid #eee;
          position: relative;
        }

        #faststream-player-container.active {
          display: block;
        }

        #faststream-list-container {
          max-height: 400px;
          overflow-y: auto;
          flex: 1;
          background: #fafafa;
        }

        #faststream-list-container::-webkit-scrollbar {
          width: 8px;
        }

        #faststream-list-container::-webkit-scrollbar-track {
          background: #f1f1f1;
        }

        #faststream-list-container::-webkit-scrollbar-thumb {
          background: #888;
          border-radius: 4px;
        }

        #faststream-list-container::-webkit-scrollbar-thumb:hover {
          background: #555;
        }

        .faststream-source-row {
          padding: 14px;
          border-bottom: 1px solid #eee;
          display: flex;
          justify-content: space-between;
          align-items: center;
          background: #fff;
          transition: background 0.2s;
          cursor: pointer;
        }

        .faststream-source-row:hover {
          background: #f5f5f5;
        }

        .faststream-source-row.playing {
          background: #e8f4fd;
        }

        .faststream-source-row.playing:hover {
          background: #d4e9f7;
        }

        .faststream-source-info {
          flex: 1;
          min-width: 0;
        }

        .faststream-source-header {
          font-weight: 600;
          font-size: 13px;
          color: #333;
          display: flex;
          align-items: center;
          gap: 8px;
        }

        .faststream-source-number {
          color: #667eea;
          font-weight: bold;
        }

        .faststream-source-type {
          background: #667eea;
          color: #fff;
          padding: 2px 6px;
          border-radius: 3px;
          font-size: 11px;
          font-weight: bold;
        }

        .faststream-playable-badge {
          background: #28a745;
          color: #fff;
          padding: 2px 6px;
          border-radius: 3px;
          font-size: 10px;
          font-weight: bold;
        }

        .faststream-not-playable-badge {
          background: #dc3545;
          color: #fff;
          padding: 2px 6px;
          border-radius: 3px;
          font-size: 10px;
          font-weight: bold;
        }

        .faststream-source-url {
          font-size: 11px;
          color: #666;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          margin-top: 6px;
          word-break: break-all;
        }

        .faststream-source-status {
          font-size: 10px;
          color: #4CAF50;
          font-weight: bold;
          margin-top: 4px;
        }

        .faststream-source-buttons {
          display: flex;
          gap: 6px;
          margin-left: 10px;
        }

        .faststream-btn-copy, .faststream-btn-play {
          padding: 6px 12px;
          border-radius: 4px;
          border: none;
          cursor: pointer;
          font-size: 12px;
          font-weight: 500;
          transition: all 0.2s;
          white-space: nowrap;
        }

        .faststream-btn-copy {
          background: #f0f0f0;
          color: #333;
          border: 1px solid #ddd;
        }

        .faststream-btn-copy:hover {
          background: #e0e0e0;
        }

        .faststream-btn-play {
          background: #667eea;
          color: #fff;
        }

        .faststream-btn-play:hover {
          background: #5568d3;
        }

        #faststream-empty {
          padding: 30px 20px;
          text-align: center;
          color: #999;
          font-size: 14px;
        }

        .faststream-video-wrapper {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          transform-origin: center;
          transition: transform 0.2s;
          display: flex;
          align-items: center;
          justify-content: center;
        }

        #faststream-video {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          max-width: 100%;
          max-height: 100%;
        }

        .faststream-player-controls {
          position: absolute;
          bottom: 50px;
          right: 10px;
          display: flex;
          gap: 8px;
          z-index: 10;
          flex-wrap: wrap;
          align-items: center;
          justify-content: flex-end;
        }

        .faststream-quality-select {
          padding: 8px 12px;
          border-radius: 4px;
          border: 1px solid #999;
          background: #333;
          color: #fff;
          font-size: 12px;
          cursor: pointer;
          font-weight: 500;
        }

        .faststream-zoom-container {
          display: flex;
          gap: 4px;
          background: #333;
          border-radius: 4px;
          padding: 4px;
        }

        .faststream-zoom-btn {
          padding: 6px 10px;
          border: 1px solid #999;
          background: #333;
          color: #fff;
          cursor: pointer;
          font-size: 12px;
          border-radius: 3px;
          transition: all 0.2s;
        }

        .faststream-zoom-btn:hover {
          background: #444;
        }

        .faststream-zoom-btn.reset {
          min-width: 38px;
          font-weight: bold;
          font-size: 11px;
        }

        .faststream-control-btn {
          padding: 8px 12px;
          border-radius: 4px;
          border: 1px solid #999;
          background: #333;
          color: #fff;
          cursor: pointer;
          font-size: 14px;
          transition: all 0.2s;
        }

        .faststream-control-btn:hover {
          background: #444;
        }

        .faststream-control-btn.active {
          background: #667eea;
          border-color: #667eea;
        }

        #faststream-player-wrapper {
          position: relative;
          width: 100%;
          padding-bottom: 56.25%;
          overflow: hidden;
        }

        /* ===== COLOR CONTROL PANEL ===== */
        .faststream-color-panel {
          display: none;
          position: absolute;
          top: 10px;
          right: 10px;
          width: 250px;
          max-width: calc(100% - 20px);
          padding: 10px 12px 6px 12px;
          background: rgba(18, 18, 18, 0.92);
          border: 1px solid rgba(255, 255, 255, 0.15);
          border-radius: 8px;
          box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
          z-index: 20;
          color: #fff;
          font-size: 12px;
          backdrop-filter: blur(4px);
        }

        .faststream-color-panel.active {
          display: block;
        }

        .faststream-color-header {
          display: flex;
          align-items: center;
          justify-content: space-between;
          margin-bottom: 10px;
          font-size: 12px;
          font-weight: 600;
          color: #fff;
        }

        .faststream-color-reset {
          background: #444;
          color: #fff;
          border: 1px solid #666;
          border-radius: 4px;
          padding: 3px 9px;
          font-size: 11px;
          cursor: pointer;
          transition: background 0.2s;
        }

        .faststream-color-reset:hover {
          background: #666;
        }

        .faststream-color-row {
          display: grid;
          grid-template-columns: 68px 1fr 40px;
          align-items: center;
          gap: 8px;
          margin-bottom: 8px;
        }

        .faststream-color-label {
          color: #ddd;
          font-size: 11px;
          white-space: nowrap;
        }

        .faststream-color-value {
          color: #a9b6e8;
          font-size: 11px;
          text-align: right;
          font-variant-numeric: tabular-nums;
        }

        .faststream-color-slider {
          -webkit-appearance: none;
          appearance: none;
          width: 100%;
          height: 4px;
          border-radius: 2px;
          background: #555;
          outline: none;
          cursor: pointer;
        }

        .faststream-color-slider::-webkit-slider-thumb {
          -webkit-appearance: none;
          appearance: none;
          width: 14px;
          height: 14px;
          border-radius: 50%;
          background: #667eea;
          border: none;
          cursor: pointer;
        }

        .faststream-color-slider::-moz-range-thumb {
          width: 14px;
          height: 14px;
          border-radius: 50%;
          background: #667eea;
          border: none;
          cursor: pointer;
        }

        .faststream-color-slider::-moz-range-track {
          height: 4px;
          border-radius: 2px;
          background: #555;
        }
      `;
      this.shadowRoot.appendChild(style);

      // Create main container
      const container = document.createElement('div');
      container.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 10px;
        z-index: 2147483647;
      `;

      // Create button
      this.btn = document.createElement('button');
      this.btn.id = 'faststream-btn';
      this.btn.textContent = '▶';
      this.btn.title = 'FastStream Video Detector';

      this.btn.addEventListener('click', () => {
        this.panel.classList.toggle('open');
        if (this.panel.classList.contains('open')) {
          this.update();
        }
      });

      // Create panel
      this.panel = document.createElement('div');
      this.panel.id = 'faststream-panel';

      this.playerContainer = document.createElement('div');
      this.playerContainer.id = 'faststream-player-container';

      this.listContainer = document.createElement('div');
      this.listContainer.id = 'faststream-list-container';

      this.panel.appendChild(this.playerContainer);
      this.panel.appendChild(this.listContainer);

      container.appendChild(this.btn);
      container.appendChild(this.panel);

      this.shadowRoot.appendChild(container);

      this.createPlayer();
    }

    createPlayer() {
      const wrap = document.createElement('div');
      wrap.id = 'faststream-player-wrapper';

      this.videoWrapper = document.createElement('div');
      this.videoWrapper.className = 'faststream-video-wrapper';

      this.video = document.createElement('video');
      this.video.id = 'faststream-video';
      this.video.controls = true;

      this.videoWrapper.appendChild(this.video);

      const controls = document.createElement('div');
      controls.className = 'faststream-player-controls';

      // Quality selector
      this.q = document.createElement('select');
      this.q.className = 'faststream-quality-select';

      // Zoom controls
      const zoomContainer = document.createElement('div');
      zoomContainer.className = 'faststream-zoom-container';

      const zoomOut = document.createElement('button');
      zoomOut.textContent = '−';
      zoomOut.title = 'Zoom Out';
      zoomOut.className = 'faststream-zoom-btn';
      zoomOut.addEventListener('click', () => this.setZoom(this.currentZoom - 0.2));

      const zoomReset = document.createElement('button');
      zoomReset.textContent = '100%';
      zoomReset.title = 'Reset Zoom';
      zoomReset.className = 'faststream-zoom-btn reset';
      zoomReset.addEventListener('click', () => this.setZoom(1));

      const zoomIn = document.createElement('button');
      zoomIn.textContent = '+';
      zoomIn.title = 'Zoom In';
      zoomIn.className = 'faststream-zoom-btn';
      zoomIn.addEventListener('click', () => this.setZoom(this.currentZoom + 0.2));

      zoomContainer.appendChild(zoomOut);
      zoomContainer.appendChild(zoomReset);
      zoomContainer.appendChild(zoomIn);

      // Color controls toggle button
      const colorBtn = document.createElement('button');
      colorBtn.textContent = '🎨';
      colorBtn.title = 'Color Adjustments (brightness / contrast / saturation / gamma)';
      colorBtn.className = 'faststream-control-btn';
      colorBtn.addEventListener('click', () => {
        const isActive = this.colorPanel.classList.toggle('active');
        colorBtn.classList.toggle('active', isActive);
      });

      // Fullscreen button
      const fs = document.createElement('button');
      fs.textContent = '⛶';
      fs.title = 'Toggle Fullscreen';
      fs.className = 'faststream-control-btn';
      fs.addEventListener('click', () => {
        if (!document.fullscreenElement) {
          wrap.requestFullscreen().catch((err) => {
            console.warn('Fullscreen not available:', err);
          });
        } else {
          document.exitFullscreen();
        }
      });

      // Close button
      const close = document.createElement('button');
      close.textContent = '✕';
      close.title = 'Close Player';
      close.className = 'faststream-control-btn';
      close.addEventListener('click', () => {
        this.playerContainer.classList.remove('active');
        this.playerExpanded = false;
        this.video.pause();
        this.video.src = '';
        if (this.hls) {
          this.hls.destroy();
          this.hls = null;
        }
        if (this.dash) {
          this.dash.reset();
          this.dash = null;
        }
        this.currentZoom = 1;
        this.updateZoom();

        // Reset color adjustments
        this.colorPanel.classList.remove('active');
        colorBtn.classList.remove('active');
        this.resetColorSettings();
      });

      this.q.addEventListener('change', () => {
        if (this.hls && this.q.value !== 'auto') {
          this.hls.currentLevel = +this.q.value;
        } else if (this.hls) {
          this.hls.currentLevel = -1;
        }
      });

      controls.appendChild(this.q);
      controls.appendChild(zoomContainer);
      controls.appendChild(colorBtn);
      controls.appendChild(fs);
      controls.appendChild(close);

      // Build the color adjustment panel
      const colorPanel = this.createColorControls();

      wrap.appendChild(this.videoWrapper);
      wrap.appendChild(colorPanel);
      wrap.appendChild(controls);
      this.playerContainer.appendChild(wrap);
    }

    /* =========================
       COLOR CONTROLS
    ========================= */
    createColorControls() {
      const panel = document.createElement('div');
      panel.className = 'faststream-color-panel';

      const header = document.createElement('div');
      header.className = 'faststream-color-header';

      const title = document.createElement('span');
      title.textContent = 'Color Adjustments';

      const reset = document.createElement('button');
      reset.className = 'faststream-color-reset';
      reset.textContent = 'Reset';
      reset.addEventListener('click', (e) => {
        e.stopPropagation();
        this.resetColorSettings();
      });

      header.appendChild(title);
      header.appendChild(reset);
      panel.appendChild(header);

      const pct = (v) => `${Math.round(v)}%`;

      this.brightnessSlider = this.addSlider(
        panel,
        'Brightness',
        { min: 0, max: 200, step: 1, value: 100 },
        pct,
        (v) => {
          this.color.brightness = v / 100;
          this.applyVideoFilters();
        }
      );

      this.contrastSlider = this.addSlider(
        panel,
        'Contrast',
        { min: 0, max: 200, step: 1, value: 100 },
        pct,
        (v) => {
          this.color.contrast = v / 100;
          this.applyVideoFilters();
        }
      );

      this.saturationSlider = this.addSlider(
        panel,
        'Saturation',
        { min: 0, max: 200, step: 1, value: 100 },
        pct,
        (v) => {
          this.color.saturation = v / 100;
          this.applyVideoFilters();
        }
      );

      this.gammaSlider = this.addSlider(
        panel,
        'Gamma',
        { min: 0.2, max: 3, step: 0.05, value: 1 },
        (v) => v.toFixed(2),
        (v) => {
          this.color.gamma = v;
          this.applyVideoFilters();
        }
      );

      this.colorPanel = panel;
      return panel;
    }

    addSlider(parent, label, opts, format, onChange) {
      const row = document.createElement('div');
      row.className = 'faststream-color-row';

      const labelEl = document.createElement('label');
      labelEl.className = 'faststream-color-label';
      labelEl.textContent = label;

      const input = document.createElement('input');
      input.type = 'range';
      input.className = 'faststream-color-slider';
      input.min = String(opts.min);
      input.max = String(opts.max);
      input.step = String(opts.step);
      input.value = String(opts.value);

      const valueEl = document.createElement('span');
      valueEl.className = 'faststream-color-value';
      valueEl.textContent = format(opts.value);

      input.addEventListener('input', () => {
        const v = parseFloat(input.value);
        valueEl.textContent = format(v);
        onChange(v);
      });

      row.appendChild(labelEl);
      row.appendChild(input);
      row.appendChild(valueEl);
      parent.appendChild(row);

      return input;
    }

    applyVideoFilters() {
      const { brightness, contrast, saturation, gamma } = this.color;
      const parts = [];

      // Gamma via SVG filter (only when non-neutral to save performance)
      if (Math.abs(gamma - 1) > 0.001 && this.gammaFilter) {
        this.gammaFilter.setExponent(gamma);
        parts.push(`url(#${this.gammaFilter.id})`);
      }

      if (Math.abs(brightness - 1) > 0.001) parts.push(`brightness(${brightness})`);
      if (Math.abs(contrast - 1) > 0.001) parts.push(`contrast(${contrast})`);
      if (Math.abs(saturation - 1) > 0.001) parts.push(`saturate(${saturation})`);

      this.video.style.filter = parts.length ? parts.join(' ') : 'none';
    }

    resetColorSettings() {
      this.color = { brightness: 1, contrast: 1, saturation: 1, gamma: 1 };

      const fire = (slider, value) => {
        if (!slider) return;
        slider.value = String(value);
        slider.dispatchEvent(new Event('input'));
      };

      fire(this.brightnessSlider, 100);
      fire(this.contrastSlider, 100);
      fire(this.saturationSlider, 100);
      fire(this.gammaSlider, 1);

      // Ensure it is fully cleared even if a slider was missing
      this.applyVideoFilters();
    }

    setZoom(value) {
      this.currentZoom = Math.max(0.5, Math.min(3, value));
      this.updateZoom();
    }

    updateZoom() {
      this.videoWrapper.style.transform = `scale(${this.currentZoom})`;
    }

    async play(url) {
      this.playerExpanded = true;
      this.playerContainer.classList.add('active');
      this.q.innerHTML = '';

      if (this.hls) {
        this.hls.destroy();
        this.hls = null;
      }
      if (this.dash) {
        this.dash.reset();
        this.dash = null;
      }

      this.currentZoom = 1;
      this.updateZoom();

      this.video.src = '';
      this.video.load();

      // Keep color filters applied to the new source
      this.applyVideoFilters();

      if (url.includes('m3u8') && typeof Hls !== 'undefined') {
        const variants = await this.d.setQualityVariants(url);

        this.hls = new Hls();
        this.hls.loadSource(url);
        this.hls.attachMedia(this.video);

        this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
          this.q.innerHTML = '<option value="auto">Auto</option>';

          if (variants.length > 0) {
            variants.forEach((variant, index) => {
              const o = document.createElement('option');
              o.value = index;
              o.text = variant.label || 'Unknown';
              this.q.appendChild(o);
            });
          } else {
            this.hls.levels.forEach((l, i) => {
              const o = document.createElement('option');
              o.value = i;
              o.text = l.height ? `${l.height}p` : 'Unknown';
              this.q.appendChild(o);
            });
          }
          this.video.play().catch((err) => console.warn('Play failed:', err));
        });

        this.hls.on(Hls.Events.ERROR, (event, data) => {
          if (data.fatal) {
            console.error('FastStream: HLS error:', data);
            if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
              this.hls.startLoad();
            } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
              this.hls.recoverMediaError();
            } else {
              this.video.src = url;
              this.video.play().catch((err) => console.warn('Fallback play failed:', err));
            }
          }
        });
      } else if (url.includes('mpd') && typeof dashjs !== 'undefined') {
        this.dash = dashjs.MediaPlayer().create();
        this.dash.initialize(this.video, url, true);
        this.q.innerHTML = '<option>Auto</option>';
        this.video.play().catch((err) => console.warn('Play failed:', err));
      } else {
        this.video.src = url;
        this.q.innerHTML = '<option>Auto</option>';
        this.video.play().catch((err) => console.warn('Play failed:', err));
      }
    }

    copyToClipboard(text) {
      GM_setClipboard(text);
      alert('URL copied to clipboard!');
    }

    update() {
      if (!this.panel.classList.contains('open')) return;

      this.listContainer.innerHTML = '';
      const sources = this.d.getSortedUniqueSources();

      if (sources.length === 0) {
        const empty = document.createElement('div');
        empty.id = 'faststream-empty';
        empty.textContent = 'No video streams detected yet. Open a video page and it will appear here.';
        this.listContainer.appendChild(empty);
        return;
      }

      sources.forEach((s, index) => {
        const row = document.createElement('div');
        row.className = 'faststream-source-row' + (s.isPlaying ? ' playing' : '');

        const info = document.createElement('div');
        info.className = 'faststream-source-info';

        const header = document.createElement('div');
        header.className = 'faststream-source-header';

        const numSpan = document.createElement('span');
        numSpan.className = 'faststream-source-number';
        numSpan.textContent = `#${index + 1}`;

        const typeSpan = document.createElement('span');
        typeSpan.className = 'faststream-source-type';
        typeSpan.textContent = s.type;

        const badge = document.createElement('span');
        badge.className = s.isPlayable
          ? 'faststream-playable-badge'
          : 'faststream-not-playable-badge';
        badge.textContent = s.isPlayable ? '✓ External' : '⚠ Browser Only';

        header.appendChild(numSpan);
        header.appendChild(typeSpan);
        header.appendChild(badge);

        const urlDiv = document.createElement('div');
        urlDiv.className = 'faststream-source-url';
        urlDiv.textContent = s.url.substring(0, 70) + (s.url.length > 70 ? '...' : '');
        urlDiv.title = s.url;

        const statusDiv = document.createElement('div');
        if (s.isPlaying) {
          statusDiv.className = 'faststream-source-status';
          statusDiv.textContent = '● PLAYING';
        }

        info.appendChild(header);
        info.appendChild(urlDiv);
        if (s.isPlaying) info.appendChild(statusDiv);

        const buttons = document.createElement('div');
        buttons.className = 'faststream-source-buttons';

        const copy = document.createElement('button');
        copy.textContent = '📋 Copy';
        copy.className = 'faststream-btn-copy';
        copy.addEventListener('click', () => this.copyToClipboard(s.url));

        const play = document.createElement('button');
        play.textContent = '▶ Play';
        play.className = 'faststream-btn-play';
        play.addEventListener('click', () => this.play(s.url));

        buttons.appendChild(copy);
        buttons.appendChild(play);

        row.appendChild(info);
        row.appendChild(buttons);
        this.listContainer.appendChild(row);
      });
    }
  }

  /* =========================
     INIT
  ========================= */
  function init() {
    const d = new Detector();
    const ui = new UI(d);
    window.fastUI = ui;
    console.log('FastStream v2.1 initialized');
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();