Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 50 additions & 21 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
{
"branches": ["main"],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [
{ "message": "*fix/*", "release": "patch" },
{ "message": "*hotfix/*", "release": "patch" },
{ "message": "*feat/*", "release": "minor" },
{ "message": "*release/*", "release": "major" }
]
}],
"@semantic-release/release-notes-generator",
["@semantic-release/npm", {
"pkgRoot": "."
}],
["@semantic-release/git", {
"assets": ["package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}],
"@semantic-release/github"
]
"branches": ["main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
{
"message": "*/fix/*",
"release": "patch"
},
{
"message": "*/hotfix/*",
"release": "patch"
},
{
"message": "*/feat/*",
"release": "minor"
},
{
"message": "*/release/*",
"release": "major"
},
{
"type": "fix",
"release": "patch"
},
{
"type": "feat",
"release": "minor"
}
]
}
],
"@semantic-release/release-notes-generator",
[
"@semantic-release/npm",
{
"pkgRoot": "."
}
],
[
"@semantic-release/git",
{
"assets": ["package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"@shotstack/schemas": "1.8.7",
"@shotstack/shotstack-canvas": "^2.0.17",
"@shotstack/schemas": "1.9.0",
"@shotstack/shotstack-canvas": "^2.1.5",
"howler": "^2.2.4",
"mediabunny": "^1.11.2",
"opentype.js": "^1.3.4",
Expand Down
103 changes: 81 additions & 22 deletions src/components/canvas/players/rich-caption-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class RichCaptionPlayer extends Player {
private readonly fontRegistrationCache = new Map<string, Promise<boolean>>();
private lastRegisteredFontKey: string = "";
private pendingLayoutId: number = 0;
private resolvedPauseThreshold: number = 500;

constructor(edit: Edit, clipConfiguration: ResolvedClip) {
const { fit, ...configWithoutFit } = clipConfiguration;
Expand All @@ -63,7 +64,7 @@ export class RichCaptionPlayer extends Player {
let words: WordTiming[];
if (richCaptionAsset.src) {
words = await this.fetchAndParseSubtitle(richCaptionAsset.src);
(richCaptionAsset as Record<string, unknown>)['pauseThreshold'] = 5;
this.resolvedPauseThreshold = 5;
} else {
words = ((richCaptionAsset as RichCaptionAsset & { words?: WordTiming[] }).words ?? []).map((w: WordTiming) => ({
text: w.text,
Expand Down Expand Up @@ -381,9 +382,9 @@ export class RichCaptionPlayer extends Player {
const payload: Record<string, unknown> = {
type: asset.type,
words: words.map(w => ({ text: w.text, start: w.start, end: w.end, confidence: w.confidence })),
font: { family: resolvedFamily, ...asset.font },
font: { ...asset.font, family: resolvedFamily },
width,
height,
height
};

const optionalFields: Record<string, unknown> = {
Expand All @@ -396,7 +397,7 @@ export class RichCaptionPlayer extends Player {
style: asset.style,
wordAnimation: asset.wordAnimation,
align: asset.align,
pauseThreshold: (asset as Record<string, unknown>)['pauseThreshold'],
pauseThreshold: this.resolvedPauseThreshold
};

for (const [key, value] of Object.entries(optionalFields)) {
Expand All @@ -406,32 +407,51 @@ export class RichCaptionPlayer extends Player {
}

if (customFonts.length > 0) {
payload['customFonts'] = customFonts;
payload["customFonts"] = customFonts;
}

return payload;
}

private buildLayoutConfig(asset: CanvasRichCaptionAsset, frameWidth: number, frameHeight: number): CaptionLayoutConfig {
const { font, style, align, padding: rawPadding } = asset;
const padding = typeof rawPadding === "number" ? rawPadding : (rawPadding?.left ?? 0);

let padding: { top: number; right: number; bottom: number; left: number };
if (typeof rawPadding === "number") {
padding = { top: rawPadding, right: rawPadding, bottom: rawPadding, left: rawPadding };
} else if (rawPadding) {
const p = rawPadding as { top?: number; right?: number; bottom?: number; left?: number };
padding = { top: p.top ?? 0, right: p.right ?? 0, bottom: p.bottom ?? 0, left: p.left ?? 0 };
} else {
padding = { top: 0, right: 0, bottom: 0, left: 0 };
}

const totalHorizontalPadding = padding.left + padding.right;
const availableWidth = totalHorizontalPadding > 0
? frameWidth - totalHorizontalPadding
: frameWidth * 0.9;

const fontSize = font?.size ?? 24;
const lineHeight = style?.lineHeight ?? 1.2;
const availableHeight = frameHeight - padding.top - padding.bottom;
const maxLines = Math.max(1, Math.min(10, Math.floor(availableHeight / (fontSize * lineHeight))));

return {
frameWidth,
frameHeight,
availableWidth: frameWidth * 0.9,
maxLines: 2,
verticalAlign: align?.vertical ?? "bottom",
availableWidth,
maxLines,
verticalAlign: align?.vertical ?? "middle",
horizontalAlign: align?.horizontal ?? "center",
paddingLeft: padding,
fontSize: font?.size ?? 24,
padding,
fontSize,
fontFamily: font?.family ?? "Roboto",
fontWeight: String(font?.weight ?? "400"),
letterSpacing: style?.letterSpacing ?? 0,
wordSpacing: typeof style?.wordSpacing === "number" ? style.wordSpacing : 0,
lineHeight: style?.lineHeight ?? 1.2,
lineHeight,
textTransform: (style?.textTransform as CaptionLayoutConfig["textTransform"]) ?? "none",
pauseThreshold: asset.pauseThreshold ?? 500
pauseThreshold: this.resolvedPauseThreshold
};
}

Expand Down Expand Up @@ -530,20 +550,56 @@ export class RichCaptionPlayer extends Player {
}

protected override onDimensionsChanged(): void {
if (!this.layoutEngine || !this.validatedAsset || !this.canvas || !this.painter) return;
if (this.words.length === 0) return;

const { width, height } = this.getSize();
this.rebuildForCurrentSize();
}

this.canvas.width = width;
this.canvas.height = height;
private async rebuildForCurrentSize(): Promise<void> {
const currentTimeMs = this.getPlaybackTime() * 1000;

if (this.texture) {
this.texture.destroy();
this.texture = null;
}
if (this.sprite) {
this.contentContainer.removeChild(this.sprite);
this.sprite.destroy();
this.sprite = null;
}
if (this.contentContainer.mask) {
const { mask } = this.contentContainer;
this.contentContainer.mask = null;
if (mask instanceof pixi.Graphics) {
mask.destroy();
}
}

this.captionLayout = null;
this.validatedAsset = null;
this.generatorConfig = null;
this.canvas = null;
this.painter = null;

const { width, height } = this.getSize();
const asset = this.clipConfiguration.asset as RichCaptionAsset;

const canvasPayload = this.buildCanvasPayload(asset, this.words);
const canvasValidation = CanvasRichCaptionAssetSchema.safeParse(canvasPayload);
if (!canvasValidation.success) {
return;
}
this.validatedAsset = canvasValidation.data;

this.generatorConfig = createDefaultGeneratorConfig(width, height, 1);

this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;
this.painter = createWebPainter(this.canvas);

if (!this.layoutEngine) return;

const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
const canvasTextMeasurer = this.createCanvasTextMeasurer();
if (canvasTextMeasurer) {
Expand All @@ -552,11 +608,14 @@ export class RichCaptionPlayer extends Player {

this.pendingLayoutId += 1;
const layoutId = this.pendingLayoutId;
this.layoutEngine.layoutCaption(this.words, layoutConfig).then(layout => {
if (layoutId !== this.pendingLayoutId) return;
this.captionLayout = layout;
this.renderFrameSync(this.getPlaybackTime() * 1000);
});

const layout = await this.layoutEngine.layoutCaption(this.words, layoutConfig);

if (layoutId !== this.pendingLayoutId) return;

this.captionLayout = layout;

this.renderFrameSync(currentTimeMs);
}

public override supportsEdgeResize(): boolean {
Expand Down
15 changes: 15 additions & 0 deletions src/core/ui/selection-handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export class SelectionHandles implements CanvasOverlayRegistration {
transform?: { rotate?: { angle: number } };
} | null = null;

private edgeResizeNotifyTimer: ReturnType<typeof setTimeout> | null = null;
private static readonly EDGE_RESIZE_NOTIFY_INTERVAL_MS = 100;

// Bound event handlers for cleanup
private onClipSelectedBound: (payload: { trackIndex: number; clipIndex: number }) => void;
private onSelectionClearedBound: () => void;
Expand Down Expand Up @@ -469,6 +472,11 @@ export class SelectionHandles implements CanvasOverlayRegistration {
// Commit with explicit final state (adds to history, doesn't execute)
this.edit.commitClipUpdate(this.selectedClipId, this.initialClipConfiguration, finalClip);

if (this.edgeResizeNotifyTimer) {
clearTimeout(this.edgeResizeNotifyTimer);
this.edgeResizeNotifyTimer = null;
}

// Notify player if dimensions changed (corner or edge resize)
if ((this.scaleDirection || this.edgeDragDirection) && this.selectedPlayer) {
this.selectedPlayer.notifyDimensionsChanged();
Expand Down Expand Up @@ -644,6 +652,13 @@ export class SelectionHandles implements CanvasOverlayRegistration {
});
this.edit.resolveClip(this.selectedClipId);

if (this.selectedPlayer.supportsEdgeResize() && !this.edgeResizeNotifyTimer) {
this.edgeResizeNotifyTimer = setTimeout(() => {
this.edgeResizeNotifyTimer = null;
this.selectedPlayer?.notifyDimensionsChanged();
}, SelectionHandles.EDGE_RESIZE_NOTIFY_INTERVAL_MS);
}

this.showDimensionLabel(rounded.width, rounded.height);
}

Expand Down
Loading
Loading