channel-worker 2.5.30 → 2.5.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.30",
3
+ "version": "2.5.32",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -140,8 +140,20 @@ async function focusFbSearch(page, log) {
140
140
  if (box) {
141
141
  try {
142
142
  await box.click({ timeout: 2500 });
143
- await page.keyboard.down('Control'); await page.keyboard.press('A'); await page.keyboard.up('Control');
144
- await page.keyboard.press('Backspace');
143
+ // CLEAR the previous keyword first. FB's search is a React-controlled
144
+ // input — Ctrl+A+Backspace didn't reset its value (keywords appended:
145
+ // "kw1kw2"). locator.fill('') dispatches the input events React needs,
146
+ // reliably emptying it; verify + fall back to mass-Backspace.
147
+ await box.fill('').catch(() => {});
148
+ let cur = await box.evaluate(el => (el.value != null ? el.value : (el.textContent || ''))).catch(() => '');
149
+ if (cur && cur.trim()) {
150
+ await page.keyboard.press('End').catch(() => {});
151
+ for (let i = 0; i < cur.length + 8; i++) await page.keyboard.press('Backspace').catch(() => {});
152
+ cur = await box.evaluate(el => (el.value != null ? el.value : (el.textContent || ''))).catch(() => '');
153
+ }
154
+ if (cur && cur.trim()) { log('info', `[warmup-fb] search box not fully cleared (still "${cur.slice(0, 20)}") — typing anyway`); }
155
+ // Re-focus so the subsequent human-typing lands in the box.
156
+ await box.click({ timeout: 2000 }).catch(() => {});
145
157
  return true;
146
158
  } catch {}
147
159
  }
@@ -231,8 +243,25 @@ async function clickResultByHref(page, href) {
231
243
  }
232
244
  }
233
245
 
246
+ // Read the open reel/video's playback state — used to advance early when it
247
+ // ENDS or STALLS instead of idling until watch_time elapses.
248
+ async function readVideoState(page) {
249
+ return page.evaluate(() => {
250
+ const v = document.querySelector('video');
251
+ if (!v) return null;
252
+ if (v.paused && !v.ended) { try { v.play(); } catch {} }
253
+ return {
254
+ ended: !!v.ended,
255
+ currentTime: Number(v.currentTime) || 0,
256
+ duration: (isFinite(v.duration) && v.duration > 0) ? v.duration : 0,
257
+ loop: !!v.loop,
258
+ };
259
+ }).catch(() => null);
260
+ }
261
+
234
262
  // Watch whatever reel/video is currently open (after clicking a result): ensure
235
- // the <video> plays, watch a random span with a mid-view scroll. Best-effort.
263
+ // the <video> plays, watch a random span but ADVANCE EARLY if a non-looping
264
+ // video ends/stalls (don't sit on a finished video). Mid-view scroll. Best-effort.
236
265
  async function watchCurrent(page, minSec, maxSec, log) {
237
266
  try {
238
267
  await page.waitForTimeout(randInt(2500, 4500)); // let the reel viewer mount
@@ -244,12 +273,27 @@ async function watchCurrent(page, minSec, maxSec, log) {
244
273
  return true;
245
274
  }).catch(() => false);
246
275
  if (!hasVideo) { log('info', '[warmup-fb] no <video> after click — skipping'); return false; }
276
+
247
277
  const watchMs = randInt(minSec, maxSec) * 1000;
248
278
  log('info', `[warmup-fb] watching reel for ~${Math.round(watchMs / 1000)}s (url=${page.url().slice(-28)})`);
249
- const half = Math.floor(watchMs / 2);
250
- await page.waitForTimeout(half);
251
- await organicScroll(page, randInt(1, 2));
252
- await page.waitForTimeout(watchMs - half);
279
+ const TICK = 1000;
280
+ let watched = 0, scrolled = false, lastT = -1, stallTicks = 0;
281
+ while (watched < watchMs) {
282
+ const st = await readVideoState(page);
283
+ // A non-looping video that ended/stalled → stop waiting, go to next.
284
+ if (st && !st.loop) {
285
+ if (st.ended || (st.duration > 0 && st.currentTime >= st.duration - 0.6)) {
286
+ log('info', `[warmup-fb] reel ended at ${Math.round(st.currentTime)}s — advancing`);
287
+ break;
288
+ }
289
+ if (Math.abs(st.currentTime - lastT) < 0.15) { stallTicks++; if (stallTicks >= 6) { log('info', '[warmup-fb] reel stalled — advancing'); break; } }
290
+ else stallTicks = 0;
291
+ lastT = st.currentTime;
292
+ }
293
+ if (!scrolled && watched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
294
+ await page.waitForTimeout(TICK);
295
+ watched += TICK;
296
+ }
253
297
  return true;
254
298
  } catch (e) {
255
299
  log('info', `[warmup-fb] watch failed: ${String(e.message || e).slice(0, 90)}`);
@@ -145,6 +145,23 @@ async function isAdShowing(page) {
145
145
  }).catch(() => false);
146
146
  }
