diff --git a/frontend/app.js b/frontend/app.js
index e45e50a..2d16bac 100755
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -4892,6 +4892,7 @@ const Remote = (() => {
Lyrics-only display that follows this screen. On the projector's computer or TV browser open
${escapeHtml(location.origin)}/?present=${host.code} (same code), or scan the QR.
đź–Ą Open presenter window here
+ Copy OBS browser-source URL
Connected phones & screens
@@ -4927,6 +4928,7 @@ const Remote = (() => {
else if (act.dataset.act === 'stop') stopHosting();
else if (act.dataset.act === 'code') hostSend({ type: 'new-code' });
else if (act.dataset.act === 'unpair') unpairAll();
+ else if (act.dataset.act === 'obs') { navigator.clipboard.writeText(LowerThird.url(location.origin, host.code)).then(() => toast('OBS URL copied'), () => toast('Copy blocked')); }
else if (act.dataset.act === 'present') {
// A second window on this computer (drag it to the projector display).
window.open(`/?present=${host.code}`, 'ytp-presenter', 'popup,width=1280,height=720');
@@ -5944,7 +5946,8 @@ const Presenter = (() => {
const noop = { boot() {}, active: () => false };
const view = $('presenterView');
if (!WEB || !view || typeof WebSocket === 'undefined') return noop;
- const KEY = 'ytpPresenterPair';
+ const overlay = LowerThird.active();
+ const KEY = overlay ? `ytpOverlayPair:${new URLSearchParams(location.search).get('code') || ''}` : 'ytpPresenterPair';
const load = () => { try { return JSON.parse(localStorage.getItem(KEY) || 'null'); } catch { return null; } };
const save = (v) => { try { if (v) localStorage.setItem(KEY, JSON.stringify(v)); else localStorage.removeItem(KEY); } catch { /* blocked */ } };
const wsUrl = (q) => `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/remote?${new URLSearchParams(q)}`;
@@ -6001,7 +6004,7 @@ const Presenter = (() => {
let m; try { m = JSON.parse(e.data); } catch { return; }
if (m.type === 'hello') { st.hostName = m.hostName || 'screen'; if (m.state) onState(m.state); else setStatus(`Following ${st.hostName}`); }
else if (m.type === 'state') onState(m.state);
- else if (m.type === 'host-offline') setStatus(`${st.hostName || 'The screen'} is offline — waiting…`);
+ else if (m.type === 'host-offline') { if (overlay) { st.all = []; LowerThird.paint([], -1); } setStatus(`${st.hostName || 'The screen'} is offline — waiting…`); }
};
ws.onclose = (e) => {
if (st.ws !== ws) return;
@@ -6044,6 +6047,7 @@ const Presenter = (() => {
}
function render(v) {
+ if (overlay) { LowerThird.paint([], -1); st.idx = -2; tick(true); return; }
const sung = st.all.map((l, i) => ({ ...l, i })).filter((l) => l.kind === 'line');
const title = $('pvTitle');
if (!sung.length) {
@@ -6075,6 +6079,7 @@ const Presenter = (() => {
function tick(force = false) {
if (view.classList.contains('hidden') || !st.all.length || !st.state) return;
+ if (overlay) { LowerThird.paint(st.all, LyricsCore.activeIndex(st.all, liveTime(), st.docOff + (Number(st.state.off) || 0))); return; }
const idx = LyricsCore.activeIndex(st.all, liveTime(), st.docOff + (Number(st.state.off) || 0));
if (idx === st.idx && !force) return;
st.idx = idx;
@@ -6108,7 +6113,7 @@ const Presenter = (() => {
function boot() {
let param = null;
- try { param = new URLSearchParams(location.search).get('present'); } catch { /* no URL */ }
+ try { param = new URLSearchParams(location.search).get(overlay ? 'code' : 'present'); } catch { /* no URL */ }
if (param === null) return;
try {
const u = new URL(location.href);
@@ -6117,7 +6122,8 @@ const Presenter = (() => {
} catch { /* keep */ }
show();
const code = String(param).replace(/\D/g, '');
- if (code.length === 6) pair(code);
+ if (overlay && load()) connect();
+ else if (code.length === 6) pair(code);
else if (load()) connect();
else showPairForm();
setInterval(() => tick(), 200);
@@ -12436,6 +12442,7 @@ async function boot() {
syncOnLaunch();
startPlaylistInboxWatch();
Presenter.boot();
+ if (LowerThird.active()) return;
SectionRail.boot();
Share.bootFromUrl();
bootViewFromUrl();
@@ -12444,6 +12451,7 @@ async function boot() {
Party.boot();
PartyDJ.register(() => $('navPartyBtn').click());
Remote.boot();
+ LowerThird.register(() => $('remoteChip').click());
Transition.renderButton();
PiP.render();
LyricsWindow.configure({ prepare: () => { if (!isServiceModeOpen()) Notes.stageLyrics.set(true); }, view: () => isServiceModeOpen() && data.settings.serviceLyrics ? $('serviceLyrics') : $('stageLyrics'), line: () => $('serviceLyricsList')?.querySelector('.active')?.textContent || $('stageLyricsList')?.querySelector('.active')?.textContent || current?.meta?.title || 'Nothing playing', toggle: () => { Player.toggle(); updatePlayBtn(); }, next: playNext, refresh: () => { Notes.stageLyrics.render(true); Notes.serviceRefresh(); }, toast });
diff --git a/frontend/index.html b/frontend/index.html
index 819d93c..8a19a29 100755
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -28,6 +28,7 @@
+
@@ -658,6 +659,7 @@
+