/**
 * mov.js — MP4 → Apple Live Photo MOV 转换（纯字节手术，不重新编码）
 *
 * 处理内容：
 * 1. ftyp 品牌改写为 'qt  '（QuickTime MOV 标识）；
 * 2. 在 moov 内追加 meta box：
 *      meta (full box v0)
 *        hdlr  handler='mdta'
 *        keys  namespace='mdta', key="com.apple.quicktime.content.identifier"
 *        ilst  index=1 → data(type=UTF-8, locale=0) = UUID
 *    这是 Apple Photos 配对 Live Photo 所需的视频侧元数据；
 * 3. 在 moov 内追加 still-image-time 时间元数据轨道（与 iPhone 原生实况视频结构一致）：
 *      trak
 *        tkhd (flags=0xF, 不参与播放)
 *        edts → elst  空编辑(empty edit)把单一样本的呈现时刻定位到静态帧时刻
 *        mdia
 *          mdhd timescale=600 duration=1
 *          hdlr handler='mdta'
 *          minf → nmhd / dinf / stbl
 *            stsd → mebx → keys → 3 个键条目（still-image-time / live-photo-still-image-transform /
 *                    reference-dimensions），每条目 [size][index][keyd('mdta'+key)][dtyp(选择器0+well-known)]
 *            stts / stsc / stsz (1 个样本, 105 字节) / stco (指向尾部新增 mdat)
 *      样本数据（105 字节）：[9B: key1=0xFF(-1)][80B: key2=3×3 单应矩阵 9×float64][16B: key3=宽高 2×float32]
 *    静态帧时刻默认取视频时长中点（华为动态照片视频覆盖按下快门前后的时段）。
 * 4. 修正 stco/co64 偏移（插入新轨道造成的字节位移）；
 * 5. 将 mvhd/tkhd/mdhd 的创建/修改时间对齐到照片拍摄时间（照片库排序显示正确）；
 * 6. 更新 mvhd 的 next_track_ID。
 */

function u32(v) { return [(v >>> 24) & 255, (v >> 16) & 255, (v >> 8) & 255, v & 255]; }
function fourCC(s) { return [s.charCodeAt(0), s.charCodeAt(1), s.charCodeAt(2), s.charCodeAt(3)]; }
function box(type, payload) {
  return [...u32(payload.length + 8), ...fourCC(type), ...payload];
}
function fullBox(type, version, flags, payload) {
  return box(type, [version, (flags >> 16) & 255, (flags >> 8) & 255, flags & 255, ...payload]);
}
function readU32(b, o) { return ((b[o] << 24) | (b[b.length > o + 1 ? o + 1 : o] << 16) | (b[o + 2] << 8) | b[o + 3]) >>> 0; }

/** QuickTime 单位矩阵（9 × u32） */
const UNITY_MATRIX = [
  0x00010000, 0, 0,
  0, 0x00010000, 0,
  0, 0, 0x40000000
];

/** 遍历容器 box，返回子 box 列表 [{type, off, size, hdr}] */
function children(b, start, end) {
  const out = [];
  let off = start;
  while (off + 8 <= end) {
    let size = readU32(b, off);
    const type = String.fromCharCode(b[off + 4], b[off + 5], b[off + 6], b[off + 7]);
    let hdr = 8;
    if (size === 1) {
      size = Number((BigInt(readU32(b, off + 8)) << 32n) | BigInt(readU32(b, off + 12)));
      hdr = 16;
    } else if (size === 0) size = end - off;
    if (size < 8) break;
    out.push({ type, off, size, hdr });
    off += size;
  }
  return out;
}

/**
 * 构建 Apple QuickTime 元数据 meta box（moov 级）
 *
 * 写入 10 个 Apple QuickTime Keys（与 iPhone 15 Pro Max 原生参照同构）：
 *   1. com.apple.quicktime.location.accuracy.horizontal          (type=0x01, UTF-8 "5.000000")
 *   2. com.apple.quicktime.full-frame-rate-playback-intent      (type=0x15, int64 高位=0+低位=int32 BE)
 *   3. com.apple.quicktime.content.identifier                   (type=0x01, UUID 大写 36 字符)
 *   4. com.apple.quicktime.live-photo.vitality-score            (type=0x17, float32 BE)
 *   5. com.apple.quicktime.live-photo.vitality-scoring-version  (type=0x15, int64)
 *   6. com.apple.quicktime.location.ISO6709                     (type=0x01, GPS 字符串, 有 GPS 才写入)
 *   7. com.apple.quicktime.make                                 (type=0x01, "Apple")
 *   8. com.apple.quicktime.model                                (type=0x01, 用户设备型号)
 *   9. com.apple.quicktime.software                             (type=0x01, iOS 版本)
 *  10. com.apple.quicktime.creationdate                         (type=0x01, ISO 8601)
 *
 * 三处关键细节：
 * 1. meta 本身**不是** full box，payload 直接以 hdlr 开始。
 *    若写入 4 字节 version/flags，解析器会把这 4 个 0 当作"子 box size=0"
 *    而立即结束解析（exiftool 报 "Terminator found in Meta"），
 *    导致 ContentIdentifier 完全读不到，Live Photo 配对失败。
 * 2. keys 内的 key 值**不带** null 结尾（key_size = 8 + key 长度）。
 * 3. hdlr 为 32 字节，省略 name 字段。
 *
 * @param {string} uuid    ContentIdentifier (UUID 大写 36 字符)
 * @param {object} [opts]  { make, model, software, iso6709, photoDate }
 */
function buildMetaBox(uuid, opts) {
  opts = opts || {};
  const photoDate = opts.photoDate || new Date();
  const tzOffset = -photoDate.getTimezoneOffset();
  const tzSign = tzOffset >= 0 ? '+' : '-';
  const tzAbs = Math.abs(tzOffset);
  const tzHH = String(Math.floor(tzAbs / 60)).padStart(2, '0');
  const tzMM = String(tzAbs % 60).padStart(2, '0');
  const creationDate =
    photoDate.getFullYear() + '-' +
    String(photoDate.getMonth() + 1).padStart(2, '0') + '-' +
    String(photoDate.getDate()).padStart(2, '0') + 'T' +
    String(photoDate.getHours()).padStart(2, '0') + ':' +
    String(photoDate.getMinutes()).padStart(2, '0') + ':' +
    String(photoDate.getSeconds()).padStart(2, '0') +
    tzSign + tzHH + tzMM;

  // 10 个 keys（参照 iPhone 15 Pro Max 原生顺序）。无 GPS 时跳过 ISO 6709 和
  // location.accuracy.horizontal，保证 key 序号 1-based 与 ilst item.type 严格对应。
  const candidateKeys = [
    { key: 'com.apple.quicktime.location.accuracy.horizontal', type: 0x01, value: '5.000000', requiresGps: true },
    { key: 'com.apple.quicktime.full-frame-rate-playback-intent', type: 0x15, value: 1 },
    { key: 'com.apple.quicktime.content.identifier', type: 0x01, value: uuid },
    { key: 'com.apple.quicktime.live-photo.vitality-score', type: 0x17, value: 0.939828753471375 },
    { key: 'com.apple.quicktime.live-photo.vitality-scoring-version', type: 0x15, value: 4 },
    { key: 'com.apple.quicktime.location.ISO6709', type: 0x01, value: opts.iso6709, requiresGps: true },
    { key: 'com.apple.quicktime.make', type: 0x01, value: opts.make || 'Apple' },
    { key: 'com.apple.quicktime.model', type: 0x01, value: opts.model || 'iPhone' },
    { key: 'com.apple.quicktime.software', type: 0x01, value: opts.software || '17.0' },
    { key: 'com.apple.quicktime.creationdate', type: 0x01, value: creationDate },
  ];
  const active = candidateKeys.filter(k =>
    k.value !== undefined && k.value !== null && !(k.requiresGps && !opts.iso6709));

  // keys box：[full box hdr][count][key_atoms]
  const keyAtoms = active.map(k => {
    const keyBytes = [...new TextEncoder().encode(k.key)];
    return [...u32(8 + keyBytes.length), ...fourCC('mdta'), ...keyBytes];
  });
  const keys = fullBox('keys', 0, 0, [...u32(active.length), ...keyAtoms.flat()]);

  // ilst：每个 item = [size][type=key_index_1based][data box]
  //   data box = [size]['data'][type_code][locale=0][value_bytes]
  const items = active.map((k, i) => {
    let valueBytes;
    if (k.type === 0x01) {
      valueBytes = [...new TextEncoder().encode(String(k.value))];
    } else if (k.type === 0x15) {
      // signed int64 BE：参照写入格式 = [00 00 00 00][int32 BE] 共 8 字节
      //   高 4 字节 = 0，低 4 字节 = int32 值。exiftool 把 0x15 视为 int64s
      valueBytes = [...u32(0), ...u32((k.value | 0) >>> 0)];
    } else if (k.type === 0x17) {
      // float32 BE（参照 LivePhotoVitalityScore: 0x3F70989E = 0.9398...）
      valueBytes = f32be(k.value);
    }
    const dataBox = box('data', [...u32(k.type), ...u32(0), ...valueBytes]);
    return [...u32(8 + dataBox.length), ...u32(i + 1), ...dataBox];
  });
  const ilst = box('ilst', items.flat());

  // hdlr（meta 内）：34 字节，含 2 字节空 name
  const hdlr = fullBox('hdlr', 0, 0, [...u32(0), ...fourCC('mdta'), ...u32(0), ...u32(0), ...u32(0), 0, 0]);

  return box('meta', [...hdlr, ...keys, ...ilst]);
}

