From fa8aacd59dc0a8981d5b9f68bda78bceab0636b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Wed, 10 Dec 2025 17:36:12 +0100 Subject: [PATCH] Fix playback --- src/main/resources/static/css/styles.css | 121 +++++++++++--------- src/main/resources/static/js/broadcast.js | 50 +++++--- src/main/resources/templates/dashboard.html | 52 +++++---- src/main/resources/templates/index.html | 81 +++---------- 4 files changed, 150 insertions(+), 154 deletions(-) diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index dfdcc75..4143528 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -66,13 +66,18 @@ body { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 28px; align-items: center; - background: rgba(15, 23, 42, 0.8); + background: rgba(15, 23, 42, 0.85); border: 1px solid #1f2937; padding: 28px; border-radius: 16px; box-shadow: 0 25px 60px rgba(0, 0, 0, 0.45); } +.hero-compact { + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + padding: 24px; +} + .hero-text h1 { font-size: 32px; line-height: 1.2; @@ -84,6 +89,34 @@ body { line-height: 1.6; } +.pill-list { + list-style: none; + padding: 0; + margin: 16px 0 0; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.pill-list.minimal { + margin-top: 12px; +} + +.pill-list li { + background: rgba(124, 58, 237, 0.12); + border: 1px solid rgba(124, 58, 237, 0.2); + color: #e9d5ff; + padding: 8px 12px; + border-radius: 999px; + font-weight: 600; + font-size: 14px; +} + +.pill-list.minimal li { + background: rgba(124, 58, 237, 0.08); + border-color: rgba(124, 58, 237, 0.18); +} + .eyebrow { text-transform: uppercase; letter-spacing: 1px; @@ -124,6 +157,13 @@ body { width: 100%; } +.block { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + .button.ghost { background: transparent; border: 1px solid #2d3a57; @@ -140,29 +180,6 @@ body { background: #475569; } -.stats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 12px; - margin-top: 18px; -} - -.stat { - padding: 12px; - background: #0b1220; - border-radius: 10px; - border: 1px solid #1f2937; -} - -.stat-value { - font-weight: 700; -} - -.stat-label { - color: #94a3b8; - font-size: 13px; -} - .hero-panel { background: #0b1220; border: 1px solid #1f2937; @@ -210,46 +227,34 @@ body { gap: 6px; } -.feature-list { - list-style: none; - padding: 0; - margin: 14px 0 18px; +.preview-summary { + margin: 10px 0 18px; +} + +.panel-actions { display: flex; flex-direction: column; + gap: 10px; +} + +.preview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } -.feature-title { - font-weight: 700; -} - -.feature-desc { - color: #94a3b8; - margin-top: 4px; -} - -.info-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 16px; - margin: 28px 0; -} - -.info-card { - background: #0b1220; +.preview-card { + background: rgba(255, 255, 255, 0.02); border: 1px solid #1f2937; - padding: 16px; border-radius: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); -} - -.info-card h4 { - margin: 10px 0 6px; -} - -.info-card p { + padding: 12px; color: #cbd5e1; - line-height: 1.5; +} + +.preview-card p { + margin: 8px 0 0; + color: #cbd5e1; + line-height: 1.4; } .landing-footer { @@ -262,6 +267,10 @@ body { border-radius: 12px; } +.landing-footer.compact { + margin-top: 18px; +} + .admin-layout { min-height: 100vh; display: flex; diff --git a/src/main/resources/static/js/broadcast.js b/src/main/resources/static/js/broadcast.js index fe8c5d2..8888afb 100644 --- a/src/main/resources/static/js/broadcast.js +++ b/src/main/resources/static/js/broadcast.js @@ -522,13 +522,16 @@ function ensureMedia(asset) { element.crossOrigin = 'anonymous'; if (isVideoElement(element)) { element.loop = true; - applyMediaVolume(element, asset); element.playsInline = true; element.autoplay = true; + element.controls = false; element.onloadeddata = draw; element.onloadedmetadata = () => recordDuration(asset.id, element.duration); element.preload = 'auto'; element.addEventListener('error', () => clearMedia(asset.id)); + const volume = applyMediaVolume(element, asset); + element.defaultMuted = volume === 0; + element.muted = element.defaultMuted; setVideoSource(element, asset); } else { element.onload = draw; @@ -625,13 +628,7 @@ function setVideoSource(element, asset) { function applyVideoSource(element, objectUrl, asset) { element.src = objectUrl; - const playback = asset.speed ?? 1; - element.playbackRate = Math.max(playback, 0.01); - if (playback === 0) { - element.pause(); - } else { - element.play().catch(() => queueAudioForUnlock({ element })); - } + startVideoPlayback(element, asset); } function getCachedSource(element) { @@ -680,20 +677,43 @@ function applyMediaSettings(element, asset) { if (!isVideoElement(element)) { return; } + startVideoPlayback(element, asset); +} + +function startVideoPlayback(element, asset) { const nextSpeed = asset.speed ?? 1; const effectiveSpeed = Math.max(nextSpeed, 0.01); if (element.playbackRate !== effectiveSpeed) { element.playbackRate = effectiveSpeed; } - applyMediaVolume(element, asset); - if (nextSpeed === 0) { + const volume = applyMediaVolume(element, asset); + element.defaultMuted = volume === 0; + element.muted = element.defaultMuted; + + if (effectiveSpeed === 0) { element.pause(); - } else { - const playPromise = element.play(); - if (playPromise?.catch) { - playPromise.catch(() => queueAudioForUnlock({ element })); - } + return; } + + const attemptPlay = (allowFallback) => { + const playPromise = element.play(); + if (playPromise?.then) { + playPromise.then(() => { + if (volume > 0) { + element.muted = false; + } + }).catch(() => { + if (!allowFallback) { + queueAudioForUnlock({ element }); + return; + } + element.muted = true; + element.play().catch(() => queueAudioForUnlock({ element })); + }); + } + }; + + attemptPlay(true); } function startRenderLoop() { diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index ada9a19..b40ff69 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -7,22 +7,34 @@
-
-
-

Logged in

-

Welcome back, user

-

Control your channel overlay, invite trusted teammates, and get to broadcasting faster.

-
- Primary channel: channel - Secure dashboard -
+
+
+

Dashboard

+

Hello, user

+

Manage overlay assets, keep your canvas sized correctly, and hand off control to trusted mods.

+
    +
  • Channel: channel
  • +
  • Secure dashboard
  • +
-
- Open broadcast overlay - Open admin console -
- -
+
+
+
+

Quick actions

+

Jump into your overlay

+
+ Live +
+
+

Open your broadcast view or delegate control without leaving this page.

+
+
@@ -30,9 +42,9 @@
-

Canvas setup

-

Overlay dimensions

-

Match the canvas size to your streaming output so overlays sit exactly where you expect.

+

Canvas

+

Overlay size

+

Match your OBS output so overlays land where you expect.

Visible to admins
@@ -57,7 +69,7 @@

Collaboration

Channel admins

-

Invite moderators or teammates to manage overlay assets without sharing your login.

+

Invite moderators to help manage assets.

@@ -73,7 +85,7 @@

Your access

Channels you administer

-

Hop into a teammate's overlay console with the right permissions.

+

Jump into a teammate's overlay console.

No admin invitations yet.

diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 4ef51b4..f540c0f 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -18,86 +18,41 @@ Login with Twitch
-
+
-

Overlay toolkit for busy streamers

-

Design, schedule, and control your Twitch overlays with ease.

-

Imgfloat keeps your branding consistent while letting mods and trusted admins help run the show. Upload assets, schedule when they appear, and keep everything in sync across your scenes.

+

Overlay toolkit

+

Keep your Twitch overlays tidy.

+

Upload artwork, drop it into a shared dashboard, and stay in sync with your mods.

Login with Twitch - No bots to invite. Connect securely with Twitch OAuth. -
-
-
-
Live
-
Status preview for your overlay
-
-
-
Admins
-
Invite trusted helpers in seconds
-
-
-
Assets
-
Manage images & animations from one place
-
+ Secure OAuth login. No bots needed.
+
    +
  • Instant overlay updates
  • +
  • Shared access for teammates
  • +
-
+
-

Preview

-

Overlay control center

+

Ready to go live

+

Preview & publish quickly

Secure
-
    -
  • -
    -
    Live dashboard
    -
    Monitor your broadcast overlay and see changes instantly.
    -
    -
  • -
  • -
    -
    Invite admins
    -
    Share access with team members without sharing credentials.
    -
    -
  • -
  • -
    -
    Asset library
    -
    Upload, tag, and reuse assets across events and layouts.
    -
    -
  • -
- Get started +
+

Spot check your canvas, push assets live, and keep everything aligned with your stream.

+
+ Open dashboard
-
-
-
Step 1
-

Connect your Twitch account

-

Authenticate once with Twitch OAuth to unlock the dashboard and sync your channel context. No complex setup required.

-
-
-
Step 2
-

Upload overlays & widgets

-

Add images, animations, or sponsor placements. Imgfloat keeps them organized and ready for your next broadcast.

-
-
-
Step 3
-

Invite trusted admins

-

Share control with moderators so someone always has the controls, even while you focus on the stream.

-
-
- -