CHZZK Initial Highest Quality (Internal API)

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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();
})();