/**
 * 构建视频轨道内私有 meta box（5 个 Apple 相机 QuickTime Keys）
 *
 * 参照 iPhone 15 Pro Max 原生 apple.MOV 的视频轨道（即 moov/trak@N 的 mdia 后追加的 meta box），
 * 包含如下 5 个 key（用于声明"这段视频是用 iPhone 拍摄的"）：
 *   1. com.apple.quicktime.camera.lens_model                       (type=0x01, UTF-8 lens 描述字符串)
 *   2. com.apple.quicktime.camera.focal_length.35mm_equivalent     (type=0x01, UTF-8 数字字符串 "23")
 *   3. com.apple.quicktime.camera.lens_irisfnumber                 (type=0x01, UTF-8 光圈字符串 "F1.78")
 *   4. com.apple.quicktime.apple-maker-note.74                     (type=0x15, int64 高位=0 + 低位=int32 BE)
 *   5. com.apple.quicktime.apple-maker-note.97                     (type=0x15, int64 高位=0 + 低位=int32 BE)
 *
 * hdlr 与 moov meta 结构同构但 handler='mhlr'（参照 apple.MOV trak#1 meta @2531092 内 hdlr @2531100 size=34）。
 * 若调用方未提供 camera 参数，使用 iPhone 14/15 Pro Max 通用占位值，使 Synology 等识别器
 * 拿到完整的相机字段集以判定为 iPhone 拍摄的实况视频。
 *
 * @param {{
 *   lensModel?: string,           // 例 "iPhone 15 Pro Max back camera 6.765mm f/1.78"
 *   focal35mmEquivalent?: string, // 例 "23"
 *   lensIrisFNumber?: string,     // 例 "F1.78"
 *   makerNote74?: number,         // 默认 2
 *   makerNote97?: number,         // 默认 24
 * }} [opts]
 */
function buildTrakMetaBox(opts) {
  opts = opts || {};
  const lensModel = opts.lensModel || 'iPhone back camera 6.86mm f/1.78';
  const focal35  = opts.focal35mmEquivalent || '26';
  const irisF    = opts.lensIrisFNumber || 'F1.78';
  // apple-maker-note.74 / .97 参照 iPhone 15 Pro Max 原生写入 int32（4 字节 BE），
  // 与 moov meta 的 int64（8 字节 BE）格式不同。Synology 识别器按 type=0x15 自适应。
  // 实际取值参照 iPhone 原生（74=2 / 97=24），可能对应不同相机场景下的私有标志。
  const mn74     = (opts.makerNote74 != null) ? (opts.makerNote74 | 0) : 2;
  const mn97     = (opts.makerNote97 != null) ? (opts.makerNote97 | 0) : 24;

  // QuickTime locale = [region 2 字节 ASCII][language 15 位压缩]。
  // 参照 iPhone 原生为 0x4E5A15C7 = 'NZ'(0x4E5A) + 'eng'(e=5,n=14,g=7 → (5<<10)|(14<<5)|7 = 0x15C7)。
  // 仅三个 UTF-8 相机字段带 locale，两个 apple-maker-note 为 0，与参照逐字节一致。
  const LOCALE_ENG_NZ = 0x4E5A15C7;
  const entries = [
    { key: 'com.apple.quicktime.camera.lens_model',                    type: 0x01, value: lensModel, locale: LOCALE_ENG_NZ },
    { key: 'com.apple.quicktime.camera.focal_length.35mm_equivalent',  type: 0x01, value: focal35,   locale: LOCALE_ENG_NZ },
    { key: 'com.apple.quicktime.camera.lens_irisfnumber',              type: 0x01, value: irisF,     locale: LOCALE_ENG_NZ },
    { key: 'com.apple.quicktime.apple-maker-note.74',                  type: 0x15, value: mn74,      locale: 0 },
    { key: 'com.apple.quicktime.apple-maker-note.97',                  type: 0x15, value: mn97,      locale: 0 },
  ];

  // keys box: [full box hdr][count][key_atoms]
  const keyAtoms = entries.map(k => {
    const kb = [...new TextEncoder().encode(k.key)];
    return [...u32(8 + kb.length), ...fourCC('mdta'), ...kb];
  });
  const keys = fullBox('keys', 0, 0, [...u32(entries.length), ...keyAtoms.flat()]);

  // ilst: 每个 item = [size][type=key_index_1based_BE][data box]
  const items = entries.map((k, i) => {
    let valueBytes;
    if (k.type === 0x01) {
      valueBytes = [...new TextEncoder().encode(String(k.value))];
    } else if (k.type === 0x15) {
      // int32 BE（4 字节）—— 与参照 apple.MOV trak#1 ilst item[4]/[5] 一致；
      // 与 moov meta 的 int64（8 字节）不同，是 Apple QuickTime Key 在不同
      // 位置（moov 级 vs trak 级）的格式差异。
      valueBytes = [...u32((k.value | 0) >>> 0)];
    }
    const dataBox = box('data', [...u32(k.type), ...u32(k.locale || 0), ...valueBytes]);
    return [...u32(8 + dataBox.length), ...u32(i + 1), ...dataBox];
  });
  const ilst = box('ilst', items.flat());

  // hdlr 32 字节：handler='mhlr', reserved×3, 2-byte empty name slot（与 moov meta 同结构）
  const hdlr = fullBox('hdlr', 0, 0, [...u32(0), ...fourCC('mhlr'), ...u32(0), ...u32(0), ...u32(0), 0, 0]);

  return box('meta', [...hdlr, ...keys, ...ilst]);
}

/** still-image-time 样本数据（Apple 规定的 -1 标记，8 字节 box 头 + 1 字节负载） */
const STILL_IMAGE_SAMPLE = [...u32(9), 0, 0, 0, 1, 0xFF];

/** 大端 float64 / float32 序列化 */
function f64be(v) { const b = new ArrayBuffer(8); new DataView(b).setFloat64(0, v, false); return [...new Uint8Array(b)]; }
function f32be(v) { const b = new ArrayBuffer(4); new DataView(b).setFloat32(0, v, false); return [...new Uint8Array(b)]; }

/**
 * 构建 still-image-time 轨道的元数据样本（与 iPhone 原生 105 字节样本同构）：
 *   [size=9 ][local_key=1][0xFF]                      still-image-time = -1
 *   [size=80][local_key=2][9 × float64]               live-photo-still-image-transform（3×3 单应矩阵）
 *   [size=16][local_key=3][2 × float32]               reference-dimensions（静态图宽高）
 * 变换矩阵取单位阵：华为动态照片的视频与静态图视野一致（Apple 原生为轻微裁切缩放，
 * 该矩阵只影响实况回放的取景渲染，不影响配对识别）。
 * reference-dimensions 取视频轨道像素尺寸（iPhone 原生实测值 1920×1440 = 视频轨尺寸）。
 */
function buildStillImageTimeSample(videoWidth, videoHeight) {
  const identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
  const transform = identity.flatMap(f64be); // 72 字节
  const dims = [...f32be(videoWidth || 0), ...f32be(videoHeight || 0)]; // 8 字节
  return [
    ...STILL_IMAGE_SAMPLE,                       // 9 字节：key 1 = -1
    ...u32(8 + transform.length), ...u32(2), ...transform, // 80 字节：key 2
    ...u32(8 + dims.length), ...u32(3), ...dims            // 16 字节：key 3
  ];
}

/** Pascal 字符串（QuickTime hdlr name 格式：1 字节长度 + 内容，无结尾 null） */
function pascalStr(s) {
  const b = new TextEncoder().encode(s);
  return [b.length, ...b];
}

/**
 * 构建 still-image-time 时间元数据轨道
 * 结构经与 iPhone 12 原生实况视频（IMG_6234.MOV Track5）逐字节比对校准：
 *   trak
 *     tkhd  flags=0xF（enabled|inMovie|inPreview|inPoster），duration = 静态时刻 + 样本时长
 *     tref → cdsc(视频轨 ID)  ← 元数据轨道描述视频轨道（Apple 原生必备）
 *     edts → elst  两条编辑：空编辑(静态时刻, media_time=-1) + 样本(样本时长, media_time=0)
 *     mdia
 *       mdhd timescale=600 duration=1（单个样本）
 *       hdlr  predefined='mhlr', handler='meta', vendor='appl', flags=1, name="Core Media Metadata"
 *       minf
 *         gmhd → gmin (graphicsmode=0x40, opcolor=0x8000×3)
 *         hdlr  predefined='dhlr', handler='alis', vendor='appl', name="Core Media Data Handler"
 *         dinf → dref → 'alis' 自引用数据引用（QuickTime 原生，而非 ISO 的 'url '）
 *         stbl
 *           stsd → mebx → keys → key(local_key_id=1) → keyd('mdta'+key, 无 null) + dtyp(well-known 类型 65)
 *           stts / stsc / stsz(样本 9 字节) / stco
 *   样本数据：[size=9][type=local_key_id=1][0xFF]，0xFF 按 int8 解释为 -1
 *
 * @param {number} trackId 新轨道 ID
 * @param {number} videoTrackId 被描述的视频轨道 ID（tref/cdsc 指向它）
 * @param {number} movieDuration 影片总时长（moov/mvhd timescale 单位）
 * @param {number} stillUnits 静态帧时刻（moov/mvhd timescale 单位；0 表示首帧）
 * @param {number} movieTimescale moov/mvhd timescale（用于换算样本时长）
 * @param {number} [videoWidth] 视频轨像素宽（写入 reference-dimensions 键，取自视频 tkhd）
 * @param {number} [videoHeight] 视频轨像素高（写入 reference-dimensions 键，取自视频 tkhd）
 * @returns {{ bytes: number[], trackId: number }}
 */
