mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
Add a "Streaming" section to the GENERAL settings tab with a quality dropdown (Auto / Low 360p / Medium 480p / High / HD 720p). The setting persists to localStorage and applies to all live streams: - LiveWebcamsPanel: appends vq= to direct embeds, passes through proxy - LiveNewsPanel: sets quality via YT.Player API onReady + desktop proxy - YouTube embed proxy: accepts vq param, calls setPlaybackQuality() Closes #365
This commit is contained in:
@@ -59,6 +59,7 @@ export default async function handler(request) {
|
||||
|
||||
const autoplay = parseFlag(url.searchParams.get('autoplay'), '1');
|
||||
const mute = parseFlag(url.searchParams.get('mute'), '1');
|
||||
const vq = ['small', 'medium', 'large', 'hd720', 'hd1080'].includes(url.searchParams.get('vq') || '') ? url.searchParams.get('vq') : '';
|
||||
|
||||
const origin = sanitizeOrigin(url.searchParams.get('origin'));
|
||||
const parentOrigin = sanitizeParentOrigin(url.searchParams.get('parentOrigin'), origin);
|
||||
@@ -120,6 +121,7 @@ export default async function handler(request) {
|
||||
events:{
|
||||
onReady:function(){
|
||||
window.parent.postMessage({type:'yt-ready'},parentOrigin);
|
||||
${vq ? `if(player.setPlaybackQuality)player.setPlaybackQuality('${vq}');` : ''}
|
||||
if(${autoplay}===1){player.playVideo()}
|
||||
startMuteSync();
|
||||
},
|
||||
@@ -145,6 +147,7 @@ export default async function handler(request) {
|
||||
case'mute':player.mute();break;
|
||||
case'unmute':player.unMute();break;
|
||||
case'loadVideo':if(m.videoId)player.loadVideoById(m.videoId);break;
|
||||
case'setQuality':if(m.quality&&player.setPlaybackQuality)player.setPlaybackQuality(m.quality);break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isDesktopRuntime, getRemoteApiBaseUrl, getApiBaseUrl } from '@/services
|
||||
import { t } from '../services/i18n';
|
||||
import { loadFromStorage, saveToStorage } from '@/utils';
|
||||
import { STORAGE_KEYS, SITE_VARIANT } from '@/config';
|
||||
import { getStreamQuality } from '@/services/ai-flow-settings';
|
||||
|
||||
// YouTube IFrame Player API types
|
||||
type YouTubePlayer = {
|
||||
@@ -13,6 +14,7 @@ type YouTubePlayer = {
|
||||
pauseVideo(): void;
|
||||
loadVideoById(videoId: string): void;
|
||||
cueVideoById(videoId: string): void;
|
||||
setPlaybackQuality?(quality: string): void;
|
||||
getIframe?(): HTMLIFrameElement;
|
||||
getVolume?(): number;
|
||||
destroy(): void;
|
||||
@@ -799,6 +801,8 @@ export class LiveNewsPanel extends Panel {
|
||||
if (this.youtubeOrigin) params.set('origin', this.youtubeOrigin);
|
||||
const parentOrigin = this.parentPostMessageOrigin;
|
||||
if (parentOrigin) params.set('parentOrigin', parentOrigin);
|
||||
const quality = getStreamQuality();
|
||||
if (quality !== 'auto') params.set('vq', quality);
|
||||
return `/api/youtube/embed?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -965,6 +969,8 @@ export class LiveNewsPanel extends Panel {
|
||||
this.currentVideoId = this.activeChannel.videoId || null;
|
||||
const iframe = this.player?.getIframe?.();
|
||||
if (iframe) iframe.referrerPolicy = 'strict-origin-when-cross-origin';
|
||||
const quality = getStreamQuality();
|
||||
if (quality !== 'auto') this.player?.setPlaybackQuality?.(quality);
|
||||
this.syncPlayerState();
|
||||
this.startMuteSyncPolling();
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isDesktopRuntime, getRemoteApiBaseUrl } from '@/services/runtime';
|
||||
import { escapeHtml } from '@/utils/sanitize';
|
||||
import { t } from '../services/i18n';
|
||||
import { trackWebcamSelected, trackWebcamRegionFiltered } from '@/services/analytics';
|
||||
import { getStreamQuality, subscribeStreamQualityChange } from '@/services/ai-flow-settings';
|
||||
|
||||
type WebcamRegion = 'middle-east' | 'europe' | 'asia' | 'americas';
|
||||
|
||||
@@ -67,6 +68,7 @@ export class LiveWebcamsPanel extends Panel {
|
||||
this.createToolbar();
|
||||
this.setupIntersectionObserver();
|
||||
this.setupIdleDetection();
|
||||
subscribeStreamQualityChange(() => this.render());
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -159,6 +161,7 @@ export class LiveWebcamsPanel extends Panel {
|
||||
}
|
||||
|
||||
private buildEmbedUrl(videoId: string): string {
|
||||
const quality = getStreamQuality();
|
||||
if (isDesktopRuntime()) {
|
||||
const remoteBase = getRemoteApiBaseUrl();
|
||||
const params = new URLSearchParams({
|
||||
@@ -166,9 +169,11 @@ export class LiveWebcamsPanel extends Panel {
|
||||
autoplay: '1',
|
||||
mute: '1',
|
||||
});
|
||||
if (quality !== 'auto') params.set('vq', quality);
|
||||
return `${remoteBase}/api/youtube/embed?${params.toString()}`;
|
||||
}
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&controls=0&modestbranding=1&playsinline=1&rel=0`;
|
||||
const vq = quality !== 'auto' ? `&vq=${quality}` : '';
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&controls=0&modestbranding=1&playsinline=1&rel=0${vq}`;
|
||||
}
|
||||
|
||||
private createIframe(feed: WebcamFeed): HTMLIFrameElement {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { FEEDS, INTEL_SOURCES, SOURCE_REGION_MAP } from '@/config/feeds';
|
||||
import { PANEL_CATEGORY_MAP } from '@/config/panels';
|
||||
import { SITE_VARIANT } from '@/config/variant';
|
||||
import { LANGUAGES, changeLanguage, getCurrentLanguage, t } from '@/services/i18n';
|
||||
import { getAiFlowSettings, setAiFlowSetting } from '@/services/ai-flow-settings';
|
||||
import { getAiFlowSettings, setAiFlowSetting, getStreamQuality, setStreamQuality, STREAM_QUALITY_OPTIONS } from '@/services/ai-flow-settings';
|
||||
import type { StreamQuality } from '@/services/ai-flow-settings';
|
||||
import { escapeHtml } from '@/utils/sanitize';
|
||||
import { trackLanguageChange } from '@/services/analytics';
|
||||
import type { PanelConfig } from '@/types';
|
||||
@@ -148,6 +149,12 @@ export class UnifiedSettings {
|
||||
this.overlay.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
// Stream quality select
|
||||
if (target.id === 'us-stream-quality') {
|
||||
setStreamQuality(target.value as StreamQuality);
|
||||
return;
|
||||
}
|
||||
|
||||
// Language select
|
||||
if (target.closest('.unified-settings-lang-select')) {
|
||||
trackLanguageChange(target.value);
|
||||
@@ -299,6 +306,22 @@ export class UnifiedSettings {
|
||||
`;
|
||||
}
|
||||
|
||||
// Streaming quality section
|
||||
const currentQuality = getStreamQuality();
|
||||
html += `<div class="ai-flow-section-label">${t('components.insights.sectionStreaming')}</div>`;
|
||||
html += `<div class="ai-flow-toggle-row">
|
||||
<div class="ai-flow-toggle-label-wrap">
|
||||
<div class="ai-flow-toggle-label">${t('components.insights.streamQualityLabel')}</div>
|
||||
<div class="ai-flow-toggle-desc">${t('components.insights.streamQualityDesc')}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
html += `<select class="unified-settings-lang-select" id="us-stream-quality">`;
|
||||
for (const opt of STREAM_QUALITY_OPTIONS) {
|
||||
const selected = opt.value === currentQuality ? ' selected' : '';
|
||||
html += `<option value="${opt.value}"${selected}>${opt.label}</option>`;
|
||||
}
|
||||
html += `</select>`;
|
||||
|
||||
// Language section
|
||||
html += `<div class="ai-flow-section-label">${t('header.languageLabel')}</div>`;
|
||||
html += `<select class="unified-settings-lang-select">`;
|
||||
|
||||
@@ -954,6 +954,9 @@
|
||||
"settingsTitle": "Settings",
|
||||
"sectionMap": "Map",
|
||||
"sectionAi": "AI Analysis",
|
||||
"sectionStreaming": "Streaming",
|
||||
"streamQualityLabel": "Video Quality",
|
||||
"streamQualityDesc": "Set quality for all live streams (lower saves bandwidth)",
|
||||
"mapFlashLabel": "Live Event Pulse",
|
||||
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
|
||||
"aiFlowTitle": "Settings",
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
const STORAGE_KEY_BROWSER_MODEL = 'wm-ai-flow-browser-model';
|
||||
const STORAGE_KEY_CLOUD_LLM = 'wm-ai-flow-cloud-llm';
|
||||
const STORAGE_KEY_MAP_NEWS_FLASH = 'wm-map-news-flash';
|
||||
const STORAGE_KEY_STREAM_QUALITY = 'wm-stream-quality';
|
||||
const EVENT_NAME = 'ai-flow-changed';
|
||||
const STREAM_QUALITY_EVENT = 'stream-quality-changed';
|
||||
|
||||
export interface AiFlowSettings {
|
||||
browserModel: boolean;
|
||||
@@ -73,3 +75,39 @@ export function subscribeAiFlowChange(cb: (changedKey?: keyof AiFlowSettings) =>
|
||||
window.addEventListener(EVENT_NAME, handler);
|
||||
return () => window.removeEventListener(EVENT_NAME, handler);
|
||||
}
|
||||
|
||||
// ── Stream Quality ──
|
||||
|
||||
export type StreamQuality = 'auto' | 'small' | 'medium' | 'large' | 'hd720';
|
||||
|
||||
export const STREAM_QUALITY_OPTIONS: { value: StreamQuality; label: string }[] = [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'small', label: 'Low (360p)' },
|
||||
{ value: 'medium', label: 'Medium (480p)' },
|
||||
{ value: 'large', label: 'High (480p+)' },
|
||||
{ value: 'hd720', label: 'HD (720p)' },
|
||||
];
|
||||
|
||||
export function getStreamQuality(): StreamQuality {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_STREAM_QUALITY);
|
||||
if (raw && ['auto', 'small', 'medium', 'large', 'hd720'].includes(raw)) return raw as StreamQuality;
|
||||
} catch { /* ignore */ }
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
export function setStreamQuality(quality: StreamQuality): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY_STREAM_QUALITY, quality);
|
||||
} catch { /* ignore */ }
|
||||
window.dispatchEvent(new CustomEvent(STREAM_QUALITY_EVENT, { detail: { quality } }));
|
||||
}
|
||||
|
||||
export function subscribeStreamQualityChange(cb: (quality: StreamQuality) => void): () => void {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail as { quality: StreamQuality };
|
||||
cb(detail.quality);
|
||||
};
|
||||
window.addEventListener(STREAM_QUALITY_EVENT, handler);
|
||||
return () => window.removeEventListener(STREAM_QUALITY_EVENT, handler);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user