147
147
 
148
+ // Read the main <video> playback state. Used to advance early when a video
149
+ // ENDS or STALLS instead of sitting idle on a finished video until watch_time
150
+ // elapses (the "video chạy hết rồi không nhảy" report). Returns null if no video.
151
+ async function readVideoState(page) {
152
+ return page.evaluate(() => {
153
+ const v = document.querySelector('video');
154
+ if (!v) return null;
155
+ if (v.paused && !v.ended) { try { v.play(); } catch {} } // nudge autoplay
156
+ return {
157
+ ended: !!v.ended,
158
+ currentTime: Number(v.currentTime) || 0,
159
+ duration: (isFinite(v.duration) && v.duration > 0) ? v.duration : 0,
160
+ loop: !!v.loop,
161
+ };
162
+ }).catch(() => null);
163
+ }
164
+
148
165
  // Watch one video: navigate, SKIP any ads (so we watch the real video, not the
149
166
  // ad), then let the real content play for a random span — counting only
150
167
  // non-ad time toward the budget — with a small mid-view scroll like a real
@@ -168,6 +185,7 @@ async function watchVideo(page, url, minSec, maxSec, log) {
168
185
  let adWaited = 0; // ms spent sitting through/again skipping ads
169
186
  let scrolled = false;
170
187
  let skips = 0;
188
+ let lastT = -1, stallTicks = 0;
171
189
 
172
190
  while (realWatched < watchMs) {
173
191
  const adOn = await isAdShowing(page);
@@ -177,6 +195,18 @@ async function watchVideo(page, url, minSec, maxSec, log) {
177
195
  if (adWaited >= MAX_AD_WAIT_MS) { log('warn', `[warmup] ${url.slice(-11)} ads exceeded ${MAX_AD_WAIT_MS/1000}s — moving on`); break; }
178
196
  } else {
179
197
  realWatched += TICK;
198
+ // Advance early if the (non-looping) video ENDED or STALLED, instead of
199
+ // idling on a finished video until watch_time runs out.
200
+ const st = await readVideoState(page);
201
+ if (st && !st.loop) {
202
+ if (st.ended || (st.duration > 0 && st.currentTime >= st.duration - 0.6)) {
203
+ log('info', `[warmup] ${url.slice(-11)} ended at ${Math.round(st.currentTime)}s — advancing`);
204
+ break;
205
+ }
206
+ if (Math.abs(st.currentTime - lastT) < 0.15) { stallTicks++; if (stallTicks >= 6) { log('info', `[warmup] ${url.slice(-11)} stalled — advancing`); break; } }
207
+ else stallTicks = 0;
208
+ lastT = st.currentTime;
209
+ }
180
210
  // Mid-view scroll once, roughly halfway through real watch time.
181
211
  if (!scrolled && realWatched >= watchMs / 2) { await organicScroll(page, randInt(1, 2)); scrolled = true; }
182
212
  }