fix: Windows Playwright HTML rendering under Streamlit

Streamlit/Tornado switches the asyncio policy to WindowsSelectorEventLoopPolicy on Windows, which breaks Playwright subprocess creation. Fixed by using an explicit ProactorEventLoop on Windows, and tracking the event loop per browser instance to detect cross-loop reuse.
This commit is contained in:
jonathanzhan1975 authored and GitHub committed 2026-05-06 11:40:29 +08:00
1 parent 755fc3593e
commit af52cf674a
2 files changed
+42 -3

No files matched your search

+24 -3
View File
@@ -25,6 +25,7 @@ Linux Environment Requirements:
Playwright browser install: playwright install --with-deps chromium Playwright browser install: playwright install --with-deps chromium
""" """
import asyncio
import os import os
import re import re
import tempfile import tempfile
@@ -55,6 +56,7 @@ class HTMLFrameGenerator:
_browser = None _browser = None
_playwright = None _playwright = None
_browser_loop = None
def __init__(self, template_path: str): def __init__(self, template_path: str):
""" """
@@ -307,7 +309,22 @@ class HTMLFrameGenerator:
@classmethod @classmethod
async def _ensure_browser(cls): async def _ensure_browser(cls):
"""Lazily initialize a shared Playwright browser instance""" """Lazily initialize a shared Playwright browser instance"""
if cls._browser is None or not cls._browser.is_connected(): current_loop = asyncio.get_running_loop()
browser_usable = (
cls._browser is not None
and cls._browser_loop is current_loop
and cls._browser.is_connected()
)
if not browser_usable:
if cls._browser is not None and cls._browser_loop is not current_loop:
logger.warning(
"Detected cross-loop Playwright browser reuse attempt; "
"recreating browser for current event loop"
)
cls._browser = None
cls._playwright = None
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
cls._playwright = await async_playwright().start() cls._playwright = await async_playwright().start()
cls._browser = await cls._playwright.chromium.launch( cls._browser = await cls._playwright.chromium.launch(
@@ -318,6 +335,7 @@ class HTMLFrameGenerator:
'--disable-extensions', '--disable-extensions',
] ]
) )
cls._browser_loop = current_loop
logger.debug("Initialized Playwright Chromium browser") logger.debug("Initialized Playwright Chromium browser")
return cls._browser return cls._browser
@@ -327,6 +345,7 @@ class HTMLFrameGenerator:
if cls._browser: if cls._browser:
await cls._browser.close() await cls._browser.close()
cls._browser = None cls._browser = None
cls._browser_loop = None
if cls._playwright: if cls._playwright:
await cls._playwright.stop() await cls._playwright.stop()
cls._playwright = None cls._playwright = None
@@ -410,5 +429,7 @@ class HTMLFrameGenerator:
return output_path return output_path
except Exception as e: except Exception as e:
logger.error(f"Failed to render HTML template: {e}") logger.exception("Failed to render HTML template")
raise RuntimeError(f"HTML rendering failed: {e}") raise RuntimeError(
f"HTML rendering failed: {type(e).__name__}: {e}"
) from e
+18
View File
@@ -15,6 +15,7 @@ Async helper functions for web UI
""" """
import asyncio import asyncio
import sys
import tomllib import tomllib
from pathlib import Path from pathlib import Path
@@ -23,6 +24,23 @@ from loguru import logger
def run_async(coro): def run_async(coro):
"""Run async coroutine in sync context""" """Run async coroutine in sync context"""
if sys.platform == "win32":
# Streamlit/Tornado may switch the global asyncio policy to
# WindowsSelectorEventLoopPolicy, which breaks subprocess-based
# libraries such as Playwright on Windows. Use an explicit
# Proactor loop here so this sync bridge does not depend on the
# ambient global policy.
loop = asyncio.ProactorEventLoop()
try:
return loop.run_until_complete(coro)
finally:
try:
from pixelle_video.services.frame_html import HTMLFrameGenerator
loop.run_until_complete(HTMLFrameGenerator.close_browser())
except Exception as e:
logger.debug(f"Failed to cleanup HTML frame browser before loop close: {e}")
loop.close()
return asyncio.run(coro) return asyncio.run(coro)