@bitmagic/cli 0.1.75-dev.0 → 0.1.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +26 -0
  2. package/assets/skills/building-ads/SKILL.md +98 -0
  3. package/dist/ads/check.d.ts +31 -0
  4. package/dist/ads/check.js +142 -0
  5. package/dist/ads/check.js.map +1 -0
  6. package/dist/ads/client.d.ts +78 -0
  7. package/dist/ads/client.js +146 -0
  8. package/dist/ads/client.js.map +1 -0
  9. package/dist/ads/paths.d.ts +11 -0
  10. package/dist/ads/paths.js +36 -0
  11. package/dist/ads/paths.js.map +1 -0
  12. package/dist/ads/probe.d.ts +12 -0
  13. package/dist/ads/probe.js +44 -0
  14. package/dist/ads/probe.js.map +1 -0
  15. package/dist/ads/publish.d.ts +31 -0
  16. package/dist/ads/publish.js +108 -0
  17. package/dist/ads/publish.js.map +1 -0
  18. package/dist/ads/record.d.ts +19 -0
  19. package/dist/ads/record.js +53 -0
  20. package/dist/ads/record.js.map +1 -0
  21. package/dist/ads/render-plan.d.ts +57 -0
  22. package/dist/ads/render-plan.js +127 -0
  23. package/dist/ads/render-plan.js.map +1 -0
  24. package/dist/ads/render.d.ts +51 -0
  25. package/dist/ads/render.js +212 -0
  26. package/dist/ads/render.js.map +1 -0
  27. package/dist/cli.d.ts +1 -0
  28. package/dist/cli.js +2 -0
  29. package/dist/cli.js.map +1 -1
  30. package/dist/commands/ads.d.ts +5 -0
  31. package/dist/commands/ads.js +240 -0
  32. package/dist/commands/ads.js.map +1 -0
  33. package/dist/event/launch.d.ts +2 -2
  34. package/dist/event/launch.js +2 -2
  35. package/dist/event/launch.js.map +1 -1
  36. package/dist/event/preflight.js +2 -2
  37. package/dist/event/preflight.js.map +1 -1
  38. package/dist/scaffold/ads-skill-files.d.ts +2 -0
  39. package/dist/scaffold/ads-skill-files.js +7 -0
  40. package/dist/scaffold/ads-skill-files.js.map +1 -0
  41. package/dist/scaffold/project-files.js +1 -1
  42. package/dist/scaffold/project.js +2 -0
  43. package/dist/scaffold/project.js.map +1 -1
  44. package/dist/scaffold/skill-files.js +2 -1
  45. package/dist/scaffold/skill-files.js.map +1 -1
  46. package/package.json +4 -4
