CHZZK Initial Highest Quality (Internal API)

치지직(chzzk) 방송 초기 화질을 1080p로 고정합니다.

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         CHZZK Initial Highest Quality (Internal API)
// @name:ko      치지직 초기화질을 최고화질로 고정하는 스크립트 (내부 API사용)
// @namespace    local/scriptcat/chzzk-initial-highest-quality-internal
// @version      0.1.9
// @description  치지직(chzzk) 방송 초기 화질을 1080p로 고정합니다.
// @author       떱_
// @match        https://chzzk.naver.com/live/*
// @match        https://chzzk.naver.com/?multiview=true
// @match        https://chzzk.naver.com/lives
// @run-at       document-start
// @grant        none
// @license      MIT
// @noframes
// ==/UserScript==

(() => {
  'use strict';

  if (window.__CHZZK_INITIAL_HIGHEST_QUALITY_INTERNAL_V18__) return;
  window.__CHZZK_INITIAL_HIGHEST_QUALITY_INTERNAL_V18__ = true;

  const NAME = 'CHZZK Initial Highest Quality (Internal API)';
  const VERSION = '0.1.9';

  const CONFIG = {
    maxWaitMs: 30000,
    pollIntervalMs: 180,
    selectionVerifyMs: 3000,
    passiveResumeWaitMs: 400,
    resumeVerifyMs: 1500,
    maxApplyAttempts: 2,
    routeCheckMs: 1000,
    targetStabilityPolls: 2,
    radioConfirmMs: 300,

    requireFreshChannelResource: true,
    freshResourceSettleMs: 100,
    acceptOpaqueStreamResource: true,
    bypassRadioOnlyPlayback: true,
    respectTrustedUserSelection: true,

    /* 마스크는 실제 화질 전환 직전에만 적용합니다. */
    visualMaskEnabled: true,
    visualMaskColor: '#000',
    visualMaskFadeMs: 160,
    stableFrameWaitMs: 1800,
    revealDelayMs: 40,
    switchMaskMaxMs: 3000,

    /* Greasy Fork 배포본 기본값. 문제 확인 시 true로 바꾸면 됩니다. */
    debug: false
  };

  const state = {
    version: VERSION,
    channelId: '',
    generation: 0,
    startedAt: 0,
    resetPerfNow: 0,
    deadlineAt: 0,

    freshPlaybackSeen: false,
    freshResourceAtPerf: 0,
    freshResourceKind: '',
    resourceQuality: '',
    targetResourceAtPerf: 0,

    controllerFound: false,
    controllerCandidateCount: 0,
    selectedControllerTrackCount: 0,
    targetStableKey: '',
    targetStableCount: 0,
    staleHighestIgnored: 0,

    playbackMode: 'unknown',
    radioOnlyDetected: false,
    radioCandidateSince: 0,
    radioEvidenceCount: 0,

    videoFound: false,
    playbackStarted: false,
    decodedVideoWidth: 0,
    decodedVideoHeight: 0,

    source: '',
    availableFixedQualities: [],
    target: null,
    selectedBefore: null,
    selectedAfter: null,

    applyAttempts: 0,
    done: false,
    doneReason: '',
    userSelectedManually: false,

    videoWasPlayingBeforeSwitch: false,
    videoPausedAfterSwitch: false,
    resumeAttempts: 0,
    resumeResult: '',

    visualMaskArmed: false,
    visualMaskReleased: false,
    visualMaskReason: '',
    visualMaskArmedAt: 0,
    visualMaskReleasedAt: 0,

    switchStartedAtPerf: 0,
    targetSelectedAtPerf: 0,
    targetFrameReadyAtPerf: 0,

    lastError: ''
  };

  let pollTimer = null;
  let routeTimer = null;
  let perfObserver = null;
  let maskFailOpenTimer = null;
  let applying = false;
  let polling = false;
  let pollQueued = false;
  let clickListenerInstalled = false;

  const observedVideos = new WeakSet();
  const MASK_ATTR = 'data-chzzk-initial-quality-mask';
  const MASK_STYLE_ID = 'chzzk-initial-quality-mask-style';

  function log(...args) {
    if (CONFIG.debug) console.log(`🟢 [${NAME}]`, ...args);
  }

  function warn(...args) {
    console.warn(`🟡 [${NAME}]`, ...args);
  }

  function recordError(error) {
    state.lastError = String(error && (error.stack || error.message) || error);
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function getChannelId() {
    return location.pathname.match(/^\/live\/([^/?#]+)/)?.[1] || '';
  }

  function isLivePath() {
    return /^\/live\/[^/?#]+/.test(location.pathname);
  }

  function textOf(el) {
    try {
      return String(el?.innerText ?? el?.textContent ?? '')
        .replace(/\s+/g, ' ')
        .trim();
    } catch (_) {
      return '';
    }
  }

  function schedulePoll() {
    if (pollQueued || state.done || !isLivePath()) return;
    pollQueued = true;

    queueMicrotask(() => {
      pollQueued = false;
      void poll();
    });
  }

  function ensureVisualMaskStyle() {
    if (!CONFIG.visualMaskEnabled) return;
    if (document.getElementById(MASK_STYLE_ID)) return;

    const style = document.createElement('style');
    style.id = MASK_STYLE_ID;
    style.textContent = `
      #live_player_layout,
      #live_player_layout .pzp {
        background: ${CONFIG.visualMaskColor} !important;
      }

      html[${MASK_ATTR}="pending"] #live_player_layout .pzp-pc__video {
        opacity: 0 !important;
      }

      html[${MASK_ATTR}="pending"] #live_player_layout .pzp-pc__video,
      html[${MASK_ATTR}="revealing"] #live_player_layout .pzp-pc__video {
        transition: opacity ${CONFIG.visualMaskFadeMs}ms ease-out !important;
      }

      html[${MASK_ATTR}="revealing"] #live_player_layout .pzp-pc__video {
        opacity: 1 !important;
      }
    `;

    const append = () => {
      if (style.isConnected) return true;
      const root = document.head || document.documentElement;
      if (!root) return false;
      root.appendChild(style);
      return true;
    };

    if (!append()) {
      const observer = new MutationObserver(() => {
        if (append()) observer.disconnect();
      });
      observer.observe(document, { childList: true, subtree: true });
    }
  }

  function forceClearVisualMask(reason = 'force-clear') {
    if (maskFailOpenTimer) {
      clearTimeout(maskFailOpenTimer);
      maskFailOpenTimer = null;
    }

    document.documentElement?.removeAttribute(MASK_ATTR);

    if (state.visualMaskArmed && !state.visualMaskReleased) {
      state.visualMaskReleased = true;
      state.visualMaskReason = reason;
      state.visualMaskReleasedAt = Date.now();
    }
  }

  function armVisualMask(reason, generation = state.generation) {
    if (!CONFIG.visualMaskEnabled) return;

    ensureVisualMaskStyle();
    const html = document.documentElement;
    if (!html) return;

    if (maskFailOpenTimer) clearTimeout(maskFailOpenTimer);

    html.setAttribute(MASK_ATTR, 'pending');
    state.visualMaskArmed = true;
    state.visualMaskReleased = false;
    state.visualMaskReason = reason;
    state.visualMaskArmedAt = Date.now();
    state.visualMaskReleasedAt = 0;

    maskFailOpenTimer = setTimeout(() => {
      if (
        generation === state.generation &&
        state.visualMaskArmed &&
        !state.visualMaskReleased
      ) {
        releaseVisualMask('switch-mask-timeout', true);
      }
    }, CONFIG.switchMaskMaxMs);
  }

  function releaseVisualMask(reason, immediate = false) {
    if (!CONFIG.visualMaskEnabled) return;
    if (!state.visualMaskArmed || state.visualMaskReleased) return;

    state.visualMaskReleased = true;
    state.visualMaskReason = reason;
    state.visualMaskReleasedAt = Date.now();

    if (maskFailOpenTimer) {
      clearTimeout(maskFailOpenTimer);
      maskFailOpenTimer = null;
    }

    const html = document.documentElement;
    if (!html) return;

    if (immediate || CONFIG.visualMaskFadeMs <= 0) {
      html.removeAttribute(MASK_ATTR);
      return;
    }

    html.setAttribute(MASK_ATTR, 'revealing');
    setTimeout(() => {
      if (document.documentElement?.getAttribute(MASK_ATTR) === 'revealing') {
        document.documentElement.removeAttribute(MASK_ATTR);
      }
    }, CONFIG.visualMaskFadeMs + 80);
  }

  function parseQuality(value) {
    const match = String(value || '').match(/\b(\d{3,4})p\b/i);
    if (!match) return null;
    const height = Number(match[1]);
    if (!Number.isFinite(height)) return null;
    return { label: `${height}p`, height };
  }

  function isAbrLike(value) {
    const text = String(value || '').trim().toLowerCase();
    return text === 'abr' || text.includes('자동');
  }

  function summarizeTrack(track) {
    if (!track || typeof track !== 'object') return null;

    return {
      id: track.id ?? null,
      label: track.label ?? '',
      quality: track.quality ?? '',
      width: Number(track.width ?? track.videoWidth ?? 0) || 0,
      height: Number(track.height ?? track.videoHeight ?? 0) || 0,
      videoBitRate: Number(
        track.videoBitRate ?? track.bitrate ?? track.bandwidth ?? 0
      ) || 0,
      selected: !!track.selected,
      checked: !!track.checked,
      avoidReencoding: !!track.dataset?.avoidReencoding || !!track.avoidReencoding,
      audioOnly: !!track.audioOnly
    };
  }

  function isFixedNumericTrack(item) {
    if (!item || item.id == null || item.audioOnly) return false;
    if (isAbrLike(item.id) || isAbrLike(item.label) || isAbrLike(item.quality)) {
      return false;
    }

    const parsed = parseQuality(item.quality) || parseQuality(item.label);
    if (!parsed && !item.height) return false;

    return !/audio|audioonly|radio/i.test(
      `${item.id || ''} ${item.label || ''} ${item.quality || ''}`
    );
  }

  function getCandidateScore(item) {
    const parsed = parseQuality(item.quality) || parseQuality(item.label);
    const height = Number(item.height || parsed?.height || 0);
    const width = Number(item.width || 0);
    const bitrate = Number(item.videoBitRate || 0);

    return (
      height * 1_000_000_000 +
      width * 1_000_000 +
      bitrate +
      (item.avoidReencoding ? 100_000 : 0)
    );
  }

  function findQualityControllers() {
    const controllers = [];
    const seen = new WeakSet();

    const add = (vm) => {
      if (!vm || typeof vm !== 'object' || seen.has(vm)) return;
      if (vm.$el instanceof Element && !vm.$el.isConnected) return;

      if (
        typeof vm.getVideoTracksList === 'function' ||
        typeof vm.formattedVideoTracks === 'function' ||
        typeof vm.selectVideoTrack === 'function'
      ) {
        seen.add(vm);
        controllers.push(vm);
      }
    };

    for (const el of document.querySelectorAll(
      '#live_player_layout .pzp-setting-quality-pane,' +
      '#live_player_layout li.pzp-ui-setting-quality-item,' +
      '#live_player_layout .pzp-setting-intro-quality'
    )) {
      let vm = el.__vue__;
      for (let depth = 0; vm && depth < 8; depth++, vm = vm.$parent) add(vm);
    }

    const rootVm = document.querySelector('#live_player_layout .pzp')?.__vue__;
    if (rootVm) {
      const queue = [rootVm];
      const traversed = new WeakSet();
      let visited = 0;

      while (queue.length && visited < 120) {
        const vm = queue.shift();
        if (!vm || typeof vm !== 'object' || traversed.has(vm)) continue;
        traversed.add(vm);
        visited++;
        add(vm);
        for (const child of vm.$children || []) queue.push(child);
      }
    }

    return controllers;
  }

  function getTrackModels(controller) {
    const rawMap = new Map();
    const rows = [];

    try {
      const raw = controller?.getVideoTracksList?.();
      if (Array.isArray(raw)) {
        for (const track of raw) {
          const summary = summarizeTrack(track);
          if (summary?.id != null) rawMap.set(String(summary.id), summary);
        }
      }
    } catch (error) {
      recordError(error);
    }

    try {
      const formatted = controller?.formattedVideoTracks?.();
      if (Array.isArray(formatted)) {
        for (const item of formatted) {
          const id = item?.id ?? null;
          const raw = id != null ? rawMap.get(String(id)) : null;

          rows.push({
            ...(raw || {}),
            id,
            label: item?.quality ?? raw?.label ?? '',
            quality: item?.quality ?? raw?.quality ?? '',
            width: Number(raw?.width ?? item?.width ?? 0) || 0,
            height: Number(raw?.height ?? item?.height ?? 0) || 0,
            videoBitRate: Number(raw?.videoBitRate ?? item?.videoBitRate ?? 0) || 0,
            selected: !!(item?.selected || raw?.selected),
            checked: !!(item?.checked || raw?.checked),
            avoidReencoding: !!(
              raw?.avoidReencoding ||
              item?.avoidReencoding ||
              /\(원본\)|passthrough/i.test(String(item?.passthrough || ''))
            ),
            source: 'formattedVideoTracks'
          });
        }
      }
    } catch (error) {
      recordError(error);
    }

    if (!rows.length) {
      for (const raw of rawMap.values()) {
        rows.push({ ...raw, source: 'getVideoTracksList' });
      }
    }

    return rows;
  }

  function analyzeQualityControllers() {
    const candidates = findQualityControllers()
      .map((controller) => {
        const rows = getTrackModels(controller);
        const fixedRows = rows.filter(isFixedNumericTrack);
        const maxFixedHeight = fixedRows.reduce((max, item) => {
          const parsed = parseQuality(item.quality) || parseQuality(item.label);
          return Math.max(max, Number(item.height || parsed?.height || 0));
        }, 0);
        const abrRows = rows.filter((item) => (
          isAbrLike(item.id) || isAbrLike(item.label) || isAbrLike(item.quality)
        ));
        const elementText = textOf(controller?.$el);
        const score =
          fixedRows.length * 1_000_000_000 +
          maxFixedHeight * 1_000_000 +
          rows.length * 10_000 +
          (/1080p|720p|480p|360p/i.test(elementText) ? 1_000 : 0);

        return { controller, rows, fixedRows, abrRows, maxFixedHeight, score };
      })
      .sort((a, b) => b.score - a.score);

    const withFixed = candidates.find((item) => (
      item.fixedRows.length > 0 &&
      typeof item.controller.selectVideoTrack === 'function'
    )) || null;

    state.controllerCandidateCount = candidates.length;
    state.selectedControllerTrackCount = withFixed?.rows.length || 0;

    return {
      candidates,
      withFixed,
      totalFixedTracks: candidates.reduce((sum, item) => sum + item.fixedRows.length, 0)
    };
  }

  function chooseHighestFixedTrack(rows) {
    const fixed = rows
      .filter(isFixedNumericTrack)
      .map((item) => ({
        ...item,
        parsed: parseQuality(item.quality) || parseQuality(item.label)
      }));

    state.availableFixedQualities = fixed
      .map((item) => ({
        id: item.id,
        label: item.parsed?.label || item.quality || item.label,
        width: item.width,
        height: item.height || item.parsed?.height || 0,
        selected: item.selected || item.checked,
        source: item.source
      }))
      .sort((a, b) => b.height - a.height || b.width - a.width);

    return fixed.sort((a, b) => getCandidateScore(b) - getCandidateScore(a))[0] || null;
  }

  function getSelectedTrack(rows) {
    return rows.find((item) => item.selected || item.checked) || null;
  }

  function summarizeSelected(track) {
    if (!track) return null;
    return {
      id: track.id,
      label:
        parseQuality(track.quality)?.label ||
        parseQuality(track.label)?.label ||
        track.quality ||
        track.label
    };
  }

  function selectedMatchesTarget(controller, target) {
    const selected = getSelectedTrack(getTrackModels(controller));
    state.selectedAfter = summarizeSelected(selected);
    return !!(selected && String(selected.id) === String(target.id));
  }

  function updateTargetStability(controller, target, rows) {
    const key = [
      state.channelId,
      target.id,
      ...rows.map((item) => item.id).sort()
    ].join('|');

    if (key === state.targetStableKey) {
      state.targetStableCount++;
    } else {
      state.targetStableKey = key;
      state.targetStableCount = 1;
    }

    return state.targetStableCount >= CONFIG.targetStabilityPolls;
  }

  function findVideo() {
    return document.querySelector(
      '#live_player_layout video.webplayer-internal-video,' +
      '#live_player_layout video'
    );
  }

  function hasPlaybackStarted(video) {
    return !!(
      video instanceof HTMLVideoElement &&
      !video.paused &&
      !video.ended &&
      video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA
    );
  }

  function attachVideoEvents(video) {
    if (!(video instanceof HTMLVideoElement) || observedVideos.has(video)) return;
    observedVideos.add(video);

    for (const type of ['loadedmetadata', 'loadeddata', 'canplay', 'playing', 'pause', 'resize']) {
      video.addEventListener(type, schedulePoll, { passive: true });
    }
  }

  function getTargetLabel(target) {
    return (
      target?.parsed?.label ||
      parseQuality(target?.quality)?.label ||
      parseQuality(target?.label)?.label ||
      ''
    );
  }

  function targetFrameEvidence(controller, target, video) {
    if (!(video instanceof HTMLVideoElement)) return false;

    const targetLabel = getTargetLabel(target);
    const targetHeight = Number(target?.height || target?.parsed?.height || 0);
    const selected = selectedMatchesTarget(controller, target);
    const numericResource = !!(
      targetLabel &&
      state.resourceQuality.toLowerCase() === targetLabel.toLowerCase()
    );
    const decodedHeight = Number(video.videoHeight || 0);
    const decodedTarget = !!(
      targetHeight > 0 &&
      decodedHeight >= Math.max(1, targetHeight - 8)
    );
    const switchedLongEnough = !!(
      state.switchStartedAtPerf &&
      performance.now() - state.switchStartedAtPerf >= 250
    );

    return !!(
      hasPlaybackStarted(video) &&
      video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA &&
      (
        numericResource ||
        (decodedTarget && switchedLongEnough && (
          state.freshResourceKind !== 'opaque-stream' || selected
        ))
      )
    );
  }

  async function waitForCondition(test, timeoutMs, intervalMs = 80, generation = state.generation) {
    const started = Date.now();

    while (Date.now() - started < timeoutMs) {
      if (generation !== state.generation) return false;
      try {
        if (test()) return true;
      } catch (_) {}
      await sleep(intervalMs);
    }

    return false;
  }

  async function waitForStablePlayback(video, stableMs, timeoutMs, generation) {
    const started = Date.now();
    let stableSince = 0;

    while (Date.now() - started < timeoutMs) {
      if (generation !== state.generation) return false;

      if (hasPlaybackStarted(video)) {
        if (!stableSince) stableSince = Date.now();
        if (Date.now() - stableSince >= stableMs) return true;
      } else {
        stableSince = 0;
      }

      await sleep(50);
    }

    return false;
  }

  async function waitForOneVideoFrame(video, generation) {
    if (generation !== state.generation) return;

    if (typeof video.requestVideoFrameCallback !== 'function') {
      await sleep(80);
      return;
    }

    await Promise.race([
      new Promise((resolve) => video.requestVideoFrameCallback(() => resolve())),
      sleep(350)
    ]);
  }

  async function revealWhenTargetFrameReady(controller, target, video, generation) {
    if (!state.visualMaskArmed || state.visualMaskReleased) return false;

    const ready = await waitForCondition(
      () => targetFrameEvidence(controller, target, video),
      CONFIG.stableFrameWaitMs,
      50,
      generation
    );

    if (generation !== state.generation) return false;

    if (ready) {
      await waitForOneVideoFrame(video, generation);
      if (CONFIG.revealDelayMs > 0) await sleep(CONFIG.revealDelayMs);
      state.targetFrameReadyAtPerf = performance.now();
      releaseVisualMask('target-frame-ready');
      return true;
    }

    releaseVisualMask('target-frame-wait-timeout');
    return false;
  }

  function looksLikeRadioOnlyPlayer(analysis) {
    const playerText = textOf(document.querySelector('#live_player_layout .pzp'));
    const explicitRadioUi =
      /라디오\s*모드로\s*재생\s*중|영상도\s*함께\s*보려면|멤버십을\s*시작|치트키\s*이용자도\s*시청\s*가능/i
        .test(playerText);

    return !!(
      CONFIG.bypassRadioOnlyPlayback &&
      state.freshPlaybackSeen &&
      state.playbackStarted &&
      analysis.totalFixedTracks === 0 &&
      (explicitRadioUi || state.freshResourceKind === 'audio-only')
    );
  }

  function confirmRadioOnly(evidence) {
    if (!evidence) {
      state.radioCandidateSince = 0;
      state.radioEvidenceCount = 0;
      return false;
    }

    state.radioEvidenceCount++;
    if (!state.radioCandidateSince) state.radioCandidateSince = Date.now();

    return !!(
      state.radioEvidenceCount >= 2 &&
      Date.now() - state.radioCandidateSince >= CONFIG.radioConfirmMs
    );
  }

  function finish(reason) {
    if (state.done) return;

    if (state.visualMaskArmed && !state.visualMaskReleased) {
      releaseVisualMask(`finish:${reason}`, !reason.startsWith('highest-fixed'));
    }

    state.done = true;
    state.doneReason = reason;

    if (pollTimer) {
      clearInterval(pollTimer);
      pollTimer = null;
    }

    try {
      perfObserver?.disconnect();
    } catch (_) {}
    perfObserver = null;

    log('완료:', reason, state.target || '');
  }

  async function restorePlaybackIfNeeded(controller, video, generation) {
    const naturallyStable = await waitForStablePlayback(
      video,
      160,
      CONFIG.passiveResumeWaitMs,
      generation
    );

    if (naturallyStable) {
      state.resumeResult = 'continued-or-resumed-naturally';
      return true;
    }

    state.videoPausedAfterSwitch = !!video.paused;

    if (!state.videoWasPlayingBeforeSwitch) {
      state.resumeResult = 'not-restored-because-playback-had-not-started';
      return false;
    }

    state.resumeAttempts++;

    try {
      await Promise.resolve(controller?.$store?.dispatch?.('play'));
    } catch (error) {
      recordError(error);
    }

    let resumed = await waitForStablePlayback(video, 140, 650, generation);

    if (!resumed) {
      state.resumeAttempts++;
      try {
        await video.play();
      } catch (error) {
        recordError(error);
      }
      resumed = await waitForStablePlayback(
        video,
        140,
        CONFIG.resumeVerifyMs,
        generation
      );
    }

    state.resumeResult = resumed ? 'restored-after-track-switch' : 'resume-failed';
    return resumed;
  }

  async function verifySelection(controller, target, video, generation) {
    const targetLabel = getTargetLabel(target);

    return waitForCondition(() => {
      if (selectedMatchesTarget(controller, target)) {
        state.targetSelectedAtPerf ||= performance.now();
        return true;
      }

      if (
        targetLabel &&
        state.resourceQuality.toLowerCase() === targetLabel.toLowerCase()
      ) {
        state.targetResourceAtPerf ||= performance.now();
        return true;
      }

      return targetFrameEvidence(controller, target, video);
    }, CONFIG.selectionVerifyMs, 100, generation);
  }

  async function applyHighestTrack(controller, target, video, generation) {
    if (
      applying ||
      state.done ||
      state.userSelectedManually ||
      generation !== state.generation
    ) {
      return;
    }

    applying = true;
    state.applyAttempts++;

    try {
      const rows = getTrackModels(controller);
      state.selectedBefore = summarizeSelected(getSelectedTrack(rows));
      state.videoWasPlayingBeforeSwitch = hasPlaybackStarted(video);
      state.switchStartedAtPerf = performance.now();

      armVisualMask('quality-switch', generation);

      log(
        `재생 시작 후 내부 API 선택 시도 ${state.applyAttempts}/${CONFIG.maxApplyAttempts}:`,
        getTargetLabel(target) || String(target.id),
        target.id
      );

      await Promise.resolve(controller.selectVideoTrack(target.id));
      if (generation !== state.generation) return;

      state.source = 'controller.selectVideoTrack';

      const selected = await verifySelection(controller, target, video, generation);
      if (generation !== state.generation) return;

      if (!selected) {
        releaseVisualMask('selection-not-confirmed', true);

        if (state.applyAttempts >= CONFIG.maxApplyAttempts) {
          finish('selection-verification-failed');
        }
        return;
      }

      const playing = await restorePlaybackIfNeeded(controller, video, generation);
      if (generation !== state.generation) return;

      const frameReady = await revealWhenTargetFrameReady(
        controller,
        target,
        video,
        generation
      );
      if (generation !== state.generation) return;

      if (!playing) {
        finish('highest-fixed-selected-but-playback-paused');
      } else if (state.resumeResult === 'restored-after-track-switch') {
        finish(frameReady
          ? 'highest-fixed-selected-playback-restored'
          : 'highest-fixed-selected-playback-restored-frame-unconfirmed');
      } else {
        finish(frameReady
          ? 'highest-fixed-selected-playing'
          : 'highest-fixed-selected-playing-frame-unconfirmed');
      }
    } catch (error) {
      if (generation !== state.generation) return;

      recordError(error);
      releaseVisualMask('internal-api-error', true);
      warn('내부 품질 API 호출 실패:', error);

      if (state.applyAttempts >= CONFIG.maxApplyAttempts) {
        finish('internal-api-error');
      }
    } finally {
      if (generation === state.generation) applying = false;
    }
  }

  async function poll() {
    if (polling || applying || state.done || state.userSelectedManually) return;
    polling = true;
    const generation = state.generation;

    try {
      if (!isLivePath()) {
        finish('left-live-page');
        return;
      }

      if (Date.now() > state.deadlineAt) {
        finish('controller-or-playback-timeout');
        return;
      }

      const video = findVideo();
      if (video) {
        attachVideoEvents(video);
        state.videoFound = true;
        state.playbackStarted = hasPlaybackStarted(video);
        state.decodedVideoWidth = Number(video.videoWidth || 0);
        state.decodedVideoHeight = Number(video.videoHeight || 0);
      }

      const analysis = analyzeQualityControllers();
      state.controllerFound = analysis.candidates.length > 0;

      if (confirmRadioOnly(looksLikeRadioOnlyPlayer(analysis))) {
        state.radioOnlyDetected = true;
        state.playbackMode = 'radio-only';
        forceClearVisualMask('radio-only-pass-through');
        finish('radio-only-pass-through');
        return;
      }

      const candidate = analysis.withFixed;
      if (!candidate || !video) return;

      const controller = candidate.controller;
      const rows = candidate.rows;
      const target = chooseHighestFixedTrack(rows);
      if (!target) return;

      state.playbackMode = 'video';
      state.target = {
        id: target.id,
        label: getTargetLabel(target) || target.quality || target.label,
        width: target.width,
        height: target.height || target.parsed?.height || 0,
        source: target.source
      };

      if (!updateTargetStability(controller, target, rows)) return;

      if (CONFIG.requireFreshChannelResource && !state.freshPlaybackSeen) return;
      if (
        state.freshResourceAtPerf &&
        performance.now() - state.freshResourceAtPerf < CONFIG.freshResourceSettleMs
      ) {
        return;
      }
      if (!state.playbackStarted) return;

      const selected = getSelectedTrack(rows);
      const selectedIsTarget = !!(
        selected && String(selected.id) === String(target.id)
      );
      const targetLabel = getTargetLabel(target);
      const numericResourceIsTarget = !!(
        targetLabel &&
        state.resourceQuality.toLowerCase() === targetLabel.toLowerCase()
      );
      if (numericResourceIsTarget) {
        state.selectedBefore = summarizeSelected(selected);
        state.selectedAfter = summarizeSelected(selected);
        state.targetSelectedAtPerf ||= performance.now();
        state.targetFrameReadyAtPerf ||= performance.now();
        finish('already-highest-fixed');
        return;
      }

      if (selectedIsTarget) {
        state.staleHighestIgnored++;
        log(
          '선택값은 최고화질이지만 실제 프레임이 확인되지 않아 내부 API를 재적용:',
          state.resourceQuality || state.freshResourceKind || 'unknown'
        );
      }

      if (state.applyAttempts < CONFIG.maxApplyAttempts) {
        void applyHighestTrack(controller, target, video, generation);
      }
    } finally {
      polling = false;
    }
  }

  function installResourceObserver() {
    if (typeof PerformanceObserver === 'undefined') return;

    const generation = state.generation;
    const resetPerfNow = state.resetPerfNow;

    try {
      perfObserver = new PerformanceObserver((list) => {
        if (generation !== state.generation) return;

        for (const entry of list.getEntries()) {
          if (Number(entry.startTime || 0) + 1 < resetPerfNow) continue;

          const name = String(entry.name || '');
          const isStreamingResource =
            /\.(?:m3u8|m4s|m4v|ts)(?:\?|$)/i.test(name) &&
            /(?:livecloud|nlive|nvelop|navercdn|pstatic)/i.test(name);
          if (!isStreamingResource) continue;

          const numericMatch = name.match(
            /\/(2160p|1440p|1080p|720p|480p|360p|144p)\//i
          );
          const audioOnly = /audioOnly|audio_only|\/radio(?:\/|_|-)/i.test(name);

          if (numericMatch) {
            state.resourceQuality = numericMatch[1];
            state.freshResourceKind = 'numeric-video';
            if (
              state.target?.label &&
              state.target.label.toLowerCase() === numericMatch[1].toLowerCase()
            ) {
              state.targetResourceAtPerf ||= performance.now();
            }
          } else if (audioOnly) {
            state.resourceQuality = 'audioOnly';
            state.freshResourceKind = 'audio-only';
          } else if (CONFIG.acceptOpaqueStreamResource) {
            state.freshResourceKind = 'opaque-stream';
          } else {
            continue;
          }

          if (!state.freshPlaybackSeen) {
            state.freshPlaybackSeen = true;
            state.freshResourceAtPerf = performance.now();
          }
        }

        schedulePoll();
      });

      perfObserver.observe({ type: 'resource', buffered: true });
    } catch (error) {
      recordError(error);
    }
  }

  function installTrustedClickCancel() {
    if (clickListenerInstalled || !CONFIG.respectTrustedUserSelection) return;

    document.addEventListener('click', (event) => {
      if (!event.isTrusted || state.done) return;

      const target = event.target instanceof Element
        ? event.target.closest(
            '#live_player_layout li.pzp-ui-setting-quality-item,' +
            '#live_player_layout .pzp-setting-quality-pane [role="menuitem"]'
          )
        : null;
      if (!target) return;

      if (/\b\d{3,4}p\b|자동|abr/i.test(textOf(target))) {
        state.userSelectedManually = true;
        forceClearVisualMask('user-selected-quality-manually');
        finish('user-selected-quality-manually');
      }
    }, true);

    clickListenerInstalled = true;
  }

  function resetForChannel(reason) {
    if (pollTimer) {
      clearInterval(pollTimer);
      pollTimer = null;
    }
    try {
      perfObserver?.disconnect();
    } catch (_) {}
    perfObserver = null;

    forceClearVisualMask('channel-reset');
    applying = false;
    polling = false;
    pollQueued = false;

    state.generation++;
    state.channelId = getChannelId();
    state.startedAt = Date.now();
    state.resetPerfNow = performance.now();
    state.deadlineAt = state.startedAt + CONFIG.maxWaitMs;

    state.freshPlaybackSeen = false;
    state.freshResourceAtPerf = 0;
    state.freshResourceKind = '';
    state.resourceQuality = '';
    state.targetResourceAtPerf = 0;

    state.controllerFound = false;
    state.controllerCandidateCount = 0;
    state.selectedControllerTrackCount = 0;
    state.targetStableKey = '';
    state.targetStableCount = 0;
    state.staleHighestIgnored = 0;

    state.playbackMode = 'unknown';
    state.radioOnlyDetected = false;
    state.radioCandidateSince = 0;
    state.radioEvidenceCount = 0;

    state.videoFound = false;
    state.playbackStarted = false;
    state.decodedVideoWidth = 0;
    state.decodedVideoHeight = 0;

    state.source = '';
    state.availableFixedQualities = [];
    state.target = null;
    state.selectedBefore = null;
    state.selectedAfter = null;

    state.applyAttempts = 0;
    state.done = false;
    state.doneReason = '';
    state.userSelectedManually = false;

    state.videoWasPlayingBeforeSwitch = false;
    state.videoPausedAfterSwitch = false;
    state.resumeAttempts = 0;
    state.resumeResult = '';

    state.visualMaskArmed = false;
    state.visualMaskReleased = false;
    state.visualMaskReason = '';
    state.visualMaskArmedAt = 0;
    state.visualMaskReleasedAt = 0;

    state.switchStartedAtPerf = 0;
    state.targetSelectedAtPerf = 0;
    state.targetFrameReadyAtPerf = 0;
    state.lastError = '';

    installResourceObserver();
    pollTimer = setInterval(schedulePoll, CONFIG.pollIntervalMs);
    schedulePoll();

    log('채널 초기화:', reason, state.channelId, `generation=${state.generation}`);
  }

  function watchRouteChanges() {
    let lastPath = location.pathname;

    const check = (reason) => {
      if (location.pathname === lastPath) return;
      lastPath = location.pathname;

      if (isLivePath()) {
        resetForChannel('spa-route-change');
      } else if (!state.done) {
        forceClearVisualMask('left-live-page');
        finish('left-live-page');
      }
    };

    for (const methodName of ['pushState', 'replaceState']) {
      const original = history[methodName];
      if (typeof original !== 'function' || original.__chzzkQualityRouteWrapped) {
        continue;
      }

      function wrappedHistoryMethod(...args) {
        const result = original.apply(this, args);
        queueMicrotask(() => check(`history.${methodName}`));
        return result;
      }

      Object.defineProperty(wrappedHistoryMethod, '__chzzkQualityRouteWrapped', {
        value: true
      });
      history[methodName] = wrappedHistoryMethod;
    }

    window.addEventListener('popstate', () => check('popstate'));
    routeTimer = setInterval(() => check('interval'), CONFIG.routeCheckMs);
  }

  function diag() {
    const now = performance.now();

    return {
      name: NAME,
      version: VERSION,
      href: location.href,
      channelId: state.channelId,
      generation: state.generation,

      freshPlaybackSeen: state.freshPlaybackSeen,
      freshResourceAtPerf: state.freshResourceAtPerf,
      freshResourceKind: state.freshResourceKind,
      resourceQuality: state.resourceQuality,
      targetResourceAtPerf: state.targetResourceAtPerf,

      controllerFound: state.controllerFound,
      controllerCandidateCount: state.controllerCandidateCount,
      selectedControllerTrackCount: state.selectedControllerTrackCount,
      targetStableCount: state.targetStableCount,
      staleHighestIgnored: state.staleHighestIgnored,

      playbackMode: state.playbackMode,
      radioOnlyDetected: state.radioOnlyDetected,
      videoFound: state.videoFound,
      playbackStarted: state.playbackStarted,
      decodedVideoWidth: state.decodedVideoWidth,
      decodedVideoHeight: state.decodedVideoHeight,

      source: state.source,
      availableFixedQualities: state.availableFixedQualities,
      target: state.target,
      selectedBefore: state.selectedBefore,
      selectedAfter: state.selectedAfter,

      videoWasPlayingBeforeSwitch: state.videoWasPlayingBeforeSwitch,
      videoPausedAfterSwitch: state.videoPausedAfterSwitch,
      resumeAttempts: state.resumeAttempts,
      resumeResult: state.resumeResult,

      visualMaskArmed: state.visualMaskArmed,
      visualMaskReleased: state.visualMaskReleased,
      visualMaskReason: state.visualMaskReason,
      visualMaskDurationMs:
        state.visualMaskArmedAt && state.visualMaskReleasedAt
          ? state.visualMaskReleasedAt - state.visualMaskArmedAt
          : 0,

      switchStartedAtPerf: state.switchStartedAtPerf,
      targetSelectedAtPerf: state.targetSelectedAtPerf,
      targetFrameReadyAtPerf: state.targetFrameReadyAtPerf,
      switchToVisibleMs:
        state.switchStartedAtPerf && state.visualMaskReleasedAt
          ? Math.max(0, state.visualMaskReleasedAt - (performance.timeOrigin + state.switchStartedAtPerf))
          : 0,

      applyAttempts: state.applyAttempts,
      done: state.done,
      doneReason: state.doneReason,
      userSelectedManually: state.userSelectedManually,
      remainingWaitMs: Math.max(0, state.deadlineAt - Date.now()),
      elapsedPerfMs: Math.round(now),
      lastError: state.lastError
    };
  }

  function init() {
    ensureVisualMaskStyle();
    installTrustedClickCancel();
    watchRouteChanges();

    window.__CHZZK_INITIAL_QUALITY__ = {
      version: VERSION,
      config: CONFIG,
      diag,
      retry() {
        if (!isLivePath()) return false;
        resetForChannel('manual-retry');
        return true;
      },
      stop() {
        forceClearVisualMask('stopped-manually');
        finish('stopped-manually');
        return true;
      }
    };

    if (isLivePath()) resetForChannel('initial-load');
    log(`v${VERSION} loaded`);
  }

  init();
})();