function buildStillImageTimeTrak(trackId, videoTrackId, movieDuration, stillUnits, movieTimescale, videoWidth, videoHeight) {
  // 样本时长（媒体 timescale=600 下 1 tick），换算到影片 timescale
  const sampleDurMovieUnits = Math.max(1, Math.round((movieTimescale || 600) / 600));

  // --- tkhd (v0, flags=0xF：与 iPhone 原生一致，参与影片) ---
  const tkhd = fullBox('tkhd', 0, 0xF, [
    ...u32(0), ...u32(0),                     // creation / modification
    ...u32(trackId), ...u32(0),               // track_ID / reserved
    ...u32(stillUnits + sampleDurMovieUnits), // duration = 空编辑 + 样本
    0, 0, 0, 0, 0, 0, 0, 0,                  // reserved ×8
    0, 0,                                     // layer
    0, 0,                                     // alternate_group
    0, 0,                                     // volume = 0
    0, 0,                                     // reserved
    ...UNITY_MATRIX.flatMap(u32),
    ...u32(0), ...u32(0)                      // width / height = 0
  ]);

  // --- tref → cdsc：本轨道描述视频轨道 ---
  const tref = box('tref', [...box('cdsc', [...u32(videoTrackId)])]);

  // --- edts → elst：空编辑(静态时刻) + 样本编辑 ---
  const RATE = 0x00010000;
  const elstEntries = stillUnits > 0
    ? [
        ...u32(stillUnits), 0xFF, 0xFF, 0xFF, 0xFF, ...u32(RATE), // 空编辑（media_time = -1）
        ...u32(sampleDurMovieUnits), ...u32(0), ...u32(RATE)      // 样本（media_time = 0）
      ]
    : [...u32(sampleDurMovieUnits), ...u32(0), ...u32(RATE)];
  const elst = fullBox('elst', 0, 0, [...u32(stillUnits > 0 ? 2 : 1), ...elstEntries]);
  const edts = box('edts', [...elst]);

  // --- mdia：mdhd + hdlr('meta') + minf ---
  const mdhd = fullBox('mdhd', 0, 0, [
    ...u32(0), ...u32(0),            // creation / modification
    ...u32(600),                     // timescale
    ...u32(1),                       // duration = 1 tick（单个样本）
    0x55, 0xC4,                      // language 'und'
    0, 0                             // quality
  ]);
  // QuickTime 原生 hdlr：component type / subtype / manufacturer / flags / mask / Pascal name
  const mdiaHdlr = fullBox('hdlr', 0, 0, [
    ...fourCC('mhlr'), ...fourCC('meta'), ...fourCC('appl'),
    ...u32(1), ...u32(0),
    ...pascalStr('Core Media Metadata')
  ]);

  // --- minf：gmhd + dhlr + dinf + stbl ---
  const gmin = fullBox('gmin', 0, 0, [
    0x00, 0x40,                       // graphics mode = 64 (srcCopy)
    0x80, 0x00, 0x80, 0x00, 0x80, 0x00, // opcolor
    0x00, 0x00,                        // balance
    0x00, 0x00                         // reserved
  ]);
  const gmhd = box('gmhd', [...gmin]);
  const dhlr = fullBox('hdlr', 0, 0, [
    ...fourCC('dhlr'), ...fourCC('alis'), ...fourCC('appl'),
    ...u32(0), ...u32(0),
    ...pascalStr('Core Media Data Handler')
  ]);
  // QuickTime 原生数据引用：'alis' 自引用（flags=1），而非 ISO 的 'url '
  const dref = fullBox('dref', 0, 0, [...u32(1), ...fullBox('alis', 0, 1, [])]);
  const dinf = box('dinf', [...dref]);

  // --- stbl：stsd(mebx+keys/keyd/dtyp) + stts + stsc + stsz + stco ---
  // keys：与 iPhone 原生一致共 3 个键（still-image-time / transform / reference-dimensions）
  // 每个键条目：[size(4)][key_index(4)][keyd box][dtyp box]
  // keyd：'mdta' + key 字符串（无 null 结尾）
  // dtyp：16 字节 = [00 00 00 00][well-known 类型码]。前 4 字节为选择器（0 = 后接 well-known 码），
  //       缺失会被严格解析器按 UTI 长度误读（Synology Photos 解析失败的一个根因）
  const metaKeys = [
    { key: 'com.apple.quicktime.still-image-time', type: 65 },
    { key: 'com.apple.quicktime.live-photo-still-image-transform', type: 83 },
    { key: 'com.apple.quicktime.live-photo-still-image-transform-reference-dimensions', type: 71 }
  ];
  const keyAtoms = metaKeys.map((m, i) => {
    const keyStr = [...new TextEncoder().encode(m.key)];
    const keyd = box('keyd', [...fourCC('mdta'), ...keyStr]);
    const dtyp = box('dtyp', [0, 0, 0, 0, ...u32(m.type)]); // 选择器 0 + well-known 码，16 字节
    return [...u32(8 + keyd.length + dtyp.length), ...u32(i + 1), ...keyd, ...dtyp];
  });
  const keysBox = box('keys', keyAtoms.flat());
  const mebx = box('mebx', [0, 0, 0, 0, 0, 0, 0, 1, ...keysBox]); // reserved×6 + data_ref_idx=1
  const stsd = fullBox('stsd', 0, 0, [...u32(1), ...mebx]);

  const stts = fullBox('stts', 0, 0, [...u32(1), ...u32(1), ...u32(1)]);       // 1 个样本, delta=1 tick
  const stsc = fullBox('stsc', 0, 0, [...u32(1), ...u32(1), ...u32(1), ...u32(1)]);
  const sample = buildStillImageTimeSample(videoWidth, videoHeight);
  const stsz = fullBox('stsz', 0, 0, [...u32(0), ...u32(1), ...u32(sample.length)]);

  const stco = fullBox('stco', 0, 0, [...u32(1), ...u32(0)]); // 占位，装配后按 box 结构定位回填

  const stbl = box('stbl', [...stsd, ...stts, ...stsc, ...stsz, ...stco]);
  const minf = box('minf', [...gmhd, ...dhlr, ...dinf, ...stbl]);
  const mdia = box('mdia', [...mdhd, ...mdiaHdlr, ...minf]);

  const trak = box('trak', [...tkhd, ...tref, ...edts, ...mdia]);
  return { bytes: trak, trackId };
}

/** Mac epoch (1904) 秒 */
function macEpoch(date) {
  return Math.floor(date.getTime() / 1000) - 2082844800;
}

/** 读取 mvhd 信息：timescale / duration / next_track_ID 及各字段偏移 */
function readMvhd(b, mvhd) {
  const version = b[mvhd.off + 8];
  if (version === 1) {
    return {
      timescale: readU32(b, mvhd.off + 8 + 20),
      duration: Number((BigInt(readU32(b, mvhd.off + 8 + 24)) << 32n) | BigInt(readU32(b, mvhd.off + 8 + 28))),
      nextTrackIdOff: mvhd.off + 8 + 108
    };
  }
  return {
    timescale: readU32(b, mvhd.off + 8 + 12),
    duration: readU32(b, mvhd.off + 8 + 16),
    nextTrackIdOff: mvhd.off + 8 + 96
  };
}

/**
 * 将 MP4 字节转为带 Live Photo 元数据的 MOV
 * @param {Uint8Array} mp4
 * @param {string} uuid ContentIdentifier（与照片侧一致）
 * @param {Date|null} photoDate 照片拍摄时间（用于对齐创建时间与 creationdate 字段）
 * @param {{stillTimeSeconds?: number, photoWidth?: number, photoHeight?: number,
 *          exifMeta?: { make?: string, model?: string, software?: string, iso6709?: string|null },
 *          camera?: { lensModel?: string, focal35mmEquivalent?: string, lensIrisFNumber?: string,
 *                     makerNote74?: number, makerNote97?: number }}} [opts]
 *          静态帧时刻、静态图像素尺寸、从原始 EXIF 提取的 Make/Model/Software/GPS、相机镜头参数（写入 video trak meta）
 * @returns {{ bytes: Uint8Array, warnings: string[], stillTimeSeconds: number }}
 */
/**
 * 将文件头的 ftyp 规范化为 Apple QuickTime 标准形态：
 *   size=20, major='qt  ', minor=0, compatible=['qt  ']
 *
 * 参照 iPhone 原生 apple.MOV 实测（前 20 字节完全一致）。原始 MP4 的 ftyp 常带
 * 3~4 个兼容品牌（isom / iso2 / avc1 / mp41…），若只是把每个槽位逐个改写为
 * 'qt  '，会留下 [qt  , qt  , qt  , qt  ] 这种与真机不同构的冗余列表。
 *
 * ftyp 位于文件开头，尺寸变化会让后续内容整体位移，因此必须同步修正
 * moov 内所有 stco / co64 的 chunk 偏移。本函数只在转换入口调用一次，
 * 此时尚未插入任何新轨道，故所有 stco 均为原始偏移，可无条件平移。
 *
 * @param {Uint8Array} mp4
 * @param {string[]} warnings
 * @returns {Uint8Array} 规范化后的字节（若本来就合规则原样返回）
 */