@@ -0,0 +1,44 @@
1
+ /**
2
+ * What a media file is, according to ffprobe: its frame size, its length, and whether it has sound.
3
+ * The one place `bitmagic ads` reads a file's real shape — a render is checked by what ffprobe says
4
+ * it wrote, not by what the render meant to write.
5
+ */
6
+ import { captureFfmpegTool } from '@bitmagic/asset-core/trailer';
7
+ import { CliError } from '../errors.js';
8
+ /** ffprobe's JSON for one file, parsed. Pure, so the parsing is testable without ffprobe. */
9
+ export function parseProbe(json, file) {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(json);
13
+ }
14
+ catch {
15
+ throw new CliError(`ffprobe answered in a shape this CLI cannot read for ${file}.`);
16
+ }
17
+ const root = (typeof parsed === 'object' && parsed !== null ? parsed : {});
18
+ const streams = Array.isArray(root.streams) ? root.streams : [];
19
+ const video = streams.find((stream) => stream.codec_type === 'video');
20
+ if (!video || typeof video.width !== 'number' || typeof video.height !== 'number') {
21
+ throw new CliError(`${file} has no picture ffprobe can read.`);
22
+ }
23
+ const formatDuration = Number(root.format?.duration);
24
+ const streamDuration = Number(video.duration);
25
+ const durationSec = Number.isFinite(formatDuration) ? formatDuration : Number.isFinite(streamDuration) ? streamDuration : 0;
26
+ return {
27
+ width: video.width,
28
+ height: video.height,
29
+ durationSec,
30
+ hasAudio: streams.some((stream) => stream.codec_type === 'audio'),
31
+ codec: typeof video.codec_name === 'string' ? video.codec_name : '',
32
+ };
33
+ }
34
+ export async function probeMedia(file) {
35
+ const stdout = await captureFfmpegTool('ffprobe', [
36
+ '-v', 'error',
37
+ '-print_format', 'json',
38
+ '-show_streams',
39
+ '-show_format',
40
+ file,
41
+ ]);
42
+ return parseProbe(stdout.toString('utf-8'), file);
43
+ }
44
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/ads/probe.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAYxC,6FAA6F;AAC7F,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAY;IACnD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,QAAQ,CAAC,wDAAwD,IAAI,GAAG,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAGxE,CAAC;IACF,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IACtE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClF,MAAM,IAAI,QAAQ,CAAC,GAAG,IAAI,mCAAmC,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACrD,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5H,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW;QACX,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC;QACjE,KAAK,EAAE,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;KACpE,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,SAAS,EAAE;QAChD,IAAI,EAAE,OAAO;QACb,eAAe,EAAE,MAAM;QACvB,eAAe;QACf,cAAc;QACd,IAAI;KACL,CAAC,CAAC;IACH,OAAO,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { Output } from '../output.js';
2
+ import { type CheckDeps } from './check.js';
3
+ export interface AdsPublishDeps {
4
+ fetch: typeof globalThis.fetch;
5
+ sleep: (ms: number) => Promise<void>;
6
+ now?: () => number;
7
+ /** Test-only: the credentials store; production reads ~/.bitmagic. */
8
+ baseDir?: string;
9
+ probe?: CheckDeps['probe'];
10
+ }
11
+ export interface AdsPublishResult {
12
+ gameId: string;
13
+ packageId: string;
14
+ promoteUrl: string;
15
+ promotable: boolean;
16
+ uploaded: string[];
17
+ reused: string[];
18
+ }
19
+ export declare function sha256Of(bytes: Uint8Array | string): string;
20
+ /** The copy's fingerprint as registered: its normalized JSON, so whitespace edits do not count. */
21
+ export declare function copySha256(copy: unknown): string;
22
+ export declare function runAdsPublish(options: {
23
+ root: string;
24
+ output: Output;
25
+ deps?: AdsPublishDeps;
26
+ }): Promise<AdsPublishResult>;
27
+ /**
28
+ * What changed on disk since the last publish from this machine, by file name ("ads.json" for the
29
+ * copy). Empty when nothing did — or when there is no record to compare with.
30
+ */
31
+ export declare function changedSincePublish(root: string): string[];
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `bitmagic ads publish` — upload the ad package to Bitmagic's cloud. Nothing is advertised.
3
+ *
4
+ * Refuses locally first (`checkAdPackage`, the same rules api-server applies), then uploads each of
5
+ * the six files through a signed PUT under the game's own `game-media/<gameId>/`, and registers the
6
+ * package. A file whose bytes match the last upload (`.bitmagic/ads/published.json`) is not sent
7
+ * again: rewriting a headline re-registers the package in a second.
8
+ *
9
+ * What it hands back is the page where the creator can choose to pay — `bitmagic ads buy` opens it.
10
+ */
11
+ import { createHash } from 'crypto';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { AD_IMAGE_SLOT_NAMES, AD_VIDEO_SLOT_NAMES, normalizeAdCopy } from '@bitmagic/asset-core/ad-package';
15
+ import { CliError } from '../errors.js';
16
+ import { loadProjectContext } from '../project/context.js';
17
+ import { resolveEnvironmentForCommand, resolveAuth0ClientId } from '../config/environments.js';
18
+ import { getAccessToken } from '../auth/session.js';
19
+ import { putSignedUrl } from '../http/signed-upload.js';
20
+ import { checkAdPackage, CONTENT_TYPES, isComplete, manifestFrom } from './check.js';
21
+ import { adsDir, adCopyPath } from './paths.js';
22
+ import { mintAdUploadUrl, registerAdPackage } from './client.js';
23
+ import { readAdRecord, writeAdRecord } from './record.js';
24
+ import { probeMedia } from './probe.js';
25
+ function defaultDeps() {
26
+ return { fetch: globalThis.fetch, sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
27
+ }
28
+ export function sha256Of(bytes) {
29
+ return createHash('sha256').update(bytes).digest('hex');
30
+ }
31
+ /** The copy's fingerprint as registered: its normalized JSON, so whitespace edits do not count. */
32
+ export function copySha256(copy) {
33
+ return sha256Of(JSON.stringify(normalizeAdCopy(copy)));
34
+ }
35
+ export async function runAdsPublish(options) {
36
+ const { root, output } = options;
37
+ const deps = options.deps ?? defaultDeps();
38
+ const check = await checkAdPackage(root, { probe: deps.probe ?? probeMedia });
39
+ const { copy, videos, images } = check;
40
+ if (!check.ok || copy === null || !isComplete(AD_VIDEO_SLOT_NAMES, videos) || !isComplete(AD_IMAGE_SLOT_NAMES, images)) {
41
+ throw new CliError(`The ad package is not ready:\n${check.problems.map((p) => ` - ${p}`).join('\n')}\nFix these and run \`bitmagic ads check\`.`, 1, 'ads_invalid');
42
+ }
43
+ const { metadata } = loadProjectContext(root);
44
+ const gameId = metadata.gameId;
45
+ const environment = resolveEnvironmentForCommand({ cwd: root, baseDir: deps.baseDir });
46
+ const accessToken = await getAccessToken(environment, resolveAuth0ClientId(environment), { fetch: deps.fetch, sleep: deps.sleep, now: deps.now }, deps.baseDir);
47
+ const previous = readAdRecord(root);
48
+ const reusable = previous !== null && previous.gameId === gameId ? previous.files : {};
49
+ const files = {};
50
+ const uploaded = [];
51
+ const reused = [];
52
+ for (const file of [...Object.values(videos), ...Object.values(images)]) {
53
+ const bytes = new Uint8Array(fs.readFileSync(file.path));
54
+ const sha256 = sha256Of(bytes);
55
+ const before = reusable[file.name];
56
+ if (before !== undefined && before.sha256 === sha256 && before.bytes === bytes.byteLength) {
57
+ files[file.name] = before;
58
+ reused.push(file.name);
59
+ continue;
60
+ }
61
+ output.info(`Uploading ${file.name} (${(bytes.byteLength / (1024 * 1024)).toFixed(1)} MiB)...`);
62
+ const ticket = await mintAdUploadUrl(environment, accessToken, { gameId, contentType: CONTENT_TYPES[file.kind], bytes: bytes.byteLength }, deps);
63
+ // Exactly the header the signature bound: game-media tickets pin only the content type.
64
+ await putSignedUrl(ticket.uploadUrl, bytes, { 'Content-Type': ticket.contentType }, deps);
65
+ files[file.name] = { url: ticket.publicUrl, sha256, bytes: bytes.byteLength };
66
+ uploaded.push(file.name);
67
+ }
68
+ const manifest = manifestFrom(check.gameName, copy, videos, images, (file) => files[file.name].url);
69
+ const registered = await registerAdPackage(environment, accessToken, { gameId, manifest }, deps);
70
+ const now = deps.now ?? Date.now;
71
+ writeAdRecord(root, {
72
+ gameId,
73
+ packageId: registered.packageId,
74
+ at: new Date(now()).toISOString(),
75
+ copySha256: copySha256(copy),
76
+ files,
77
+ });
78
+ return { gameId, packageId: registered.packageId, promoteUrl: registered.promoteUrl, promotable: registered.promotable, uploaded, reused };
79
+ }
80
+ /**
81
+ * What changed on disk since the last publish from this machine, by file name ("ads.json" for the
82
+ * copy). Empty when nothing did — or when there is no record to compare with.
83
+ */
84
+ export function changedSincePublish(root) {
85
+ const record = readAdRecord(root);
86
+ if (record === null)
87
+ return [];
88
+ const changed = [];
89
+ try {
90
+ if (copySha256(JSON.parse(fs.readFileSync(adCopyPath(root), 'utf-8'))) !== record.copySha256)
91
+ changed.push('ads.json');
92
+ }
93
+ catch {
94
+ changed.push('ads.json');
95
+ }
96
+ for (const [name, before] of Object.entries(record.files)) {
97
+ try {
98
+ const bytes = new Uint8Array(fs.readFileSync(path.join(adsDir(root), name)));
99
+ if (bytes.byteLength !== before.bytes || sha256Of(bytes) !== before.sha256)
100
+ changed.push(name);
101
+ }
102
+ catch {
103
+ changed.push(name);
104
+ }
105
+ }
106
+ return changed;
107
+ }
108
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/ads/publish.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export interface AdRecordFile {
2
+ /** The CDN URL api-server issued, under the game's own `game-media/<gameId>/`. */
3
+ url: string;
4
+ sha256: string;
5
+ bytes: number;
6
+ }
7
+ export interface AdRecord {
8
+ gameId: string;
9
+ packageId: string;
10
+ /** ISO 8601. */
11
+ at: string;
12
+ /** SHA-256 of the copy as registered, so a later edit to ads.json is noticed. */
13
+ copySha256: string;
14
+ /** Keyed by file name inside `.bitmagic/ads/` (`video-16x9.mp4`, `image-1x1.jpg`, …). */
15
+ files: Record<string, AdRecordFile>;
16
+ }
17
+ export declare function writeAdRecord(root: string, record: AdRecord): void;
18
+ /** The last publish, or null when there is none this reader can trust. */
19
+ export declare function readAdRecord(root: string): AdRecord | null;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The on-disk record of the last `bitmagic ads publish` (`.bitmagic/ads/published.json`).
3
+ *
4
+ * Two jobs. It lets a re-publish skip every file that has not changed since — an agent that only
5
+ * rewrites a headline re-registers the package without re-uploading three videos — by keeping each
6
+ * uploaded file's SHA-256 beside the URL api-server issued for it. And it lets `bitmagic ads buy`
7
+ * say when the package on the server is older than what is on disk.
8
+ *
9
+ * Every field is checked on read, in the style of trailer/publish-record.ts: the file outlives CLI
10
+ * versions, and a half-written or older-shaped record must read as "nothing uploaded yet", never as
11
+ * a URL reused for the wrong file.
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import { adRecordPath } from './paths.js';
16
+ export function writeAdRecord(root, record) {
17
+ fs.mkdirSync(path.dirname(adRecordPath(root)), { recursive: true });
18
+ fs.writeFileSync(adRecordPath(root), `${JSON.stringify(record, null, 2)}\n`);
19
+ }
20
+ function isRecordFile(value) {
21
+ if (typeof value !== 'object' || value === null)
22
+ return false;
23
+ const file = value;
24
+ return typeof file.url === 'string' && typeof file.sha256 === 'string' && typeof file.bytes === 'number';
25
+ }
26
+ /** The last publish, or null when there is none this reader can trust. */
27
+ export function readAdRecord(root) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(fs.readFileSync(adRecordPath(root), 'utf-8'));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ if (typeof parsed !== 'object' || parsed === null)
36
+ return null;
37
+ const record = parsed;
38
+ if (typeof record.gameId !== 'string'
39
+ || typeof record.packageId !== 'string'
40
+ || typeof record.at !== 'string'
41
+ || typeof record.copySha256 !== 'string'
42
+ || typeof record.files !== 'object'
43
+ || record.files === null) {
44
+ return null;
45
+ }
46
+ const files = {};
47
+ for (const [name, file] of Object.entries(record.files)) {
48
+ if (isRecordFile(file))
49
+ files[name] = { url: file.url, sha256: file.sha256, bytes: file.bytes };
50
+ }
51
+ return { gameId: record.gameId, packageId: record.packageId, at: record.at, copySha256: record.copySha256, files };
52
+ }
53
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/ads/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAoB1C,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,MAAgB;IAC1D,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,IAAI,GAAG,KAAgC,CAAC;IAC9C,OAAO,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC;AAC3G,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,IACE,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;WAC9B,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;WACpC,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ;WAC7B,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;WACrC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;WAChC,MAAM,CAAC,KAAK,KAAK,IAAI,EACxB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAiC,EAAE,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAgC,CAAC,EAAE,CAAC;QACnF,IAAI,YAAY,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;IAClG,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC;AACrH,CAAC"}
@@ -0,0 +1,57 @@
1
+ export type FitMode = 'scale' | 'crop' | 'blur';
2
+ /** Gameplay loses at most this much of a side to a crop; past it, blur-fill keeps the HUD whole. */
3
+ export declare const VIDEO_MAX_CROP_LOSS = 0.12;
4
+ /** A cover is art, framed around its middle: it can lose more before blur-fill looks better. */
5
+ export declare const COVER_MAX_CROP_LOSS = 0.45;
6
+ /** The frame rate every ad video is written at. */
7
+ export declare const AD_FPS = 30;
8
+ /** How long the end card fades in over. */
9
+ export declare const END_CARD_FADE_SEC = 0.35;
10
+ /** The sound fades out over the last second. */
11
+ export declare const AUDIO_FADE_SEC = 1;
12
+ export declare function fitMode(srcWidth: number, srcHeight: number, dstWidth: number, dstHeight: number, maxCropLoss: number): FitMode;
13
+ /**
14
+ * The filtergraph fragment fitting `[input]` into a `width`×`height` frame as `[output]`. `id`
15
+ * keeps the labels of several fragments in one graph apart.
16
+ */
17
+ export declare function fitFilter(mode: FitMode, width: number, height: number, input: string, output: string, id: string): string;
18
+ export interface SegmentChoice {
19
+ startSec: number;
20
+ durationSec: number;
21
+ }
22
+ /**
23
+ * The part of a source an ad uses: from `start`, at most `seconds`, never past `usableEnd` (the
24
+ * source's length, or where a trailer's own end card begins). Null when what is left is shorter
25
+ * than an ad may be.
26
+ */
27
+ export declare function chooseSegment(usableEnd: number, start: number, seconds: number): SegmentChoice | null;
28
+ /** Seconds as ffmpeg reads them: no exponent, at most three decimals. */
29
+ export declare function sec(n: number): string;
30
+ export interface VideoPlan {
31
+ source: string;
32
+ sourceWidth: number;
33
+ sourceHeight: number;
34
+ sourceHasAudio: boolean;
35
+ segment: SegmentChoice;
36
+ width: number;
37
+ height: number;
38
+ /** A full-frame PNG with alpha, faded in for the last `endCardSec`; omitted for no end card. */
39
+ endCardPng?: string;
40
+ endCardSec: number;
41
+ out: string;
42
+ }
43
+ /** The complete ffmpeg argument list for one ad video. */
44
+ export declare function videoArgs(plan: VideoPlan): string[];
45
+ export interface StillPlan {
46
+ source: string;
47
+ sourceWidth: number;
48
+ sourceHeight: number;
49
+ /** Where in a video to take the frame; omitted for an image source. */
50
+ atSec?: number;
51
+ maxCropLoss: number;
52
+ width: number;
53
+ height: number;
54
+ out: string;
55
+ }
56
+ /** The ffmpeg argument list for one still (JPEG). */
57
+ export declare function stillArgs(plan: StillPlan): string[];
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The pure half of `bitmagic ads render`: how a source fits each ad frame, and the exact ffmpeg
3
+ * arguments that make it. No filesystem, no processes, so every decision here is testable.
4
+ *
5
+ * Three ways to fit a source into a frame of another shape:
6
+ *
7
+ * - `scale` — the shapes match (within 2%): resize.
8
+ * - `crop` — they nearly match: fill the frame and trim the overflow. Fine for a few percent of a
9
+ * cover image; wrong for gameplay, where the HUD sits at the edges.
10
+ * - `blur` — they do not: the source whole, centred, over a blurred and darkened copy of itself
11
+ * filling the frame. The standard way a landscape game clip becomes a 9:16 ad.
12
+ *
13
+ * Text is never drawn by ffmpeg (the trailer tooling's ffmpeg has no libfreetype): the end card is
14
+ * a PNG rasterized in Chrome, faded in over the last seconds.
15
+ */
16
+ import { AD_VIDEO_SECONDS } from '@bitmagic/asset-core/ad-package';
17
+ /** Gameplay loses at most this much of a side to a crop; past it, blur-fill keeps the HUD whole. */
18
+ export const VIDEO_MAX_CROP_LOSS = 0.12;
19
+ /** A cover is art, framed around its middle: it can lose more before blur-fill looks better. */
20
+ export const COVER_MAX_CROP_LOSS = 0.45;
21
+ /** The frame rate every ad video is written at. */
22
+ export const AD_FPS = 30;
23
+ /** How long the end card fades in over. */
24
+ export const END_CARD_FADE_SEC = 0.35;
25
+ /** The sound fades out over the last second. */
26
+ export const AUDIO_FADE_SEC = 1;
27
+ export function fitMode(srcWidth, srcHeight, dstWidth, dstHeight, maxCropLoss) {
28
+ const src = srcWidth / srcHeight;
29
+ const dst = dstWidth / dstHeight;
30
+ if (Math.abs(src - dst) / dst <= 0.02)
31
+ return 'scale';
32
+ // The share of the cropped side that survives a fill-and-crop.
33
+ const kept = Math.min(src, dst) / Math.max(src, dst);
34
+ return 1 - kept <= maxCropLoss ? 'crop' : 'blur';
35
+ }
36
+ /** Even, so every intermediate frame is valid yuv420p. */
37
+ function even(n) {
38
+ return Math.max(2, Math.round(n / 2) * 2);
39
+ }
40
+ /**
41
+ * The filtergraph fragment fitting `[input]` into a `width`×`height` frame as `[output]`. `id`
42
+ * keeps the labels of several fragments in one graph apart.
43
+ */
44
+ export function fitFilter(mode, width, height, input, output, id) {
45
+ if (mode === 'scale')
46
+ return `[${input}]scale=${width}:${height}:flags=lanczos,setsar=1[${output}]`;
47
+ if (mode === 'crop') {
48
+ return `[${input}]scale=${width}:${height}:force_original_aspect_ratio=increase:flags=lanczos,crop=${width}:${height},setsar=1[${output}]`;
49
+ }
50
+ // Blur at a quarter of the size: the same look for a fraction of the work.
51
+ const smallW = even(width / 4);
52
+ const smallH = even(height / 4);
53
+ return [
54
+ `[${input}]split=2[${id}bgsrc][${id}fgsrc]`,
55
+ `[${id}bgsrc]scale=${smallW}:${smallH}:force_original_aspect_ratio=increase,crop=${smallW}:${smallH},gblur=sigma=10,`
56
+ + `scale=${width}:${height},drawbox=x=0:y=0:w=iw:h=ih:color=black@0.35:t=fill[${id}bg]`,
57
+ `[${id}fgsrc]scale=${width}:${height}:force_original_aspect_ratio=decrease:force_divisible_by=2:flags=lanczos,setsar=1[${id}fg]`,
58
+ `[${id}bg][${id}fg]overlay=x=(W-w)/2:y=(H-h)/2,setsar=1[${output}]`,
59
+ ].join(';');
60
+ }
61
+ /**
62
+ * The part of a source an ad uses: from `start`, at most `seconds`, never past `usableEnd` (the
63
+ * source's length, or where a trailer's own end card begins). Null when what is left is shorter
64
+ * than an ad may be.
65
+ */
66
+ export function chooseSegment(usableEnd, start, seconds) {
67
+ const available = usableEnd - start;
68
+ const durationSec = Math.min(seconds, available, AD_VIDEO_SECONDS.max);
69
+ if (!(durationSec >= AD_VIDEO_SECONDS.min))
70
+ return null;
71
+ // Whole frames, so every slot is exactly as long as the others.
72
+ return { startSec: start, durationSec: Math.floor(durationSec * AD_FPS) / AD_FPS };
73
+ }
74
+ /** Seconds as ffmpeg reads them: no exponent, at most three decimals. */
75
+ export function sec(n) {
76
+ return (Math.round(n * 1000) / 1000).toFixed(3).replace(/\.?0+$/, '') || '0';
77
+ }
78
+ /** The complete ffmpeg argument list for one ad video. */
79
+ export function videoArgs(plan) {
80
+ const { segment, width, height } = plan;
81
+ const duration = sec(segment.durationSec);
82
+ const args = ['-y', '-hide_banner', '-loglevel', 'error', '-ss', sec(segment.startSec), '-t', duration, '-i', plan.source];
83
+ let next = 1;
84
+ let cardInput = null;
85
+ if (plan.endCardPng !== undefined) {
86
+ cardInput = next++;
87
+ args.push('-loop', '1', '-framerate', String(AD_FPS), '-t', duration, '-i', plan.endCardPng);
88
+ }
89
+ let silenceInput = null;
90
+ if (!plan.sourceHasAudio) {
91
+ // A silent track rather than none: some placements treat a video with no audio stream as broken.
92
+ silenceInput = next++;
93
+ args.push('-f', 'lavfi', '-t', duration, '-i', 'anullsrc=r=48000:cl=stereo');
94
+ }
95
+ const mode = fitMode(plan.sourceWidth, plan.sourceHeight, width, height, VIDEO_MAX_CROP_LOSS);
96
+ const graph = [`[0:v]fps=${AD_FPS}[src]`, fitFilter(mode, width, height, 'src', 'base', 'v')];
97
+ if (cardInput !== null) {
98
+ const cardAt = Math.max(0, segment.durationSec - plan.endCardSec);
99
+ graph.push(`[${cardInput}:v]format=rgba,fade=t=in:st=${sec(cardAt)}:d=${sec(END_CARD_FADE_SEC)}:alpha=1[card]`);
100
+ graph.push('[base][card]overlay=0:0,format=yuv420p[v]');
101
+ }
102
+ else {
103
+ graph.push('[base]format=yuv420p[v]');
104
+ }
105
+ if (plan.sourceHasAudio) {
106
+ const fadeAt = Math.max(0, segment.durationSec - AUDIO_FADE_SEC);
107
+ graph.push(`[0:a]aresample=48000,afade=t=out:st=${sec(fadeAt)}:d=${sec(AUDIO_FADE_SEC)}[a]`);
108
+ }
109
+ args.push('-filter_complex', graph.join(';'), '-map', '[v]', '-map', silenceInput !== null ? `${silenceInput}:a` : '[a]', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p', '-profile:v', 'high', '-r', String(AD_FPS), '-c:a', 'aac', '-b:a', '128k', '-ar', '48000', '-ac', '2', '-t', duration, '-movflags', '+faststart', plan.out);
110
+ return args;
111
+ }
112
+ /** The ffmpeg argument list for one still (JPEG). */
113
+ export function stillArgs(plan) {
114
+ const mode = fitMode(plan.sourceWidth, plan.sourceHeight, plan.width, plan.height, plan.maxCropLoss);
115
+ const seek = plan.atSec !== undefined ? ['-ss', sec(plan.atSec)] : [];
116
+ return [
117
+ '-y', '-hide_banner', '-loglevel', 'error',
118
+ ...seek,
119
+ '-i', plan.source,
120
+ '-filter_complex', fitFilter(mode, plan.width, plan.height, '0:v', 'v', 'i'),
121
+ '-map', '[v]',
122
+ '-frames:v', '1',
123
+ '-q:v', '2',
124
+ plan.out,
125
+ ];
126
+ }
127
+ //# sourceMappingURL=render-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-plan.js","sourceRoot":"","sources":["../../src/ads/render-plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import { type AdCopy, type AdImageSlot, type AdVideoSlot } from '@bitmagic/asset-core/ad-package';
2
+ import type { Output } from '../output.js';
3
+ import { type SegmentChoice } from './render-plan.js';
4
+ /** The end card's second line unless `--cta` says otherwise. True of every game on bitmagic.ai. */
5
+ export declare const DEFAULT_CTA = "Play now in your browser";
6
+ export interface AdsRenderOptions {
7
+ root: string;
8
+ output: Output;
9
+ /** The source video. Default: the newest `bitmagic trailer` render. */
10
+ video?: string;
11
+ /** A portrait (9:16) source for the 9:16 video, instead of blur-filling the main one. */
12
+ portrait?: string;
13
+ /** The source of the stills. Default: `.bitmagic/cover.webp`, else a frame of the video. */
14
+ image?: string;
15
+ /** How long the ads run. Default AD_VIDEO_SECONDS.recommended. */
16
+ seconds?: number;
17
+ /** Where in the source to start. Default 0. */
18
+ start?: number;
19
+ /** The end card's second line. Default DEFAULT_CTA. */
20
+ cta?: string;
21
+ /** False skips the end card. */
22
+ endCard: boolean;
23
+ }
24
+ export interface RenderedFile {
25
+ file: string;
26
+ width: number;
27
+ height: number;
28
+ durationSec: number;
29
+ bytes: number;
30
+ }
31
+ export interface AdsRenderResult {
32
+ source: string;
33
+ portraitSource?: string;
34
+ stillSource: string;
35
+ segment: SegmentChoice;
36
+ videos: Record<AdVideoSlot, RenderedFile>;
37
+ images: Record<AdImageSlot, RenderedFile>;
38
+ endCard: boolean;
39
+ /** True when this run wrote the empty copy file the agent now has to fill in. */
40
+ copyTemplateWritten: boolean;
41
+ }
42
+ /** The newest `bitmagic trailer` render under `.bitmagic/trailer/`, or null. */
43
+ export declare function newestTrailerRender(root: string): string | null;
44
+ /**
45
+ * Where the usable footage ends: where a trailer render's own end card begins (read from its EDL
46
+ * sidecar), else the end of the file. The ad draws its own end card at every size.
47
+ */
48
+ export declare function usableEnd(file: string, durationSec: number): number;
49
+ /** The copy file an agent fills in: every field present, every line empty, so `check` lists them. */
50
+ export declare function emptyCopy(): AdCopy;
51
+ export declare function runAdsRender(options: AdsRenderOptions): Promise<AdsRenderResult>;