@ossclip/scenes 0.1.36 → 0.1.38
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/package.json +2 -2
- package/src/SfxTrack.tsx +10 -4
- package/src/sfx-track.ts +50 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossclip/scenes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "ossclip's scene library and stage geometry — React components shared by preview and render",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"zod": "^3.25.0",
|
|
22
|
-
"@ossclip/core": "0.1.
|
|
22
|
+
"@ossclip/core": "0.1.38"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"react": ">=18",
|
package/src/SfxTrack.tsx
CHANGED
|
@@ -14,10 +14,15 @@ import { visibleSfxCues, type SfxCueProps } from "./sfx-track";
|
|
|
14
14
|
* overlay would have meant (and would have needed its own gain staging to
|
|
15
15
|
* avoid clipping the take it lands on).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* BOUNDED Sequences (2026-09-18). These carried no duration at all — "an
|
|
18
|
+
* effect plays for its own length" — which is true of the SOUND and false of
|
|
19
|
+
* the tag: an unbounded Sequence keeps its `<Audio>` mounted to the end of the
|
|
20
|
+
* composition, so the mounted count climbed with the playhead until the
|
|
21
|
+
* editor's Player ran out of shared audio tags and the error boundary ate the
|
|
22
|
+
* whole stage (`SFX_FALLBACK_DURATION_SEC` has the field report). The length
|
|
23
|
+
* comes from the pack's `durationSec`, and cues still overlap the
|
|
24
|
+
* composition's end — `visibleSfxCues` clamps there, the same truncation
|
|
25
|
+
* Remotion would have done at the last frame.
|
|
21
26
|
*
|
|
22
27
|
* Deliberately invisible to the editor, the Watermark/CoverInVideo rule: no
|
|
23
28
|
* `data-edit-id`, nothing to hit-test, nothing rendered at all. Placement
|
|
@@ -34,6 +39,7 @@ export const SfxTrack: React.FC<{ cues: readonly SfxCueProps[] }> = ({ cues }) =
|
|
|
34
39
|
// key, and React would remount one of them on any list change.
|
|
35
40
|
key={`${cue.from}-${i}`}
|
|
36
41
|
from={cue.from}
|
|
42
|
+
durationInFrames={cue.durationInFrames}
|
|
37
43
|
layout="none"
|
|
38
44
|
>
|
|
39
45
|
<Audio
|
package/src/sfx-track.ts
CHANGED
|
@@ -13,8 +13,34 @@ export interface SfxCueProps {
|
|
|
13
13
|
soundFile: string;
|
|
14
14
|
atSec: number;
|
|
15
15
|
gain: number;
|
|
16
|
+
/**
|
|
17
|
+
* The sound's own length, when the pack declared one. Present so the
|
|
18
|
+
* renderer can BOUND the cue's Sequence — see `SFX_FALLBACK_DURATION_SEC`
|
|
19
|
+
* for why an unbounded one took the editor's preview down (2026-09-18).
|
|
20
|
+
*/
|
|
21
|
+
durationSec?: number;
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
/**
|
|
25
|
+
* How long a cue's Sequence lasts when the pack declared no `durationSec`.
|
|
26
|
+
*
|
|
27
|
+
* There has to be a bound. An unbounded Sequence keeps its `<Audio>` MOUNTED
|
|
28
|
+
* for the rest of the composition, so the mounted-audio count only ever grew
|
|
29
|
+
* with the playhead — and the editor's `<Player>` mounts its audio through a
|
|
30
|
+
* fixed pool of shared tags. The sixth cue of this project's thirteen threw
|
|
31
|
+
* "Tried to simultaneously mount 6 <Html5Audio /> tags", Remotion's error
|
|
32
|
+
* boundary replaced the whole stage with a warning triangle, and NOTHING
|
|
33
|
+
* played again — not even the takes before the cue — until a reload (field
|
|
34
|
+
* report 2026-09-18, ".ossclip/CAA ship karachi After"). The render never saw
|
|
35
|
+
* it: an ffmpeg render mounts no shared tags.
|
|
36
|
+
*
|
|
37
|
+
* Generous on purpose: longer than any sound in the bundled packs, so a cue
|
|
38
|
+
* with no declared length is bounded without being TRUNCATED. A sound that
|
|
39
|
+
* really does run past it is cut off at ten seconds, which is the trade a
|
|
40
|
+
* pack author fixes by declaring `durationSec`.
|
|
41
|
+
*/
|
|
42
|
+
export const SFX_FALLBACK_DURATION_SEC = 10;
|
|
43
|
+
|
|
18
44
|
/**
|
|
19
45
|
* The loudest a cue may play. `HTMLMediaElement.volume` THROWS an
|
|
20
46
|
* IndexSizeError above 1, so an un-clamped gain would take the editor's
|
|
@@ -39,17 +65,30 @@ export function sfxCuesFor(value: unknown): SfxCueProps[] {
|
|
|
39
65
|
const cues: SfxCueProps[] = [];
|
|
40
66
|
for (const entry of value) {
|
|
41
67
|
if (typeof entry !== "object" || entry === null) continue;
|
|
42
|
-
const v = entry as {
|
|
68
|
+
const v = entry as {
|
|
69
|
+
soundFile?: unknown;
|
|
70
|
+
atSec?: unknown;
|
|
71
|
+
gain?: unknown;
|
|
72
|
+
durationSec?: unknown;
|
|
73
|
+
};
|
|
43
74
|
if (typeof v.soundFile !== "string" || v.soundFile.length === 0) continue;
|
|
44
75
|
if (typeof v.atSec !== "number" || !Number.isFinite(v.atSec) || v.atSec < 0) continue;
|
|
45
76
|
// An absent gain is 1 (play it as recorded); a mangled one is refused
|
|
46
77
|
// rather than defaulted, because a wrong LEVEL is audible and silent
|
|
47
78
|
// about being wrong.
|
|
48
79
|
if (v.gain !== undefined && (typeof v.gain !== "number" || !Number.isFinite(v.gain))) continue;
|
|
80
|
+
// A mangled length is DROPPED, not the cue: the sound still has a fallback
|
|
81
|
+
// bound to fall back on, and silencing a whoosh over a number nobody hears
|
|
82
|
+
// would be a worse trade than the rest of this parser's drops.
|
|
83
|
+
const durationSec =
|
|
84
|
+
typeof v.durationSec === "number" && Number.isFinite(v.durationSec) && v.durationSec > 0
|
|
85
|
+
? v.durationSec
|
|
86
|
+
: undefined;
|
|
49
87
|
cues.push({
|
|
50
88
|
soundFile: v.soundFile,
|
|
51
89
|
atSec: v.atSec,
|
|
52
90
|
gain: Math.min(SFX_MAX_VOLUME, Math.max(0, v.gain ?? 1)),
|
|
91
|
+
...(durationSec === undefined ? {} : { durationSec }),
|
|
53
92
|
});
|
|
54
93
|
}
|
|
55
94
|
return cues;
|
|
@@ -64,17 +103,24 @@ export function sfxCuesFor(value: unknown): SfxCueProps[] {
|
|
|
64
103
|
* than clamped to it: a whoosh planned for a moment the render no longer
|
|
65
104
|
* reaches must not pile onto the final frame with everything else that fell
|
|
66
105
|
* off the end.
|
|
106
|
+
*
|
|
107
|
+
* `durationInFrames` per cue is the LENGTH the Sequence gets (2026-09-18): the
|
|
108
|
+
* declared `durationSec`, or `SFX_FALLBACK_DURATION_SEC`, clamped so a cue
|
|
109
|
+
* near the end never asks for frames the composition does not have — Remotion
|
|
110
|
+
* truncates the tail anyway, and a Sequence that ends WITH the composition is
|
|
111
|
+
* the same mounted-forever tag this bound exists to retire.
|
|
67
112
|
*/
|
|
68
113
|
export function visibleSfxCues(
|
|
69
114
|
cues: readonly SfxCueProps[],
|
|
70
115
|
fps: number,
|
|
71
116
|
durationInFrames: number,
|
|
72
|
-
): Array<SfxCueProps & { from: number }> {
|
|
73
|
-
const out: Array<SfxCueProps & { from: number }> = [];
|
|
117
|
+
): Array<SfxCueProps & { from: number; durationInFrames: number }> {
|
|
118
|
+
const out: Array<SfxCueProps & { from: number; durationInFrames: number }> = [];
|
|
74
119
|
for (const cue of cues) {
|
|
75
120
|
const from = Math.round(cue.atSec * fps);
|
|
76
121
|
if (from < 0 || from >= durationInFrames) continue;
|
|
77
|
-
|
|
122
|
+
const want = Math.ceil((cue.durationSec ?? SFX_FALLBACK_DURATION_SEC) * fps);
|
|
123
|
+
out.push({ ...cue, from, durationInFrames: Math.max(1, Math.min(want, durationInFrames - from)) });
|
|
78
124
|
}
|
|
79
125
|
return out;
|
|
80
126
|
}
|