From ec0fa7f5d11452dd7efa079dc73192e0ad193fa7 Mon Sep 17 00:00:00 2001 From: yuanfazheng Date: Thu, 2 Jul 2026 19:32:26 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90Electron=E3=80=91update=20packages/tui?= =?UTF-8?q?kit-atomicx-vue3-electron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tuikit-atomicx-vue3-electron/package.json | 8 +- .../components/CoHostPanel/BattlePanel.vue | 545 ++++++++--- .../components/CoHostPanel/CoHostPanel.vue | 529 ++++++----- .../CoHostPanel/ConfigSettingPanel.vue | 39 +- .../CoHostPanel/ConnectionPanel.vue | 497 ++++++++-- .../CoHostPanel/RecommendHostList.vue | 40 +- .../components/CoHostPanel/battleAutoStart.ts | 57 ++ .../src/components/CoHostPanel/constants.ts | 15 + .../CoHostPanel/i18n/en-US/index.ts | 13 +- .../CoHostPanel/i18n/zh-CN/index.ts | 14 +- .../src/components/CoHostPanel/index.ts | 14 +- .../src/components/CoHostPanel/inviteMutex.ts | 62 ++ .../LiveAudienceList/LiveAudienceList.vue | 2 +- .../LiveAudienceList/LiveAudienceListH5.vue | 2 +- .../components/LiveAudienceList/constants.ts | 17 + .../src/components/LiveAudienceList/index.ts | 2 +- .../CoreViewDecorate/BattleDecorate.vue | 888 ++++++++++-------- .../CoreViewDecorate/BattleUserDecorate.vue | 24 +- .../CoreViewDecorate/LiveCoreDecorate.vue | 2 + .../src/components/LiveView/index.vue | 4 +- .../StreamMixer/LocalMixer/index.vue | 92 +- .../components/StreamMixer/MacStreamMixer.vue | 1 - .../src/report/MetricsKey.ts | 1 + .../src/subEntry/live/live.ts | 3 +- .../src/types/coHost.ts | 23 +- .../src/types/index.ts | 1 + .../src/types/liveSummary.ts | 40 + .../src/types/seat.ts | 39 +- .../src/utils/loginCoordinator.ts | 78 ++ 29 files changed, 2092 insertions(+), 960 deletions(-) create mode 100644 packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts create mode 100644 packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts create mode 100644 packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/constants.ts create mode 100644 packages/tuikit-atomicx-vue3-electron/src/types/liveSummary.ts create mode 100644 packages/tuikit-atomicx-vue3-electron/src/utils/loginCoordinator.ts diff --git a/packages/tuikit-atomicx-vue3-electron/package.json b/packages/tuikit-atomicx-vue3-electron/package.json index 7dd5c9e..17ba3ad 100644 --- a/packages/tuikit-atomicx-vue3-electron/package.json +++ b/packages/tuikit-atomicx-vue3-electron/package.json @@ -1,6 +1,6 @@ { "name": "tuikit-atomicx-vue3-electron", - "version": "5.10.0", + "version": "6.3.0", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -40,9 +40,9 @@ "@tencentcloud/lite-chat": "^1.6.4", "@tencentcloud/chat-uikit-engine-lite": "~1.0.7", "@tencentcloud/tui-core-lite": "~1.0.1", - "@tencentcloud/tuiroom-engine-electron": "~4.1.1", - "@tencentcloud/uikit-base-component-vue3": "~1.4.3", - "trtc-electron-sdk": "13.3.801-alpha.3", + "@tencentcloud/tuiroom-engine-electron": "~4.2.0", + "@tencentcloud/uikit-base-component-vue3": "~1.4.5", + "trtc-electron-sdk": "13.3.802-beta.1", "vue": "^3.4.0" }, "dependencies": { diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue index 24693ff..cd4d6cd 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/BattlePanel.vue @@ -15,6 +15,7 @@ v-if="!isUserInvited(user.userId, user.liveId)" size="small" type="primary" + :disabled="pendingInviteLiveIds.has(user.liveId) || hasPendingConnectionInvite" @click="handleSendBattleRequest(user)" > {{ t('Invite battle') }} @@ -30,93 +31,430 @@ - - - {{ t('Are you sure you want to exit the battle') }} - - - + + - - --> + + + diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue index 1761432..d762504 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/ConfigSettingPanel.vue @@ -2,9 +2,9 @@ @@ -68,7 +68,7 @@ diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue index d200b6b..93e0916 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/RecommendHostList.vue @@ -7,7 +7,7 @@
@@ -55,13 +55,33 @@ import { IconRefresh, useUIKit, TUIToast, TOAST_TYPE } from '@tencentcloud/uikit import { TUIErrorCode } from '@tencentcloud/tuiroom-engine-electron'; import { ref, onMounted, onUnmounted, computed } from 'vue'; import { useCoHostState } from '../../states/CoHostState'; -import { useLiveListState } from '../../states/LiveListState'; import { Avatar } from '../Avatar'; import { CoHostStatus } from '../../types'; const { t } = useUIKit(); -const { liveListCursor, fetchLiveList } = useLiveListState(); -const { coHostStatus, invitees, candidates } = useCoHostState(); +const { coHostStatus, connected, invitees, candidates, candidatesCursor, getCoHostCandidates } = useCoHostState(); + +/** + * Merge invitees and candidates into a unified list for rendering. + * - Exclude users that are already connected (they should not appear in "Invite more"). + * - De-duplicate by `${userId}-${liveId}` so the same user never appears twice + * when they exist in both `invitees` and `candidates`. + * Invitees come first to preserve the "already invited" visual order. + */ +const displayUserList = computed(() => { + const connectedKeys = new Set(connected.value.map(u => `${u.userId}-${u.liveId}`)); + const seen = new Set(); + const result: typeof invitees.value = []; + for (const user of [...invitees.value, ...candidates.value]) { + const key = `${user.userId}-${user.liveId}`; + if (connectedKeys.has(key) || seen.has(key)) { + continue; + } + seen.add(key); + result.push(user); + } + return result; +}); const recommendHostListContentRef = ref(null); const loadMoreRef = ref(null); @@ -69,7 +89,7 @@ let intersectionObserver: IntersectionObserver | null = null; const refreshInviteesLoading = ref(false); const loadMoreLoading = ref(false); -const hasMoreLive = computed(() => liveListCursor.value !== ''); +const hasMoreLive = computed(() => candidatesCursor.value !== ''); async function handleRefreshInvitees() { refreshInviteesLoading.value = true; if (recommendHostListContentRef.value) { @@ -77,10 +97,7 @@ async function handleRefreshInvitees() { } Promise.all([ new Promise((resolve) => setTimeout(resolve, 500)), - fetchLiveList({ - cursor: '', - count: 20, - }), + getCoHostCandidates(''), ]) .catch(error => { if (error.code === TUIErrorCode.ERR_FREQ_LIMIT) { @@ -101,10 +118,7 @@ onMounted(() => { if (item.isIntersecting && !loadMoreLoading.value && hasMoreLive.value) { loadMoreLoading.value = true; try { - await fetchLiveList({ - cursor: liveListCursor.value, - count: 20, - }); + await getCoHostCandidates(candidatesCursor.value); } catch (error) { console.error('Load more users failed:', error); } finally { diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts new file mode 100644 index 0000000..15ffbd9 --- /dev/null +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/battleAutoStart.ts @@ -0,0 +1,57 @@ +import { ref, computed } from 'vue'; + +// ---------------------------------------------------------------------------- +// Shared "battle auto-start in progress" signal between BattlePanel and +// ConnectionPanel. +// +// Background: +// A PK initiated from the Battle tab is a two-phase flow. An "Invite battle" +// first establishes a plain co-host connection (`withBattle: true`), and only +// AFTER every accepter has actually connected does BattlePanel fire ONE +// aggregated `requestBattle` (`needResponse: false`) to truly start the PK. +// Between "connection established" and "onBattleStarted" the CoHostPanel +// auto-switches to the Connection tab, whose footer shows a "Start battle" +// button. Clicking it in that window issues a SECOND `requestBattle` for the +// same round, which the SDK rejects as a duplicate and surfaces a +// "Request battle failed" toast. +// +// Fix: +// BattlePanel owns the auto-start lifecycle and keeps this flag in sync at +// every transition of its round bookkeeping (invites pending, accepters +// waiting to connect, the connection-ready safety timer armed, or the +// aggregated requestBattle in flight). ConnectionPanel reads it to disable +// its "Start battle" button (and to guard the handler) for the whole +// auto-start window, so the two `requestBattle` paths can never collide. +// Note: this intentionally does NOT block sending more "Invite battle" +// invitations to other hosts — multiple hosts may be invited at once and +// every accepter joins the same aggregated PK. +// +// Why module-level + reactive: +// The CoHostPanel dialog uses an internal `v-if`, so both panels mount and +// unmount together; a module-level Vue `ref` is the single source of truth +// that survives those cycles and is shared across both panels. Mirrors the +// same pattern used by `inviteMutex.ts`. +// +// Mirrors the identical module in +// `uikit-component-vue3/.../battleAutoStart.ts` to keep the three-end +// (Web kit / Mac kit / Win demo) PK invite flow behavior aligned. +// ---------------------------------------------------------------------------- + +const battleAutoStartInProgress = ref(false); + +// Set by BattlePanel at every transition of its auto-start bookkeeping. +export const setBattleAutoStartInProgress = (value: boolean) => { + battleAutoStartInProgress.value = value; +}; + +// Defensive reset, called when the local host leaves the co-host connection or +// the live room (and when a battle starts/ends), so a stale flag can never +// leave "Start battle" disabled into the next session. +export const resetBattleAutoStart = () => { + battleAutoStartInProgress.value = false; +}; + +export const useBattleAutoStart = () => { + const isBattleAutoStartInProgress = computed(() => battleAutoStartInProgress.value); + return { isBattleAutoStartInProgress }; +}; diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts index 2a67d0e..1d8488c 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/constants.ts @@ -1,3 +1,18 @@ export const ERROR_MESSAGE = { 100412: 'there is no one valid room for battle', }; + +/** + * Default timeout (in seconds) for outbound co-host connection invitations + * dispatched via `requestHostConnection`. The same value is mirrored into + * `extensionInfo` so the invitee can render a matching countdown. + */ +export const COHOST_REQUEST_TIMEOUT_SECONDS = 30; + +/** + * Default timeout (in seconds) for outbound battle (PK) invitations + * dispatched via `requestBattle`. Kept as a separate constant from + * `COHOST_REQUEST_TIMEOUT_SECONDS` so the two flows can diverge later + * without affecting each other. + */ +export const BATTLE_REQUEST_TIMEOUT_SECONDS = 30; diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts index 3007f95..4884c0b 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/en-US/index.ts @@ -1,7 +1,5 @@ export const resource = { 'Host Battle': 'Host Battle', - 'Host Connection': 'Host Connection', - 'CoHost': 'Co-host', 'Current seat': 'Current seat', 'Seat is empty': 'Seat is empty', 'Invite more': 'Invite more', @@ -16,6 +14,10 @@ export const resource = { 'Exit connection': 'Exit connection', 'No hosts available to invite': 'No hosts available to invite', 'Disconnect': 'Disconnect', + // Co-host audio mute toggle. Dedicated keys so they never collide with the + // global 'Mute'/'Unmute' used by the audience chat-ban menu. + 'Mute Audio': 'Mute Audio', + 'Unmute Audio': 'Unmute Audio', 'Cancel': 'Cancel', 'Invite': 'Invite', 'Inviting': 'Inviting', @@ -30,8 +32,9 @@ export const resource = { 'Are you sure you want to exit the connection': 'Are you sure you want to exit the connection?', 'Layout Template': 'Layout Template', 'Dynamic Grid Layout': 'Dynamic Grid Layout', - 'Dynamic Grid9 Layout': 'Dynamic Grid9 Layout', + 'Dynamic Grid9 Layout': 'Dynamic Grid Layout', 'Dynamic 1v6 Layout': 'Dynamic 1v6 Layout', + 'Landscape Fixed 2 Seats Layout': 'Landscape Fixed 2 Seats Layout', // Toast messages with specific error codes 'Send co-host request failed, Room not exist': 'Send co-host request failed, Room does not exist', 'Send co-host request failed, Room is connecting': 'Send co-host request failed, Room is connecting', @@ -41,8 +44,6 @@ export const resource = { 'Co-host invitation sent to user': 'Co-host invitation sent', 'Co-host invitation cancelled for user': 'Co-host invitation cancelled', 'Co-host request cancelled by user': 'Co-host request cancelled', - 'Co-host request rejected by user': 'Co-host request rejected', - 'Co-host request timeout for user': 'Co-host request timeout', 'Co-host user joined event': 'Co-host user joined', 'Co-host user left event': '{{ userName }} Co-host user left', // Battle Panel content @@ -72,8 +73,6 @@ export const resource = { 'Send battle request failed': 'Send battle request failed', 'Battle invitation sent to user': 'PK invitation has been sent to {{ userName }}', 'Battle request cancelled by user': '{{ userName }} canceled the PK request', - 'Battle request rejected by user': '{{ userName }} rejected the PK request', - 'Battle request timeout for user': 'PK request timeout for {{ userName }}', 'Anchor battle settings': 'Anchor battle settings', 'Battle duration': 'Battle duration', 'Number minutes': '{{number}} minutes', diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts index 328a182..ed6cd4b 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/i18n/zh-CN/index.ts @@ -1,7 +1,6 @@ export const resource = { 'Host Battle': '主播 PK', 'Host Connection': '主播连线', - 'CoHost': '连主播', 'No application for co-host': '暂无连主播申请', 'Current seat': '当前麦位', 'Seat is empty': '麦位为空', @@ -17,6 +16,10 @@ export const resource = { 'Start battle': '发起 PK', 'Exit connection': '退出连线', 'Disconnect': '断开连接', + // Co-host audio mute toggle. Dedicated keys so they never collide with the + // global 'Mute'/'Unmute' used by the audience chat-ban menu. + 'Mute Audio': '静音', + 'Unmute Audio': '取消静音', 'Cancel': '取消', 'Invite': '邀请', 'Inviting': '邀请中', @@ -31,8 +34,9 @@ export const resource = { 'Are you sure you want to exit the connection': '确定要退出连线吗?', 'Layout Template': '布局模板', 'Dynamic Grid Layout': '动态网格布局', - 'Dynamic Grid9 Layout': '动态宫格布局', + 'Dynamic Grid9 Layout': '动态九宫格布局', 'Dynamic 1v6 Layout': '动态1v6布局', + 'Landscape Fixed 2 Seats Layout': '横屏 2 人固定布局', // Toast messages with specific error codes 'Send co-host request failed, Room not exist': '发起连线失败,对方主播房间不存在', 'Send co-host request failed, Room is connecting': '发起连线失败,对方主播正在建立连线中', @@ -41,8 +45,6 @@ export const resource = { // Success messages with parameters - using simple strings for now 'Co-host invitation sent to user': '已向{{ userName }}发送连线邀请', 'Co-host request cancelled by user': '{{ userName }}取消了连线请求', - 'Co-host request rejected by user': '{{ userName }}已拒绝连线', - 'Co-host request timeout for user': '发给{{ userName }}的连线请求无应答', 'Co-host user joined event': '{{ userName }}已加入连线', 'Co-host user left event': '{{ userName }}已离开连线', // Battle Panel content @@ -72,9 +74,7 @@ export const resource = { 'Send battle request failed': '发起PK失败', 'Battle invitation sent to user': '已向{{ userName }}发送PK邀请', 'Battle request cancelled by user': '{{ userName }}取消了PK请求', - 'Battle request rejected by user': '{{ userName }}已拒绝PK', - 'Battle request timeout for user': '发给{{ userName }}的PK请求无应答', - 'Anchor battle settings': '主播PK设置', + 'Anchor battle settings': '主播 PK 设置', 'Battle duration': '发起 PK 时长', 'Number minutes': '{{number}} 分钟', 'Connection Layout': '连线布局', diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts index 5e0857e..d6ad132 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/index.ts @@ -1,7 +1,7 @@ -// import CoHostPanelPC from './CoHostPanel.vue'; -// import { addI18n } from '../../i18n'; -// import { enResource, zhResource } from './i18n'; -// addI18n('en-US', { translation: enResource }); -// addI18n('zh-CN', { translation: zhResource }); -// const CoHostPanel = CoHostPanelPC; -// export { CoHostPanel }; +import CoHostPanelPC from './CoHostPanel.vue'; +import { addI18n } from '../../i18n'; +import { enResource, zhResource } from './i18n'; +addI18n('en-US', { translation: enResource }); +addI18n('zh-CN', { translation: zhResource }); +const CoHostPanel = CoHostPanelPC; +export { CoHostPanel }; diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts new file mode 100644 index 0000000..0e4606c --- /dev/null +++ b/packages/tuikit-atomicx-vue3-electron/src/components/CoHostPanel/inviteMutex.ts @@ -0,0 +1,62 @@ +import { ref, computed, type Ref, type ComputedRef } from 'vue'; +import type { SeatUserInfo } from '../../types'; + +// ---------------------------------------------------------------------------- +// Mutual exclusion between the two CoHost invite kinds: "Invite battle" (PK) +// and "Invite connection" (plain co-host). +// +// Background: +// Both invites are issued through the SAME SDK call (`requestHostConnection`) +// and are only distinguished by `extensionInfo.withBattle`. Both pending +// invites land in the SAME authoritative, reactive `invitees` list, which +// does NOT carry the `withBattle` flag. So the invite kind cannot be told +// apart from `invitees` alone and must be recorded separately. +// +// Rule: +// Once a PK invite is outstanding, ALL "Invite connection" buttons are +// disabled (but more PK invites may still be sent), and vice-versa. The two +// kinds are mutually exclusive globally; the same kind may be sent multiple +// times. When every invite of a kind settles (accept / reject / cancel / +// timeout / join), `invitees` empties out and the mutual exclusion lifts +// automatically. +// +// Why module-level + reactive: +// The parent CoHostPanel dialog mounts BattlePanel and ConnectionPanel at the +// same time but uses an internal `v-if`, so both tabs must share one source +// of truth that also survives the dialog's mount/unmount cycles. A +// module-level Vue `ref` satisfies both. +// +// Mirrors `uikit-component-vue3/.../CoHostPanel/inviteMutex.ts` to keep the +// three-end (Web kit / Mac kit / Win demo) invite behavior aligned. +// ---------------------------------------------------------------------------- + +export type InviteType = 'battle' | 'connection'; + +// Module-level reactive map: liveId -> the invite kind last sent to it. +// Created once on module evaluation and shared by every panel instance. +const inviteTypeByLiveId = ref>(new Map()); + +// Record the invite kind sent to a given liveId. We replace the whole Map so +// Vue reactivity reliably observes the mutation. Stale entries are harmless: +// the derived flags below intersect this map with the authoritative `invitees` +// list, so an entry whose invite has already settled is naturally filtered out. +export const markInviteType = (liveId: string, type: InviteType) => { + const next = new Map(inviteTypeByLiveId.value); + next.set(liveId, type); + inviteTypeByLiveId.value = next; +}; + +// Derive the two mutual-exclusion flags from the authoritative, reactive +// `invitees` list. A pending invite counts as a battle (resp. connection) +// invite only when its liveId was tagged accordingly. +export const useInviteMutex = ( + invitees: Ref | ComputedRef +) => { + const hasPendingBattleInvite = computed(() => + invitees.value.some(user => inviteTypeByLiveId.value.get(user.liveId) === 'battle') + ); + const hasPendingConnectionInvite = computed(() => + invitees.value.some(user => inviteTypeByLiveId.value.get(user.liveId) === 'connection') + ); + return { hasPendingBattleInvite, hasPendingConnectionInvite }; +}; diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceList.vue b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceList.vue index 8266010..a1a2902 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceList.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceList.vue @@ -80,7 +80,7 @@ import { useLoginState } from '../../states/LoginState'; import { Avatar } from '../Avatar'; import UserActionMenu from './UserActionMenu.vue'; import type { AudienceInfo } from '../../types'; -import { MAX_AUDIENCE_COUNT } from './index'; +import { MAX_AUDIENCE_COUNT } from './constants'; const { t } = useUIKit(); const currentViewerTarget = ref(null); diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceListH5.vue b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceListH5.vue index 51323e1..0587263 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceListH5.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/LiveAudienceListH5.vue @@ -30,7 +30,7 @@ import { useLiveAudienceState } from '../../states/LiveAudienceState'; import { type AudienceInfo } from '../../types'; import { useUIKit } from '@tencentcloud/uikit-base-component-vue3'; import { Avatar } from '../Avatar'; -import { MAX_AUDIENCE_COUNT } from './index'; +import { MAX_AUDIENCE_COUNT } from './constants'; const { t } = useUIKit(); const currentViewerTarget = ref(null); diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/constants.ts b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/constants.ts new file mode 100644 index 0000000..fab3514 --- /dev/null +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/constants.ts @@ -0,0 +1,17 @@ +/** + * Shared constants for the LiveAudienceList component. + * + * Kept as a dependency-free leaf module so that both the barrel (`index.ts`) + * and the component SFCs can import these constants without creating a + * circular dependency. The previous `*.vue -> ./index` reverse import formed + * an `index.ts <-> *.vue` cycle, which made the bundler interleave the + * barrel body between the component definitions and triggered a + * "Cannot access '...' before initialization" (TDZ) error in production. + * + * This is the Electron-package mirror of the same fix originally landed for + * the Web package in commit `059e74bfe` (`tuikit-atomicx-vue3`). Do NOT move + * these constants back into `index.ts`. + */ + +/** Maximum number of audiences shown in the live audience list. */ +export const MAX_AUDIENCE_COUNT = 200; diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/index.ts b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/index.ts index d9a775a..c3f8fc8 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/index.ts +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveAudienceList/index.ts @@ -3,8 +3,8 @@ import LiveAudienceListH5 from './LiveAudienceListH5.vue'; import { addI18n } from '../../i18n'; import { enResource, zhResource } from './i18n'; import { isMobile } from '../../utils/environment'; +import { MAX_AUDIENCE_COUNT } from './constants'; -const MAX_AUDIENCE_COUNT = 200; const LiveAudienceList = isMobile ? LiveAudienceListH5 : LiveAudienceListPC; addI18n('en-US', { translation: enResource }); diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleDecorate.vue b/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleDecorate.vue index 461cd27..0585b3c 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleDecorate.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleDecorate.vue @@ -1,417 +1,475 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleUserDecorate.vue b/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleUserDecorate.vue index 36dbeb4..c308c06 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleUserDecorate.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/CoreViewDecorate/BattleUserDecorate.vue @@ -27,7 +27,7 @@ import { SeatUserInfo, CoHostLayoutTemplate } from '../../../types'; import { useBattleState } from '../../../states/BattleState'; import { useLiveListState } from '../../../states/LiveListState'; import { useCoHostState } from '../../../states/CoHostState'; -import { ref, computed, watch } from 'vue'; +import { computed } from 'vue'; import BattleTopBadge from '../assets/svg/BattleTopBadge.svg'; import BattleOrdinaryBadge from '../assets/svg/BattleOrdinaryBadge.svg'; import { useUIKit } from '@tencentcloud/uikit-base-component-vue3'; @@ -48,7 +48,15 @@ const { currentLive } = useLiveListState(); const { connected } = useCoHostState(); const { currentBattleInfo, battleScore } = useBattleState(); -const isInBattle = ref(false); +// The PK badge overlay is gated strictly on an active battle: it appears only +// after `onBattleStarted` populates `currentBattleInfo.battleId`, and hides the +// moment the battle ends (battleId cleared). Driving this directly off +// `battleId` avoids showing the overlay on a mere invitee-accept (which fills +// `battleScore` via `onUserJoinBattle` before the battle officially starts) and +// prevents stale state from a previous battle leaking into the next one. +const isInBattle = computed( + () => currentBattleInfo.value?.battleId !== null && currentBattleInfo.value?.battleId !== undefined +); const showBattleUserDecorate = computed(() => { const showUserDecorateInGrid = currentLive.value?.layoutTemplate === CoHostLayoutTemplate.HostDynamicGrid; @@ -65,18 +73,6 @@ function getBattleLevel(userId: string) { return currentBattleScoreList.value.indexOf(battleScore.value.get(userId) || 0) + 1; }; -let battleTimer: NodeJS.Timeout | null = null; -watch(() => currentBattleInfo.value?.battleId, (newVal) => { - if(newVal !== null && newVal !== undefined) { - isInBattle.value = true; - } else { - if(battleTimer) return; - battleTimer = setTimeout(() => { - isInBattle.value = false; - }, 5000); - } -}, { immediate: true }); - diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/index.vue b/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/index.vue index f904b02..1742c1f 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/index.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/LiveView/index.vue @@ -206,8 +206,8 @@ function handleLandscapeVideoLayoutForAudioConnect(layoutList: any[]) { audioLayoutTemplate.push({ x: 20, y: 140, w: 150, h: 150 }); } else { audioLayoutTemplate.push({ x: 20, y: 510, w: 120, h: 120 }); - audioLayoutTemplate.push({ x: 20, y: 380, w: 120, h: 120 }); - audioLayoutTemplate.push({ x: 20, y: 250, w: 120, h: 120 }); + audioLayoutTemplate.push({ x: 20, y: 385, w: 120, h: 120 }); + audioLayoutTemplate.push({ x: 20, y: 260, w: 120, h: 120 }); } for (let i = 1; i < layoutList.length && (i - 1) < audioLayoutTemplate.length; ++i) { diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/LocalMixer/index.vue b/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/LocalMixer/index.vue index a01e93c..639221e 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/LocalMixer/index.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/LocalMixer/index.vue @@ -34,7 +34,9 @@ import TUIRoomEngine, { import { useUIKit } from '@tencentcloud/uikit-base-component-vue3'; import { TRTCStreamLayoutMode } from 'trtc-electron-sdk'; import { useRoomEngine } from '../../../hooks/useRoomEngine'; +import { useCoHostState } from '../../../states/CoHostState'; import { useLiveListState } from '../../../states/LiveListState'; +import { useSeatStore } from '../../../states/SeatStore'; import { useVideoMixerState } from '../../../states/VideoMixerState'; import { LiveOrientation } from '../../../types'; import { debounce } from '../../../utils/utils'; @@ -43,6 +45,19 @@ import MixerControl from './MixerControl.vue'; const { t } = useUIKit(); const { currentLive } = useLiveListState(); +const { seatList } = useSeatStore(); +const { connected: coHostConnectedList } = useCoHostState(); + +// CoGuest (audience mic-link) participants, counted by occupied seats. +const coGuestSeatCount = computed(() => seatList.value.filter(seat => !!seat.userInfo).length); +// CoHost (cross-room PK / connection) participants. The list includes the local +// host once connected, so it is empty / 1 while broadcasting alone. +const coHostConnectedCount = computed(() => coHostConnectedList.value.length); +// Whether the local video shares the canvas with others. Mirrors the Windows +// behavior: Fit when the host broadcasts alone, Fill once others join. +const isMultiPersonLayout = computed( + () => coGuestSeatCount.value > 1 || coHostConnectedCount.value > 1, +); const mixControlRef = ref | null>(null); const { @@ -248,41 +263,50 @@ onMounted(() => { } }); - watch(() => currentLive.value?.layoutTemplate, (newVal) => { - if (newVal !== null && newVal !== undefined) { - let fillMode; - if (newVal < 200 || newVal >= 600) { - // Portrait template - fillMode = TRTCVideoFillMode.TRTCVideoFillMode_Fill; - } else { - // Landscape template - fillMode = TRTCVideoFillMode.TRTCVideoFillMode_Fit; - } - - const mixerView = document.getElementById('local-video-mixer'); - if (!mixerView) { - return; - } - const width = mixerView.offsetWidth; - const height = mixerView.offsetHeight; - - const mediaSourceManager = roomEngine.instance?.getTRTCCloud().getMediaMixingManager(); - mediaSourceManager?.setStreamLayout({ - layoutMode: TRTCStreamLayoutMode.Custom, - userList: [{ - userId: '', - fillMode, - rect: { - left: 0, - top: 0, - right: width, - bottom: height, - }, - zOrder: 1, - }], - }); + // Apply the local-mixer stream layout, aligning the fill mode with Windows: + // Fit when broadcasting alone (whole frame, letterboxed) and Fill once others + // join via CoHost (cross-room PK / connection) or CoGuest (audience mic-link), + // so the host tile fills its cell without black bars. + const applyStreamLayout = () => { + const layoutTemplate = currentLive.value?.layoutTemplate; + if (layoutTemplate === null || layoutTemplate === undefined) { + return; } - }); + + const fillMode = isMultiPersonLayout.value + ? TRTCVideoFillMode.TRTCVideoFillMode_Fill + : TRTCVideoFillMode.TRTCVideoFillMode_Fit; + + const mixerView = document.getElementById('local-video-mixer'); + if (!mixerView) { + return; + } + const width = mixerView.offsetWidth; + const height = mixerView.offsetHeight; + + const mediaSourceManager = roomEngine.instance?.getTRTCCloud().getMediaMixingManager(); + mediaSourceManager?.setStreamLayout({ + layoutMode: TRTCStreamLayoutMode.Custom, + userList: [{ + userId: '', + fillMode, + rect: { + left: 0, + top: 0, + right: width, + bottom: height, + }, + zOrder: 1, + }], + }); + }; + + watch( + () => [currentLive.value?.layoutTemplate, isMultiPersonLayout.value], + () => { + applyStreamLayout(); + }, + ); }); onBeforeUnmount(async () => { diff --git a/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/MacStreamMixer.vue b/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/MacStreamMixer.vue index 93823b6..bd1bc5a 100644 --- a/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/MacStreamMixer.vue +++ b/packages/tuikit-atomicx-vue3-electron/src/components/StreamMixer/MacStreamMixer.vue @@ -1,4 +1,3 @@ -