@@ -18,6 +18,7 @@ import {
1818 Match ,
1919 on ,
2020 onCleanup ,
21+ onMount ,
2122 Show ,
2223 Switch ,
2324} from "solid-js" ;
@@ -59,9 +60,16 @@ const MIN_PLAYER_CONTENT_HEIGHT = 320;
5960const MIN_TIMELINE_HEIGHT = 240 ;
6061const RESIZE_HANDLE_HEIGHT = 16 ;
6162const MIN_PLAYER_HEIGHT = MIN_PLAYER_CONTENT_HEIGHT + RESIZE_HANDLE_HEIGHT ;
62-
6363const TIMELINE_RESIZE_GRIP_MARKS = [ 0 , 1 , 2 ] as const ;
6464
65+ function logCropProfile (
66+ stage : string ,
67+ data : Record < string , number | string | boolean | null > = { } ,
68+ ) {
69+ if ( ! import . meta. env . DEV ) return ;
70+ console . info ( "[crop-profile]" , stage , data ) ;
71+ }
72+
6573function getEditorErrorMessage ( error : unknown ) {
6674 return error instanceof Error ? error . message : String ( error ) ;
6775}
@@ -772,24 +780,182 @@ function Dialogs() {
772780 const [ crop , setCrop ] = createSignal ( CROP_ZERO ) ;
773781 const [ aspect , setAspect ] = createSignal < Ratio | null > ( null ) ;
774782
783+ const initialPreviewUrl = dialog ( ) . previewUrl ?? null ;
775784 const [ frameBlobUrl , setFrameBlobUrl ] = createSignal <
776785 string | null
777- > ( null ) ;
786+ > ( initialPreviewUrl ) ;
787+ const [ frameSource , setFrameSource ] = createSignal <
788+ "captured-preview" | "accurate-frame" | "screenshot"
789+ > ( initialPreviewUrl ? "captured-preview" : "screenshot" ) ;
790+ const cropOpenedAt = performance . now ( ) ;
791+ const screenshotSrc = convertFileSrc (
792+ `${ editorInstance . path } /screenshots/display.jpg` ,
793+ ) ;
794+
795+ let cancelled = false ;
796+ let frameLoadDelayTimeoutId :
797+ | ReturnType < typeof globalThis . setTimeout >
798+ | undefined ;
799+ let frameLoadTimeoutId :
800+ | ReturnType < typeof globalThis . setTimeout >
801+ | undefined ;
802+ let frameLoadIdleId : number | undefined ;
803+ let accurateFrameRequested = false ;
804+ const idleWindow = globalThis as typeof globalThis & {
805+ requestIdleCallback ?: (
806+ callback : ( ) => void ,
807+ options ?: { timeout ?: number } ,
808+ ) => number ;
809+ cancelIdleCallback ?: ( handle : number ) => void ;
810+ } ;
811+
812+ const clearScheduledAccurateFrame = ( ) => {
813+ if ( frameLoadDelayTimeoutId !== undefined ) {
814+ globalThis . clearTimeout ( frameLoadDelayTimeoutId ) ;
815+ frameLoadDelayTimeoutId = undefined ;
816+ }
817+ if ( frameLoadIdleId !== undefined ) {
818+ idleWindow . cancelIdleCallback ?.( frameLoadIdleId ) ;
819+ frameLoadIdleId = undefined ;
820+ }
821+ if ( frameLoadTimeoutId !== undefined ) {
822+ globalThis . clearTimeout ( frameLoadTimeoutId ) ;
823+ frameLoadTimeoutId = undefined ;
824+ }
825+ } ;
826+
827+ const setPreviewBlob = (
828+ blob : Blob ,
829+ source : "accurate-frame" ,
830+ ) => {
831+ const nextUrl = URL . createObjectURL ( blob ) ;
832+ const previousUrl = frameBlobUrl ( ) ;
833+ setFrameBlobUrl ( nextUrl ) ;
834+ setFrameSource ( source ) ;
835+ if ( previousUrl ) {
836+ URL . revokeObjectURL ( previousUrl ) ;
837+ }
838+ } ;
839+
840+ const requestAccurateFrame = ( reason : string ) => {
841+ if ( accurateFrameRequested || cancelled ) return ;
778842
779- commands
780- . getDisplayFrameForCropping ( FPS )
781- . then ( ( pngBytes ) => {
782- const blob = new Blob ( [ new Uint8Array ( pngBytes ) ] , {
783- type : "image/png" ,
843+ clearScheduledAccurateFrame ( ) ;
844+
845+ accurateFrameRequested = true ;
846+ const frameRequestStartedAt = performance . now ( ) ;
847+ logCropProfile ( "accurate-frame-request-start" , {
848+ elapsedMs : Number (
849+ ( frameRequestStartedAt - cropOpenedAt ) . toFixed ( 2 ) ,
850+ ) ,
851+ reason,
852+ } ) ;
853+
854+ void commands
855+ . getDisplayFrameForCropping ( FPS )
856+ . then ( ( pngBytes ) => {
857+ if ( cancelled ) return ;
858+
859+ setPreviewBlob (
860+ new Blob ( [ new Uint8Array ( pngBytes ) ] , {
861+ type : "image/png" ,
862+ } ) ,
863+ "accurate-frame" ,
864+ ) ;
865+ logCropProfile ( "accurate-frame-request-finish" , {
866+ elapsedMs : Number (
867+ ( performance . now ( ) - cropOpenedAt ) . toFixed ( 2 ) ,
868+ ) ,
869+ requestMs : Number (
870+ ( performance . now ( ) - frameRequestStartedAt ) . toFixed (
871+ 2 ,
872+ ) ,
873+ ) ,
874+ reason,
875+ } ) ;
876+ } )
877+ . catch ( ( error : unknown ) => {
878+ if ( cancelled ) return ;
879+ console . warn ( "Display frame fetch failed:" , error ) ;
880+ logCropProfile ( "accurate-frame-request-failed" , {
881+ elapsedMs : Number (
882+ ( performance . now ( ) - cropOpenedAt ) . toFixed ( 2 ) ,
883+ ) ,
884+ requestMs : Number (
885+ ( performance . now ( ) - frameRequestStartedAt ) . toFixed (
886+ 2 ,
887+ ) ,
888+ ) ,
889+ message :
890+ error instanceof Error
891+ ? error . message
892+ : String ( error ) ,
893+ reason,
894+ } ) ;
784895 } ) ;
785- const url = URL . createObjectURL ( blob ) ;
786- setFrameBlobUrl ( url ) ;
787- } )
788- . catch ( ( error : unknown ) => {
789- console . warn ( "Display frame fetch failed:" , error ) ;
896+ } ;
897+
898+ const scheduleAccurateFrame = (
899+ reason : string ,
900+ options : {
901+ delayMs ?: number ;
902+ idleTimeoutMs : number ;
903+ fallbackDelayMs : number ;
904+ } ,
905+ ) => {
906+ const queueIdleFrame = ( ) => {
907+ const loadFrame = ( ) => requestAccurateFrame ( reason ) ;
908+
909+ if ( idleWindow . requestIdleCallback ) {
910+ frameLoadIdleId = idleWindow . requestIdleCallback (
911+ ( ) => {
912+ frameLoadIdleId = undefined ;
913+ loadFrame ( ) ;
914+ } ,
915+ {
916+ timeout : options . idleTimeoutMs ,
917+ } ,
918+ ) ;
919+ return ;
920+ }
921+
922+ frameLoadTimeoutId = globalThis . setTimeout ( ( ) => {
923+ frameLoadTimeoutId = undefined ;
924+ loadFrame ( ) ;
925+ } , options . fallbackDelayMs ) ;
926+ } ;
927+
928+ if ( ! options . delayMs ) {
929+ queueIdleFrame ( ) ;
930+ return ;
931+ }
932+
933+ frameLoadDelayTimeoutId = globalThis . setTimeout ( ( ) => {
934+ frameLoadDelayTimeoutId = undefined ;
935+ if ( cancelled || accurateFrameRequested ) return ;
936+ queueIdleFrame ( ) ;
937+ } , options . delayMs ) ;
938+ } ;
939+
940+ onMount ( ( ) => {
941+ logCropProfile ( "dialog-mounted" , {
942+ elapsedMs : Number (
943+ ( performance . now ( ) - cropOpenedAt ) . toFixed ( 2 ) ,
944+ ) ,
945+ recordingDurationSec : Math . round (
946+ editorInstance . recordingDuration ,
947+ ) ,
948+ } ) ;
949+
950+ scheduleAccurateFrame ( "immediate" , {
951+ idleTimeoutMs : 500 ,
952+ fallbackDelayMs : 16 ,
790953 } ) ;
954+ } ) ;
791955
792956 onCleanup ( ( ) => {
957+ cancelled = true ;
958+ clearScheduledAccurateFrame ( ) ;
793959 const url = frameBlobUrl ( ) ;
794960 if ( url ) {
795961 URL . revokeObjectURL ( url ) ;
@@ -959,12 +1125,33 @@ function Dialogs() {
9591125 < img
9601126 class = "shadow pointer-events-none max-h-[70vh]"
9611127 alt = "Current frame"
962- src = {
963- frameBlobUrl ( ) ??
964- convertFileSrc (
965- `${ editorInstance . path } /screenshots/display.jpg` ,
966- )
1128+ onError = { ( ) => {
1129+ const failedSource = frameSource ( ) ;
1130+ logCropProfile ( "preview-image-failed" , {
1131+ elapsedMs : Number (
1132+ ( performance . now ( ) - cropOpenedAt ) . toFixed (
1133+ 2 ,
1134+ ) ,
1135+ ) ,
1136+ source : failedSource ,
1137+ } ) ;
1138+ requestAccurateFrame (
1139+ failedSource === "screenshot"
1140+ ? "screenshot-load-failed"
1141+ : "preview-load-failed" ,
1142+ ) ;
1143+ } }
1144+ onLoad = { ( ) =>
1145+ logCropProfile ( "preview-image-loaded" , {
1146+ elapsedMs : Number (
1147+ ( performance . now ( ) - cropOpenedAt ) . toFixed (
1148+ 2 ,
1149+ ) ,
1150+ ) ,
1151+ source : frameSource ( ) ,
1152+ } )
9671153 }
1154+ src = { frameBlobUrl ( ) ?? screenshotSrc }
9681155 />
9691156 </ Cropper >
9701157 </ div >
0 commit comments