media-source-extract-lite

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

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

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 索引输出、折叠徽标、直连识别与智能跳播')
})()