export function normalizeFtypToQuickTime(mp4, warnings) {
  const top = children(mp4, 0, mp4.length);
  const ftyp = top.find(c => c.type === 'ftyp');
  if (!ftyp || ftyp.off !== 0) return mp4; // 无 ftyp 或不在文件头：保持原样

  const STANDARD_SIZE = 20;
  const qt = fourCC('qt  ');
  const expect = [...qt, 0, 0, 0, 0, ...qt]; // major + minor(0) + 1 个兼容品牌

  if (ftyp.size === STANDARD_SIZE) {
    const out = new Uint8Array(mp4);
    out.set(new Uint8Array(expect), 8);
    return out;
  }

  const delta = STANDARD_SIZE - ftyp.size; // 通常为负（收缩）
  const out = new Uint8Array(mp4.length + delta);
  const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
  dv.setUint32(0, STANDARD_SIZE);
  out.set(new Uint8Array([...fourCC('ftyp'), ...expect]), 4);
  out.set(mp4.subarray(ftyp.size), STANDARD_SIZE);

  // 平移所有既有 chunk 偏移
  const moov = children(out, 0, out.length).find(c => c.type === 'moov');
  if (!moov) {
    warnings.push('ftyp 规范化后未找到 moov，stco/co64 偏移未修正');
    return out;
  }
  for (const trak of children(out, moov.off + 8, moov.off + moov.size)) {
    if (trak.type !== 'trak') continue;
    const mdia = children(out, trak.off + 8, trak.off + trak.size).find(x => x.type === 'mdia');
    if (!mdia) continue;
    const minf = children(out, mdia.off + 8, mdia.off + mdia.size).find(x => x.type === 'minf');
    if (!minf) continue;
    const stbl = children(out, minf.off + 8, minf.off + minf.size).find(x => x.type === 'stbl');
    if (!stbl) continue;
    for (const s of children(out, stbl.off + 8, stbl.off + stbl.size)) {
      if (s.type !== 'stco' && s.type !== 'co64') continue;
      const entryCount = readU32(out, s.off + 12);
      const fieldSize = s.type === 'stco' ? 4 : 8;
      for (let i = 0; i < entryCount; i++) {
        const p = s.off + 16 + i * fieldSize;
        if (s.type === 'stco') {
          const v = readU32(out, p) + delta;
          if (v >= 0) dv.setUint32(p, v);
        } else {
          const v = Number(dv.getBigUint64(p)) + delta;
          if (v >= 0) dv.setBigUint64(p, BigInt(v));
        }
      }
    }
  }
  return out;
}

/**
 * 极简模式用：把 ftyp 规范化为标准 MP4（mp42 主品牌，兼容 isom/mp42）。
 *
 * 这是关键修复点：参照 makelivephoto.com 的可用产物其视频是 **普通 mp42/isom MP4**
 * （ISO 14496-14，24 字节 ftyp：major=mp42, minor=0, compat=[mp42,isom]），
 * 而早期我们把 ftyp 改成 QuickTime 的 'qt  ' 并把文件命名为 .mov —— 实测
 * **Synology Photos 对 'qt  ' 品牌的视频补帧完全不识别**，这才是 .pvt 导入失败的根因。
 * 改用 mp42/isom 命名 .mp4 后，与参照逐字节同构、可被正常配对。
 *
 * 兼容品牌顺序严格按参照：['mp42','isom']。
 */
export function normalizeFtypToMp4(mp4, warnings) {
  const top = children(mp4, 0, mp4.length);
  const ftyp = top.find(c => c.type === 'ftyp');
  if (!ftyp || ftyp.off !== 0) return mp4; // 无 ftyp 或不在文件头：保持原样

  const expect = [
    ...fourCC('mp42'), 0, 0, 0, 0,  // major=mp42, minor=0
    ...fourCC('mp42'), ...fourCC('isom') // 兼容品牌 [mp42, isom]
  ];
  const NEW_SIZE = 8 + expect.length; // 24
  const delta = NEW_SIZE - ftyp.size;

  if (delta === 0) {
    const out = new Uint8Array(mp4);
    out.set(new Uint8Array(expect), 8);
    return out;
  }

  const out = new Uint8Array(mp4.length + delta);
  const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
  dv.setUint32(0, NEW_SIZE);
  out.set(new Uint8Array([...fourCC('ftyp'), ...expect]), 4);
  out.set(mp4.subarray(ftyp.size), NEW_SIZE);

  // 平移所有既有 chunk 偏移
  const moov = children(out, 0, out.length).find(c => c.type === 'moov');
  if (!moov) {
    warnings.push('ftyp 规范化后未找到 moov，stco/co64 偏移未修正');
    return out;
  }
  for (const trak of children(out, moov.off + 8, moov.off + moov.size)) {
    if (trak.type !== 'trak') continue;
    const mdia = children(out, trak.off + 8, trak.off + trak.size).find(x => x.type === 'mdia');
    if (!mdia) continue;
    const minf = children(out, mdia.off + 8, mdia.off + mdia.size).find(x => x.type === 'minf');
    if (!minf) continue;
    const stbl = children(out, minf.off + 8, minf.off + minf.size).find(x => x.type === 'stbl');
    if (!stbl) continue;
    for (const s of children(out, stbl.off + 8, stbl.off + stbl.size)) {
      if (s.type !== 'stco' && s.type !== 'co64') continue;
      const entryCount = readU32(out, s.off + 12);
      const fieldSize = s.type === 'stco' ? 4 : 8;
      for (let i = 0; i < entryCount; i++) {
        const p = s.off + 16 + i * fieldSize;
        if (s.type === 'stco') {
          const v = readU32(out, p) + delta;
          if (v >= 0) dv.setUint32(p, v);
        } else {
          const v = Number(dv.getBigUint64(p)) + delta;
          if (v >= 0) dv.setBigUint64(p, BigInt(v));
        }
      }
    }
  }
  return out;
}

