@@ -4,7 +4,8 @@ import type { Canvas } from "@canvas/shotstack-canvas";
44import type { Edit } from "@core/edit-session" ;
55import { ASSET_SETTLE_TIMEOUT_MS , ExportCoordinator } from "@core/export/export-coordinator" ;
66import { ExportProgressUI } from "@core/export/export-progress-ui" ;
7- import { ExportError } from "@core/export/export-utils" ;
7+ import { ExportError , exportBitrate } from "@core/export/export-utils" ;
8+ import { calculateSizeFromPreset } from "@core/output-settings-manager" ;
89import { canEncodeVideo , CanvasSink , CanvasSource , Input , Output } from "mediabunny" ;
910
1011const mockOutputs : Array < { cancel : jest . Mock ; state : string } > = [ ] ;
@@ -63,8 +64,9 @@ function createHarness() {
6364 isPlaying : false ,
6465 playbackTime : 1.5 ,
6566 totalDuration : 1 ,
67+ size : { ...SIZE } ,
6668 tracks : [ ] as Array < Array < { clipId : string ; needsResolution : boolean } > > ,
67- getEdit : ( ) => ( { output : { fps : 2 , size : SIZE } } ) ,
69+ getEdit : ( ) => ( { output : { fps : 2 } } ) ,
6870 getOutputFormat : jest . fn ( ( ) => "mp4" ) ,
6971 getResolvedEdit : ( ) => ( { timeline : { tracks : [ ] } , output : { format : "mp4" } } ) ,
7072 getFontUrlByFamilyAndWeight : ( ) => null ,
@@ -413,6 +415,35 @@ describe("editor restart", () => {
413415 } ) ;
414416} ) ;
415417
418+ describe ( "output size" , ( ) => {
419+ it ( "draws and encodes at the size the editor lays clips out in, with a bitrate for that size" , async ( ) => {
420+ const { exporter, edit, renderer } = createHarness ( ) ;
421+ // An edit sized by resolution "hd" and aspectRatio "9:16", with no output.size.
422+ edit . size = calculateSizeFromPreset ( "hd" , "9:16" ) ;
423+
424+ await exporter . exportBlob ( ) ;
425+
426+ expect ( renderer . extract . pixels ) . toHaveBeenCalledWith ( expect . objectContaining ( { frame : { x : 0 , y : 0 , width : 720 , height : 1280 } } ) ) ;
427+ expect ( canEncodeVideo ) . toHaveBeenCalledWith ( "avc" , { width : 720 , height : 1280 , bitrate : 2_222_222 } ) ;
428+ expect ( CanvasSource ) . toHaveBeenCalledWith ( expect . objectContaining ( { width : 720 , height : 1280 } ) , { codec : "avc" , bitrate : 2_222_222 } ) ;
429+ exporter . dispose ( ) ;
430+ } ) ;
431+ } ) ;
432+
433+ describe ( "exportBitrate" , ( ) => {
434+ it ( "keeps 5 Mbps at 1080p in either orientation and scales with pixel count" , ( ) => {
435+ expect ( exportBitrate ( { width : 1920 , height : 1080 } ) ) . toBe ( 5_000_000 ) ;
436+ expect ( exportBitrate ( { width : 1080 , height : 1920 } ) ) . toBe ( 5_000_000 ) ;
437+ expect ( exportBitrate ( { width : 3840 , height : 2160 } ) ) . toBe ( 20_000_000 ) ;
438+ expect ( exportBitrate ( { width : 1280 , height : 720 } ) ) . toBe ( 2_222_222 ) ;
439+ } ) ;
440+
441+ it ( "never drops below 1 Mbps" , ( ) => {
442+ expect ( exportBitrate ( { width : 640 , height : 360 } ) ) . toBe ( 1_000_000 ) ;
443+ expect ( exportBitrate ( { width : 512 , height : 288 } ) ) . toBe ( 1_000_000 ) ;
444+ } ) ;
445+ } ) ;
446+
416447describe ( "ExportCoordinator.export" , ( ) => {
417448 it ( "wraps a failed download as an export-phase error and removes the overlay" , async ( ) => {
418449 const { exporter } = createHarness ( ) ;
0 commit comments