@@ -175,11 +175,12 @@ const cdpCall = async (socketBuf, id, method, params) => {
175175 }
176176} ;
177177
178- const cdpEval = async ( socketBuf , id , expression , { awaitPromise = false } = { } ) => {
178+ const cdpEval = async ( socketBuf , id , expression , { awaitPromise = false , contextId = undefined } = { } ) => {
179179 const result = await cdpCall ( socketBuf , id , 'Runtime.evaluate' , {
180180 expression,
181181 returnByValue : true ,
182- awaitPromise
182+ awaitPromise,
183+ ...( contextId === undefined ? { } : { contextId } )
183184 } ) ;
184185 return result ?. result ?. value ;
185186} ;
@@ -208,6 +209,43 @@ const findTarget = async (predicate, label) => {
208209 throw new Error ( `Timed out waiting for target: ${ label } ` ) ;
209210} ;
210211
212+ const flattenFrameTree = ( frameTree ) => {
213+ const out = [ frameTree ] ;
214+ for ( const child of frameTree . childFrames ?? [ ] ) {
215+ out . push ( ...flattenFrameTree ( child ) ) ;
216+ }
217+ return out ;
218+ } ;
219+
220+ const connectToTargetWithFrameContext = async ( targetPredicate , framePredicate , targetLabel , frameLabel ) => {
221+ const start = Date . now ( ) ;
222+ while ( Date . now ( ) - start < TIMEOUT_MS ) {
223+ const targets = await httpGetJson ( '/json/list' ) ;
224+ for ( const target of targets . filter ( targetPredicate ) ) {
225+ let socketBuf ;
226+ try {
227+ socketBuf = await connectToTarget ( target ) ;
228+ const tree = await cdpCall ( socketBuf , 101 , 'Page.getFrameTree' , { } ) ;
229+ const frameNode = flattenFrameTree ( tree . frameTree ) . find ( ( node ) => framePredicate ( node . frame ) ) ;
230+ if ( ! frameNode ) {
231+ socketBuf . socket . end ( ) ;
232+ continue ;
233+ }
234+ const world = await cdpCall ( socketBuf , 102 , 'Page.createIsolatedWorld' , {
235+ frameId : frameNode . frame . id ,
236+ worldName : 'ytcf-verify' ,
237+ grantUniveralAccess : true
238+ } ) ;
239+ return { socketBuf, contextId : world . executionContextId , target } ;
240+ } catch {
241+ socketBuf ?. socket . destroy ( ) ;
242+ }
243+ }
244+ await sleep ( 250 ) ;
245+ }
246+ throw new Error ( `Timed out waiting for ${ targetLabel } with ${ frameLabel } ` ) ;
247+ } ;
248+
211249const clickByText = async ( socketBuf , id , text ) => {
212250 const expr = `
213251 (() => {
@@ -232,6 +270,16 @@ const waitForSelector = async (socketBuf, id, selector) => {
232270 throw new Error ( `Timed out waiting for selector: ${ selector } ` ) ;
233271} ;
234272
273+ const waitForEval = async ( socketBuf , id , expression , label , options = { } ) => {
274+ const start = Date . now ( ) ;
275+ while ( Date . now ( ) - start < TIMEOUT_MS ) {
276+ const ok = await cdpEval ( socketBuf , id , expression , options ) ;
277+ if ( ok ) return true ;
278+ await sleep ( 250 ) ;
279+ }
280+ throw new Error ( `Timed out waiting for: ${ label } ` ) ;
281+ } ;
282+
235283const main = async ( ) => {
236284 const swTarget = await findTarget (
237285 ( t ) => t . type === 'service_worker' && typeof t . url === 'string' && t . url . includes ( 'chat-background' ) ,
@@ -281,22 +329,24 @@ const main = async () => {
281329 sw . socket . end ( ) ;
282330 console . log ( 'storage snapshot:' , JSON . stringify ( storageSnapshot ) ) ;
283331
284- const liveChatTarget = await findTarget (
285- ( t ) => typeof t . url === 'string' && t . url . includes ( '/live_chat' ) && t . url . includes ( 'jfKfPfyJRdk' ) ,
286- 'live_chat popout'
332+ const { socketBuf : live , contextId : liveContextId } = await connectToTargetWithFrameContext (
333+ ( t ) => t . type === 'page' && typeof t . url === 'string' && t . url . includes ( '/watch' ) && t . url . includes ( 'X4VbdwhkE10' ) ,
334+ ( frame ) => frame . name === 'chatframe' && frame . url . includes ( '/live_chat' ) && frame . url . includes ( 'continuation=' ) ,
335+ 'youtube watch page' ,
336+ 'embedded live chat frame'
287337 ) ;
288338
289- const live = await connectToTarget ( liveChatTarget ) ;
290-
291339 // Injected bar exists.
292- const hasButtons = await cdpEval ( live , 1 , "!!document.querySelector('.ytcf-launch-button')" ) ;
293- const hasMountContainer = await cdpEval ( live , 2 , "!!document.querySelector('.ytcf-iframe')" ) ;
294- if ( ! hasButtons || ! hasMountContainer ) {
295- throw new Error ( 'Injected YTCF controls not present on live_chat page' ) ;
296- }
340+ await waitForEval (
341+ live ,
342+ 1 ,
343+ "!!document.querySelector('.ytcf-launch-button') && !!document.querySelector('.ytcf-iframe')" ,
344+ 'injected YTCF controls on embedded live_chat frame' ,
345+ { contextId : liveContextId }
346+ ) ;
297347
298348 // Use "Popout" so the embed surface becomes a top-level CDP target.
299- await cdpEval ( live , 3 , "document.querySelector('.ytcf-popout-button')?.click(); true" ) ;
349+ await cdpEval ( live , 3 , "document.querySelector('.ytcf-popout-button')?.click(); true" , { contextId : liveContextId } ) ;
300350
301351 // First run should redirect the embed to setup.html.
302352 const setupTarget = await findTarget (
@@ -331,7 +381,7 @@ const main = async () => {
331381 ytEmbed . socket . end ( ) ;
332382
333383 // Open Settings from injected bar.
334- await cdpEval ( live , 30 , "document.querySelector('.ytcf-settings-button')?.click(); true" ) ;
384+ await cdpEval ( live , 30 , "document.querySelector('.ytcf-settings-button')?.click(); true" , { contextId : liveContextId } ) ;
335385 const optionsTarget = await findTarget (
336386 ( t ) => typeof t . url === 'string' && t . url . includes ( 'options.html' ) ,
337387 'options.html'
0 commit comments