export function mp4ToLivePhotoMov(mp4, uuid, photoDate, opts) {
  const warnings = [];
  // 入口处先规范化 ftyp（会同步修正 stco），后续所有偏移计算都基于规范化后的字节
  const b = normalizeFtypToQuickTime(mp4, warnings);
  const top = children(b, 0, b.length);
  if (!top.some(c => c.type === 'moov')) throw new Error('MP4 中未找到 moov box');
  if (!top.some(c => c.type === 'ftyp')) warnings.push('未找到 ftyp box（仍尝试转换）');

  const moov = top.find(c => c.type === 'moov');
  const insertPos = moov.off + moov.size;
  const moovChildren = children(b, moov.off + 8, moov.off + moov.size);

  // mvhd：时长 / timescale / next_track_ID
  const mvhd = moovChildren.find(c => c.type === 'mvhd');
  let movieTimescale = 600, movieDuration = 0, nextTrackIdOff = -1;
  if (mvhd) {
    const info = readMvhd(b, mvhd);
    movieTimescale = info.timescale || 600;
    movieDuration = (info.duration === 0xFFFFFFFF || info.duration > 0x7FFFFFFF) ? 0 : info.duration;
    nextTrackIdOff = info.nextTrackIdOff;
  } else {
    warnings.push('未找到 mvhd box，元数据轨道时长按 0 处理');
  }

  // 已有轨道 ID（tkhd v0/v1 的 track_ID 均位于 version/flags 后第 12 字节）
  let maxTrackId = 0;
  let videoTrackId = 0; // tref/cdsc 指向的视频轨道
  for (const c of moovChildren) {
    if (c.type === 'trak') {
      const subs = children(b, c.off + 8, c.off + c.size);
      const tkhd = subs.find(x => x.type === 'tkhd');
      if (tkhd && tkhd.size >= 8 + 16) {
        const id = readU32(b, tkhd.off + 8 + 12);
        maxTrackId = Math.max(maxTrackId, id);
        if (!videoTrackId) {
          // 判断是否视频轨：mdia/hdlr handler = 'vide'
          const mdia = subs.find(x => x.type === 'mdia');
          if (mdia) {
            for (const m of children(b, mdia.off + 8, mdia.off + mdia.size)) {
              if (m.type === 'hdlr' && m.size >= 20) {
                const h = String.fromCharCode(b[m.off + 16], b[m.off + 17], b[m.off + 18], b[m.off + 19]);
                if (h === 'vide') { videoTrackId = id; break; }
              }
            }
          }
          if (!videoTrackId) videoTrackId = id; // 回退：第一个轨道
        }
      }
    }
  }
  const newTrackId = maxTrackId + 1;
  if (!videoTrackId) videoTrackId = 1;

  // 视频轨尺寸（tkhd 尾部 width/height，16.16 定点数；缺失时回退 stsd 样本条目 avc1/hvc1 的宽高）：
  // iPhone 原生 reference-dimensions 键存的是视频像素尺寸（参照实测 1920×1440 = 视频轨尺寸）
  let videoW = (opts && opts.photoWidth) || 0, videoH = (opts && opts.photoHeight) || 0;
  for (const c of moovChildren) {
    if (c.type !== 'trak') continue;
    const subs = children(b, c.off + 8, c.off + c.size);
    const tkhd = subs.find(x => x.type === 'tkhd');
    if (!tkhd || readU32(b, tkhd.off + 8 + 12) !== videoTrackId) continue;
    const ver = b[tkhd.off + 8];
    const fixedOff = tkhd.off + 8 + (ver === 1 ? 92 : 80); // v1 时间字段多 8 字节
    if (tkhd.size >= (fixedOff - tkhd.off) + 8) {
      const w = readU32(b, fixedOff) / 65536, h = readU32(b, fixedOff + 4) / 65536;
      if (w > 0 && h > 0) { videoW = w; videoH = h; }
    }
    // 后备：stsd 首个样本条目（avc1/hvc1 等）的 width/height（u16，位于条目头 +24/+26）
    if (!(videoW > 0 && videoH > 0)) {
      const mdia = subs.find(x => x.type === 'mdia');
      if (mdia) {
        for (const m of children(b, mdia.off + 8, mdia.off + mdia.size)) {
          if (m.type !== 'minf') continue;
          for (const n of children(b, m.off + 8, m.off + m.size)) {
            if (n.type !== 'stbl') continue;
            for (const q of children(b, n.off + 8, n.off + n.size)) {
              if (q.type !== 'stsd' || q.size < 8 + 8 + 28) continue;
              const entryOff = q.off + 8 + 8; // 跳过 ver/flags + entry_count
              const w = (b[entryOff + 24] << 8) | b[entryOff + 25];
              const h = (b[entryOff + 26] << 8) | b[entryOff + 27];
              if (w > 0 && h > 0) { videoW = w; videoH = h; }
            }
          }
        }
      }
    }
    break;
  }

  // 静态帧时刻：显式指定 > 视频时长中点
  let stillUnits;
  if (opts && typeof opts.stillTimeSeconds === 'number' && isFinite(opts.stillTimeSeconds) && opts.stillTimeSeconds > 0) {
    stillUnits = Math.min(Math.floor(opts.stillTimeSeconds * movieTimescale), movieDuration > 0 ? movieDuration : 0);
  } else {
    stillUnits = movieDuration > 0 ? Math.floor(movieDuration / 2) : 0;
  }
  const stillTimeSeconds = movieTimescale > 0 ? stillUnits / movieTimescale : 0;

  // 构建 meta box（10 个 QuickTime Keys）与 still-image-time 轨道
  const exifMeta = (opts && opts.exifMeta) || {};
  const metaOpts = {
    make: exifMeta.make || 'Apple',
    model: exifMeta.model || '',
    software: exifMeta.software || '',
    iso6709: exifMeta.iso6709 || null,
    photoDate: photoDate || new Date(),
  };
  // 若调用方未提供 Model/Software（华为原始 EXIF 不含 Apple 信息），默认填入
  // 与 iPhone 15 Pro Max 参照一致的值，使识别器拿到完整字段集。
  if (!metaOpts.model) metaOpts.model = 'iPhone';
  if (!metaOpts.software) metaOpts.software = '17.0';
  const metaBytes = buildMetaBox(uuid, metaOpts);
  const { bytes: trakBytes } = buildStillImageTimeTrak(newTrackId, videoTrackId, movieDuration, stillUnits, movieTimescale,
    videoW, videoH);
  const insertBytes = [...trakBytes, ...metaBytes];

  // 组装输出：原文件 [0, insertPos) + 新轨道 + meta + 其余 + 尾部新 mdat
  const sample = buildStillImageTimeSample(videoW, videoH);
  const delta = insertBytes.length;
  const newMdat = box('mdat', [...sample]);
  let out = new Uint8Array(b.length + delta + newMdat.length);
  out.set(b.subarray(0, insertPos), 0);
  out.set(insertBytes, insertPos);
  out.set(b.subarray(insertPos), insertPos + delta);
  out.set(newMdat, b.length + delta);

  const dv = new DataView(out.buffer);
  // 新轨道 stco → 指向尾部 mdat 内的样本（按 box 结构定位，避免手工累加偏移）
  const sampleAbsOffset = b.length + delta + 8; // 跳过 mdat box 头
  {
    const insertedTrak = children(out, moov.off + 8, moov.off + moov.size + delta)
      .find(c => {
        if (c.type !== 'trak' || c.off < insertPos || c.off >= insertPos + delta) return false;
        const subs = children(out, c.off + 8, c.off + c.size);
        const tkhd = subs.find(x => x.type === 'tkhd');
        return tkhd && readU32(out, tkhd.off + 8 + 12) === newTrackId;
      });
    if (insertedTrak) {
      const t = children(out, insertedTrak.off + 8, insertedTrak.off + insertedTrak.size)
        .find(x => x.type === 'mdia');
      const m = t && children(out, t.off + 8, t.off + t.size).find(x => x.type === 'minf');
      const s = m && children(out, m.off + 8, m.off + m.size).find(x => x.type === 'stbl');
      const stco = s && children(out, s.off + 8, s.off + s.size).find(x => x.type === 'stco');
      if (stco) dv.setUint32(stco.off + 16, sampleAbsOffset);
      else warnings.push('未能定位新轨道 stco（元数据样本偏移未写入）');
    } else {
      warnings.push('未能定位新插入的元数据轨道');
    }
  }

  // 更新 moov size（新轨道与 meta 均成为 moov 子 box）
  dv.setUint32(moov.off, moov.size + delta);

  // 更新 mvhd next_track_ID
  if (nextTrackIdOff >= 0 && readU32(out, nextTrackIdOff) <= newTrackId) {
    dv.setUint32(nextTrackIdOff, newTrackId + 1);
  }

  // ftyp 品牌改写为 QuickTime：major='qt  '，minor=0，所有兼容品牌槽位='qt  '。
  // 标准形态（size=20、仅 1 个 'qt  ' 兼容品牌）已在入口 normalizeFtypToQuickTime 完成，
  // 此处为幂等保险：确保即便走了非常规分支，品牌也一定是 QuickTime 而非 isom/avc1。
  const ftyp = top.find(c => c.type === 'ftyp');
  if (ftyp && ftyp.off === 0) {
    const qt = [...fourCC('qt  ')];
    const minorOff = ftyp.off + 12;
    dv.setUint32(minorOff, 0);
    for (let p = ftyp.off + 16; p + 4 <= ftyp.off + ftyp.size; p += 4) out.set(qt, p);
    out.set(qt, ftyp.off + 8);
  }

  // 视频轨道 tkhd flags 提升为 0xF（enabled|inMovie|inPreview|inPoster，与 iPhone 原生一致）
  {
    const videoTrak = moovChildren.find(c => {
      if (c.type !== 'trak') return false;
      const subs = children(b, c.off + 8, c.off + c.size);
      const tkhd = subs.find(x => x.type === 'tkhd');
      return tkhd && readU32(b, tkhd.off + 8 + 12) === videoTrackId;
    });
    if (videoTrak) {
      const tkhd = children(out, videoTrak.off + 8, videoTrak.off + videoTrak.size).find(x => x.type === 'tkhd');
      if (tkhd) {
        const flagsOff = tkhd.off + 8 + 1;
        dv.setUint8(flagsOff + 0, 0x00); dv.setUint8(flagsOff + 1, 0x00); dv.setUint8(flagsOff + 2, 0x0F);
      }
    }
  }

  // 修正已有 stco/co64 偏移：遍历所有 trak（新轨道 stco 已是绝对值，不经过此循环）
  const newMoovChildren = children(out, moov.off + 8, moov.off + moov.size + delta);
  let fixedStco = 0;
  function fixStbl(stbl) {
    const subs = children(out, stbl.off + 8, stbl.off + stbl.size);
    for (const s of subs) {
      if (s.type === 'stco' || s.type === 'co64') {
        const entryCount = dv.getUint32(s.off + 12);
        const fieldSize = s.type === 'stco' ? 4 : 8;
        let p = s.off + 16;
        for (let i = 0; i < entryCount && p + fieldSize <= s.off + s.size; i++) {
          if (s.type === 'stco') {
            const v = dv.getUint32(p);
            if (v >= insertPos) { dv.setUint32(p, v + delta); fixedStco++; }
          } else {
            const v = dv.getBigUint64(p);
            if (v >= BigInt(insertPos)) { dv.setBigUint64(p, v + BigInt(delta)); fixedStco++; }
          }
          p += fieldSize;
        }
      }
    }
  }
  for (const c of newMoovChildren) {
    if (c.type === 'trak') {
      const trakSubs = children(out, c.off + 8, c.off + c.size);
      // 跳过新插入的 still-image-time 轨道（其 stco 已为最终绝对值）
      const isNew = trakSubs.find(x => x.type === 'tkhd') &&
        readU32(out, trakSubs.find(x => x.type === 'tkhd').off + 8 + 12) === newTrackId;
      if (isNew) continue;
      const mdia = trakSubs.find(x => x.type === 'mdia');
      if (mdia) {
        const mdiaSubs = children(out, mdia.off + 8, mdia.off + mdia.size);
        const minf = mdiaSubs.find(x => x.type === 'minf');
        if (minf) {
          const minfSubs = children(out, minf.off + 8, minf.off + minf.size);
          const stbl = minfSubs.find(x => x.type === 'stbl');
          if (stbl) fixStbl(stbl);
        }
      }
    }
  }
  if (fixedStco === 0) warnings.push('未修正任何 chunk 偏移（若 moov 位于 mdat 之后属正常）');

  // 时间对齐：mvhd / tkhd / mdhd
  if (photoDate) {
    const macTime = macEpoch(photoDate);
    if (macTime > 0 && macTime < 0xffffffff) {
      function patchTimes(boxObj) {
        const version = out[boxObj.off + 8];
        let p = boxObj.off + 12;
        if (version === 1) {
          dv.setBigUint64(p, BigInt(macTime)); dv.setBigUint64(p + 8, BigInt(macTime));
        } else {
          dv.setUint32(p, macTime); dv.setUint32(p + 4, macTime);
        }
      }
      for (const c of newMoovChildren) {
        if (c.type === 'mvhd') patchTimes(c);
        if (c.type === 'trak') {
          const subs = children(out, c.off + 8, c.off + c.size);
          for (const s of subs) {
            if (s.type === 'tkhd' || s.type === 'mdia') {
              const s2 = children(out, s.off + 8, s.off + s.size);
              for (const x of s2) if (x.type === 'mdhd' || x.type === 'tkhd') patchTimes(x);
              if (s.type === 'tkhd') patchTimes(s);
            }
          }
        }
      }
    }
  }

  // ====================================================================
  // Phase 2: 视频轨内注入 5 个 Apple 相机 QuickTime Keys meta box
  // ====================================================================
  // 参照 iPhone 15 Pro Max 原生 apple.MOV：视频轨的 mdia 后追加一个 meta box，含 5 个
  // Apple 相机私有 Keys（lens_model / focal_length.35mm_equivalent / lens_irisfnumber /
  // apple-maker-note.74 / 97）。Synology / Apple Photos 用它们判断视频是否由 iPhone 拍摄。
  // 此处必须在 Phase 1 之后执行，因为视频轨内容与 mvhd/ftyp 修改已完成，
  // 重新切片整个 out 缓冲即可，不必重建所有原 box。
  const cameraOpts = (opts && opts.camera) || {};
  const trakMetaBytes = buildTrakMetaBox(cameraOpts);
  const metaDelta = trakMetaBytes.length;

  // 找到当前 out 中视频轨（已含 trak flags 升级、stco 偏移修正）
  const videoTrakPhase2 = children(out, moov.off + 8, moov.off + moov.size + delta).find(c => {
    if (c.type !== 'trak') return false;
    const subs = children(out, c.off + 8, c.off + c.size);
    const tkhd = subs.find(x => x.type === 'tkhd');
    return tkhd && readU32(out, tkhd.off + 8 + 12) === videoTrackId;
  });
  // Phase 1 追加的 still-image-time 轨道（其 stco 写入了绝对样本偏移）
  const stillImageTimeTrak = children(out, moov.off + 8, moov.off + moov.size + delta).find(c => {
    if (c.type !== 'trak') return false;
    const subs = children(out, c.off + 8, c.off + c.size);
    const tkhd = subs.find(x => x.type === 'tkhd');
    return tkhd && readU32(out, tkhd.off + 8 + 12) === newTrackId;
  });

  if (videoTrakPhase2 && metaDelta > 0) {
    const insertP2 = videoTrakPhase2.off + videoTrakPhase2.size;
    // 重新分配缓冲区，把 trakMeta 切片到视频轨末尾（仍属于 trak 子 box，结构合法）
    const out2 = new Uint8Array(out.length + metaDelta);
    out2.set(out.subarray(0, insertP2), 0);
    out2.set(trakMetaBytes, insertP2);
    out2.set(out.subarray(insertP2), insertP2 + metaDelta);

    const dv2 = new DataView(out2.buffer);
    // 视频轨 size += metaDelta，moov size += metaDelta
    dv2.setUint32(videoTrakPhase2.off, videoTrakPhase2.size + metaDelta);
    dv2.setUint32(moov.off, dv2.getUint32(moov.off) + metaDelta);

    // 修正 stco/co64：插入点之后的样本偏移都需再 +metaDelta
    function fixStblPhase2(stbl) {
      const subs = children(out2, stbl.off + 8, stbl.off + stbl.size);
      for (const s of subs) {
        if (s.type === 'stco' || s.type === 'co64') {
          const entryCount = dv2.getUint32(s.off + 12);
          const fieldSize = s.type === 'stco' ? 4 : 8;
          let p = s.off + 16;
          for (let i = 0; i < entryCount && p + fieldSize <= s.off + s.size; i++) {
            if (s.type === 'stco') {
              const v = dv2.getUint32(p);
              if (v >= insertP2) dv2.setUint32(p, v + metaDelta);
            } else {
              const v = dv2.getBigUint64(p);
              if (v >= BigInt(insertP2)) dv2.setBigUint64(p, v + BigInt(metaDelta));
            }
            p += fieldSize;
          }
        }
      }
    }
    // 遍历所有已有 trak 的 stbl 进行偏移修正（视频轨 + still-image-time 都在内）
    const allTraksP2 = children(out2, moov.off + 8, moov.off + moov.size + delta + metaDelta);
    for (const c of allTraksP2) {
      if (c.type !== 'trak') continue;
      const subs = children(out2, c.off + 8, c.off + c.size);
      const mdia = subs.find(x => x.type === 'mdia');
      if (!mdia) continue;
      const minf = children(out2, mdia.off + 8, mdia.off + mdia.size).find(x => x.type === 'minf');
      if (!minf) continue;
      const stbl = children(out2, minf.off + 8, minf.off + minf.size).find(x => x.type === 'stbl');
      if (stbl) fixStblPhase2(stbl);
    }

    out = out2;
  } else if (!videoTrakPhase2) {
    warnings.push('Phase 2: 未找到视频轨，跳过 camera meta 注入');
  }

  return { bytes: out, warnings, stillTimeSeconds };
}

