This release adds microphone processing controls and local chat controls, expands platform packages, and synchronizes the website and guides. It also fixes native H.264 input/format handling, signaling recovery, fresh STUN discovery, and UDP ownership during overlapping connections. This is **v1.8.0**. Browser/Server v23, Native control v9 and capture v7 remain unchanged, with the existing 1440p ceiling. Additional resolutions, capability negotiation and Magicsock are separate follow-up work. ## Validation - Full local Web check: 79 files / 1,317 tests, TypeScript and production build. - Full Go checks, platform builds and Windows native capture regressions/probe. - Native H.264 Host lifecycle, Browser-to-App fanout and App-exit recovery, and Native H.264 through embedded SFU: all passed with owned process/port/profile cleanup. - Website build, documentation and repository hygiene passed. Existing UI acceptance covers narrow screens, both languages/themes, architecture downloads and all 23 film checks; final packaging and Go race checks run on the merged revision in CI. - Physical macOS/Linux capture, representative public networks and unmatched field reports retain their documented evidence limits. No claim that every blur/disconnection report is resolved. ## Release notes ### 中文 这是一次功能与兼容性更新:麦克风和聊天增加了实用的小选项,App 与 Server 支持更多设备,同时修复了原生分享和连接恢复中的若干问题。现有 1.x 协议保持不变。 #### 新增与改进 - 浏览器麦克风新增「降噪与回声消除」开关,默认开启回声消除、降噪和自动增益;分享音乐或使用虚拟音频输入时,可按需关闭。 - 可调整弹幕字号和透明度,并将当前保留的聊天记录导出为 TXT。当前会话最多保留 1,000 条文字消息,刷新页面后清空;需要保存时请先导出。 - 新增 Intel Mac、Linux ARM64 App,以及 Linux ARM64 Server 和容器镜像。官网下载区可直接选择对应架构,并显示最新版本和发布时间。 - 更新官网演示、README 截图和中英文教程,明确浏览器分享也可选择窗口或屏幕;开始分享前显示房间号占位,保持底栏控件位置稳定。 - 官网新增直达问题排查的入口,补充浏览器 WebRTC 限制、校园网和 Windows 显卡相关的排查步骤。 - App 模式选择页显示免费开源说明、作者与贡献者署名,以及官方网站和源码链接。 #### 修复 - 修正部分 H.264 原生分享的格式兼容性校验,以及 Windows 编码器输入长度和输出缓冲区对齐问题。 - 清理已失效的重连任务和 Native 接收信令,避免旧连接继续影响当前会话。 - 修复 App 重连时可能复用过期公网地址的问题;重新探测使用当前地址,并处理探测丢包和过期响应,保留其他正常连接。 - 修复 App 新旧媒体连接重叠时共用端口造成的冲突,避免准备或撤销新连接打断仍在使用的画面;关闭连接时及时取消未完成的端口映射请求。 - 无站点密码时,连接恢复不再误引导用户输入站点密码。 - 房间失效或会话终止时,开播前后的原因提示统一显示在电视状态图标中,避免额外提示条。 - 修正 Linux 原生采集的时间戳与关键帧时间基准。感谢 Pedro Antonio 在 [#449](https://github.com/TNTcraftHIM/Piik/pull/449) 的贡献。 #### 使用说明 下载对应平台的完整压缩包,退出旧 App 后解压使用。Windows 继续提供 x64;macOS 提供 Apple 芯片与 Intel 版本,原生采集需要 macOS 13 或更新版本。Linux 提供 x64 与 ARM64,原生采集还需安装[系统组件](https://github.com/TNTcraftHIM/Piik/blob/main/native/capture/linux/README.md)。macOS 和 Linux 的实机覆盖仍有限,欢迎反馈。 ### English This update adds useful microphone and chat controls, brings Piik to more devices, and fixes several native-sharing and connection-recovery issues. The existing 1.x protocols remain unchanged. #### New and improved - Browser microphones now have a “Noise & echo reduction” toggle. Echo cancellation, noise suppression and automatic gain control are on by default; turn them off when appropriate for music or virtual audio inputs. - Adjust chat-overlay text size and opacity, and export retained messages as TXT. The current session keeps up to 1,000 text messages and clears on page refresh; export first if you want to keep them. - Added Intel Mac and Linux ARM64 App packages, plus Linux ARM64 Server and container images. The website provides architecture selectors and shows the latest release and publication date. - Updated the website demonstration, README screenshots and bilingual guides, clarifying that browser capture can also select windows or screens. A room-code placeholder keeps the bottom bar stable before sharing starts. - Added direct website links to troubleshooting, with steps for browser WebRTC restrictions, campus networks and Windows graphics issues. - The App mode-selection page now shows its free and open-source status, author and contributor credit, and links to the official website and source. #### Fixes - Corrected H.264 format compatibility checks and Windows encoder input-length and output-buffer alignment handling. - Retired stale reconnect tasks and Native receive signals so they cannot continue affecting the current session. - Fixed App reconnection reusing an expired public address. Fresh discovery handles lost probes and stale responses while preserving other working connections. - Fixed UDP socket conflicts between overlapping App media connections so preparing or canceling a replacement preserves the active picture. Closing a connection now cancels unfinished gateway mapping requests. - Connection recovery on passwordless sites no longer incorrectly asks for a site passphrase. - Expired-room and session-termination reasons now use the same TV status indicator before and during sharing, without an extra notice strip. - Corrected Linux native capture timestamps and keyframe time bases. Thanks to Pedro Antonio for [#449](https://github.com/TNTcraftHIM/Piik/pull/449). #### Updating Download the complete archive for your platform, quit the old App, and extract the new package. Windows remains x64-only. macOS packages support Apple silicon and Intel; native capture requires macOS 13 or newer. Linux supports x64 and ARM64 and requires additional [system components](https://github.com/TNTcraftHIM/Piik/blob/main/native/capture/linux/README.md) for native capture. Physical-device coverage on macOS and Linux remains limited; feedback is welcome. ## Attribution Co-authored-by: Pedro Antonio <wrenchwastaken@gmail.com>
215 lines
9.2 KiB
JavaScript
215 lines
9.2 KiB
JavaScript
import { mountBrands } from './assets/brand.js';
|
|
import { initialLanguage, rememberLanguage } from './assets/language.js';
|
|
import { createTextCycle, startTextRotation } from '../src/client/ui/text-rotation.ts';
|
|
import { DEFAULT_RELEASE_API_URL, parseReleaseMetadata } from '../src/client/lib/release-update.ts';
|
|
mountBrands();
|
|
const architecturePickers = document.querySelectorAll('.download-architecture');
|
|
function syncDownloadLinks(picker) {
|
|
const selected = picker.querySelector('input:checked');
|
|
for (const link of picker.closest('.download-card').querySelectorAll('a')) {
|
|
const target = new URL(link.href);
|
|
target.pathname = target.pathname.replace(/piik-app-[a-z0-9]+-[a-z0-9]+\.zip$/, `piik-app-${selected.value}.zip`);
|
|
link.href = target.href;
|
|
}
|
|
}
|
|
for (const picker of architecturePickers) {
|
|
picker.disabled = picker.querySelectorAll('input').length < 2;
|
|
syncDownloadLinks(picker);
|
|
picker.addEventListener('change', () => syncDownloadLinks(picker));
|
|
}
|
|
// Content and download links remain usable without scripts or release metadata.
|
|
const releaseLabel = document.getElementById('download-release');
|
|
if (releaseLabel) {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
void fetch(DEFAULT_RELEASE_API_URL, {
|
|
credentials: 'omit', referrerPolicy: 'no-referrer', redirect: 'error',
|
|
signal: controller.signal, headers: { Accept: 'application/vnd.github+json' },
|
|
}).then(async response => {
|
|
if (!response.ok) return null;
|
|
return parseReleaseMetadata(await response.json());
|
|
})
|
|
.then(release => {
|
|
if (!release?.publishedAt) return;
|
|
releaseLabel.querySelector('b').textContent = release.version;
|
|
const date = releaseLabel.querySelector('time');
|
|
date.dateTime = release.publishedAt;
|
|
date.textContent = release.publishedAt.slice(0, 10);
|
|
releaseLabel.hidden = false;
|
|
}).catch(() => {}).finally(() => clearTimeout(timeout));
|
|
}
|
|
const root = document.documentElement;
|
|
const language = document.getElementById('language');
|
|
const theme = document.getElementById('theme');
|
|
const motion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
const motionControl = document.getElementById('reduce-motion');
|
|
const motionStorageKey = 'piik:site-reduced-motion';
|
|
let preferStill = false;
|
|
try {
|
|
preferStill = localStorage.getItem(motionStorageKey) === 'true';
|
|
} catch {
|
|
// Restricted storage still permits a preference for this visit.
|
|
}
|
|
const reducedMotion = () => motion.matches || preferStill;
|
|
const description = document.querySelector('meta[name="description"]');
|
|
const roomIllustration = document.querySelector('.living-room');
|
|
const film = document.getElementById('film-preview');
|
|
const filmFrame = document.getElementById('website-film');
|
|
const filmLink = document.querySelector('[data-film-link]');
|
|
const welcomeLines = document.getElementById('welcome-lines')?.content.children;
|
|
let stopWelcome = () => {};
|
|
let syncWelcomeMotion = () => {};
|
|
function syncWelcome() {
|
|
if (!welcomeLines) return;
|
|
const element = document.getElementById('welcome-line');
|
|
const pool = Array.from(welcomeLines).filter(line => line.lang === root.lang);
|
|
const next = createTextCycle(pool);
|
|
const show = () => element.replaceChildren(...(pool.length ? [next().cloneNode(true)] : []));
|
|
element.hidden = pool.length === 0;
|
|
show();
|
|
syncWelcomeMotion = () => {
|
|
stopWelcome();
|
|
stopWelcome = pool.length > 1 && !reducedMotion() ? startTextRotation(show, element) : () => {};
|
|
};
|
|
syncWelcomeMotion();
|
|
}
|
|
function syncFilmPreferences() {
|
|
const target = new URL(filmLink.href);
|
|
target.searchParams.set('lang', root.lang);
|
|
target.searchParams.set('theme', theme.value);
|
|
filmLink.href = target.href;
|
|
filmFrame.contentWindow?.postMessage({type:'piik-film-preferences',lang:root.lang,theme:theme.value}, location.origin);
|
|
}
|
|
const copy = {
|
|
en: {
|
|
title: document.title,
|
|
description: description.content,
|
|
themes: Object.fromEntries(Array.from(theme.options, (option) => [option.value, option.textContent])),
|
|
image: roomIllustration.alt,
|
|
},
|
|
'zh-CN': {
|
|
title: 'Piik — 来,看点好康的。',
|
|
description: '游戏、绘画、电影、照片。Piik 是免费开源的屏幕共享工具,邀请最多 20 位朋友用浏览器观看。',
|
|
themes: { system: '跟随系统', light: '浅色', dark: '深色' },
|
|
image: '戴着小金冠的房主分享 RPG 游戏、绘画、旅行照片和动画电影,三位朋友坐在沙发上观看。',
|
|
},
|
|
};
|
|
function setLanguage(lang) {
|
|
root.lang = lang;
|
|
const chinese = lang === 'zh-CN';
|
|
language.lang = chinese ? 'en' : 'zh-CN';
|
|
language.textContent = chinese ? 'English' : '简体中文';
|
|
language.setAttribute('aria-label', chinese ? 'Switch to English' : '切换到简体中文');
|
|
const current = copy[root.lang];
|
|
document.title = current.title;
|
|
description.content = current.description;
|
|
roomIllustration.alt = current.image;
|
|
document.querySelector('.site-header nav').setAttribute('aria-label', chinese ? '主导航' : 'Main');
|
|
document.querySelector('.guide-links').setAttribute('aria-label', chinese ? '教程目录' : 'Guides');
|
|
for (const link of document.querySelectorAll('[data-doc-page]')) {
|
|
link.href = `./docs/${chinese ? 'zh/' : ''}${link.dataset.docPage}${chinese ? '' : '?lang=en'}`;
|
|
}
|
|
filmFrame.title = chinese ? 'Piik 宣传片' : 'Piik introduction';
|
|
for (const option of theme.options) option.textContent = current.themes[option.value];
|
|
syncWelcome();
|
|
syncFilmPreferences();
|
|
}
|
|
language.addEventListener('click', () => {
|
|
setLanguage(root.lang === 'zh-CN' ? 'en' : 'zh-CN');
|
|
rememberLanguage(root.lang);
|
|
});
|
|
function syncThemeColor() {
|
|
document.querySelector('meta[name="theme-color"]').content = getComputedStyle(root).getPropertyValue('--wall').trim();
|
|
}
|
|
theme.addEventListener('change', () => {
|
|
root.dataset.theme = theme.value;
|
|
syncThemeColor();
|
|
syncFilmPreferences();
|
|
});
|
|
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncThemeColor);
|
|
syncThemeColor();
|
|
document.querySelector('.preferences').hidden = false;
|
|
|
|
// Ordinary links carry explicit choices between the page and its film.
|
|
const preferences = new URLSearchParams(location.search);
|
|
setLanguage(initialLanguage());
|
|
if (['light', 'dark'].includes(preferences.get('theme'))) {
|
|
theme.value = preferences.get('theme');
|
|
root.dataset.theme = theme.value;
|
|
syncThemeColor();
|
|
}
|
|
syncFilmPreferences();
|
|
filmLink.setAttribute('aria-controls', 'film-preview');
|
|
filmLink.setAttribute('aria-expanded', 'false');
|
|
filmLink.addEventListener('click', (event) => {
|
|
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
|
event.preventDefault();
|
|
const target = new URL(event.currentTarget.href);
|
|
target.searchParams.set('embedded', '1');
|
|
if (!film.open) filmFrame.src = target.href;
|
|
film.hidden = false;
|
|
film.open = true;
|
|
filmLink.setAttribute('aria-expanded', 'true');
|
|
film.querySelector('summary').focus({preventScroll:true});
|
|
film.scrollIntoView({block:'start'});
|
|
});
|
|
film.addEventListener('toggle', () => {
|
|
if (film.open) return;
|
|
// Closing releases the frame, its music and its animation loop together.
|
|
filmFrame.removeAttribute('src');
|
|
film.hidden = true;
|
|
filmLink.setAttribute('aria-expanded', 'false');
|
|
filmLink.focus({preventScroll:true});
|
|
});
|
|
|
|
// Native disclosures remain usable without scripts; section links also reveal them.
|
|
function revealGuide(hash) {
|
|
const target = document.getElementById(hash.slice(1));
|
|
if (target?.matches('details.guide')) target.open = true;
|
|
}
|
|
document.querySelectorAll('a[href^="#"]').forEach((link) => {
|
|
link.addEventListener('click', () => revealGuide(link.hash));
|
|
});
|
|
window.addEventListener('hashchange', () => revealGuide(location.hash));
|
|
revealGuide(location.hash);
|
|
|
|
const illustrations = document.querySelectorAll('.step-art');
|
|
document.querySelectorAll('.step-art').forEach((control) => {
|
|
const replayScene = () =>
|
|
control.getAnimations({ subtree: true }).forEach((animation) => {
|
|
if (animation instanceof CSSAnimation) animation.currentTime = 0;
|
|
});
|
|
(control.closest('li') ?? control).addEventListener('pointerenter', replayScene);
|
|
control.addEventListener('focus', replayScene);
|
|
control.addEventListener('click', () => {
|
|
control.focus({ preventScroll: true });
|
|
replayScene();
|
|
});
|
|
});
|
|
function syncMotionPreference() {
|
|
const still = reducedMotion();
|
|
root.toggleAttribute('data-reduced-motion', still);
|
|
motionControl.checked = still;
|
|
motionControl.disabled = motion.matches;
|
|
motionControl.setAttribute('aria-describedby', motion.matches ? 'display-motion-help display-motion-system' : 'display-motion-help');
|
|
document.getElementById('display-motion-system').hidden = !motion.matches;
|
|
illustrations.forEach((button) => {
|
|
button.disabled = still;
|
|
});
|
|
roomIllustration.src = './assets/living-room.svg' + (still ? '#still' : '');
|
|
syncWelcomeMotion();
|
|
}
|
|
motionControl.addEventListener('change', () => {
|
|
preferStill = motionControl.checked;
|
|
try {
|
|
if (preferStill) localStorage.setItem(motionStorageKey, 'true');
|
|
else localStorage.removeItem(motionStorageKey);
|
|
} catch {
|
|
// The current page keeps its explicit choice without persistence.
|
|
}
|
|
syncMotionPreference();
|
|
});
|
|
motion.addEventListener('change', syncMotionPreference);
|
|
syncMotionPreference();
|
|
document.getElementById('display-settings').hidden = false;
|