@jant/core 0.5.3 → 0.5.4
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.
- package/dist/{app-C481ssbr.js → app-BtNdUAqz.js} +5 -5
- package/dist/{app-BgMwEN-M.js → app-DLINgGBd.js} +1 -1
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-CQvi1Buw.css → client-BErXNT6k.css} +1 -1
- package/dist/client/_assets/{client-CJQYvkEx.js → client-CtAgWT8i.js} +1 -1
- package/dist/client/_assets/{client-auth-CfBiCAB7.js → client-auth-DJ_5wx9N.js} +39 -39
- package/dist/index.js +1 -1
- package/dist/node.js +2 -2
- package/package.json +1 -1
- package/src/client/components/jant-compose-editor.ts +72 -0
- package/src/client/tiptap/paste-media.ts +49 -33
- package/src/client/video-processor.ts +9 -0
- package/src/lib/__tests__/mp4-track-flags.test.ts +117 -0
- package/src/lib/mp4-track-flags.ts +71 -0
- package/src/styles/ui.css +18 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as url_exports } from "./url-umUptr5z.js";
|
|
2
|
-
import { A as NAV_ITEM_TYPES, C as toPostView, D as MAX_MEDIA_ATTACHMENTS, E as FORMATS, M as STATUSES, N as TEXT_ATTACHMENT_CONTENT_FORMATS, O as MAX_PINNED_POSTS, S as toNavItemViews, T as toSearchResultView, _ as createMediaContext, b as toMediaView, f as defaultFeedRenderer, j as SORT_ORDERS, k as MEDIA_KINDS, t as createApp, v as toArchiveGroups, w as toPostViews, x as toNavItemView, y as toArchiveGroupsWithMedia } from "./app-
|
|
2
|
+
import { A as NAV_ITEM_TYPES, C as toPostView, D as MAX_MEDIA_ATTACHMENTS, E as FORMATS, M as STATUSES, N as TEXT_ATTACHMENT_CONTENT_FORMATS, O as MAX_PINNED_POSTS, S as toNavItemViews, T as toSearchResultView, _ as createMediaContext, b as toMediaView, f as defaultFeedRenderer, j as SORT_ORDERS, k as MEDIA_KINDS, t as createApp, v as toArchiveGroups, w as toPostViews, x as toNavItemView, y as toArchiveGroupsWithMedia } from "./app-BtNdUAqz.js";
|
|
3
3
|
import { T as time_exports, a as markdown_exports } from "./export-CR9Megtb.js";
|
|
4
4
|
import "./env-CgaH9Mut.js";
|
|
5
5
|
import "./github-sync-DYZq9rQp.js";
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./url-umUptr5z.js";
|
|
2
|
-
import { F as BUILTIN_COLOR_THEMES, I as getPublicAssetBasePath, L as isAssetPath, P as buildThemeStyle, a as resolveDatabaseDialect, c as resolveConfig, d as getFontThemeCssVariables, g as schema_exports, h as createNodeDatabase, i as createSiteService, l as BUILTIN_FONT_THEMES, m as sqliteSchemaBundle, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as pgSchemaBundle, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as getCjkSerifCssVariables } from "./app-
|
|
2
|
+
import { F as BUILTIN_COLOR_THEMES, I as getPublicAssetBasePath, L as isAssetPath, P as buildThemeStyle, a as resolveDatabaseDialect, c as resolveConfig, d as getFontThemeCssVariables, g as schema_exports, h as createNodeDatabase, i as createSiteService, l as BUILTIN_FONT_THEMES, m as sqliteSchemaBundle, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as pgSchemaBundle, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as getCjkSerifCssVariables } from "./app-BtNdUAqz.js";
|
|
3
3
|
import { t as createExportService } from "./export-CR9Megtb.js";
|
|
4
4
|
import { b as getSiteResolutionMode, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as shouldTrustProxy, y as getPort } from "./env-CgaH9Mut.js";
|
|
5
5
|
import "./github-sync-DYZq9rQp.js";
|
|
@@ -474,7 +474,7 @@ async function createNodeRequestHandler(options) {
|
|
|
474
474
|
async function start(env = process.env, app) {
|
|
475
475
|
const handler = await createNodeRequestHandler({
|
|
476
476
|
env,
|
|
477
|
-
app: async () => app ?? (await import("./app-
|
|
477
|
+
app: async () => app ?? (await import("./app-DLINgGBd.js")).createApp()
|
|
478
478
|
});
|
|
479
479
|
const hostname = resolveHost(env);
|
|
480
480
|
const port = resolvePort(env);
|
package/package.json
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
uploadAndInsertInlineImage,
|
|
47
47
|
adoptPendingInlineImageUploads,
|
|
48
48
|
} from "../tiptap/inline-image-upload.js";
|
|
49
|
+
import { getClipboardFiles } from "../tiptap/paste-media.js";
|
|
49
50
|
import { isSafeAbsoluteUrl } from "../../lib/url.js";
|
|
50
51
|
import { randomUUID } from "../random-uuid.js";
|
|
51
52
|
import {
|
|
@@ -276,6 +277,13 @@ export class JantComposeEditor extends LitElement {
|
|
|
276
277
|
"jant:slash-command-discovered",
|
|
277
278
|
this._onSlashCommandDiscovered,
|
|
278
279
|
);
|
|
280
|
+
this.addEventListener("dragenter", this._onDragEnter);
|
|
281
|
+
// Capture phase: a file dragover is stopped here before ProseMirror sees
|
|
282
|
+
// it, so its drop cursor never appears for file drags (the drop position
|
|
283
|
+
// is decided by _shouldPasteInlineImage, not the cursor).
|
|
284
|
+
this.addEventListener("dragover", this._onDragOver, true);
|
|
285
|
+
this.addEventListener("dragleave", this._onDragLeave);
|
|
286
|
+
this.addEventListener("drop", this._onDrop);
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
disconnectedCallback() {
|
|
@@ -291,6 +299,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
291
299
|
this._onSlashCommandDiscovered,
|
|
292
300
|
);
|
|
293
301
|
document.removeEventListener("click", this._onDocClickBound);
|
|
302
|
+
this.removeEventListener("dragenter", this._onDragEnter);
|
|
303
|
+
this.removeEventListener("dragover", this._onDragOver, true);
|
|
304
|
+
this.removeEventListener("dragleave", this._onDragLeave);
|
|
305
|
+
this.removeEventListener("drop", this._onDrop);
|
|
294
306
|
hideSlashCommandHint(this);
|
|
295
307
|
this._emojiContainer?.remove();
|
|
296
308
|
this._emojiPickerEl = null;
|
|
@@ -298,6 +310,66 @@ export class JantComposeEditor extends LitElement {
|
|
|
298
310
|
this._filePickerCleanup = null;
|
|
299
311
|
}
|
|
300
312
|
|
|
313
|
+
// Tracks dragenter/dragleave nesting so the highlight only clears when the
|
|
314
|
+
// pointer actually leaves the editor, not when it crosses a child element.
|
|
315
|
+
#dragDepth = 0;
|
|
316
|
+
|
|
317
|
+
#dragHasFiles(event: DragEvent): boolean {
|
|
318
|
+
const types = event.dataTransfer?.types;
|
|
319
|
+
return types ? Array.from(types).includes("Files") : false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private _onDragEnter = (event: DragEvent) => {
|
|
323
|
+
if (!this.#dragHasFiles(event)) return;
|
|
324
|
+
this.#dragDepth += 1;
|
|
325
|
+
this.classList.add("compose-editor-dragover");
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
private _onDragOver = (event: DragEvent) => {
|
|
329
|
+
if (!this.#dragHasFiles(event)) return;
|
|
330
|
+
// Keep the file dragover away from ProseMirror so its drop cursor stays
|
|
331
|
+
// hidden; internal content drags still bubble through untouched.
|
|
332
|
+
event.stopPropagation();
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
private _onDragLeave = (event: DragEvent) => {
|
|
338
|
+
if (!this.#dragHasFiles(event)) return;
|
|
339
|
+
this.#dragDepth = Math.max(0, this.#dragDepth - 1);
|
|
340
|
+
if (this.#dragDepth === 0) {
|
|
341
|
+
this.classList.remove("compose-editor-dragover");
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
private _onDrop = (event: DragEvent) => {
|
|
346
|
+
this.#dragDepth = 0;
|
|
347
|
+
this.classList.remove("compose-editor-dragover");
|
|
348
|
+
// Drops onto the TipTap body are claimed by the pasteMedia plugin's
|
|
349
|
+
// handleDrop, which calls preventDefault — skip those to avoid handling
|
|
350
|
+
// the same files twice.
|
|
351
|
+
if (event.defaultPrevented) return;
|
|
352
|
+
const files = getClipboardFiles(event.dataTransfer);
|
|
353
|
+
if (files.length === 0) return;
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
|
|
356
|
+
const inlineFiles: File[] = [];
|
|
357
|
+
const attachmentFiles: File[] = [];
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
if (this._editor && this._shouldPasteInlineImage(file)) {
|
|
360
|
+
inlineFiles.push(file);
|
|
361
|
+
} else {
|
|
362
|
+
attachmentFiles.push(file);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (const file of inlineFiles) {
|
|
366
|
+
this._uploadAndInsertImage(file);
|
|
367
|
+
}
|
|
368
|
+
if (attachmentFiles.length > 0) {
|
|
369
|
+
this.addFiles(attachmentFiles);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
301
373
|
private _onSlashCommandDiscovered = () => {
|
|
302
374
|
markSlashCommandDiscovered();
|
|
303
375
|
};
|
|
@@ -76,6 +76,47 @@ export const PasteMedia = Extension.create<PasteMediaOptions>({
|
|
|
76
76
|
addProseMirrorPlugins() {
|
|
77
77
|
const extension = this;
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Routes dropped/pasted files into inline images or attachments using the
|
|
81
|
+
* same decision as the host (`shouldInsertInline`). Returns false when
|
|
82
|
+
* there is nothing this extension can handle, so the caller leaves the
|
|
83
|
+
* event to the editor's default behavior.
|
|
84
|
+
*/
|
|
85
|
+
const routeFiles = (files: File[]): boolean => {
|
|
86
|
+
const inlineFiles = files.filter(
|
|
87
|
+
(file) => extension.options.shouldInsertInline?.(file) === true,
|
|
88
|
+
);
|
|
89
|
+
const attachmentFiles = files.filter(
|
|
90
|
+
(file) => !inlineFiles.includes(file),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
inlineFiles.length === 0 &&
|
|
95
|
+
(attachmentFiles.length === 0 ||
|
|
96
|
+
extension.options.onPasteFiles === undefined)
|
|
97
|
+
) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const file of inlineFiles) {
|
|
102
|
+
const uploadInlineImage = extension.options.uploadInlineImage;
|
|
103
|
+
if (uploadInlineImage) {
|
|
104
|
+
void uploadInlineImage(file);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
void uploadAndInsertInlineImage(extension.editor, file);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
attachmentFiles.length > 0 &&
|
|
112
|
+
extension.options.onPasteFiles !== undefined
|
|
113
|
+
) {
|
|
114
|
+
extension.options.onPasteFiles(attachmentFiles);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
|
|
79
120
|
return [
|
|
80
121
|
new Plugin({
|
|
81
122
|
key: pasteMediaPluginKey,
|
|
@@ -83,40 +124,15 @@ export const PasteMedia = Extension.create<PasteMediaOptions>({
|
|
|
83
124
|
handlePaste(_view, event) {
|
|
84
125
|
const files = getClipboardFiles(event.clipboardData);
|
|
85
126
|
if (files.length === 0) return false;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
inlineFiles.length === 0 &&
|
|
96
|
-
(attachmentFiles.length === 0 ||
|
|
97
|
-
extension.options.onPasteFiles === undefined)
|
|
98
|
-
) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
127
|
+
if (!routeFiles(files)) return false;
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
handleDrop(_view, event) {
|
|
132
|
+
const files = getClipboardFiles(event.dataTransfer);
|
|
133
|
+
if (files.length === 0) return false;
|
|
134
|
+
if (!routeFiles(files)) return false;
|
|
102
135
|
event.preventDefault();
|
|
103
|
-
|
|
104
|
-
for (const file of inlineFiles) {
|
|
105
|
-
const uploadInlineImage = extension.options.uploadInlineImage;
|
|
106
|
-
if (uploadInlineImage) {
|
|
107
|
-
void uploadInlineImage(file);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
void uploadAndInsertInlineImage(extension.editor, file);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
attachmentFiles.length > 0 &&
|
|
115
|
-
extension.options.onPasteFiles !== undefined
|
|
116
|
-
) {
|
|
117
|
-
extension.options.onPasteFiles(attachmentFiles);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
136
|
return true;
|
|
121
137
|
},
|
|
122
138
|
},
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - Strips spurious rotation metadata from the output (mediabunny may
|
|
8
8
|
* bake rotation into pixels AND write a display matrix, causing the
|
|
9
9
|
* browser to double-rotate)
|
|
10
|
+
* - Clears the alternate_group track flag (mediabunny sets it non-zero,
|
|
11
|
+
* which stops Safari's native video controls from auto-hiding)
|
|
10
12
|
* - Extracts poster frame + blurhash during processing
|
|
11
13
|
*
|
|
12
14
|
* Requires WebCodecs API support — check `isSupported()` before use.
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
} from "mediabunny";
|
|
26
28
|
import { encode } from "blurhash";
|
|
27
29
|
import { normalizeDurationSeconds } from "../lib/video-playback.js";
|
|
30
|
+
import { zeroTrackAlternateGroups } from "../lib/mp4-track-flags.js";
|
|
28
31
|
|
|
29
32
|
/** Maximum pixels for the long edge of the output video. */
|
|
30
33
|
const MAX_LONG_EDGE = 1920;
|
|
@@ -222,6 +225,12 @@ async function processToFile(
|
|
|
222
225
|
const buffer = target.buffer;
|
|
223
226
|
if (!buffer) throw new Error("Video processing produced no output");
|
|
224
227
|
|
|
228
|
+
// Mediabunny tags each track with a non-zero alternate_group, which makes
|
|
229
|
+
// Safari treat tracks as mutually exclusive alternates and never auto-hide
|
|
230
|
+
// the native <video> control bar during playback. Zero it so the controls
|
|
231
|
+
// behave like any other MP4.
|
|
232
|
+
zeroTrackAlternateGroups(buffer);
|
|
233
|
+
|
|
225
234
|
// Detect whether this browser double-rotates. Chrome's WebCodecs
|
|
226
235
|
// bakes rotation into the pixel data AND mediabunny writes a display
|
|
227
236
|
// matrix → the browser applies the matrix again (double-rotation).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { zeroTrackAlternateGroups } from "../mp4-track-flags.js";
|
|
3
|
+
|
|
4
|
+
const str4 = (s: string): number[] => [...s].map((c) => c.charCodeAt(0));
|
|
5
|
+
const u32 = (n: number): number[] => [
|
|
6
|
+
(n >>> 24) & 0xff,
|
|
7
|
+
(n >>> 16) & 0xff,
|
|
8
|
+
(n >>> 8) & 0xff,
|
|
9
|
+
n & 0xff,
|
|
10
|
+
];
|
|
11
|
+
const u16 = (n: number): number[] => [(n >>> 8) & 0xff, n & 0xff];
|
|
12
|
+
const box = (type: string, ...payload: number[][]): number[] => {
|
|
13
|
+
const body = payload.flat();
|
|
14
|
+
return [...u32(body.length + 8), ...str4(type), ...body];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** A `tkhd` box with a given version and alternate_group value. */
|
|
18
|
+
function tkhd(version: 0 | 1, alternateGroup: number): number[] {
|
|
19
|
+
// creation, modification, trackID, reserved: 4 fields, 4 or 8 bytes each.
|
|
20
|
+
const idAndTimes =
|
|
21
|
+
version === 1 ? new Array(24).fill(0) : new Array(16).fill(0);
|
|
22
|
+
const duration = version === 1 ? new Array(8).fill(0) : new Array(4).fill(0);
|
|
23
|
+
return box(
|
|
24
|
+
"tkhd",
|
|
25
|
+
[version, 0, 0, 0], // version + flags
|
|
26
|
+
idAndTimes,
|
|
27
|
+
duration,
|
|
28
|
+
new Array(8).fill(0), // reserved[2]
|
|
29
|
+
u16(0), // layer
|
|
30
|
+
u16(alternateGroup),
|
|
31
|
+
new Array(48).fill(0), // volume, reserved, matrix, width, height
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("zeroTrackAlternateGroups", () => {
|
|
36
|
+
it("zeroes a non-zero alternate_group in every tkhd (v0)", () => {
|
|
37
|
+
const bytes = new Uint8Array(
|
|
38
|
+
box(
|
|
39
|
+
"moov",
|
|
40
|
+
box("trak", tkhd(0, 1), box("mdia", [])),
|
|
41
|
+
box("trak", tkhd(0, 2), box("mdia", [])),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
zeroTrackAlternateGroups(bytes.buffer);
|
|
45
|
+
|
|
46
|
+
const view = new DataView(bytes.buffer);
|
|
47
|
+
// Locate both tkhd boxes and confirm alternate_group is now 0.
|
|
48
|
+
const groups: number[] = [];
|
|
49
|
+
for (let i = 0; i + 8 <= bytes.length; i++) {
|
|
50
|
+
if (
|
|
51
|
+
String.fromCharCode(
|
|
52
|
+
bytes[i + 4],
|
|
53
|
+
bytes[i + 5],
|
|
54
|
+
bytes[i + 6],
|
|
55
|
+
bytes[i + 7],
|
|
56
|
+
) === "tkhd"
|
|
57
|
+
) {
|
|
58
|
+
groups.push(view.getUint16(i + 8 + 4 + 20 + 8 + 2));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
expect(groups).toEqual([0, 0]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles version 1 tkhd boxes", () => {
|
|
65
|
+
const bytes = new Uint8Array(box("moov", box("trak", tkhd(1, 7))));
|
|
66
|
+
zeroTrackAlternateGroups(bytes.buffer);
|
|
67
|
+
|
|
68
|
+
const view = new DataView(bytes.buffer);
|
|
69
|
+
let offset = -1;
|
|
70
|
+
for (let i = 0; i + 8 <= bytes.length; i++) {
|
|
71
|
+
if (
|
|
72
|
+
String.fromCharCode(
|
|
73
|
+
bytes[i + 4],
|
|
74
|
+
bytes[i + 5],
|
|
75
|
+
bytes[i + 6],
|
|
76
|
+
bytes[i + 7],
|
|
77
|
+
) === "tkhd"
|
|
78
|
+
) {
|
|
79
|
+
offset = i + 8 + 4 + 32 + 8 + 2;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
expect(view.getUint16(offset)).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("touches nothing but the alternate_group field", () => {
|
|
86
|
+
const original = new Uint8Array(
|
|
87
|
+
box(
|
|
88
|
+
"moov",
|
|
89
|
+
box("trak", tkhd(0, 0x0102), box("mdia", box("mdhd", u32(0)))),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
const copy = new Uint8Array(original);
|
|
93
|
+
zeroTrackAlternateGroups(copy.buffer);
|
|
94
|
+
|
|
95
|
+
const diffs: number[] = [];
|
|
96
|
+
for (let i = 0; i < original.length; i++) {
|
|
97
|
+
if (original[i] !== copy[i]) diffs.push(i);
|
|
98
|
+
}
|
|
99
|
+
// Exactly the two bytes of one alternate_group field changed.
|
|
100
|
+
expect(diffs).toHaveLength(2);
|
|
101
|
+
expect(diffs[1]).toBe(diffs[0] + 1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("leaves an already-zero alternate_group untouched", () => {
|
|
105
|
+
const original = new Uint8Array(box("moov", box("trak", tkhd(0, 0))));
|
|
106
|
+
const copy = new Uint8Array(original);
|
|
107
|
+
zeroTrackAlternateGroups(copy.buffer);
|
|
108
|
+
expect([...copy]).toEqual([...original]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("ignores files with no tkhd box", () => {
|
|
112
|
+
const original = new Uint8Array(box("ftyp", str4("isom")));
|
|
113
|
+
const copy = new Uint8Array(original);
|
|
114
|
+
zeroTrackAlternateGroups(copy.buffer);
|
|
115
|
+
expect([...copy]).toEqual([...original]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place patching of MP4 track header (`tkhd`) flags.
|
|
3
|
+
*
|
|
4
|
+
* Mediabunny's MP4 muxer writes a non-zero `alternate_group` into every
|
|
5
|
+
* `tkhd` box (video → 1, audio → 2). Per ISO-BMFF, a non-zero
|
|
6
|
+
* `alternate_group` marks a track as one of several mutually exclusive
|
|
7
|
+
* alternates. Safari's native `<video>` player reacts to this by never
|
|
8
|
+
* auto-hiding the control bar during playback — the controls stay pinned.
|
|
9
|
+
* ffmpeg writes 0 here; matching that restores normal control-bar behavior.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** ISO-BMFF container boxes that hold a `tkhd` somewhere below them. */
|
|
13
|
+
const CONTAINER_TYPES = new Set(["moov", "trak"]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Byte offset of the `alternate_group` field within a `tkhd` box, measured
|
|
17
|
+
* from the start of the box. The field sits after the box header, the
|
|
18
|
+
* version/flags word, the (version-sized) time/duration fields, the two
|
|
19
|
+
* reserved words, and the 2-byte `layer` field.
|
|
20
|
+
*/
|
|
21
|
+
function alternateGroupOffset(version: number): number {
|
|
22
|
+
// header(8) + version/flags(4) + variable middle + reserved(8) + layer(2):
|
|
23
|
+
// v0: creation(4) modification(4) trackID(4) reserved(4) duration(4) = 20
|
|
24
|
+
// v1: creation(8) modification(8) trackID(4) reserved(4) duration(8) = 32
|
|
25
|
+
return 8 + 4 + (version === 1 ? 32 : 20) + 8 + 2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Zero the `alternate_group` field of every `tkhd` box in an MP4 buffer,
|
|
30
|
+
* operating in place. Safe to call on any ISO-BMFF file; boxes without a
|
|
31
|
+
* `tkhd` are left untouched.
|
|
32
|
+
*
|
|
33
|
+
* @param buffer - The MP4 file bytes. Mutated in place.
|
|
34
|
+
* @example
|
|
35
|
+
* zeroTrackAlternateGroups(mediabunnyOutput);
|
|
36
|
+
*/
|
|
37
|
+
export function zeroTrackAlternateGroups(buffer: ArrayBuffer): void {
|
|
38
|
+
const view = new DataView(buffer);
|
|
39
|
+
|
|
40
|
+
const walk = (start: number, end: number): void => {
|
|
41
|
+
let pos = start;
|
|
42
|
+
while (pos + 8 <= end) {
|
|
43
|
+
let size = view.getUint32(pos);
|
|
44
|
+
const type = String.fromCharCode(
|
|
45
|
+
view.getUint8(pos + 4),
|
|
46
|
+
view.getUint8(pos + 5),
|
|
47
|
+
view.getUint8(pos + 6),
|
|
48
|
+
view.getUint8(pos + 7),
|
|
49
|
+
);
|
|
50
|
+
if (size === 0) size = end - pos;
|
|
51
|
+
if (size < 8 || pos + size > end) break;
|
|
52
|
+
|
|
53
|
+
if (type === "tkhd") {
|
|
54
|
+
const version = view.getUint8(pos + 8);
|
|
55
|
+
const fieldOffset = pos + alternateGroupOffset(version);
|
|
56
|
+
if (
|
|
57
|
+
fieldOffset + 2 <= pos + size &&
|
|
58
|
+
view.getUint16(fieldOffset) !== 0
|
|
59
|
+
) {
|
|
60
|
+
view.setUint16(fieldOffset, 0);
|
|
61
|
+
}
|
|
62
|
+
} else if (CONTAINER_TYPES.has(type)) {
|
|
63
|
+
walk(pos + 8, pos + size);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pos += size;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
walk(0, buffer.byteLength);
|
|
71
|
+
}
|
package/src/styles/ui.css
CHANGED
|
@@ -4432,6 +4432,24 @@
|
|
|
4432
4432
|
position: relative;
|
|
4433
4433
|
}
|
|
4434
4434
|
|
|
4435
|
+
jant-compose-editor.compose-editor-dragover {
|
|
4436
|
+
position: relative;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
jant-compose-editor.compose-editor-dragover::after {
|
|
4440
|
+
content: "";
|
|
4441
|
+
position: absolute;
|
|
4442
|
+
inset: 6px;
|
|
4443
|
+
z-index: 5;
|
|
4444
|
+
pointer-events: none;
|
|
4445
|
+
border-radius: 14px;
|
|
4446
|
+
background: color-mix(in srgb, var(--site-accent) 4%, transparent);
|
|
4447
|
+
box-shadow:
|
|
4448
|
+
inset 0 0 0 1.5px color-mix(in srgb, var(--site-accent) 45%, transparent),
|
|
4449
|
+
inset 0 0 28px -12px
|
|
4450
|
+
color-mix(in srgb, var(--site-accent) 45%, transparent);
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4435
4453
|
.compose-edit-loading {
|
|
4436
4454
|
display: flex;
|
|
4437
4455
|
flex: 1;
|