Add full-image lightbox and restore WLED state on stop
- Add GET /picture-streams/full-image endpoint to serve full-res images - Click static image preview thumbnail to open full-res lightbox - Snapshot WLED state (on/off, lor, AudioReactive) before streaming - Restore saved WLED state when streaming stops Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@ from typing import List, Dict, Any
|
||||
import httpx
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from fastapi.responses import Response
|
||||
|
||||
from wled_controller import __version__
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
@@ -1612,6 +1613,38 @@ async def validate_image(
|
||||
return ImageValidateResponse(valid=False, error=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/picture-streams/full-image", tags=["Picture Streams"])
|
||||
async def get_full_image(
|
||||
_auth: AuthRequired,
|
||||
source: str = Query(..., description="Image URL or local file path"),
|
||||
):
|
||||
"""Serve the full-resolution image for lightbox preview."""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
if source.startswith(("http://", "https://")):
|
||||
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
|
||||
response = await client.get(source)
|
||||
response.raise_for_status()
|
||||
pil_image = Image.open(io.BytesIO(response.content))
|
||||
else:
|
||||
path = Path(source)
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
pil_image = Image.open(path)
|
||||
|
||||
pil_image = pil_image.convert("RGB")
|
||||
buf = io.BytesIO()
|
||||
pil_image.save(buf, format="JPEG", quality=90)
|
||||
buf.seek(0)
|
||||
return Response(content=buf.getvalue(), media_type="image/jpeg")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/v1/picture-streams", response_model=PictureStreamResponse, tags=["Picture Streams"], status_code=201)
|
||||
async def create_picture_stream(
|
||||
data: PictureStreamCreate,
|
||||
|
||||
@@ -120,6 +120,8 @@ class ProcessorState:
|
||||
# Capture libraries (BetterCam, MSS, DXcam) use thread-local state,
|
||||
# so all calls must run on the same thread.
|
||||
capture_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
||||
# WLED state snapshot taken before streaming starts (to restore on stop)
|
||||
wled_state_before: Optional[dict] = None
|
||||
|
||||
|
||||
class ProcessorManager:
|
||||
@@ -401,6 +403,24 @@ class ProcessorManager:
|
||||
# Resolve stream settings
|
||||
self._resolve_stream_settings(state)
|
||||
|
||||
# Snapshot WLED state before streaming changes it
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as http:
|
||||
resp = await http.get(f"{state.device_url}/json/state")
|
||||
resp.raise_for_status()
|
||||
wled_state = resp.json()
|
||||
state.wled_state_before = {
|
||||
"on": wled_state.get("on", True),
|
||||
"lor": wled_state.get("lor", 0),
|
||||
}
|
||||
# AudioReactive is optional (usermod)
|
||||
if "AudioReactive" in wled_state:
|
||||
state.wled_state_before["AudioReactive"] = wled_state["AudioReactive"]
|
||||
logger.info(f"Saved WLED state before streaming: {state.wled_state_before}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not snapshot WLED state: {e}")
|
||||
state.wled_state_before = None
|
||||
|
||||
# Connect to WLED device
|
||||
try:
|
||||
use_ddp = state.led_count > 500
|
||||
@@ -488,6 +508,19 @@ class ProcessorManager:
|
||||
pass
|
||||
state.task = None
|
||||
|
||||
# Restore WLED state that was changed when streaming started
|
||||
if state.wled_state_before:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as http:
|
||||
await http.post(
|
||||
f"{state.device_url}/json/state",
|
||||
json=state.wled_state_before,
|
||||
)
|
||||
logger.info(f"Restored WLED state: {state.wled_state_before}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not restore WLED state: {e}")
|
||||
state.wled_state_before = None
|
||||
|
||||
# Close WLED connection
|
||||
if state.wled_client:
|
||||
await state.wled_client.close()
|
||||
|
||||
@@ -81,11 +81,28 @@ function closeLightbox(event) {
|
||||
if (event && event.target && event.target.closest('.lightbox-content')) return;
|
||||
const lightbox = document.getElementById('image-lightbox');
|
||||
lightbox.classList.remove('active');
|
||||
document.getElementById('lightbox-image').src = '';
|
||||
const img = document.getElementById('lightbox-image');
|
||||
// Revoke blob URL if one was used
|
||||
if (img.src.startsWith('blob:')) URL.revokeObjectURL(img.src);
|
||||
img.src = '';
|
||||
document.getElementById('lightbox-stats').style.display = 'none';
|
||||
unlockBody();
|
||||
}
|
||||
|
||||
async function openFullImageLightbox(imageSource) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/picture-streams/full-image?source=${encodeURIComponent(imageSource)}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const blob = await resp.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
openLightbox(blobUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to load full image:', err);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
// Close in order: overlay lightboxes first, then modals
|
||||
@@ -3627,6 +3644,8 @@ async function validateStaticImage() {
|
||||
_lastValidatedImageSource = source;
|
||||
if (data.valid) {
|
||||
previewImg.src = data.preview;
|
||||
previewImg.style.cursor = 'pointer';
|
||||
previewImg.onclick = () => openFullImageLightbox(source);
|
||||
infoEl.textContent = `${data.width} × ${data.height} px`;
|
||||
previewContainer.style.display = '';
|
||||
statusEl.textContent = t('streams.validate_image.valid');
|
||||
|
||||
Reference in New Issue
Block a user