diff --git a/js/app/package.json b/js/app/package.json index 03cbf3004..ff00a43ff 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -53,6 +53,7 @@ "@react-navigation/native-stack": "^8.0.0-alpha.16", "@sentry/cli": "^3.3.3", "@sentry/react-native": "^7.11.0", + "@streamplace/core": "workspace:*", "@streamplace/components": "workspace:*", "@tanstack/react-query": "^5.59.19", "@zxing/browser": "^0.1.5", diff --git a/js/app/utils/game.ts b/js/app/utils/game.ts index b4629b8cf..27e5b9fbd 100644 --- a/js/app/utils/game.ts +++ b/js/app/utils/game.ts @@ -1,20 +1,3 @@ -import { games } from "streamplace"; - -const COVER_MEDIA_TYPES: Set< - games.gamesgamesgamesgames.defs.MediaItem["mediaType"] -> = new Set(["cover", "coverSquare"]); - -export function getGameCoverUrl( - media: games.gamesgamesgamesgames.defs.MediaItem[] | undefined, - did: string, -): string | undefined { - const coverItem = - media?.find((m) => COVER_MEDIA_TYPES.has(m.mediaType)) ?? media?.[0]; - const cid = (coverItem?.blob as any)?.ref?.toString(); - if (!cid) return undefined; - return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${cid}@jpeg`; -} - -export function getDidFromAtUri(uri: string): string { - return uri.split("/")[2] ?? ""; -} +// Game-activity helpers. Canonical implementation lives in +// @streamplace/core; this keeps the historical import path working. +export { getDidFromAtUri, getGameCoverUrl } from "@streamplace/core"; diff --git a/js/components/src/utils/did.ts b/js/components/src/utils/did.ts index 80a6d11ba..4c1f7bde1 100644 --- a/js/components/src/utils/did.ts +++ b/js/components/src/utils/did.ts @@ -1,61 +1,8 @@ -export interface DIDDocument { - id: string; - service?: Array<{ - id: string; - type?: string; - serviceEndpoint?: string; - }>; - [key: string]: any; -} - -export async function resolveDIDDocument(did: string): Promise { - let didDocUrl: string; - - if (did.startsWith("did:web:")) { - // For did:web, construct the URL directly - const domain = did.replace("did:web:", "").replace(/:/g, "/"); - didDocUrl = `https://${domain}/.well-known/did.json`; - } else if (did.startsWith("did:plc:")) { - // For did:plc, use plc.directory - didDocUrl = `https://plc.directory/${did}`; - } else { - throw new Error(`Unsupported DID method: ${did}`); - } - - const response = await fetch(didDocUrl); - if (!response.ok) { - throw new Error( - `Failed to resolve DID document for ${did}: ${response.status}`, - ); - } - - return response.json(); -} - -export function getPDSServiceEndpoint(didDoc: DIDDocument): string { - const pdsService = didDoc.service?.find((s) => s.id === "#atproto_pds"); - - if (!pdsService?.serviceEndpoint) { - throw new Error("No PDS service endpoint found in DID document"); - } - - return pdsService.serviceEndpoint; -} - -export async function getBlob( - did: string, - cid: string, - didDoc?: DIDDocument, -): Promise { - const doc = didDoc || (await resolveDIDDocument(did)); - const pdsEndpoint = getPDSServiceEndpoint(doc); - - const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`; - - const response = await fetch(blobUrl); - if (!response.ok) { - throw new Error(`Failed to fetch blob: ${response.status}`); - } - - return response.blob(); -} +// DID-document resolution helpers. Canonical implementation lives in +// @streamplace/core; this keeps the historical import path working. +export { + getBlob, + getPDSServiceEndpoint, + resolveDIDDocument, + type DIDDocument, +} from "@streamplace/core"; diff --git a/js/core/src/index.ts b/js/core/src/index.ts index 1a6a6f359..48eff6f30 100644 --- a/js/core/src/index.ts +++ b/js/core/src/index.ts @@ -5,7 +5,9 @@ export * from "./livestream-store"; export * from "./hooks/use-actor-typeahead"; export * from "./lib/browser"; +export * from "./lib/did"; export * from "./lib/facet"; +export * from "./lib/game"; export * from "./lib/pds-hosts"; export { getPlatform } from "./lib/platform"; export type { CorePlatform } from "./lib/platform"; diff --git a/js/core/src/livestream-store/index.tsx b/js/core/src/livestream-store/index.tsx index bf502f068..be6c0f8ca 100644 --- a/js/core/src/livestream-store/index.tsx +++ b/js/core/src/livestream-store/index.tsx @@ -3,6 +3,7 @@ // reducers, and pure utilities. The React hooks and context live in // @streamplace/components. export * from "./chat-reducer"; +export * from "./connect"; export * from "./problems"; export * from "./state"; export * from "./store"; diff --git a/js/i18n/locales/en-US/common.ftl b/js/i18n/locales/en-US/common.ftl index c28ab0a19..c1d6f1e2d 100644 --- a/js/i18n/locales/en-US/common.ftl +++ b/js/i18n/locales/en-US/common.ftl @@ -291,3 +291,65 @@ select-license = Select a license… ## Login login-show-live-on-bluesky = Show when I'm live on Bluesky login-show-live-on-bluesky-description = Gives your Bluesky avatar the red LIVE ring while you stream and lets Streamplace post announcements for you. Uncheck to sign in without granting any access to your Bluesky account. + +## Video Player - Controls +player-play = Play +player-pause = Pause +player-mute = Mute +player-unmute = Unmute +player-seek = Seek +player-settings = Settings +player-fullscreen = Fullscreen +player-exit-fullscreen = Exit fullscreen +player-theatre = Theatre mode +player-exit-theatre = Exit theatre mode +player-pip = Picture-in-picture +player-exit-pip = Exit picture-in-picture +player-enable-danmu = Enable danmu +player-disable-danmu = Disable danmu +player-quality = Quality +player-latency = Latency +player-quality-auto = Auto + +## Video Player - Stats panel +player-stats = Player stats +player-stats-resolution = Resolution +player-stats-viewport = Viewport +player-stats-bitrate = Bitrate +player-stats-skipped = Skipped +player-stats-buffer = Buffer +player-stats-codecs = Codecs +player-stats-protocol = Protocol +player-stats-latency-mode = Latency Mode +player-stats-render-surface = Render Surface +player-stats-session = Session +player-protocol-webrtc = WebRTC +player-protocol-hls = HLS +player-latency-low = Low Latency +player-latency-standard = Standard + +## Video Player - Errors +player-error-network = Network error +player-error-playback = Playback error +player-error-format = Stream format not supported +player-error-stream-not-live = Stream not live +player-error-network-retrying = Network error. Retrying. +player-error-media-recovering = Media error. Recovering. +player-error-hls-unsupported = Your browser doesn't support HLS playback. +player-error-stream-unavailable = Stream unavailable. Stopped reconnecting. +player-error-sdp-failed = Failed to create SDP offer +player-error-ice-failed = Failed to gather ICE candidates +player-error-negotiation-failed = WebRTC negotiation failed +player-error-stream-stalled = Stream stalled. Reconnecting. +player-error-connection-lost = Connection lost. Reconnecting. + +## Upload +upload-error-format = File doesn't appear to be a supported video format (MP4, WebM, MKV, MOV, AVI, OGG, FLV, MPEG-TS). +upload-error-timeout = Processing timed out. Please try again later. +upload-error-processing = Processing failed +upload-error-not-logged-in = Not logged in +upload-title-untitled = Untitled + +## Emoji picker +emoji-skin-tone = Skin tone +emoji-search = Search emoji… diff --git a/js/i18n/public/locales/en-US/common.json b/js/i18n/public/locales/en-US/common.json index 9b79eea0d..8f628cec7 100644 --- a/js/i18n/public/locales/en-US/common.json +++ b/js/i18n/public/locales/en-US/common.json @@ -218,5 +218,59 @@ "no-videos-yet-upload": "No videos yet.", "optional": "Optional", "required": "Required", - "select-license": "Select a license…" + "select-license": "Select a license…", + "login-show-live-on-bluesky": "Show when I'm live on Bluesky", + "login-show-live-on-bluesky-description": "Gives your Bluesky avatar the red LIVE ring while you stream and lets Streamplace post announcements for you. Uncheck to sign in without granting any access to your Bluesky account.", + "player-play": "Play", + "player-pause": "Pause", + "player-mute": "Mute", + "player-unmute": "Unmute", + "player-seek": "Seek", + "player-settings": "Settings", + "player-fullscreen": "Fullscreen", + "player-exit-fullscreen": "Exit fullscreen", + "player-theatre": "Theatre mode", + "player-exit-theatre": "Exit theatre mode", + "player-pip": "Picture-in-picture", + "player-exit-pip": "Exit picture-in-picture", + "player-enable-danmu": "Enable danmu", + "player-disable-danmu": "Disable danmu", + "player-quality": "Quality", + "player-latency": "Latency", + "player-quality-auto": "Auto", + "player-stats": "Player stats", + "player-stats-resolution": "Resolution", + "player-stats-viewport": "Viewport", + "player-stats-bitrate": "Bitrate", + "player-stats-skipped": "Skipped", + "player-stats-buffer": "Buffer", + "player-stats-codecs": "Codecs", + "player-stats-protocol": "Protocol", + "player-stats-latency-mode": "Latency Mode", + "player-stats-render-surface": "Render Surface", + "player-stats-session": "Session", + "player-protocol-webrtc": "WebRTC", + "player-protocol-hls": "HLS", + "player-latency-low": "Low Latency", + "player-latency-standard": "Standard", + "player-error-network": "Network error", + "player-error-playback": "Playback error", + "player-error-format": "Stream format not supported", + "player-error-stream-not-live": "Stream not live", + "player-error-network-retrying": "Network error. Retrying.", + "player-error-media-recovering": "Media error. Recovering.", + "player-error-hls-unsupported": "Your browser doesn't support HLS playback.", + "player-error-stream-unavailable": "Stream unavailable. Stopped reconnecting.", + "player-error-sdp-failed": "Failed to create SDP offer", + "player-error-ice-failed": "Failed to gather ICE candidates", + "player-error-negotiation-failed": "WebRTC negotiation failed", + "player-error-stream-stalled": "Stream stalled. Reconnecting.", + "player-error-connection-lost": "Connection lost. Reconnecting.", + "upload-error-format": "File doesn't appear to be a supported video format (MP4, WebM, MKV, MOV, AVI, OGG, FLV, MPEG-TS).", + "upload-error-timeout": "Processing timed out. Please try again later.", + "upload-error-processing": "Processing failed", + "upload-error-not-logged-in": "Not logged in", + "upload-title-untitled": "Untitled", + "emoji-skin-tone": "Skin tone", + "emoji-search": "Search emoji…" } diff --git a/js/i18n/public/locales/en-US/settings.json b/js/i18n/public/locales/en-US/settings.json index 457536511..6ef4e1082 100644 --- a/js/i18n/public/locales/en-US/settings.json +++ b/js/i18n/public/locales/en-US/settings.json @@ -30,6 +30,8 @@ "currently-translating-description": "Some parts of the app may look incomplete. Thank you for your patience!", "debug-recording-title": "Allow {$host} to record your livestream for debugging and improving the service", "debug-recording-description": "Optional", + "livestream-recording-title": "Record my livestreams into VODs on {$host}", + "livestream-recording-description": "When enabled, your livestreams are saved so you can finalize and publish them as videos", "manage-keys": "Manage Keys", "settings-title": "Settings", "error": "Error", @@ -41,6 +43,12 @@ "languages": "Languages", "privacy-security": "Privacy & Security", "streaming": "Streaming", + "notifications": "Notifications", + "notifications-title": "Push Notifications", + "notifications-web-description": "Get notified when streamers you follow go live.", + "notifications-mobile-description": "Push notifications are managed by your device settings.", + "notifications-blocked-description": "Blocked — you denied notification permission in your browser.", + "notifications-blocked-help": "To re-enable, update site permissions in your browser settings, then toggle this on again.", "cancel": "Cancel", "create": "Create", "delete": "Delete", @@ -105,6 +113,7 @@ "events": "Events", "events-livestream": "Livestream Events", "events-chat": "Chat Events", + "events-stream-received": "Stream Received Events", "untitled-webhook": "Untitled Webhook", "inactive": "Inactive", "active": "Active", diff --git a/js/i18n/public/locales/es-ES/common.json b/js/i18n/public/locales/es-ES/common.json index 153dc7a0b..c4448a6fc 100644 --- a/js/i18n/public/locales/es-ES/common.json +++ b/js/i18n/public/locales/es-ES/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "Más información sobre el autoalojamiento", "pds-selector-info": "Cada servidor tiene sus propias políticas y estándares de fiabilidad. Tus datos ATProto viven en el servidor que elijas y puedes migrar más tarde. Nota: Streamplace tiene sus propias reglas de moderación; puedes ser expulsado de Streamplace independientemente del servidor que elijas.", "pds-selector-read-policies": "Lee los Términos de Servicio y la Política de Privacidad de {$label} antes de continuar.", - "pds-selector-handle-policy-checkbox": "He leído y acepto la política de identificadores" + "pds-selector-handle-policy-checkbox": "He leído y acepto la política de identificadores", + "login-show-live-on-bluesky": "Mostrar cuando estoy en directo en Bluesky", + "login-show-live-on-bluesky-description": "Añade el anillo rojo LIVE a tu avatar de Bluesky mientras transmites y permite que Streamplace publique anuncios por ti. Desmárcalo para iniciar sesión sin conceder ningún acceso a tu cuenta de Bluesky." } diff --git a/js/i18n/public/locales/es-ES/settings.json b/js/i18n/public/locales/es-ES/settings.json index d9277d763..cda42b357 100644 --- a/js/i18n/public/locales/es-ES/settings.json +++ b/js/i18n/public/locales/es-ES/settings.json @@ -67,6 +67,7 @@ "languages": "Idiomas", "privacy-security": "Privacidad y Seguridad", "streaming": "Transmisión", + "notifications": "Notificaciones", "sign-in": "Iniciar Sesión", "log-out": "Cerrar sesión", "account-greeting": "Hola, @{$handle}.", diff --git a/js/i18n/public/locales/fr-FR/common.json b/js/i18n/public/locales/fr-FR/common.json index 339d534c0..1a6809ae6 100644 --- a/js/i18n/public/locales/fr-FR/common.json +++ b/js/i18n/public/locales/fr-FR/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "En savoir plus sur l'auto-hébergement", "pds-selector-info": "Chaque hôte a ses propres politiques et normes de fiabilité. Vos données ATProto se trouvent sur l'hôte que vous choisissez et vous pouvez migrer ultérieurement. Remarque : Streamplace a ses propres règles de modération — vous pouvez être banni de Streamplace quel que soit l'hôte choisi.", "pds-selector-read-policies": "Lisez les Conditions d'utilisation et la Politique de confidentialité de {$label} avant de continuer.", - "pds-selector-handle-policy-checkbox": "J'ai lu et j'accepte la politique des identifiants" + "pds-selector-handle-policy-checkbox": "J'ai lu et j'accepte la politique des identifiants", + "login-show-live-on-bluesky": "Afficher quand je suis en direct sur Bluesky", + "login-show-live-on-bluesky-description": "Ajoute l'anneau rouge LIVE à votre avatar Bluesky pendant vos streams et permet à Streamplace de publier des annonces pour vous. Décochez la case pour vous connecter sans accorder aucun accès à votre compte Bluesky." } diff --git a/js/i18n/public/locales/fr-FR/settings.json b/js/i18n/public/locales/fr-FR/settings.json index e16c7d50d..4cdf81ac7 100644 --- a/js/i18n/public/locales/fr-FR/settings.json +++ b/js/i18n/public/locales/fr-FR/settings.json @@ -67,6 +67,7 @@ "languages": "Langues", "privacy-security": "Confidentialité et Sécurité", "streaming": "Diffusion", + "notifications": "Notifications", "sign-in": "Se connecter", "log-out": "Se déconnecter", "account-greeting": "Salut, @{$handle}.", diff --git a/js/i18n/public/locales/pt-BR/common.json b/js/i18n/public/locales/pt-BR/common.json index 2c35c532b..348dd8f80 100644 --- a/js/i18n/public/locales/pt-BR/common.json +++ b/js/i18n/public/locales/pt-BR/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "Saiba mais sobre auto-hospedagem", "pds-selector-info": "Cada host tem suas próprias políticas e padrões de confiabilidade. Seus dados ATProto ficam no host que você escolher e você pode migrar depois. Nota: O Streamplace tem suas próprias regras de moderação — você pode ser banido do Streamplace independentemente do host escolhido.", "pds-selector-read-policies": "Leia os Termos de Serviço e a Política de Privacidade de {$label} antes de continuar.", - "pds-selector-handle-policy-checkbox": "Li e concordo com a política de identificadores" + "pds-selector-handle-policy-checkbox": "Li e concordo com a política de identificadores", + "login-show-live-on-bluesky": "Mostrar quando estou ao vivo no Bluesky", + "login-show-live-on-bluesky-description": "Adiciona o anel vermelho LIVE ao seu avatar do Bluesky enquanto você transmite e permite que o Streamplace publique anúncios por você. Desmarque para entrar sem conceder nenhum acesso à sua conta do Bluesky." } diff --git a/js/i18n/public/locales/pt-BR/settings.json b/js/i18n/public/locales/pt-BR/settings.json index 391c97857..f0d956b57 100644 --- a/js/i18n/public/locales/pt-BR/settings.json +++ b/js/i18n/public/locales/pt-BR/settings.json @@ -67,6 +67,7 @@ "languages": "Idiomas", "privacy-security": "Privacidade e Segurança", "streaming": "Transmissão", + "notifications": "Notificações", "sign-in": "Entrar", "log-out": "Sair", "account-greeting": "Olá, @{$handle}.", diff --git a/js/i18n/public/locales/ro-RO/common.json b/js/i18n/public/locales/ro-RO/common.json index a0f1f2766..8b7137fdb 100644 --- a/js/i18n/public/locales/ro-RO/common.json +++ b/js/i18n/public/locales/ro-RO/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "Aflați mai multe despre găzduirea proprie", "pds-selector-info": "Fiecare gazdă are propriile politici și standarde de fiabilitate. Datele dvs. ATProto se află pe gazda pe care o alegeți și le puteți migra ulterior. Notă: Streamplace are propriile reguli de moderare - accesul dvs. poate fi interzis pe Streamplace indiferent de gazda pe care o alegeți.", "pds-selector-read-policies": "Citiți Termenii și condițiile și Politica de confidențialitate ale {$label} înainte de a continua.", - "pds-selector-handle-policy-checkbox": "Am citit și sunt de acord cu politica de gestionare" + "pds-selector-handle-policy-checkbox": "Am citit și sunt de acord cu politica de gestionare", + "login-show-live-on-bluesky": "Arată când sunt live pe Bluesky", + "login-show-live-on-bluesky-description": "Adaugă inelul roșu LIVE avatarului dvs. de Bluesky în timp ce transmiteți și permite Streamplace să publice anunțuri în numele dvs. Debifați pentru a vă conecta fără a acorda niciun acces la contul dvs. de Bluesky." } diff --git a/js/i18n/public/locales/ro-RO/settings.json b/js/i18n/public/locales/ro-RO/settings.json index c81963fb0..38ee3e585 100644 --- a/js/i18n/public/locales/ro-RO/settings.json +++ b/js/i18n/public/locales/ro-RO/settings.json @@ -40,6 +40,7 @@ "languages": "Limbi", "privacy-security": "Confidențialitate și securitate", "streaming": "Streaming", + "notifications": "Notificări", "cancel": "Anulare", "create": "Creare", "delete": "Ștergere", diff --git a/js/i18n/public/locales/zh-Hans/common.json b/js/i18n/public/locales/zh-Hans/common.json index 8cf1e39bb..082dd6d61 100644 --- a/js/i18n/public/locales/zh-Hans/common.json +++ b/js/i18n/public/locales/zh-Hans/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "了解有关自组主机的更多信息", "pds-selector-info": "每个主机都有自己的政策和可靠性标准。您的 ATProto 数据存储在您选择的主机上,您可以以后进行迁移。注意:Streamplace 有自己的审核规则——无论您选择哪个主机,都可能会被 Streamplace 禁止使用。", "pds-selector-read-policies": "在继续之前,请阅读 {$label} 的服务条款和隐私政策。", - "pds-selector-handle-policy-checkbox": "我已阅读并同意处理政策" + "pds-selector-handle-policy-checkbox": "我已阅读并同意处理政策", + "login-show-live-on-bluesky": "在 Bluesky 上显示我的直播状态", + "login-show-live-on-bluesky-description": "直播时为您的 Bluesky 头像加上红色 LIVE 圆环,并允许 Streamplace 代您发布直播公告。取消勾选即可在不授予任何 Bluesky 账号访问权限的情况下登录。" } diff --git a/js/i18n/public/locales/zh-Hans/settings.json b/js/i18n/public/locales/zh-Hans/settings.json index 154437fed..9af46c5ca 100644 --- a/js/i18n/public/locales/zh-Hans/settings.json +++ b/js/i18n/public/locales/zh-Hans/settings.json @@ -41,6 +41,7 @@ "languages": "语言", "privacy-security": "隐私与安全", "streaming": "串流", + "notifications": "通知", "cancel": "取消", "create": "创建", "delete": "删除", diff --git a/js/i18n/public/locales/zh-Hant/common.json b/js/i18n/public/locales/zh-Hant/common.json index e1e1614f8..e06e5aa8b 100644 --- a/js/i18n/public/locales/zh-Hant/common.json +++ b/js/i18n/public/locales/zh-Hant/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "了解更多關於自行架設的資訊", "pds-selector-info": "每個主機都有自己的政策和可靠性標準。您的 ATProto 資料存放在您選擇的主機上,您可以之後進行遷移。注意:Streamplace 有自己的審核規則——無論您選擇哪個主機,都可能被 Streamplace 封禁。", "pds-selector-read-policies": "在繼續之前,請閱讀 {$label} 的服務條款和隱私政策。", - "pds-selector-handle-policy-checkbox": "我已閱讀並同意識別碼政策" + "pds-selector-handle-policy-checkbox": "我已閱讀並同意識別碼政策", + "login-show-live-on-bluesky": "在 Bluesky 上顯示我的直播狀態", + "login-show-live-on-bluesky-description": "直播時為您的 Bluesky 頭像加上紅色 LIVE 圓環,並允許 Streamplace 代您發布直播公告。取消勾選即可在不授予任何 Bluesky 帳號存取權限的情況下登入。" } diff --git a/js/i18n/public/locales/zh-Hant/settings.json b/js/i18n/public/locales/zh-Hant/settings.json index eb00e7699..5c5990c52 100644 --- a/js/i18n/public/locales/zh-Hant/settings.json +++ b/js/i18n/public/locales/zh-Hant/settings.json @@ -67,6 +67,7 @@ "languages": "語言", "privacy-security": "隱私與安全", "streaming": "串流", + "notifications": "通知", "sign-in": "登入", "log-out": "登出", "account-greeting": "嗨,@{$handle}。", diff --git a/js/web/public/locales/en-US/common.json b/js/web/public/locales/en-US/common.json index 9b79eea0d..8f628cec7 100644 --- a/js/web/public/locales/en-US/common.json +++ b/js/web/public/locales/en-US/common.json @@ -218,5 +218,59 @@ "no-videos-yet-upload": "No videos yet.", "optional": "Optional", "required": "Required", - "select-license": "Select a license…" + "select-license": "Select a license…", + "login-show-live-on-bluesky": "Show when I'm live on Bluesky", + "login-show-live-on-bluesky-description": "Gives your Bluesky avatar the red LIVE ring while you stream and lets Streamplace post announcements for you. Uncheck to sign in without granting any access to your Bluesky account.", + "player-play": "Play", + "player-pause": "Pause", + "player-mute": "Mute", + "player-unmute": "Unmute", + "player-seek": "Seek", + "player-settings": "Settings", + "player-fullscreen": "Fullscreen", + "player-exit-fullscreen": "Exit fullscreen", + "player-theatre": "Theatre mode", + "player-exit-theatre": "Exit theatre mode", + "player-pip": "Picture-in-picture", + "player-exit-pip": "Exit picture-in-picture", + "player-enable-danmu": "Enable danmu", + "player-disable-danmu": "Disable danmu", + "player-quality": "Quality", + "player-latency": "Latency", + "player-quality-auto": "Auto", + "player-stats": "Player stats", + "player-stats-resolution": "Resolution", + "player-stats-viewport": "Viewport", + "player-stats-bitrate": "Bitrate", + "player-stats-skipped": "Skipped", + "player-stats-buffer": "Buffer", + "player-stats-codecs": "Codecs", + "player-stats-protocol": "Protocol", + "player-stats-latency-mode": "Latency Mode", + "player-stats-render-surface": "Render Surface", + "player-stats-session": "Session", + "player-protocol-webrtc": "WebRTC", + "player-protocol-hls": "HLS", + "player-latency-low": "Low Latency", + "player-latency-standard": "Standard", + "player-error-network": "Network error", + "player-error-playback": "Playback error", + "player-error-format": "Stream format not supported", + "player-error-stream-not-live": "Stream not live", + "player-error-network-retrying": "Network error. Retrying.", + "player-error-media-recovering": "Media error. Recovering.", + "player-error-hls-unsupported": "Your browser doesn't support HLS playback.", + "player-error-stream-unavailable": "Stream unavailable. Stopped reconnecting.", + "player-error-sdp-failed": "Failed to create SDP offer", + "player-error-ice-failed": "Failed to gather ICE candidates", + "player-error-negotiation-failed": "WebRTC negotiation failed", + "player-error-stream-stalled": "Stream stalled. Reconnecting.", + "player-error-connection-lost": "Connection lost. Reconnecting.", + "upload-error-format": "File doesn't appear to be a supported video format (MP4, WebM, MKV, MOV, AVI, OGG, FLV, MPEG-TS).", + "upload-error-timeout": "Processing timed out. Please try again later.", + "upload-error-processing": "Processing failed", + "upload-error-not-logged-in": "Not logged in", + "upload-title-untitled": "Untitled", + "emoji-skin-tone": "Skin tone", + "emoji-search": "Search emoji…" } diff --git a/js/web/public/locales/en-US/settings.json b/js/web/public/locales/en-US/settings.json index 457536511..6ef4e1082 100644 --- a/js/web/public/locales/en-US/settings.json +++ b/js/web/public/locales/en-US/settings.json @@ -30,6 +30,8 @@ "currently-translating-description": "Some parts of the app may look incomplete. Thank you for your patience!", "debug-recording-title": "Allow {$host} to record your livestream for debugging and improving the service", "debug-recording-description": "Optional", + "livestream-recording-title": "Record my livestreams into VODs on {$host}", + "livestream-recording-description": "When enabled, your livestreams are saved so you can finalize and publish them as videos", "manage-keys": "Manage Keys", "settings-title": "Settings", "error": "Error", @@ -41,6 +43,12 @@ "languages": "Languages", "privacy-security": "Privacy & Security", "streaming": "Streaming", + "notifications": "Notifications", + "notifications-title": "Push Notifications", + "notifications-web-description": "Get notified when streamers you follow go live.", + "notifications-mobile-description": "Push notifications are managed by your device settings.", + "notifications-blocked-description": "Blocked — you denied notification permission in your browser.", + "notifications-blocked-help": "To re-enable, update site permissions in your browser settings, then toggle this on again.", "cancel": "Cancel", "create": "Create", "delete": "Delete", @@ -105,6 +113,7 @@ "events": "Events", "events-livestream": "Livestream Events", "events-chat": "Chat Events", + "events-stream-received": "Stream Received Events", "untitled-webhook": "Untitled Webhook", "inactive": "Inactive", "active": "Active", diff --git a/js/web/public/locales/es-ES/common.json b/js/web/public/locales/es-ES/common.json index 153dc7a0b..c4448a6fc 100644 --- a/js/web/public/locales/es-ES/common.json +++ b/js/web/public/locales/es-ES/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "Más información sobre el autoalojamiento", "pds-selector-info": "Cada servidor tiene sus propias políticas y estándares de fiabilidad. Tus datos ATProto viven en el servidor que elijas y puedes migrar más tarde. Nota: Streamplace tiene sus propias reglas de moderación; puedes ser expulsado de Streamplace independientemente del servidor que elijas.", "pds-selector-read-policies": "Lee los Términos de Servicio y la Política de Privacidad de {$label} antes de continuar.", - "pds-selector-handle-policy-checkbox": "He leído y acepto la política de identificadores" + "pds-selector-handle-policy-checkbox": "He leído y acepto la política de identificadores", + "login-show-live-on-bluesky": "Mostrar cuando estoy en directo en Bluesky", + "login-show-live-on-bluesky-description": "Añade el anillo rojo LIVE a tu avatar de Bluesky mientras transmites y permite que Streamplace publique anuncios por ti. Desmárcalo para iniciar sesión sin conceder ningún acceso a tu cuenta de Bluesky." } diff --git a/js/web/public/locales/es-ES/settings.json b/js/web/public/locales/es-ES/settings.json index d9277d763..cda42b357 100644 --- a/js/web/public/locales/es-ES/settings.json +++ b/js/web/public/locales/es-ES/settings.json @@ -67,6 +67,7 @@ "languages": "Idiomas", "privacy-security": "Privacidad y Seguridad", "streaming": "Transmisión", + "notifications": "Notificaciones", "sign-in": "Iniciar Sesión", "log-out": "Cerrar sesión", "account-greeting": "Hola, @{$handle}.", diff --git a/js/web/public/locales/fr-FR/common.json b/js/web/public/locales/fr-FR/common.json index 339d534c0..1a6809ae6 100644 --- a/js/web/public/locales/fr-FR/common.json +++ b/js/web/public/locales/fr-FR/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "En savoir plus sur l'auto-hébergement", "pds-selector-info": "Chaque hôte a ses propres politiques et normes de fiabilité. Vos données ATProto se trouvent sur l'hôte que vous choisissez et vous pouvez migrer ultérieurement. Remarque : Streamplace a ses propres règles de modération — vous pouvez être banni de Streamplace quel que soit l'hôte choisi.", "pds-selector-read-policies": "Lisez les Conditions d'utilisation et la Politique de confidentialité de {$label} avant de continuer.", - "pds-selector-handle-policy-checkbox": "J'ai lu et j'accepte la politique des identifiants" + "pds-selector-handle-policy-checkbox": "J'ai lu et j'accepte la politique des identifiants", + "login-show-live-on-bluesky": "Afficher quand je suis en direct sur Bluesky", + "login-show-live-on-bluesky-description": "Ajoute l'anneau rouge LIVE à votre avatar Bluesky pendant vos streams et permet à Streamplace de publier des annonces pour vous. Décochez la case pour vous connecter sans accorder aucun accès à votre compte Bluesky." } diff --git a/js/web/public/locales/fr-FR/settings.json b/js/web/public/locales/fr-FR/settings.json index e16c7d50d..4cdf81ac7 100644 --- a/js/web/public/locales/fr-FR/settings.json +++ b/js/web/public/locales/fr-FR/settings.json @@ -67,6 +67,7 @@ "languages": "Langues", "privacy-security": "Confidentialité et Sécurité", "streaming": "Diffusion", + "notifications": "Notifications", "sign-in": "Se connecter", "log-out": "Se déconnecter", "account-greeting": "Salut, @{$handle}.", diff --git a/js/web/public/locales/pt-BR/common.json b/js/web/public/locales/pt-BR/common.json index 2c35c532b..348dd8f80 100644 --- a/js/web/public/locales/pt-BR/common.json +++ b/js/web/public/locales/pt-BR/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "Saiba mais sobre auto-hospedagem", "pds-selector-info": "Cada host tem suas próprias políticas e padrões de confiabilidade. Seus dados ATProto ficam no host que você escolher e você pode migrar depois. Nota: O Streamplace tem suas próprias regras de moderação — você pode ser banido do Streamplace independentemente do host escolhido.", "pds-selector-read-policies": "Leia os Termos de Serviço e a Política de Privacidade de {$label} antes de continuar.", - "pds-selector-handle-policy-checkbox": "Li e concordo com a política de identificadores" + "pds-selector-handle-policy-checkbox": "Li e concordo com a política de identificadores", + "login-show-live-on-bluesky": "Mostrar quando estou ao vivo no Bluesky", + "login-show-live-on-bluesky-description": "Adiciona o anel vermelho LIVE ao seu avatar do Bluesky enquanto você transmite e permite que o Streamplace publique anúncios por você. Desmarque para entrar sem conceder nenhum acesso à sua conta do Bluesky." } diff --git a/js/web/public/locales/pt-BR/settings.json b/js/web/public/locales/pt-BR/settings.json index 391c97857..f0d956b57 100644 --- a/js/web/public/locales/pt-BR/settings.json +++ b/js/web/public/locales/pt-BR/settings.json @@ -67,6 +67,7 @@ "languages": "Idiomas", "privacy-security": "Privacidade e Segurança", "streaming": "Transmissão", + "notifications": "Notificações", "sign-in": "Entrar", "log-out": "Sair", "account-greeting": "Olá, @{$handle}.", diff --git a/js/web/public/locales/ro-RO/common.json b/js/web/public/locales/ro-RO/common.json index a0f1f2766..8b7137fdb 100644 --- a/js/web/public/locales/ro-RO/common.json +++ b/js/web/public/locales/ro-RO/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "Aflați mai multe despre găzduirea proprie", "pds-selector-info": "Fiecare gazdă are propriile politici și standarde de fiabilitate. Datele dvs. ATProto se află pe gazda pe care o alegeți și le puteți migra ulterior. Notă: Streamplace are propriile reguli de moderare - accesul dvs. poate fi interzis pe Streamplace indiferent de gazda pe care o alegeți.", "pds-selector-read-policies": "Citiți Termenii și condițiile și Politica de confidențialitate ale {$label} înainte de a continua.", - "pds-selector-handle-policy-checkbox": "Am citit și sunt de acord cu politica de gestionare" + "pds-selector-handle-policy-checkbox": "Am citit și sunt de acord cu politica de gestionare", + "login-show-live-on-bluesky": "Arată când sunt live pe Bluesky", + "login-show-live-on-bluesky-description": "Adaugă inelul roșu LIVE avatarului dvs. de Bluesky în timp ce transmiteți și permite Streamplace să publice anunțuri în numele dvs. Debifați pentru a vă conecta fără a acorda niciun acces la contul dvs. de Bluesky." } diff --git a/js/web/public/locales/ro-RO/settings.json b/js/web/public/locales/ro-RO/settings.json index c81963fb0..38ee3e585 100644 --- a/js/web/public/locales/ro-RO/settings.json +++ b/js/web/public/locales/ro-RO/settings.json @@ -40,6 +40,7 @@ "languages": "Limbi", "privacy-security": "Confidențialitate și securitate", "streaming": "Streaming", + "notifications": "Notificări", "cancel": "Anulare", "create": "Creare", "delete": "Ștergere", diff --git a/js/web/public/locales/zh-Hans/common.json b/js/web/public/locales/zh-Hans/common.json index 8cf1e39bb..082dd6d61 100644 --- a/js/web/public/locales/zh-Hans/common.json +++ b/js/web/public/locales/zh-Hans/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "了解有关自组主机的更多信息", "pds-selector-info": "每个主机都有自己的政策和可靠性标准。您的 ATProto 数据存储在您选择的主机上,您可以以后进行迁移。注意:Streamplace 有自己的审核规则——无论您选择哪个主机,都可能会被 Streamplace 禁止使用。", "pds-selector-read-policies": "在继续之前,请阅读 {$label} 的服务条款和隐私政策。", - "pds-selector-handle-policy-checkbox": "我已阅读并同意处理政策" + "pds-selector-handle-policy-checkbox": "我已阅读并同意处理政策", + "login-show-live-on-bluesky": "在 Bluesky 上显示我的直播状态", + "login-show-live-on-bluesky-description": "直播时为您的 Bluesky 头像加上红色 LIVE 圆环,并允许 Streamplace 代您发布直播公告。取消勾选即可在不授予任何 Bluesky 账号访问权限的情况下登录。" } diff --git a/js/web/public/locales/zh-Hans/settings.json b/js/web/public/locales/zh-Hans/settings.json index 154437fed..9af46c5ca 100644 --- a/js/web/public/locales/zh-Hans/settings.json +++ b/js/web/public/locales/zh-Hans/settings.json @@ -41,6 +41,7 @@ "languages": "语言", "privacy-security": "隐私与安全", "streaming": "串流", + "notifications": "通知", "cancel": "取消", "create": "创建", "delete": "删除", diff --git a/js/web/public/locales/zh-Hant/common.json b/js/web/public/locales/zh-Hant/common.json index e1e1614f8..e06e5aa8b 100644 --- a/js/web/public/locales/zh-Hant/common.json +++ b/js/web/public/locales/zh-Hant/common.json @@ -42,5 +42,7 @@ "pds-selector-learn-more": "了解更多關於自行架設的資訊", "pds-selector-info": "每個主機都有自己的政策和可靠性標準。您的 ATProto 資料存放在您選擇的主機上,您可以之後進行遷移。注意:Streamplace 有自己的審核規則——無論您選擇哪個主機,都可能被 Streamplace 封禁。", "pds-selector-read-policies": "在繼續之前,請閱讀 {$label} 的服務條款和隱私政策。", - "pds-selector-handle-policy-checkbox": "我已閱讀並同意識別碼政策" + "pds-selector-handle-policy-checkbox": "我已閱讀並同意識別碼政策", + "login-show-live-on-bluesky": "在 Bluesky 上顯示我的直播狀態", + "login-show-live-on-bluesky-description": "直播時為您的 Bluesky 頭像加上紅色 LIVE 圓環,並允許 Streamplace 代您發布直播公告。取消勾選即可在不授予任何 Bluesky 帳號存取權限的情況下登入。" } diff --git a/js/web/public/locales/zh-Hant/settings.json b/js/web/public/locales/zh-Hant/settings.json index eb00e7699..5c5990c52 100644 --- a/js/web/public/locales/zh-Hant/settings.json +++ b/js/web/public/locales/zh-Hant/settings.json @@ -67,6 +67,7 @@ "languages": "語言", "privacy-security": "隱私與安全", "streaming": "串流", + "notifications": "通知", "sign-in": "登入", "log-out": "登出", "account-greeting": "嗨,@{$handle}。", diff --git a/js/web/src/components/player/hls-player.tsx b/js/web/src/components/player/hls-player.tsx index 9a758ea1b..37d385bc7 100644 --- a/js/web/src/components/player/hls-player.tsx +++ b/js/web/src/components/player/hls-player.tsx @@ -5,6 +5,7 @@ // sibling of this file with the same shape. import Hls, { type Level } from "hls.js"; import { useEffect, useImperativeHandle, useRef, type RefObject } from "react"; +import { useTranslation } from "react-i18next"; import type { PlayerBackendHandle, PlayerStats, QualityOption } from "./player"; import { readQualityPreference } from "./player"; @@ -43,6 +44,7 @@ export function HLSPlayer({ ref?: RefObject; }) { const hlsRef = useRef(null); + const { t } = useTranslation(); // Ref so the stats-polling interval (a child of the main effect) can // call the latest onStatsChange without re-creating the interval on // every parent render. @@ -80,7 +82,7 @@ export function HLSPlayer({ hlsRef.current = hls; hls.on(Hls.Events.MANIFEST_PARSED, (_e, data) => { - onQualitiesChange?.(buildQualities(hls.levels)); + onQualitiesChange?.(buildQualities(hls.levels, t)); // Restore persisted quality preference. const saved = readQualityPreference(); if (saved !== null && saved >= -1 && saved < hls.levels.length) { @@ -99,17 +101,17 @@ export function HLSPlayer({ if (!data.fatal) return; const status = (data.response as Response | undefined)?.status; if (status === 404) { - onError?.("Stream not live"); + onError?.(t("player-error-stream-not-live")); hls.stopLoad(); return; } switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: - onError?.("Network error. Retrying."); + onError?.(t("player-error-network-retrying")); hls.startLoad(); return; case Hls.ErrorTypes.MEDIA_ERROR: - onError?.("Media error. Recovering."); + onError?.(t("player-error-media-recovering")); hls.recoverMediaError(); return; default: @@ -148,7 +150,7 @@ export function HLSPlayer({ video.load(); }; } else { - onError?.("Your browser doesn't support HLS playback."); + onError?.(t("player-error-hls-unsupported")); } }, [ src, @@ -231,13 +233,16 @@ export function HLSPlayer({ return null; } -function buildQualities(levels: Level[]): QualityOption[] { +function buildQualities( + levels: Level[], + t: (key: string) => string, +): QualityOption[] { const explicit = levels.map((level, index) => ({ index, label: labelForLevel(level, index), })); // "Auto" maps to hls.js currentLevel = -1. - return [{ index: -1, label: "Auto" }, ...explicit]; + return [{ index: -1, label: t("player-quality-auto") }, ...explicit]; } function labelForLevel(level: Level, index: number): string { diff --git a/js/web/src/components/player/player-controls.tsx b/js/web/src/components/player/player-controls.tsx index d39fc667b..933733597 100644 --- a/js/web/src/components/player/player-controls.tsx +++ b/js/web/src/components/player/player-controls.tsx @@ -14,6 +14,7 @@ import { VolumeX, } from "lucide-react"; import { type RefObject, useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useFullscreen } from "../../contexts/fullscreen-context"; import { cn } from "../../lib/utils"; import MuIcon from "../svg/mu"; @@ -95,6 +96,7 @@ export function PlayerControls({ const [settingsOpen, setSettingsOpen] = useState(false); const { theatre, setTheatre } = useFullscreen(); + const { t } = useTranslation(); // Mirror video element state into React. useEffect(() => { @@ -276,13 +278,13 @@ export function PlayerControls({ ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0", )} - aria-label="Play" + aria-label={t("player-play")} aria-hidden={!bigPlay} tabIndex={bigPlay ? 0 : -1} >
- Play + {t("player-play")}
@@ -298,7 +300,7 @@ export function PlayerControls({ ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0", )} - aria-label="Unmute" + aria-label={t("player-unmute")} aria-hidden={!showUnmutePrompt} tabIndex={showUnmutePrompt ? 0 : -1} > @@ -307,7 +309,7 @@ export function PlayerControls({ - Unmute + {t("player-unmute")} @@ -325,7 +327,7 @@ export function PlayerControls({ value={currentTime} onValueChange={onSeekInput} className="h-1 w-full min-w-full cursor-pointer sm:w-48" - aria-label="Seek" + aria-label={t("player-seek")} /> )}
@@ -333,7 +335,7 @@ export function PlayerControls({ type="button" onClick={togglePlay} className="p-1 text-white transition-colors hover:text-white/80" - aria-label={playing ? "Pause" : "Play"} + aria-label={playing ? t("player-pause") : t("player-play")} > {playing ? ( @@ -359,7 +361,7 @@ export function PlayerControls({ type="button" onClick={toggleMute} className="p-1 text-white transition-colors hover:text-white/80" - aria-label={muted ? "Unmute" : "Mute"} + aria-label={muted ? t("player-unmute") : t("player-mute")} > {muted || volume === 0 ? ( @@ -387,9 +389,7 @@ export function PlayerControls({ "p-1 transition-colors", isPiP ? "text-white" : "text-white/40 hover:text-white/80", )} - aria-label={ - isPiP ? "Exit picture-in-picture" : "Picture-in-picture" - } + aria-label={isPiP ? t("player-exit-pip") : t("player-pip")} > @@ -402,7 +402,9 @@ export function PlayerControls({ "p-1 transition-colors", theatre ? "text-white" : "text-white/40 hover:text-white/80", )} - aria-label={theatre ? "Exit theatre mode" : "Theatre mode"} + aria-label={ + theatre ? t("player-exit-theatre") : t("player-theatre") + } > @@ -414,7 +416,9 @@ export function PlayerControls({ "p-1 transition-colors", showDanmu ? "text-white" : "text-white/40 hover:text-white/80", )} - aria-label={showDanmu ? "Disable danmu" : "Enable danmu"} + aria-label={ + showDanmu ? t("player-disable-danmu") : t("player-enable-danmu") + } > @@ -422,7 +426,7 @@ export function PlayerControls({ - Latency + {t("player-latency")} onUseWebRTCChange(v === "webrtc")} @@ -455,7 +459,7 @@ export function PlayerControls({ <> - Quality + {t("player-quality")} onQualityChange(Number(v))} @@ -487,7 +491,11 @@ export function PlayerControls({ type="button" onClick={toggleFullscreen} className="p-1 text-white transition-colors hover:text-white/80" - aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"} + aria-label={ + isFullscreen + ? t("player-exit-fullscreen") + : t("player-fullscreen") + } > {isFullscreen ? ( diff --git a/js/web/src/components/player/player.tsx b/js/web/src/components/player/player.tsx index effd4ef22..e66dda344 100644 --- a/js/web/src/components/player/player.tsx +++ b/js/web/src/components/player/player.tsx @@ -13,6 +13,7 @@ import { useState, type RefObject, } from "react"; +import { useTranslation } from "react-i18next"; import { cn } from "../../lib/utils"; import { HLSPlayer } from "./hls-player"; import { PlayerControls } from "./player-controls"; @@ -152,6 +153,7 @@ export function Player({ const containerRef = useRef(null); const backendRef = useRef(null); const idleTimerRef = useRef | null>(null); + const { t } = useTranslation(); const [playing, setPlaying] = useState(false); const [hasPlayed, setHasPlayed] = useState(false); @@ -230,12 +232,12 @@ export function Player({ if (code === 1) return; surfaceError( code === 2 - ? "Network error" + ? t("player-error-network") : code === 3 - ? "Playback error" + ? t("player-error-playback") : code === 4 - ? "Stream format not supported" - : "Playback error", + ? t("player-error-format") + : t("player-error-playback"), ); }; @@ -363,8 +365,12 @@ export function Player({ {active && showStats && stats && ( )} @@ -442,10 +448,11 @@ function StatsOverlay({ sessionId, }: { stats: PlayerStats; - protocol: "HLS" | "WebRTC"; - latencyMode: "Standard" | "Low Latency"; + protocol: string; + latencyMode: string; sessionId: string; }) { + const { t } = useTranslation(); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const dragStart = useRef({ x: 0, y: 0, posX: 0, posY: 0 }); @@ -489,7 +496,7 @@ function StatsOverlay({ return (
e.stopPropagation()} @@ -509,12 +516,15 @@ function StatsOverlay({ Stats
- + - + {stats.ttfbEstimate !== undefined && stats.ttfbEstimate > 0 && ( )} @@ -525,24 +535,29 @@ function StatsOverlay({ } /> - + {stats.latencyToBroadcaster !== undefined && ( )} - {stats.codecs && } - - - + {stats.codecs && ( + + )} + + + {stats.hlsVersion && ( )} - +
); diff --git a/js/web/src/components/player/webrtc-player.tsx b/js/web/src/components/player/webrtc-player.tsx index 505c8e3fd..7ac6a481c 100644 --- a/js/web/src/components/player/webrtc-player.tsx +++ b/js/web/src/components/player/webrtc-player.tsx @@ -4,7 +4,8 @@ // shared across backends. This is a sibling of hls-player.tsx with the same // prop shape so the dispatch in PlayerBackend is a single conditional. import { useEffect, useImperativeHandle, useRef, type RefObject } from "react"; -import { type StreamplaceAgent, place } from "streamplace"; +import { useTranslation } from "react-i18next"; +import { place, type StreamplaceAgent } from "streamplace"; import { getStreamplaceUrl } from "../../lib/streamplace-url"; import type { PlayerBackendHandle, PlayerStats } from "./player"; @@ -118,6 +119,7 @@ export function WebRTCPlayer({ activeRef.current = active; onErrorRef.current = onError; onStatsChangeRef.current = onStatsChange; + const { t } = useTranslation(); useEffect(() => { if (!active) return; @@ -151,7 +153,7 @@ export function WebRTCPlayer({ reconnectAttemptsRef.current += 1; if (reconnectAttemptsRef.current > MAX_RECONNECT_ATTEMPTS) { gaveUpRef.current = true; - onErrorRef.current?.("Stream unavailable. Stopped reconnecting."); + onErrorRef.current?.(t("player-error-stream-unavailable")); return; } reconnectTimerRef.current = setTimeout(() => { @@ -172,7 +174,7 @@ export function WebRTCPlayer({ offerToReceiveVideo: true, }); if (!offer.sdp) { - onErrorRef.current?.("Failed to create SDP offer"); + onErrorRef.current?.(t("player-error-sdp-failed")); return; } @@ -181,7 +183,7 @@ export function WebRTCPlayer({ // Wait for ICE gathering to complete (or time out). const gatheredOffer = await waitForICE(pc); if (!gatheredOffer || !gatheredOffer.sdp) { - onErrorRef.current?.("Failed to gather ICE candidates"); + onErrorRef.current?.(t("player-error-ice-failed")); return; } @@ -189,7 +191,8 @@ export function WebRTCPlayer({ if (cancelled) return; // POST the offer via the WHEP endpoint. - const response = await agent.client.call(place.stream.playback.whep, + const response = await agent.client.call( + place.stream.playback.whep, gatheredOffer.sdp as any, { params: { rendition: "source", streamer }, @@ -209,7 +212,7 @@ export function WebRTCPlayer({ } catch (err) { if (cancelled) return; console.error("WebRTC negotiation failed:", err); - onErrorRef.current?.("WebRTC negotiation failed"); + onErrorRef.current?.(t("player-error-negotiation-failed")); scheduleReconnect(); } } @@ -276,7 +279,7 @@ export function WebRTCPlayer({ } if (hasReceivedFrames && now - lastChangeTime > STUCK_THRESHOLD_MS) { - onErrorRef.current?.("Stream stalled. Reconnecting."); + onErrorRef.current?.(t("player-error-stream-stalled")); scheduleReconnect(); return; } @@ -375,7 +378,7 @@ export function WebRTCPlayer({ state === "closed" || state === "disconnected" ) { - onErrorRef.current?.("Connection lost. Reconnecting."); + onErrorRef.current?.(t("player-error-connection-lost")); scheduleReconnect(); } }); diff --git a/js/web/src/components/stream/emoji-picker.tsx b/js/web/src/components/stream/emoji-picker.tsx index a2d3c45f4..3f8a9b509 100644 --- a/js/web/src/components/stream/emoji-picker.tsx +++ b/js/web/src/components/stream/emoji-picker.tsx @@ -32,6 +32,7 @@ import { useState, } from "react"; import { createPortal } from "react-dom"; +import { useTranslation } from "react-i18next"; import { useEmojiData } from "../../lib/emoji-data"; const CATEGORY_ICONS: { label: string; Icon: LucideIcon }[] = [ @@ -151,6 +152,7 @@ export function SkinToneTrigger({ setOpen, skinTone, }: SkinTonePickerOpen & { skinTone: SkinTone }) { + const { t } = useTranslation(); const [, , skinToneVariations] = useFrimousseSkinTone("👋"); const current = skinToneVariations.find((v) => v.skinTone === skinTone) ?? @@ -159,7 +161,7 @@ export function SkinToneTrigger({ return (