Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
},
"dependencies": {
"@shotstack/schemas": "1.9.3",
"@shotstack/shotstack-canvas": "^2.1.10",
"@shotstack/shotstack-canvas": "^2.1.12",
"howler": "^2.2.4",
"mediabunny": "^1.11.2",
"opentype.js": "^1.3.4",
Expand Down
62 changes: 15 additions & 47 deletions src/components/canvas/players/rich-caption-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import {
generateRichCaptionFrame,
createDefaultGeneratorConfig,
createWebPainter,
buildCaptionLayoutConfig,
parseSubtitleToWords,
CanvasRichCaptionAssetSchema,
type CanvasRichCaptionAsset,
type CaptionLayout,
type CaptionLayoutConfig,
type RichCaptionGeneratorConfig,
type WordTiming
} from "@shotstack/shotstack-canvas";
Expand Down Expand Up @@ -217,8 +217,9 @@ export class RichCaptionPlayer extends Player {
this.validatedAsset = canvasValidation.data;

const { width, height } = this.getSize();
const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
const canvasTextMeasurer = this.createCanvasTextMeasurer();
const layoutConfig = buildCaptionLayoutConfig(this.validatedAsset, width, height);
const letterSpacing = this.validatedAsset?.style?.letterSpacing;
const canvasTextMeasurer = this.createCanvasTextMeasurer(letterSpacing);
if (canvasTextMeasurer) {
layoutConfig.measureTextWidth = canvasTextMeasurer;
}
Expand Down Expand Up @@ -250,9 +251,10 @@ export class RichCaptionPlayer extends Player {
this.layoutEngine = new CaptionLayoutEngine(this.fontRegistry);

const { width, height } = this.getSize();
const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
const layoutConfig = buildCaptionLayoutConfig(this.validatedAsset, width, height);

const canvasTextMeasurer = this.createCanvasTextMeasurer();
const letterSpacing = this.validatedAsset?.style?.letterSpacing;
const canvasTextMeasurer = this.createCanvasTextMeasurer(letterSpacing);
if (canvasTextMeasurer) {
layoutConfig.measureTextWidth = canvasTextMeasurer;
}
Expand Down Expand Up @@ -500,51 +502,16 @@ export class RichCaptionPlayer extends Player {
return payload;
}

private buildLayoutConfig(asset: CanvasRichCaptionAsset, frameWidth: number, frameHeight: number): CaptionLayoutConfig {
const { font, style, align, padding: rawPadding } = asset;

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,
maxLines,
verticalAlign: align?.vertical ?? "middle",
horizontalAlign: align?.horizontal ?? "center",
padding,
fontSize,
fontFamily: font?.family ?? "Roboto",
fontWeight: String(font?.weight ?? "400"),
letterSpacing: style?.letterSpacing ?? 0,
lineHeight,
textTransform: (style?.textTransform as CaptionLayoutConfig["textTransform"]) ?? "none",
pauseThreshold: this.resolvedPauseThreshold
};
}

private createCanvasTextMeasurer(): ((text: string, font: string) => number) | undefined {
private createCanvasTextMeasurer(letterSpacing?: number): ((text: string, font: string) => number) | undefined {
try {
const measureCanvas = document.createElement("canvas");
const ctx = measureCanvas.getContext("2d");
if (!ctx) return undefined;

if (letterSpacing) {
(ctx as unknown as Record<string, unknown>)["letterSpacing"] = `${letterSpacing}px`;
}

return (text: string, font: string): number => {
ctx.font = font;
return ctx.measureText(text).width;
Expand Down Expand Up @@ -684,8 +651,9 @@ export class RichCaptionPlayer extends Player {

if (!this.layoutEngine) return;

const layoutConfig = this.buildLayoutConfig(this.validatedAsset, width, height);
const canvasTextMeasurer = this.createCanvasTextMeasurer();
const layoutConfig = buildCaptionLayoutConfig(this.validatedAsset, width, height);
const letterSpacing = this.validatedAsset?.style?.letterSpacing;
const canvasTextMeasurer = this.createCanvasTextMeasurer(letterSpacing);
if (canvasTextMeasurer) {
layoutConfig.measureTextWidth = canvasTextMeasurer;
}
Expand Down
39 changes: 38 additions & 1 deletion tests/rich-caption-player.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,43 @@ jest.mock("@shotstack/shotstack-canvas", () => {
}),
createWebPainter: (...args: unknown[]) => mockCreateWebPainter(...args),
parseSubtitleToWords: (...args: unknown[]) => mockParseSubtitleToWords(...args),
buildCaptionLayoutConfig: (asset: Record<string, unknown>, frameWidth: number, frameHeight: number) => {
const rawPadding = asset['padding'] as number | { top?: number; right?: number; bottom?: number; left?: number } | undefined;
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) {
padding = { top: rawPadding.top ?? 0, right: rawPadding.right ?? 0, bottom: rawPadding.bottom ?? 0, left: rawPadding.left ?? 0 };
} else {
padding = { top: 0, right: 0, bottom: 0, left: 0 };
}
const font = asset['font'] as { size?: number; family?: string; weight?: number | string } | undefined;
const style = asset['style'] as { letterSpacing?: number; lineHeight?: number; textTransform?: string } | undefined;
const align = asset['align'] as { vertical?: string; horizontal?: string } | undefined;
const fontSize = font?.size ?? 24;
const lineHeight = style?.lineHeight ?? 1.2;
const availableWidth = frameWidth - padding.left - padding.right;
const availableHeight = frameHeight - padding.top - padding.bottom;
const maxLines = Math.max(1, Math.min(10, Math.floor(availableHeight / (fontSize * lineHeight))));
const vertical = align?.vertical;
const horizontal = align?.horizontal;
return {
frameWidth,
frameHeight,
availableWidth,
maxLines,
verticalAlign: (() => { if (vertical === "top") return "top"; if (vertical === "middle") return "middle"; return "bottom"; })(),
horizontalAlign: (() => { if (horizontal === "left") return "left"; if (horizontal === "right") return "right"; return "center"; })(),
padding,
fontSize,
fontFamily: font?.family ?? "Roboto",
fontWeight: String(font?.weight ?? "400"),
letterSpacing: style?.letterSpacing ?? 0,
lineHeight,
textTransform: style?.textTransform ?? "none",
pauseThreshold: (asset['pauseThreshold'] as number) ?? 500,
};
},
CanvasRichCaptionAssetSchema: {
safeParse: jest.fn().mockImplementation((asset: unknown) => ({
success: true,
Expand Down Expand Up @@ -1324,7 +1361,7 @@ describe("RichCaptionPlayer", () => {
await player.load();

const layoutConfig = mockLayoutCaption.mock.calls[0]?.[1];
expect(layoutConfig.availableWidth).toBe(1920 * 0.9);
expect(layoutConfig.availableWidth).toBe(1920);
expect(layoutConfig.padding).toEqual({ top: 0, right: 0, bottom: 0, left: 0 });
});

Expand Down
Loading