diff --git a/packages/engine/src/services/videoFrameExtractor.ts b/packages/engine/src/services/videoFrameExtractor.ts index 989705f8d1..b0015b3ae3 100644 --- a/packages/engine/src/services/videoFrameExtractor.ts +++ b/packages/engine/src/services/videoFrameExtractor.ts @@ -14,6 +14,7 @@ import { createConcurrencyLimit } from "../utils/concurrencyLimit.js"; import { parseHTML } from "linkedom"; import { isPathInside } from "@hyperframes/parsers/asset-paths"; import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution"; +import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha"; import { MEDIA_RENDER_ID_ATTR, isAudibleVideoElement, @@ -1580,10 +1581,7 @@ export function codecMayHaveAlpha(codec: string | undefined): boolean { } export function decoderForCodec(codec: string | undefined): string { - const c = (codec ?? "").toLowerCase(); - if (c === "vp9") return "libvpx-vp9"; - if (c === "vp8") return "libvpx"; - return c; + return alphaDecoderFor(codec) ?? (codec ?? "").toLowerCase(); } export function resolveFrameFormat( diff --git a/packages/engine/src/utils/ffprobe.test.ts b/packages/engine/src/utils/ffprobe.test.ts index 969028ad2f..b1cdae9d6d 100644 --- a/packages/engine/src/utils/ffprobe.test.ts +++ b/packages/engine/src/utils/ffprobe.test.ts @@ -5,12 +5,7 @@ import { mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs import { tmpdir } from "os"; import { basename, resolve } from "path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - extractMediaMetadata, - extractPngMetadataFromBuffer, - parseFrameRate, - pixelFormatHasAlpha, -} from "./ffprobe.js"; +import { extractMediaMetadata, extractPngMetadataFromBuffer, parseFrameRate } from "./ffprobe.js"; function crc32(buf: Buffer): number { let crc = 0xffffffff; @@ -1737,27 +1732,6 @@ describe("crc32 works on every runtime the package declares", () => { }); }); -describe("pix_fmt alpha detection", () => { - // The old pattern's (^|[^a-z]) anchor bound to `yuva` alone, and the list - // omitted formats real files actually use. - const ALPHA = [ - "yuva420p", - "rgba", - "argb", - "bgra", - "abgr", - "gbrap", - "ya8", - "ya16le", - "ayuv64le", - "yuva444p12le", - ]; - const OPAQUE = ["yuv420p", "rgb24", "gray", "gbrp", "nv12", "yuv444p10le", "bgr0", "rgb0"]; - - it.each(ALPHA)("detects alpha in %s", (fmt) => expect(pixelFormatHasAlpha(fmt)).toBe(true)); - it.each(OPAQUE)("reports %s as opaque", (fmt) => expect(pixelFormatHasAlpha(fmt)).toBe(false)); -}); - describe("audio duration decode probe never fails the call and needs no profile allowlist", () => { afterEach(() => { vi.resetModules(); diff --git a/packages/engine/src/utils/ffprobe.ts b/packages/engine/src/utils/ffprobe.ts index d45f64c15a..655aca9914 100644 --- a/packages/engine/src/utils/ffprobe.ts +++ b/packages/engine/src/utils/ffprobe.ts @@ -5,6 +5,7 @@ import { chunkCrc32 } from "./crc32.js"; import { StringDecoder } from "node:string_decoder"; import { basename } from "path"; import { redactTelemetryString } from "@hyperframes/core"; +import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha"; import { urlInputArgs } from "./urlInput.js"; import { FFPROBE_PATH_ENV, getFfprobeBinary } from "./ffmpegBinaries.js"; import { ManagedChildProcess } from "./managedChildProcess.js"; @@ -604,23 +605,6 @@ export function extractPngMetadataFromBuffer(buf: Buffer): StillImageMetadata | return width > 0 && height > 0 ? { width, height, colorSpace: colorSpaceFromCicp } : null; } -/** - * Does this pix_fmt carry an alpha channel? - * - * Exported so the test asserts the shipped predicate rather than a copy of - * the pattern. Anchored at the start, matching studio-server's - * mediaMetadata.ts: the previous inline pattern bound its `(^|[^a-z])` anchor - * to the first alternative only — `|` is looser than concatenation — so the - * guard was decorative for every other name. It also missed abgr, ya8/ya16 - * and ayuv64, and its `gray[a-z0-9]*a` branch matched only gray8a/gray16a, - * names FFmpeg renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG - * reported hasAlpha:false, resolveFrameFormat picked jpg, and the overlay - * flattened to an opaque rectangle. - */ -export function pixelFormatHasAlpha(pixelFormat: string): boolean { - return /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya|ayuv)/i.test(pixelFormat); -} - function extractStillImageMetadata(filePath: string): StillImageMetadata | null { try { return extractPngMetadataFromBuffer(readFileSync(filePath)); @@ -791,16 +775,7 @@ export async function extractMediaMetadata(filePath: string): Promise { + const ALPHA = [ + "yuva420p", + "yuva444p10le", + "yuva444p12le", + "rgba", + "argb", + "bgra", + "abgr", + "gbrap", + "gbrap12le", + "ya8", + "ya16le", + "ayuv64le", + "YUVA420P", + ]; + const OPAQUE = [ + "yuv420p", + "yuv422p10le", + "yuv444p10le", + "rgb24", + "gray", + "gbrp", + "nv12", + "bgr0", + "rgb0", + ]; + + it.each(ALPHA)("detects alpha in %s", (fmt) => expect(pixelFormatHasAlpha(fmt)).toBe(true)); + it.each(OPAQUE)("reports %s as opaque", (fmt) => expect(pixelFormatHasAlpha(fmt)).toBe(false)); + it("reports a missing pix_fmt as opaque", () => + expect(pixelFormatHasAlpha(undefined)).toBe(false)); +}); + +describe("videoStreamHasAlpha", () => { + // ffprobe 8.1 on a libvpx-vp9 yuva420p encode: the alpha lives only in the tag. + const vp9Alpha = { codec_name: "vp9", pix_fmt: "yuv420p", tags: { ALPHA_MODE: "1" } }; + + it("reads the VP8/VP9 alpha_mode tag in any casing", () => { + expect(videoStreamHasAlpha(vp9Alpha)).toBe(true); + expect(videoStreamHasAlpha({ pix_fmt: "yuv420p", tags: { alpha_mode: "1" } })).toBe(true); + }); + + it("reads an alpha pix_fmt with no tag", () => { + expect(videoStreamHasAlpha({ pix_fmt: "yuva444p10le" })).toBe(true); + }); + + it("reports opaque streams", () => { + expect(videoStreamHasAlpha({ pix_fmt: "yuv420p" })).toBe(false); + expect(videoStreamHasAlpha({ pix_fmt: "yuv420p", tags: { ALPHA_MODE: "0" } })).toBe(false); + expect(videoStreamHasAlpha({ pix_fmt: "yuv420p", tags: { ENCODER: "Lavc" } })).toBe(false); + }); +}); + +describe("alphaDecoderFor", () => { + it("picks the libvpx decoders that read the alpha side channel", () => { + expect(alphaDecoderFor("vp9")).toBe("libvpx-vp9"); + expect(alphaDecoderFor("VP9")).toBe("libvpx-vp9"); + expect(alphaDecoderFor("vp8")).toBe("libvpx"); + }); + + it("leaves other codecs on ffmpeg's default decoder", () => { + expect(alphaDecoderFor("prores")).toBeNull(); + expect(alphaDecoderFor("h264")).toBeNull(); + expect(alphaDecoderFor(undefined)).toBeNull(); + }); +}); diff --git a/packages/parsers/src/mediaAlpha.ts b/packages/parsers/src/mediaAlpha.ts new file mode 100644 index 0000000000..4f208fc2c8 --- /dev/null +++ b/packages/parsers/src/mediaAlpha.ts @@ -0,0 +1,27 @@ +const ALPHA_PIX_FMT_RE = /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya|ayuv)/i; + +export function pixelFormatHasAlpha(pixFmt: string | undefined): boolean { + return pixFmt !== undefined && ALPHA_PIX_FMT_RE.test(pixFmt); +} + +export interface FfprobeAlphaFields { + pix_fmt?: string; + tags?: Record; +} + +/** True when an ffprobe video stream carries alpha. VP8/VP9 alpha rides in a side channel: + * ffprobe reports `yuv420p` plus an `alpha_mode=1` tag, whose casing differs across builds. */ +export function videoStreamHasAlpha(stream: FfprobeAlphaFields): boolean { + if (pixelFormatHasAlpha(stream.pix_fmt)) return true; + return Object.entries(stream.tags ?? {}).some( + ([key, value]) => key.toLowerCase() === "alpha_mode" && value === "1", + ); +} + +/** The decoder that reads a VP8/VP9 alpha side channel; ffmpeg's native ones drop it. */ +export function alphaDecoderFor(codecName: string | undefined): "libvpx-vp9" | "libvpx" | null { + const codec = codecName?.toLowerCase(); + if (codec === "vp9") return "libvpx-vp9"; + if (codec === "vp8") return "libvpx"; + return null; +} diff --git a/packages/parsers/tsup.config.ts b/packages/parsers/tsup.config.ts index 75bd2cf17f..c07f14c8f9 100644 --- a/packages/parsers/tsup.config.ts +++ b/packages/parsers/tsup.config.ts @@ -21,6 +21,7 @@ export default defineConfig({ colorGradingContract: "src/colorGradingContract.ts", subCompositionValidity: "src/subCompositionValidity.ts", ffBinaries: "src/ffBinaries.ts", + mediaAlpha: "src/mediaAlpha.ts", assetResolution: "src/assetResolution.ts", htmlAttributeSpans: "src/htmlAttributeSpans.ts", timeSpans: "src/timeSpans.ts", diff --git a/packages/studio-server/src/helpers/mediaCodecMap.test.ts b/packages/studio-server/src/helpers/mediaCodecMap.test.ts index cad1265f3b..3ab8ab549e 100644 --- a/packages/studio-server/src/helpers/mediaCodecMap.test.ts +++ b/packages/studio-server/src/helpers/mediaCodecMap.test.ts @@ -22,7 +22,10 @@ import { const FAKE_FFPROBE_PATH = process.execPath; function makeRunner( - codecByPath: Record, + codecByPath: Record< + string, + string | { codecName: string; pixFmt?: string; tags?: Record } + >, ): FfprobeRunner { return (_command, args) => { const filePath = args[args.length - 1] ?? ""; @@ -34,6 +37,7 @@ function makeRunner( codec_type: "video", codec_name: normalized.codecName, pix_fmt: normalized.pixFmt, + tags: normalized.tags, }, ] : []; @@ -164,6 +168,23 @@ describe("probeAssetCodec", () => { }); }); + it("flags a VP9 whose alpha is only in the ALPHA_MODE tag, so it gets the VP8 proxy", async () => { + const project = tmpProject(); + const videoPath = join(project, "cutout.webm"); + writeFileSync(videoPath, "fake video bytes"); + + // What ffprobe prints for a libvpx-vp9 yuva420p encode. + const facts = await probeAssetCodec( + videoPath, + makeRunner({ + [videoPath]: { codecName: "vp9", pixFmt: "yuv420p", tags: { ALPHA_MODE: "1" } }, + }), + ); + + expect(facts?.hasAlpha).toBe(true); + expect(proxyVariantFor(facts!)).toBe("vp8"); + }); + it("returns null (never throws) when ffprobe is unresolvable", async () => { const project = tmpProject(); const videoPath = join(project, "clip.mp4"); diff --git a/packages/studio-server/src/helpers/mediaCodecMap.ts b/packages/studio-server/src/helpers/mediaCodecMap.ts index 7650661abe..8966e22422 100644 --- a/packages/studio-server/src/helpers/mediaCodecMap.ts +++ b/packages/studio-server/src/helpers/mediaCodecMap.ts @@ -9,7 +9,7 @@ import { isUnresolvedAssetPlaceholder, resolveExistingLocalAsset as resolveAsset, } from "@hyperframes/parsers/asset-resolution"; -import { pixelFormatHasAlpha, probeMediaMetadata, type FfprobeRunner } from "./mediaMetadata.js"; +import { probeMediaMetadata, type FfprobeRunner } from "./mediaMetadata.js"; /** * One reusable answer to "what codec is this asset, and is it browser-hostile?", @@ -24,7 +24,7 @@ export interface AssetCodecFacts { * when no representative mime exists (ProRes: browsers never decode it, so * the runtime always proxies rather than probing `canPlayType`). */ representativeMime: string | null; - /** Source carries an alpha channel (ffprobe pix_fmt). Alpha sources use a + /** Source carries an alpha channel (`MediaMetadata.hasAlpha`). Alpha sources use a * VP8/WebM proxy so their transparency is preserved across Chromium builds. */ hasAlpha: boolean; } @@ -205,7 +205,7 @@ async function probeCodecFacts( if (metadata.kind !== "video") return null; const codecName = metadata.color.codecName; if (!codecName) return null; - return codecFactsFor(codecName, pixelFormatHasAlpha(metadata.color.pixelFormat)); + return codecFactsFor(codecName, metadata.hasAlpha === true); } interface CachedAssetProbe { diff --git a/packages/studio-server/src/helpers/mediaMetadata.test.ts b/packages/studio-server/src/helpers/mediaMetadata.test.ts index 414368c595..73006c0e4c 100644 --- a/packages/studio-server/src/helpers/mediaMetadata.test.ts +++ b/packages/studio-server/src/helpers/mediaMetadata.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { classifyMediaColor, pixelFormatHasAlpha, probeMediaMetadata } from "./mediaMetadata.js"; +import { classifyMediaColor, probeMediaMetadata } from "./mediaMetadata.js"; afterEach(() => vi.unstubAllEnvs()); @@ -175,23 +175,3 @@ describe("probeMediaMetadata", () => { expect(metadata).toMatchObject({ kind: "video", color: { codecName: "h264" } }); }); }); - -describe("pixelFormatHasAlpha", () => { - it("detects alpha-bearing pixel formats", () => { - for (const pixFmt of [ - "yuva420p", - "yuva444p10le", - "rgba", - "argb", - "bgra", - "abgr", - "gbrap12le", - "ya8", - ]) { - expect(pixelFormatHasAlpha(pixFmt)).toBe(true); - } - for (const pixFmt of ["yuv420p", "yuv422p10le", "rgb24", "gbrp", "gray", undefined]) { - expect(pixelFormatHasAlpha(pixFmt)).toBe(false); - } - }); -}); diff --git a/packages/studio-server/src/helpers/mediaMetadata.ts b/packages/studio-server/src/helpers/mediaMetadata.ts index d99b0f53aa..ccfb415fa0 100644 --- a/packages/studio-server/src/helpers/mediaMetadata.ts +++ b/packages/studio-server/src/helpers/mediaMetadata.ts @@ -6,6 +6,7 @@ import { type ToneMapSourceColour, } from "@hyperframes/core"; import { findFfBinary } from "@hyperframes/parsers/ff-binaries"; +import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha"; export interface FfprobeRunResult { status: number | null; @@ -73,6 +74,8 @@ export interface MediaMetadata { color: MediaColorMetadata; /** Video audio stream, when probed. Absent means the drop path stays muted. */ hasAudio?: boolean; + /** Video stream carries alpha (pix_fmt or the VP8/VP9 alpha_mode tag). */ + hasAlpha?: boolean; probeError?: string; } @@ -86,6 +89,7 @@ interface FfprobeStream { color_primaries?: string; bits_per_raw_sample?: string; disposition?: { attached_pic?: number }; + tags?: Record; } const VIDEO_EXT = new Set([ @@ -137,17 +141,6 @@ function colorLabel(input: { return "SDR/unknown"; } -// Conservative alpha-bearing pix_fmt list: yuva* (yuva420p, yuva444p10le...), -// rgba/argb/bgra/abgr (packed RGB+alpha), gbrap* (planar GBR+alpha, ProRes -// 4444 decodes to these), ya* (gray+alpha). Prefix match keeps bit-depth / -// endianness suffixes covered. -const ALPHA_PIX_FMT_RE = /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya)/; - -/** True when an ffprobe `pix_fmt` carries an alpha component. */ -export function pixelFormatHasAlpha(pixFmt: string | undefined): boolean { - return pixFmt !== undefined && ALPHA_PIX_FMT_RE.test(pixFmt.toLowerCase()); -} - export function classifyMediaColor(stream: FfprobeStream | null | undefined): MediaColorMetadata { const colorPrimaries = lower(stream?.color_primaries); const colorSpace = lower(stream?.color_space); @@ -222,7 +215,7 @@ export async function probeMediaMetadata( "-v", "error", "-show_entries", - "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic", + "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic:stream_tags=alpha_mode", "-of", "json", "--", @@ -247,6 +240,7 @@ export async function probeMediaMetadata( const metadata: MediaMetadata = { kind, color: classifyMediaColor(stream) }; if (kind === "video") { metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === "audio"); + metadata.hasAlpha = stream !== undefined && videoStreamHasAlpha(stream); } return metadata; } catch { diff --git a/packages/studio-server/src/helpers/proxyTranscoder.test.ts b/packages/studio-server/src/helpers/proxyTranscoder.test.ts index 74e7785354..f4ba57d4af 100644 --- a/packages/studio-server/src/helpers/proxyTranscoder.test.ts +++ b/packages/studio-server/src/helpers/proxyTranscoder.test.ts @@ -23,13 +23,21 @@ const realFfmpegFilters = realFfmpeg : ""; const canToneMapForReal = Boolean(findFfBinary("ffprobe")) && /\szscale\s/.test(realFfmpegFilters ?? ""); +const realFfmpegEncoders = realFfmpeg + ? spawnSync(realFfmpeg, ["-hide_banner", "-encoders"], { encoding: "utf8" }).stdout + : ""; +const canEncodeVpxAlphaForReal = + Boolean(findFfBinary("ffprobe")) && + /\slibvpx\s/.test(realFfmpegEncoders ?? "") && + /\slibvpx-vp9\s/.test(realFfmpegEncoders ?? ""); -function meanLuma(path: string, filters = ""): number { +function meanLuma(path: string, filters = "", inputArgs: string[] = []): number { const stats = execFileSync( realFfmpeg!, [ "-v", "error", + ...inputArgs, "-i", path, "-frames:v", @@ -134,6 +142,7 @@ async function loadModule( ffmpegPath: string | undefined, isHdr = false, hdrTransfer: string | null = isHdr ? "pq" : null, + codecName?: string, ): Promise { vi.resetModules(); vi.doMock("node:child_process", () => { @@ -146,7 +155,7 @@ async function loadModule( vi.doMock("./mediaMetadata.js", () => ({ probeMediaMetadata: async () => ({ kind: "video", - color: { isHdr, hdrTransfer }, + color: { isHdr, hdrTransfer, codecName }, }), probeFirstFrameColour: async () => ({}), })); @@ -267,6 +276,57 @@ describe("resolveProxy", () => { await expect(resultPromise).resolves.toBe(vp8Path); }); + it("decodes a VP9 alpha source with libvpx-vp9 so the VP8 proxy keeps its alpha", async () => { + const { spawn, calls } = createSpawnSpy(); + const { resolveProxy } = await loadModule(spawn, FFMPEG_PATH, false, null, "vp9"); + const projectDir = tmpProject(); + const sourcePath = join(projectDir, "cutout.webm"); + writeFileSync(sourcePath, "source-bytes"); + + const vp8 = resolveProxy(projectDir, sourcePath, "vp8"); + await flush(); + const vp8Args = calls[0]!.args; + expect(vp8Args.slice(vp8Args.indexOf("-i") - 2, vp8Args.indexOf("-i"))).toEqual([ + "-c:v", + "libvpx-vp9", + ]); + succeed(calls[0]!, "fake-vp8-bytes"); + await vp8; + + const h264 = resolveProxy(projectDir, sourcePath, "h264"); + await flush(); + expect(calls[1]!.args.slice(0, calls[1]!.args.indexOf("-i"))).not.toContain("-c:v"); + succeed(calls[1]!); + await h264; + }); + + it.skipIf(!canEncodeVpxAlphaForReal)( + "keeps a real VP9 alpha webm transparent through probe, variant and transcode", + async () => { + vi.resetModules(); + vi.doUnmock("./mediaMetadata.js"); + const { resolveProxy } = await import("./proxyTranscoder.js"); + const { probeAssetCodec, proxyVariantFor } = await import("./mediaCodecMap.js"); + const projectDir = tmpProject(); + const sourcePath = join(projectDir, "cutout.webm"); + // Left half fully transparent, right half opaque: mean alpha 127.5. + execFileSync(realFfmpeg!, [ + ...["-v", "error", "-f", "lavfi", "-i"], + "testsrc2=s=64x64:d=0.2,format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='if(lt(X,32),0,255)'", + ...["-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-auto-alt-ref", "0"], + ...["-metadata:s:v:0", "alpha_mode=1", sourcePath], + ]); + + const facts = await probeAssetCodec(sourcePath); + expect(facts).toMatchObject({ codecName: "vp9", hasAlpha: true }); + const proxyPath = await resolveProxy(projectDir, sourcePath, proxyVariantFor(facts!)); + expect(proxyPath).toMatch(/\.webm$/); + const meanAlpha = meanLuma(proxyPath, "alphaextract,", ["-c:v", "libvpx"]); + expect(Math.abs(meanAlpha - 127.5)).toBeLessThan(8); + }, + 60_000, + ); + it("returns without spawning on a cache hit", async () => { const { spawn, calls } = createSpawnSpy(); const { resolveProxy, getProxyCachePath } = await loadModule(spawn, FFMPEG_PATH); diff --git a/packages/studio-server/src/helpers/proxyTranscoder.ts b/packages/studio-server/src/helpers/proxyTranscoder.ts index 79c2a94235..70855fd1fe 100644 --- a/packages/studio-server/src/helpers/proxyTranscoder.ts +++ b/packages/studio-server/src/helpers/proxyTranscoder.ts @@ -8,6 +8,7 @@ import { type PreviewProxyBox, } from "@hyperframes/core"; import { findFfBinary } from "@hyperframes/parsers/ff-binaries"; +import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha"; import { probeFirstFrameColour, probeMediaMetadata } from "./mediaMetadata.js"; import { cleanupProxyCache } from "./proxyCache.js"; import { PROXY_VARIANT_CONFIG, type ProxyVariant } from "./mediaCodecMap.js"; @@ -444,7 +445,9 @@ async function runFfmpeg( : [evenScale, `format=${pixelFormat}`].join(","); return new Promise((resolvePromise, reject) => { - const commonArgs = ["-y", "-i", sourcePath, "-vf", videoFilter]; + const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null; + const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : []; + const commonArgs = ["-y", ...decoderArgs, "-i", sourcePath, "-vf", videoFilter]; const h264Args = [ "-c:v", "libx264", diff --git a/scripts/package-size-budgets.json b/scripts/package-size-budgets.json index 38f896dcc5..97b4031c72 100644 --- a/scripts/package-size-budgets.json +++ b/scripts/package-size-budgets.json @@ -6,7 +6,7 @@ "@hyperframes/gcp-cloud-run": { "packed": 49412, "unpacked": 192672 }, "@hyperframes/inspector": { "packed": 808905, "unpacked": 3645432 }, "@hyperframes/lint": { "packed": 168745, "unpacked": 654607 }, - "@hyperframes/parsers": { "packed": 190591, "unpacked": 831243 }, + "@hyperframes/parsers": { "packed": 191000, "unpacked": 831243 }, "@hyperframes/player": { "packed": 157145, "unpacked": 541601 }, "@hyperframes/producer": { "packed": 768210, "unpacked": 2807938 }, "@hyperframes/sdk": { "packed": 90662, "unpacked": 325069 },