@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossclip/scenes",
3
- "version": "0.1.36",
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.36"
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
- * No duration on the Sequences: an effect plays for its own length. That is
18
- * also why cues can overlap the composition's own end — Remotion truncates the
19
- * tail at the last frame, and a whoosh clipped by the end of the video is the
20
- * same thing an editor would do by hand.
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 { soundFile?: unknown; atSec?: unknown; gain?: unknown };
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
- out.push({ ...cue, from });
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
  }