/**
 * 构建"极简" Apple QuickTime 元数据 meta box——默认只写 1 个键：
 *   com.apple.quicktime.content.identifier = UUID
 *
 * 这是 makelive（RhetTbull）/ makelivephoto.com 实际采用的配对写法：
 * 仅在 moov 级 meta 内放 ContentIdentifier，不写其他 QuickTime Keys、
 * 不写 5 个 video trak Keys、不加 still-image-time 轨道。
 *
 * 经验证据：Synology Photos 的 Live Photo 配对检测器对"仅 ContentIdentifier 匹配"
 * 的极简写法稳定识别；任何额外展示键（make/model/creationdate/location）都会
 * 导致不识别（第 8 轮实测回退）。
 *
 * 可选保留拍摄地点：当 opts.iso6709 非空时，追加第二个 key
 *   com.apple.quicktime.location.ISO6709 = ISO 6709 字符串。
 * 注意：加 location key 会偏离 makelivephoto.com 参照（其 1.mp4 仅 ContentIdentifier），
 * 可能破坏 Synology 识别——因此默认不传 iso6709 时保持纯极简（仅 1 key）。
 *
 * @param {string} uuid ContentIdentifier（与照片侧一致）
 * @param {{ iso6709?: string|null }} [opts] 可选拍摄地点（ISO 6709 字符串）
 */
function buildMetaBoxMinimal(uuid, opts) {
  opts = opts || {};
  const entries = [
    { key: 'com.apple.quicktime.content.identifier', value: uuid },
  ];
  if (opts.iso6709) {
    entries.push({ key: 'com.apple.quicktime.location.ISO6709', value: opts.iso6709 });
  }
  const keyAtoms = entries.map(k => {
    const keyBytes = [...new TextEncoder().encode(k.key)];
    return [...u32(8 + keyBytes.length), ...fourCC('mdta'), ...keyBytes];
  });
  const keys = fullBox('keys', 0, 0, [...u32(entries.length), ...keyAtoms.flat()]);
  const items = entries.map((k, i) => {
    const valueBytes = [...new TextEncoder().encode(String(k.value))];
    const dataBox = box('data', [...u32(0x01), ...u32(0), ...valueBytes]);
    return [...u32(8 + dataBox.length), ...u32(i + 1), ...dataBox];
  });
  const ilst = box('ilst', items.flat());
  const hdlr = fullBox('hdlr', 0, 0, [...u32(0), ...fourCC('mdta'), ...u32(0), ...u32(0), ...u32(0), 0, 0]);
  return box('meta', [...hdlr, ...keys, ...ilst]);
}

/**
 * 极简配对转换（makelive 兼容 / 推荐用于 Synology Photos）
 *
 * 与 mp4ToLivePhotoMov（完整 iPhone 原生模式）的区别：
 *  - ✅ 规范化 ftyp 为**标准 MP4（mp42/isom）**（与 makelivephoto.com 参照一致；
 *       关键：不能用 'qt  ' 品牌，否则 Synology Photos 不识别该视频补帧）；
 *  - ✅ 在 moov 内注入 meta box，仅含 ContentIdentifier（配对必需）；
 *  - ✅ 视频文件以 .mp4 扩展名导出（非 .mov）；
 *  - ❌ 不写 still-image-time 时间元数据轨道；
 *  - ❌ 不写其他 moov QuickTime Keys（make/model/creationdate 等——实测会破坏 Synology 识别）；
 *  - ❌ 不写 5 个 video trak 相机 Keys。
 * 视频流本身零改动（不重新编码）。
 *
 * 返回 { bytes, warnings, stillTimeSeconds }，与 mp4ToLivePhotoMov 同构便于调用方统一。
 *
 * @param {Uint8Array} mp4
 * @param {string} uuid ContentIdentifier（与照片侧一致）
 * @param {Date|null} [photoDate] 拍摄时间（写入 mvhd CreateDate/ModifyDate）
 * @param {object} [opts] { exifMeta?: { iso6709?: string|null } } 可选拍摄地点（写入 location.ISO6709 key）
 */
