media-source-extract-lite

MSE/fMP4 完整捕获、严格分片校验、YouTube 索引输出、智能跳播与内嵌精简 MP4 合并

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Advertisement:

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

Advertisement:

// ==UserScript==
// @name         media-source-extract-lite
// @namespace    local.media-source-extract-lite
// @version      2.2.1
// @description  MSE/fMP4 完整捕获、严格分片校验、YouTube 索引输出、智能跳播与内嵌精简 MP4 合并
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
  'use strict'

  if (!window.MediaSource || !window.SourceBuffer) return
  if (window.__MSE_EXTRACT_LITE_INSTALLED__) return
  window.__MSE_EXTRACT_LITE_INSTALLED__ = true

  var MOVIE_TIMESCALE = 1000
  var MAX_REMUX_BYTES = 1200 * 1024 * 1024
  var EPS = 1e-7
  var MIN_VIDEO_SAMPLES = 3
  var MIN_VIDEO_BYTES = 32 * 1024
  var MAX_AV_START_DRIFT = 30

  var nativeAddSourceBuffer = MediaSource.prototype.addSourceBuffer
  var nativeEndOfStream = MediaSource.prototype.endOfStream
  var nativeAppendBuffer = SourceBuffer.prototype.appendBuffer
  var mediaSourceSessions = new WeakMap()
  var sourceBufferTracks = new WeakMap()
  var sessions = []
  var activeSession = null
  var sessionSerial = 0
  var trackSerial = 0
  var selectedSessionId = 0
  var ui = null
  var uiRefreshPending = false
  var installedAt = Date.now()
  var addSourceBufferCalls = 0
  var appendBufferCalls = 0
  var directMediaItems = []
  var selectedDirectMediaId = ''
  var directMediaSignature = ''
  var directScanStarted = false
  var directScanDirty = true
  var directLastScanAt = 0
  var DIRECT_SCAN_MIN_INTERVAL = 250
  var DIRECT_SCAN_IDLE_INTERVAL = 2000
  var directSelectionTouched = false
  var mediaElementIds = new WeakMap()
  var mediaElementSerial = 0
  var smartJumpEnabled = false
  var smartJumpLastAt = 0
  var smartJumpCount = 0
  var smartJumpLastTarget = 0
  var SMART_JUMP_INTERVAL = 1500
  var SMART_JUMP_COOLDOWN = 2500
  var SMART_JUMP_MIN_LEAD = 12
  var SMART_JUMP_MIN_DISTANCE = 7
  var SMART_JUMP_BACKOFF = 3
  var SMART_COMPLETE_TOLERANCE = 1.5
  var SMART_JUMP_RESET_DELAY = 2200
  var SMART_JUMP_FRONTIER_REGRESSION = 3
  var smartJumpTrackedMedia = null
  var smartJumpTrackedSrc = ''
  var smartJumpTrackedDuration = 0
  var smartJumpTrackedWidth = 0
  var smartJumpTrackedHeight = 0
  var smartJumpTrackedFrontier = 0
  var smartJumpSuspendUntil = 0
  var smartJumpResetReason = ''
  var smartJumpResetAt = 0
  var smartJumpInternalSeekUntil = 0
  var smartJumpPendingConfigAt = 0
  var smartJumpPendingConfigReason = ''
  var runtimeTimer = 0
  var runtimeLastDirectScan = 0
  var runtimeLastStatus = 0
  var runtimeLastRate = 0
  var runtimeLastSmartJump = 0

  // MP4 解析器已精简内嵌,不再依赖 CDN、Trusted Types、CSP 或动态执行。

  function now() {
    return Date.now()
  }

  // 所有高频捕获事件只安排一次 UI 刷新,避免同一事件循环重复扫描 DOM 和重建面板。
  function requestUIRefresh() {
    if (uiRefreshPending) return
    uiRefreshPending = true
    var run = function () {
      uiRefreshPending = false
      refreshUI()
    }
    if (typeof requestAnimationFrame === 'function' && !document.hidden) requestAnimationFrame(run)
    else setTimeout(run, 0)
  }

  function safeNumber(value, fallback) {
    return typeof value === 'number' && isFinite(value) ? value : fallback
  }

  function safeTitle() {
    var title = document.title || 'media'
    try {
      if (window.top && window.top.document && window.top.document.title) {
        title = window.top.document.title
      }
    } catch (e) {}
    return sanitizeFilename(title || 'media')
  }

  function sanitizeFilename(name) {
    return String(name || 'media')
      .replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
      .replace(/[. ]+$/g, '')
      .slice(0, 180) || 'media'
  }

  function normalizeDirectMediaUrl(value) {
    if (typeof value !== 'string') return ''
    var url = value.trim()
      .replace(/\\u002F/gi, '/')
      .replace(/\\u0026/gi, '&')
      .replace(/\\\//g, '/')
    if (!url || /^(?:blob|data|javascript|mediastream):/i.test(url)) return ''
    try {
      url = new URL(url, location.href).href
    } catch (e) {
      return ''
    }
    return /^https?:\/\//i.test(url) ? url : ''
  }

  function isManifestMediaUrl(url) {
    if (!url) return false
    try {
      var parsed = new URL(url, location.href)
      var path = parsed.pathname.toLowerCase()
      if (/\.(?:m3u8|mpd)$/.test(path)) return true
      var type = String(parsed.searchParams.get('mime_type') || parsed.searchParams.get('type') || '').toLowerCase()
      return type.indexOf('mpegurl') !== -1 || type.indexOf('dash') !== -1
    } catch (e) {
      return /\.(?:m3u8|mpd)(?:$|[?#])/i.test(String(url))
    }
  }

  function mediaElementId(element) {
    var id = mediaElementIds.get(element)
    if (!id) {
      id = ++mediaElementSerial
      mediaElementIds.set(element, id)
    }
    return id
  }

  function mediaElementVisible(element) {
    try {
      var style = getComputedStyle(element)
      if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false
      var rect = element.getBoundingClientRect()
      return rect.width > 1 && rect.height > 1
    } catch (e) {
      return false
    }
  }

  function mediaUrlExtension(url, element) {
    var path = ''
    try { path = new URL(url).pathname } catch (e) {}
    var match = path.match(/\.([a-z0-9]{2,5})$/i)
    var ext = match ? match[1].toLowerCase() : ''
    if (/^(?:mp4|m4v|webm|mov|mkv|mp3|m4a|aac|ogg|wav)$/.test(ext)) return ext
    var type = String(element && element.getAttribute && element.getAttribute('type') || '').toLowerCase()
    if (type.indexOf('webm') !== -1) return 'webm'
    if (type.indexOf('mpeg') !== -1 && element && element.tagName === 'AUDIO') return 'mp3'
    if (element && element.tagName === 'AUDIO') return 'm4a'
    return 'mp4'
  }

  function mediaTimeLabel(seconds) {
    seconds = Number(seconds)
    if (!isFinite(seconds) || seconds <= 0) return ''
    var total = Math.round(seconds)
    var h = Math.floor(total / 3600)
    var m = Math.floor((total % 3600) / 60)
    var sec = total % 60
    return h ? h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0') : m + ':' + String(sec).padStart(2, '0')
  }

  function directMediaLabel(item) {
    var parts = []
    parts.push(item.kind === 'audio' ? '辅助音频 ' + item.elementId : '视频 ' + item.elementId)
    if (item.width && item.height) parts.push(item.width + '×' + item.height)
    var duration = mediaTimeLabel(item.duration)
    if (duration) parts.push(duration)
    if (item.playing) parts.push('播放中')
    else if (item.ended) parts.push('已结束')
    else parts.push('已暂停')
    parts.push('直连 ' + item.extension.toUpperCase())
    return parts.join(' · ')
  }

  function scanDirectMediaElements(force) {
    if (!document.querySelectorAll) return false
    var scanNow = now()
    if (!force && scanNow - directLastScanAt < DIRECT_SCAN_MIN_INTERVAL) return false
    if (!force && !directScanDirty && scanNow - directLastScanAt < DIRECT_SCAN_IDLE_INTERVAL) return false
    directLastScanAt = scanNow
    directScanDirty = false
    var nodes
    try { nodes = document.querySelectorAll('video, audio') } catch (e) { return false }
    var items = []
    var byUrl = Object.create(null)

    for (var i = 0; i < nodes.length; i++) {
      var element = nodes[i]
      var raw = ''
      try { raw = element.currentSrc || element.src || element.getAttribute('src') || '' } catch (e2) {}
      var url = normalizeDirectMediaUrl(raw)
      if (!url || isManifestMediaUrl(url)) continue

      var id = mediaElementId(element)
      var audioDecodedBytes = safeNumber(element.webkitAudioDecodedByteCount, 0)
      var videoDecodedBytes = safeNumber(element.webkitVideoDecodedByteCount, 0)
      var mediaKind = element.tagName === 'AUDIO' ? 'audio' : 'video'
      // 某些播放器使用隐藏 VIDEO 元素承载纯音频;按实际解码信息修正类型。
      if (mediaKind === 'video' &&
          Number(element.videoWidth || 0) === 0 &&
          Number(element.videoHeight || 0) === 0 &&
          videoDecodedBytes === 0 && audioDecodedBytes > 0) {
        mediaKind = 'audio'
      }
      var visible = mediaElementVisible(element)
      var item = {
        id: 'direct:' + id + ':' + url,
        elementId: id,
        element: element,
        url: url,
        kind: mediaKind,
        width: Number(element.videoWidth || 0),
        height: Number(element.videoHeight || 0),
        duration: Number(element.duration || 0),
        playing: !element.paused && !element.ended && Number(element.readyState || 0) > 1,
        ended: !!element.ended,
        visible: visible,
        audioDecodedBytes: audioDecodedBytes,
        videoDecodedBytes: videoDecodedBytes,
        primaryVideo: element.tagName === 'VIDEO' && (
          Number(element.videoWidth || 0) > 0 ||
          Number(element.videoHeight || 0) > 0 ||
          videoDecodedBytes > 0 ||
          visible
        ),
        extension: mediaUrlExtension(url, element),
        title: safeTitle()
      }
      item.label = directMediaLabel(item)

      var old = byUrl[url]
      if (!old ||
          (item.playing ? 8 : 0) + (item.visible ? 4 : 0) + item.width * item.height / 1000000 >
          (old.playing ? 8 : 0) + (old.visible ? 4 : 0) + old.width * old.height / 1000000) {
        byUrl[url] = item
      }
    }

    Object.keys(byUrl).forEach(function (url) { items.push(byUrl[url]) })
    items.sort(function (a, b) {
      if (a.playing !== b.playing) return a.playing ? -1 : 1
      if (a.visible !== b.visible) return a.visible ? -1 : 1
      if (a.kind !== b.kind) return a.kind === 'video' ? -1 : 1
      var areaA = a.width * a.height
      var areaB = b.width * b.height
      if (areaA !== areaB) return areaB - areaA
      return a.elementId - b.elementId
    })

    var signature = items.map(function (item) {
      return item.id + '|' + item.label
    }).join('\n')
    if (signature === directMediaSignature) return false

    directMediaSignature = signature
    directMediaItems = items

    var selectedExists = directMediaItems.some(function (item) { return item.id === selectedDirectMediaId })
    if (!selectedExists) selectedDirectMediaId = ''

    // 自动模式只选择真正的视频元素,绝不把独立音轨当成完整视频。
    // 一旦 MSE 已经收到媒体数据,自动直连选择立即让位给 MSE。
    if (!directSelectionTouched) {
      var mseSession = targetSession()
      if (sessionHasMediaData(mseSession)) {
        selectedDirectMediaId = ''
      } else {
        var selectedAuto = null
        for (var ai = 0; ai < directMediaItems.length; ai++) {
          if (directMediaItems[ai].kind === 'video' && directMediaItems[ai].primaryVideo) {
            selectedAuto = directMediaItems[ai]
            break
          }
        }
        selectedDirectMediaId = selectedAuto ? selectedAuto.id : ''
      }
    }
    return true
  }

  function selectedDirectMedia() {
    scanDirectMediaElements()
    for (var i = 0; i < directMediaItems.length; i++) {
      if (directMediaItems[i].id === selectedDirectMediaId) return directMediaItems[i]
    }
    return null
  }

  function sessionHasMediaData(session) {
    return !!(session && session.totalBytes > 0 && sessionOperationCount(session) > 0)
  }

  function effectiveDirectMedia() {
    var direct = selectedDirectMedia()
    if (!direct) return null
    if (directSelectionTouched) return direct
    if (direct.kind === 'audio') return null
    if (sessionHasMediaData(targetSession())) return null
    return direct
  }

  function markDirectMediaDirty() {
    directScanDirty = true
    requestUIRefresh()
  }

  function scheduleDirectScan() {
    if (directScanStarted) return
    directScanStarted = true

    function scanAndRefresh() {
      if (scanDirectMediaElements(true)) requestUIRefresh()
    }

    ;[0, 100, 300, 800, 1500, 3000].forEach(function (delay) {
      setTimeout(scanAndRefresh, delay)
    })
    document.addEventListener('play', markDirectMediaDirty, true)
    document.addEventListener('pause', markDirectMediaDirty, true)
    document.addEventListener('loadedmetadata', markDirectMediaDirty, true)
    document.addEventListener('durationchange', markDirectMediaDirty, true)
    document.addEventListener('emptied', markDirectMediaDirty, true)
    document.addEventListener('DOMContentLoaded', markDirectMediaDirty, { once: true })
  }

  function cloneBufferSource(source) {
    if (source instanceof ArrayBuffer) {
      return source.slice(0)
    }
    if (ArrayBuffer.isView(source)) {
      return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength)
    }
    throw new TypeError('appendBuffer 参数不是 ArrayBuffer 或 ArrayBufferView')
  }

  function snapshotRanges(ranges) {
    var result = []
    try {
      for (var i = 0; i < ranges.length; i++) {
        result.push([ranges.start(i), ranges.end(i)])
      }
    } catch (e) {}
    return result
  }

  function getSession(mediaSource) {
    var session = mediaSourceSessions.get(mediaSource)
    if (session) return session

    session = {
      id: ++sessionSerial,
      mediaSource: mediaSource,
      title: safeTitle(),
      createdAt: now(),
      endedAt: 0,
      sequence: 0,
      tracks: [],
      totalBytes: 0,
      warnings: []
    }
    mediaSourceSessions.set(mediaSource, session)
    sessions.push(session)
    activeSession = session
    requestUIRefresh()
    return session
  }

  function getTrackKind(mime) {
    mime = String(mime || '').toLowerCase()
    var hasAudioCodec = /(?:mp4a|opus|vorbis|ac-3|ec-3|flac)/.test(mime)
    var hasVideoCodec = /(?:avc1|avc3|hvc1|hev1|vp09|vp9|av01)/.test(mime)
    if (mime.indexOf('audio/') === 0) return 'audio'
    // 少数播放器会把纯音频 fMP4 声明为 video/mp4,需根据 codecs 修正。
    if (hasAudioCodec && !hasVideoCodec) return 'audio'
    if (mime.indexOf('video/') === 0 || hasVideoCodec) return 'video'
    return 'unknown'
  }

  function isMp4Mime(mime) {
    return /\b(?:video|audio)\/mp4\b/i.test(String(mime || ''))
  }

  function isWebMMime(mime) {
    return /\b(?:video|audio)\/webm\b/i.test(String(mime || ''))
  }

  function addWarning(session, text) {
    if (session.warnings.indexOf(text) === -1) session.warnings.push(text)
  }

  function createCaptureTrack(session, sourceBuffer, mime) {
    var track = {
      id: ++trackSerial,
      session: session,
      sourceBuffer: sourceBuffer,
      initialMime: String(mime || ''),
      currentMime: String(mime || ''),
      kindHint: getTrackKind(mime),
      operations: [],
      events: [],
      totalBytes: 0,
      lastTimestampOffset: null,
      initSegmentCount: 0,
      pendingOperations: [],
      appendCallNo: 0,
      failedCallCount: 0,
      createdAt: now()
    }
    session.tracks.push(track)
    return track
  }

  function bufferSourceMetadata(source) {
    if (source instanceof ArrayBuffer) {
      return {
        constructorName: 'ArrayBuffer',
        byteOffset: 0,
        byteLength: source.byteLength,
        bufferByteLength: source.byteLength
      }
    }
    if (ArrayBuffer.isView(source)) {
      return {
        constructorName: source && source.constructor && source.constructor.name
          ? String(source.constructor.name)
          : 'ArrayBufferView',
        byteOffset: source.byteOffset,
        byteLength: source.byteLength,
        bufferByteLength: source.buffer.byteLength
      }
    }
    return {
      constructorName: typeof source,
      byteOffset: 0,
      byteLength: 0,
      bufferByteLength: 0
    }
  }

  function makeCaptureIntegrityError(message) {
    var error = new Error(message)
    error.captureIntegrity = true
    return error
  }

  function isMp4InitSegment(buffer) {
    var bytes
    try { bytes = new Uint8Array(buffer) } catch (e) { return false }
    var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
    var end = Math.min(bytes.byteLength, 256 * 1024)
    var p = 0
    while (p + 8 <= end) {
      var size = view.getUint32(p, false)
      var type = String.fromCharCode(bytes[p + 4], bytes[p + 5], bytes[p + 6], bytes[p + 7])
      var header = 8
      if (size === 1) {
        if (p + 16 > end) return false
        size = view.getUint32(p + 8, false) * 4294967296 + view.getUint32(p + 12, false)
        header = 16
      } else if (size === 0) {
        size = bytes.byteLength - p
      }
      if (!isFinite(size) || size < header || p + size > bytes.byteLength) return false
      if (type === 'moov') return true
      p += size
    }
    return false
  }

  function removePendingOperation(track, operation) {
    var index = track.pendingOperations.indexOf(operation)
    if (index !== -1) track.pendingOperations.splice(index, 1)
  }

  function finishPendingAppend(track, sourceBuffer, state) {
    var operation = track.pendingOperations.shift()
    if (!operation) return

    operation.completedAt = now()
    operation.bufferedAfter = snapshotRanges(sourceBuffer.buffered)
    operation.state = state || 'ok'

    if (operation.state === 'ok') {
      track.operations.push(operation)
      track.totalBytes += operation.byteLength
      track.session.totalBytes += operation.byteLength
      if (!directSelectionTouched) selectedDirectMediaId = ''
    } else {
      track.failedCallCount++
      track.events.push({
        seq: ++track.session.sequence,
        type: 'append-' + operation.state,
        callNo: operation.callNo,
        byteLength: operation.byteLength,
        at: operation.completedAt
      })
      operation.data = null
    }

    activeSession = track.session
    requestUIRefresh()
  }

  function captureAppend(track, sourceBuffer, originalAppend, input) {
    appendBufferCalls++

    // 必须在调用原生 appendBuffer 之前同步复制。页面可能随后复用、转移或释放输入缓冲区。
    var metadata = bufferSourceMetadata(input)
    var copied = cloneBufferSource(input)

    if (isMp4InitSegment(copied)) {
      track.initSegmentCount++
      track.events.push({
        seq: ++track.session.sequence,
        type: 'init',
        at: now(),
        index: track.initSegmentCount
      })
      if (track.initSegmentCount > 1 && smartJumpEnabled) {
        markSmartJumpConfigChange('检测到新的 MP4 初始化段')
      }
    }

    var timestampOffset = safeNumber(sourceBuffer.timestampOffset, 0)
    var operation = {
      seq: ++track.session.sequence,
      callNo: ++track.appendCallNo,
      type: 'append',
      state: 'pending',
      data: copied,
      byteLength: copied.byteLength,
      constructorName: metadata.constructorName,
      sourceByteOffset: metadata.byteOffset,
      sourceByteLength: metadata.byteLength,
      sourceBufferByteLength: metadata.bufferByteLength,
      mime: track.currentMime,
      mode: sourceBuffer.mode === 'sequence' ? 'sequence' : 'segments',
      timestampOffset: timestampOffset,
      timestampOffsetChanged: track.lastTimestampOffset === null ||
        Math.abs(timestampOffset - track.lastTimestampOffset) > EPS,
      appendWindowStart: safeNumber(sourceBuffer.appendWindowStart, 0),
      appendWindowEnd: safeNumber(sourceBuffer.appendWindowEnd, Infinity),
      bufferedBefore: snapshotRanges(sourceBuffer.buffered),
      bufferedAfter: [],
      startedAt: now(),
      completedAt: 0
    }

    track.lastTimestampOffset = timestampOffset
    track.pendingOperations.push(operation)

    try {
      return originalAppend.call(sourceBuffer, input)
    } catch (error) {
      removePendingOperation(track, operation)
      track.failedCallCount++
      operation.state = 'thrown'
      operation.completedAt = now()
      operation.data = null
      track.events.push({
        seq: ++track.session.sequence,
        type: 'append-thrown',
        callNo: operation.callNo,
        byteLength: operation.byteLength,
        message: String(error && error.message || error),
        at: operation.completedAt
      })
      requestUIRefresh()
      throw error
    }
  }

  function hookSourceBuffer(track) {
    var sourceBuffer = track.sourceBuffer

    // update 在 updateend 之前触发。必须在这里结算当前操作,避免页面在自己的 update
    // 监听器中立即追加下一块时,旧 pending 状态导致新块被绕过。
    sourceBuffer.addEventListener('update', function () {
      finishPendingAppend(track, sourceBuffer, 'ok')
    })
    sourceBuffer.addEventListener('error', function () {
      finishPendingAppend(track, sourceBuffer, 'error')
    })
    sourceBuffer.addEventListener('abort', function () {
      finishPendingAppend(track, sourceBuffer, 'aborted')
    })
    sourceBuffer.addEventListener('updateend', function () {
      requestUIRefresh()
    })

    if (typeof sourceBuffer.changeType === 'function') {
      var originalChangeType = sourceBuffer.changeType
      sourceBuffer.changeType = function (mime) {
        var result = originalChangeType.call(sourceBuffer, mime)
        track.currentMime = String(mime || '')
        track.events.push({
          seq: ++track.session.sequence,
          type: 'changeType',
          mime: track.currentMime,
          at: now()
        })
        addWarning(track.session, '检测到 SourceBuffer.changeType;导出时会严格隔离不同解码配置,避免强行混合后黑屏。')
        if (smartJumpEnabled) {
          resetSmartJumpProgress('检测到 SourceBuffer.changeType,智能跳播已按新媒体状态重置', smartMediaElement(), SMART_JUMP_RESET_DELAY)
        }
        requestUIRefresh()
        return result
      }
    }

    if (typeof sourceBuffer.remove === 'function') {
      var originalRemove = sourceBuffer.remove
      sourceBuffer.remove = function (start, end) {
        track.events.push({
          seq: ++track.session.sequence,
          type: 'remove',
          start: Number(start),
          end: Number(end),
          at: now()
        })
        return originalRemove.call(sourceBuffer, start, end)
      }
    }

    if (typeof sourceBuffer.abort === 'function') {
      var originalAbort = sourceBuffer.abort
      sourceBuffer.abort = function () {
        track.events.push({
          seq: ++track.session.sequence,
          type: 'abort-call',
          at: now()
        })
        markSmartJumpConfigChange('SourceBuffer 已中止并准备重建缓冲')
        return originalAbort.call(sourceBuffer)
      }
    }
  }

  // 原型级 Hook 可覆盖 SourceBuffer.prototype.appendBuffer.call(...) 等调用形式,
  // 不再依赖给单个实例赋值后页面始终通过该实例属性调用。
  SourceBuffer.prototype.appendBuffer = function (input) {
    var track = sourceBufferTracks.get(this)
    if (!track) return nativeAppendBuffer.call(this, input)
    return captureAppend(track, this, nativeAppendBuffer, input)
  }

  MediaSource.prototype.addSourceBuffer = function (mime) {
    addSourceBufferCalls++
    var sourceBuffer = nativeAddSourceBuffer.call(this, mime)
    var session = getSession(this)
    var track = createCaptureTrack(session, sourceBuffer, mime)
    sourceBufferTracks.set(sourceBuffer, track)
    hookSourceBuffer(track)
    activeSession = session
    ensureUI()
    requestUIRefresh()
    return sourceBuffer
  }

  MediaSource.prototype.endOfStream = function () {
    var session = mediaSourceSessions.get(this)
    var result = nativeEndOfStream.apply(this, arguments)
    if (session) {
      session.endedAt = now()
      activeSession = session
      requestUIRefresh()
    }
    return result
  }

  function formatBytes(bytes) {
    if (!isFinite(bytes) || bytes <= 0) return '0 B'
    var units = ['B', 'KB', 'MB', 'GB', 'TB']
    var index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)))
    return (bytes / Math.pow(1024, index)).toFixed(index < 2 ? 0 : 1) + ' ' + units[index]
  }

  function sessionKindHints(session) {
    var result = { video: false, audio: false }
    if (!session || !session.tracks) return result
    for (var i = 0; i < session.tracks.length; i++) {
      var kind = session.tracks[i].kindHint
      if (kind === 'video') result.video = true
      if (kind === 'audio') result.audio = true
    }
    return result
  }

  function sessionActivityWindow(session) {
    var start = Infinity
    var end = -Infinity
    var count = 0
    if (session && session.tracks) {
      for (var i = 0; i < session.tracks.length; i++) {
        var operations = session.tracks[i].operations || []
        for (var j = 0; j < operations.length; j++) {
          if (operations[j].state !== 'ok') continue
          start = Math.min(start, operations[j].startedAt || operations[j].completedAt || session.createdAt)
          end = Math.max(end, operations[j].completedAt || operations[j].startedAt || session.createdAt)
          count++
        }
      }
    }
    if (!isFinite(start)) start = session ? session.createdAt : 0
    if (!isFinite(end)) end = session && session.endedAt ? session.endedAt : start
    return { start: start, end: end, count: count }
  }

  function sessionOperationTimes(session) {
    var times = []
    if (!session || !session.tracks) return times
    for (var i = 0; i < session.tracks.length; i++) {
      var operations = session.tracks[i].operations || []
      for (var j = 0; j < operations.length; j++) {
        if (operations[j].state !== 'ok') continue
        times.push(operations[j].completedAt || operations[j].startedAt || 0)
      }
    }
    times.sort(function (a, b) { return a - b })
    return times
  }

  function timingAffinity(first, second) {
    var a = sessionOperationTimes(first)
    var b = sessionOperationTimes(second)
    if (!a.length || !b.length) return 0
    var small = a.length <= b.length ? a : b
    var large = a.length <= b.length ? b : a
    var used = new Array(large.length)
    var matches = 0
    var cursor = 0
    var tolerance = 1800

    for (var i = 0; i < small.length; i++) {
      while (cursor < large.length && large[cursor] < small[i] - tolerance) cursor++
      var best = -1
      var bestDiff = Infinity
      for (var j = Math.max(0, cursor - 1); j < large.length && large[j] <= small[i] + tolerance; j++) {
        if (used[j]) continue
        var diff = Math.abs(large[j] - small[i])
        if (diff < bestDiff) {
          best = j
          bestDiff = diff
        }
      }
      if (best >= 0) {
        used[best] = true
        matches++
      }
    }
    return matches / Math.max(1, Math.min(a.length, b.length))
  }

  function activityOverlapRatio(first, second) {
    var a = sessionActivityWindow(first)
    var b = sessionActivityWindow(second)
    var overlap = Math.max(0, Math.min(a.end, b.end) - Math.max(a.start, b.start))
    var base = Math.max(1, Math.min(Math.max(1, a.end - a.start), Math.max(1, b.end - b.start)))
    return overlap / base
  }

  var fastPlaybackEnabled = false
  var FAST_PLAYBACK_RATE = 10

  function applyPlaybackRate() {
    var rate = fastPlaybackEnabled ? FAST_PLAYBACK_RATE : 1
    var media = document.querySelectorAll('video, audio')
    for (var i = 0; i < media.length; i++) {
      try {
        media[i].defaultPlaybackRate = rate
        media[i].playbackRate = rate
      } catch (e) {}
    }
  }

  function updateSpeedButton() {
    if (!ui || !ui.speedButton) return
    ui.speedButton.textContent = fastPlaybackEnabled ? '倍速播放:10×(点击关闭)' : '倍速播放:关闭'
    ui.speedButton.style.background = fastPlaybackEnabled ? '#ef6c00' : '#6d4c41'
  }

  function toggleFastPlayback() {
    fastPlaybackEnabled = !fastPlaybackEnabled
    if (fastPlaybackEnabled && smartJumpEnabled) {
      smartJumpEnabled = false
      clearSmartJumpProgress()
      updateSmartJumpButton()
    }
    applyPlaybackRate()
    runtimeLastRate = now()
    updateSpeedButton()
    requestUIRefresh()
    updateMediaLoadStatus()
  }

  function bufferedMetrics(media) {
    var result = {
      valid: false,
      duration: 0,
      current: 0,
      playedPercent: 0,
      frontier: 0,
      frontierPercent: 0,
      coverage: 0,
      coveragePercent: 0,
      lead: 0,
      currentRangeEnd: 0,
      complete: false,
      ranges: []
    }
    if (!media) return result

    var duration = Number(media.duration)
    var current = Number(media.currentTime)
    if (!isFinite(duration) || duration <= 0 || !isFinite(current)) return result

    result.valid = true
    result.duration = duration
    result.current = Math.max(0, Math.min(duration, current))
    result.playedPercent = Math.max(0, Math.min(100, result.current / duration * 100))

    var ranges = media.buffered
    var total = 0
    var frontier = 0
    var currentEnd = 0
    try {
      for (var i = 0; i < ranges.length; i++) {
        var start = Math.max(0, Number(ranges.start(i)) || 0)
        var end = Math.min(duration, Number(ranges.end(i)) || 0)
        if (end <= start) continue
        result.ranges.push([start, end])
        total += end - start
        if (end > frontier) frontier = end
        if (result.current >= start - 0.25 && result.current <= end + 0.25) {
          if (end > currentEnd) currentEnd = end
        }
      }
    } catch (e) {}

    result.frontier = frontier
    result.currentRangeEnd = currentEnd
    result.coverage = Math.min(duration, total)
    result.coveragePercent = Math.max(0, Math.min(100, result.coverage / duration * 100))
    result.frontierPercent = Math.max(0, Math.min(100, frontier / duration * 100))
    result.lead = Math.max(0, currentEnd - result.current)
    result.complete = frontier >= duration - Math.max(SMART_COMPLETE_TOLERANCE, duration * 0.001)
    return result
  }

  function smartMediaElement() {
    var direct = effectiveDirectMedia()
    if (direct && direct.kind === 'video' && direct.element && direct.element.isConnected !== false) {
      return direct.element
    }

    var videos
    try { videos = document.querySelectorAll('video') } catch (e) { return null }
    var best = null
    var bestScore = -Infinity
    for (var i = 0; i < videos.length; i++) {
      var video = videos[i]
      var metrics = bufferedMetrics(video)
      if (!metrics.valid) continue
      var score = 0
      if (!video.paused && !video.ended) score += 100
      if (mediaElementVisible(video)) score += 30
      if (video.readyState >= 2) score += 10
      score += Math.min(20, Number(video.videoWidth || 0) * Number(video.videoHeight || 0) / 100000)
      score += metrics.frontierPercent / 20
      if (score > bestScore) {
        best = video
        bestScore = score
      }
    }
    return best
  }

  function updateSmartJumpButton() {
    if (!ui || !ui.smartJumpButton) return
    ui.smartJumpButton.textContent = smartJumpEnabled
      ? '智能跳播:开启(已跳 ' + smartJumpCount + ' 次)'
      : '智能跳播:关闭'
    ui.smartJumpButton.style.background = smartJumpEnabled ? '#00897b' : '#455a64'
  }

  function smartMediaSrc(media) {
    if (!media) return ''
    try { return String(media.currentSrc || media.src || '') } catch (e) { return '' }
  }

  function markSmartJumpConfigChange(reason) {
    smartJumpPendingConfigAt = now()
    smartJumpPendingConfigReason = String(reason || '媒体配置可能已变化')
  }

  function resetSmartJumpProgress(reason, media, delay) {
    var t = Date.now()
    smartJumpLastAt = 0
    smartJumpCount = 0
    smartJumpLastTarget = 0
    smartJumpSuspendUntil = t + Math.max(0, Number(delay == null ? SMART_JUMP_RESET_DELAY : delay) || 0)
    smartJumpResetReason = String(reason || '媒体状态已变化')
    smartJumpResetAt = t
    smartJumpTrackedMedia = media || null
    smartJumpTrackedSrc = smartMediaSrc(media)
    smartJumpTrackedDuration = media ? safeNumber(Number(media.duration), 0) : 0
    smartJumpTrackedWidth = media ? Number(media.videoWidth || 0) : 0
    smartJumpTrackedHeight = media ? Number(media.videoHeight || 0) : 0
    smartJumpTrackedFrontier = 0
    smartJumpPendingConfigAt = 0
    smartJumpPendingConfigReason = ''
    updateSmartJumpButton()
    updateMediaLoadStatus()
  }

  function clearSmartJumpProgress() {
    smartJumpLastAt = 0
    smartJumpCount = 0
    smartJumpLastTarget = 0
    smartJumpSuspendUntil = 0
    smartJumpResetReason = ''
    smartJumpResetAt = 0
    smartJumpTrackedMedia = null
    smartJumpTrackedSrc = ''
    smartJumpTrackedDuration = 0
    smartJumpTrackedWidth = 0
    smartJumpTrackedHeight = 0
    smartJumpTrackedFrontier = 0
    smartJumpInternalSeekUntil = 0
    smartJumpPendingConfigAt = 0
    smartJumpPendingConfigReason = ''
  }

  function syncSmartJumpMedia(media, metrics) {
    if (!media) return false
    var src = smartMediaSrc(media)
    var duration = safeNumber(Number(media.duration), 0)
    var width = Number(media.videoWidth || 0)
    var height = Number(media.videoHeight || 0)
    var reason = ''

    if (smartJumpTrackedMedia && smartJumpTrackedMedia !== media) {
      reason = '已切换到新的播放对象'
    } else if (smartJumpTrackedSrc && src && smartJumpTrackedSrc !== src) {
      reason = '媒体地址已变化,可能已切换画质'
    } else if (smartJumpTrackedWidth && smartJumpTrackedHeight && width && height &&
               (smartJumpTrackedWidth !== width || smartJumpTrackedHeight !== height)) {
      // 分辨率变化也可能只是插播广告,单独出现时不再重置跳播。
      markSmartJumpConfigChange('分辨率由 ' + smartJumpTrackedWidth + '×' + smartJumpTrackedHeight +
        ' 变化为 ' + width + '×' + height)
    } else if (smartJumpTrackedDuration && duration &&
               Math.abs(smartJumpTrackedDuration - duration) > Math.max(2, smartJumpTrackedDuration * 0.02)) {
      reason = '视频时长发生变化,已重新建立跳播状态'
    } else if (metrics && smartJumpTrackedFrontier > 0 && metrics.frontier > 0 &&
               metrics.frontier + SMART_JUMP_FRONTIER_REGRESSION < smartJumpTrackedFrontier && !media.seeking) {
      reason = smartJumpPendingConfigAt && now() - smartJumpPendingConfigAt < 10000
        ? smartJumpPendingConfigReason + ',且缓冲区已重建'
        : '缓冲区已重建,可能已切换画质'
    }

    if (reason) {
      resetSmartJumpProgress(reason, media, SMART_JUMP_RESET_DELAY)
      smartJumpTrackedFrontier = metrics ? metrics.frontier : 0
      return true
    }

    smartJumpTrackedMedia = media
    if (src) smartJumpTrackedSrc = src
    if (duration) smartJumpTrackedDuration = duration
    if (width && height) {
      smartJumpTrackedWidth = width
      smartJumpTrackedHeight = height
    }
    if (metrics) smartJumpTrackedFrontier = metrics.frontier
    return false
  }

  function smartJumpTick() {
    if (!smartJumpEnabled) return
    var media = smartMediaElement()
    if (!media || media.paused || media.ended || media.seeking || media.readyState < 2) {
      updateMediaLoadStatus()
      return
    }

    var metrics = bufferedMetrics(media)
    if (syncSmartJumpMedia(media, metrics)) return
    if (!metrics.valid || metrics.complete || !metrics.currentRangeEnd) {
      updateMediaLoadStatus()
      return
    }

    var nowTime = Date.now()
    if (nowTime < smartJumpSuspendUntil) {
      updateMediaLoadStatus()
      return
    }
    if (nowTime - smartJumpLastAt < SMART_JUMP_COOLDOWN) return
    if (metrics.lead < SMART_JUMP_MIN_LEAD) return

    var target = Math.min(metrics.duration - 0.5, metrics.currentRangeEnd - SMART_JUMP_BACKOFF)
    if (!isFinite(target) || target - metrics.current < SMART_JUMP_MIN_DISTANCE) return

    try {
      smartJumpInternalSeekUntil = nowTime + 900
      media.currentTime = Math.max(0, target)
      smartJumpLastAt = nowTime
      smartJumpLastTarget = target
      smartJumpCount++
      updateSmartJumpButton()
      updateMediaLoadStatus()
    } catch (e) {}
  }

  function toggleSmartJump() {
    smartJumpEnabled = !smartJumpEnabled
    if (smartJumpEnabled && fastPlaybackEnabled) {
      fastPlaybackEnabled = false
      applyPlaybackRate()
      updateSpeedButton()
    }
    if (smartJumpEnabled) {
      resetSmartJumpProgress('智能跳播已启动,等待缓冲稳定', smartMediaElement(), 800)
      runtimeLastSmartJump = 0
      smartJumpTick()
    } else {
      clearSmartJumpProgress()
    }
    updateSmartJumpButton()
    updateMediaLoadStatus()
  }

  function mediaStateLabel(media, metrics) {
    if (!media || !metrics || !metrics.valid) return '加载状态:等待视频元数据'
    if (metrics.complete) {
      return '✅ 加载完成:缓冲前沿已到视频末尾' +
        (media.ended ? ',播放已结束' : '')
    }
    if (!metrics.ranges.length) return '加载状态:尚无可用缓冲区间'

    var text = '加载前沿 ' + metrics.frontierPercent.toFixed(1) + '% · 播放 ' + metrics.playedPercent.toFixed(1) + '%'
    if (metrics.coveragePercent + 0.1 < metrics.frontierPercent) {
      text += ' · 当前缓存覆盖 ' + metrics.coveragePercent.toFixed(1) + '%'
    }
    if (metrics.lead > 0.1) text += ' · 领先 ' + Math.round(metrics.lead) + ' 秒'
    if (smartJumpEnabled) {
      text += Date.now() < smartJumpSuspendUntil ? ' · 等待新画质缓冲稳定' : ' · 智能跳播中'
    }
    return text
  }

  function updateMediaLoadStatus() {
    if (!ui || !ui.loadStatus) return
    var media = smartMediaElement()
    var metrics = bufferedMetrics(media)
    var text
    var complete = false

    if (!media) {
      var session = targetSession()
      var mseEnded = !!(session && (session.endedAt || (session.mediaSource && session.mediaSource.readyState === 'ended')))
      if (mseEnded) {
        text = '✅ 捕获完成:MediaSource 已结束,所有已追加片段均已保存'
        complete = true
      } else {
        text = '加载状态:等待可跟踪的视频元素'
      }
    } else {
      text = mediaStateLabel(media, metrics)
      complete = metrics.complete
    }

    if (smartJumpResetReason && smartJumpEnabled && Date.now() - smartJumpResetAt < 12000) {
      text += '<br><span style="color:#ffd180">' + escapeHtml(smartJumpResetReason) + '</span>'
    }
    if (smartJumpLastTarget > 0 && smartJumpEnabled) {
      text += '<br><span style="color:#80cbc4">最近跳至 ' + escapeHtml(mediaTimeLabel(smartJumpLastTarget)) + '</span>'
    }
    ui.loadStatus.innerHTML = text
    ui.loadStatus.style.color = complete ? '#a5d6a7' : '#cfd8dc'
  }

  function runtimeTick() {
    var tickNow = now()
    if (tickNow - runtimeLastDirectScan >= DIRECT_SCAN_IDLE_INTERVAL) {
      runtimeLastDirectScan = tickNow
      if (scanDirectMediaElements(true)) requestUIRefresh()
    }
    if (tickNow - runtimeLastStatus >= 1000) {
      runtimeLastStatus = tickNow
      updateMediaLoadStatus()
    }
    if (fastPlaybackEnabled && tickNow - runtimeLastRate >= 1000) {
      runtimeLastRate = tickNow
      applyPlaybackRate()
    }
    if (smartJumpEnabled && tickNow - runtimeLastSmartJump >= SMART_JUMP_INTERVAL) {
      runtimeLastSmartJump = tickNow
      smartJumpTick()
    }
  }

  function startRuntimeScheduler() {
    if (runtimeTimer) return
    runtimeTimer = setInterval(runtimeTick, 500)
  }

  document.addEventListener('ratechange', function (event) {
    if (!fastPlaybackEnabled) return
    var target = event.target
    if (!target || (target.tagName !== 'VIDEO' && target.tagName !== 'AUDIO')) return
    if (Math.abs(target.playbackRate - FAST_PLAYBACK_RATE) > 0.01) {
      setTimeout(function () {
        if (fastPlaybackEnabled) applyPlaybackRate()
      }, 0)
    }
  }, true)

  function isVideoEventTarget(target) {
    return !!(target && target.tagName === 'VIDEO')
  }

  function resetSmartJumpFromMediaEvent(event, reason, delay) {
    if (!smartJumpEnabled || !isVideoEventTarget(event.target)) return
    var current = smartMediaElement()
    if (current && current !== event.target) return
    resetSmartJumpProgress(reason, event.target, delay)
  }

  document.addEventListener('loadstart', function (event) {
    resetSmartJumpFromMediaEvent(event, '视频开始重新加载,智能跳播已重置', SMART_JUMP_RESET_DELAY)
  }, true)

  document.addEventListener('emptied', function (event) {
    resetSmartJumpFromMediaEvent(event, '视频缓冲已清空,可能正在切换画质', SMART_JUMP_RESET_DELAY)
  }, true)

  document.addEventListener('loadedmetadata', function (event) {
    if (!smartJumpEnabled || !isVideoEventTarget(event.target)) return
    var current = smartMediaElement()
    if (current && current !== event.target) return
    syncSmartJumpMedia(event.target, bufferedMetrics(event.target))
  }, true)

  document.addEventListener('seeking', function (event) {
    if (!smartJumpEnabled || !isVideoEventTarget(event.target)) return
    if (Date.now() <= smartJumpInternalSeekUntil) return
    var current = smartMediaElement()
    if (current && current !== event.target) return
    resetSmartJumpProgress('检测到手动拖动,智能跳播状态已重新计算', event.target, 1000)
  }, true)

  function sessionById(id) {
    for (var i = 0; i < sessions.length; i++) {
      if (sessions[i].id === id) return sessions[i]
    }
    return null
  }

  function targetSession() {
    if (selectedSessionId) {
      var selected = sessionById(selectedSessionId)
      if (selected) return selected
      selectedSessionId = 0
    }
    return activeSession
  }

  function sessionOperationCount(session) {
    var count = 0
    if (!session || !session.tracks) return count
    for (var i = 0; i < session.tracks.length; i++) {
      count += session.tracks[i].operations.length
    }
    return count
  }

  function sessionLabel(session) {
    var hints = sessionKindHints(session)
    var kind = hints.video && hints.audio ? '视频+音频' : (hints.video ? '视频' : (hints.audio ? '音频' : '媒体'))
    var count = sessionOperationCount(session)
    return '捕获流 #' + session.id + ' · ' + kind + ' · ' + formatBytes(session.totalBytes) + ' · ' + count + ' 次追加'
  }

  function refreshSessionSelect() {
    if (!ui || !ui.sessionSelect) return
    var select = ui.sessionSelect
    var options = [{ value: 'auto', text: '自动:最近收到数据的捕获流' }]
    for (var i = 0; i < sessions.length; i++) {
      var session = sessions[i]
      if (!session.tracks.length && !session.totalBytes) continue
      options.push({ value: String(session.id), text: sessionLabel(session) })
    }

    var signature = options.map(function (item) { return item.value + '=' + item.text }).join('|')
    if (select.__signature !== signature) {
      select.__signature = signature
      select.textContent = ''
      for (var j = 0; j < options.length; j++) {
        var option = document.createElement('option')
        option.value = options[j].value
        option.textContent = options[j].text
        select.appendChild(option)
      }
    }

    var wanted = selectedSessionId ? String(selectedSessionId) : 'auto'
    var exists = false
    for (var k = 0; k < select.options.length; k++) {
      if (select.options[k].value === wanted) {
        exists = true
        break
      }
    }
    select.value = exists ? wanted : 'auto'
  }

  function refreshDirectSelect() {
    if (!ui || !ui.directSelect) return
    scanDirectMediaElements()
    var select = ui.directSelect
    if (!directMediaItems.length) {
      select.style.display = 'none'
      select.textContent = ''
      select.__signature = ''
      return
    }

    select.style.display = 'block'
    var signature = directMediaItems.map(function (item) { return item.id + '=' + item.label }).join('|')
    if (select.__signature !== signature) {
      select.__signature = signature
      select.textContent = ''

      var mseOption = document.createElement('option')
      mseOption.value = ''
      mseOption.textContent = sessionHasMediaData(targetSession()) ? '优先使用 MSE 视频流' : '自动识别完整视频'
      select.appendChild(mseOption)

      for (var i = 0; i < directMediaItems.length; i++) {
        var option = document.createElement('option')
        option.value = directMediaItems[i].id
        option.textContent = directMediaItems[i].label
        select.appendChild(option)
      }
    }

    var wanted = selectedDirectMediaId || ''
    var exists = false
    for (var j = 0; j < select.options.length; j++) {
      if (select.options[j].value === wanted) { exists = true; break }
    }
    select.value = exists ? wanted : ''
  }

  function capturedAppendCount() {
    var count = 0
    for (var i = 0; i < sessions.length; i++) {
      count += sessionOperationCount(sessions[i])
    }
    return count
  }

  function directVideoCount() {
    var count = 0
    for (var i = 0; i < directMediaItems.length; i++) {
      if (directMediaItems[i].kind === 'video' && directMediaItems[i].primaryVideo) count++
    }
    return count
  }

  function badgeCountInfo() {
    var appendCount = capturedAppendCount()
    var directCount = directVideoCount()
    if (appendCount > 0) {
      return {
        count: appendCount,
        text: appendCount > 999 ? '999+' : String(appendCount),
        title: '已捕获 ' + appendCount + ' 个 MSE 媒体片段'
      }
    }
    if (directCount > 0) {
      return {
        count: directCount,
        text: directCount > 999 ? '999+' : String(directCount),
        title: '已识别 ' + directCount + ' 个直连视频'
      }
    }
    return {
      count: 0,
      text: '0',
      title: '尚未捕获媒体;点击展开面板'
    }
  }

  function setPanelExpanded(expanded) {
    if (!ui) return
    ui.expanded = !!expanded
    ui.box.style.display = ui.expanded ? 'block' : 'none'
    ui.badge.setAttribute('aria-expanded', ui.expanded ? 'true' : 'false')
    ui.badge.style.background = ui.expanded ? '#1565c0' : (badgeCountInfo().count > 0 ? '#2e7d32' : '#263238')
    ui.badge.title = (ui.expanded ? '收起操作面板;' : '展开操作面板;') + badgeCountInfo().title
  }

  function updateBadge() {
    if (!ui || !ui.badge || !ui.badgeCount) return
    var info = badgeCountInfo()
    ui.badgeCount.textContent = info.text
    ui.badgeCount.style.background = info.count > 0 ? '#ff9800' : '#607d8b'
    ui.badge.style.background = ui.expanded ? '#1565c0' : (info.count > 0 ? '#2e7d32' : '#263238')
    ui.badge.title = (ui.expanded ? '收起操作面板;' : '展开操作面板;') + info.title
  }

  function ensureUI() {
    if (ui || !document.documentElement) return

    var badge = document.createElement('button')
    badge.type = 'button'
    badge.id = 'mse-extract-lite-badge'
    badge.setAttribute('aria-label', '媒体捕获工具')
    badge.setAttribute('aria-expanded', 'false')
    badge.style.cssText = [
      'position:fixed',
      'right:18px',
      'top:60px',
      'z-index:2147483647',
      'display:flex',
      'align-items:center',
      'gap:7px',
      'min-width:52px',
      'height:34px',
      'padding:0 9px',
      'border:1px solid rgba(255,255,255,.22)',
      'border-radius:17px',
      'background:#263238',
      'color:#fff',
      'box-shadow:0 3px 12px rgba(0,0,0,.35)',
      'cursor:pointer',
      'font:600 13px/1 Arial,sans-serif'
    ].join(';')

    var badgeIcon = document.createElement('span')
    badgeIcon.textContent = '▶'
    badgeIcon.style.cssText = 'font-size:12px;line-height:1'

    var badgeCount = document.createElement('span')
    badgeCount.textContent = '0'
    badgeCount.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;box-sizing:border-box;border-radius:10px;background:#607d8b;color:#fff;font:700 12px/20px Arial,sans-serif'

    badge.appendChild(badgeIcon)
    badge.appendChild(badgeCount)

    var box = document.createElement('div')
    box.id = 'mse-extract-lite-panel'
    box.style.cssText = [
      'position:fixed',
      'right:18px',
      'top:102px',
      'z-index:2147483647',
      'display:none',
      'width:320px',
      'padding:12px',
      'box-sizing:border-box',
      'background:rgba(24,30,36,.94)',
      'color:#fff',
      'font:13px/1.55 Arial,sans-serif',
      'border:1px solid rgba(255,255,255,.18)',
      'border-radius:6px',
      'box-shadow:0 4px 18px rgba(0,0,0,.35)'
    ].join(';')

    var sessionSelect = document.createElement('select')
    sessionSelect.style.cssText = 'display:block;width:100%;margin:0 0 8px;padding:7px 8px;box-sizing:border-box;border:1px solid #607d8b;border-radius:4px;background:#263238;color:#fff'
    sessionSelect.title = '只列出实际创建的 MediaSource 捕获流,不修改 URL.createObjectURL,也不依赖 DOM video 绑定。'

    var directSelect = document.createElement('select')
    directSelect.style.cssText = 'display:none;width:100%;margin:0 0 8px;padding:7px 8px;box-sizing:border-box;border:1px solid #8d6e63;border-radius:4px;background:#3e2723;color:#fff'
    directSelect.title = '列出页面 video/audio 元素当前实际使用的 HTTP(S) 媒体地址;blob: 和 data: 地址不会显示。'

    var status = document.createElement('div')
    status.style.cssText = 'margin-bottom:8px;word-break:break-all'

    var loadStatus = document.createElement('div')
    loadStatus.style.cssText = 'margin:0 0 8px;padding:7px 8px;border-radius:4px;background:rgba(255,255,255,.07);color:#cfd8dc;word-break:break-all'
    loadStatus.textContent = '加载状态:等待视频元数据'

    var mergeButton = makeButton('合并并下载 MP4', '#2e7d32')
    var copyUrlButton = makeButton('复制媒体 URL', '#5d4037')
    copyUrlButton.style.display = 'none'
    var smartJumpButton = makeButton('智能跳播:关闭', '#455a64')
    var speedButton = makeButton('倍速播放:关闭', '#6d4c41')
    var rawButton = makeButton('下载原始轨道', '#1565c0')
    var jsonButton = makeButton('导出时间轴 JSON', '#555')
    var closeButton = makeButton('隐藏', '#444')

    sessionSelect.addEventListener('change', function () {
      if (sessionSelect.value === 'auto') {
        selectedSessionId = 0
      } else {
        selectedSessionId = parseInt(sessionSelect.value, 10) || 0
      }
      if (smartJumpEnabled) resetSmartJumpProgress('已切换捕获流,智能跳播状态已重置', smartMediaElement(), 1000)
      requestUIRefresh()
    })

    directSelect.addEventListener('change', function () {
      selectedDirectMediaId = directSelect.value
      directSelectionTouched = !!directSelect.value
      if (smartJumpEnabled) resetSmartJumpProgress('已切换媒体目标,智能跳播状态已重置', smartMediaElement(), 1000)
      requestUIRefresh()
    })

    mergeButton.addEventListener('click', function () {
      var direct = effectiveDirectMedia()
      if (direct) {
        downloadDirectMedia(direct).catch(function (error) {
          console.error('[MSE Extract Lite] 直连下载失败', error)
          alert('直连下载失败:' + error.message)
        })
        return
      }

      var session = targetSession()
      if (!session) return alert('尚未捕获到 MediaSource 会话,也未选择可下载的 video/audio 直连地址')
      exportMergedMp4(session).catch(function (error) {
        console.error('[MSE Extract Lite] 合并失败', error)
        alert('合并失败:' + error.message + '\n\n将下载原始轨道和时间轴信息。')
        downloadRawTracks(session)
        downloadTimelineJson(session)
      })
    })

    copyUrlButton.addEventListener('click', function () {
      var direct = selectedDirectMedia()
      if (!direct || !direct.url) return alert('当前没有选中可复制的 HTTP(S) 媒体地址')
      copyTextToClipboard(direct.url).then(function () {
        setPanelStatus('媒体 URL 已复制到剪贴板')
        setTimeout(requestUIRefresh, 1200)
      }).catch(function (error) {
        console.error('[MSE Extract Lite] 复制 URL 失败', error)
        window.prompt('自动复制失败,请手动复制媒体 URL:', direct.url)
      })
    })

    smartJumpButton.addEventListener('click', toggleSmartJump)
    speedButton.addEventListener('click', toggleFastPlayback)

    rawButton.addEventListener('click', function () {
      var session = targetSession()
      if (!session) return alert('尚未捕获到 MediaSource 会话')
      downloadRawTracks(session)
    })

    jsonButton.addEventListener('click', function () {
      var session = targetSession()
      if (!session) return alert('尚未捕获到 MediaSource 会话')
      downloadTimelineJson(session)
    })

    badge.addEventListener('click', function () {
      setPanelExpanded(!ui.expanded)
      if (ui.expanded) requestUIRefresh()
    })

    closeButton.addEventListener('click', function () {
      setPanelExpanded(false)
    })

    box.appendChild(sessionSelect)
    box.appendChild(directSelect)
    box.appendChild(status)
    box.appendChild(loadStatus)
    box.appendChild(mergeButton)
    box.appendChild(copyUrlButton)
    box.appendChild(smartJumpButton)
    box.appendChild(speedButton)
    box.appendChild(rawButton)
    box.appendChild(jsonButton)
    box.appendChild(closeButton)
    document.documentElement.appendChild(badge)
    document.documentElement.appendChild(box)

    ui = {
      box: box,
      badge: badge,
      badgeCount: badgeCount,
      expanded: false,
      status: status,
      sessionSelect: sessionSelect,
      directSelect: directSelect,
      mergeButton: mergeButton,
      copyUrlButton: copyUrlButton,
      speedButton: speedButton,
      smartJumpButton: smartJumpButton,
      loadStatus: loadStatus
    }
    updateSpeedButton()
    updateSmartJumpButton()
    updateMediaLoadStatus()
    updateBadge()
  }

  function makeButton(text, color) {
    var button = document.createElement('button')
    button.type = 'button'
    button.textContent = text
    button.style.cssText = 'display:block;width:100%;margin:6px 0 0;padding:8px 10px;border:0;border-radius:4px;background:' + color + ';color:#fff;cursor:pointer;font-weight:600'
    return button
  }

  function refreshUI() {
    ensureUI()
    if (!ui) return
    refreshSessionSelect()
    refreshDirectSelect()
    updateBadge()

    var selectedDirect = selectedDirectMedia()
    var direct = effectiveDirectMedia()
    var session = targetSession()
    updateMediaLoadStatus()
    ui.copyUrlButton.style.display = selectedDirect ? 'block' : 'none'
    if (selectedDirect) ui.copyUrlButton.textContent = selectedDirect.kind === 'audio' ? '复制音频 URL' : '复制视频 URL'

    if (direct) {
      ui.mergeButton.textContent = direct.kind === 'audio' ? '下载直连音频' : '下载直连视频'
      ui.status.innerHTML = [
        '<b>已识别 video/audio 直连媒体</b>',
        '<div>' + escapeHtml(direct.label) + '</div>',
        '<div style="margin-top:4px;color:#b0bec5">地址来自元素的 currentSrc/src,不经过 MSE 捕获与重封装。</div>',
        direct.kind === 'audio' ? '<div style="margin-top:4px;color:#ffd180">这是独立音轨,不代表完整视频;仅在手动选择时下载。</div>' : '',
        '<div>addSourceBuffer:' + addSourceBufferCalls + ' appendBuffer:' + appendBufferCalls + '</div>'
      ].join('')
      return
    }

    if (!session) {
      var videoCount = 0
      var audioCount = 0
      try {
        videoCount = document.querySelectorAll('video').length
        audioCount = document.querySelectorAll('audio').length
      } catch (e) {}
      ui.mergeButton.textContent = preferRawFragmentMux() ? '原始分片合并并下载 MP4' : '合并并下载 MP4'
      ui.status.innerHTML = [
        '<b>MSE 捕获器已立即安装</b>',
        '<div>正在等待媒体数据或普通 video/audio 地址…</div>',
        '<div>addSourceBuffer:' + addSourceBufferCalls + ' appendBuffer:' + appendBufferCalls + '</div>',
        '<div>页面 video:' + videoCount + ' audio:' + audioCount + ' 运行:' + Math.max(0, Math.round((Date.now() - installedAt) / 1000)) + ' 秒</div>',
        '<div style="margin-top:5px;color:#b0bec5">普通 HTTPS src 会直接列出;blob: 视频则等待 MSE 捕获。</div>'
      ].join('')
      return
    }
    ui.mergeButton.textContent = preferRawFragmentMux() ? '原始分片合并并下载 MP4' : '合并并下载 MP4'

    var operationCount = 0
    var pendingCount = 0
    for (var i = 0; i < session.tracks.length; i++) {
      operationCount += session.tracks[i].operations.length
      pendingCount += session.tracks[i].pendingOperations.length
    }

    var videoHints = 0
    var audioHints = 0
    for (var kt = 0; kt < session.tracks.length; kt++) {
      if (session.tracks[kt].kindHint === 'video') videoHints++
      if (session.tracks[kt].kindHint === 'audio') audioHints++
    }

    ui.status.innerHTML = [
      '<b>实际捕获流 #' + session.id + '</b>',
      '<div>选择:' + (selectedSessionId ? '手动固定' : '自动跟随最近数据') + '</div>',
      '<div>轨道:视频 ' + videoHints + ' 音频 ' + audioHints + ' 页面流 ' + sessions.length + '</div>',
      '<div>追加:' + operationCount +
        (pendingCount ? ' 处理中:' + pendingCount : '') + '</div>',
      '<div>Hook:addSourceBuffer ' + addSourceBufferCalls + ' appendBuffer ' + appendBufferCalls + '</div>',
      '<div>已保存:' + formatBytes(session.totalBytes) + '</div>',
      '<div>播放速度:' + (fastPlaybackEnabled ? FAST_PLAYBACK_RATE + '×' : '正常') + '</div>',
      session.warnings.length ? '<div style="margin-top:5px;color:#ffd180">警告:' + escapeHtml(session.warnings[session.warnings.length - 1]) + '</div>' : ''
    ].join('')
  }

  function escapeHtml(text) {
    return String(text).replace(/[&<>"']/g, function (char) {
      return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]
    })
  }

  function copyTextToClipboard(text) {
    text = String(text || '')
    if (!text) return Promise.reject(new Error('复制内容为空'))

    if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
      return navigator.clipboard.writeText(text).catch(function () {
        return legacyCopyText(text)
      })
    }
    return legacyCopyText(text)
  }

  function legacyCopyText(text) {
    return new Promise(function (resolve, reject) {
      var textarea = document.createElement('textarea')
      textarea.value = text
      textarea.setAttribute('readonly', '')
      textarea.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0'
      document.documentElement.appendChild(textarea)
      textarea.select()
      textarea.setSelectionRange(0, textarea.value.length)
      var ok = false
      try { ok = document.execCommand('copy') } catch (error) {}
      textarea.remove()
      if (ok) resolve()
      else reject(new Error('浏览器拒绝写入剪贴板'))
    })
  }

  function downloadBlob(blob, filename) {
    var url = URL.createObjectURL(blob)
    var anchor = document.createElement('a')
    anchor.href = url
    anchor.download = sanitizeFilename(filename)
    anchor.style.display = 'none'
    document.documentElement.appendChild(anchor)
    anchor.click()
    anchor.remove()
    setTimeout(function () { URL.revokeObjectURL(url) }, 120000)
  }

  function setPanelStatus(text) {
    if (ui && ui.status) ui.status.innerHTML = '<b>' + escapeHtml(text) + '</b>'
  }

  function openDirectUrl(url) {
    var anchor = document.createElement('a')
    anchor.href = url
    anchor.target = '_blank'
    anchor.rel = 'noopener noreferrer'
    anchor.style.display = 'none'
    document.documentElement.appendChild(anchor)
    anchor.click()
    anchor.remove()
  }

  async function downloadDirectMedia(format) {
    ui.mergeButton.disabled = true
    ui.mergeButton.textContent = '正在下载直连媒体…'
    setPanelStatus('正在请求直连媒体…')
    try {
      var response
      try {
        response = await fetch(format.url, {
          method: 'GET',
          credentials: 'include',
          cache: 'force-cache',
          referrer: location.href,
          referrerPolicy: 'strict-origin-when-cross-origin'
        })
      } catch (error) {
        openDirectUrl(format.url)
        throw new Error('页面 fetch 被 CORS 或防盗链阻止,已在新标签页打开媒体地址')
      }
      if (!response.ok) {
        openDirectUrl(format.url)
        throw new Error('媒体服务器返回 HTTP ' + response.status + ',已在新标签页打开媒体地址')
      }

      var total = Number(response.headers.get('content-length')) || 0
      if (total > MAX_REMUX_BYTES) {
        openDirectUrl(format.url)
        throw new Error('文件达到 ' + formatBytes(total) + ',超过浏览器内存保存上限,已打开媒体地址')
      }

      var buffer
      if (response.body && response.body.getReader) {
        var reader = response.body.getReader()
        var chunks = []
        var received = 0
        var lastUpdate = 0
        while (true) {
          var part = await reader.read()
          if (part.done) break
          if (!part.value || !part.value.byteLength) continue
          chunks.push(part.value)
          received += part.value.byteLength
          if (received > MAX_REMUX_BYTES) {
            try { await reader.cancel() } catch (e) {}
            openDirectUrl(format.url)
            throw new Error('媒体数据超过浏览器内存保存上限,已打开媒体地址')
          }
          if (now() - lastUpdate > 300) {
            lastUpdate = now()
            setPanelStatus('正在下载:' + formatBytes(received) + (total ? ' / ' + formatBytes(total) : ''))
          }
        }
        var bytes = new Uint8Array(received)
        var offset = 0
        for (var i = 0; i < chunks.length; i++) {
          bytes.set(chunks[i], offset)
          offset += chunks[i].byteLength
        }
        buffer = bytes.buffer
      } else {
        buffer = await response.arrayBuffer()
      }
      if (!buffer || !buffer.byteLength) throw new Error('媒体响应为空')

      var suffix = []
      if (format.width && format.height) suffix.push(format.width + 'x' + format.height)
      var extension = format.extension || (format.kind === 'audio' ? 'm4a' : 'mp4')
      var filename = sanitizeFilename(format.title || safeTitle()) + (suffix.length ? '-' + suffix.join('-') : '') + '.' + extension
      var mime = format.kind === 'audio' ? 'audio/' + (extension === 'mp3' ? 'mpeg' : extension) : 'video/' + extension
      downloadBlob(new Blob([buffer], { type: mime }), filename)
      setPanelStatus('直连媒体已保存')
    } finally {
      ui.mergeButton.disabled = false
      var currentDirect = effectiveDirectMedia()
      ui.mergeButton.textContent = currentDirect ? (currentDirect.kind === 'audio' ? '下载直连音频' : '下载直连视频') : '合并并下载 MP4'
      setTimeout(requestUIRefresh, 1200)
    }
  }

  function rawExtension(mime) {
    if (isMp4Mime(mime)) return 'mp4'
    if (isWebMMime(mime)) return 'webm'
    var base = String(mime || '').split(';')[0].split('/')[1]
    return base || 'bin'
  }

  function downloadRawTracks(session) {
    var base = session.title || safeTitle()
    for (var i = 0; i < session.tracks.length; i++) {
      var track = session.tracks[i]
      var operations = track.operations.filter(function (operation) {
        return operation.state === 'ok' && operation.data
      })
      if (!operations.length) continue
      var mime = operations[0].mime || track.initialMime || 'application/octet-stream'
      var parts = operations.map(function (operation) { return operation.data })
      downloadBlob(
        new Blob(parts, { type: String(mime).split(';')[0] }),
        base + '-track-' + (i + 1) + '.' + rawExtension(mime)
      )
    }
  }

  // ---------------------------------------------------------------------------
  // Fragment-preserving MP4 muxer
  // ---------------------------------------------------------------------------
  // YouTube 等站点的原始 MSE 轨道通常已经是可连续播放的 fMP4。传统路径会把
  // moof/mdat 拆成样本后重建 flat MP4;若站点使用非常规分片边界,容易在解析
  // 阶段丢失样本。本路径保留原始媒体负载,只修正初始化信息、轨道 ID、tfdt、
  // mfhd,并生成 mfra/tfra 随机访问索引。

  var RAW_TOP_LEVEL_TYPES = {
    ftyp: 1, styp: 1, moov: 1, moof: 1, mdat: 1,
    sidx: 1, ssix: 1, emsg: 1, prft: 1, mfra: 1, free: 1, skip: 1
  }

  function rawWriteU32(bytes, offset, value) {
    new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setUint32(offset, value >>> 0, false)
  }

  function rawWriteU64(bytes, offset, value) {
    var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
    var high = Math.floor(value / 4294967296)
    var low = value - high * 4294967296
    view.setUint32(offset, high >>> 0, false)
    view.setUint32(offset + 4, low >>> 0, false)
  }

  function rawCloneRange(bytes, box) {
    return bytes.subarray(box.start, box.end).slice()
  }

  function rawBoxAt(bytes, offset, limit) {
    limit = limit === undefined ? bytes.byteLength : limit
    if (offset < 0 || offset + 8 > limit) return null
    var size = readU32(bytes, offset)
    var type = readType(bytes, offset + 4)
    var headerSize = 8
    var zeroSize = false
    if (size === 1) {
      if (offset + 16 > limit) return null
      size = readU64(bytes, offset + 8)
      headerSize = 16
    } else if (size === 0) {
      zeroSize = true
      size = limit - offset
    }
    if (!isFinite(size) || size < headerSize || offset + size > limit) return null
    return {
      type: type,
      start: offset,
      end: offset + size,
      size: size,
      headerSize: headerSize,
      data: offset + headerSize,
      zeroSize: zeroSize
    }
  }

  function rawContainerLooksValid(bytes, box) {
    if (!box) return false
    if (box.type === 'moof') {
      var moofChildren = scanBoxesRange(bytes, box.data, box.end)
      var hasTraf = false
      for (var i = 0; i < moofChildren.length; i++) {
        if (moofChildren[i].type !== 'traf') continue
        hasTraf = true
        var trafChildren = scanBoxesRange(bytes, moofChildren[i].data, moofChildren[i].end)
        var hasTfhd = false
        var hasTrun = false
        for (var j = 0; j < trafChildren.length; j++) {
          if (trafChildren[j].type === 'tfhd') hasTfhd = true
          if (trafChildren[j].type === 'trun') hasTrun = true
        }
        if (hasTfhd && hasTrun) return true
      }
      return hasTraf && moofChildren.length > 0
    }
    if (box.type === 'moov') {
      var moovChildren = scanBoxesRange(bytes, box.data, box.end)
      var hasMvhd = false
      var hasTrak = false
      for (var k = 0; k < moovChildren.length; k++) {
        if (moovChildren[k].type === 'mvhd') hasMvhd = true
        if (moovChildren[k].type === 'trak') hasTrak = true
      }
      return hasMvhd && hasTrak
    }
    return true
  }

  function rawFindNextBoundary(bytes, start) {
    var end = bytes.byteLength
    for (var offset = Math.max(0, start); offset + 8 <= end; offset++) {
      var type = readType(bytes, offset + 4)
      if (type !== 'moof' && type !== 'moov' && type !== 'ftyp' && type !== 'styp') continue
      var box = rawBoxAt(bytes, offset, end)
      if (!box || box.zeroSize) continue
      if (type === 'moof' || type === 'moov') {
        if (!rawContainerLooksValid(bytes, box)) continue
      }
      return offset
    }
    return end
  }

  function rawScanTopLevel(bytes) {
    var boxes = []
    var offset = 0
    var end = bytes.byteLength
    while (offset + 8 <= end) {
      var type = readType(bytes, offset + 4)
      var box = rawBoxAt(bytes, offset, end)
      var valid = !!box && !!RAW_TOP_LEVEL_TYPES[type]
      if (valid && (type === 'moof' || type === 'moov')) valid = rawContainerLooksValid(bytes, box)

      if (valid) {
        if (type === 'mdat' && box.zeroSize) {
          var next = rawFindNextBoundary(bytes, box.data)
          box.end = next
          box.size = next - box.start
        }
        boxes.push(box)
        offset = box.end
        continue
      }

      var nextOffset = rawFindNextBoundary(bytes, offset + 1)
      if (nextOffset <= offset || nextOffset >= end) break
      offset = nextOffset
    }
    return boxes
  }

  function rawChild(bytes, parent, type) {
    var list = scanBoxesRange(bytes, parent.data, parent.end)
    for (var i = 0; i < list.length; i++) if (list[i].type === type) return list[i]
    return null
  }

  function rawChildren(bytes, parent, type) {
    var list = scanBoxesRange(bytes, parent.data, parent.end)
    if (!type) return list
    return list.filter(function (box) { return box.type === type })
  }

  function rawFullFlags(bytes, box) {
    return readU32(bytes, box.data) & 0xffffff
  }

  function rawConcatTrackBytes(captureTrack) {
    var operations = captureTrack.operations.filter(function (operation) {
      return operation.state === 'ok' && operation.data && isMp4Mime(operation.mime || captureTrack.initialMime)
    })
    if (!operations.length) throw new Error('轨道 #' + captureTrack.id + ' 没有原始 MP4 数据')
    var total = 0
    for (var i = 0; i < operations.length; i++) total += operations[i].data.byteLength
    if (total > MAX_REMUX_BYTES) throw new Error('原始轨道达到 ' + formatBytes(total) + ',超过浏览器内存处理上限')
    var output = new Uint8Array(total)
    var offset = 0
    for (var j = 0; j < operations.length; j++) {
      output.set(new Uint8Array(operations[j].data), offset)
      offset += operations[j].data.byteLength
    }
    return output
  }

  function rawTrackHandler(bytes, trak) {
    var mdia = rawChild(bytes, trak, 'mdia')
    var hdlr = mdia ? rawChild(bytes, mdia, 'hdlr') : null
    if (!hdlr || hdlr.data + 12 > hdlr.end) return 'unknown'
    var handler = readType(bytes, hdlr.data + 8)
    return handler === 'vide' ? 'video' : (handler === 'soun' ? 'audio' : 'unknown')
  }

  function rawParseInit(bytes, moov, kindHint, ftyp) {
    var children = rawChildren(bytes, moov)
    var mvhd = null
    var mvex = null
    var traks = []
    for (var i = 0; i < children.length; i++) {
      if (children[i].type === 'mvhd') mvhd = children[i]
      if (children[i].type === 'mvex') mvex = children[i]
      if (children[i].type === 'trak') traks.push(children[i])
    }
    if (!mvhd || !traks.length) throw new Error('原始初始化段缺少 mvhd/trak')

    var trak = null
    var kind = 'unknown'
    for (var t = 0; t < traks.length; t++) {
      var candidateKind = rawTrackHandler(bytes, traks[t])
      if (!trak || candidateKind === kindHint) {
        trak = traks[t]
        kind = candidateKind
      }
      if (candidateKind === kindHint) break
    }
    if (!trak || kind === 'unknown') throw new Error('原始初始化段没有可识别的音视频轨道')

    var tkhd = rawChild(bytes, trak, 'tkhd')
    var mdia = rawChild(bytes, trak, 'mdia')
    var mdhd = mdia ? rawChild(bytes, mdia, 'mdhd') : null
    var minf = mdia ? rawChild(bytes, mdia, 'minf') : null
    var stbl = minf ? rawChild(bytes, minf, 'stbl') : null
    var stsd = stbl ? rawChild(bytes, stbl, 'stsd') : null
    if (!tkhd || !mdhd || !stsd) throw new Error('原始初始化段缺少 tkhd/mdhd/stsd')

    var tkVersion = bytes[tkhd.data]
    var tkFields = tkhd.data + 4
    var trackId = tkVersion === 1 ? readU32(bytes, tkFields + 16) : readU32(bytes, tkFields + 8)
    var mdVersion = bytes[mdhd.data]
    var mdFields = mdhd.data + 4
    var timescale = mdVersion === 1 ? readU32(bytes, mdFields + 16) : readU32(bytes, mdFields + 8)
    var mvVersion = bytes[mvhd.data]
    var mvFields = mvhd.data + 4
    var movieTimescale = mvVersion === 1 ? readU32(bytes, mvFields + 16) : readU32(bytes, mvFields + 8)

    var entryCount = stsd.data + 8 <= stsd.end ? readU32(bytes, stsd.data + 4) : 0
    var entryStart = stsd.data + 8
    var entrySize = entryCount && entryStart + 8 <= stsd.end ? readU32(bytes, entryStart) : 0
    if (!entrySize || entryStart + entrySize > stsd.end) throw new Error('原始初始化段没有有效 sample entry')
    var description = bytes.subarray(entryStart, entryStart + entrySize).slice()
    var entryType = description.byteLength >= 8 ? readType(description, 4) : ''
    if (entryType === 'encv' || entryType === 'enca') throw new Error('检测到加密原始轨道 ' + entryType)
    var configKey = decoderConfigurationKey(description, kind, { timescale: timescale })

    var trex = null
    var defaults = { descriptionIndex: 1, duration: 0, size: 0, flags: 0 }
    if (mvex) {
      var trexList = rawChildren(bytes, mvex, 'trex')
      for (var x = 0; x < trexList.length; x++) {
        var p = trexList[x].data + 4
        if (p + 20 > trexList[x].end) continue
        var trexId = readU32(bytes, p)
        if (trexId !== trackId && trexList.length > 1) continue
        trex = trexList[x]
        defaults = {
          descriptionIndex: readU32(bytes, p + 4) || 1,
          duration: readU32(bytes, p + 8),
          size: readU32(bytes, p + 12),
          flags: readU32(bytes, p + 16)
        }
        break
      }
    }

    return {
      kind: kind,
      trackId: trackId,
      timescale: timescale || (kind === 'audio' ? 48000 : 90000),
      movieTimescale: movieTimescale || MOVIE_TIMESCALE,
      configKey: configKey,
      description: description,
      ftypBytes: ftyp ? rawCloneRange(bytes, ftyp) : null,
      mvhdBytes: rawCloneRange(bytes, mvhd),
      trakBytes: rawCloneRange(bytes, trak),
      trexBytes: trex ? rawCloneRange(bytes, trex) : null,
      defaults: defaults
    }
  }

  function rawParseFragment(bytes, moof, mdat, init, order) {
    var trafs = rawChildren(bytes, moof, 'traf')
    var selected = null
    var tfhd = null
    for (var i = 0; i < trafs.length; i++) {
      var candidate = rawChild(bytes, trafs[i], 'tfhd')
      if (!candidate) continue
      var candidateId = readU32(bytes, candidate.data + 4)
      if (!selected || candidateId === init.trackId) {
        selected = trafs[i]
        tfhd = candidate
      }
      if (candidateId === init.trackId) break
    }
    if (!selected || !tfhd) return null

    var tfFlags = rawFullFlags(bytes, tfhd)
    var p = tfhd.data + 4
    var sourceTrackId = readU32(bytes, p)
    p += 4
    var defaults = {
      descriptionIndex: init.defaults.descriptionIndex,
      duration: init.defaults.duration,
      size: init.defaults.size,
      flags: init.defaults.flags
    }
    if (tfFlags & 0x000001) p += 8
    if (tfFlags & 0x000002) { defaults.descriptionIndex = readU32(bytes, p) || 1; p += 4 }
    if (tfFlags & 0x000008) { defaults.duration = readU32(bytes, p); p += 4 }
    if (tfFlags & 0x000010) { defaults.size = readU32(bytes, p); p += 4 }
    if (tfFlags & 0x000020) { defaults.flags = readU32(bytes, p); p += 4 }

    var tfdt = rawChild(bytes, selected, 'tfdt')
    var start = 0
    if (tfdt) start = bytes[tfdt.data] === 1 ? readU64(bytes, tfdt.data + 4) : readU32(bytes, tfdt.data + 4)

    var duration = 0
    var firstSync = true
    var syncKnown = init.kind === 'audio'
    var runs = rawChildren(bytes, selected, 'trun')
    for (var r = 0; r < runs.length; r++) {
      var run = runs[r]
      var runFlags = rawFullFlags(bytes, run)
      var rp = run.data + 4
      var sampleCount = readU32(bytes, rp)
      rp += 4
      var firstSampleFlags = null
      if (runFlags & 0x000001) rp += 4
      if (runFlags & 0x000004) { firstSampleFlags = readU32(bytes, rp); rp += 4 }
      for (var s = 0; s < sampleCount; s++) {
        var sampleDuration = runFlags & 0x000100 ? readU32(bytes, rp) : defaults.duration
        if (runFlags & 0x000100) rp += 4
        if (runFlags & 0x000200) rp += 4
        var sampleFlags = runFlags & 0x000400
          ? readU32(bytes, rp)
          : (s === 0 && firstSampleFlags !== null ? firstSampleFlags : defaults.flags)
        if (runFlags & 0x000400) rp += 4
        if (runFlags & 0x000800) rp += 4
        if (r === 0 && s === 0 && init.kind === 'video') {
          syncKnown = !!(runFlags & 0x000400) || firstSampleFlags !== null || defaults.flags !== 0
          firstSync = (sampleFlags & 0x00010000) === 0
        }
        duration += sampleDuration || 1
      }
    }
    if (!duration || !runs.length) return null

    return {
      order: order,
      sourceTrackId: sourceTrackId,
      start: start,
      duration: duration,
      firstSync: firstSync,
      syncKnown: syncKnown,
      moofBytes: rawCloneRange(bytes, moof),
      mdatBytes: rawCloneRange(bytes, mdat),
      mdatZeroSize: mdat.zeroSize
    }
  }

  function rawNormalizeRun(run) {
    var fragments = run.fragments.slice().sort(function (a, b) { return a.order - b.order })
    var cursor = 0
    var firstSource = fragments.length ? fragments[0].start : 0
    for (var i = 0; i < fragments.length; i++) {
      var fragment = fragments[i]
      var sourceStart = fragment.start - firstSource
      var duration = fragment.duration
      var expected = cursor
      var tolerance = Math.max(run.init.timescale * 2, duration * 2)
      var normalized = i === 0 ? 0 : sourceStart
      if (i > 0 && (normalized < expected - tolerance || normalized > expected + tolerance * 8)) normalized = expected
      if (i > 0 && normalized < expected) normalized = expected
      fragment.normalizedStart = Math.max(0, Math.round(normalized))
      cursor = fragment.normalizedStart + duration
    }
    run.fragments = fragments
    run.duration = cursor
    run.durationSec = cursor / run.init.timescale
    run.mediaBytes = fragments.reduce(function (sum, fragment) {
      return sum + Math.max(0, fragment.mdatBytes.byteLength - 8)
    }, 0)
    return run
  }

  function rawFindEmbeddedContainer(bytes, box) {
    if (!box || box.type !== 'mdat') return -1
    for (var offset = box.data; offset + 8 <= box.end; offset++) {
      var type = readType(bytes, offset + 4)
      if (type !== 'moof' && type !== 'moov') continue
      var nested = rawBoxAt(bytes, offset, box.end)
      if (!nested || nested.zeroSize || !rawContainerLooksValid(bytes, nested)) continue
      return offset
    }
    return -1
  }

  function rawValidateTopLevelContinuity(bytes, boxes, captureTrack) {
    if (!boxes || !boxes.length) {
      throw makeCaptureIntegrityError('轨道 #' + captureTrack.id + ' 没有可验证的 MP4 顶层盒')
    }

    var expectedOffset = 0
    for (var i = 0; i < boxes.length; i++) {
      var box = boxes[i]
      if (box.start !== expectedOffset) {
        throw makeCaptureIntegrityError(
          '轨道 #' + captureTrack.id + ' 在偏移 ' + expectedOffset +
          ' 处存在 ' + Math.abs(box.start - expectedOffset) + ' 字节未识别或缺失数据'
        )
      }

      if (box.type === 'mdat' && !box.zeroSize) {
        var embedded = rawFindEmbeddedContainer(bytes, box)
        if (embedded >= 0) {
          throw makeCaptureIntegrityError(
            '轨道 #' + captureTrack.id + ' 的 mdat 在偏移 ' + embedded +
            ' 处吞入了后续有效 moof/moov;通常表示中间 appendBuffer 数据块缺失'
          )
        }
      }
      expectedOffset = box.end
    }

    if (expectedOffset !== bytes.byteLength) {
      throw makeCaptureIntegrityError(
        '轨道 #' + captureTrack.id + ' 尾部有 ' +
        (bytes.byteLength - expectedOffset) + ' 字节未形成完整 MP4 box'
      )
    }
  }

  function rawBuildTrackModel(captureTrack) {
    var cacheKey = captureTrack.operations.length + ':' + captureTrack.totalBytes
    if (captureTrack.rawFragmentCache && captureTrack.rawFragmentCache.key === cacheKey) return captureTrack.rawFragmentCache.value

    var bytes = rawConcatTrackBytes(captureTrack)
    var top = rawScanTopLevel(bytes)
    rawValidateTopLevelContinuity(bytes, top, captureTrack)
    var ftyp = null
    var currentInit = null
    var periods = []
    var order = 0

    for (var i = 0; i < top.length; i++) {
      var box = top[i]
      if (box.type === 'ftyp' && !ftyp) ftyp = box
      if (box.type === 'moov') {
        try {
          currentInit = rawParseInit(bytes, box, captureTrack.kindHint, ftyp)
          periods.push({ init: currentInit, fragments: [] })
        } catch (error) {
          currentInit = null
        }
        continue
      }
      if (box.type !== 'moof' || !currentInit || !periods.length) continue
      var mdat = null
      for (var next = i + 1; next < top.length; next++) {
        if (top[next].type === 'mdat') { mdat = top[next]; break }
        if (top[next].type === 'moof' || top[next].type === 'moov') break
      }
      if (!mdat) continue
      var fragment = rawParseFragment(bytes, box, mdat, currentInit, ++order)
      if (fragment) periods[periods.length - 1].fragments.push(fragment)
    }

    var runs = []
    for (var p = 0; p < periods.length; p++) {
      var period = periods[p]
      if (!period.fragments.length) continue
      var previous = runs.length ? runs[runs.length - 1] : null
      if (previous && previous.init.kind === period.init.kind && previous.init.configKey === period.init.configKey) {
        previous.fragments = previous.fragments.concat(period.fragments)
      } else {
        runs.push({ init: period.init, fragments: period.fragments.slice() })
      }
    }
    for (var r = 0; r < runs.length; r++) rawNormalizeRun(runs[r])
    runs.sort(function (a, b) {
      if (b.durationSec !== a.durationSec) return b.durationSec - a.durationSec
      return b.mediaBytes - a.mediaBytes
    })
    if (!runs.length) throw new Error('轨道 #' + captureTrack.id + ' 没有可保留的 moof/mdat 分片')

    var chosen = runs[0]
    var model = {
      captureTrack: captureTrack,
      kind: chosen.init.kind,
      init: chosen.init,
      fragments: chosen.fragments,
      duration: chosen.duration,
      durationSec: chosen.durationSec,
      mediaBytes: chosen.mediaBytes,
      alternativeRuns: Math.max(0, runs.length - 1)
    }
    captureTrack.rawFragmentCache = { key: cacheKey, value: model }
    return model
  }

  function rawModelsFromSession(session) {
    var models = []
    for (var i = 0; i < session.tracks.length; i++) {
      var track = session.tracks[i]
      if (!track.operations.length || !isMp4Mime(track.currentMime || track.initialMime)) continue
      try {
        models.push(rawBuildTrackModel(track))
      } catch (error) {
        console.warn('[MSE Extract Lite] 原始分片轨道解析失败 #' + track.id, error)
      }
    }
    return models
  }

  function rawBestModel(models, kind) {
    var best = null
    for (var i = 0; i < models.length; i++) {
      if (models[i].kind !== kind) continue
      if (!best || models[i].durationSec > best.durationSec ||
        (models[i].durationSec === best.durationSec && models[i].mediaBytes > best.mediaBytes)) best = models[i]
    }
    return best
  }

  function rawFindCompanion(primarySession, primaryModel, missingKind) {
    var best = null
    for (var i = 0; i < sessions.length; i++) {
      var candidateSession = sessions[i]
      if (candidateSession === primarySession || !candidateSession.totalBytes) continue
      var hints = sessionKindHints(candidateSession)
      if (!hints[missingKind]) continue
      var roughAffinity = timingAffinity(primarySession, candidateSession)
      var roughOverlap = activityOverlapRatio(primarySession, candidateSession)
      if (roughAffinity < 0.12 && roughOverlap < 0.12 && Math.abs(candidateSession.createdAt - primarySession.createdAt) > 8000) continue
      var model = rawBestModel(rawModelsFromSession(candidateSession), missingKind)
      if (!model) continue
      var ratio = primaryModel && primaryModel.durationSec > 0
        ? Math.min(primaryModel.durationSec, model.durationSec) / Math.max(primaryModel.durationSec, model.durationSec)
        : 1
      var score = roughAffinity * 180 + roughOverlap * 80 + ratio * 140 - Math.abs(candidateSession.createdAt - primarySession.createdAt) / 1000
      if (!best || score > best.score) best = { model: model, session: candidateSession, score: score }
    }
    return best && best.score >= 70 ? best : null
  }

  function rawCollectModels(session) {
    var local = rawModelsFromSession(session)
    var video = rawBestModel(local, 'video')
    var audio = rawBestModel(local, 'audio')
    var companions = []
    if (video && !audio) {
      var audioCompanion = rawFindCompanion(session, video, 'audio')
      if (audioCompanion) {
        audio = audioCompanion.model
        companions.push(audioCompanion.session.id)
      }
    } else if (audio && !video) {
      var videoCompanion = rawFindCompanion(session, audio, 'video')
      if (videoCompanion) {
        video = videoCompanion.model
        companions.push(videoCompanion.session.id)
      }
    }
    if (!video && !audio) throw new Error('没有找到可保留的原始 fMP4 音视频轨道')
    return { video: video, audio: audio, companionSessionIds: companions }
  }

  function rawPatchMvhdBox(raw, duration, nextTrackId) {
    var bytes = raw.slice()
    var box = scanBoxesRange(bytes, 0, bytes.byteLength)[0]
    if (!box || box.type !== 'mvhd') throw new Error('mvhd 修复失败')
    var version = bytes[box.data]
    var fields = box.data + 4
    if (version === 1) rawWriteU64(bytes, fields + 20, duration)
    else rawWriteU32(bytes, fields + 12, duration)
    rawWriteU32(bytes, box.end - 4, nextTrackId)
    return bytes
  }

  function rawPatchTrakBox(raw, trackId, movieDuration, mediaDuration) {
    var bytes = raw.slice()
    var trak = scanBoxesRange(bytes, 0, bytes.byteLength)[0]
    if (!trak || trak.type !== 'trak') throw new Error('trak 修复失败')
    var tkhd = rawChild(bytes, trak, 'tkhd')
    var mdia = rawChild(bytes, trak, 'mdia')
    var mdhd = mdia ? rawChild(bytes, mdia, 'mdhd') : null
    if (!tkhd || !mdhd) throw new Error('trak 缺少 tkhd/mdhd')

    var version = bytes[tkhd.data]
    var fields = tkhd.data + 4
    rawWriteU32(bytes, version === 1 ? fields + 16 : fields + 8, trackId)
    if (version === 1) rawWriteU64(bytes, fields + 28, movieDuration)
    else rawWriteU32(bytes, fields + 20, movieDuration)

    version = bytes[mdhd.data]
    fields = mdhd.data + 4
    if (version === 1) rawWriteU64(bytes, fields + 20, mediaDuration)
    else rawWriteU32(bytes, fields + 12, mediaDuration)
    return bytes
  }

  function rawPatchTrexBox(raw, trackId) {
    var bytes = raw.slice()
    var trex = scanBoxesRange(bytes, 0, bytes.byteLength)[0]
    if (!trex || trex.type !== 'trex') throw new Error('trex 修复失败')
    rawWriteU32(bytes, trex.data + 4, trackId)
    return bytes
  }

  function rawBuildCombinedMoov(models, durationSec) {
    var base = models.video || models.audio
    var movieTimescale = base.init.movieTimescale || MOVIE_TIMESCALE
    var movieDuration = Math.max(1, Math.ceil(durationSec * movieTimescale))
    var writer = new BoxWriter(32768)
    writer.begin('moov')
    writer.data(rawPatchMvhdBox(base.init.mvhdBytes, movieDuration, models.video && models.audio ? 3 : 2))

    if (models.video) {
      writer.data(rawPatchTrakBox(
        models.video.init.trakBytes,
        1,
        movieDuration,
        Math.max(1, Math.ceil(models.video.durationSec * models.video.init.timescale))
      ))
    }
    if (models.audio) {
      writer.data(rawPatchTrakBox(
        models.audio.init.trakBytes,
        models.video ? 2 : 1,
        movieDuration,
        Math.max(1, Math.ceil(models.audio.durationSec * models.audio.init.timescale))
      ))
    }

    writer.begin('mvex')
    writer.beginFull('mehd', 1, 0)
    writer.u64(movieDuration)
    writer.end()
    if (models.video && models.video.init.trexBytes) writer.data(rawPatchTrexBox(models.video.init.trexBytes, 1))
    if (models.audio && models.audio.init.trexBytes) writer.data(rawPatchTrexBox(models.audio.init.trexBytes, models.video ? 2 : 1))
    writer.end()
    writer.end()
    return writer.result()
  }

  function rawPatchFragment(fragment, newTrackId, sequenceNumber, outputOffset) {
    var bytes = fragment.moofBytes.slice()
    var moof = scanBoxesRange(bytes, 0, bytes.byteLength)[0]
    if (!moof || moof.type !== 'moof') throw new Error('moof 修复失败')
    var mfhd = rawChild(bytes, moof, 'mfhd')
    if (mfhd) rawWriteU32(bytes, mfhd.data + 4, sequenceNumber)
    var trafs = rawChildren(bytes, moof, 'traf')
    for (var i = 0; i < trafs.length; i++) {
      var tfhd = rawChild(bytes, trafs[i], 'tfhd')
      if (!tfhd) continue
      var flags = rawFullFlags(bytes, tfhd)
      var p = tfhd.data + 4
      rawWriteU32(bytes, p, newTrackId)
      p += 4
      if (flags & 0x000001) rawWriteU64(bytes, p, outputOffset)
      var tfdt = rawChild(bytes, trafs[i], 'tfdt')
      if (tfdt) {
        if (bytes[tfdt.data] === 1) rawWriteU64(bytes, tfdt.data + 4, fragment.normalizedStart)
        else {
          if (fragment.normalizedStart > 0xffffffff) throw new Error('32 位 tfdt 超出可表示范围')
          rawWriteU32(bytes, tfdt.data + 4, fragment.normalizedStart)
        }
      }
    }
    return bytes
  }

  function rawPatchMdat(fragment) {
    var bytes = fragment.mdatBytes.slice()
    if (bytes.byteLength < 8) throw makeCaptureIntegrityError('mdat 过小')

    var size = readU32(bytes, 0)
    if (size === 1) {
      if (bytes.byteLength < 16) throw makeCaptureIntegrityError('扩展 mdat 头不完整')
      var declared64 = readU64(bytes, 8)
      if (declared64 !== bytes.byteLength) {
        throw makeCaptureIntegrityError(
          '捕获数据不完整:扩展 mdat 声明 ' + declared64 +
          ' 字节,实际只有 ' + bytes.byteLength + ' 字节'
        )
      }
      return bytes
    }

    if (size === 0) {
      if (!fragment.mdatZeroSize) {
        throw makeCaptureIntegrityError('size=0 mdat 的分片边界未经验证')
      }
      if (bytes.byteLength > 0xffffffff) {
        throw makeCaptureIntegrityError('单个 mdat 超过 4 GB,无法原位修复')
      }
      rawWriteU32(bytes, 0, bytes.byteLength)
      return bytes
    }

    if (size !== bytes.byteLength) {
      throw makeCaptureIntegrityError(
        '捕获数据不完整:mdat 声明 ' + size +
        ' 字节,实际只有 ' + bytes.byteLength + ' 字节'
      )
    }
    return bytes
  }

  function rawBuildMfra(trackEntries) {
    var writer = new BoxWriter(8192)
    writer.begin('mfra')
    for (var i = 0; i < trackEntries.length; i++) {
      var track = trackEntries[i]
      writer.beginFull('tfra', 1, 0)
      writer.u32(track.trackId)
      writer.u32(0) // traf/trun/sample number 均使用 1 字节。
      writer.u32(track.entries.length)
      for (var j = 0; j < track.entries.length; j++) {
        writer.u64(track.entries[j].time)
        writer.u64(track.entries[j].offset)
        writer.u8(1)
        writer.u8(1)
        writer.u8(1)
      }
      writer.end()
    }
    writer.beginFull('mfro', 0, 0)
    var sizePatch = writer.position
    writer.u32(0)
    writer.end()
    var mfraSize = writer.position
    writer.end()
    writer.patchU32(sizePatch, mfraSize)
    return writer.result()
  }

  function rawBuildIndexedFragmentedMp4(session) {
    var collected = rawCollectModels(session)
    var models = { video: collected.video, audio: collected.audio }
    var base = models.video || models.audio
    var durationSec = Math.max(models.video ? models.video.durationSec : 0, models.audio ? models.audio.durationSec : 0)
    if (!(durationSec > 0)) throw new Error('原始分片没有有效时长')

    var ftyp = base.init.ftypBytes || makeFtyp()
    var moov = rawBuildCombinedMoov(models, durationSec)
    var fragments = []
    if (models.video) {
      for (var v = 0; v < models.video.fragments.length; v++) {
        fragments.push({ model: models.video, fragment: models.video.fragments[v], trackId: 1, kind: 'video' })
      }
    }
    if (models.audio) {
      var audioTrackId = models.video ? 2 : 1
      for (var a = 0; a < models.audio.fragments.length; a++) {
        fragments.push({ model: models.audio, fragment: models.audio.fragments[a], trackId: audioTrackId, kind: 'audio' })
      }
    }
    fragments.sort(function (first, second) {
      var at = first.fragment.normalizedStart / first.model.init.timescale
      var bt = second.fragment.normalizedStart / second.model.init.timescale
      if (at !== bt) return at - bt
      return first.trackId - second.trackId
    })

    var parts = [ftyp, moov]
    var outputOffset = ftyp.byteLength + moov.byteLength
    var sequence = 1
    var indexByTrack = Object.create(null)
    var mediaBytes = 0

    for (var i = 0; i < fragments.length; i++) {
      var item = fragments[i]
      var fragment = item.fragment
      item.outputOffset = outputOffset
      var moofBytes = rawPatchFragment(fragment, item.trackId, sequence++, outputOffset)
      var mdatBytes = rawPatchMdat(fragment)
      parts.push(moofBytes, mdatBytes)
      if (!indexByTrack[item.trackId]) indexByTrack[item.trackId] = []
      if (item.kind === 'audio' || fragment.firstSync || !fragment.syncKnown) {
        indexByTrack[item.trackId].push({ time: fragment.normalizedStart, offset: outputOffset })
      }
      outputOffset += moofBytes.byteLength + mdatBytes.byteLength
      mediaBytes += Math.max(0, mdatBytes.byteLength - (readU32(mdatBytes, 0) === 1 ? 16 : 8))
    }

    if (models.video && (!indexByTrack[1] || !indexByTrack[1].length)) {
      indexByTrack[1] = []
      for (var fallbackIndex = 0; fallbackIndex < fragments.length; fallbackIndex++) {
        if (fragments[fallbackIndex].trackId !== 1) continue
        indexByTrack[1].push({
          time: fragments[fallbackIndex].fragment.normalizedStart,
          offset: fragments[fallbackIndex].outputOffset
        })
      }
    }

    var trackEntries = []
    Object.keys(indexByTrack).sort().forEach(function (id) {
      trackEntries.push({ trackId: parseInt(id, 10), entries: indexByTrack[id] })
    })
    var mfra = rawBuildMfra(trackEntries)
    parts.push(mfra)

    if (models.video && models.video.alternativeRuns) {
      addWarning(session, '检测到视频解码配置切换;原始分片模式仅保留连续时长最长的一套配置。')
    }
    if (models.audio && models.audio.alternativeRuns) {
      addWarning(session, '检测到音频解码配置切换;原始分片模式仅保留连续时长最长的一套配置。')
    }
    if (collected.companionSessionIds.length) {
      addWarning(session, '原始分片模式已关联 MediaSource 会话 #' + collected.companionSessionIds.join('、') + ' 的配对轨道。')
    }

    return {
      blob: new Blob(parts, { type: 'video/mp4' }),
      durationSec: durationSec,
      mediaBytes: mediaBytes,
      hasVideo: !!models.video,
      hasAudio: !!models.audio,
      videoFragments: models.video ? models.video.fragments.length : 0,
      audioFragments: models.audio ? models.audio.fragments.length : 0
    }
  }

  function validateIndexedFragmentedMp4(result) {
    if (!result || !result.blob || result.blob.size < 1024) return Promise.reject(new Error('原始分片合并结果为空'))
    if (result.mediaBytes && result.blob.size < result.mediaBytes) return Promise.reject(new Error('原始分片合并结果小于媒体负载'))
    return result.blob.arrayBuffer().then(function (buffer) {
      var bytes = new Uint8Array(buffer)
      var top = scanBoxesRange(bytes, 0, bytes.byteLength)
      var moov = false
      var mfra = false
      var moofCount = 0
      var mdatBytes = 0
      for (var i = 0; i < top.length; i++) {
        if (top[i].type === 'moov') moov = true
        if (top[i].type === 'mfra') mfra = true
        if (top[i].type === 'moof') moofCount++
        if (top[i].type === 'mdat') mdatBytes += top[i].end - top[i].data
      }
      if (!moov || !mfra || !moofCount || !mdatBytes) throw new Error('原始分片合并结果缺少 moov/moof/mdat/mfra')
      if (result.mediaBytes && mdatBytes < result.mediaBytes * 0.98) throw new Error('原始分片输出的 mdat 覆盖率异常')
      return result
    })
  }

  function preferRawFragmentMux() {
    var host = String(location.hostname || '').toLowerCase()
    return host === 'youtube.com' || host === 'www.youtube.com' || /(^|\.)youtube\.com$/.test(host) || host === 'youtu.be'
  }

  function sessionToJson(session) {
    return {
      version: 3,
      title: session.title,
      createdAt: session.createdAt,
      endedAt: session.endedAt,
      totalBytes: session.totalBytes,
      warnings: session.warnings.slice(),
      tracks: session.tracks.map(function (track) {
        return {
          id: track.id,
          initialMime: track.initialMime,
          currentMime: track.currentMime,
          kindHint: track.kindHint,
          totalBytes: track.totalBytes,
          appendCallCount: track.appendCallNo,
          capturedCallCount: track.appendCallNo,
          failedCallCount: track.failedCallCount,
          events: track.events.slice(),
          operations: track.operations.map(function (operation) {
            return {
              seq: operation.seq,
              callNo: operation.callNo,
              state: operation.state,
              byteLength: operation.byteLength,
              constructorName: operation.constructorName,
              sourceByteOffset: operation.sourceByteOffset,
              sourceByteLength: operation.sourceByteLength,
              sourceBufferByteLength: operation.sourceBufferByteLength,
              mime: operation.mime,
              mode: operation.mode,
              timestampOffset: operation.timestampOffset,
              timestampOffsetChanged: operation.timestampOffsetChanged,
              appendWindowStart: operation.appendWindowStart,
              appendWindowEnd: isFinite(operation.appendWindowEnd) ? operation.appendWindowEnd : null,
              bufferedBefore: operation.bufferedBefore,
              bufferedAfter: operation.bufferedAfter,
              startedAt: operation.startedAt,
              completedAt: operation.completedAt
            }
          })
        }
      })
    }
  }

  function downloadTimelineJson(session) {
    var json = JSON.stringify(sessionToJson(session), null, 2)
    downloadBlob(new Blob([json], { type: 'application/json;charset=utf-8' }), session.title + '-timeline.json')
  }

  function readU32(bytes, offset) {
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, false)
  }

  function readU64(bytes, offset) {
    var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
    var high = view.getUint32(offset, false)
    var low = view.getUint32(offset + 4, false)
    return high * 4294967296 + low
  }

  function readType(bytes, offset) {
    return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3])
  }

  function scanTopLevelBoxes(bytes) {
    var boxes = []
    var offset = 0
    while (offset + 8 <= bytes.byteLength) {
      var size = readU32(bytes, offset)
      var type = readType(bytes, offset + 4)
      var headerSize = 8
      if (size === 1) {
        if (offset + 16 > bytes.byteLength) return null
        size = readU64(bytes, offset + 8)
        headerSize = 16
      } else if (size === 0) {
        size = bytes.byteLength - offset
      }
      if (!isFinite(size) || size < headerSize || offset + size > bytes.byteLength) return null
      boxes.push({ type: type, start: offset, size: size, end: offset + size, headerSize: headerSize })
      offset += size
    }
    if (offset !== bytes.byteLength) return null
    return boxes
  }

  function fnv1a(bytes) {
    var hash = 2166136261
    for (var i = 0; i < bytes.length; i++) {
      hash ^= bytes[i]
      hash = Math.imul(hash, 16777619)
    }
    return (hash >>> 0).toString(16)
  }

  function prepareMp4Inputs(captureTrack) {
    var operations = captureTrack.operations.filter(function (operation) {
      return operation.state === 'ok' && operation.data && isMp4Mime(operation.mime || captureTrack.initialMime)
    })
    if (!operations.length) throw new Error('轨道 #' + captureTrack.id + ' 没有可用的 MP4 数据')

    var chunks = []
    var totalBytes = 0
    for (var i = 0; i < operations.length; i++) {
      var bytes = new Uint8Array(operations[i].data)
      chunks.push({ start: totalBytes, end: totalBytes + bytes.byteLength, bytes: bytes, operation: operations[i] })
      totalBytes += bytes.byteLength
    }
    if (totalBytes > MAX_REMUX_BYTES) {
      throw new Error('单轨数据达到 ' + formatBytes(totalBytes) + ',浏览器内存重封装风险过高,请下载原始轨道后用 FFmpeg 合并')
    }

    function locate(position) {
      var low = 0
      var high = chunks.length - 1
      while (low <= high) {
        var mid = (low + high) >> 1
        var chunk = chunks[mid]
        if (position < chunk.start) high = mid - 1
        else if (position >= chunk.end) low = mid + 1
        else return mid
      }
      return -1
    }

    function byteAt(position) {
      var index = locate(position)
      if (index < 0) return -1
      return chunks[index].bytes[position - chunks[index].start]
    }

    function u32At(position) {
      return byteAt(position) * 16777216 + byteAt(position + 1) * 65536 +
        byteAt(position + 2) * 256 + byteAt(position + 3)
    }

    function u64At(position) {
      return u32At(position) * 4294967296 + u32At(position + 4)
    }

    function typeAt(position) {
      return String.fromCharCode(byteAt(position), byteAt(position + 1), byteAt(position + 2), byteAt(position + 3))
    }

    function copyRange(start, end, target, offset) {
      var index = locate(start)
      if (index < 0) throw new Error('MP4 分片范围越界')
      while (start < end && index < chunks.length) {
        var chunk = chunks[index]
        var from = Math.max(start, chunk.start)
        var to = Math.min(end, chunk.end)
        if (to > from) {
          var part = chunk.bytes.subarray(from - chunk.start, to - chunk.start)
          target.set(part, offset)
          offset += part.byteLength
          start = to
        }
        index++
      }
      return offset
    }

    function concatRanges(ranges) {
      var total = 0
      for (var r = 0; r < ranges.length; r++) total += ranges[r].end - ranges[r].start
      var output = new Uint8Array(total)
      var offset = 0
      for (var j = 0; j < ranges.length; j++) {
        var rangeOffset = offset
        offset = copyRange(ranges[j].start, ranges[j].end, output, offset)
        // 输出缓冲不再保留 appendBuffer 边界;把 size=0 改写为显式长度,防止解析时吞掉后续 box。
        if (ranges[j].zeroSize && ranges[j].end - ranges[j].start <= 0xffffffff) {
          new DataView(output.buffer).setUint32(rangeOffset, ranges[j].end - ranges[j].start, false)
        }
      }
      return output.buffer
    }

    function hashRange(start, end) {
      var hash = 2166136261
      var index = locate(start)
      while (start < end && index >= 0 && index < chunks.length) {
        var chunk = chunks[index]
        var from = Math.max(start, chunk.start)
        var to = Math.min(end, chunk.end)
        for (var p = from - chunk.start; p < to - chunk.start; p++) {
          hash ^= chunk.bytes[p]
          hash = Math.imul(hash, 16777619)
        }
        start = to
        index++
      }
      return (hash >>> 0).toString(16)
    }

    function operationAt(position) {
      var index = locate(position)
      return index >= 0 ? chunks[index].operation : operations[0]
    }

    var segmentBoundaryTypes = {
      ftyp: 1, styp: 1, moov: 1, moof: 1, sidx: 1, ssix: 1,
      emsg: 1, prft: 1, mfra: 1, pdin: 1
    }

    function plausibleSegmentBoundary(position) {
      if (position < 0 || position + 8 > totalBytes) return false
      var size = u32At(position)
      var type = typeAt(position + 4)
      var headerSize = 8
      if (!segmentBoundaryTypes[type]) return false
      if (size === 1) {
        if (position + 16 > totalBytes) return false
        size = u64At(position + 8)
        headerSize = 16
      } else if (size === 0) {
        // 新片段通常不会以 size=0 的控制 box 开始;拒绝该候选可降低媒体负载误命中的概率。
        return false
      }
      if (!isFinite(size) || size < headerSize || position + size > totalBytes) return false

      // moof 必须至少含一个 traf。仅匹配到 ASCII “moof” 不能作为媒体负载边界。
      if (type === 'moof') {
        var childPos = position + headerSize
        var hasTraf = false
        while (childPos + 8 <= position + size) {
          var childSize = u32At(childPos)
          var childType = typeAt(childPos + 4)
          var childHeader = 8
          if (childSize === 1) {
            if (childPos + 16 > position + size) return false
            childSize = u64At(childPos + 8)
            childHeader = 16
          } else if (childSize === 0) {
            childSize = position + size - childPos
          }
          if (!isFinite(childSize) || childSize < childHeader || childPos + childSize > position + size) return false
          if (childType === 'traf') hasTraf = true
          childPos += childSize
        }
        if (childPos !== position + size || !hasTraf) return false
      }
      return true
    }

    function zeroSizedBoxEnd(offset) {
      var index = locate(offset)
      if (index < 0) return totalBytes

      // appendBuffer 可能只是一个 size=0 mdat 的中间块。不能把 mdat 强制截在当前 append 末尾;
      // 向后越过所有“续传块”,直到某个 append 的开头能验证为新的顶层媒体片段。
      for (var i = index + 1; i < chunks.length; i++) {
        var boundary = chunks[i].start
        if (plausibleSegmentBoundary(boundary)) return boundary
      }
      return totalBytes
    }

    function scanChunkBoxes() {
      var boxes = []
      var offset = 0
      while (offset + 8 <= totalBytes) {
        var size = u32At(offset)
        var originalSize = size
        var type = typeAt(offset + 4)
        var headerSize = 8
        if (size === 1) {
          if (offset + 16 > totalBytes) return null
          size = u64At(offset + 8)
          headerSize = 16
        } else if (size === 0) {
          var zeroEnd = zeroSizedBoxEnd(offset)
          size = zeroEnd - offset
        }
        if (!isFinite(size) || size < headerSize || offset + size > totalBytes) return null
        boxes.push({ type: type, start: offset, end: offset + size, size: size, headerSize: headerSize, zeroSize: originalSize === 0 })
        offset += size
      }
      return offset === totalBytes ? boxes : null
    }

    var boxes = scanChunkBoxes()
    if (!boxes) {
      return [{
        buffer: concatRanges([{ start: 0, end: totalBytes }]),
        moofMeta: [],
        defaultMeta: operations[0],
        warnings: ['无法完整识别顶层 MP4 box;将按单一初始化段解析。'],
        periodId: String(captureTrack.id) + ':1',
        periodOrder: 1,
        initSignature: 'unknown',
        mediaPayloadBytes: 0
      }]
    }

    var periods = []
    var pendingFtyp = null
    var current = null
    var periodSerial = 0
    var warnings = []

    function newPeriod(moov, operation) {
      periodSerial++
      current = {
        ranges: [],
        moofMeta: [],
        defaultMeta: operation || operations[0],
        hasMedia: false,
        hasMdat: false,
        mediaPayloadBytes: 0,
        periodId: String(captureTrack.id) + ':' + periodSerial,
        periodOrder: periodSerial,
        initSignature: hashRange(moov.start, moov.end)
      }
      if (pendingFtyp) current.ranges.push({ start: pendingFtyp.start, end: pendingFtyp.end, zeroSize: pendingFtyp.zeroSize })
      current.ranges.push({ start: moov.start, end: moov.end, zeroSize: moov.zeroSize })
    }

    function finishPeriod() {
      if (!current || !current.hasMedia || !current.hasMdat) {
        current = null
        return
      }
      periods.push({
        buffer: concatRanges(current.ranges),
        moofMeta: current.moofMeta,
        defaultMeta: current.defaultMeta,
        warnings: [],
        periodId: current.periodId,
        periodOrder: current.periodOrder,
        initSignature: current.initSignature,
        mediaPayloadBytes: current.mediaPayloadBytes
      })
      current = null
    }

    for (var b = 0; b < boxes.length; b++) {
      var box = boxes[b]
      var operation = operationAt(box.start)

      if (box.type === 'ftyp') {
        pendingFtyp = box
        continue
      }
      if (box.type === 'moov') {
        if (current && current.hasMedia) finishPeriod()
        newPeriod(box, operation)
        continue
      }
      if (!current) {
        if (box.type === 'moof' || box.type === 'mdat') {
          warnings.push('发现缺少初始化段的媒体数据,已跳过;请从页面刷新后开始捕获。')
        }
        continue
      }

      current.ranges.push({ start: box.start, end: box.end, zeroSize: box.zeroSize })
      if (box.type === 'moof') {
        current.hasMedia = true
        current.moofMeta.push(operation)
        if (!current.defaultMeta) current.defaultMeta = operation
      } else if (box.type === 'mdat') {
        current.hasMdat = true
        current.mediaPayloadBytes += Math.max(0, box.size - box.headerSize)
      }
    }
    finishPeriod()

    if (!periods.length) {
      return [{
        buffer: concatRanges([{ start: 0, end: totalBytes }]),
        moofMeta: [],
        defaultMeta: operations[0],
        warnings: ['未能拆分初始化段;将按原始字节解析。'].concat(warnings),
        periodId: String(captureTrack.id) + ':1',
        periodOrder: 1,
        initSignature: 'unknown',
        mediaPayloadBytes: 0
      }]
    }
    if (periods.length > 1) warnings.push('检测到 ' + periods.length + ' 个初始化周期;将按编码配置和时序识别插入段。')
    for (var q = 0; q < periods.length; q++) periods[q].warnings = warnings.slice()
    return periods
  }

  /*
   * MiniMP4:仅实现本脚本实际需要的 ISO-BMFF/fMP4 子集。
   * 支持:moov/trak/mdhd/hdlr/stsd/trex 与 moof/traf/tfhd/tfdt/trun/mdat。
   * 不包含 MP4Box 的分段器、item metadata、字幕、文件写入器等无关模块。
   */
  function miniParseMp4(prepared) {
    var bytes = new Uint8Array(prepared.buffer)
    var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)

    function u8(p) { return view.getUint8(p) }
    function u16(p) { return view.getUint16(p, false) }
    function i32(p) { return view.getInt32(p, false) }
    function u32(p) { return view.getUint32(p, false) }
    function u64(p) { return u32(p) * 4294967296 + u32(p + 4) }
    function typeAt(p) { return String.fromCharCode(bytes[p], bytes[p + 1], bytes[p + 2], bytes[p + 3]) }
    function hex2(n) { return (n < 16 ? '0' : '') + n.toString(16) }

    function boxes(start, end) {
      var out = []
      var p = start
      while (p + 8 <= end) {
        var size = u32(p)
        var type = typeAt(p + 4)
        var header = 8
        if (size === 1) {
          if (p + 16 > end) break
          size = u64(p + 8)
          header = 16
        } else if (size === 0) {
          size = end - p
        }
        if (!isFinite(size) || size < header || p + size > end) break
        out.push({ type: type, start: p, end: p + size, size: size, header: header, data: p + header })
        p += size
      }
      return out
    }

    function child(parent, type) {
      var list = boxes(parent.data, parent.end)
      for (var i = 0; i < list.length; i++) if (list[i].type === type) return list[i]
      return null
    }

    function children(parent, type) {
      var list = boxes(parent.data, parent.end)
      if (!type) return list
      return list.filter(function (box) { return box.type === type })
    }

    function flags(box) {
      return (u8(box.data + 1) << 16) | (u8(box.data + 2) << 8) | u8(box.data + 3)
    }

    function lang(value) {
      var a = ((value >> 10) & 31) + 96
      var b = ((value >> 5) & 31) + 96
      var c = (value & 31) + 96
      return a < 97 || b < 97 || c < 97 ? 'und' : String.fromCharCode(a, b, c)
    }

    function slice(start, end) { return bytes.subarray(start, end) }

    function hasFourCC(raw, value) {
      var a = value.charCodeAt(0), b = value.charCodeAt(1), c = value.charCodeAt(2), d = value.charCodeAt(3)
      for (var i = 4; i + 3 < raw.length; i++) {
        if (raw[i] === a && raw[i + 1] === b && raw[i + 2] === c && raw[i + 3] === d) return true
      }
      return false
    }

    function codecOf(entry, raw) {
      var type = entry.type
      if ((type === 'avc1' || type === 'avc3') && raw.length >= 94) {
        for (var p = 86; p + 12 <= raw.length;) {
          var size = raw[p] * 16777216 + (raw[p + 1] << 16) + (raw[p + 2] << 8) + raw[p + 3]
          var sub = String.fromCharCode(raw[p + 4], raw[p + 5], raw[p + 6], raw[p + 7])
          if (size < 8 || p + size > raw.length) break
          if (sub === 'avcC' && size >= 12) return type + '.' + hex2(raw[p + 9]) + hex2(raw[p + 10]) + hex2(raw[p + 11])
          p += size
        }
      }
      return type
    }

    function findSampleEntryChild(raw, wanted, start) {
      var p = start || 8
      while (p + 8 <= raw.length) {
        var size = raw[p] * 16777216 + raw[p + 1] * 65536 + raw[p + 2] * 256 + raw[p + 3]
        var type = String.fromCharCode(raw[p + 4], raw[p + 5], raw[p + 6], raw[p + 7])
        if (size < 8 || p + size > raw.length) break
        if (type === wanted) return { start: p, data: p + 8, end: p + size }
        p += size
      }
      return null
    }

    function nalLengthSize(description) {
      if (!description || !description.raw) return 4
      var raw = description.raw
      if (description.type === 'avc1' || description.type === 'avc3') {
        var avcc = findSampleEntryChild(raw, 'avcC', 86)
        if (avcc && avcc.data + 5 <= avcc.end) return (raw[avcc.data + 4] & 3) + 1
      }
      if (description.type === 'hvc1' || description.type === 'hev1') {
        var hvcc = findSampleEntryChild(raw, 'hvcC', 86)
        if (hvcc && hvcc.data + 22 <= hvcc.end) return (raw[hvcc.data + 21] & 3) + 1
      }
      return 4
    }

    function randomAccessFromSample(data, description) {
      if (!data || !description) return null
      var type = String(description.type || '').toLowerCase()
      var avc = type === 'avc1' || type === 'avc3'
      var hevc = type === 'hvc1' || type === 'hev1'
      if (!avc && !hevc) return null
      var lengthSize = nalLengthSize(description)
      var p = 0
      var sawVcl = false
      while (p + lengthSize <= data.length) {
        var size = 0
        for (var i = 0; i < lengthSize; i++) size = size * 256 + data[p + i]
        p += lengthSize
        if (!size || p + size > data.length) break
        var nalType = avc ? (data[p] & 31) : ((data[p] >> 1) & 63)
        if (avc) {
          if (nalType === 5) return true
          if (nalType >= 1 && nalType <= 5) sawVcl = true
        } else {
          if (nalType >= 16 && nalType <= 21) return true
          if (nalType <= 31) sawVcl = true
        }
        p += size
      }
      return sawVcl ? false : null
    }

    function parseStsd(stsd, kind) {
      if (!stsd || stsd.data + 8 > stsd.end) return []
      var count = u32(stsd.data + 4)
      var p = stsd.data + 8
      var entries = []
      for (var i = 0; i < count && p + 8 <= stsd.end; i++) {
        var size = u32(p)
        if (size < 16 || p + size > stsd.end) break
        var entry = { type: typeAt(p + 4), start: p, end: p + size, size: size }
        var raw = slice(p, p + size)
        var info = {
          type: entry.type,
          raw: raw,
          encrypted: entry.type === 'encv' || entry.type === 'enca' || hasFourCC(raw, 'sinf'),
          codec: codecOf(entry, raw),
          width: 0,
          height: 0,
          sampleRate: 0,
          channelCount: 0,
          sampleSize: 16
        }
        if (kind === 'video' && size >= 36) {
          info.width = u16(p + 32)
          info.height = u16(p + 34)
        } else if (kind === 'audio' && size >= 36) {
          info.channelCount = u16(p + 24)
          info.sampleSize = u16(p + 26) || 16
          info.sampleRate = Math.floor(u32(p + 32) / 65536)
        }
        entries.push(info)
        p += size
      }
      return entries
    }

    var top = boxes(0, bytes.length)
    var moov = null
    for (var ti = 0; ti < top.length; ti++) if (top[ti].type === 'moov') { moov = top[ti]; break }
    if (!moov) throw new Error('未找到 moov 初始化段')

    var trexMap = Object.create(null)
    var mvex = child(moov, 'mvex')
    if (mvex) {
      var trexList = children(mvex, 'trex')
      for (var tx = 0; tx < trexList.length; tx++) {
        var trex = trexList[tx]
        var tp = trex.data + 4
        if (tp + 20 > trex.end) continue
        trexMap[u32(tp)] = {
          descriptionIndex: u32(tp + 4) || 1,
          duration: u32(tp + 8),
          size: u32(tp + 12),
          flags: u32(tp + 16)
        }
      }
    }

    var trackMap = Object.create(null)
    var trackList = []
    var traks = children(moov, 'trak')
    for (var tr = 0; tr < traks.length; tr++) {
      var trak = traks[tr]
      var tkhd = child(trak, 'tkhd')
      var mdia = child(trak, 'mdia')
      if (!tkhd || !mdia) continue
      var tkFields = tkhd.data + 4
      var trackId = u8(tkhd.data) === 1 ? u32(tkFields + 16) : u32(tkFields + 8)
      var mdhd = child(mdia, 'mdhd')
      var hdlr = child(mdia, 'hdlr')
      var minf = child(mdia, 'minf')
      var stbl = minf ? child(minf, 'stbl') : null
      var stsd = stbl ? child(stbl, 'stsd') : null
      if (!trackId || !mdhd || !hdlr || !stsd) continue

      var handler = typeAt(hdlr.data + 8)
      var kind = handler === 'vide' ? 'video' : (handler === 'soun' ? 'audio' : 'other')
      if (kind === 'other') continue
      var mdFields = mdhd.data + 4
      var version = u8(mdhd.data)
      var timescale = version === 1 ? u32(mdFields + 16) : u32(mdFields + 8)
      var langOffset = version === 1 ? mdFields + 28 : mdFields + 16
      var descriptions = parseStsd(stsd, kind)
      if (!descriptions.length) continue
      for (var ee = 0; ee < descriptions.length; ee++) {
        if (descriptions[ee].encrypted) throw new Error('检测到加密样本条目 ' + descriptions[ee].type + ',脚本无法生成可播放的明文 MP4')
      }
      var first = descriptions[0]
      var track = {
        id: trackId,
        kind: kind,
        timescale: timescale || (kind === 'audio' ? 48000 : 90000),
        language: langOffset + 2 <= mdhd.end ? lang(u16(langOffset)) : 'und',
        descriptions: descriptions,
        codec: first.codec || first.type,
        width: kind === 'video' ? (first.width || Math.round(u32(tkhd.end - 8) / 65536)) : 0,
        height: kind === 'video' ? (first.height || Math.round(u32(tkhd.end - 4) / 65536)) : 0,
        sampleRate: kind === 'audio' ? first.sampleRate : 0,
        channelCount: kind === 'audio' ? first.channelCount : 0,
        sampleSize: kind === 'audio' ? first.sampleSize : 16,
        samples: [],
        nextDts: 0
      }
      trackMap[trackId] = track
      trackList.push(track)
    }
    if (!trackList.length) throw new Error('moov 中没有可识别的音视频轨道')

    function nextMdat(index) {
      for (var i = index + 1; i < top.length; i++) {
        if (top[i].type === 'mdat') return top[i]
        if (top[i].type === 'moof' || top[i].type === 'moov') break
      }
      return null
    }

    var moofNumber = 0
    for (var bi = 0; bi < top.length; bi++) {
      var moof = top[bi]
      if (moof.type !== 'moof') continue
      moofNumber++
      var mdat = nextMdat(bi)
      var trafs = children(moof, 'traf')
      var previousRunEnd = mdat ? mdat.data : moof.end

      for (var fi = 0; fi < trafs.length; fi++) {
        var traf = trafs[fi]
        var tfhd = child(traf, 'tfhd')
        if (!tfhd) continue
        var tfFlags = flags(tfhd)
        var fp = tfhd.data + 4
        var id = u32(fp); fp += 4
        var trackInfo = trackMap[id]
        if (!trackInfo && trackList.length === 1) {
          // 某些播放器重建初始化段后会改变 fragment track_ID;单轨 SourceBuffer 可安全映射到唯一轨道。
          trackInfo = trackList[0]
          trackMap[id] = trackInfo
        }
        if (!trackInfo) continue
        var defaults = trexMap[id] || trexMap[trackInfo.id] || { descriptionIndex: 1, duration: 0, size: 0, flags: 0 }
        var baseDataOffset = null
        var descriptionIndex = defaults.descriptionIndex || 1
        var defaultDuration = defaults.duration || 0
        var defaultSize = defaults.size || 0
        var defaultFlags = defaults.flags || 0
        if (tfFlags & 0x000001) { baseDataOffset = u64(fp); fp += 8 }
        if (tfFlags & 0x000002) { descriptionIndex = u32(fp) || 1; fp += 4 }
        if (tfFlags & 0x000008) { defaultDuration = u32(fp); fp += 4 }
        if (tfFlags & 0x000010) { defaultSize = u32(fp); fp += 4 }
        if (tfFlags & 0x000020) { defaultFlags = u32(fp); fp += 4 }

        var tfdt = child(traf, 'tfdt')
        var decodeTime = trackInfo.nextDts || 0
        if (tfdt) decodeTime = u8(tfdt.data) === 1 ? u64(tfdt.data + 4) : u32(tfdt.data + 4)
        var base = baseDataOffset !== null ? baseDataOffset : moof.start
        if (tfFlags & 0x020000) base = moof.start
        var runs = children(traf, 'trun')
        var runEnd = previousRunEnd

        for (var ri = 0; ri < runs.length; ri++) {
          var trun = runs[ri]
          var runVersion = u8(trun.data)
          var runFlags = flags(trun)
          var rp = trun.data + 4
          var count = u32(rp); rp += 4
          var dataOffset = null
          var firstFlags = null
          if (runFlags & 0x000001) { dataOffset = i32(rp); rp += 4 }
          if (runFlags & 0x000004) { firstFlags = u32(rp); rp += 4 }
          var dataPos = dataOffset !== null ? base + dataOffset : runEnd
          if (mdat && (dataPos < mdat.data || dataPos >= mdat.end)) dataPos = ri === 0 ? mdat.data : runEnd

          for (var si = 0; si < count; si++) {
            var duration = runFlags & 0x000100 ? u32(rp) : defaultDuration
            if (runFlags & 0x000100) rp += 4
            var size = runFlags & 0x000200 ? u32(rp) : defaultSize
            if (runFlags & 0x000200) rp += 4
            var sampleFlags = runFlags & 0x000400 ? u32(rp) : (si === 0 && firstFlags !== null ? firstFlags : defaultFlags)
            if (runFlags & 0x000400) rp += 4
            var cto = 0
            if (runFlags & 0x000800) { cto = runVersion === 1 ? i32(rp) : u32(rp); rp += 4 }
            if (!duration) duration = 1
            if (!size) throw new Error('轨道 #' + id + ' 的样本大小为 0')
            if (dataPos < 0 || dataPos + size > bytes.length) throw new Error('样本数据越界')
            var desc = trackInfo.descriptions[Math.max(0, descriptionIndex - 1)] || trackInfo.descriptions[0]
            var sampleData = slice(dataPos, dataPos + size)
            var flagsKnown = !!(runFlags & 0x000400) || (si === 0 && firstFlags !== null) || defaultFlags !== 0
            var flagSync = (sampleFlags & 0x00010000) === 0
            var payloadSync = trackInfo.kind === 'video' ? randomAccessFromSample(sampleData, desc) : null
            var syncKnown = trackInfo.kind === 'audio' || flagsKnown || payloadSync !== null
            var isSync = trackInfo.kind === 'audio' || (flagsKnown ? flagSync : (payloadSync === null ? flagSync : payloadSync))
            trackInfo.samples.push({
              dts: decodeTime,
              cts: decodeTime + cto,
              duration: duration,
              timescale: trackInfo.timescale,
              isSync: isSync,
              syncKnown: syncKnown,
              description: desc.raw,
              data: sampleData,
              moofNumber: moofNumber
            })
            dataPos += size
            decodeTime += duration
          }
          runEnd = dataPos
        }
        previousRunEnd = Math.max(previousRunEnd, runEnd)
        trackInfo.nextDts = decodeTime
      }
    }

    var output = []
    for (var oi = 0; oi < trackList.length; oi++) if (trackList[oi].samples.length) output.push(trackList[oi])
    if (!output.length) throw new Error('没有解析到 fMP4 媒体样本')
    return output
  }

  function parseMp4Buffer(prepared) {
    return Promise.resolve({ tracks: miniParseMp4(prepared), prepared: prepared })
  }

  function serializeBox(box) {
    if (box instanceof Uint8Array) return box.slice()
    if (box instanceof ArrayBuffer) return new Uint8Array(box.slice(0))
    if (ArrayBuffer.isView(box)) return new Uint8Array(box.buffer, box.byteOffset, box.byteLength).slice()
    throw new Error('无效的 MP4 样本描述')
  }

  function sampleMeta(sample, prepared) {
    return sample && sample.moofNumber ? (prepared.moofMeta[sample.moofNumber - 1] || prepared.defaultMeta) : prepared.defaultMeta
  }

  function extractParsedTracks(parsed, captureTrack) {
    var result = []
    for (var t = 0; t < parsed.tracks.length; t++) {
      var source = parsed.tracks[t]
      var allSamples = []

      for (var i = 0; i < source.samples.length; i++) {
        var sample = source.samples[i]
        var meta = sampleMeta(sample, parsed.prepared)
        allSamples.push({
          sourceDts: sample.dts / source.timescale,
          sourceCts: sample.cts / source.timescale,
          duration: Math.max(sample.duration / source.timescale, 1 / source.timescale),
          isSync: sample.isSync !== false,
          syncKnown: sample.syncKnown !== false,
          description: sample.description,
          descriptionKey: descriptionKey(sample.description),
          data: sample.data,
          appendSeq: meta ? meta.seq : 0,
          meta: meta || parsed.prepared.defaultMeta,
          moofNumber: sample.moofNumber || 0,
          periodId: parsed.prepared.periodId,
          periodOrder: parsed.prepared.periodOrder,
          initSignature: parsed.prepared.initSignature
        })
      }

      applyMseTimeline(allSamples, source.kind)
      allSamples = allSamples.filter(function (sample) { return sample.keep !== false })
      var byDescription = Object.create(null)
      for (var si = 0; si < allSamples.length; si++) {
        var key = allSamples[si].descriptionKey
        if (!byDescription[key]) byDescription[key] = []
        byDescription[key].push(allSamples[si])
      }

      for (var descKey in byDescription) {
        var samples = deduplicateSamples(byDescription[descKey])
        if (!samples.length) continue
        var fourcc = readType(samples[0].description, 4).toLowerCase()
        var family = /^(?:avc1|avc3)$/.test(fourcc) ? 'avc' : (/^(?:hvc1|hev1)$/.test(fourcc) ? 'hevc' : fourcc)
        var decoderSignature = decoderConfigurationKey(samples[0].description, source.kind, {
          family: family,
          timescale: source.timescale,
          width: source.width,
          height: source.height,
          sampleRate: source.sampleRate,
          channelCount: source.channelCount
        })
        var trackSignature = [source.kind, decoderSignature].join('|')
        for (var sk = 0; sk < samples.length; sk++) {
          samples[sk].trackSignature = trackSignature
          samples[sk].exactSignature = trackSignature
          samples[sk].decoderSignature = decoderSignature
        }
        result.push({
          kind: source.kind,
          codec: source.codec || fourcc,
          timescale: source.timescale,
          width: source.width || 0,
          height: source.height || 0,
          sampleRate: source.sampleRate || 0,
          channelCount: source.channelCount || 0,
          sampleSize: source.sampleSize || 16,
          language: source.language || 'und',
          captureTrackId: captureTrack.id,
          periodId: parsed.prepared.periodId,
          periodOrder: parsed.prepared.periodOrder,
          initSignature: parsed.prepared.initSignature,
          trackSignature: trackSignature,
          exactSignature: trackSignature,
          decoderSignature: decoderSignature,
          descriptionKey: descKey,
          compatibilityKey: trackSignature,
          samples: samples
        })
      }
    }
    return result
  }

  function subtractBufferedRanges(after, before) {
    var output = []
    after = after || []
    before = before || []
    for (var i = 0; i < after.length; i++) {
      var pieces = [{ start: after[i][0], end: after[i][1] }]
      for (var j = 0; j < before.length; j++) {
        var next = []
        for (var p = 0; p < pieces.length; p++) {
          var piece = pieces[p]
          var bs = before[j][0]
          var be = before[j][1]
          if (be <= piece.start + EPS || bs >= piece.end - EPS) {
            next.push(piece)
            continue
          }
          if (bs > piece.start + EPS) next.push({ start: piece.start, end: Math.min(bs, piece.end) })
          if (be < piece.end - EPS) next.push({ start: Math.max(be, piece.start), end: piece.end })
        }
        pieces = next
      }
      for (var k = 0; k < pieces.length; k++) {
        if (pieces[k].end > pieces[k].start + 0.005) output.push(pieces[k])
      }
    }
    return output
  }

  function inferBufferedShift(meta, sourcePresentationStart, sourcePresentationEnd, predictedStart) {
    if (!meta) return null
    var expected = Math.max(0.001, sourcePresentationEnd - sourcePresentationStart)
    var additions = subtractBufferedRanges(meta.bufferedAfter, meta.bufferedBefore)
    var best = null
    var bestScore = Infinity
    for (var i = 0; i < additions.length; i++) {
      var length = additions[i].end - additions[i].start
      // 只有新增范围覆盖了大部分片段时才用它校正,避免重叠追加造成误判。
      if (length < Math.min(expected * 0.55, Math.max(0.08, expected - 0.35))) continue
      var score = Math.abs(length - expected) + Math.abs(additions[i].start - predictedStart) * 0.15
      if (score < bestScore) {
        best = additions[i]
        bestScore = score
      }
    }
    if (!best) return null
    return best.start - sourcePresentationStart
  }

  function applyMseTimeline(samples, kind) {
    var groups = []
    var groupMap = Object.create(null)

    for (var i = 0; i < samples.length; i++) {
      var sample = samples[i]
      // bufferedBefore/bufferedAfter 属于一次 appendBuffer,而不是单个 moof。
      // 同一次 append 中可能包含多个 moof;按 appendSeq 分组可避免把每个 moof 都校正到同一时间起点。
      var key = sample.meta && sample.meta.seq ? 'a' + sample.meta.seq : 'm' + (sample.moofNumber || i)
      var group = groupMap[key]
      if (!group) {
        group = {
          key: key,
          meta: sample.meta || {},
          samples: [],
          order: sample.meta ? sample.meta.seq : i
        }
        groupMap[key] = group
        groups.push(group)
      }
      group.samples.push(sample)
    }

    groups.sort(function (a, b) { return a.order - b.order })
    var runningEnd = null

    for (var g = 0; g < groups.length; g++) {
      var current = groups[g]
      var minDts = Infinity
      var minCts = Infinity
      var maxPresentationEnd = -Infinity
      for (var j = 0; j < current.samples.length; j++) {
        minDts = Math.min(minDts, current.samples[j].sourceDts)
        minCts = Math.min(minCts, current.samples[j].sourceCts)
        maxPresentationEnd = Math.max(maxPresentationEnd, current.samples[j].sourceCts + current.samples[j].duration)
      }

      var meta = current.meta || {}
      var mode = meta.mode === 'sequence' ? 'sequence' : 'segments'
      var offset = safeNumber(meta.timestampOffset, 0)
      var shift

      if (mode === 'sequence') {
        var groupStart
        if (runningEnd === null || meta.timestampOffsetChanged) groupStart = offset
        else groupStart = runningEnd
        shift = groupStart - minDts
      } else {
        shift = offset
      }

      var predictedPresentationStart = minCts + shift
      var bufferedShift = inferBufferedShift(meta, minCts, maxPresentationEnd, predictedPresentationStart)
      if (bufferedShift !== null && Math.abs(bufferedShift - shift) < Math.max(30, maxPresentationEnd - minCts + 5)) {
        shift = bufferedShift
      }

      var groupEnd = -Infinity
      for (var k = 0; k < current.samples.length; k++) {
        var item = current.samples[k]
        item.dts = item.sourceDts + shift
        item.cts = item.sourceCts + shift
        var ptsEnd = item.cts + item.duration
        var windowStart = safeNumber(meta.appendWindowStart, 0)
        var windowEnd = safeNumber(meta.appendWindowEnd, Infinity)
        item.keep = ptsEnd > windowStart + EPS && item.cts < windowEnd - EPS
        groupEnd = Math.max(groupEnd, item.dts + item.duration)
      }

      if (kind === 'video') keepDecodeStart(current.samples)
      if (mode === 'sequence') runningEnd = groupEnd
      else runningEnd = runningEnd === null ? groupEnd : Math.max(runningEnd, groupEnd)
    }
  }

  function keepDecodeStart(samples) {
    var firstKept = -1
    for (var i = 0; i < samples.length; i++) {
      if (samples[i].keep !== false) {
        firstKept = i
        break
      }
    }
    if (firstKept <= 0 || samples[firstKept].isSync) return
    for (var j = firstKept - 1; j >= 0; j--) {
      samples[j].keep = true
      if (samples[j].isSync) break
    }
  }

  function quickSampleDataKey(sample) {
    if (sample.__payloadKey) return sample.__payloadKey
    var data = sample.data || new Uint8Array(0)
    var hash = 2166136261
    // 仅在 DTS 冲突或周期去重时调用;完整哈希可避免不同画面恰好拥有相同头尾字节。
    for (var i = 0; i < data.byteLength; i++) {
      hash ^= data[i]
      hash = Math.imul(hash, 16777619)
    }
    sample.__payloadKey = data.byteLength + ':' + (hash >>> 0).toString(16)
    return sample.__payloadKey
  }

  function samplesAreExactDuplicates(a, b) {
    if (!a || !b) return false
    if (Math.abs(a.dts - b.dts) >= 1e-6) return false
    if (Math.abs(a.duration - b.duration) >= 1e-6) return false
    if ((a.descriptionKey || '') !== (b.descriptionKey || '')) return false
    return quickSampleDataKey(a) === quickSampleDataKey(b)
  }

  function deduplicateSamples(samples) {
    samples.sort(function (a, b) {
      if (a.dts !== b.dts) return a.dts - b.dts
      if (a.appendSeq !== b.appendSeq) return a.appendSeq - b.appendSeq
      return (a.moofNumber || 0) - (b.moofNumber || 0)
    })

    var output = []
    for (var i = 0; i < samples.length; i++) {
      var sample = samples[i]
      var duplicateIndex = -1
      // 同一 DTS 可能是清晰度切换、初始化段重建或时间戳复位,不能仅凭 DTS 删除。
      // 只把描述、时长和媒体负载都一致的样本视为真正重复。
      for (var j = output.length - 1; j >= 0; j--) {
        if (Math.abs(output[j].dts - sample.dts) >= 1e-6) break
        if (samplesAreExactDuplicates(output[j], sample)) {
          duplicateIndex = j
          break
        }
      }
      if (duplicateIndex >= 0) {
        if (sample.appendSeq >= output[duplicateIndex].appendSeq) output[duplicateIndex] = sample
      } else {
        output.push(sample)
      }
    }
    return output
  }

  function cloneParsedTracks(parsedTracks) {
    return parsedTracks.map(function (track) {
      var copy = {}
      for (var key in track) copy[key] = track[key]
      copy.samples = track.samples.slice()
      return copy
    })
  }

  function mediaSampleStats(parsedTracks, kind) {
    var count = 0
    var bytes = 0
    var sync = 0
    var start = Infinity
    var end = -Infinity
    for (var i = 0; i < parsedTracks.length; i++) {
      if (parsedTracks[i].kind !== kind) continue
      for (var j = 0; j < parsedTracks[i].samples.length; j++) {
        var sample = parsedTracks[i].samples[j]
        count++
        bytes += sample.data ? sample.data.byteLength : 0
        if (sample.isSync) sync++
        start = Math.min(start, sample.cts)
        end = Math.max(end, sample.cts + sample.duration)
      }
    }
    return { count: count, bytes: bytes, sync: sync, start: start, end: end, span: isFinite(start) && isFinite(end) ? end - start : 0 }
  }

  function mergeSampleIntervals(samples) {
    var sorted = samples.slice().sort(function (a, b) { return a.cts - b.cts })
    var intervals = []
    for (var i = 0; i < sorted.length; i++) {
      var start = sorted[i].cts
      var end = sorted[i].cts + sorted[i].duration
      if (!isFinite(start) || !isFinite(end) || end <= start) continue
      var last = intervals.length ? intervals[intervals.length - 1] : null
      var tolerance = Math.max(0.12, sorted[i].duration * 3)
      if (last && start <= last.end + tolerance) last.end = Math.max(last.end, end)
      else intervals.push({ start: start, end: end })
    }
    return intervals
  }

  function overlapWithIntervals(sample, intervals) {
    var start = sample.cts
    var end = sample.cts + sample.duration
    for (var i = 0; i < intervals.length; i++) {
      if (end > intervals[i].start - 0.08 && start < intervals[i].end + 0.08) return true
    }
    return false
  }

  function sampleInterval(samples) {
    var start = Infinity
    var end = -Infinity
    for (var i = 0; i < samples.length; i++) {
      start = Math.min(start, samples[i].cts)
      end = Math.max(end, samples[i].cts + samples[i].duration)
    }
    return { start: start, end: end, span: isFinite(start) && isFinite(end) ? end - start : 0 }
  }

  function shiftSamples(samples, shift) {
    if (!shift) return
    for (var i = 0; i < samples.length; i++) {
      samples[i].dts += shift
      samples[i].cts += shift
      samples[i].timelineFallback = true
    }
  }

  function normalizeStrictGroup(group) {
    var buckets = Object.create(null)
    for (var i = 0; i < group.tracks.length; i++) {
      var track = group.tracks[i]
      var bucketKey = [track.captureSessionId || 0, track.captureTrackId || 0, track.periodId || ('p' + i)].join(':')
      if (!buckets[bucketKey]) {
        buckets[bucketKey] = {
          key: bucketKey,
          order: Infinity,
          periodOrder: track.periodOrder || 0,
          samples: [],
          base: track
        }
      }
      var bucket = buckets[bucketKey]
      bucket.samples = bucket.samples.concat(track.samples)
      for (var s = 0; s < track.samples.length; s++) bucket.order = Math.min(bucket.order, track.samples[s].appendSeq || Infinity)
    }

    var periods = Object.keys(buckets).map(function (key) {
      var bucket = buckets[key]
      bucket.samples = deduplicateSamples(bucket.samples)
      if (!isFinite(bucket.order)) bucket.order = bucket.periodOrder || 0
      return bucket
    }).filter(function (bucket) { return bucket.samples.length })

    periods.sort(function (a, b) {
      if (a.order !== b.order) return a.order - b.order
      return a.periodOrder - b.periodOrder
    })

    var normalized = []
    var runningEnd = null
    var previousPeriodOrder = null
    var repaired = 0
    var skippedDuplicates = 0

    for (var p = 0; p < periods.length; p++) {
      var period = periods[p]
      var interval = sampleInterval(period.samples)
      if (!isFinite(interval.start) || !isFinite(interval.end)) continue
      var originalStart = interval.start
      var originalGap = runningEnd === null ? 0 : originalStart - runningEnd
      var overlap = runningEnd === null ? 0 : runningEnd - originalStart
      var overlapThreshold = Math.max(0.25, Math.min(1.0, interval.span * 0.25))

      // 某些站点在重建初始化段后让 tfdt 从 0 重新开始,而 buffered 增量又无法可靠取得。
      // 若后续周期大面积压在前一周期上,按真实 append 顺序连续重建,避免十几 MB 被折叠成几秒。
      if (runningEnd !== null && overlap > overlapThreshold) {
        shiftSamples(period.samples, runningEnd - originalStart)
        interval = sampleInterval(period.samples)
        repaired++
      }

      var first = period.samples[0]
      if (first) {
        var skippedConfigPeriod = previousPeriodOrder !== null && period.periodOrder && previousPeriodOrder && period.periodOrder > previousPeriodOrder + 1
        var realGap = originalGap > Math.max(0.6, (first.duration || 0.04) * 10)
        first.hardBoundary = normalized.length === 0 || skippedConfigPeriod || realGap
      }

      for (var n = 0; n < period.samples.length; n++) normalized.push(period.samples[n])
      runningEnd = runningEnd === null ? interval.end : Math.max(runningEnd, interval.end)
      previousPeriodOrder = period.periodOrder || previousPeriodOrder
    }

    group.samples = deduplicateSamples(normalized)
    group.repairedPeriods = repaired
    group.skippedDuplicatePeriods = skippedDuplicates
    var intervals = mergeSampleIntervals(group.samples)
    group.intervals = intervals
    group.span = 0
    group.bytes = 0
    group.duration = 0
    for (var j = 0; j < group.samples.length; j++) {
      group.bytes += group.samples[j].data.byteLength
      group.duration += group.samples[j].duration
    }
    for (var k = 0; k < intervals.length; k++) group.span += intervals[k].end - intervals[k].start
  }

  function makeStrictGroups(list) {
    var groups = Object.create(null)
    for (var i = 0; i < list.length; i++) {
      var key = list[i].decoderSignature || list[i].exactSignature || list[i].trackSignature || list[i].compatibilityKey || (list[i].kind + '|default')
      if (!groups[key]) groups[key] = { key: key, tracks: [], duration: 0, span: 0, bytes: 0, samples: [] }
      groups[key].tracks.push(list[i])
    }
    for (var name in groups) normalizeStrictGroup(groups[name])
    return groups
  }

  function bestStrictGroup(groups, preferredIntervals) {
    var best = null
    for (var name in groups) {
      var candidate = groups[name]
      var overlap = 0
      if (preferredIntervals && preferredIntervals.length) {
        for (var s = 0; s < candidate.samples.length; s++) {
          if (overlapWithIntervals(candidate.samples[s], preferredIntervals)) overlap += candidate.samples[s].duration
        }
      }
      candidate.overlap = overlap
      if (!best ||
          (preferredIntervals && overlap > best.overlap + 0.01) ||
          ((!preferredIntervals || Math.abs(overlap - best.overlap) <= 0.01) && candidate.span > best.span + 0.01) ||
          (Math.abs(candidate.span - best.span) <= 0.01 && candidate.duration > best.duration + 0.01) ||
          (Math.abs(candidate.duration - best.duration) <= 0.01 && candidate.bytes > best.bytes)) best = candidate
    }
    return best
  }

  function canonicalDescription(samples) {
    var choices = Object.create(null)
    var best = null
    for (var i = 0; i < samples.length; i++) {
      var key = samples[i].descriptionKey || descriptionKey(samples[i].description)
      if (!choices[key]) choices[key] = { key: key, description: samples[i].description, duration: 0, bytes: 0 }
      choices[key].duration += samples[i].duration
      choices[key].bytes += samples[i].data ? samples[i].data.byteLength : 0
    }
    for (var name in choices) {
      var choice = choices[name]
      if (!best || choice.duration > best.duration + 0.01 || (Math.abs(choice.duration - best.duration) <= 0.01 && choice.bytes > best.bytes)) best = choice
    }
    return best
  }

  function groupToMergedTrack(group, kind, clipIntervals) {
    if (!group || !group.tracks.length) return null
    var base = group.tracks[0]
    var samples = group.samples
    if (clipIntervals && clipIntervals.length) {
      samples = samples.filter(function (sample) { return overlapWithIntervals(sample, clipIntervals) })
    }
    samples = deduplicateSamples(samples)
    if (!samples.length) return null

    // 同一解码配置中允许 btrt/pasp/色彩等非关键字段变化,但输出统一使用覆盖时间最长的 sample entry。
    // 这样不会因为 YouTube 重建 init 段而产生几十个无意义的 stsd 条目。
    var canonical = canonicalDescription(samples)
    var normalizedSamples = []
    for (var i = 0; i < samples.length; i++) {
      var copy = {}
      for (var field in samples[i]) copy[field] = samples[i][field]
      if (canonical) {
        copy.description = canonical.description
        copy.descriptionKey = canonical.key
      }
      normalizedSamples.push(copy)
    }

    return {
      kind: kind,
      codec: base.codec,
      timescale: base.timescale,
      width: base.width || 0,
      height: base.height || 0,
      sampleRate: base.sampleRate || 0,
      channelCount: base.channelCount || 0,
      sampleSize: base.sampleSize || 16,
      language: base.language || 'und',
      compatibilityKey: group.key,
      repairedPeriods: group.repairedPeriods || 0,
      skippedDuplicatePeriods: group.skippedDuplicatePeriods || 0,
      samples: normalizedSamples
    }
  }

  function mergeTracksByKind(parsedTracks, session) {
    var videoList = []
    var audioList = []
    for (var i = 0; i < parsedTracks.length; i++) {
      if (parsedTracks[i].kind === 'video') videoList.push(parsedTracks[i])
      else if (parsedTracks[i].kind === 'audio') audioList.push(parsedTracks[i])
    }

    var merged = []
    var videoGroups = makeStrictGroups(videoList)
    var videoNames = Object.keys(videoGroups)
    var selectedVideo = bestStrictGroup(videoGroups, null)
    var videoIntervals = selectedVideo ? selectedVideo.intervals : []
    if (videoNames.length > 1 && session) {
      addWarning(session, '检测到清晰度或解码配置切换;仅保留覆盖时间最长的严格视频配置,并按其时间范围裁剪音频,避免黑屏和音画错位。')
    }
    var videoTrack = groupToMergedTrack(selectedVideo, 'video', null)
    if (selectedVideo && selectedVideo.repairedPeriods && session) {
      addWarning(session, '检测到初始化周期时间戳复位;已按 appendBuffer 顺序修复 ' + selectedVideo.repairedPeriods + ' 个视频周期。')
    }
    if (videoTrack) merged.push(videoTrack)

    var audioGroups = makeStrictGroups(audioList)
    var audioNames = Object.keys(audioGroups)
    var selectedAudio = bestStrictGroup(audioGroups, videoIntervals.length ? videoIntervals : null)
    if (audioNames.length > 1 && session) {
      addWarning(session, '检测到多个音频解码配置;已选择与视频时间范围重叠最多的一组。')
    }
    var audioTrack = groupToMergedTrack(selectedAudio, 'audio', videoIntervals.length ? videoIntervals : null)
    if (selectedAudio && selectedAudio.repairedPeriods && session) {
      addWarning(session, '检测到音频周期时间戳复位;已按 appendBuffer 顺序修复 ' + selectedAudio.repairedPeriods + ' 个音频周期。')
    }
    if (audioTrack) merged.push(audioTrack)
    return merged
  }

  function descriptionKey(bytes) {
    return bytes.byteLength + ':' + fnv1a(bytes)
  }

  function sampleEntryChildOffset(bytes, kind) {
    if (kind === 'video') return bytes.byteLength >= 86 ? 86 : 8
    if (kind === 'audio') {
      if (bytes.byteLength < 36) return 8
      var version = (bytes[16] << 8) | bytes[17]
      if (version === 1 && bytes.byteLength >= 52) return 52
      if (version === 2 && bytes.byteLength >= 72) return 72
      return 36
    }
    return 8
  }

  function decoderConfigurationBoxes(bytes, kind) {
    var wanted = kind === 'video'
      ? { avcC: 1, hvcC: 1, av1C: 1, vpcC: 1, esds: 1, dvcC: 1, dvvC: 1 }
      : { esds: 1, dOps: 1, dfLa: 1, alac: 1, dac3: 1, dec3: 1, dac4: 1, mhaC: 1 }
    var output = []
    var queue = [{ start: sampleEntryChildOffset(bytes, kind), end: bytes.byteLength }]
    while (queue.length) {
      var range = queue.shift()
      var p = range.start
      while (p + 8 <= range.end) {
        var size = bytes[p] * 16777216 + bytes[p + 1] * 65536 + bytes[p + 2] * 256 + bytes[p + 3]
        var type = String.fromCharCode(bytes[p + 4], bytes[p + 5], bytes[p + 6], bytes[p + 7])
        if (size === 1 || size < 8 || p + size > range.end) break
        if (wanted[type]) output.push(type + ':' + size + ':' + fnv1a(bytes.subarray(p, p + size)))
        if (type === 'wave' || type === 'sinf' || type === 'schi') queue.push({ start: p + 8, end: p + size })
        p += size
      }
    }
    return output
  }

  function decoderConfigurationKey(description, kind, info) {
    var bytes = description instanceof Uint8Array ? description : new Uint8Array(description)
    var type = bytes.byteLength >= 8 ? readType(bytes, 4).toLowerCase() : 'unknown'
    var details = []
    if (kind === 'video' && bytes.byteLength >= 36) {
      details.push(((bytes[32] << 8) | bytes[33]) + 'x' + ((bytes[34] << 8) | bytes[35]))
    } else if (kind === 'audio' && bytes.byteLength >= 36) {
      var channels = (bytes[24] << 8) | bytes[25]
      var rate = bytes[32] * 16777216 + bytes[33] * 65536 + bytes[34] * 256 + bytes[35]
      details.push(Math.floor(rate / 65536) + 'x' + channels)
    }
    var configs = decoderConfigurationBoxes(bytes, kind)
    // 未识别到 codec config 时才回退到完整 sample entry;已识别时忽略 btrt/pasp/colr 等非解码字段。
    if (!configs.length) configs.push('full:' + descriptionKey(bytes))
    return [
      info && info.family ? info.family : type,
      info && info.timescale ? info.timescale : 0,
      details.join(','),
      configs.join(',')
    ].join('|')
  }

  function trimVideoStartToSync(samples) {
    if (!samples.length) return samples
    samples = samples.slice().sort(function (a, b) {
      if (a.dts !== b.dts) return a.dts - b.dts
      return a.appendSeq - b.appendSeq
    })

    var regions = []
    var current = []
    var previous = null
    for (var i = 0; i < samples.length; i++) {
      var sample = samples[i]
      var gap = previous ? sample.dts - (previous.dts + previous.duration) : 0
      var boundary = !current.length || sample.hardBoundary || gap > Math.max(0.6, (previous ? previous.duration : sample.duration) * 10)
      if (boundary && current.length) {
        regions.push(current)
        current = []
      }
      current.push(sample)
      previous = sample
    }
    if (current.length) regions.push(current)

    var output = []
    for (var r = 0; r < regions.length; r++) {
      var region = regions[r]
      var syncIndex = -1
      var anyKnown = false
      for (var j = 0; j < region.length; j++) {
        if (region[j].syncKnown !== false) anyKnown = true
        if (region[j].isSync) {
          syncIndex = j
          break
        }
      }
      // AVC/HEVC 或样本标志能确认关键帧时,从首个关键帧开始。
      // 若 codec 没有可用的关键帧标志,则保留该连续区间,避免整个 YouTube 周期被误删。
      if (syncIndex >= 0) {
        for (var k = syncIndex; k < region.length; k++) output.push(region[k])
      } else if (!anyKnown) {
        for (var u = 0; u < region.length; u++) output.push(region[u])
      }
    }
    return output
  }

  function prepareOutputTracks(tracks) {
    // 确保每个视频初始化周期从关键帧开始,避免从 P/B 帧起播。
    var videoStart = Infinity
    for (var vt = 0; vt < tracks.length; vt++) {
      if (tracks[vt].kind !== 'video') continue
      tracks[vt].samples = trimVideoStartToSync(tracks[vt].samples)
      for (var vs = 0; vs < tracks[vt].samples.length; vs++) videoStart = Math.min(videoStart, tracks[vt].samples[vs].cts)
    }
    if (tracks.some(function (track) { return track.kind === 'video' }) && !isFinite(videoStart)) {
      throw new Error('视频轨没有可解码关键帧')
    }

    // 视频因关键帧对齐而向后裁剪时,同步去掉更早的音频,防止开头有声无画。
    if (isFinite(videoStart)) {
      for (var at = 0; at < tracks.length; at++) {
        if (tracks[at].kind !== 'audio') continue
        tracks[at].samples = tracks[at].samples.filter(function (sample) {
          return sample.cts + sample.duration > videoStart - 0.05
        })
      }
    }

    var starts = []
    var globalStart = Infinity
    for (var i = 0; i < tracks.length; i++) {
      var start = Infinity
      for (var j = 0; j < tracks[i].samples.length; j++) start = Math.min(start, tracks[i].samples[j].cts)
      starts.push(start)
      globalStart = Math.min(globalStart, start)
    }
    if (!isFinite(globalStart)) throw new Error('没有可输出的媒体样本')

    var minStart = Infinity
    var maxStart = -Infinity
    for (var st = 0; st < starts.length; st++) {
      if (!isFinite(starts[st])) continue
      minStart = Math.min(minStart, starts[st])
      maxStart = Math.max(maxStart, starts[st])
    }
    // 只有音视频起点异常相差很大时才分别归零。正常的编码延迟仍保留。
    var normalizeIndependently = isFinite(minStart) && isFinite(maxStart) && maxStart - minStart > MAX_AV_START_DRIFT

    for (var t = 0; t < tracks.length; t++) {
      var track = tracks[t]
      if (!track.samples.length) throw new Error((track.kind === 'video' ? '视频' : '音频') + '轨没有可用样本')

      var descriptions = []
      var descriptionMap = Object.create(null)
      var normalized = []
      var trackBase = normalizeIndependently ? starts[t] : globalStart
      if (!isFinite(trackBase)) trackBase = 0

      for (var s = 0; s < track.samples.length; s++) {
        var sample = track.samples[s]
        if (!sample.description || !sample.data || !sample.data.byteLength) continue
        var entryBytes = serializeBox(sample.description)
        if (!entryBytes || entryBytes.byteLength < 16) continue
        var key = descriptionKey(entryBytes)
        var descriptionIndex = descriptionMap[key]
        if (descriptionIndex === undefined) {
          descriptions.push(entryBytes)
          descriptionIndex = descriptions.length
          descriptionMap[key] = descriptionIndex
        }
        normalized.push({
          data: sample.data,
          dtsSec: sample.dts - trackBase,
          ctsSec: sample.cts - trackBase,
          durationSec: sample.duration,
          isSync: sample.isSync,
          appendSeq: sample.appendSeq,
          descriptionIndex: descriptionIndex,
          periodId: sample.periodId || ''
        })
      }

      normalized.sort(function (a, b) {
        if (a.dtsSec !== b.dtsSec) return a.dtsSec - b.dtsSec
        return a.appendSeq - b.appendSeq
      })
      if (!normalized.length || !descriptions.length) throw new Error((track.kind === 'video' ? '视频' : '音频') + '轨样本描述无效')
      if (track.kind === 'video' && !normalized.some(function (sample) { return sample.isSync })) {
        throw new Error('视频轨没有可解码关键帧')
      }

      track.sampleEntries = descriptions
      track.samples = buildTrackMediaTimeline(normalized, track.timescale)
    }

    return tracks
  }

  function buildTrackMediaTimeline(samples, timescale) {
    if (!samples.length) return []
    var output = []
    var mediaCursor = 0
    var decodeStartSec = samples[0].dtsSec
    var originalPresentationStartSec = Infinity
    var compactPresentationStart = Infinity
    var compactPresentationEnd = -Infinity
    var group = {
      presentationStartSec: 0,
      presentationEndSec: 0,
      mediaTime: 0,
      mediaStart: 0,
      mediaEnd: 0
    }

    for (var i = 0; i < samples.length; i++) {
      var current = samples[i]
      originalPresentationStartSec = Math.min(originalPresentationStartSec, current.ctsSec)

      // 关键:只使用样本自己的 duration,不再把 DTS 跳变写成上一帧的超长时长。
      // MSE 发生 discontinuity 后,输出轨道按实际样本时长连续重建。
      var duration = Math.max(1, Math.round(current.durationSec * timescale))
      var compositionOffset = Math.round((current.ctsSec - current.dtsSec) * timescale)
      var item = {
        data: current.data,
        dtsSec: current.dtsSec,
        ctsSec: current.ctsSec,
        durationSec: current.durationSec,
        mediaDts: mediaCursor,
        mediaCts: mediaCursor + compositionOffset,
        duration: duration,
        isSync: current.isSync,
        descriptionIndex: current.descriptionIndex,
        group: group,
        fileOffset: 0,
        offsetPatch: null
      }
      output.push(item)
      compactPresentationStart = Math.min(compactPresentationStart, item.mediaCts)
      compactPresentationEnd = Math.max(compactPresentationEnd, item.mediaCts + duration)
      mediaCursor += duration
    }

    var initialDelaySec = Math.max(0, originalPresentationStartSec)
    var compactDurationSec = Math.max(0, (compactPresentationEnd - compactPresentationStart) / timescale)
    group.presentationStartSec = initialDelaySec
    group.presentationEndSec = initialDelaySec + compactDurationSec
    group.mediaTime = Math.max(0, compactPresentationStart)
    group.mediaEnd = mediaCursor
    return output
  }

  function runLengthPairs(values) {
    var result = []
    if (!values.length) return result
    var value = values[0]
    var count = 1
    for (var i = 1; i < values.length; i++) {
      if (values[i] === value) count++
      else {
        result.push([count, value])
        value = values[i]
        count = 1
      }
    }
    result.push([count, value])
    return result
  }

  function BoxWriter(initialSize) {
    this.buffer = new ArrayBuffer(initialSize || 65536)
    this.view = new DataView(this.buffer)
    this.bytes = new Uint8Array(this.buffer)
    this.position = 0
    this.stack = []
  }

  BoxWriter.prototype.ensure = function (count) {
    if (this.position + count <= this.buffer.byteLength) return
    var size = Math.max(this.buffer.byteLength * 2, this.position + count + 65536)
    var next = new ArrayBuffer(size)
    new Uint8Array(next).set(new Uint8Array(this.buffer, 0, this.position))
    this.buffer = next
    this.view = new DataView(next)
    this.bytes = new Uint8Array(next)
  }

  BoxWriter.prototype.u8 = function (value) {
    this.ensure(1)
    this.view.setUint8(this.position, value & 255)
    this.position += 1
  }

  BoxWriter.prototype.u16 = function (value) {
    this.ensure(2)
    this.view.setUint16(this.position, value & 65535, false)
    this.position += 2
  }

  BoxWriter.prototype.i16 = function (value) {
    this.ensure(2)
    this.view.setInt16(this.position, value, false)
    this.position += 2
  }

  BoxWriter.prototype.u24 = function (value) {
    this.u8(value >>> 16)
    this.u8(value >>> 8)
    this.u8(value)
  }

  BoxWriter.prototype.u32 = function (value) {
    this.ensure(4)
    this.view.setUint32(this.position, value >>> 0, false)
    this.position += 4
  }

  BoxWriter.prototype.i32 = function (value) {
    this.ensure(4)
    this.view.setInt32(this.position, value | 0, false)
    this.position += 4
  }

  BoxWriter.prototype.u64 = function (value) {
    var high = Math.floor(value / 4294967296)
    var low = value - high * 4294967296
    this.u32(high)
    this.u32(low)
  }

  BoxWriter.prototype.string = function (value) {
    this.ensure(value.length)
    for (var i = 0; i < value.length; i++) this.u8(value.charCodeAt(i))
  }

  BoxWriter.prototype.data = function (value) {
    var bytes = value instanceof Uint8Array ? value : new Uint8Array(value)
    this.ensure(bytes.byteLength)
    this.bytes.set(bytes, this.position)
    this.position += bytes.byteLength
  }

  BoxWriter.prototype.fixed1616 = function (value) {
    this.u32(Math.round(value * 65536))
  }

  BoxWriter.prototype.fixed88 = function (value) {
    this.u16(Math.round(value * 256))
  }

  BoxWriter.prototype.begin = function (type) {
    var start = this.position
    this.u32(0)
    this.string(type)
    this.stack.push(start)
    return start
  }

  BoxWriter.prototype.beginFull = function (type, version, flags) {
    var start = this.begin(type)
    this.u8(version || 0)
    this.u24(flags || 0)
    return start
  }

  BoxWriter.prototype.end = function () {
    if (!this.stack.length) throw new Error('MP4 box 栈为空')
    var start = this.stack.pop()
    var size = this.position - start
    this.patchU32(start, size)
    return size
  }

  BoxWriter.prototype.patchU32 = function (position, value) {
    this.view.setUint32(position, value >>> 0, false)
  }

  BoxWriter.prototype.patchU64 = function (position, value) {
    var high = Math.floor(value / 4294967296)
    var low = value - high * 4294967296
    this.view.setUint32(position, high >>> 0, false)
    this.view.setUint32(position + 4, low >>> 0, false)
  }

  BoxWriter.prototype.result = function () {
    return new Uint8Array(this.buffer.slice(0, this.position))
  }

  function writeIdentityMatrix(writer) {
    writer.u32(0x00010000); writer.u32(0); writer.u32(0)
    writer.u32(0); writer.u32(0x00010000); writer.u32(0)
    writer.u32(0); writer.u32(0); writer.u32(0x40000000)
  }

  function makeFtyp() {
    var writer = new BoxWriter(64)
    writer.begin('ftyp')
    writer.string('isom')
    writer.u32(0x200)
    writer.string('isom')
    writer.string('iso6')
    writer.string('mp41')
    writer.string('dash')
    writer.end()
    return writer.result()
  }

  function buildEdits(samples, timescale) {
    if (!samples.length) return []
    var group = samples[0].group
    var edits = []
    if (group.presentationStartSec > 0.001) {
      edits.push({
        segmentDuration: Math.max(1, Math.round(group.presentationStartSec * MOVIE_TIMESCALE)),
        mediaTime: -1
      })
    }
    var presentationDuration = group.presentationEndSec - group.presentationStartSec
    if (!(presentationDuration > 0)) presentationDuration = group.mediaEnd / timescale
    edits.push({
      segmentDuration: Math.max(1, Math.round(presentationDuration * MOVIE_TIMESCALE)),
      mediaTime: group.mediaTime
    })
    return edits
  }

  function writeMovieHeader(writer, duration) {
    writer.beginFull('mvhd', 0, 0)
    writer.u32(0); writer.u32(0)
    writer.u32(MOVIE_TIMESCALE)
    writer.u32(duration)
    writer.fixed1616(1)
    writer.fixed88(1)
    writer.u16(0)
    writer.u32(0); writer.u32(0)
    writeIdentityMatrix(writer)
    for (var i = 0; i < 6; i++) writer.u32(0)
    writer.u32(3)
    writer.end()
  }

  function writeTrack(writer, track, trackId, useCo64) {
    var samples = track.samples
    var edits = buildEdits(samples, track.timescale)
    var trackDuration = 0
    for (var e = 0; e < edits.length; e++) trackDuration += edits[e].segmentDuration
    var mediaDuration = samples.length ? samples[samples.length - 1].mediaDts + samples[samples.length - 1].duration : 0

    writer.begin('trak')

    writer.beginFull('tkhd', 0, 0x000007)
    writer.u32(0); writer.u32(0)
    writer.u32(trackId)
    writer.u32(0)
    writer.u32(trackDuration)
    writer.u32(0); writer.u32(0)
    writer.u16(0)
    writer.u16(0)
    writer.fixed88(track.kind === 'audio' ? 1 : 0)
    writer.u16(0)
    writeIdentityMatrix(writer)
    writer.fixed1616(track.kind === 'video' ? track.width : 0)
    writer.fixed1616(track.kind === 'video' ? track.height : 0)
    writer.end()

    if (edits.length && !(edits.length === 1 && edits[0].mediaTime === 0)) {
      var useVersion1 = false
      for (var ei = 0; ei < edits.length; ei++) {
        if (edits[ei].segmentDuration > 0xffffffff || edits[ei].mediaTime > 0x7fffffff) useVersion1 = true
      }
      writer.begin('edts')
      writer.beginFull('elst', useVersion1 ? 1 : 0, 0)
      writer.u32(edits.length)
      for (var ej = 0; ej < edits.length; ej++) {
        if (useVersion1) {
          writer.u64(edits[ej].segmentDuration)
          if (edits[ej].mediaTime < 0) {
            writer.u32(0xffffffff); writer.u32(0xffffffff)
          } else {
            writer.u64(edits[ej].mediaTime)
          }
        } else {
          writer.u32(edits[ej].segmentDuration)
          writer.i32(edits[ej].mediaTime)
        }
        writer.i16(1)
        writer.i16(0)
      }
      writer.end()
      writer.end()
    }

    writer.begin('mdia')
    writer.beginFull('mdhd', 0, 0)
    writer.u32(0); writer.u32(0)
    writer.u32(track.timescale)
    writer.u32(mediaDuration)
    writer.u16(0x55c4)
    writer.u16(0)
    writer.end()

    writer.beginFull('hdlr', 0, 0)
    writer.u32(0)
    writer.string(track.kind === 'audio' ? 'soun' : 'vide')
    writer.u32(0); writer.u32(0); writer.u32(0)
    writer.string(track.kind === 'audio' ? 'SoundHandler' : 'VideoHandler')
    writer.u8(0)
    writer.end()

    writer.begin('minf')
    if (track.kind === 'audio') {
      writer.beginFull('smhd', 0, 0)
      writer.i16(0); writer.u16(0)
      writer.end()
    } else {
      writer.beginFull('vmhd', 0, 1)
      writer.u16(0)
      writer.u16(0); writer.u16(0); writer.u16(0)
      writer.end()
    }

    writer.begin('dinf')
    writer.beginFull('dref', 0, 0)
    writer.u32(1)
    writer.beginFull('url ', 0, 1)
    writer.end()
    writer.end()
    writer.end()

    writer.begin('stbl')

    writer.beginFull('stsd', 0, 0)
    writer.u32(track.sampleEntries.length)
    for (var sd = 0; sd < track.sampleEntries.length; sd++) writer.data(track.sampleEntries[sd])
    writer.end()

    var durations = samples.map(function (sample) { return sample.duration })
    var sttsRuns = runLengthPairs(durations)
    writer.beginFull('stts', 0, 0)
    writer.u32(sttsRuns.length)
    for (var st = 0; st < sttsRuns.length; st++) {
      writer.u32(sttsRuns[st][0])
      writer.u32(sttsRuns[st][1])
    }
    writer.end()

    var compositionOffsets = samples.map(function (sample) { return sample.mediaCts - sample.mediaDts })
    var hasCompositionOffsets = compositionOffsets.some(function (value) { return value !== 0 })
    if (hasCompositionOffsets) {
      var cttsVersion = compositionOffsets.some(function (value) { return value < 0 }) ? 1 : 0
      var cttsRuns = runLengthPairs(compositionOffsets)
      writer.beginFull('ctts', cttsVersion, 0)
      writer.u32(cttsRuns.length)
      for (var ct = 0; ct < cttsRuns.length; ct++) {
        writer.u32(cttsRuns[ct][0])
        if (cttsVersion === 1) writer.i32(cttsRuns[ct][1])
        else writer.u32(cttsRuns[ct][1])
      }
      writer.end()
    }

    var stscEntries = []
    var previousDescription = -1
    for (var sc = 0; sc < samples.length; sc++) {
      if (samples[sc].descriptionIndex !== previousDescription) {
        stscEntries.push({
          firstChunk: sc + 1,
          samplesPerChunk: 1,
          descriptionIndex: samples[sc].descriptionIndex
        })
        previousDescription = samples[sc].descriptionIndex
      }
    }
    writer.beginFull('stsc', 0, 0)
    writer.u32(stscEntries.length)
    for (var se = 0; se < stscEntries.length; se++) {
      writer.u32(stscEntries[se].firstChunk)
      writer.u32(stscEntries[se].samplesPerChunk)
      writer.u32(stscEntries[se].descriptionIndex)
    }
    writer.end()

    writer.beginFull('stsz', 0, 0)
    writer.u32(0)
    writer.u32(samples.length)
    for (var sz = 0; sz < samples.length; sz++) writer.u32(samples[sz].data.byteLength)
    writer.end()

    writer.beginFull(useCo64 ? 'co64' : 'stco', 0, 0)
    writer.u32(samples.length)
    for (var co = 0; co < samples.length; co++) {
      samples[co].offsetPatch = writer.position
      if (useCo64) writer.u64(0)
      else writer.u32(0)
    }
    writer.end()

    if (track.kind === 'video') {
      var syncSamples = []
      for (var ss = 0; ss < samples.length; ss++) {
        if (samples[ss].isSync) syncSamples.push(ss + 1)
      }
      if (syncSamples.length && syncSamples.length !== samples.length) {
        writer.beginFull('stss', 0, 0)
        writer.u32(syncSamples.length)
        for (var si = 0; si < syncSamples.length; si++) writer.u32(syncSamples[si])
        writer.end()
      }
    }

    writer.end()
    writer.end()
    writer.end()
    writer.end()

    return trackDuration
  }

  function makeMdatHeader(dataSize) {
    var large = dataSize + 8 > 0xffffffff
    var writer = new BoxWriter(16)
    if (large) {
      writer.u32(1)
      writer.string('mdat')
      writer.u64(dataSize + 16)
    } else {
      writer.u32(dataSize + 8)
      writer.string('mdat')
    }
    return writer.result()
  }

  function buildFlatMp4(tracks) {
    tracks = prepareOutputTracks(tracks)
    if (!tracks.length) throw new Error('没有视频或音频轨道')

    var totalDataSize = 0
    var orderedSamples = []
    for (var t = 0; t < tracks.length; t++) {
      for (var s = 0; s < tracks[t].samples.length; s++) {
        tracks[t].samples[s].trackOrder = t
        totalDataSize += tracks[t].samples[s].data.byteLength
        orderedSamples.push(tracks[t].samples[s])
      }
    }

    orderedSamples.sort(function (a, b) {
      if (a.dtsSec !== b.dtsSec) return a.dtsSec - b.dtsSec
      return a.trackOrder - b.trackOrder
    })

    var useCo64 = totalDataSize > 0xf0000000
    var ftyp = makeFtyp()
    var moovWriter = new BoxWriter(Math.min(64 * 1024 * 1024, Math.max(65536, orderedSamples.length * (useCo64 ? 12 : 8))))
    moovWriter.begin('moov')

    var durationPatchTrack = []
    var trackDurations = []
    for (var i = 0; i < tracks.length; i++) {
      trackDurations.push(0)
    }
    var estimatedMovieDuration = 0
    for (var td = 0; td < tracks.length; td++) {
      var edits = buildEdits(tracks[td].samples, tracks[td].timescale)
      var duration = 0
      for (var ed = 0; ed < edits.length; ed++) duration += edits[ed].segmentDuration
      estimatedMovieDuration = Math.max(estimatedMovieDuration, duration)
    }
    writeMovieHeader(moovWriter, estimatedMovieDuration)

    for (var tr = 0; tr < tracks.length; tr++) {
      trackDurations[tr] = writeTrack(moovWriter, tracks[tr], tr + 1, useCo64)
    }
    moovWriter.end()

    var mdatHeader = makeMdatHeader(totalDataSize)
    var dataStart = ftyp.byteLength + moovWriter.position + mdatHeader.byteLength
    var offset = dataStart
    for (var os = 0; os < orderedSamples.length; os++) {
      orderedSamples[os].fileOffset = offset
      offset += orderedSamples[os].data.byteLength
    }

    for (var pt = 0; pt < tracks.length; pt++) {
      for (var ps = 0; ps < tracks[pt].samples.length; ps++) {
        var outputSample = tracks[pt].samples[ps]
        if (useCo64) moovWriter.patchU64(outputSample.offsetPatch, outputSample.fileOffset)
        else moovWriter.patchU32(outputSample.offsetPatch, outputSample.fileOffset)
      }
    }

    var blobParts = [ftyp, moovWriter.result(), mdatHeader]
    for (var bp = 0; bp < orderedSamples.length; bp++) blobParts.push(orderedSamples[bp].data)
    return new Blob(blobParts, { type: 'video/mp4' })
  }

  function outputTrackStats(tracks) {
    var result = { dataBytes: 0, videoSamples: 0, audioSamples: 0, expectVideo: false, expectAudio: false }
    for (var i = 0; i < tracks.length; i++) {
      if (tracks[i].kind === 'video') result.expectVideo = true
      if (tracks[i].kind === 'audio') result.expectAudio = true
      for (var j = 0; j < tracks[i].samples.length; j++) {
        result.dataBytes += tracks[i].samples[j].data.byteLength
        if (tracks[i].kind === 'video') result.videoSamples++
        if (tracks[i].kind === 'audio') result.audioSamples++
      }
    }
    return result
  }

  function scanBoxesRange(bytes, start, end) {
    var output = []
    var offset = start
    while (offset + 8 <= end) {
      var size = readU32(bytes, offset)
      var header = 8
      if (size === 1) {
        if (offset + 16 > end) break
        size = readU64(bytes, offset + 8)
        header = 16
      } else if (size === 0) size = end - offset
      if (!isFinite(size) || size < header || offset + size > end) break
      output.push({ type: readType(bytes, offset + 4), start: offset, end: offset + size, data: offset + header, headerSize: header })
      offset += size
    }
    return output
  }

  function validateMp4Blob(blob, expected) {
    return new Promise(function (resolve, reject) {
      if (!blob || blob.size < 1024) return reject(new Error('输出 MP4 为空或过小'))
      if (expected.dataBytes && blob.size < expected.dataBytes) return reject(new Error('输出文件小于媒体样本总量,可能只生成了 MP4 空壳'))
      blob.arrayBuffer().then(function (buffer) {
        var bytes = new Uint8Array(buffer)
        var top = scanBoxesRange(bytes, 0, bytes.length)
        var moov = null
        var mdatBytes = 0
        for (var i = 0; i < top.length; i++) {
          if (top[i].type === 'moov') moov = top[i]
          if (top[i].type === 'mdat') mdatBytes += top[i].end - top[i].data
        }
        if (!moov || !mdatBytes) throw new Error('输出 MP4 缺少 moov 或 mdat')
        var traks = scanBoxesRange(bytes, moov.data, moov.end).filter(function (box) { return box.type === 'trak' })
        var hasVideo = false, hasAudio = false, videoSamples = 0, audioSamples = 0
        for (var t = 0; t < traks.length; t++) {
          var mdia = rawChild(bytes, traks[t], 'mdia')
          var hdlr = mdia ? rawChild(bytes, mdia, 'hdlr') : null
          var minf = mdia ? rawChild(bytes, mdia, 'minf') : null
          var stbl = minf ? rawChild(bytes, minf, 'stbl') : null
          var stsz = stbl ? rawChild(bytes, stbl, 'stsz') : null
          var stsd = stbl ? rawChild(bytes, stbl, 'stsd') : null
          if (!hdlr || !stsz || !stsd || stsz.data + 12 > stsz.end || stsd.data + 8 > stsd.end) continue
          var kind = readType(bytes, hdlr.data + 8)
          var sampleCount = readU32(bytes, stsz.data + 8)
          var entryCount = readU32(bytes, stsd.data + 4)
          if (!entryCount) continue
          if (kind === 'vide') { hasVideo = true; videoSamples += sampleCount }
          if (kind === 'soun') { hasAudio = true; audioSamples += sampleCount }
        }
        if (expected.dataBytes && mdatBytes < expected.dataBytes) throw new Error('mdat 小于媒体样本总量')
        if (expected.expectVideo && (!hasVideo || videoSamples < MIN_VIDEO_SAMPLES)) throw new Error('输出 MP4 缺少有效视频轨')
        if (expected.expectAudio && (!hasAudio || audioSamples < 1)) throw new Error('输出 MP4 缺少有效音频样本')
        resolve({ hasVideo: hasVideo, hasAudio: hasAudio, videoSamples: videoSamples, audioSamples: audioSamples, size: blob.size })
      }).catch(reject)
    })
  }

  function capturedMp4Bytes(session) {
    var total = 0
    if (!session || !session.tracks) return total
    for (var i = 0; i < session.tracks.length; i++) {
      var track = session.tracks[i]
      for (var j = 0; j < track.operations.length; j++) {
        var operation = track.operations[j]
        if (operation.state === 'ok' && operation.data && isMp4Mime(operation.mime || track.initialMime)) total += operation.byteLength || operation.data.byteLength || 0
      }
    }
    return total
  }

  async function buildAndValidateMp4(parsedTracks, session) {
    var sourceVideo = mediaSampleStats(parsedTracks, 'video')
    var sourceAudio = mediaSampleStats(parsedTracks, 'audio')
    var capturedBytes = capturedMp4Bytes(session)
    var parsedBytes = sourceVideo.bytes + sourceAudio.bytes
    if (capturedBytes > 2 * 1024 * 1024 && parsedBytes < capturedBytes * 0.08) {
      throw new Error('仅从 ' + formatBytes(capturedBytes) + ' 捕获数据中解析到 ' + formatBytes(parsedBytes) + ' 媒体样本,解析覆盖率异常,已停止生成短片')
    }
    var mergedTracks = mergeTracksByKind(cloneParsedTracks(parsedTracks), session)
    if (!mergedTracks.length) throw new Error('没有可合并的兼容音视频轨道')
    var mergedVideo = mediaSampleStats(mergedTracks, 'video')
    if (!mergedVideo.count && sourceVideo.count) throw new Error('兼容性筛选后视频轨为空')
    if (sourceVideo.bytes > 1024 * 1024 && mergedVideo.bytes && mergedVideo.bytes < sourceVideo.bytes * 0.12 && mergedVideo.span < Math.max(6, sourceVideo.span * 0.35)) {
      throw new Error('视频筛选后仅剩 ' + formatBytes(mergedVideo.bytes) + ' / ' + formatBytes(sourceVideo.bytes) + ',疑似初始化周期被错误拆分,已停止生成异常短片')
    }
    // buildFlatMp4 会按关键帧和音视频起点裁剪样本;校验基准必须在裁剪后计算。
    var blob = buildFlatMp4(mergedTracks)
    var expected = outputTrackStats(mergedTracks)
    await validateMp4Blob(blob, expected)
    return blob
  }

  async function parseCaptureSession(session, warningSession) {
    if (!session || !session.tracks || !session.tracks.length) return []
    for (var p = 0; p < session.tracks.length; p++) {
      if (session.tracks[p].pendingOperations.length) throw new Error('会话 #' + session.id + ' 仍有 appendBuffer 操作未完成,请稍后再导出')
    }

    var parsedTracks = []
    var diagnostics = []
    for (var i = 0; i < session.tracks.length; i++) {
      var captureTrack = session.tracks[i]
      if (!captureTrack.operations.length) continue
      var mime = captureTrack.operations[0].mime || captureTrack.initialMime
      if (!isMp4Mime(mime)) {
        addWarning(warningSession || session, '检测到非 MP4 MSE 轨道;浏览器合并器只支持未加密 ISO BMFF/fMP4。')
        continue
      }
      var preparedList
      try {
        preparedList = prepareMp4Inputs(captureTrack)
      } catch (prepareError) {
        diagnostics.push('轨道 #' + captureTrack.id + ':' + prepareError.message)
        continue
      }
      var trackParsed = 0
      for (var pi = 0; pi < preparedList.length; pi++) {
        var prepared = preparedList[pi]
        for (var w = 0; w < prepared.warnings.length; w++) addWarning(warningSession || session, prepared.warnings[w])
        try {
          var parsed = await parseMp4Buffer(prepared)
          var parsedPayloadBytes = 0
          for (var pt = 0; pt < parsed.tracks.length; pt++) {
            for (var ps = 0; ps < parsed.tracks[pt].samples.length; ps++) {
              parsedPayloadBytes += parsed.tracks[pt].samples[ps].data ? parsed.tracks[pt].samples[ps].data.byteLength : 0
            }
          }
          if (prepared.mediaPayloadBytes > 512 * 1024 && parsedPayloadBytes < prepared.mediaPayloadBytes * 0.2) {
            throw new Error('仅解析到 ' + formatBytes(parsedPayloadBytes) + ' / ' + formatBytes(prepared.mediaPayloadBytes) + ' 的 mdat 负载,疑似媒体盒跨 append 边界未完整重组')
          }
          var extracted = extractParsedTracks(parsed, captureTrack)
          for (var et = 0; et < extracted.length; et++) extracted[et].captureSessionId = session.id
          if (extracted.length) {
            parsedTracks = parsedTracks.concat(extracted)
            trackParsed += extracted.length
          } else {
            diagnostics.push('轨道 #' + captureTrack.id + ' 周期 ' + prepared.periodOrder + ':没有有效样本')
          }
        } catch (periodError) {
          diagnostics.push('轨道 #' + captureTrack.id + ' 周期 ' + prepared.periodOrder + ':' + periodError.message)
          addWarning(warningSession || session, '已跳过无法独立解析的 MP4 周期 #' + prepared.periodOrder + ':' + periodError.message)
        }
      }
      if (!trackParsed) addWarning(warningSession || session, '轨道 #' + captureTrack.id + ' 未找到可用媒体周期。')
    }
    if (!parsedTracks.length && diagnostics.length) {
      throw new Error('没有解析到 fMP4 媒体样本;' + diagnostics.slice(0, 3).join(';'))
    }
    return parsedTracks
  }

  function parsedKindStats(parsedTracks, kind) {
    var stats = mediaSampleStats(parsedTracks, kind)
    stats.duration = stats.span
    return stats
  }

  function companionScore(primarySession, candidateSession, primaryStats, candidateStats) {
    if (!candidateStats || !candidateStats.count || !(candidateStats.duration > 0)) return -Infinity
    var score = 0
    var affinity = timingAffinity(primarySession, candidateSession)
    var overlap = activityOverlapRatio(primarySession, candidateSession)
    score += affinity * 180
    score += Math.min(1, overlap) * 80

    if (primaryStats && primaryStats.duration > 0) {
      var ratio = Math.min(primaryStats.duration, candidateStats.duration) /
        Math.max(primaryStats.duration, candidateStats.duration)
      score += ratio * 140
      if (ratio < 0.55) score -= 180
      else if (ratio < 0.75) score -= 45
    }

    var createdDiff = Math.abs((primarySession.createdAt || 0) - (candidateSession.createdAt || 0)) / 1000
    score += Math.max(0, 35 - createdDiff * 2)
    if (affinity < 0.18 && overlap < 0.18) score -= 160
    return score
  }

  async function findCompanionTracks(primarySession, primaryTracks, missingKind) {
    var oppositeKind = missingKind === 'audio' ? 'video' : 'audio'
    var primaryStats = parsedKindStats(primaryTracks, oppositeKind)
    var best = null

    for (var i = 0; i < sessions.length; i++) {
      var candidateSession = sessions[i]
      if (candidateSession === primarySession || !candidateSession.totalBytes) continue
      var hints = sessionKindHints(candidateSession)
      if (!hints[missingKind]) continue

      var roughAffinity = timingAffinity(primarySession, candidateSession)
      var roughOverlap = activityOverlapRatio(primarySession, candidateSession)
      var createdGap = Math.abs((primarySession.createdAt || 0) - (candidateSession.createdAt || 0))
      // 短视频页面可能预加载很多会话。先按真实 appendBuffer 活动过滤,避免解析无关视频。
      if (roughAffinity < 0.12 && roughOverlap < 0.12 && createdGap > 8000) continue

      var candidateTracks
      try {
        candidateTracks = await parseCaptureSession(candidateSession, primarySession)
      } catch (error) {
        console.warn('[MSE Extract Lite] 忽略无法解析的候选会话 #' + candidateSession.id, error)
        continue
      }
      var candidateStats = parsedKindStats(candidateTracks, missingKind)
      var score = companionScore(primarySession, candidateSession, primaryStats, candidateStats)
      if (!best || score > best.score) {
        best = {
          session: candidateSession,
          tracks: candidateTracks.filter(function (track) { return track.kind === missingKind }),
          stats: candidateStats,
          score: score
        }
      }
    }

    if (!best || !best.tracks.length || best.score < 80) return null
    return best
  }

  async function collectExportTracks(session) {
    var parsedTracks = await parseCaptureSession(session, session)
    if (!parsedTracks.length) throw new Error('没有解析到可重封装的未加密 MP4 音视频样本')

    var videoStats = parsedKindStats(parsedTracks, 'video')
    var audioStats = parsedKindStats(parsedTracks, 'audio')
    var companions = []

    if (videoStats.count && !audioStats.count) {
      var audioCompanion = await findCompanionTracks(session, parsedTracks, 'audio')
      if (audioCompanion) {
        parsedTracks = parsedTracks.concat(audioCompanion.tracks)
        companions.push(audioCompanion.session.id)
        addWarning(session, '视频与音频位于不同 MediaSource 会话;已自动关联音频会话 #' + audioCompanion.session.id + '。')
      }
    } else if (audioStats.count && !videoStats.count) {
      var videoCompanion = await findCompanionTracks(session, parsedTracks, 'video')
      if (videoCompanion) {
        parsedTracks = parsedTracks.concat(videoCompanion.tracks)
        companions.push(videoCompanion.session.id)
        addWarning(session, '音频与视频位于不同 MediaSource 会话;已自动关联视频会话 #' + videoCompanion.session.id + '。')
      }
    }

    return { tracks: parsedTracks, companionSessionIds: companions }
  }

  async function exportMergedMp4(session) {
    if (!session.tracks.length) throw new Error('当前会话没有轨道')
    for (var pendingIndex = 0; pendingIndex < session.tracks.length; pendingIndex++) {
      if (session.tracks[pendingIndex].pendingOperations.length) throw new Error('仍有 appendBuffer 操作未完成,请稍后再导出')
    }

    ui.mergeButton.disabled = true
    ui.mergeButton.textContent = '正在分析原始媒体分片…'

    try {
      // YouTube 的原始轨道通常完整可播,但其分片布局会使样本级重封装丢失大量数据。
      // 优先保留原始 moof/mdat,仅修复初始化信息和随机访问索引。
      if (preferRawFragmentMux()) {
        try {
          var indexed = rawBuildIndexedFragmentedMp4(session)
          ui.mergeButton.textContent = '正在校验原始分片与跳转索引…'
          await validateIndexedFragmentedMp4(indexed)
          if (indexed.hasVideo && !indexed.hasAudio) {
            addWarning(session, '原始分片合并未找到可配对音频轨,输出文件将只有视频。')
          }
          addWarning(session, 'YouTube 已使用原始分片保留模式:未拆解媒体样本,并已生成 mfra/tfra 跳转索引。')
          downloadBlob(indexed.blob, session.title + '-indexed.mp4')
          return
        } catch (rawError) {
          if (rawError && rawError.captureIntegrity) {
            console.error('[MSE Extract Lite] 捕获数据不完整,已取消生成输出文件', rawError)
            addWarning(session, '捕获数据不完整:' + rawError.message + ';已取消生成 indexed/fixed MP4,请重新刷新页面捕获。')
            throw rawError
          }
          console.warn('[MSE Extract Lite] 原始分片保留模式失败,回退到样本级重封装', rawError)
          addWarning(session, '原始分片保留模式失败:' + rawError.message + ';已回退到普通重封装。')
        }
      }

      ui.mergeButton.textContent = '正在解析并查找音轨…'
      var collected = await collectExportTracks(session)
      var parsedTracks = collected.tracks
      var finalVideo = parsedKindStats(parsedTracks, 'video')
      var finalAudio = parsedKindStats(parsedTracks, 'audio')
      if (finalVideo.count && !finalAudio.count) {
        addWarning(session, '未捕获到可配对音频轨;该站点可能在 Worker、跨域 iframe 或非 MSE 音频链路中播放声音。')
      }

      ui.mergeButton.textContent = '正在重建音视频时间轴…'
      var blob = await buildAndValidateMp4(parsedTracks, session)
      downloadBlob(blob, session.title + '-fixed.mp4')
    } finally {
      ui.mergeButton.disabled = false
      ui.mergeButton.textContent = preferRawFragmentMux() ? '原始分片合并并下载 MP4' : '合并并下载 MP4'
      requestUIRefresh()
    }
  }

  window.__MSE_EXTRACT_LITE_DEBUG__ = {
    miniParseMp4: miniParseMp4,
    buildFlatMp4: buildFlatMp4,
    validateMp4Blob: validateMp4Blob,
    buildAndValidateMp4: buildAndValidateMp4,
    buildTrackMediaTimeline: buildTrackMediaTimeline,
    mergeTracksByKind: mergeTracksByKind,
    mergeSampleIntervals: mergeSampleIntervals,
    decoderConfigurationKey: decoderConfigurationKey,
    normalizeStrictGroup: normalizeStrictGroup,
    prepareOutputTracks: prepareOutputTracks,
    prepareMp4Inputs: prepareMp4Inputs,
    scanTopLevelBoxes: scanTopLevelBoxes,
    sessionToJson: sessionToJson,
    collectExportTracks: collectExportTracks,
    timingAffinity: timingAffinity,
    activityOverlapRatio: activityOverlapRatio,
    bufferedMetrics: bufferedMetrics,
    smartMediaElement: smartMediaElement,
    smartJumpTick: smartJumpTick,
    resetSmartJumpProgress: resetSmartJumpProgress,
    syncSmartJumpMedia: syncSmartJumpMedia,
    sessions: sessions,
    targetSession: targetSession,
    rawBuildTrackModel: rawBuildTrackModel,
    rawBuildIndexedFragmentedMp4: rawBuildIndexedFragmentedMp4,
    validateIndexedFragmentedMp4: validateIndexedFragmentedMp4
  }

  // 默认只显示捕获数量徽标;点击徽标后展开完整操作面板。
  scheduleDirectScan()
  requestUIRefresh()
  setTimeout(requestUIRefresh, 1000)
  setTimeout(requestUIRefresh, 3000)
  startRuntimeScheduler()

  console.info('[MSE Extract Lite 2.2.1] 已安装完整 appendBuffer 捕获、严格 mdat 校验、YouTube 索引输出、折叠徽标、直连识别与智能跳播')
})()