@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from "vitest";
22import { Hono } from "hono" ;
33import { existsSync , mkdirSync , mkdtempSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
44import { tmpdir } from "node:os" ;
5- import { dirname , join } from "node:path" ;
5+ import { basename , dirname , join } from "node:path" ;
66import { registerFreezeFrameRoutes , type FrameExtractor } from "./freezeFrame" ;
77import { fileContentVersion } from "../helpers/fileVersion" ;
88import type { StudioApiAdapter } from "../types" ;
@@ -16,7 +16,11 @@ const html = `<div data-composition-id="main" data-start="0" data-duration="6">
1616<video id="talk" class="clip" src="media/talk.mp4" data-start="0" data-duration="6" data-track-index="0"></video>
1717</div>` ;
1818
19- function setup ( extract : FrameExtractor , file = { path : "index.html" , html } ) {
19+ function setup (
20+ extract : FrameExtractor ,
21+ file = { path : "index.html" , html } ,
22+ stillToken ?: ( ) => string ,
23+ ) {
2024 const dir = mkdtempSync ( join ( tmpdir ( ) , "hf-freeze-" ) ) ;
2125 tempDirs . push ( dir ) ;
2226 mkdirSync ( dirname ( join ( dir , file . path ) ) , { recursive : true } ) ;
@@ -31,7 +35,7 @@ function setup(extract: FrameExtractor, file = { path: "index.html", html }) {
3135 startRender : ( ) => ( { id : "j" , status : "rendering" , progress : 0 , outputPath : "/tmp/o.mp4" } ) ,
3236 } ;
3337 const app = new Hono ( ) ;
34- registerFreezeFrameRoutes ( app , adapter , extract ) ;
38+ registerFreezeFrameRoutes ( app , adapter , extract , stillToken ) ;
3539 const post = ( body : unknown ) =>
3640 app . request ( "http://localhost/projects/demo/file-mutations/freeze-frame" , {
3741 method : "POST" ,
@@ -56,17 +60,20 @@ describe("freeze-frame route", () => {
5660 } ) ;
5761 const body : { before ?: string ; after ?: string ; imageSrc ?: string } = await res . json ( ) ;
5862 expect ( res . status ) . toBe ( 200 ) ;
63+ const output = calls [ 0 ] ?. at ( - 1 ) ?? "" ;
5964 expect ( calls [ 0 ] ) . toEqual ( [
60- "-y " ,
65+ "-n " ,
6166 "-ss" ,
6267 "2.5" ,
6368 "-i" ,
6469 join ( dir , "media/talk.mp4" ) ,
6570 "-frames:v" ,
6671 "1" ,
67- join ( dir , "assets/freeze/talk-2500.png" ) ,
72+ output ,
6873 ] ) ;
69- expect ( body . imageSrc ) . toBe ( "assets/freeze/talk-2500.png" ) ;
74+ expect ( dirname ( output ) ) . toBe ( join ( dir , "assets/freeze" ) ) ;
75+ expect ( basename ( output ) ) . toMatch ( / ^ t a l k - [ 0 - 9 a - f ] { 10 } - 2 5 0 0 - [ 0 - 9 a - f ] { 8 } \. p n g $ / ) ;
76+ expect ( body . imageSrc ) . toBe ( `assets/freeze/${ basename ( output ) } ` ) ;
7077 expect ( body . before ) . toBe ( html ) ;
7178 expect ( readFileSync ( join ( dir , "index.html" ) , "utf-8" ) ) . toBe ( body . after ) ;
7279 expect ( body . after ) . toContain ( 'id="talk-freeze"' ) ;
@@ -118,11 +125,102 @@ describe("freeze-frame route", () => {
118125 } ) ;
119126 const body : { imageSrc ?: string ; after ?: string } = await res . json ( ) ;
120127 expect ( res . status ) . toBe ( 200 ) ;
121- const output = calls [ 0 ] ?. at ( - 1 ) ;
122- expect ( output ) . toBe ( join ( dir , "assets/freeze/ ____________outside_frame-2500.png" ) ) ;
123- expect ( dirname ( output ?? "" ) ) . toBe ( join ( dir , "assets/freeze" ) ) ;
124- expect ( body . imageSrc ) . toBe ( " ../assets/freeze/____________outside_frame-2500.png" ) ;
125- expect ( body . after ) . toContain ( ' src="../assets/freeze/____________outside_frame-2500.png"' ) ;
128+ const output = calls [ 0 ] ?. at ( - 1 ) ?? "" ;
129+ expect ( basename ( output ) ) . toMatch ( / ^ _ _ _ _ _ _ _ _ _ _ _ _ o u t s i d e _ f r a m e - [ 0 - 9 a - f ] { 10 } - 2 5 0 0 - / ) ;
130+ expect ( dirname ( output ) ) . toBe ( join ( dir , "assets/freeze" ) ) ;
131+ expect ( body . imageSrc ) . toBe ( ` ../assets/freeze/${ basename ( output ) } ` ) ;
132+ expect ( body . after ) . toContain ( ` src="../assets/freeze/${ basename ( output ) } "` ) ;
126133 expect ( existsSync ( join ( dir , ".." , "outside" ) ) ) . toBe ( false ) ;
127134 } ) ;
135+
136+ describe ( "still identity" , ( ) => {
137+ const twoVideos = (
138+ a : string ,
139+ b : string ,
140+ ) => `<div data-composition-id="main" data-start="0" data-duration="6">
141+ <video id="${ a } " class="clip" src="media/a.mp4" data-start="0" data-duration="6" data-track-index="0"></video>
142+ <video id="${ b } " class="clip" src="media/b.mp4" data-start="0" data-duration="6" data-track-index="1"></video>
143+ </div>` ;
144+
145+ function writingExtractor ( outputs : string [ ] ) : FrameExtractor {
146+ return async ( args ) => {
147+ const output = args . at ( - 1 ) ?? "" ;
148+ outputs . push ( output ) ;
149+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) ) ;
150+ try {
151+ writeFileSync ( output , `frame of ${ args [ 4 ] } #${ outputs . length } ` , {
152+ flag : args . includes ( "-y" ) ? "w" : "wx" ,
153+ } ) ;
154+ return { ok : true } ;
155+ } catch ( error ) {
156+ return { ok : false , error : String ( error ) } ;
157+ }
158+ } ;
159+ }
160+
161+ function freeze ( post : ( body : unknown ) => Promise < Response > , dir : string , id : string ) {
162+ return post ( {
163+ path : "index.html" ,
164+ expectedVersion : fileContentVersion ( readFileSync ( join ( dir , "index.html" ) , "utf-8" ) ) ,
165+ target : { id } ,
166+ playhead : 2.5 ,
167+ } ) ;
168+ }
169+
170+ it ( "gives two ids that sanitise alike distinct stills and keeps the first one's pixels" , async ( ) => {
171+ const outputs : string [ ] = [ ] ;
172+ const source = twoVideos ( "a.b" , "a_b" ) ;
173+ const { dir, post } = setup ( writingExtractor ( outputs ) , { path : "index.html" , html : source } ) ;
174+ expect ( ( await freeze ( post , dir , "a.b" ) ) . status ) . toBe ( 200 ) ;
175+ const first = readFileSync ( outputs [ 0 ] ?? "" , "utf-8" ) ;
176+ expect ( ( await freeze ( post , dir , "a_b" ) ) . status ) . toBe ( 200 ) ;
177+ expect ( outputs [ 1 ] ) . not . toBe ( outputs [ 0 ] ) ;
178+ expect ( readFileSync ( outputs [ 0 ] ?? "" , "utf-8" ) ) . toBe ( first ) ;
179+ } ) ;
180+
181+ it ( "keeps ids sharing an 80-character prefix apart" , async ( ) => {
182+ const outputs : string [ ] = [ ] ;
183+ const prefix = "v" . repeat ( 80 ) ;
184+ const source = twoVideos ( `${ prefix } 1` , `${ prefix } 2` ) ;
185+ const { dir, post } = setup ( writingExtractor ( outputs ) , { path : "index.html" , html : source } ) ;
186+ expect ( ( await freeze ( post , dir , `${ prefix } 1` ) ) . status ) . toBe ( 200 ) ;
187+ expect ( ( await freeze ( post , dir , `${ prefix } 2` ) ) . status ) . toBe ( 200 ) ;
188+ expect ( new Set ( outputs ) . size ) . toBe ( 2 ) ;
189+ } ) ;
190+
191+ it ( "writes a new still when the same clip is frozen again at the same time" , async ( ) => {
192+ const outputs : string [ ] = [ ] ;
193+ const { dir, post } = setup ( writingExtractor ( outputs ) ) ;
194+ expect ( ( await freeze ( post , dir , "talk" ) ) . status ) . toBe ( 200 ) ;
195+ const first = readFileSync ( outputs [ 0 ] ?? "" , "utf-8" ) ;
196+ writeFileSync ( join ( dir , "index.html" ) , html ) ;
197+ expect ( ( await freeze ( post , dir , "talk" ) ) . status ) . toBe ( 200 ) ;
198+ expect ( outputs [ 1 ] ) . not . toBe ( outputs [ 0 ] ) ;
199+ expect ( readFileSync ( outputs [ 0 ] ?? "" , "utf-8" ) ) . toBe ( first ) ;
200+ } ) ;
201+
202+ it ( "gives concurrent requests distinct stills" , async ( ) => {
203+ const outputs : string [ ] = [ ] ;
204+ const { dir, post } = setup ( writingExtractor ( outputs ) ) ;
205+ const results = await Promise . all ( [ freeze ( post , dir , "talk" ) , freeze ( post , dir , "talk" ) ] ) ;
206+ expect ( results . map ( ( res ) => res . status ) . sort ( ) ) . toEqual ( [ 200 , 409 ] ) ;
207+ expect ( new Set ( outputs ) . size ) . toBe ( 2 ) ;
208+ expect ( outputs . map ( ( output ) => readFileSync ( output , "utf-8" ) ) ) . toEqual ( [
209+ expect . stringContaining ( "#" ) ,
210+ expect . stringContaining ( "#" ) ,
211+ ] ) ;
212+ } ) ;
213+
214+ it ( "refuses, without extracting, when the still's name is already taken" , async ( ) => {
215+ const outputs : string [ ] = [ ] ;
216+ const { dir, post } = setup ( writingExtractor ( outputs ) , undefined , ( ) => "fixed" ) ;
217+ expect ( ( await freeze ( post , dir , "talk" ) ) . status ) . toBe ( 200 ) ;
218+ const first = readFileSync ( outputs [ 0 ] ?? "" , "utf-8" ) ;
219+ writeFileSync ( join ( dir , "index.html" ) , html ) ;
220+ expect ( ( await freeze ( post , dir , "talk" ) ) . status ) . toBe ( 409 ) ;
221+ expect ( outputs ) . toHaveLength ( 1 ) ;
222+ expect ( readFileSync ( outputs [ 0 ] ?? "" , "utf-8" ) ) . toBe ( first ) ;
223+ expect ( readFileSync ( join ( dir , "index.html" ) , "utf-8" ) ) . toBe ( html ) ;
224+ } ) ;
225+ } ) ;
128226} ) ;
0 commit comments