export function mp4ToLivePhotoMovMinimal(mp4, uuid, photoDate, opts) {
  const warnings = [];
  const exifMeta = (opts && opts.exifMeta) || {};
  // 1) 规范化 ftyp 为 mp42/isom（同步修正 stco/co64）—— 不能用 qt 品牌
  let b = normalizeFtypToMp4(mp4, warnings);
  let top = children(b, 0, b.length);
  let moov = top.find(c => c.type === 'moov');
  if (!moov) throw new Error('MP4 中未找到 moov box');

  // 2) 删除 moov 内 udta（源视频可能自带 udta/meta，会与我们的 moov/meta 重复，干扰 Synology 检测器）
  //    REF MP4 只有 1 个 moov/meta，没有 udta/meta
  {
    const mc = children(b, moov.off + 8, moov.off + moov.size);
    const udta = mc.find(c => c.type === 'udta');
    if (udta) {
      const before = b.subarray(0, udta.off);
      const after = b.subarray(udta.off + udta.size);
      const nb = new Uint8Array(b.length - udta.size);
      nb.set(before, 0);
      nb.set(after, udta.off);
      // 修正 moov size
      const ndv = new DataView(nb.buffer);
      ndv.setUint32(moov.off, moov.size - udta.size);
      // 修正 stco/co64：udta 之后的样本偏移需 -udta.size
      const nmc = children(nb, moov.off + 8, moov.off + moov.size - udta.size);
      for (const c of nmc) {
        if (c.type !== 'trak') continue;
        const subs = children(nb, c.off + 8, c.off + c.size);
        const mdia = subs.find(x => x.type === 'mdia');
        if (!mdia) continue;
        const minf = children(nb, mdia.off + 8, mdia.off + mdia.size).find(x => x.type === 'minf');
        if (!minf) continue;
        const stbl = children(nb, minf.off + 8, minf.off + minf.size).find(x => x.type === 'stbl');
        if (!stbl) continue;
        for (const s of children(nb, stbl.off + 8, stbl.off + stbl.size)) {
          if (s.type !== 'stco' && s.type !== 'co64') continue;
          const cnt = ndv.getUint32(s.off + 12);
          const fs = s.type === 'stco' ? 4 : 8;
          let p = s.off + 16;
          for (let i = 0; i < cnt && p + fs <= s.off + s.size; i++) {
            if (s.type === 'stco') {
              const v = ndv.getUint32(p);
              if (v >= udta.off + udta.size) { ndv.setUint32(p, v - udta.size); }
            } else {
              const v = ndv.getBigUint64(p);
              if (v >= BigInt(udta.off + udta.size)) { ndv.setBigUint64(p, v - BigInt(udta.size)); }
            }
            p += fs;
          }
        }
      }
      b = nb;
      warnings.push(`已删除 udta box（${udta.size} 字节），避免与 moov/meta 重复`);
      top = children(b, 0, b.length);
      moov = top.find(c => c.type === 'moov');
    }
  }

  // 3) 写入 mvhd CreateDate/ModifyDate（REF 有实际时间，不能是 0）
  //    mvhd v0: size(4) + 'mvhd'(4) + version(1) + flags(3) + ctime(4) + mtime(4) + timescale(4) + duration(4)...
  //    ctime/mtime 在 box 头后 +8 处（version+flags 后）
  //    时间优先用拍摄时间 photoDate（保留原拍摄时间），缺省回退当前时间。
  {
    const mc = children(b, moov.off + 8, moov.off + moov.size);
    const mvhd = mc.find(c => c.type === 'mvhd');
    if (mvhd) {
      const dv = new DataView(b.buffer);
      const appleSec = Math.floor(((photoDate instanceof Date ? photoDate.getTime() : Date.now()) / 1000)) + 2082844800; // Apple epoch (1904-01-01) 秒数
      dv.setUint32(mvhd.off + 8 + 4, appleSec); // ctime（version+flags 后 +4）
      dv.setUint32(mvhd.off + 8 + 8, appleSec); // mtime
      warnings.push('已写入 mvhd CreateDate/ModifyDate（拍摄时间）');
    }
  }

  // 4) 构建 meta box（仅 ContentIdentifier，可选追加 location.ISO6709），追加到 moov 末尾（moov 内合法子 box）
  const metaBytes = buildMetaBoxMinimal(uuid, { iso6709: exifMeta.iso6709 || null });
  const delta = metaBytes.length;
  const insertPos = moov.off + moov.size;
  const out = new Uint8Array(b.length + delta);
  out.set(b.subarray(0, insertPos), 0);
  out.set(metaBytes, insertPos);
  out.set(b.subarray(insertPos), insertPos + delta);

  const dv = new DataView(out.buffer);
  // 5) moov size 增长
  dv.setUint32(moov.off, moov.size + delta);

  // 6) 修正所有 stco/co64：插入点之后的样本偏移需 +delta（mdat 在 moov 之后整体后移）
  const newMoovChildren = children(out, moov.off + 8, moov.off + moov.size + delta);
  let fixed = 0;
  for (const c of newMoovChildren) {
    if (c.type !== 'trak') continue;
    const subs = children(out, c.off + 8, c.off + c.size);
    const mdia = subs.find(x => x.type === 'mdia');
    if (!mdia) continue;
    const minf = children(out, mdia.off + 8, mdia.off + mdia.size).find(x => x.type === 'minf');
    if (!minf) continue;
    const stbl = children(out, minf.off + 8, minf.off + minf.size).find(x => x.type === 'stbl');
    if (!stbl) continue;
    for (const s of children(out, stbl.off + 8, stbl.off + stbl.size)) {
      if (s.type !== 'stco' && s.type !== 'co64') continue;
      const cnt = dv.getUint32(s.off + 12);
      const fs = s.type === 'stco' ? 4 : 8;
      let p = s.off + 16;
      for (let i = 0; i < cnt && p + fs <= s.off + s.size; i++) {
        if (s.type === 'stco') {
          const v = dv.getUint32(p);
          if (v >= insertPos) { dv.setUint32(p, v + delta); fixed++; }
        } else {
          const v = dv.getBigUint64(p);
          if (v >= BigInt(insertPos)) { dv.setBigUint64(p, v + BigInt(delta)); fixed++; }
        }
        p += fs;
      }
    }
  }
  if (fixed === 0) warnings.push('未修正任何 chunk 偏移（moov 位于 mdat 之后属正常）');

  return { bytes: out, warnings, stillTimeSeconds: 0 };
}

/**
 * 验证产物结构（测试/调试用）：
 * - 解析 moov/meta → keys/ilst 并返回 ContentIdentifier UUID；
 * - 探测 still-image-time 时间元数据轨道及其静态帧时刻。
 */
