Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Web/web-vite-vue3/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tencentcloud/live-uikit-vue",
"version": "6.2.0",
"version": "6.3.0",
"scripts": {
"dev": "vite --force",
"dev:business": "cross-env STYLE_PRESET=business vite --force",
Expand All @@ -18,16 +18,16 @@
"lint": "./node_modules/.bin/eslint ./src --no-error-on-unmatched-pattern"
},
"dependencies": {
"@tencentcloud/tuiroom-engine-js": "~4.1.2-beta.2",
"@tencentcloud/uikit-base-component-vue3": "1.4.4",
"@tencentcloud/tuiroom-engine-js": "~4.2.0",
"@tencentcloud/uikit-base-component-vue3": "1.4.5",
"@tencentcloud/universal-api": "^2.0.9",
"axios": "^0.27.2",
"js-cookie": "^3.0.1",
"mitt": "^3.0.0",
"pinia": "^2.0.13",
"qs": "^6.10.3",
"rtc-detect": "^1.0.3",
"tuikit-atomicx-vue3": "6.2.0",
"tuikit-atomicx-vue3": "6.3.0",
"vue": "^3.2.25",
"vue-i18n": "^9.10.2",
"vue-router": "^4.0.14"
Expand Down
54 changes: 25 additions & 29 deletions Web/web-vite-vue3/src/TUILiveKit/LivePusherView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
</div>
</div>
<div
v-if="isToolsExpanded"
v-show="isToolsExpanded"
class="main-left-bottom-tools"
>
<CoGuestButton />
Expand Down Expand Up @@ -161,15 +161,7 @@
{{ t('End live') }}
</TUIButton>
<TUIButton
v-if="currentBattleInfo?.battleId"
type="primary"
color="red"
@click="handleEndBattle"
>
{{ t('End battle') }}
</TUIButton>
<TUIButton
v-else-if="coHostStatus === CoHostStatus.Connected"
v-if="coHostStatus === CoHostStatus.Connected && !currentBattleInfo?.battleId"
type="primary"
color="red"
@click="handleExitConnection"
Expand All @@ -183,8 +175,8 @@
</template>

<script lang="ts" setup>
import { computed, defineProps, ref, onMounted, onUnmounted } from 'vue';
import TUIRoomEngine, { TUISeatMode } from '@tencentcloud/tuiroom-engine-js';
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { TUISeatMode } from '@tencentcloud/tuiroom-engine-js';
import {
IconArrowStrokeBack,
TUIDialog,
Expand All @@ -202,6 +194,7 @@ import {
LiveAudienceList,
BarrageList,
BarrageInput,
useBarrageState,
useLiveListState,
useLiveAudienceState,
useLoginState,
Expand All @@ -216,6 +209,8 @@ import {
LiveListEvent,
LiveEndedReason,
LiveListEventInfo,
BarrageEvent,
Barrage,
} from 'tuikit-atomicx-vue3';
import CoGuestButton from './component/CoGuestButton.vue';
import CoHostButton from './component/CoHostButton.vue';
Expand Down Expand Up @@ -257,13 +252,15 @@ const rtcSupportChecked = ref(false);
const isToolsExpanded = ref(true);
const exitLiveDialogVisible = ref(false);
const { loginUserInfo } = useLoginState();
const { currentLive, startLive, endLive, joinLive, subscribeEvent, unsubscribeEvent, updateLiveInfo } = useLiveListState();
const { currentLive, startLive, endLive, joinLive, subscribeEvent: subscribeLiveListEvent, unsubscribeEvent: unsubscribeLiveListEvent, updateLiveInfo } = useLiveListState();
const roomEngine = useRoomEngine();
const { audienceCount } = useLiveAudienceState();
const { openLocalMicrophone } = useDeviceState();
const { coHostStatus, exitHostConnection } = useCoHostState();
const { currentBattleInfo, exitBattle } = useBattleState();
const { currentBattleInfo } = useBattleState();
const { connected: coGuestConnected } = useCoGuestState();
const { subscribeEvent: subscribeBarrageEvent, unsubscribeEvent: unsubscribeBarrageEvent} = useBarrageState();

const isInLive = computed(() => !!currentLive.value?.liveId);
const loading = ref(false);
const liveParamsEditForm = ref({
Expand All @@ -284,7 +281,7 @@ const liveParams = computed(() => ({

const endLiveDialogMessage = computed(() => {
if (currentBattleInfo.value?.battleId) {
return t('Currently in PK state, do you need to "end PK" or "end live broadcast"');
return t('You are currently live streaming and in a PK battle. Are you sure you want to exit?');
}
if (coHostStatus.value === CoHostStatus.Connected) {
return t('Currently connected, do you need to "exit connection" or "end live broadcast"');
Expand Down Expand Up @@ -434,18 +431,6 @@ const handleEndLive = async () => {
throw error;
}
};
const handleEndBattle = async () => {
if (!currentBattleInfo.value?.battleId) {
exitLiveDialogVisible.value = false;
return;
}
exitLiveDialogVisible.value = false;
try {
await exitBattle({ battleId: currentBattleInfo.value?.battleId });
} catch (error) {
console.error('[LivePusherView] exitBattle from end-live dialog failed', error);
}
};
const handleExitConnection = async () => {
if (coHostStatus.value === CoHostStatus.Disconnected) {
exitLiveDialogVisible.value = false;
Expand Down Expand Up @@ -483,8 +468,18 @@ const handleLiveEnded = (eventInfo: LiveListEventInfo) => {
});
};

const handleCustomMessageReceived = (barrage: Barrage) => {
if (barrage.businessId === 'violation_alert') {
TUIToast.warning({
message: t('The current display or content may pose a violation risk. Please be aware of the platform regulations'),
duration: 3000,
});
}
};

onMounted(async () => {
subscribeEvent(LiveListEvent.onLiveEnded, handleLiveEnded);
subscribeLiveListEvent(LiveListEvent.onLiveEnded, handleLiveEnded);
subscribeBarrageEvent(BarrageEvent.onCustomMessageReceived, handleCustomMessageReceived)
// Guard the pusher entry against browsers that cannot push video.
// We intentionally subscribe to the live-end event first so an
// allowed user never misses an event during the (cached) probe.
Expand All @@ -502,7 +497,8 @@ onMounted(async () => {
});

onUnmounted(() => {
unsubscribeEvent(LiveListEvent.onLiveEnded, handleLiveEnded);
unsubscribeLiveListEvent(LiveListEvent.onLiveEnded, handleLiveEnded);
unsubscribeBarrageEvent(BarrageEvent.onCustomMessageReceived, handleCustomMessageReceived);
updateLiveInfo({ layoutTemplate: 0 });
});
</script>
Expand Down
7 changes: 0 additions & 7 deletions Web/web-vite-vue3/src/TUILiveKit/component/LayoutSwitch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import DynamicGrid9 from '../icons/dynamic-grid9.vue';
import Fixed1v6 from '../icons/fixed-1v6.vue';
import FixedGrid9 from '../icons/fixed-grid9.vue';
import HorizontalFloat from '../icons/horizontal-float.vue';
import Horizontal1v1 from '../icons/horizontal-1v1.vue';

const { t } = useUIKit();
const { currentLive, updateLiveInfo } = useLiveListState();
Expand Down Expand Up @@ -109,12 +108,6 @@ const horizontalLayoutOptions = computed(() => [
templateId: TUISeatLayoutTemplate.LandscapeDynamic_1v3,
label: t('Landscape Template'),
},
{
id: 'LandscapeDynamic_1v1',
icon: Horizontal1v1,
templateId: TUISeatLayoutTemplate.LandscapeDynamic_1v1,
label: t('Landscape 1v1 Layout'),
},
]);

const layoutOptions = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<template></template>

<script setup lang="ts">
import { onMounted, onUnmounted, watch } from 'vue';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { TOAST_TYPE, TUIToast, useUIKit } from '@tencentcloud/uikit-base-component-vue3';
import { useLiveListState, useCoHostState, CoHostEvent, SeatUserInfo, useLiveSeatState, useLoginState, useBattleState, CoHostStatus, useCoGuestState, BattleEvent, BattleEventInfoMap } from 'tuikit-atomicx-vue3';
import { showNotification, hideNotification } from '../base-component/Notification';
import { BATTLE_REQUEST_TIMEOUT_SECONDS } from '../constants';

const { loginUserInfo } = useLoginState();
const { currentLive } = useLiveListState();
Expand Down Expand Up @@ -33,7 +34,7 @@ function safeJsonParse(extensionInfo: string) {
}

watch(() => coGuestApplicants.value.length, () => {
if(coGuestApplicants.value.length > 0 && (applicant.value || invitees.value.length > 0 || connected.value.length > 0)) {
if (coGuestApplicants.value.length > 0 && (applicant.value || invitees.value.length > 0 || connected.value.length > 0)) {
coGuestApplicants.value.forEach((item) => {
rejectApplication({
userId: item.userId,
Expand All @@ -44,14 +45,14 @@ watch(() => coGuestApplicants.value.length, () => {
});

const handleCoHostRequestReceived = async ({ inviter, extensionInfo }: { inviter: SeatUserInfo, extensionInfo: string }) => {
if(coGuestApplicants.value.length > 0) {
if (coGuestApplicants.value.length > 0) {
rejectHostConnection({
liveId: inviter.liveId,
liveId: inviter.liveId,
});
return;
}
const hasMoreSeatUser = seatList.value.filter((item) => item.userInfo?.userId).length > 1;
const allSeatUserInCoGuest = seatList.value.filter((item) => item.userInfo?.liveId !== currentLive.value?.liveId).length === 0;
const hasMoreSeatUser = seatList.value.filter(item => item.userInfo?.userId).length > 1;
const allSeatUserInCoGuest = seatList.value.filter(item => item.userInfo?.liveId !== currentLive.value?.liveId).length === 0;
if (hasMoreSeatUser && allSeatUserInCoGuest) {
await rejectHostConnection({
liveId: inviter.liveId,
Expand All @@ -62,7 +63,7 @@ const handleCoHostRequestReceived = async ({ inviter, extensionInfo }: { inviter
const isBattle = extensionInfoObj.withBattle;
showNotification({
cancelText: t('Reject'),
message: isBattle? t('Received battle invitation from userName', { userName: inviter.userName || inviter.userId }) : t('Co-host request received from user', { userName: inviter.userName || inviter.userId }),
message: isBattle ? t('Received battle invitation from userName', { userName: inviter.userName || inviter.userId }) : t('Co-host request received from user', { userName: inviter.userName || inviter.userId }),
duration: extensionInfoObj.timeout,
onAccept: () => {
acceptHostConnection({
Expand Down Expand Up @@ -109,31 +110,39 @@ const handleUserExitBattle = (eventInfo: { battleId: string, battleUser: SeatUse
}
};

const pendingBattleId = ref('');

const onBattleRequestReceived = (eventInfo: { battleId: string, inviter: SeatUserInfo, invitee: SeatUserInfo }) => {
hideNotification();
pendingBattleId.value = eventInfo.battleId;
showNotification({
cancelText: t('Reject'),
message: t('Received battle invitation from userName', { userName: eventInfo.inviter.userName || eventInfo.inviter.userId}),
message: t('Received battle invitation from userName', { userName: eventInfo.inviter.userName || eventInfo.inviter.userId }),
duration: BATTLE_REQUEST_TIMEOUT_SECONDS,
onAccept: () => {
acceptBattle({
battleId: eventInfo.battleId,
});
pendingBattleId.value = '';
hideNotification();
},
onCancel: () => {
rejectBattle({
battleId: eventInfo.battleId,
});
pendingBattleId.value = '';
hideNotification();
},
onTimeout: () => {
pendingBattleId.value = '';
hideNotification();
},
});
};

const onBattleRequestCancelled = (eventInfo: { battleId: string, inviter: SeatUserInfo, invitee: SeatUserInfo }) => {
TUIToast({ type: TOAST_TYPE.INFO, message: t('Battle request cancelled by user', { userName: eventInfo.invitee.userName || eventInfo.invitee.userId}) });
TUIToast({ type: TOAST_TYPE.INFO, message: t('Battle request cancelled by user', { userName: eventInfo.inviter.userName || eventInfo.inviter.userId }) });
pendingBattleId.value = '';
hideNotification();
};

Expand All @@ -147,6 +156,11 @@ let timeoutUsers: string[] = [];
let timeoutTimer: NodeJS.Timeout | null = null;

const onBattleRequestTimeout = (eventInfo: { battleId: string, inviter: SeatUserInfo, invitee: SeatUserInfo }) => {
// Only the inviter should be notified that the PK request got no response.
// The invitee that ignored the request must not get this extra toast.
if (eventInfo.inviter.userId !== loginUserInfo.value?.userId) {
return;
}
timeoutUsers.push(eventInfo.invitee.userName || eventInfo.invitee.userId);

if (timeoutTimer) {
Expand All @@ -157,12 +171,12 @@ const onBattleRequestTimeout = (eventInfo: { battleId: string, inviter: SeatUser
if (timeoutUsers.length === 1) {
TUIToast({
type: TOAST_TYPE.INFO,
message: t('Battle request timeout for user', { userName: timeoutUsers[0] })
message: t('Battle request timeout for user', { userName: timeoutUsers[0] }),
});
} else {
TUIToast({
type: TOAST_TYPE.INFO,
message: t('Battle request timeout for multiple users', { userName: timeoutUsers.join('、') })
message: t('Battle request timeout for multiple users', { userName: timeoutUsers.join('、') }),
});
}

Expand All @@ -171,6 +185,31 @@ const onBattleRequestTimeout = (eventInfo: { battleId: string, inviter: SeatUser
}, 500);
};

watch(coHostStatus, (status) => {
if (status === CoHostStatus.Disconnected) {
if (pendingBattleId.value) {
rejectBattle({ battleId: pendingBattleId.value });
pendingBattleId.value = '';
}
hideNotification();
}
});

// Hide any pending invitation popup once the local user leaves/ends the live.
// The popup is rendered imperatively into document.body and is fully decoupled
// from reactive state, so it would otherwise stay on screen after the room is
// gone. currentLive.liveId is reset to '' on leaveLive/endLive, so we hide the
// popup on that truthy -> falsy transition. Covers both co-host and battle popups.
watch(
() => currentLive.value?.liveId,
(newLiveId, oldLiveId) => {
if (oldLiveId && !newLiveId) {
pendingBattleId.value = '';
hideNotification();
}
}
);

onMounted(() => {
subscribeCoHostEvent(CoHostEvent.onCoHostRequestReceived, handleCoHostRequestReceived);
subscribeCoHostEvent(CoHostEvent.onCoHostRequestCancelled, handleCoHostRequestCancelled);
Expand All @@ -195,6 +234,9 @@ onUnmounted(() => {
unsubscribeBattleEvent(BattleEvent.onBattleRequestCancelled, onBattleRequestCancelled);
unsubscribeBattleEvent(BattleEvent.onBattleRequestReject, onBattleRequestRejected);
unsubscribeBattleEvent(BattleEvent.onBattleRequestTimeout, onBattleRequestTimeout);
// Safety net: the popup lives in document.body, so clear it on teardown to
// avoid a stale notification lingering after this component is destroyed.
hideNotification();
});
</script>

Expand Down
15 changes: 15 additions & 0 deletions Web/web-vite-vue3/src/TUILiveKit/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,18 @@ export function parseLiveErrorMessage(error: string) {
}
return '';
}

/**
* Battle (PK) invitation timeout in seconds, used by `LivePusherNotification`
* on the invitee side to start the accept/reject countdown. Kept in sync with
* `BATTLE_REQUEST_TIMEOUT_SECONDS` declared in:
* - ui-component/packages/uikit-component-vue3/.../CoHostPanel/constants.ts
* - ui-component/packages/uikit-component-vue3-electron/.../CoHostPanel/constants.ts
* - live/demos/electron-webpack-vue3/.../CoHostPanel/constants.ts
* The SDK does not propagate the inviter-side timeout via `BattleRequestReceivedEventInfo`,
* so the invitee falls back to this client-local constant. If the constant
* diverges across files the invitee countdown will desync from the inviter
* timeout, but no other behavior changes.
*/
export const BATTLE_REQUEST_TIMEOUT_SECONDS = 30;

2 changes: 2 additions & 0 deletions Web/web-vite-vue3/src/TUILiveKit/i18n/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const resource = {
Play: 'Play',
'Failed to end live': 'Failed to end live',
'You are currently live streaming. Do you want to end it?': 'You are currently live streaming. Do you want to end it?',
'You are currently live streaming and in a PK battle. Are you sure you want to exit?': 'You are currently live streaming and in a PK battle. Are you sure you want to end the live stream?',
'You are currently live streaming. Logging out will automatically end the live stream. Are you sure you want to log out?': 'You are currently live streaming. Logging out will automatically end the live stream. Are you sure you want to log out?',
'End live failed when log out': 'End live failed when log out',
'Received battle invitation from userName': 'Received battle invitation from {{userName}}',
Expand Down Expand Up @@ -267,4 +268,5 @@ export const resource = {
'The host has restored your camera permission. Please turn on the camera manually.': 'The host has restored your camera permission. Please turn on the camera manually.',
'The live room has been closed': 'The live room has been closed',
'Stream closed due to content violation': 'Stream closed due to content violation',
'The current display or content may pose a violation risk. Please be aware of the platform regulations': 'The current display or content may pose a violation risk. Please be aware of the platform regulations',
};
2 changes: 2 additions & 0 deletions Web/web-vite-vue3/src/TUILiveKit/i18n/zh-CN/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const resource = {
Play: '开始播放',
'Failed to end live': '结束直播失败',
'You are currently live streaming. Do you want to end it?': '您当前正在直播,确定要结束吗?',
'You are currently live streaming and in a PK battle. Are you sure you want to exit?': '您正在直播,且处于 PK 状态,确定要结束直播吗?',
'You are currently live streaming. Logging out will automatically end the live stream. Are you sure you want to log out?': '您当前正在直播中,退出登录将自动结束直播,确认退出吗?',
'End live failed when log out': '退出登录时结束直播失败',
'Received battle invitation from userName': '收到{{userName}}的PK邀请',
Expand Down Expand Up @@ -267,4 +268,5 @@ export const resource = {
'The host has restored your camera permission. Please turn on the camera manually.': '主播已恢复您的摄像头权限,请手动开启摄像头',
'The live room has been closed': '直播间已关闭',
'Stream closed due to content violation': '直播内容违规已被强制关播',
'The current display or content may pose a violation risk. Please be aware of the platform regulations': '当前画面或内容存在违规风险,请注意平台规范',
};