export function verifyLivePhotoMov(bytes) {
  const top = children(bytes, 0, bytes.length);
  const moov = top.find(c => c.type === 'moov');
  if (!moov) return { ok: false, error: 'no moov' };
  const subs = children(bytes, moov.off + 8, moov.off + moov.size);
  const meta = subs.find(c => c.type === 'meta');
  if (!meta) return { ok: false, error: 'no meta' };
  // meta 非 full box：payload 直接以 hdlr 开始（与 exiftool / iPhone 原生一致）
  const msubs = children(bytes, meta.off + 8, meta.off + meta.size);
  const hdlr = msubs.find(c => c.type === 'hdlr');
  const keys = msubs.find(c => c.type === 'keys');
  const ilst = msubs.find(c => c.type === 'ilst');
  if (!hdlr || !keys || !ilst) return { ok: false, error: 'meta missing parts' };
  const handler = String.fromCharCode(bytes[hdlr.off + 16], bytes[hdlr.off + 17], bytes[hdlr.off + 18], bytes[hdlr.off + 19]);

  // 解析所有 keys：每个 key = [size][namespace 4 'mdta'][key string]
  const keysList = [];
  const keysCount = readU32(bytes, keys.off + 12); // full box hdr(4+4) + count
  {
    let p = keys.off + 16;
    for (let i = 0; i < keysCount; i++) {
      const ksz = readU32(bytes, p);
      const ns = String.fromCharCode(bytes[p + 4], bytes[p + 5], bytes[p + 6], bytes[p + 7]);
      const keyStr = new TextDecoder().decode(bytes.slice(p + 8, p + ksz)).replace(/\0+$/, '');
      keysList.push({ index: i + 1, key: keyStr, ns });
      p += ksz;
    }
  }

  // 解析 ilst items：每个 item = [size][key_index_1based][data box [size]['data'][type_code][locale=0][value]]
  const ilstItems = children(bytes, ilst.off + 8, ilst.off + ilst.size);
  const entries = keysList.map(k => {
    const item = ilstItems.find(it => readU32(bytes, it.off + 4) === k.index);
    if (!item) return { ...k, typeCode: null, value: null };
    const data = children(bytes, item.off + 8, item.off + item.size).find(c => c.type === 'data');
    if (!data) return { ...k, typeCode: null, value: null };
    const typeCode = readU32(bytes, data.off + 8);
    const locale = readU32(bytes, data.off + 12);
    const valueBytes = bytes.slice(data.off + 16, data.off + data.size);
    let value;
    if (typeCode === 0x01) {
      value = new TextDecoder().decode(valueBytes);
    } else if (typeCode === 0x15 && valueBytes.length >= 8) {
      // int64 BE：前 4 字节高位 = 0，低 4 字节 = int32 BE 值
      const lo = readU32(bytes, data.off + 20);
      value = lo;
    } else if (typeCode === 0x17 && valueBytes.length >= 4) {
      value = new DataView(bytes.buffer, bytes.byteOffset).getFloat32(data.off + 16);
    } else {
      value = valueBytes;
    }
    return { ...k, typeCode, locale, value };
  });

  const ci = entries.find(e => e.key === 'com.apple.quicktime.content.identifier');

  // still-image-time 轨道探测
  const stillImageTime = { present: false, seconds: null, trackId: null, sampleValue: null };
  const mvhd = subs.find(c => c.type === 'mvhd');
  const movieTimescale = mvhd ? readMvhd(bytes, mvhd).timescale : 600;
  for (const c of subs) {
    if (c.type !== 'trak') continue;
    const tsubs = children(bytes, c.off + 8, c.off + c.size);
    const mdia = tsubs.find(x => x.type === 'mdia');
    if (!mdia) continue;
    const m2 = children(bytes, mdia.off + 8, mdia.off + mdia.size);
    const mh = m2.find(x => x.type === 'hdlr');
    if (!mh) continue;
    const mhType = String.fromCharCode(bytes[mh.off + 16], bytes[mh.off + 17], bytes[mh.off + 18], bytes[mh.off + 19]);
    if (mhType !== 'meta') continue; // 时间元数据轨道的媒体 handler 为 'meta'（与 iPhone 原生一致）
    // 在 stsd/mebx/keys/keyd 中查找 still-image-time 键
    const minf = m2.find(x => x.type === 'minf');
    if (!minf) continue;
    const minfSubs = children(bytes, minf.off + 8, minf.off + minf.size);
    const stbl = minfSubs.find(x => x.type === 'stbl');
    if (!stbl) continue;
    const stblSubs = children(bytes, stbl.off + 8, stbl.off + stbl.size);
    const stsd = stblSubs.find(x => x.type === 'stsd');
    if (!stsd) continue;
    const entries = children(bytes, stsd.off + 16, stsd.off + stsd.size);
    const mebx = entries.find(x => x.type === 'mebx');
    if (!mebx) continue;
    const mebxSubs = children(bytes, mebx.off + 16, mebx.off + mebx.size);
    const keysBox = mebxSubs.find(x => x.type === 'keys');
    if (!keysBox) continue;
    let found = false, localKeyId = 0, keyCount = 0;
    for (const ka of children(bytes, keysBox.off + 8, keysBox.off + keysBox.size)) {
      keyCount++;
      const kid = readU32(bytes, ka.off + 4);
      for (const kd of children(bytes, ka.off + 8, ka.off + ka.size)) {
        if (kd.type !== 'keyd') continue;
        const ns = String.fromCharCode(bytes[kd.off + 8], bytes[kd.off + 9], bytes[kd.off + 10], bytes[kd.off + 11]);
        const keyStr = new TextDecoder().decode(bytes.slice(kd.off + 12, kd.off + kd.size)).replace(/\0+$/, '');
        if (ns === 'mdta' && keyStr === 'com.apple.quicktime.still-image-time') { found = true; localKeyId = kid; }
      }
    }
    if (!found) continue;
    stillImageTime.keyCount = keyCount;
    // 静态帧时刻 = elst 前导空编辑时长 / 影片 timescale
    let emptyUnits = 0;
    const edts = tsubs.find(x => x.type === 'edts');
    if (edts) {
      const es = children(bytes, edts.off + 8, edts.off + edts.size);
      const elst = es.find(x => x.type === 'elst');
      if (elst) {
        const cnt = readU32(bytes, elst.off + 12);
        let p = elst.off + 16;
        for (let i = 0; i < cnt; i++) {
          const segDur = readU32(bytes, p);
          const mediaTime = readU32(bytes, p + 4) | 0;
          if (mediaTime !== -1) break;
          emptyUnits += segDur;
          p += 12;
        }
      }
    }
    const tkhd = tsubs.find(x => x.type === 'tkhd');
    stillImageTime.present = true;
    stillImageTime.trackId = tkhd ? readU32(bytes, tkhd.off + 8 + 12) : null;
    // tref/cdsc：元数据轨道应描述视频轨道
    const tref = tsubs.find(x => x.type === 'tref');
    if (tref) {
      for (const tr of children(bytes, tref.off + 8, tref.off + tref.size)) {
        if (tr.type === 'cdsc' && tr.size >= 12) stillImageTime.cdscTarget = readU32(bytes, tr.off + 8);
      }
    }
    stillImageTime.seconds = emptyUnits / (movieTimescale || 600);
    // 样本（105 字节）：[9B key1=-1][80B key2=变换矩阵][16B key3=参考尺寸]
    const stco = stblSubs.find(x => x.type === 'stco');
    if (stco) {
      const off = readU32(bytes, stco.off + 16);
      const sub1Size = readU32(bytes, off);
      stillImageTime.sampleValue = sub1Size === 9 && readU32(bytes, off + 4) === localKeyId ? ((bytes[off + 8] << 24) >> 24) : null;
      const sub2Size = readU32(bytes, off + 9);
      const sub3Size = readU32(bytes, off + 9 + sub2Size);
      stillImageTime.sampleSize = 9 + sub2Size + sub3Size;
      stillImageTime.transformPresent = sub2Size === 80; // 8 头 + 9 × float64
      if (sub3Size === 16) {
        stillImageTime.referenceDimensions = [
          new DataView(bytes.buffer, bytes.byteOffset).getFloat32(off + 9 + sub2Size + 8),
          new DataView(bytes.buffer, bytes.byteOffset).getFloat32(off + 9 + sub2Size + 12)
        ];
      }
    }
  }

  return {
    ok: true, handler,
    key: ci ? ci.key : null,
    value: ci ? ci.value : null,
    keys: entries,                // 全部 QuickTime Keys（含 typeCode/value）
    keyCount: entries.length,
    stillImageTime,
  };
}

/**
 * 验证视频轨道内 meta box 的 5 个 Apple 相机 QuickTime Keys。
 * 依据仍是与 verifyLivePhotoMov 相同，但定位路径是 moov/trak(vide)/mdia 后的 meta box。
 */
export function verifyVideoTrakMeta(bytes) {
  const top = children(bytes, 0, bytes.length);
  const moov = top.find(c => c.type === 'moov');
  if (!moov) return { ok: false, keyCount: 0, keys: [] };
  const subs = children(bytes, moov.off + 8, moov.off + moov.size);

  // 找到视频轨：mdia/hdlr handler='vide'
  let videoTrak = null;
  for (const c of subs) {
    if (c.type !== 'trak') continue;
    const tsubs = children(bytes, c.off + 8, c.off + c.size);
    const mdia = tsubs.find(x => x.type === 'mdia');
    if (!mdia) continue;
    const m2 = children(bytes, mdia.off + 8, mdia.off + mdia.size);
    const mh = m2.find(x => x.type === 'hdlr');
    if (!mh) continue;
    const mhType = String.fromCharCode(bytes[mh.off + 16], bytes[mh.off + 17], bytes[mh.off + 18], bytes[mh.off + 19]);
    if (mhType === 'vide') { videoTrak = c; break; }
  }
  if (!videoTrak) return { ok: false, keyCount: 0, keys: [] };

  // 在 trak 子 box 中找 meta box（不在 mdia 内，与参照 apple.MOV 一致）
  const trakSubs = children(bytes, videoTrak.off + 8, videoTrak.off + videoTrak.size);
  const meta = trakSubs.find(x => x.type === 'meta');
  if (!meta) return { ok: false, keyCount: 0, keys: [] };

  const msubs = children(bytes, meta.off + 8, meta.off + meta.size);
  const hdlr = msubs.find(c => c.type === 'hdlr');
  const keys = msubs.find(c => c.type === 'keys');
  const ilst = msubs.find(c => c.type === 'ilst');
  if (!hdlr || !keys || !ilst) return { ok: false, keyCount: 0, keys: [] };
  const handler = String.fromCharCode(bytes[hdlr.off + 16], bytes[hdlr.off + 17], bytes[hdlr.off + 18], bytes[hdlr.off + 19]);

  const keysCount = readU32(bytes, keys.off + 12); // full box hdr(4+4) + count
  const keysList = [];
  {
    let p = keys.off + 16;
    for (let i = 0; i < keysCount; i++) {
      const ksz = readU32(bytes, p);
      const ns = String.fromCharCode(bytes[p + 4], bytes[p + 5], bytes[p + 6], bytes[p + 7]);
      const keyStr = new TextDecoder().decode(bytes.slice(p + 8, p + ksz)).replace(/\0+$/, '');
      keysList.push({ index: i + 1, key: keyStr, ns });
      p += ksz;
    }
  }

  const ilstItems = children(bytes, ilst.off + 8, ilst.off + ilst.size);
  const entries = keysList.map(k => {
    const item = ilstItems.find(it => readU32(bytes, it.off + 4) === k.index);
    if (!item) return { ...k, typeCode: null, value: null };
    const data = children(bytes, item.off + 8, item.off + item.size).find(c => c.type === 'data');
    if (!data) return { ...k, typeCode: null, value: null };
    const typeCode = readU32(bytes, data.off + 8);
    let valueBytes = bytes.slice(data.off + 16, data.off + data.size);
    let value;
    if (typeCode === 0x01) {
      value = new TextDecoder().decode(valueBytes);
    } else if (typeCode === 0x15 && valueBytes.length >= 8) {
      value = readU32(bytes, data.off + 20);  // int64 BE 高 4 = 0, 低 4 = int32
    } else if (typeCode === 0x15 && valueBytes.length >= 4) {
      value = readU32(bytes, data.off + 16);  // int32 BE（4 字节，参照 apple-maker-note）
    } else if (typeCode === 0x17 && valueBytes.length >= 4) {
      value = new DataView(bytes.buffer, bytes.byteOffset).getFloat32(data.off + 16);
    } else {
      value = valueBytes;
    }
    return { ...k, typeCode, value };
  });

  return { ok: true, handler, keyCount: entries.length, keys: entries };
}
